Sansan Tech Blog

Sansanのものづくりを支えるメンバーの技術やデザイン、プロダクトマネジメントの情報を発信

RxJava → Coroutinesの置き換えをAIで6倍速にした話

はじめに

こんにちは。技術本部 Sansan Engineering Unit Mobile Applicationグループの鎌田です。2022年8月にSansanに中途入社し、SansanのAndroidアプリ開発およびiOSアプリ開発に携わっています。

この記事は同じくMobile Applicationグループの朴との共著でお届けします。

Mobileチームでは「技術負債返済」をテーマとしたTech Blogリレー企画を行っています。本記事はその第7弾です。

  1. 技術負債解消に向けた継続的運用の試み(2025-09-01)
  2. 10年もののSansan Mobileで負債・リスクに向き合う(2025-10-29)
  3. Android Edge-to-Edge対応 大規模アプリですべての画面を更新するための道のり(2025-11-13)
  4. SansanのAndroid View→Jetpack Composeへの移行計画(2025-11-17)
  5. 10年ものプロダクトの技術負債: Realmのスレッド安全性を担保する(2025-12-17)
  6. 10年以上運用されているSansan iOSアプリのすべてのSwiftコードをフォーマットした話(2025-12-26)

今回は、Sansan AndroidアプリにおいてRxJavaからKotlin Coroutinesへの移行をAIを用いて爆速で行ったプロジェクトの話をお伝えします。

背景

Sansan Androidアプリでは、非同期処理にRxJavaとCoroutinesの両方が使用されていました。

2020年頃にCoroutinesが導入された際、RxJavaで実装された非同期処理を段階的にCoroutinesへ置き換える計画が立てられました。

しかし、利用頻度の少ない機能やアプリの基盤となる機能の置き換えが進まず、2025年時点でもRxJavaの置き換えは完了していませんでした。

そのため、Mobileチームで行われている技術負債に対処していく取り組みの一環として、残留しているRxJavaを全てCoroutinesに置き換えようというプロジェクトがスタートしました。

技術負債プロジェクトに関してはこちら

buildersbox.corp-sansan.com

AIで爆速で行いました

RxJavaをCoroutinesに置き換える作業は176時間かかる見積もりでしたが、計画と実装に積極的にAIを活用することで、約30時間で完了させました。想定の6分の1の工数でRxJavaの置き換えを実現したことになります。

変更コード量は約14,000行、Acceptance Rateは約80%でした。QAの計画も含め、約1カ月でアプリ全体の置き換えの実装を完了させました。

以降では、どのようにAIを活用したか、そしてRxJava置き換えをスムーズに進めるためにどのような工夫をしたかをお話しします。

なお、Sansan MobileチームではCursorやClaude Codeを中心にさまざまなAIエージェントを各メンバーが使用しています。本記事では簡単のためこれらをまとめて「AI」と呼びます。

AIを用いて行った作業

影響範囲の調査

置き換えの方針を決めるため、現状把握としてアプリ内でRxJavaを使用している箇所の調査をAIで行いました。

ビルド設定ファイルでRxJavaをimplementationしている箇所からどのモジュールでRxJavaを使用しているか、あるいは import io.reactivex.Completable のようにRxJavaのAPIがimportされている箇所からRxJavaが残っているモジュールを特定できます。

AIにそれらの記述があるモジュールやクラスを探してもらうことで実装を探ることなくほぼ自動で影響範囲の規模を見積もりました。

実装

RxJavaからCoroutinesに置き換える実装方針を先に決め、どの画面にも共通している実装を置き換えるプロンプトを作成して実装作業の大部分を自動化しました。

実際に使用したプロンプトの一部を紹介します。

以下のように、アプリで実装されているRxJavaのAPIを置き換えるコード例と置き換えるポイントが記載されています。

プロンプト作成もAIに行わせていました。まずAIに案件の情報・要件・おおまかな実装方針を伝えます。次に、RxJavaの置き換えを一部だけAIに実行させ、そのうえで「ここまでの作業を別モジュールでも再現できるプロンプトを作成して」と指示します。

## アーキテクチャの原則
- ❌ Api層、Repository層、DataSource層では使用しない
(コードの例が載っている)
- ✅ UseCase層でのみResult<>、Flow<>を使用する
(コードの例が載っている)

## 実装パターン
### 1. Single<T> → suspend fun Result<T>

### Before (RxJava2)

```
fun getMeishi(): Single<List<Meishi>> =
    meishiApi.getMeishi()
        .map { it.items }
```

### After (Coroutines)

```
suspend fun getMeishiSuspend(): Result<List<Meishi>> = withContext([Dispatchers.IO](http://Dispatchers.IO)) {
    runCatching {
        meishiApi.getMeishi().await().items
    }
}
```

**ポイント:**

- `withContext(Dispatchers.IO)`でIO処理を明示的に指定
- `runCatching`で例外を`Result`型でラップ
- `.await()`でRxJavaのSingleをsuspend関数に変換
- 既存のRxJava版には`@Deprecated`アノテーションを追加

### 2. Maybe<T> → suspend fun Result<T?>

Maybe型の場合も同様にResult<T?>に変換します。空の場合はResult.success(null)を返します。

### 3. Observable<T> → Flow<T>

### Before
...(以下略)

また、AIのアウトプット精度を高める方法として、以下のような工夫をしていました。

@Deprecatedで古い実装をなるべく残す

RxJavaの置き換え中は、既存のRxJavaで実装された関数は@Deprecated(replaceWith)アノテーションで残し、置き換え完了後にまとめて削除しました。

Coroutinesで実装した関数と置き換え前の関数の対応関係をコード上で明確にしていました。全ての置き換えが完了してアプリが安全に動作することを確認した後、RxJava版のメソッドをまとめて削除するという手順を踏みました。

この方法により、置き換え前後の対応関係を常に参照できる状態で置き換えを進めていました。

// 一度にまとめて置き換えるのではなく、@Deprecatedで一時的にRxJavaの実装を残す。
// そうすることで置き換え前後の実装が分かる状態でlegacyGet()を呼び出している箇所でget()メソッドへ置き換える作業をAIでも確実に行える
+ @Deprecated("今後はCoroutines対応した方を使う", ReplaceWith("path.to.this.class.get"))
@GET("path/to/api")
fun legacyGet(
    @Path("id") id: String,
): Single<Item<Entity>>

+ @GET("path/to/api")
+ suspend fun get(
+     @Path("id") id: String,
+ ): Item<Entity>

// Deprecatedメソッドを残した状態で呼び出し箇所を置き換える
- val entity = legacyGet(id)
+ val entity = get(id)
// 安全に置き換えが確認できたタイミングでDeprecatedな方のメソッドを削除する
- @Deprecated("今後はCoroutines対応した方を使う", ReplaceWith("path.to.this.class.get"))
- @GET("path/to/api")
- fun legacyGet(
-     @Path("id") id: String,
- ): Single<Item<Entity>>

@GET("path/to/api")
suspend fun get(
    @Path("id") id: String,
): Item<Entity>

val entity = get(id)

置き換えても設計は維持する

RxJavaで使用していたエラーハンドリングを行う共通クラスのCoroutines版を事前に用意するなど、機械的に置き換えられるように準備した上でAIに実装を依頼しました。

今回置き換え実装を行った画面の中には、作成当時の設計を現在まで引き継いでいる画面もありました。本プロジェクトの目的はRxJava依存をなくすことで、設計の見直しのような目的から外れた変更まで行うと工数が膨らみがちです。そのため、そういった部分には一切手を付けず最小工数で進めることを徹底するための設計判断を行っていました。

// PresenterクラスからuseCaseを呼び出す実装を置き換える例
-        useCase.legacyDelete(itemId)
-                .observeOnUI()
-                .doOnSubscribe { view.showProgressDialog() }
-                .doFinally { view.dismissProgressDialog() }
-                .subscribeWith(
-                    object : LegacyApiCompletableObserver(errorHandler, logoutHandler) {
-                        override fun onComplete() { view.removeItem(itemId) }
-                        override fun onForbiddenError(): Boolean { view.showDeleteFailed(); return true }
-                        override fun onNotFoundError(): Boolean { view.removeItem(itemId); return true }
-                    }
-                ).addDisposable()
+        launch {
+            view.showProgressDialog()
+            useCase.delete(itemId) 
+                .also { view.dismissProgressDialog() }
+                .onSuccess { view.removeItem(itemId) }
+                .onFailureCoroutineCancelSafely { throwable ->
+                    object : ApiErrorCollector(errorHandler, logoutHandler) {
+                         override fun onForbiddenError(): Boolean { view.showDeleteFailed(); return true }
+                         override fun onNotFoundError(): Boolean { view.removeItem(itemId); return true }
+                    }.onError(throwable)
+                }
+        }

まとめ

RxJavaをCoroutinesに置き換える負債解消をAIを用いて爆速で置き換えた事例、そして、開発を高速で進めるために行なった工夫を紹介しました。

事前に基盤となる実装だけ行っておく、AIが行う作業がなるべく単純になるように実装方針を定めてチーム内で合意しておくなどAIが最大限パフォーマンスを出せるようにお膳立てをするのもAIを用いた開発の仕方の1つだと考えています。

私たちの経験が、同じ悩みを抱える開発チームの助けになれば幸いです。

Sansan技術本部ではカジュアル面談を実施しています

Sansan技術本部では中途の方向けにカジュアル面談を実施しています。Sansan技術本部での働き方、仕事の魅力について、現役エンジニアの視点からお話しします。「実際に働く人の話を直接聞きたい」「どんな人が働いているのかを事前に知っておきたい」とお考えの方は、ぜひエントリーをご検討ください。

© Sansan, Inc.