STORES Product Blog

こだわりを持ったお商売を支える「STORES」のテクノロジー部門のメンバーによるブログです。

STORES 予約 Androidアプリを Jetpack Compose でフルリニューアルしました

はじめに

こちらは STORES Advent Calendar 2022 19日目の記事です。

はじめまして、モバイルアプリエンジニアの satoryoです。

4月に入社し、 STORES 予約 の Androidアプリを担当しています。

先日その STORES 予約 Androidアプリを Jetpack Compose でフルリニューアルし、今年の9月にリリースしました。

昨年の12月一足先にリリースした STORES 予約 iOSアプリで SwiftUI を採用した背景から、AndroidJetpack Compose を採用する形で開発を続けてきました。

私は Jetpack Compose を業務で採用することが初めてで、モバイルアプリエンジニアとして大きな一歩になったと思っています。

関連ライブラリが続々とアップデートされておりまだまだリファクタリングの余地はあるものの、フルリニューアルで対応した内容や感想などをまとめましたので最後まで読んでいただけたら幸いです。

なお、記事中のソースコードはサンプルとして書き直したため、実際のプロダクトのソースコードとは異なります。

アーキテクチャ

Jetpack Compose のアーキテクチャやレイヤー構造についてはAndroid界隈でさまざまな意見が出ており非常に悩みましたが、チームとの話し合いを経て MVVM を採用しました。

アーキテクチャのレイヤー構造は以下のとおりです。

モジュール構成

Androidアプリのフルリニューアルにあたり、以下のようなモジュール構成にしました(一部省略)。

app
data
 ├── api
 └── repository
domain
 ├── model
 └── use_case
feature
 ├── screen_1
 ├── screen_2
 ├── ...

data モジュール

API通信などデータを取り扱うモジュールです。

apiモジュールではAPIクライアントの定義し、repositoryモジュールで API通信処理を実行したりDataStoreの保存、削除などを行います。

domain モジュール

ここではドメイン部分を取り扱います。

View 側から直接 repositoryモジュールを呼ばない構成にしたかったため use_caseモジュールを用意し repositoryモジュールとのハブになるように構成しています。

use_caseモジュールでは repositoryモジュールから取得したデータを、 modelモジュールに定義した data class に変換して View 側へ渡す役割をしています。

feature モジュール

View や ViewModel は featureモジュールに配置します。

Dynamic Feature Moduleを採用しており、画面ごとにモジュールを分けています。

今回はモジュール名をわかりやすく screen_1 , screen_2 としていますが、画面名に合わせて命名します。

モジュールの依存関係

上記の内容を踏まえ、各モジュールの依存関係はこのようになっています。

メインモジュールの依存関係
サブモジュールの依存関係

Jetpack Compose のサンプル実装

View や ViewModel は以下のように実装しました。

View側の実装

@Composable
fun SampleScreen() {
    val viewModel = hiltViewModel<SampleViewModel>()
    val sampleData by viewModel.sampleData.collectAsState()

    BaseSampleScreen(sampleData = sampleData) {
        viewModel.onClickButton()
    }
}

@Composable
private fun BaseSampleScreen(
    sampleData: List<SampleDataModel>,
    onClickButton: () -> Unit
) {
    Scaffold(topBar = {
        TopAppBar(
            title = {
                Text(text = "Sample Title")
            },
            navigationIcon = null,
            backgroundColor = Color.White,
            elevation = 1.dp
        )
    }) { paddingValues ->
        LazyColumn(
            modifier = Modifier
                .padding(paddingValues)
                .fillMaxSize()
        ) {
            item {
                Button {
                    onClickButton()
                }
            }

            items(sampleData) { data ->
                Text(text = data.title)
            }
        }
    }
}

@Preview
@Composable
private fun BaseSampleScreenSample() {
    BaseSampleScreen(
        sampleData = listOf(SampleDataModel(title = "title"))
    ) {}
}

ViewModelの呼び出しを hiltViewModel() で行っており、ViewModelのパラメータを参照しています。

パラメータについては後述しますが、StateFlowで実装しています。LiveDataを採用するか迷いましたが、メインスレッドとバックグラウンドスレッドで値の更新方法が異なるためややこしい( value / postValue() ) などの理由から StateFlow を採用しました。

また、レイアウトXMLのようにPreviewを見ながら開発をしたかったのですが ViewModel をComposable内で呼び出してしまうとビルドエラーでPreviewを表示できなくなってしまう問題がありました。

そこでViewModel呼び出し用の SampleScreen() と View実装兼Preview表示用の BaseSampleScreen() に分けることで解決しました。

さらに ViewModelの onClickButton() を呼び出し側で行い、Viewの実装側ではComposableの引数に実行する関数を代入できるように定義しています。

ViewModel 側の実装

@HiltViewModel
class SampleViewModel @Inject constructor(
    private val sampleUseCase: SampleUseCase
): ViewModel() {

    private val _sampleData = MutableStateFlow<List<SampleDataModel>>(emptyList())
    val sampleData: StateFlow<List<SampleDataModel>> = _sampleDataModel

    fun onClickButton() {
        viewModelScope.launch {
            // API通信を実行する
            sampleUseCase.fetchSampleData()
                .flowOn(Dispatchers.IO)
                .onEach { data ->
                    _sampleData.value = data
                }
                .catch {
                    // エラーハンドリング
                }
                .launchIn(viewModelScope)
        }
    }
}

DIにhiltを採用し、Hiltモジュールでバインディングしておいた UseCase をコンストラクタにインジェクトしています。

先ほども述べたとおりパラメータは StateFlow を採用し、 onClickButton() を実行後に値を取得したタイミング(.onEach {})でパラメータの更新を行っています。

実は UseCase の関数もほぼすべて返り値に Flow を採用しているため、例えばAPI通信時の値の取得やエラーの catch などをまとめて行うことができるので便利です。

さいごに

レイアウトXMLのコーディングが好きだった私にとって最初は Jetpack Compose に慣れず苦戦していましたが、慣れていくと徐々に思いどおりの UI を書けるようになりました。

ViewModelやUseCaseで Flow を採用しましたが Jetpack Compose との相性が良いと個人的に感じており、エラーダイアログや通信中のUI表示などイベントの通知にも Flow を使用しています。

Jetpack Compose は続々とライブラリバージョンが更新されており、現在 Android アプリの最新バージョンへのアップデートを準備中です。

STORES 予約 Androidアプリをより高い品質を目指しチームで運用していきます!

また、 STORES のモバイルアプリは他にもあり、各チームさまざまな取り組みをしています。

もし少しでも興味をもっていただけましたら、ぜひ採用サイトをご覧ください。

最後までご覧いただきありがとうございました。

STORES Advent Calendar 2022 はまだまだ続きますので引き続きお楽しみください!

jobs.st.inc