【Androidアプリ開発】MVVM アーキテクチャで Retrofit を使って API を叩く

自己紹介

みなさんこんにちは!ギリ新卒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'

参考: 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 公式より
これはAndroid Developersのアーキテクチャガイドに載っている図です。図の通りリポジトリは ViewModel と Model の間に入っており、両者を疎結合にします。データの取得や保存といったデータにアクセスするためのクラスをここで定義します。

参考:【Android】MVVMについて

一旦、説明はこんな感じで 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文などを用いてここで分岐させる必要があります。

参考: 非同期や並列処理にも役立つRxJavaの使い方

ViewModelの定義

ViewModel とは何なのかとよく議論の火種になりますが、一旦 View と Model の繋ぎの役割という認識で良いとします。Model (or Repository)のインスタンスを保持します。詳しく知りたい方は参考のサイトをご覧ください。

参考: 【Android】MVVMについて

以下のコードでは 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 を作成し、そこに値更新メソッドを用意します。 setValuepostValue の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 が作成できます。

参考: Kotlinのcompanion objectとは

HttpLoggingInterceptor

ビルド時に request と response のログを出力するために用います。それ以外にも URL や GET/POST、ステータスコードなども表示してくれて便利です。

参考: 【Kotlin】okhttp3でログを出力する

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 の完成です。

参考: retrofitでAPIを楽に使う

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 って何?と思われる方は下記のサイトをご覧ください。

デザインパターン「Factory Method」

パーミッションの設定

通信を行うために 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 を用います。

参考:lateinit による変数の初期化

ViewModeProvider

ViewModel を呼び出す際にこれを使わないと画面を回転させたときに画面が初期化されてしまうなど、 ViewModel として機能しません。 View の状態を ViewModel が保持するためには ViewModelProvider を使用します。

参考:ViewModel、ViewModelProviderについて調べてみた(Android)

findViewById

もちろんご存知だと思いますがここで XML に書いてある id=mainTextView を取得しています。詳しく知りたい方は調べてみてください。

Observer

LiveData オブジェクトを監視し、変更の通知を受け取ります。observe() が呼び出されてパラメータとして Observer{main.text = it.toString()} が渡されると、userRepos の値が保存されていれば変更が通知され、main.textit.toString() が入ります。

公式:LiveData の概要

いざ実行

実行するとこんな感じになります。Google のGitHub リポジトリをリストで受け取って文字列に変換して表示してるので大分画面は汚いです。なので、あるリポジトリの URL だけを表示するなんてことも可能です。

今回は簡略化するため色々省きましたが、Dagger を導入したり、綺麗に結果をリスト表示したり、POST を叩いたりなど書きたいことが沢山あるので今後もブログ更新していきたいと思います。

よくあるエラー

エミュレータの問題でエラーが発生することはしばしあります。下記のサイトが参考になります。 Wi-Fi の接続がきれていないか、マニュフェストにパーミッションの設定を忘れてないかなどをご確認ください。

Android でインターネットに接続するためのパーミッションを設定する

[ Android ] java.net.SocketException: Socket failed: EPERM(Operation not permitted)解決法

最後に

株式会社エムティーアイでは一緒にアプリ開発してくれるエンジニアを募集中です。気になった方は是非ご応募ください!

株式会社エムティーアイ 中途採用


  1. Jake Wharton とは Android に関わる多くのライブラリを手掛けている超有名なエンジニアです。Android 界隈ではよく Jake 神と呼ばれています。