Skip to content

Commit

Permalink
Retrofit2 + Moshi + RxJava(RxKotlin, RxAndroid)を利用したAPI通信処理 #8
Browse files Browse the repository at this point in the history
  • Loading branch information
over authored and over committed Jul 17, 2021
1 parent 0242c06 commit 8f13bc7
Show file tree
Hide file tree
Showing 22 changed files with 640 additions and 55 deletions.
14 changes: 13 additions & 1 deletion .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 0 additions & 10 deletions .idea/runConfigurations.xml

This file was deleted.

8 changes: 8 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ android {
kotlinOptions {
jvmTarget = '1.8'
}

viewBinding {
enabled = true
}
}

dependencies {
Expand All @@ -55,7 +59,11 @@ dependencies {

// retrofit2, moshi
implementation "com.squareup.retrofit2:retrofit:2.9.0"
implementation("com.squareup.retrofit2:retrofit-mock:2.9.0")
implementation "com.squareup.moshi:moshi-kotlin:1.12.0"
implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'
implementation 'com.squareup.retrofit2:adapter-rxjava3:2.9.0'
implementation("com.squareup.okhttp3:logging-interceptor:4.9.1")

testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
Expand Down
4 changes: 3 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="tokyo.oversoftware.bookexplorer">

<uses-permission android:name="android.permission.INTERNET" />

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.BookExplorer">
<activity android:name=".SearchActivity">
<activity android:name=".view.activities.SearchActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package tokyo.oversoftware.bookexplorer.di

import androidx.lifecycle.ViewModelProvider
import tokyo.oversoftware.bookexplorer.model.datastore.remote.ApiClient
import tokyo.oversoftware.bookexplorer.model.datastore.remote.BooksRemoteDataSource
import tokyo.oversoftware.bookexplorer.model.datastore.remote.GoogleBooksDataSource
import tokyo.oversoftware.bookexplorer.model.repository.BooksRepository
import tokyo.oversoftware.bookexplorer.model.repository.GoogleBooksRepository
import tokyo.oversoftware.bookexplorer.viewmodel.SearchViewModelFactory

/**
* アプリで使う実装クラスの依存関係をすべてここで解決する
*/
object DependencyInjection {

private val remoteDataSource: BooksRemoteDataSource = GoogleBooksDataSource(
apiService = ApiClient.build()
)

private val remoteRepository: BooksRepository = GoogleBooksRepository(
remoteDataSource = remoteDataSource
)

private val searchViewModelFactory: ViewModelProvider.Factory = SearchViewModelFactory(
repository = remoteRepository
)

/**
* 書籍取得用Repositoryの取得
*/
fun remoteRepository(): BooksRepository {
return remoteRepository
}

/**
* 検索用ViewModelの取得
*/
fun searchViewModelFactory(): ViewModelProvider.Factory {
return searchViewModelFactory
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package tokyo.oversoftware.bookexplorer.entity

/**
* Google Books APIで返却されるモデルを定義する
* ここで定義したデータを元にMoshiがRAW JSON <-> Class へ変換を行う
*/
data class Books(
val kind: String,
val totalItems: Int,
Expand Down Expand Up @@ -37,7 +41,6 @@ data class VolumeInfo(
val imageLinks: ImageLinks?,
val language: String?,
val previewLink: String?,
val infoLink: String?,
val canonicalVolumeLink: String?,
)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package tokyo.oversoftware.bookexplorer.entity

/**
* 画面表示用に余計な情報を削ぎ落とした書籍データ
*/
class DisplayableBook(private val book: Book) {

// 書籍のタイトル
val title: String = book.volumeInfo?.title ?: ""

// 書籍のサブタイトル
val subTitle: String = book.volumeInfo?.subtitle ?: ""

// 著者
val authors: List<String> = book.volumeInfo?.authors ?: emptyList()

// サムネイル
val thumbnailUrl: String? = book.volumeInfo?.imageLinks?.thumbnail

// リンク
fun link(): String {
// ISBNコードがあるか
val isbn10 = book.volumeInfo?.industryIdentifiers?.find { it.type == "ISBN_10" }?.identifier
if (isbn10 != null) {
return "https://www.amazon.co.jp/dp/isbn10"
}

return if (book.volumeInfo?.industryIdentifiers?.size == 0) {
// 出版社がない場合はタイトルで検索する
"https://www.google.com/search?q=" + book.volumeInfo.title
} else {
// ISBNコードがない場合は検索する
"https://www.google.com/search?q=" + book.volumeInfo?.industryIdentifiers?.get(0)?.identifier
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package tokyo.oversoftware.bookexplorer.model.datastore.remote

import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.core.Single
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.adapter.rxjava3.RxJava3CallAdapterFactory
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.http.GET
import retrofit2.http.Query
import tokyo.oversoftware.bookexplorer.BuildConfig
import tokyo.oversoftware.bookexplorer.entity.Books

/**
* Retrofitのインタフェースを定義する
*/
object ApiClient {

// APIの既定URL
private const val API_BASE_URL = "https://www.googleapis.com/books/v1/"

/**
* Retrofitのインタフェースを作成する
*/
fun build(): ApiService {
val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()

// Releaseビルド時は実行パフォーマンスの妨げになるのでログを出力しない
val httpClient: OkHttpClient = if (BuildConfig.DEBUG) {
OkHttpClient.Builder().addInterceptor(
interceptor()
).build()
} else {
OkHttpClient.Builder().build()
}

val builder = Retrofit.Builder()
.baseUrl(API_BASE_URL)
.addConverterFactory(MoshiConverterFactory.create(moshi))
.addCallAdapterFactory(RxJava3CallAdapterFactory.create())

val retrofit: Retrofit = builder.client(httpClient).build()

return retrofit.create(ApiService::class.java)
}

private fun interceptor(): HttpLoggingInterceptor {
val httpLoggingInterceptor = HttpLoggingInterceptor()
httpLoggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
return httpLoggingInterceptor
}

// API定義
interface ApiService {
@GET("volumes")
fun volumes(
@Query("q") name: String, @Query("startIndex") startIndex: Int = 0,
@Query("maxResults") limit: Int = 20
): Single<Books>
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package tokyo.oversoftware.bookexplorer.model.datastore.remote

import io.reactivex.rxjava3.core.Maybe
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.core.Single
import tokyo.oversoftware.bookexplorer.entity.Books

/**
* 書籍データをリモートから取得するデータソース
*/
interface BooksRemoteDataSource {
/**
* 書籍データを取得する
* @param searchKeyword 検索キーワード
*/
fun fetchBooks(searchKeyword: String): Single<Books>
}

/**
* Google Books Apiを利用した書籍データソース
*/
class GoogleBooksDataSource(// Retrofitのインタフェース
private val apiService: ApiClient.ApiService
) : BooksRemoteDataSource {

override fun fetchBooks(searchKeyword: String): Single<Books> {
return apiService.volumes(name = searchKeyword)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package tokyo.oversoftware.bookexplorer.model.repository

import android.util.Log
import io.reactivex.rxjava3.core.Single
import tokyo.oversoftware.bookexplorer.entity.Books
import tokyo.oversoftware.bookexplorer.model.datastore.remote.BooksRemoteDataSource

/**
* 書籍情報を取得するRepository
* 依存元からデータをどこから取得しているかをカプセル化するために利用する
*/
interface BooksRepository {
/**
* 書籍情報を取得する
* @param searchKeyword: 検索キーワード
*/
fun findBooks(searchKeyword: String): Single<Books>
}

/**
* Googleから書籍情報を取得するデータソース
* MEMO: 現在は`RemoteDataSource`しかコンストラクタで注入していないが、
* 規模の大きなアプリであればCacheDataSourceなども注入する
*/
class GoogleBooksRepository(private val remoteDataSource: BooksRemoteDataSource) : BooksRepository {

// キャッシュデータ
// 検索ワードと書籍データのペア。画面回転時のキャッシュに利用する
var cache: Pair<String, Books>? = null

/**
* 書籍情報を取得する
* @param searchKeyword: 検索キーワード
*/
override fun findBooks(searchKeyword: String): Single<Books> {
// キャッシュがあればそれを返す
if (searchKeyword == cache?.first) {
return Single.just(cache?.second)
}

return remoteDataSource.fetchBooks(searchKeyword = searchKeyword)
.doOnSuccess {
// 初回か別のキーワードであればメモリに入れておく
val differentSearchWord: Boolean = searchKeyword == cache?.first
if (cache == null || differentSearchWord) {
cache = Pair(searchKeyword, it)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package tokyo.oversoftware.bookexplorer.view.activities

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import tokyo.oversoftware.bookexplorer.databinding.ActivitySearchBinding
import tokyo.oversoftware.bookexplorer.di.DependencyInjection
import tokyo.oversoftware.bookexplorer.entity.DisplayableBook
import tokyo.oversoftware.bookexplorer.viewmodel.SearchViewModel

class SearchActivity : AppCompatActivity() {

private lateinit var binding: ActivitySearchBinding
private lateinit var viewModel: SearchViewModel

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySearchBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
setupViewModel()

viewModel.onSearch("ドラゴンボール")
}

private fun setupViewModel() {
viewModel = ViewModelProvider(
this,
DependencyInjection.searchViewModelFactory()
).get(SearchViewModel::class.java)

viewModel.items.observe(this, renderResponse)
}

private val renderResponse = Observer<List<DisplayableBook>> {
if (it.isNotEmpty()) {
binding.apiResult.text = it[0].title
}
}
}
Loading

0 comments on commit 8f13bc7

Please sign in to comment.