品質と開発効率を向上へ! Androidアプリのリアーキテクチャによる負債脱却

こんにちは。TVerAndroidエンジニアをしている石井です。

TVerサービス並びにTVerAndroidアプリは、2015年にリリースされ今年でちょうど10周年を迎えます。
10年前ともなるとCoroutinesはもちろんViewModelなどの今のAndroid開発の土台と言えるものも当時はなく、Activity, Fragmentに直接APIを実行するコードがあったりするのが普通だった時代だと思います。 TVerにもそのような実装があり技術的負債が溜まっていく中で、今後質の高いアプリケーションを提供していくためにリアーキテクチャに着手しました。

本記事では、その中で実施したリアーキテクチャをはじめとしたマルチモジュール化や移行に伴う密結合なコードの解消、AIの活用事例についてご紹介します。

マルチモジュールへの移行

従来のTVerAndroidアプリは単一モジュール構成で、コードベースが巨大化し、ビルド時間の増大や依存関係の複雑化といった課題を抱えていました。これを解決するために、マルチモジュール構成への移行を進めています。

密結合なコードのリファクタリング

マルチモジュール化する上で大きな障害になるのが、単一モジュール故の密結合です。

実際に一例を挙げてみます。
AndroidのApplicationクラスを継承し独自のApplicationクラスを作ったとしましょう。

class CustomApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        // SDKの初期化だったり色々な処理
    }
}

おそらくこの段階ではCustomApplicationは外部SDKを参照することはあっても、このクラス自体は参照されないのでマルチモジュール化しても特に問題ないです。
ここで各Fragmentから何かしらの状態を管理したくなり、ちょうど便利な複数の画面から共通でアクセス可能なApplicationを使うことにしてみましょう。

class CustomApplication : Application() {
    private var state: State? = null

    override fun onCreate() {
        super.onCreate()

        // SDKの初期化だったり色々な処理
    }

    fun setState(state: State) {
        this.state = state
    }

    fun hasState(): Boolean {
        return state != null
    }
}


class XXXFragment : Fragment() {
    fun checkAndFetchState() {
        val app = activity?.application as? CustomApplication ?: return
        if (!app.hasState()) {
            // fetch state
            app.setState(state)
        }
    }
}

実際このようなコードを書くことは今の開発ではあまりないと思いますが、レガシーコードではありがちかなと思います。
このようになってくるとXXXFragmentを別モジュールに動かした時にappとの循環参照が発生するため調整が必要になります。 これがXXXFragmentだけであれば対して問題ないですが、複数の画面に跨っている場合に影響範囲は大きくなり切り離しが難しくなります。 色々と考えた結果、共通で使える管理クラスを設置しApplicationはプロキシとして既存実装に影響を与えないように修正し、マルチモジュール対象のクラスは管理クラスから参照することで解決しました。

// common module
// DIで実装する形でも良し
object StateManager {
    private var state: State? = null

    fun setState(state: State) {
        this.state = state
    }

    fun hasState(): Boolean {
        return state != null
    }
}

// app module
class CustomApplication : Application() {
    private var state: State? = null

    override fun onCreate() {
        super.onCreate()

        // SDKの初期化だったり色々な処理
    }

    fun setState(state: State) {
        StateManager.setState(state)
    }

    fun hasState(): Boolean {
        return StateManager.hasState()
    }
}

// feature module
class XXXFragment : Fragment() {
    fun checkAndFetchState() {
        val app = activity?.application as? CustomApplication ?: return
        if (!StateManager.hasState()) {
            // fetch state
            StateManager.setState(state)
        }
    }
}

BaseFragmentからの脱却

Androidのレガシーコードでは、各画面での共通処理をBaseFragmentなど親クラスで共通化されていることがありがちです。
単一モジュールの場合、当然密結合されていてモジュール移管できないので、こちらも責務で分けていき必要に応じて各画面で使う実装に切り替えました。

  • Lifecycleに依存するものはFragmentLifecycleCallbackを利用して別軸で実行するフローを構築
  • そうでないものはInterfaceを用意してDIで利用する形に変更

例えばonResumeで計測ログを送りたいなどの場合

class ResumeLogFragmentLifecycleCallback : FragmentLifecycleCallback() {
    override fun onFragmentResumed(fm: FragmentManager, f: Fragment) {
        super.onFragmentResumed(fm, f)

        val isTarget = f.javaClass.isAnnotationPresent(RequireResumeLog::class.java)
        if (!isTarget) return
        // send resume log
    }
}

@RequireResumeLog
class XXXFragment : Fragment() {
    // implement fragment
}

このように実装することで、対象のFragmentのonResumeの際に裏側で実行されます。 また全ての画面で実行せずアノテーションなどで制御しておけば、既存で定義されているBaseFragmentと競合せず重複実行されることもないので、安全に段階的に移管することが可能になってきます。

アプリアーキテクチャガイドベースのリアーキテクチャ

前述の通り10年ほど前ではCoroutinesやViewModelはない状態でのAndroid開発なため、TVerではActivity/FragmentからAPIを実行してCallbackで処理して描画を行うなど、ビジネスロジックとUIロジックが密結合している箇所が多く存在していました。
この密結合な構造は、単体テストが非常に困難になるという大きな問題を引き起こし、結果としてコードの品質担保が難しい状態でした。
この状況を打開するため、Googleが推奨するアプリアーキテクチャガイドベースの実装への切り替えを断行しました。

まず最初にUIロジックとビジネスロジックの分離という観点でViewModelとLiveDataの活用推進を行いました。 元々Fragmentから直接APIを叩いていますが、ViewModelを用意してAPI実行、CallbackでLiveDataに流して、FragmentでObserveするフローです。
この置き換え自体は、DIも使わずそこまで難しいものではないので通常の施策の中で組み込んで対応を入れていき徐々に改善していきました。

そして今回のリアーキテクチャでRepository, UseCaseなどの導入を実施しました。この結果責務が分離したことにより単体テストがしやすい設計になり、より品質を高めていける状態にできました。

またこのリアーキテクチャに併せてCoroutinesに対応することで、suspend関数などを用いたKotlinらしい実装になる他、Flowにも対応することで今後のJetpackCompose導入の際にも適した実装にすることができました。

開発効率と品質を高めるAI活用と開発環境整備

アーキテクチャのような大規模な改修を進める上で、開発スピードと品質を両立させるために、AIの活用と開発基盤の整備も同時に進めています。

Gemini Code Assistによるレビューコストの削減

レビューコストの削減を兼ねてGitHub上でGemini Code Assistを活用しています。

developers.google.com

先にGeminiがコードレビューをすることで、明らかな修正項目が減りエンジニアのレビューコストが削減できます。これによりエンジニアが詳細な部分に対してのレビューが容易になる他、エンジニアが開発業務に充てられる時間が増え、開発生産性の向上にも繋がります。

Claude Codeによるテストコード生成

特に、リアーキテクチャによってテスト可能なコードが増えたことを受け、テストコードの作成にClaude CodeなどのAIツールを活用しています。 TVerでは全エンジニアにClaude Codeを利用できるようにしているので、普段の業務ではもちろんテストコードの作成にも活用しています。

人間が手書きするよりも迅速に、基本的なテストケースやボイラープレートなコードをAIに生成させることで、エンジニアはより複雑なロジックの設計やレビューに集中できるようになりました。これは、リアーキテクチャに伴う大量のテストコード作成において、開発速度を大きく向上させる重要な取り組みとなっています。

※ Claude Codeの全社導入についてはこちら!

techblog.tver.co.jp

Linter/Formatterによる負債解消

また、長期的に品質を維持し、開発効率を高めるために、LinterやFormatterといった開発基盤のツールも改めて整備しました。

自動でコードのフォーマットを整えることにより、複雑な実装による潜在的なバグを排除しつつコードベース全体の負債を徐々に解消し、統一されたコードスタイルを保てるようになりつつあります。 これにより細かいコーディング規約でのレビューコストを下げよりレビューに集中できるような環境を作っています。 また、既存のレガシーコードに対してのLinterの指摘はかなり多いですが、ktlintのformat機能を利用して徐々に解消しています。

最後に

TVer Androidアプリの大規模なリアーキテクチャは、アプリの品質を担保し、今後の高速な機能開発を可能にするための重要な投資です。マルチモジュール化、新しいアーキテクチャの導入、そしてAIの活用は、その基盤を強固にするための柱となっています。 TVerはこれからも、ユーザーにより良い視聴体験を提供し続けるため、技術的な挑戦と改善を続けてまいります。