11
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Android 初心者向けAdvent Calendar 2019

Day 24

Androidでいい感じにREST APIを扱う(Kotlin Coroutine, OkHttp3, Retrofit2, Moshi)

Last updated at Posted at 2019-12-24

多くのアプリが実装しているであろうAPI通信ですが、Androidではライブラリやライフサイクルの知識を持っていないと戸惑うところが多いと思います。ということで自分なりにいい感じだなと思う実装をまとめてみました。

やること

QiitaApi:userを呼び出し
https://qiita.com/api/v2/users
レスポンスを表示する。(ログです・・・)
アーキテクチャというほどではないですがMVVM + Repositoryを意識しています。
MVVMについてはこちらが非常にわかりやすいです。
https://developer.android.com/jetpack/docs/guide?hl=ja#drive-ui-from-model

使用するライブラリ
build.gradle
    def retrofitVersion = '2.6.0'
    implementation "com.squareup.retrofit2:retrofit:$retrofitVersion"
    implementation "com.squareup.retrofit2:converter-moshi:$retrofitVersion"
    
    def okhttp_version = '4.2.2'
    implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
    implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"

    def moshi_version = '1.8.0'
    implementation "com.squareup.moshi:moshi:$moshi_version"
    implementation "com.squareup.moshi:moshi-kotlin:$moshi_version"

    def coroutines = "1.3.1"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines"

    def lifecycle = "2.2.0-alpha04"
    implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle"
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle"
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle"
    implementation "androidx.lifecycle:lifecycle-runtime:$lifecycle"
    implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle"

マニフェストも忘れずに

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

というわけで早速
なんとなく全体像がわかりやすくなりそうなのでViewModelから始めます。

ViewModel

MainViewModel.kt
class MainViewModel : ViewModel() {

    private val repository = UserRepository(UserApiService.userApi)

    private val _userList = MutableLiveData<MutableList<User>>()
    val userList: LiveData<MutableList<User>>
        get() = _userList

    init {
        fetchUser()
    }

    private fun fetchUser() {
        viewModelScope.launch {
            withContext(Dispatchers.IO) {
                val user = repository.fetchUserList(1, 2)
                user?.let {
                    _userList.postValue(user)
                }
            }
        }
    }
}

シンプルにコルーチンを用いてrepositoryを操作し、データを取得しています。

ポイントとしてViewModelScopeでCoroutineをlaunchしています。
ViewModelScopeを用いればCoroutineスコープを個別に作成する必要がなく、ViewModelが破棄された時のキャンセルも自動的に行ってくれます。
https://developers-jp.googleblog.com/2019/04/android-viewmodelscope.html

User(データクラス)

User.kt
@JsonClass(generateAdapter = true)
data class User(
    val description: String?,
    val facebook_id: String?,
    val followees_count: Int?,
    val followers_count: Int?,
    val github_login_name: String?,
    val id: String?,
    val items_count: Int?,
    val linkedin_id: String?,
    val location: String?,
    val name: String?,
    val organization: String?,
    val permanent_id: Int?,
    val profile_image_url: String?,
    val team_only: Boolean?,
    val twitter_screen_name: String?,
    val website_url: String?
)

https://github.com/wuseal/JsonToKotlinClass
でなにも考えずdata classを作成。命名はとりあえず気にしない。
@JsonClass(generateAdapter = true)でmoshiのアダプターを作成しています。

Service

UserApiService.kt
object UserApiService : BaseService() {
    val userApi: UserApiInterface = getRetrofit().create(UserApiInterface::class.java)
}

open class BaseService {
    companion object {
        private const val baseUrl = "https://qiita.com"
    }

    private val client: OkHttpClient
        get() {
            return OkHttpClient.Builder()
                .addInterceptor(HttpLoggingInterceptor().apply {
                    level = HttpLoggingInterceptor.Level.BODY
                })
                .build()
        }

    fun getRetrofit(): Retrofit {
        return Retrofit.Builder()
            .client(client)
            .baseUrl(Companion.baseUrl)
            .addConverterFactory(
                MoshiConverterFactory.create(
                    Moshi.Builder().add(
                        KotlinJsonAdapterFactory()
                    ).build()
                )
            )
            .build()
    }
}

こちらでhttpクライアントを作成します。
お作法的な面もありますが、やることは

Retrofit.Builder()
.build()
.create(Class<T> service) 

でRetrofitのinterfaceを作成することです。
また今回はMoshiをJsonパース用のアダプターとして追加しています。

ちなみに・・・
★OkHttp3とRetrofitの違い
自分もノリで使っていたので。どちらもネットワーク通信用のライブラリですが、Retrofitはhttp通信用のラッパーライブラリであり、こちらではお手軽にURLのマッピングやJsonのパース処理を行うことができます。RetrofitのクライアントとしてOkHttpが用いられます。

Interface

UserApiInterface.kt
interface UserApiInterface {
    @GET("/api/v2/users")
    suspend fun fetchUser(
        @Query("page") page: Int,
        @Query("per_page") perPage: Int
    ): Response<MutableList<User>>?
}

アノテーションでHttpメソッド、エンドポイントとクエリを表現しています。ポイントは2点あり、
一つ目はレスポンスをResponseでクラスでラップしている所で、これにより後述のRepository内での判定処理がわかりやすくなっています。
二つ目はretofit2.6.0からサポートされたsuspend修飾子をfetchUser()につけている所で,コルーチン内で関数を呼びだせるようにしています。
https://github.com/square/retrofit/blob/master/CHANGELOG.md#version-260-2019-06-05

Repository

UserRepository.kt
sealed class NetworkResult<out T : Any> {
    data class Success<out T : Any>(val output: T) : NetworkResult<T>()
    data class Error(val exception: Exception) : NetworkResult<Nothing>()
}

open class BaseRepository {
    suspend fun <T : Any> apiOutput(
        call: suspend () -> Response<T>,
        error: String
    ): NetworkResult<T> {
        val response = call.invoke()
        return if (response.isSuccessful)
            NetworkResult.Success(response.body()!!)
        else
            NetworkResult.Error(IOException(error))
    }
}

class UserRepository(private val apiInterface: UserApiInterface) : BaseRepository() {
    suspend fun fetchUserList(page: Int, perPage: Int): MutableList<User>? {
        val result = apiOutput(
            call = { apiInterface.fetchUser(page, perPage) },
            error = "calling fetchUserList failed"
        )
        var output: MutableList<User>? = null

        when (result) {
            is NetworkResult.Success ->
                output = result.output
            is NetworkResult.Error ->
                Log.d("Error", "${result.exception}")
        }
        return output
    }
}

sealed classでhttpのレスポンスをハンドリングしています。
共通な処理はBaseRepositoryとしてまとめてみました。
RetrofitのResponseクラスのisSuccessful()関数でレスポンスが200かどうか判定できるようです。
/** Returns true if {@link #code()} is in the range [200..300). */

MainActivity.kt
class MainActivity : AppCompatActivity() {
    private lateinit var viewModel: MainViewModel
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
        viewModel.userList.observe(this, Observer { response ->
            Log.d("fetched user:", response.toString())
        })
    }
}

実行してみます。
###結果

D/fetched user:: [User(description=null, facebook_id=null, followees_count=0, followers_count=0, github_login_name=null, id=yoheight, items_count=0, linkedin_id=null, location=null, name=, organization=null, permanent_id=556511, profile_image_url=https://lh3.googleusercontent.com/a-/AAuE7mBQ9RlmusxDLQlnvJ5O8dgaxilKbzu1E3JAStwRxA=s50, team_only=false, twitter_screen_name=null, website_url=null), User(description=null, facebook_id=null, followees_count=1, followers_count=0, github_login_name=null, id=modokki, items_count=0, linkedin_id=null, location=null, name=, organization=null, permanent_id=556510, profile_image_url=https://lh6.googleusercontent.com/-pdZwmGAMpBY/AAAAAAAAAAI/AAAAAAAAAAA/ACHi3rdmjdOshANtnA-lE3tp2_K_fea3Kw/s50/photo.jpg, team_only=false, twitter_screen_name=null, website_url=null), User(description=null, facebook_id=null, followees_count=0, followers_count=0, github_login_name=Lpt-cycro, id=Lpt-cycro, items_count=0, linkedin_id=null, location=null, name=, organization=null, permanent_id=556509, profile_image_url=https://avatars2.githubusercontent.com/u/59194000?v=4, team_only=false, twitter_screen_name=null, website_url=null)]

##まとめ
関心を分離しつつ簡潔にAPI処理を実装できたと思います。
間違っている点がありましたらコメントいただけると幸いです!

###参考
https://qiita.com/takahirom/items/3f012d46e15a1666fa33
https://medium.com/hacktive-devs/making-network-calls-on-android-with-retrofit-kotlins-coroutines-72fd2594184b

11
15
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?