Androidアプリ開発 ネットワーク接続 RetrofitとRxjava2を使ってネットワーク処理を実装する

2018年01月08日(編集2018年01月08日)
このエントリーをはてなブックマークに追加

Androidアプリ開発では、様々なネットワーク処理が利用できます。その中でもよく利用されている方法の1つにRetrofitとRxjava2を組み合わせる方法があります。
この記事は、RetrofitとRxjava2を使ってネットワークAPIに接続してデータを取得する方法を記載した記事です。

環境はAndroid 8.1 (API level 27) です。

環境

  • macOS Sierra
  • android sdk 27
  • Oracle jdk version 1.8.0_72
  • Android Studio 3.0.1
  • kotlin 1.2.0

難易度

中級者向け

サンプルコード

Android-Library-Demo

RetrofitとRxjava2

数あるネットワーク接続実装の中で、RetrofitとRxjavaの組み合わせが、現在(2017年01月07日)もっとも一般的なパターンだと思います。
この記事では、RetrofitとRxjava2を連携させてネットワークAPIに接続します。

Retrofit

この記事は、Retrofitの使い方は理解している前提となります。理解していない場合は、この記事で学習してください。

Rxjava2

この記事は、Rxjava2の使い方は理解している前提となります。理解していない場合は、この記事で学習してください。

アプリ設計

EditTextに郵便番号を入力してボタンを押下すると、ネットワークAPIに非同期で接続して住所を取得します。エラーが発生した場合は、ダイアログを表示します。
また、サンプルコードのAPIには、郵便番号データ配信サービスのzipcloudを利用します。

実装

APIのインターフェースクラスを作成します。

{project_folder}/app/src/main/ApiService.java
interface ApiService {
    @GET("search")
    fun searchZip(@Query("zipcode") zipcode: String): Flowable<Search>
}
          

APIに接続する処理を実装します。

{project_folder}/app/src/main/RetrofitRxjava2Activity.java
class RetrofitRxjava2Activity : AppCompatActivity(), ErrorDialogFragment.OnFragmentInteractionListener {
    
    private val retrofit = Retrofit.Builder()
            .baseUrl("http://zipcloud.ibsnet.co.jp/api/")
            .addConverterFactory(GsonConverterFactory.create())
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            .build()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_retrofit_rxjava2)
        setSupportActionBar(toolbar)

        btnNormalConnect.setOnClickListener(connectListener)
        btnRetryConnect.setOnClickListener(connectRetryListener)
    }

    private var connectListener = View.OnClickListener {
        connectNormal()
    }

    // 通常接続
    private fun connectNormal() {
        val service = retrofit.create(ApiService::class.java)
        service.searchZip(zipcode = zipCode.text.toString())
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(object : Subscriber<Search> {
                    override fun onSubscribe(subscription: Subscription?) {
                        subscription?.request(1000)
                    }

                    override fun onNext(search: Search?) {
                        search?.results?.forEach({
                            answer.text = with(StringBuilder()) {
                                append(it.address1)
                                append(it.address2)
                                append(it.address3)
                                append(it.kana1)
                                append(it.kana2)
                                append(it.kana3)
                                append(it.prefcode)
                                append(it.zipcode)
                            }
                        })
                    }

                    override fun onComplete() {
                    }

                    override fun onError(throwable: Throwable?) {
                        throwable?.let {
                            val errorDialogFragment = ErrorDialogFragment.newInstance(it.message ?: "error")
                            errorDialogFragment.show(supportFragmentManager, "ErrorDialogFragment")
                        }
                    }
                })
    }
}
          

■ 実装の解説

1. Flowable

APIの結果をFlowableクラスで取得できるようにします。

@GET("search")
fun searchZip(@Query("zipcode") zipcode: String): Flowable<Search>
        

2. adapterの追加

RetrofitでRxjava2を利用する場合は、adapterを追加します。

    private val retrofit = Retrofit.Builder()
            .baseUrl("http://zipcloud.ibsnet.co.jp/api/")
            .addConverterFactory(GsonConverterFactory.create())
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            .build()
        

RxJava2CallAdapterFactoryは、CallAdapter.Factoryを継承したクラスです。Retrofitの処理結果を、RxJava2が利用できる形にします。

3. subscribeOn

Androidではメインスレッドでネットワーク処理をするとandroid.os.NetworkOnMainThreadExceptionの例外がスローされます。android.os.NetworkOnMainThreadExceptionは、アプリケーションがメインスレッドでネットワーク操作を実行しようとしたときにスローされる例外です。

これを防ぐために、スレッドを指定します。

.subscribeOn(Schedulers.io())
        

subscribeOnメソッドは、データを作る(生産者)処理をどのスレッドで実行するかを設定するメソッドです。ここでいう生産者は、Flowable<Search>になります。
Rxjavaでは、Schedulerを使って非同期処理を行うことができます。

Schedulers.io()は、必要に応じて新規のスレッドを作成します。

4. observeOn

observeOnメソッドは、データを受け取った側(消費者)の処理を、どのようなSchedule上で行うのかを設定するメソッドです。

observeOn(AndroidSchedulers.mainThread())
        

observeOnメソッドが指定したSchedulerが設定したスレッド上で、それ以降の処理を行うようになります。
この処理は、データを受け取る側(消費者)がUIのTextViewなので、AndroidSchedulers.mainThread()を使います。AndroidSchedulers.mainThread()は、すべてのUIアクションをAndroidのメインスレッドで実行することができます。このメソッドを呼びださないと、Only the original thread that created a view hierarchy can touch its viewsが発生します。

5. subscribe

subscribe処理は、非ネットワーク時のRxjavaと同じなので説明は省きます。例外処理は、ネットワークエラーの状態を、ユーザーに通知するような処理を実装すると良いでしょう。ここではdialogを使っていますが、Toastで十分だと思います。

override fun onError(throwable: Throwable?) {
    throwable?.let {
        val errorDialogFragment = ErrorDialogFragment.newInstance(it.message ?: "error")
        errorDialogFragment.show(supportFragmentManager, "ErrorDialogFragment")
    }
}
        

リトライ実装

ネットワーク処理を実装すると、ネットワーク接続に失敗した時のことを考慮して、リトライさせたいことがあります。

Rxjavaではリトライ処理も用意されています。やり方は色々あるのですが、このサンプルでは一番汎用性の高いFlowable.retryWhenメソッドを利用します。

{project_folder}/app/src/main/RetrofitRxjava2Activity.java
class RetrofitRxjava2Activity : AppCompatActivity(), ErrorDialogFragment.OnFragmentInteractionListener {
    
    private val retrofit = Retrofit.Builder()
            .baseUrl("http://zipcloud.ibsnet.co.jp/api/")
            .addConverterFactory(GsonConverterFactory.create())
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            .build()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_retrofit_rxjava2)
        setSupportActionBar(toolbar)

        btnNormalConnect.setOnClickListener(connectListener)
        btnRetryConnect.setOnClickListener(connectRetryListener)
    }

    private var connectListener = View.OnClickListener {
        connectNormal()
    }

    // 通常接続
    private fun connectNormal() {
        val service = retrofit.create(ApiService::class.java)
        service.searchZip(zipcode = zipCode.text.toString())
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(object : Subscriber<Search> {
                    override fun onSubscribe(subscription: Subscription?) {
                        subscription?.request(1000)
                    }

                    override fun onNext(search: Search?) {
                        search?.results?.forEach({
                            answer.text = with(StringBuilder()) {
                                append(it.address1)
                                append(it.address2)
                                append(it.address3)
                                append(it.kana1)
                                append(it.kana2)
                                append(it.kana3)
                                append(it.prefcode)
                                append(it.zipcode)
                            }
                        })
                    }

                    override fun onComplete() {
                    }

                    override fun onError(throwable: Throwable?) {
                        throwable?.let {
                            val errorDialogFragment = ErrorDialogFragment.newInstance(it.message ?: "error")
                            errorDialogFragment.show(supportFragmentManager, "ErrorDialogFragment")
                        }
                    }
                })
    }

    private var connectRetryListener = View.OnClickListener {
        val service = retrofit.create(ApiService::class.java)
        var retroyCount = 0
        service.searchZip(zipcode = zipCode.text.toString())
                .retryWhen(object : io.reactivex.functions.Function<Flowable<Throwable>, Flowable<Long>> {
                    override fun apply(t: Flowable<Throwable>): Flowable<Long> {
                        return t.flatMap(object : io.reactivex.functions.Function<Throwable, Flowable<Long>> {
                            override fun apply(throwable: Throwable): Flowable<Long> {
                                retroyCount = retroyCount + 1
                                if (retroyCount > 2 && throwable.message == "HTTP 404 Not Found") {
                                    return Flowable.error(throwable)
                                }
                                return Flowable.timer(5, TimeUnit.SECONDS)
                            }
                        })
                    }
                })
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(object : Subscriber<Search> {
                    override fun onSubscribe(subscription: Subscription?) {
                        Log.d("RetrofitRxjava2", "onSubscribe")
                        subscription?.request(1000)
                    }

                    override fun onNext(search: Search?) {
                        Log.d("RetrofitRxjava2", "onNext")
                        search?.results?.forEach({
                            answer.text = with(StringBuilder()) {
                                append(it.address1)
                                append(it.address2)
                                append(it.address3)
                                append(it.kana1)
                                append(it.kana2)
                                append(it.kana3)
                            }
                        })
                    }

                    override fun onComplete() {
                    }

                    override fun onError(t: Throwable?) {
                        t?.let {
                            val errorDialogFragment = ErrorDialogFragment.newInstance(it.message + " : retry ${retroyCount}回"  ?: "error")
                            errorDialogFragment.show(supportFragmentManager, "ErrorDialogFragment")
                        }
                    }

                })
    }
}
          

■ 実装の解説

1. retryWhen

retryWhenメソッドは、リトライを行うためのFlowableを生成する関数型インターフェースを引数に渡します。Flowable.errorを呼び出さない場合に、リトライ処理を実行します。

.retryWhen(object : io.reactivex.functions.Function<Flowable<Throwable>, Flowable<Long>> {
    override fun apply(t: Flowable<Throwable>): Flowable<Long> {
        return t.flatMap(object : io.reactivex.functions.Function<Throwable, Flowable<Long>> {
            override fun apply(throwable: Throwable): Flowable<Long> {
                retroyCount = retroyCount + 1
                if (retroyCount > 2 && throwable.message == "HTTP 404 Not Found") {
                    return Flowable.error(throwable)
                }
                return Flowable.timer(5, TimeUnit.SECONDS)
            }
        })
    }
})
        

ここでは、Flowable<Long>を返却するようにしています。Flowable<Long>を返却する理由は、Flowable.timer()を使って数秒の時間をあけてリトライするためです。Flowable.timer(5, TimeUnit.SECONDS)は5秒間隔でリトライを実行します。
次の接続まで時間をあけない場合は、戻り値をFlowable<Boolean>とすると良いでしょう。

.retryWhen(object : io.reactivex.functions.Function<Flowable<Throwable>, Flowable<Boolean>> {
    override fun apply(t: Flowable<Throwable>): Flowable<Boolean> {
        return t.flatMap(object : io.reactivex.functions.Function<Throwable, Flowable<Boolean>> {
            override fun apply(throwable: Throwable): Flowable<Boolean> {
                retroyCount = retroyCount + 1
                if (retroyCount > 2 && throwable.message == "HTTP 404 Not Found") {
                    return Flowable.error(throwable)
                }
                return Flowable.just(true)
            }
        })
    }
})
        

このサンプルではリトライ回数が2回を超えて、ThrowableのmessageがHTTP 404 Not Foundの時に、エラーを呼び出しています。

if (retroyCount > 2 && throwable.message == "HTTP 404 Not Found") {
    return Flowable.error(throwable)
}
        

Flowable.error(throwable)を呼び出すと、override fun onError(t: Throwable?)メソッドが呼び出されます。

override fun onError(t: Throwable?) {
    t?.let {
        val errorDialogFragment = ErrorDialogFragment.newInstance(it.message + " : retry ${retroyCount}回"  ?: "error")
        errorDialogFragment.show(supportFragmentManager, "ErrorDialogFragment")
    }
}
        

上記のようにretryWhenメソッドは、エラーの内容によってリトライ回数や条件を細かく設定できます。

ProGuard

ProGuardを使用している場合は、ProGuardに設定が必要です。

{project_folder}/app/proguard-rules.pro
### retrofit2 & rxjava2 & gson
-dontwarn okio.**
# Platform calls Class.forName on types which do not exist on Android to determine platform.
-dontnote retrofit2.Platform
# Platform used when running on Java 8 VMs. Will not be used at runtime.
-dontwarn retrofit2.Platform$Java8
-dontwarn sun.misc.Unsafe
-dontwarn org.w3c.dom.bootstrap.DOMImplementationRegistry
-dontwarn retrofit2.**
-keep class retrofit2.** { *; }
# Retain generic type information for use by reflection by converters and adapters.
-keepattributes Signature
# Retain declared checked exceptions for use by a Proxy instance.
-keepattributes Exceptions
-keepclassmembers class rx.internal.util.unsafe.** {
    long producerIndex;
    long consumerIndex;
}

-keepclasseswithmembers class * {
    @retrofit2.http.* <methods>;
}

-keep class com.google.gson.** { *; }
-keep class com.google.inject.** { *; }

# ALSO REMEMBER KEEPING YOUR MODEL CLASSES
-keep class java_lang_programming.com.android_library_demo.article92.** { *; }

-keepattributes *Annotation*
-keep class okhttp3.** { *; }
-keep interface okhttp3.** { *; }
-dontwarn okhttp3.**
          

かなりの量になります。jsonで利用するmodelをkeepすることを忘れないでください。

ファイルの肥大化が嫌な場合は、ファイルを分割すると良いでしょう。
retrofitの作者が言うには、ProGuardを使う意味などないので、外すのが1番良いとのこと。とはいえ、そうもいかない状況もありますよね。。。

ビルド

上記のコードの動作を確認してみましょう。まずは通常接続を確認します。

fig1. 通常接続

urlを変更して接続を失敗させます。

fig2. 接続失敗

リトライでも確認します。

fig3. リトライ接続

うまく動作するのが確認できました。

結論

RetrofitとRxjava2を組み合わせたネットワーク接続の方法は今後も利用されていくと思うので、理解しておきましょう。

とはいえ、Rxjavaは学習コストが高い技術なので、kotlinを利用している場合はCoroutinesを利用するべきでしょう。

関連記事

タグ検索で調べてみよう

kotlin1.2 Android8.1 RxJava2 ライブラリ