Mavericksの導入理由と使い方
こんにちは。2024年5月1日にTimeTreeに入社したAndroidエンジニアの笠松です。
弊社はビジネスネーム制を採用しており、社内ではRingと呼ばれています。
この記事ではTimeTreeでAndroidアプリで使用しているMavericks
というフレームワークをご紹介します。
この記事以降も、開発の過程で学んだ技術を定期的に共有していきます。同じ技術的な課題を抱えている読者や、TimeTreeへの入社を検討してくださっている方々の参考になれば幸いです。
Mavericksとは何か?
MavericksはAirbnb社が開発しているフレームワークです。 Airbnb, Tonalなど大規模アプリで採用されています。Mavericks
はAndroid Jetpack and Kotlin CoroutinesなどのAndroidで使用される標準ライブラリを補完する形で、簡単により安全で統一的な実装をサポートしてくれます。
なぜMavericksを導入しているか?
導入した理由を、Mavericks
導入前の課題から説明します。
Mavericks
導入前から
-
MVVM(Model-View-ViewModel)
を採用 -
View
(Activity
やFragment
)からAndroid Architecture Components
(AAC
)ViewModel
を呼び出す -
ViewModel
がUseCase
やRepository
を呼び出す
といった大枠の方針は決まっていました。
一方で、ViewModel
での状態の持ち方やViewModel
からView
への呼び出しについては画面ごとに差異がある状態でした。
このような背景からMavericks
によりViewModel
での状態の持ち方やViewModel
からView
への呼び出しをボイラーテンプレートを排除し、統一した方法で実装することを目的に導入しました。
現在、TimeTreeのAndroidアプリでは多くのView
とViewModel
がMavericks
により統一した方法で実装されています。
Mavericksの使い方
この節ではMavericks
の使用方法について説明します。
Activity / Fragment
Activity
とFragment
ではMavericksView
を継承して使用します。この継承により以下の処理が効率的に実現できます。
ViewModelの取得
AAC と同様にDelegated property
でViewModel
を取得できます。(AACとメソッド名が異なります)
Activity
におけるViewModel
の取得
private val viewModel: ViewModel by viewModel()
Fragment
におけるViewModel
の取得
private val viewModel: ViewModel by fragmentViewModel()
インテントへの値の設定と取得
Mavericks.KEY_ARG
をキーとしてIntent
に値を設定することで、ViewModel
で設定した値を受け取ることができます。args
はParcelable
またはSerializable
である必要があります。
intent.putExtra(Mavericks.KEY_ARG, args)
以下はIntent
に設定した値をViewModel
が持つState
のコンストラクタで受け取る例です。ViewModel
のState
の関係については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の引数の設定と取得
Fragment
のarguments
の設定と取得が可能です。
asMavericksArgs
はParcelable
またはSerializable
に対する拡張関数です。
arguments
の取得
private val args: YourArgsType by args()
arguments
の設定
fragment.arguments = yourArgs.asMavericksArgs()
ViewModel
ViewModel
はBaseMvRxViewModel<S : MavericksState>
を継承することでMavericks
を使用することができます。
標準的なViewのStateの実装
Mavericks
はView
の状態をViewModel
で保持するのための標準的な方法を提供します。このViewの状態をState
と呼び、 MavericksState
を継承することで宣言します。また、State
はBaseMvRxViewModel
のジェネリクスとして宣言します。
以下の例ではViewModel.State
がState
になります。
class ViewModel @AssistedInject constructor(
@Assisted state: State
) : BaseMvRxViewModel<ViewModel.State>(state) {
data class State(
val value: String,
) : MavericksState
}
以下のようにViewModel
内でState
を更新することができます。この処理はスレッドセーフです。処理はキューに登録され、バックグランドスレッドで実行されます。data class
のcopy
メソッドと同様に、オブジェクトの不変性を保つことができます。
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との連携
Mavericks
はAndroid
の依存関係注入ライブラリである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
Mavericks
はComposable
との連携もサポートします。
以下のように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のエンジニアによる記事です。メンバーのインタビューはこちらで発信中! note.com/timetree_inc/m/m4735531db852
Discussion