多くのアプリが実装しているであろう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
使用するライブラリ
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"
マニフェストも忘れずに
<uses-permission android:name="android.permission.INTERNET"/>
というわけで早速
なんとなく全体像がわかりやすくなりそうなのでViewModelから始めます。
ViewModel
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(データクラス)
@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
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
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
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). */
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