Jetpack Compose のプレビューエラーの解消

はじめに

Androidのモバイル開発ではJetpack Composeが主流になってきていますが、その機能の1つにプレビュー機能があります。所謂ホットリロードって言われる機能ですが、UIを作る段階ではこの機能を活用すると効率よく開発できます。

しかし、この機能を使っているがいくつかのviewにおいてエラーとなり、プレビューが表示されないというプロジェクトに何度か出会ったのでその解決策を記します。これはリファクタリングにもつながることなので、是非直すことをお勧めします。

結論: ViewModelの依存先をmockにせよ

ViewModelやusecaseが、ネットワーク通信層あるいはデータベース層と接続しており、かつDIを使うことでこのエラーが発生することがほとんどです。
つまり、プレビュー機能が依存関係を解決できないために発生します。
そのため、依存先をmockなどで置き換えることで、プレビューが動作するようになります。

概要

いろいろなパターンで表示されないことがあると思いますが、だいたいMVVMもしくはそれと似たようなアーキテクチャを採用しているプロダクトにおいて、ViewModelが原因であることほとんどです。厳密にはviewModel自体が悪いのではなくて、それがDIによってネットワーク層や、ローカルデータベース(Room等)を注入しているとプレビューがー表示されないです。これはDIライブラリが現状のプレビュー機能に対応していないために起こります。(swiftUIではプレビューに対応したDIツールもあります)
HiltKoinなどのDIツールを使っていると起こるので、この解決方法を記載します。

解決方法は単純なのですがViewModelが依存しているrepository等をモックにすることです。repositoryの前にUseCase層がある場合でもネットワーク関連の処理をしている箇所が普通はrepository層だと思うのでこのrepositoryのモックをつくり、差し替えて上げましょう。

具体例

前提

  • Koinライブラリを使ってDIをしている
  • MVVM
  • UseCase層はない

この前提で実際のコードでを示して説明しますが、Hiltを使っている場合や、UseCase層がある場合でも同様な方法で解決できます。

プレビューが動かない状況

@Composable
fun NewsView(private val viewModel:HogeViewModel) {
    // UI viewModelを使った値を使用
    ....
}

@Preview
@Composable
fun NewsViewPreview() {
    val viewModel = getViewMolde() 

    MyAppTheme {
        NewsView(viewModel)
    }
}
class NewsViewModel(
    private var repository: NewsRspository
) : ViewModel() {
    ....
}

class NewsRepository() {
    suspend fun fetch() {
        // APIを使ってfetchする処理など....
    }
}

さらにAppModuleなどを定義して、DIをしているとします。

internal val appModule = module {
    viewModel { NewsViewModel(get()) }
} 

改善後

やることとしては、次の4ステップです。

  1. Repositoryのインターフェースを作る
  2. インターフェースに準拠させる。
  3. インターフェースに対してDIするように変更する
  4. Mockのrepositoryを作り、プレビューで使用する

1. Repositoryのインターフェースを作る。

Repository層のインタフェースの作成
まずはrepository層のインターフェースを作ってください。名前はIHogeRepository,または、具体実装をしている方の名前をHogeRepositoeyImplにし、インターフェースの方をHoheRepositoryにするのが良いでしょう。

interface INewsRepository() {
    suspend fun fetch()
}

2. インターフェースに準拠させる。

ここで、このinterfaceを先ほどのNewsRepositoryとviewModelの引数のrepositoryに準拠させます。

- class NewsRepository() {
+ class NewsRepository(): INewsRepository {
    override suspend fun fetch() {
        // APIを使ってfetchする処理など....
    }
}
class NewsViewModel(
-    private var repository: NewsRspository
+    private var repository: INewsRspository,
) : ViewModel() {
    ....
}

3. インターフェースに対してDIするように追加修正する

次に、インターフェースに対してDIをし正常に動作するにようにしておきます。

internal val appModule = module {
+   single<INewsRepository> { NewsRepository() }
    viewModel { NewsViewModel(get()) }
} 

4. Mockのrepositoryを作り、プレビューで使用する

最後にMockのrepositoryを作りましょう。この時、InewsRespositoryを継承させておきましょう。
そして、これをプレビューのviewModelに対して使えば無事プレビューが動作するようになります。

class FakeNewsRepository(): INewsRepository {
    override suspend fun fetch() {
        // ネットワーク通信しない、ローカルで処理するような適当な処理にする。
    }
}
....

@Preview
@Composable
fun NewsViewPreview() {
-   val viewModel = getViewMolde() 
+   val repository = FakeNewsRepository()
+   val viewModel = NewsViewModel(repository)

    MyAppTheme {
        NewsView(viewModel)
    }
}

補足: アーキテクチャの改善

ここでわざわざインターフェースを作ったのには理由があります。それは依存性を逆転させることで、よりクリーンな状態に保つためです。
今回の場合はUseCase層がないので少しわかりにくいのですが、最もビジネスロジックを含む箇所(UseCase層,今回はViewModel層)がrepositoryの具体実装に依存しないようにinterfaceを噛ませているのです。これによってより責務が明確になる、テストなども作りやすくなります。
詳しくはこちらの記事がわかりやすいです。

まとめ

Jetpack Composeにおいて、プレビュー機能は非常に重要ですので動かない状態は放置せず修正しましょう!動いてない時は、プロジェクト全体について、依存性やアーキテクチャの見直しが必要である状態の場合も多いのですので、リファクタリングと思い積極的に直すように心がけることが重要だと思います。