molecule

Activity, over time

Daily observations from the public event archive.

Attention

Daily stars & forks

Loading attention chart

Star accumulation

Cumulative stars

Loading star accumulation chart

Pull requests

Daily opens & merges

Loading pull requests chart

Conversations

Daily issues & comments

Loading conversations chart

Community

Daily accounts & reviews

Loading community chart

Releases

Daily releases

Loading releases chart

Mentions

1post · 30d
Nov 40 posts
  1. Qiita · · @Nabe1216

    Jetpack AppState TransformとStateStore AppState とは Jetpack Compose を UI レイヤーを超えて拡張し、アプリのあらゆるレイヤー間でリアクティブな状態を変換および共有できるようにするライブラリ群です。 Compose の UI で使用していた State を UI を超えて使うためのヘルパーを提供するものとなっています。 AppState 1.0.0-alpha01 時点では Transform と StateStore が用意されているので、それぞれ使い方を見ていきます。 Transform Flow のようなリアクティブデータを UI で使用可能な状態に変換したり、複数の Flow をまとめて一つの State に統合したりするのが Transform のユースケースです。 Transform が提供する API は以下の変換する処理だけを提供しています。引数の onUpdate が Composable fun になっているのが特徴で、内部で購読や LaunchedEffect の使用ができるようになっています。 (つまりは UI モジュール以外での Compose Compiler の設定や Compose の buildFeature を有効にする必要がある) public fun <R> transform( initialValue: R, scope: CoroutineScope, dispatcher: CoroutineDispatcher = Dispatchers.Default, onUpdate: @Composable (R) -> R, ): State<R> @Composable public fun <R> transform( initialValue: R, dispatcher: CoroutineDispatcher = Dispatchers.Default, onUpdate: @Composable (R) -> R, ): State<R> public suspend fun listener( dispatcher: CoroutineDispatcher = Dispatchers.Default, onUpdate: @Composable () -> Unit, ) 例えば ViewModel で Transform を利用すると次のようになります。 onUpdate が Composable fun なので networkMonitor.isOnline の State の状態更新や Flow の購読を行うことができます。 interface NetworkMonitor { val isOnline: State<Boolean> } interface MediaRepository { val allTracksFlow: Flow<List<Track>> val downloadedTracksFlow: Flow<List<Track>> } class HomeViewModel( private val mediaRepository: MediaRepository, private val networkMonitor: NetworkMonitor, ) : ViewModel() { val uiState: State<HomeUiState> = transform<HomeUiState>( initialValue = HomeUiState.Loading, scope = viewModelScope, ) { val isOnline by networkMonitor.isOnline val tracks = if (isOnline) { mediaRepository.allTracksFlow.collectAsState(initial = null).value } else { mediaRepository.downloadedTracksFlow .collectAsState(initial = null).value } ?: return@transform HomeUiState.Loading HomeUiState.Success( tracks = tracks, isOnline = isOnline, ) } } 以下は LaunchedEffect を使用した debounce の実装例です。 Composable fun を使っているので LaunchedEffect のような副作用も扱うことができます。 var query: String by mutableStateOf("") val searchResults: List<String> by transform<List<String>>( initialValue = emptyList(), scope = coroutineScope, ) { var results by remember { mutableStateOf(emptyList<String>()) } LaunchedEffect(query) { if (query.isBlank()) { results = emptyList() return@LaunchedEffect } delay(300.milliseconds) results = api.search(query) } results } StateStore State をアプリ本体だけでなくて、AppWidget であったり Glasses 、 AppFunctions などの同じプロセスのコンポーネント間で共有するのがユースケースです。 StateStore が提供する API は以下で、key と CURD の機能を提供しています。 public class StateStore { public val keys: Set<StateStoreKey<*>> public fun <T> getState( stateKey: StateStoreKey<T>, defaultValue: T = stateKey.defaultValue, ): State<T> public fun <T> setState(stateKey: StateStoreKey<T>, value: T) public inline fun <T> updateState( stateKey: StateStoreKey<T>, defaultValue: T = stateKey.defaultValue, update: (T) -> T, ) } @Serializable public open class StateStoreKey<T>( public val defaultValue: T, ) 基本的な使い方は以下のように、StateStore をインスタンス化して Repository などに渡して使います。 @Module @InstallIn(SingletonComponent::class) object AppModule { @Singleton @Provides fun provideStateStore(): StateStore = StateStore() } @Serializable private object AccessTokenKey : StateStoreKey<String>("") class AuthStateHolder @Inject constructor( private val stateStore: StateStore ) { val accessToken: State<String?> = derivedStateOf { stateStore.getState(AccessTokenKey).value.ifBlank { null } } val isLoggedIn: State<Boolean> = derivedStateOf { accessToken.value != null } fun setAccessToken(token: String) { stateStore.setState(AccessTokenKey, token) } fun clear() { stateStore.setState(AccessTokenKey, "") } } StateStore と DataStore を統合することもできます。 PersistToDataStore のインターフェースを key に実装することで、永続化すべきものをマークし、StateStore#syncToDataStore で DataStore を bind します。 @Serializable private object AccessTokenKey : StateStoreKey<String>(""), PersistToDataStore @Module @InstallIn(SingletonComponent::class) object AppModule { private val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) @Singleton @Provides fun provideStateStore( @ApplicationContext context: Context, ): StateStore = StateStore().also { store -> applicationScope.launch { store.syncToDataStore( path = dataStoreFile(context.applicationContext.filesDir, "statestore.json").absolutePath, coroutineScope = applicationScope, ) } } } UI 以外で Compose を使うことについて Jetpack Compose の State は Compose の snapshot を使った汎用的な値ホルダーで、主に UI の再構成に使用されますが State 自体は UI レンダリングとは直接関係ありません。 Compose 特有の snapshot による状態管理の恩恵をアプリのアーキテクチャ全体で受けられることが UI 以外で Compose を使うことのメリットで、AppState が提供する機能になります。 StateFlow と比較した場合は、StateFlow のように「単一の巨大なStateFlowで全てを包む(StateFlow<UiState> のような関係ない値の更新でも全て再計算されてしまう)」か、「複数の StateFlow に分ける(同時に更新できないため読み取り側が不整合な中間状態を見てしまう)」のどちらかになることを避けられるのが利点になります。 ちなみにこのアプローチは Molecule が元になっています。 - * * AppState は Compose の機能を UI 以外でも使うかどうか、で議論が生まれると思います。個人的には現状の StateFlow で破綻していないなら、無理に採用する必要はないと見ています。 StateFlow を使っていて更新競合であったり combine での分岐の難しさに遭遇しているのであれば導入を検討してみるといいと思います。 参考 - developer.android.com - android/appstate-recipes - youtu.be

    4 likes

Latest stargazers

The last 10 distinct accounts we observed this year.

Most active participants

PR, issue, comment & review events in the past year.

Packages

Scan pending.

README badges

Embed a badge in your README or website.

Live mentions · last 30 days
cashapp/molecule mentions in the last 30 days