🦁

Mavericksの導入理由と使い方

2024/07/01に公開

こんにちは。2024年5月1日にTimeTreeに入社したAndroidエンジニアの笠松です。
弊社はビジネスネーム制を採用しており、社内ではRingと呼ばれています。

この記事ではTimeTreeでAndroidアプリで使用しているMavericksというフレームワークをご紹介します。

この記事以降も、開発の過程で学んだ技術を定期的に共有していきます。同じ技術的な課題を抱えている読者や、TimeTreeへの入社を検討してくださっている方々の参考になれば幸いです。

Mavericksとは何か?

MavericksはAirbnb社が開発しているフレームワークです。 AirbnbTonalなど大規模アプリで採用されています。MavericksAndroid Jetpack and Kotlin CoroutinesなどのAndroidで使用される標準ライブラリを補完する形で、簡単により安全で統一的な実装をサポートしてくれます。

なぜMavericksを導入しているか?

導入した理由を、Mavericks導入前の課題から説明します。

Mavericks導入前から

  • MVVM(Model-View-ViewModel)を採用
  • View(ActivityFragment)からAndroid Architecture Components(AAC) ViewModel を呼び出す
  • ViewModelUseCaseRepositoryを呼び出す

といった大枠の方針は決まっていました。

一方で、ViewModelでの状態の持ち方やViewModelからViewへの呼び出しについては画面ごとに差異がある状態でした。

このような背景からMavericksによりViewModelでの状態の持ち方やViewModelからViewへの呼び出しをボイラーテンプレートを排除し、統一した方法で実装することを目的に導入しました。

現在、TimeTreeのAndroidアプリでは多くのViewViewModelMavericksにより統一した方法で実装されています。

Mavericksの使い方

この節ではMavericksの使用方法について説明します。

Activity / Fragment

ActivityFragmentではMavericksView を継承して使用します。この継承により以下の処理が効率的に実現できます。

ViewModelの取得

AAC と同様にDelegated propertyViewModelを取得できます。(AACとメソッド名が異なります)

ActivityにおけるViewModelの取得

private val viewModel: ViewModel by viewModel()

FragmentにおけるViewModelの取得

private val viewModel: ViewModel by fragmentViewModel()

インテントへの値の設定と取得

Mavericks.KEY_ARGをキーとしてIntentに値を設定することで、ViewModelで設定した値を受け取ることができます。argsParcelable またはSerializable である必要があります。

intent.putExtra(Mavericks.KEY_ARG, args)

以下はIntentに設定した値をViewModelが持つStateのコンストラクタで受け取る例です。ViewModelStateの関係についてはViewModelの節で解説します。ここではViewModelからキーを意識することなく、Intentで設定した値を取得できていることを見ていただければと思います。

class ViewModel @AssistedInject constructor(
    @Assisted state: State
) : BaseMvRxViewModel<ViewModel.State>(state) {
    @Parcelize
    data class Args(
        val value:String
    ) : Parcelable

    data class State(
        val value: String,
    ) : MavericksState {
        // Intentで設定した値の取得
        constructor(args: Args) : this(
            value = args.value
        )
    }
}

Fragmentの引数の設定と取得

Fragmentargumentsの設定と取得が可能です。

asMavericksArgsParcelable またはSerializable に対する拡張関数です。

argumentsの取得

private val args: YourArgsType by args()

argumentsの設定

fragment.arguments = yourArgs.asMavericksArgs()

ViewModel

ViewModelBaseMvRxViewModel<S : MavericksState> を継承することでMavericksを使用することができます。

標準的なViewのStateの実装

MavericksViewの状態をViewModelで保持するのための標準的な方法を提供します。このViewの状態をStateと呼び、 MavericksStateを継承することで宣言します。また、StateBaseMvRxViewModelのジェネリクスとして宣言します。

以下の例ではViewModel.StateStateになります。

class ViewModel @AssistedInject constructor(
    @Assisted state: State
) : BaseMvRxViewModel<ViewModel.State>(state) {
    data class State(
        val value: String,
    ) : MavericksState
}

以下のようにViewModel内でStateを更新することができます。この処理はスレッドセーフです。処理はキューに登録され、バックグランドスレッドで実行されます。data classcopyメソッドと同様に、オブジェクトの不変性を保つことができます。

setState {
    copy(
        property = value
    )
}

Stateへのアクセスは以下のように行います。

withState { state ->
    function(state.property)
    ...
}

suspend関数内でStateにアクセスする場合、以下のようにawaitState を使用します。

val state = awaitState()

ViewにおけるStateの監視

以下のように特定のプロパティを監視し、値に変更がある場合の処理を実装できます。

viewModel.onEach(ViewModel.State::property) { property ->
   ...
}

state全体を監視したい場合は以下のように実装します。

viewModel.onEach { state ->
   ...
}

新規プロセスにおける値の復元

Androidではメモリを節約するために、プロセスの強制終了による状態の破棄を行い、新規プロセス起動時にsavedInstanceStateを使用した値の復元が行われます。Mavericksでは@PersistStateを宣言することで、その値の復元が可能になります。

data class FormState(
  @PersistState val property: String = "",
) : MavericksState

@PersistStateを使用するプロパティはParcelableまたは Serializable である必要があります。

非同期処理

Async<T> は非同期処理の状態を管理するsealedクラスです。 suspend関数のラムダに対して executeを呼び出すことで、状態に応じたAsyncを受け取ることができます。 execute呼び出し直後、 Loadingを受け取り、処理の結果に応じて Successまたは Failを受け取ります。

viewModelScope.launch {
    suspend {
      suspendFunction()
    }
        .execute {
            copy(property = it)
        }
}

Async<T>MavericksStateに保持し、以下のようにonAsyncで状態を監視することができます。

data class MyState(val property: Async<String>) : MavericksState

onAsync(MyState::property) { property ->
    ...
}

Dagger/Hiltとの連携

MavericksAndroidの依存関係注入ライブラリであるDagger/Hiltと容易に連携することができます。

BaseMvRxViewModelではMavericksViewModelFactoryによりStateの初期化を行います。そのため、Dagger/Hiltの注入対象を選択可能な@AssistedInjectを使用し、MavericksViewModelFactoryで生成するStateに注入対象外であることを示す@Assistedを指定します。

AssistedViewModelFactoryには @AssistedFactoryを指定し、@Assistedの設定方法であることをDagger/Hiltに指示します。

class ViewModel @AssistedInject constructor(
    @Assisted initialState: State,
) : BaseMvRxViewModel<ViewModel.State>(initialState) {
    data class State(
        val data: String = "",
    ) : MavericksState

    @AssistedFactory
    interface Factory : AssistedViewModelFactory<ViewModel, State> {
        override fun create(state: State): ViewModel
    }

    companion object : MavericksViewModelFactory<ViewModel, State> by hiltMavericksViewModelFactory()
}

ViewModelの注入はMavericksが用意したMavericksViewModelComponent を指定して行います。

@Module
@InstallIn(MavericksViewModelComponent::class)
interface ViewModelModule {

    @Binds
    @IntoMap
    @ViewModelKey(ViewModel::class)
    fun viewModelFactory(factory: ViewModel.Factory): AssistedViewModelFactory<*, *>
}

Composable

MavericksComposableとの連携もサポートします。

以下のようにmavericksViewModel関数によりLocalLifecycleOwnerのスコープでviewModelを取得できます。

また、MavericksViewModelの拡張関数であるcollectAsStateを使用することで、MavericksStateをリアクティブに再描画可能な状態でComposableに使用できます。

@Composable
fun Screen() {
    val viewModel: YourViewModel = mavericksViewModel()
    val state by viewModel.collectAsState()
    Box {
        Text(state.data)
    }
}

TimeTreeの採用情報🌟

お読みいただいて、ありがとうございました。

最後に、入社から2ヶ月のTimeTreeの印象を少し書かせていただきます!

希望していた公開カレンダーの開発を担当させていただき、あっという間の2ヶ月でした。

その中で、TimeTreeではたくさんのチャンスに恵まれていると感じています。そのチャンスをなるべくものにしたいと思い、周りの方に助けていただきながら、わからないながら行動しているところです。また、普段のミーティングから雑談を大切にしていることや、多くのドキュメントの冒頭に「Why」の項目があり、なぜやるかを大切している印象もあります。

そんなTimeTreeでは、ミッションに向かって一緒に挑戦してくれる仲間を探しています。TimeTreeで働くことに興味がある方はぜひ、Company Deck(会社紹介資料)や採用ページをご覧ください!

TimeTree Tech Blog

Discussion