はじめに
こんにちは!kintone開発チームのAndroidエンジニアのトニオ(@tonionagauzzi)です。この記事は、CYBOZU SUMMER BLOG FES '25の記事です。
前回の記事では、kintone Androidアプリの段階的リファクタリングの全体像と、モジュール再分割および手動テストの一部自動化について解説しました。
第2回となる本記事では、再分割したモジュール内で実施したRxJavaからKotlin Coroutinesへの移行と独自ユーティリティクラスの利用最小化について、具体的な実装例を交えながら詳しく解説します。
前回のおさらい
前回ご紹介した段階的リファクタリングの全体像をあらためて振り返ります。
- 技術と機能を境界としたモジュール再分割(第1回で解説済み)
- 手動テストの一部自動化(第1回で解説済み)
- RxJavaからCoroutinesへの移行(本記事で解説)
- 独自ユーティリティクラスの利用最小化(本記事で解説)
- シングルトンインスタンスの削減(第3回で解説予定)
- 一部ViewのJetpack Compose化(第3回で解説予定)
今後の作業を小さくタスク化して安全に進めるために、事前にモジュール再分割と自動テストの拡充を行いました。モジュールを機能と技術の境界で再分割し、コード変更にも強くなったことで、今回解説する非同期処理の移行やユーティリティクラスの整理も行いやすくなりました。
副次的効果として、アプリ全体のどこに何があるかを既にチームメンバーが把握した状態で、今回解説する作業に臨めたというのもありました。
前回記事のテストに関する補足
手動テストの一部自動化について、前回記事では書いていなかったことを少し補足します。
自動テストの増加量
リファクタリング前後で、自動テストがどれくらい増えたのかを計測してみました。すると、131ケースだった自動テストが415ケースに増えており、リファクタリング前と比べて3倍くらいに増えていました。
テストツールの変化
リファクタリング前から、自動テストには以下のツールが使用されていました。
| ツール名 | 説明 |
|---|---|
| JUnit | Javaの単体テストフレームワーク。テストメソッドの実行やアサーションを提供 |
| MockK | Kotlin向けのモッキングフレームワーク。suspend関数や拡張関数もサポート |
| Robolectric | AndroidのunitテストでAndroid SDK呼び出しをシミュレート。実機なしでContextやViewの動作を確認 |
| Espresso | AndroidのUIテストフレームワーク。実機上でViewの操作や状態確認を自動化 |
| UI Automator | システム全体のUIテストフレームワーク。複数アプリにまたがるテストが可能 |
リファクタリング後も、上記のツールを継続して使用していますが、以下のツールを新たに追加しました。
| ツール名 | 説明 |
|---|---|
| Compose UI Test | Jetpack ComposeのUIテストフレームワーク。ComposeRule を使ったコンポーネント単位のテストが可能 |
| Coroutines Test | Kotlin Coroutinesのテストサポートライブラリ。runTest や TestDispatcher を提供 |
| Turbine | FlowのテストライブラリでCash Appが開発。Flowの値変化を時系列で検証可能 |
これらのツールが増えた理由は、UI部分をユニットテストしやすいJetpack Composeに段階的に移行したことと、RxJavaからCoroutinesに移行したことによって、それぞれの部分のテストが増えたからだと考えています。
また、モックよりもフェイクを推奨するようにしました。その理由は以下のとおりです。
- フェイクは本物の実装と同様に振る舞う:フレームワークを使わないため書くのに労力がかかるが、状態のテストが可能であるため本物に近い忠実性を担保しやすく変更にも強い
- モックは本物を模倣して振る舞う:フレームワークを使って少ないコード量で書けるが、状態のテストがない分モックより忠実性が下がり、リファクタリングの影響を受けやすいため変更にも弱い
このように自動テストの強化を進めたことで、コード変更時の安全性担保を強化しました。
段階的リファクタリングの詳細
3. RxJavaからCoroutinesへの移行
背景
kintoneのAndroidアプリは2019年のリニューアル以来、RxJavaを採用してきました。
RxJavaの課題
強力なリアクティブプログラミングライブラリであるRxJavaは長年非同期処理の基盤として活躍してきましたが、アプリの機能が増えるにつれて、以下のような課題が顕在化してきました。
- 保守容易性の課題:ストリームベースのデータ処理を理解するまでに時間がかかり、オペレータチェーンやイベント購読開始/終了のライフサイクル管理が複雑になる傾向があった
- 業界トレンドの変化:Coroutinesの人気が高まる中、RxJavaの実務経験を積む機会自体が減少しており、RxJavaベースの実装である点が採用活動などで魅力に感じてもらいにくくなっていた
- 非標準ライブラリである点:Coroutinesは標準ライブラリだが、RxJavaはサードパーティなので、公式のアップデートに対応が遅れたり破壊的変更を受ける可能性が高い
破壊的変更の一例として、iOSがSwift 6でコンパイラに変更が入った際、RxSwift使用部分が並行安全チェックに引っかかりビルドできなくなったというのがあり、Androidでも同様のことがあるのではないかと危惧する意見が挙がっていました。
Coroutinesへの移行を決断した理由
私たちはRxJavaの移行先をCoroutinesに決定しました。Coroutinesと比較できるほど有力な選択肢が他に無かったので比較検討には時間をかけませんでしたが、なぜ今移行するのかの意思決定は慎重に行いました。
私たちは、いま移行を進めることで以下の利点を受けられることが今後の生産性向上において重要だと考えました。
- 保守容易性の向上:Coroutinesは可読性を高める設計思想に基づいており、たとえばsuspend関数を使って非同期処理を同期的に書ける
- 業界トレンドへの追従:Kotlinライブラリの中でも高い採用率で、年々その人気は高まっている。KotlinやAndroid、Jetpack Compose, ViewModel, Roomなどとも親和性が高い
- 標準ライブラリへの回帰:開発元であるJetBrainsと、Android開発を主導するGoogleによって強力にサポート・推奨されているため、突然破壊的変更を受けるリスクが低い
保守容易性に関しては、テスタビリティの向上も重要な決め手でした。たとえばkotlinx-coroutines-testライブラリのrunTestは、非同期テストの複雑な部分をうまく隠蔽してくれるため、開発者にとってはテストの記述量が減り、ロジックの正しさを検証することに集中できます。非同期処理のテストが書きやすくなると、テストの正確性と容易性のバランスが取れ、品質向上にも寄与すると考えました。
移行戦略
段階的な移行アプローチ
すべてのRxJavaを一度にCoroutinesに移行するのはリスクが高いので、前回再分割したモジュールごとに、RxJavaへの依存を無くしていくことにしました。


モジュールごとに移行を進める戦略は、一気に全体を移行したりパッケージやクラスごとに移行したりする戦略と比較して、以下の利点があると考えました。
- 新機能にはCoroutinesを使える: もし新機能を作ることになれば、新機能はモジュールを切ってCoroutinesを全面導入し、リファクタリングの進捗を気にせずに開発できる
- 小さく始められる: モジュールごとに移行を実施でき、依存関係が最も少ないモジュールで最初に移行してみることでコスト感覚も掴みやすい
モジュールの切れ目は一時的にRxJavaとCoroutinesが混在することになるため、未移行のモジュール側でkotlinx-coroutines-rx3ライブラリを使い、ObservableをFlowに変換しました。
RxJavaからCoroutinesへの置き換え事例を2つ紹介します。
実装例1(UIライフサイクルへの適合)
まずは、ファイルの書き出し結果を画面にスナックバーで表示する処理です。
Before(RxJava)
fileExporter.exportResult
.subscribeBy(
onNext = {
when (it) {
FileExporter.ExportResult.Success -> {
Snackbar.make(this.binding.activityRoot, CommonString.FilePreview_SaveFileComplete, Snackbar.LENGTH_LONG)
.show()
}
FileExporter.ExportResult.Error -> {
Snackbar.make(this.binding.activityRoot, CommonString.FilePreview_SaveFileError, Snackbar.LENGTH_LONG)
.show()
}
}
}
)
.disposed(by = disposeBag)
移行前はRxJavaの持つDisposableによる明示的なイベント収集の中止が必要で、Disposableは画面を閉じた際に破棄していました。したがって、多くの処理がアプリがバックグラウンドに回ってもイベントを拾い続けてしまうため、その先の処理で画面状態の考慮が必要な実装になっていました。
After(Coroutines)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
fileExporter.exportResult.collect { exportResult ->
val message = when (exportResult) {
FileExporter.ExportResult.Success -> CommonString.FilePreview_SaveFileComplete
FileExporter.ExportResult.Error -> CommonString.FilePreview_SaveFileError
}
Snackbar.make(binding.activityRoot, message, Snackbar.LENGTH_LONG).show()
}
}
}
移行後は画面のライフサイクルに合わせて、バックグラウンドに回ったらイベント収集を中断し、フォアグラウンドに戻ったらイベント収集を再開するといった切り替えが簡単にできるようになり、アプリ側での画面状態考慮も楽になりました。
実装例2(suspend関数で非同期処理を同期的に書く)
次に、プッシュ通知に必要なプッシュトークンを得る処理です。
Before(RxJava)
interface PushTokenRepository { val pushToken: Observable<String> fun requestPushToken() } internal class FirebasePushTokenRepository : PushTokenRepository { private val firebaseMessaging = FirebaseMessaging.getInstance() private val pushTokenSubject = PublishSubject.create<String>() override val pushToken: Observable<String> = this.pushTokenSubject override fun requestPushToken() { this.firebaseMessaging.token.addOnSuccessListener { this.setPushToken(it) } } }
移行前はObservableを用いて非同期処理を待っていました。requestPushToken()を呼び出す処理とpushTokenを監視する処理は別々の場所に書かれており、requestPushToken()を呼んだ後でどの処理が動くのかを見つけにくいと感じていました。
After(Coroutines)
interface PushTokenRepository { suspend fun requestPushToken(): Result<String> } internal class FirebasePushTokenRepository : PushTokenRepository { private val firebaseMessaging = FirebaseMessaging.getInstance() override suspend fun requestPushToken(): Result<String> = suspendCoroutine { continuation -> firebaseMessaging.token.addOnSuccessListener { pushToken -> continuation.resume(Result.success(pushToken)) }.addOnFailureListener { exception -> continuation.resume(Result.failure(exception)) } } }
移行後はrequestPushToken()が結果を返すようになり、requestPushToken()の呼び出し処理の次に続きの処理を書けるようになったので、処理の流れを追いかけやすくなり、コードの可読性が高まりました。
テストの移行について
テストコードは省略しますが、実装と同様にRxJavaベースのテストからCoroutinesベースのテストに移行しました。RxJavaもCoroutinesも非同期ライブラリという点では同じなのでテストの書き方自体は似ており、便利ライブラリのTurbineも活用することで、品質への悪影響を与えることなく円滑にテストを移行できました。
得られた効果
先述の保守容易性の向上、業界トレンドへの追従、標準ライブラリへの回帰といったところが得た効果になると思います。実際にRxJava未経験のメンバーが加入して活躍していることや、追加のテストが書きやすくなりテスト駆動開発を行えるようになったことなどで、RxJavaからCoroutinesへの置き換えの効果を日々感じています。
参考:スクラムチームで始めたAndroidアプリのテスト駆動開発 - Cybozu Inside Out | サイボウズエンジニアのブログ
感じた課題
その反面、Coroutinesへの移行を進める中で、以下の課題も発生しました。
- 移行期間中の複雑性:RxJavaとCoroutinesが混在し、一時的に複雑度が上昇した
- チーム内で学習が必要:RxJavaに慣れたメンバーにとってはCoroutinesに慣れる必要があった
今すぐ乗り換えるべきだったかという観点では、RxJavaも本日時点では更新が続いているライブラリなので、決して「乗り換えるべき」と言い切れる状況ではありませんでした。しかし、私たちはこの段階でCoroutinesに移行したことで後発のリファクタリング(Jetpack Composeへの移行)や新機能開発の見積もりが小さくなったと感じているので、このタイミングで移行して良かったと思います。
なお、RxJavaからCoroutinesへの移行は、他の作業も並行しながらですが、約2ヶ月かけて行いました。もし集中していれば3週間ほどで終わったのではないかと思います。
4. 独自ユーティリティクラスの利用最小化
背景
kintoneのAndroidアプリは2019年のリニューアル当時、所属エンジニアの技術スタックの都合などによりiOSとAndroidの間でできるだけ実装を合わせていたため、プロジェクト内にはJavaやKotlinに依存しない独自ユーティリティクラスがいくつか存在していました。
まず、独自ユーティリティクラスを分類すると以下の3つに分けられました。
| No | カテゴリ | 対応方針 | 実際のクラス名 | 移行先 |
|---|---|---|---|---|
| 1 | 標準ライブラリでも代替可能なクラス | 標準ライブラリに置き換える | CBMURL | android.net.Uri |
| 2 | kintone固有の共通クラス | リファクタリング中に削除し、再生成する | CBMError | 画面ごとのエラークラス |
| 3 | RxJavaで必要だったクラス | リファクタリング中に削除する | DisposableModel | 削除 |
これらのうち「2. kintone固有のクラス」「3. RxJavaで必要だったクラス」は、他の目的でリファクタリングする際に自然消滅することがわかりました。特に3.におけるDisposableModelをもとに各画面のViewModelが作られていたため、その影響でAndroidXが提供する標準のViewModelを使っていない状況でした。DisposableModelを消去し、各画面がAndroidXのViewModelを使うようにしたことは、第3回で紹介するシングルトンインスタンスの削減で詳しくお伝えします。
さて、ここでの焦点は、いつまでも残り続けそうな「1. 標準ライブラリで代替可能」なものをそのままにしておくか、思い切って書き換えるかでした。
1.の中でも目立っていたのはCBMURLだったので、本章ではCBMURLの廃止を例として取り上げます。CBMURL以外にも何個か独自ユーティリティクラスがありましたが、どのようなクラスかを説明すると長くなるので、本章ではCBMURLに絞って説明します。
CBMURLというクラスは、URLをベース URI、スキーム、パス、およびクエリに構造化する機能を提供するクラスでした。実装は割愛しますが、以下のように各所で使われていました。
CBMURL.create(url = url)?.let { cbmUrl ->
// URLを扱う処理
}
独自ユーティリティクラスの問題点
こういったクラスは当時の課題を解決するために作られ、長年kintone Androidを支えてきましたが、機能が増えるにつれて以下の問題を引き起こしていました。
- 重複実装:
java.net.URIやandroid.net.Uriが同じ機能を持っており、それらは標準のSDKで提供されている - 条件判定の複雑化:
CBMURLの場合は毎回nullチェックが必要で、先述のサンプルコードのletのような余剰処理が増える - 学習コスト:新規メンバーが独自の仕組みを理解する必要がある
CBMURLはandroid.net.Uriへ移行するのが良いと考えました。android.net.Uriの持つ機能が「URLからホストやスキーム、パス、クエリを取り出したり、URLを組み立てたりしたい」という目的と合っていたからです。
また、String型からandroid.net.Uri型へはtoUri()で簡単に変換できるので、URLをパラメーターで渡す部分はほとんどString型で受け渡すようにしました。
しかし、URIを扱うほとんどの場所でCBMURLが使われており、230箇所の修正が必要だったため、移行が先送りになっていました。
移行戦略
CBMURL利用最小化のアプローチ
これに関してはモジュールごとに進めるのは接続部分の変換処理が必要になるため効率が悪いと判断し、アプリ全体に対して一度にCBMURLを廃止しました。
具体的な置き換え例
String値で渡されてきたURLをstartRequestで開く処理です。
Before(CBMURL型で呼び出し)
CBMURL.create(url = url)?.also { url ->
startRequest(url = url)
}
After(String型で呼び出し)
startRequest(url = url)
この場合、startRequestのパラメーターはCBMURL型からString型に変わっています。このような小さな修正も、積み重なれば山となる、でした。
得られた効果
- コードベースの簡潔化:独自ユーティリティ使用部分のコード量が約50%削減
- 保守容易性の向上:標準ライブラリのバグ修正や改善を自動的に享受できるようになり、null安全になったことで品質保証コストも削減した
- 学習コストの削減:新規メンバーが独自の仕組みを覚える必要がなくなった
感じた課題
- 移行の優先順位付け:1箇所1箇所の変更は小規模でも、
CBMURL使用箇所が多かったためすべてを一度に置き換えるには数日間かかり、他作業との優先順位の判断が難しかった - 移行後の品質担保:ユニットテストが網羅的に書かれていたわけではなかったので、この変更の後はチーム全員で3日かけてフル回帰テストを行いリリースした
ある程度思い切りと勢いで進めた対応ですが、逆に言えばそれが良かったのかなと思います。3日間でアプリ全体からCBMURLをなくすことができ、対応コストは回帰テストを含めても6日間でした。
まとめ
本記事では、kintone Androidアプリの段階的リファクタリングのうち、RxJavaからCoroutinesへの移行と独自ユーティリティクラスの利用最小化について、その全容を詳しく解説しました。モダンな技術スタックへの移行を果たし、今後の機能開発をより効率的に行える基盤を整えることができました。
次回予告
最終回となる第3回では、シングルトンインスタンスの削減と一部ViewのJetpack Compose化について解説します。アーキテクチャの見直しとUIの刷新により、さらなる開発効率の向上を実現した取り組みをご紹介します。9月16日に発信予定です。お楽しみに!