自己紹介
みなさんこんにちは!ギリ新卒1年目の樋口です。 現在はソリューション事業部で受託開発を行うチームに所属しております。 昨年の12月から Android アプリ開発の勉強を始め、先輩方の支援を受けながらアプリを作っております。
背景
今、Android エンジニアは業界全体を通して慢性的な人手不足の状態です。教えてくれる人も少ない中、現場で使える技術を初心者向けに解説した記事が少ないと感じ、今回このような記事を書くに至りました。この記事を通して自分のように Retrofit や MVVM でつまずいている人の助けになれば幸いです。
この記事の到達目標
- MVVM アーキテクチャについて理解すること
- Retrofit を使って GET ができるようになること
プロジェクトの作成
今回は MVVM アーキテクチャで Retrofit を用いて GitHub API で GET を叩けるようにしようと思います。 まず最初に新規プロジェクトで Empty Activity を選択。
好きな名前で Project を保存します。 Minimum SDK は23をオススメします。 日本は2年ごとに機種変更することが多いので、Minimum SDK 23 で基本的に9割以上のユーザをカバーすることができます。不安な方はスマタブinfoをご覧ください。
また、この後 Google の GitHub アカウントのリポジトリ一覧を取得する API を叩こうと思います。
Gradleの準備
次に Gradle の準備を行なっていきます。Gradle とは Java 環境におけるビルドシステムのことで、 パッケージの導入やバージョン管理の際に用いられます。今回はアプリケーション内の build.gradle
を下記のように変更してください。
apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' // kapt apply plugin: 'kotlin-kapt' android { compileSdkVersion 29 buildToolsVersion "29.0.2" defaultConfig { applicationId "com.example.github_mvvm_retrofit2" minSdkVersion 23 targetSdkVersion 29 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } } dependencies { // moshi def moshi_version = "1.5.0" implementation "com.squareup.moshi:moshi:$moshi_version" implementation "com.squareup.moshi:moshi-kotlin:$moshi_version" implementation "com.squareup.retrofit2:converter-moshi:2.4.0" // RxJava implementation "io.reactivex.rxjava2:rxandroid:2.1.0" implementation "io.reactivex.rxjava2:rxkotlin:2.1.0" implementation "io.reactivex.rxjava2:rxjava:2.2.5" // Retrofit 2 def retrofit2_version = "2.5.0" implementation "com.squareup.retrofit2:retrofit:$retrofit2_version" implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofit2_version" implementation "com.squareup.retrofit2:converter-gson:$retrofit2_version" implementation "com.squareup.okhttp3:logging-interceptor:3.9.0" // Architecture component implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0' kapt 'androidx.lifecycle:lifecycle-compiler:2.2.0' implementation 'androidx.activity:activity-ktx:1.1.0' implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.core:core-ktx:1.2.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' }
順を追って説明します。
Kapt
kapt
と呼ばれるプラグインの追加をします。
kotlin-annotation-processing tools の略でアノテーション(先頭が@で始まるもの)を使ってコードを自動生成できるようになります。
apply plugin: 'kotlin-kapt'
Moshi
def
というキーワードの後に変数名を書くことで、変数を宣言しており、バージョン管理が少し楽になります。
ここでは Moshi という JSON ライブラリを追加しています。JSON 形式のデータを読み込んで、Java Object に変換してくれます。これがないとアプリ側で API で叩いてきた結果を扱うことが困難になります。似たようなライブラリに Gson がありますが、Jake Wharton 1が Gson is deprecated (非推奨)と発言しています。
参考: 「Gson is deprecated.」らしいのでMoshiを試してみる
// moshi def moshi_version = "1.5.0" implementation "com.squareup.moshi:moshi:$moshi_version" implementation "com.squareup.moshi:moshi-kotlin:$moshi_version" implementation "com.squareup.retrofit2:converter-moshi:2.4.0"
RxJava
RxJava とはリアクティブプログラミングをするためのライブラリです。リアクティブプログラミングとはデータが流れるように来ること(ストリーム)に着目し、その度関連あるプログラムが反応(リアクティブ)することです。
// RxJava implementation "io.reactivex.rxjava2:rxandroid:2.1.0" implementation "io.reactivex.rxjava2:rxkotlin:2.1.0" implementation "io.reactivex.rxjava2:rxjava:2.2.5"
Retrofit
Retrofit とは Square が開発している API ライブラリです。サーバ側のAPIをインタフェースとして定義するだけで API 呼び出しの実装を行い、API 定義を分離しコードの見通しを簡潔に保つことが特徴です。
参考: Retrofit
// Retrofit def retrofit2_version = "2.5.0" implementation "com.squareup.retrofit2:retrofit:$retrofit2_version" implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofit2_version" implementation "com.squareup.retrofit2:converter-gson:$retrofit2_version" implementation "com.squareup.okhttp3:logging-interceptor:3.9.0"
AndroidX
ここでは AndroidX と呼ばれるサポートライブラリを定義しています。バージョン 28 まで対応していたサポートライブラリを大幅に改良したものとなっています。
参考: AndroidXの概要
// Architecture component implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0' kapt 'androidx.lifecycle:lifecycle-compiler:2.2.0' implementation 'androidx.activity:activity-ktx:1.1.0'
残りのものは元から入っていました。
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.core:core-ktx:1.2.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
APIClient の定義
ClientApi
という名前で Interface ファイルを作成しましょう。
まず、下記のように Interface を定義します。
interface ClientApi { @GET("users/{user}/repos") fun getGithub(@Path("user") user: String): Single<Response<List<UserRepos>>> }
ここでは主に API の形式とパス、レスポンス形式の定義をしています。
API の形式
@GET
では当然のことながら GET
を叩くということを定義しています。
ここを @POST
とかに変えるともちろん POST を叩けます。その際、リクエストボディは @Boby
で定義できます。
参考: 【Android】【Retrofit】Retrofit 2.0.1使い方メモとハマりどころメモ
パスの定義
ここではURL以下のパスを定義できます。@Path
を用いることでメソッド呼び出し時に {user}
の部分を設定できるようにしています。
レスポンスの形式
Single
では値が一つしか流れてこないストリームを表しています。
参考: rx.Singleについて
API を叩いたときリポジトリの情報がリスト形式で返されるのでListをつける必要があります。最後に UserRepos が赤字で表示されるでしょう。これはレスポンスの形式を定義する必要があるのでこちらの実装を次に行います。
レスポンス形式の定義
先ほど APIClientを定義しましたが、レスポンスの形式を以下の様に定義する必要があります。
data class UserRepos( val id: String = "", val name: String = "", val html_url: String = "" )
データクラスといいデータを保持するだけのクラスを作成します。ここで扱う変数を定義しないと、 JSON レスポンスに含まれていてもオブジェクトを生成しても変数を扱うことができなくなります。
参考: Kotlinのdata class(データクラス)の使い方【初心者向け】
ちなみにPOSTをする際は同様にリクエストの形式を定義します。
リポジトリ
リポジトリの作成に入る前にいったいこれは何者なんだっていう話から入ろうと思います。 これはAndroid Developersのアーキテクチャガイドに載っている図です。図の通りリポジトリは ViewModel と Model の間に入っており、両者を疎結合にします。データの取得や保存といったデータにアクセスするためのクラスをここで定義します。
一旦、説明はこんな感じで Repository の実装に入っちゃいましょう! ClientApiRepository というクラスファイルを作成してください!
class ClientApiRepository(val clientApi: ClientApi) { fun getGithubRepos(user: String): Single<List<UserRepos>> { return clientApi.getGithub(user) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .map { val body = it.body() ?: throw IOException("failed to fetch") return@map body } } }
先ほど作った ClientApi
インターフェースを元に API を叩くためのメソッドを定義しています。ここでは以下3点に関して詳しく説明していきます。
subscribeOn
RxJava で、処理の実行スケジューラを切り替えるオペレータです。イベントを発生させる処理の実行スケジューラを指定していて、この場合は Schedulers.io()
というのを指定しています。 Schedulers.io()
は、新しい worker が要求されるたびに、新しいスレッドが開始され(その後しばらくの間アイドル状態に保たれる)、アイドル状態のスレッドが再利用される模様です。
参考: RxJavaのスケジューラ
observeOn
observeOn
以降のオペレータの実行スレッドの切り替えをしています。非同期の Observable
の結果 をUI で使用したいときは AndroidSchedulers.mainThread()
を用います。今回は GET の結果をリストで表示したいのでもちろん使います。
参考:詳解RxJava:Scheduler、非同期処理、subscribe/unsubscribe
map
ストリームに流れてくるアイテムを変換してくれます。このコードではレスポンスに body
が入っていた場合 body
を返し、空の場合は "failed to fetch"
と例外の発生を通知するようにしています。余談ですが、ステータスコードに応じて結果オブジェクトを生成する場合if文などを用いてここで分岐させる必要があります。
ViewModelの定義
ViewModel とは何なのかとよく議論の火種になりますが、一旦 View と Model の繋ぎの役割という認識で良いとします。Model (or Repository)のインスタンスを保持します。詳しく知りたい方は参考のサイトをご覧ください。
以下のコードでは Repository をインスタンス化させて View で表示するための処理をしています。
class MainActivityViewModel(val clientApiRepository: ClientApiRepository) : ViewModel() { private val _userRepos: MutableLiveData<List<UserRepos>> = MutableLiveData() val userRepos: LiveData<List<UserRepos>> = _userRepos fun getGitHub(user: String) { clientApiRepository.getGithubRepos(user) .subscribe { userRepos: List<UserRepos> -> _userRepos.postValue(userRepos) } } }
LiveData
LiveData はライフサイクルに応じた監視が可能です。これにより UI に表示する内容をデータと一致させることができます。つまり、常に最新のデータを表示できます。しかし、 LiveData の値を変更することはできないので MutableLiveData
で _userRepos
を作成し、そこに値更新メソッドを用意します。 setValue
と postValue
の2つの方法がありますが、ここでは postValue
を用いて値を更新しています。
参考: LiveData について勘違いしていたことをいくつか
Subscribe
簡単に言うと、データを流し込むための合図となるメソッドです。
RestUtilの作成
API を叩けるようにするためにRestApiを以下のように作成します。ここでURLやログ、 Converter と CallAdapter の設定をします。
object RestUtil { val ENDPOINT = "https://api.github.com/" val retrofit: Retrofit init { val interceptor = HttpLoggingInterceptor() interceptor.level = HttpLoggingInterceptor.Level.BODY val httpClient = OkHttpClient.Builder().addInterceptor(interceptor).build() val builder = Retrofit.Builder() .baseUrl(ENDPOINT) .addConverterFactory(MoshiConverterFactory.create()) .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .client(httpClient) retrofit = builder.build() } }
Object
Kotlin では class の代わりに object
キーワードで宣言すると Singleton が作成できます。
HttpLoggingInterceptor
ビルド時に request と response のログを出力するために用います。それ以外にも URL や GET/POST、ステータスコードなども表示してくれて便利です。
OkHttp
HTTP, HTTP2 通信を効率的に行うための Java と Android 用のクライアントです。今回のソースでは builer
メソッドを用いて Interceotor
を追加し、ログを出力できるようにしてからインスタンス化しています。
参考: Builderパターン(Effective Java)
Retrofit
こちらも Builder
メソッドを用いてインスタンス化しており、まず先に URL, Converter, CallAdapter の3つを定義しています。
URL: 今回叩くAPIのベースとなるURL
Converter: 変換元クラスのインスタンスを変換先クラスのインスタンスに変換するものです。ここでは
MoshiConverterFactory
を定義しています。Adapter: RetrofitでRxJavaを使った値を返すようにするために必要なものです。Retrofit 単体では Single が扱えないので AdapterFactory に
RxJava2CallAdapterFactory
を追加します。
client に先ほど OkHttpClient をインスタンス化させたものを追加して Retrofit をインスタンス化させます。あとはファクトリでインスタンス化させると clientApi
の完成です。
RepositoryFactory
ここではどのように repository をインスタンス化するか定めています。 clientApiをインスタンス化し repository を作成します。
object RepositoryFactory { fun createClientApiRepository(): ClientApiRepository { val clientApi = RestUtil.retrofit.create(ClientApi::class.java) return ClientApiRepository(clientApi) } }
RestUtil.retrofit.create(ClientApi::class.java)
で ClientApi
のオブジェクトを作成し 、リポジトリに渡してあげます。
MainViewModelFactory
ここではどのように ViewModel をインスタンス化するかを定めています。
class MainViewModelFactory(private val clientApiRepository: ClientApiRepository) : ViewModelProvider.NewInstanceFactory() { override fun <T : ViewModel?> create(modelClass: Class<T>): T { return MainActivityViewModel(clientApiRepository) as T } }
ちなみに、Factory って何?と思われる方は下記のサイトをご覧ください。
パーミッションの設定
通信を行うために AndroidManifest に以下の設定を追加する必要があります。
<uses-permission android:name="android.permission.INTERNET" />
manifest タグの中の application タグの前に追記してください。
参考:Android でインターネットに接続するためのパーミッションを設定する
XMLファイルの設定
適当に id を振りましょう。あとで Activity から呼び出すときに使います。本来であればリスト表示で綺麗に出力すべきですが、今回は MVVM で Retrofit を扱うのが目的なのでテキストで表示するだけにします。
Viewからの呼び出し
ここでは ViewModel をインスタンス化して、 View から操作できる様にします。
しかし、インスタンス化の方法で2020年3月時点の公式に書いてある ViewModelProviders
だと非推奨と表示されてしまいます。かなり名前が似てますがViewModelProviderを使います.
class MainActivity : AppCompatActivity() { private lateinit var mainActivityViewModel: MainActivityViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) mainActivityViewModel = ViewModelProvider( this, MainViewModelFactory(RepositoryFactory.createClientApiRepository()) ).get(MainActivityViewModel::class.java) mainActivityViewModel.getGitHub("google") val main = findViewById<TextView>(R.id.main) mainActivityViewModel.userRepos.observe(this, Observer { main.text = it.toString() }) } }
lateinit
lateinit
を用いることで変数の初期化を行うことができます。Kotlin では、変数をプロパティ宣言時に初期化をする必要がありますが、 Activity では onCreate
以降で行う必要があるので onCreate
より前で宣言する場合lateinit
を用います。
ViewModeProvider
ViewModel を呼び出す際にこれを使わないと画面を回転させたときに画面が初期化されてしまうなど、 ViewModel として機能しません。 View の状態を ViewModel が保持するためには ViewModelProvider
を使用します。
参考:ViewModel、ViewModelProviderについて調べてみた(Android)
findViewById
もちろんご存知だと思いますがここで XML に書いてある id=main
の TextView
を取得しています。詳しく知りたい方は調べてみてください。
Observer
LiveData オブジェクトを監視し、変更の通知を受け取ります。observe()
が呼び出されてパラメータとして Observer{main.text = it.toString()}
が渡されると、userRepos
の値が保存されていれば変更が通知され、main.text
に it.toString()
が入ります。
いざ実行
実行するとこんな感じになります。Google のGitHub リポジトリをリストで受け取って文字列に変換して表示してるので大分画面は汚いです。なので、あるリポジトリの URL だけを表示するなんてことも可能です。
今回は簡略化するため色々省きましたが、Dagger を導入したり、綺麗に結果をリスト表示したり、POST を叩いたりなど書きたいことが沢山あるので今後もブログ更新していきたいと思います。
よくあるエラー
エミュレータの問題でエラーが発生することはしばしあります。下記のサイトが参考になります。 Wi-Fi の接続がきれていないか、マニュフェストにパーミッションの設定を忘れてないかなどをご確認ください。
Android でインターネットに接続するためのパーミッションを設定する
[ Android ] java.net.SocketException: Socket failed: EPERM(Operation not permitted)解決法
最後に
株式会社エムティーアイでは一緒にアプリ開発してくれるエンジニアを募集中です。気になった方は是非ご応募ください!
- Jake Wharton とは Android に関わる多くのライブラリを手掛けている超有名なエンジニアです。Android 界隈ではよく Jake 神と呼ばれています。↩