Skip to content

Commit

Permalink
feat : Compose - collectAsStateWithLifecycle (#714)
Browse files Browse the repository at this point in the history
Add Compose-related convenience functions to collect a State variable in a lifecycle-aware manner.
Mirror `collectAsState` extension functions and create the lifecycle-aware version `collectAsStateWithLifecycle`.

Add new dependency to `androidx.lifecycle:lifecycle-runtime-compose`.

Update `ComposeSampleActivity` to use one of the new `collectAsStateWithLifecycle` extension function.

Co-authored-by: Julien Pedron <[email protected]>
  • Loading branch information
JuxBzh and Julien Pedron authored Apr 16, 2024
1 parent e88e8be commit 9e999be
Show file tree
Hide file tree
Showing 5 changed files with 540 additions and 1 deletion.
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ retrofitRxJava = "com.squareup.retrofit2:adapter-rxjava2:_"
roomRuntime = "androidx.room:room-runtime:_"
roomRxJava = "androidx.room:room-rxjava2:_"
runtimeKtx = "androidx.lifecycle:lifecycle-runtime-ktx:_"
runtimeCompose = "androidx.lifecycle:lifecycle-runtime-compose:_"
rxAndroid = "io.reactivex.rxjava2:rxandroid:_"
rxJava = "io.reactivex.rxjava2:rxjava:_"
viewModelKtx = "androidx.lifecycle:lifecycle-viewmodel-ktx:_"
Expand Down
1 change: 1 addition & 0 deletions mvrx-compose/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ dependencies {
implementation libs.composeFoundation
implementation libs.composeUi
implementation libs.viewModelCompose
implementation libs.runtimeCompose
debugImplementation libs.composeUiTestManifest

testImplementation project(':mvrx-testing')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.platform.LocalView
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.savedstate.SavedStateRegistryOwner
import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.FragmentViewModelContext
Expand Down Expand Up @@ -144,6 +146,22 @@ fun <VM : MavericksViewModel<S>, S : MavericksState> VM.collectAsState(): State<
return stateFlow.collectAsState(initial = withState(this) { it })
}

/**
* Creates a Compose State variable that will emit new values whenever this ViewModel's state changes in a lifecycle-aware manner.
* Prefer the overload with a state property reference to ensure that your composable only recomposes when the properties it uses changes.
*/
@Composable
fun <VM : MavericksViewModel<S>, S : MavericksState> VM.collectAsStateWithLifecycle(
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
minActiveState: Lifecycle.State = Lifecycle.State.STARTED
): State<S> {
return stateFlow.collectAsStateWithLifecycle(
initialValue = withState(this) { it },
lifecycleOwner = lifecycleOwner,
minActiveState = minActiveState
)
}

/**
* Creates a Compose State variable that will emit new values whenever this ViewModel's state mapped to the provided mapper changes.
* Prefer the overload with a state property reference to ensure that your composable only recomposes when the properties it uses changes.
Expand All @@ -160,6 +178,31 @@ fun <VM : MavericksViewModel<S>, S : MavericksState, O> VM.collectAsState(key: A
return mappedFlow.collectAsState(initial = withState(this) { updatedMapper(it) })
}

/**
* Creates a Compose State variable that will emit new values whenever this ViewModel's state mapped to the provided mapper changes in a lifecycle-aware manner.
* Prefer the overload with a state property reference to ensure that your composable only recomposes when the properties it uses changes.
*
* @param key An optional key that should be changed if the mapper changes. If your mapper always does the same thing, you can leave this as Unit.
* If your mapper changes (for example, reading a different state property) then, by default, you won't receive an updated state value
* until either the ViewModel emits a new state or if you change the key.
* This is analogous to `remember(key) { … }`.
*/
@Composable
fun <VM : MavericksViewModel<S>, S : MavericksState, O> VM.collectAsStateWithLifecycle(
key: Any? = Unit,
mapper: (S) -> O,
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
minActiveState: Lifecycle.State = Lifecycle.State.STARTED
): State<O> {
val updatedMapper by rememberUpdatedState(mapper)
val mappedFlow = remember(key) { stateFlow.map { updatedMapper(it) }.distinctUntilChanged() }
return mappedFlow.collectAsStateWithLifecycle(
initialValue = withState(this) { updatedMapper(it) },
lifecycleOwner = lifecycleOwner,
minActiveState = minActiveState
)
}

/**
* Creates a Compose State variable that will only update when the value of this property changes.
* Prefer this to subscribing to entire state classes which will trigger a recomposition whenever any state variable changes.
Expand All @@ -170,3 +213,22 @@ fun <VM : MavericksViewModel<S>, S : MavericksState, A> VM.collectAsState(prop1:
val mappedFlow = remember(prop1) { stateFlow.map { prop1.get(it) }.distinctUntilChanged() }
return mappedFlow.collectAsState(initial = withState(this) { prop1.get(it) })
}

/**
* Creates a Compose State variable that will only update when the value of this property changes that can be collected in a lifecycle-aware manner.
* Prefer this to subscribing to entire state classes which will trigger a recomposition whenever any state variable changes.
* If you find yourself subscribing to many state properties in a single composable, consider breaking it up into smaller ones.
*/
@Composable
fun <VM : MavericksViewModel<S>, S : MavericksState, A> VM.collectAsStateWithLifecycle(
prop1: KProperty1<S, A>,
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
minActiveState: Lifecycle.State = Lifecycle.State.STARTED
): State<A> {
val mappedFlow = remember(prop1) { stateFlow.map { prop1.get(it) }.distinctUntilChanged() }
return mappedFlow.collectAsStateWithLifecycle(
initialValue = withState(this) { prop1.get(it) },
lifecycleOwner = lifecycleOwner,
minActiveState = minActiveState
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import androidx.navigation.compose.rememberNavController
import com.airbnb.mvrx.MavericksState
import com.airbnb.mvrx.MavericksViewModel
import com.airbnb.mvrx.compose.collectAsState
import com.airbnb.mvrx.compose.collectAsStateWithLifecycle
import com.airbnb.mvrx.compose.mavericksActivityViewModel
import com.airbnb.mvrx.compose.mavericksViewModel

Expand Down Expand Up @@ -94,7 +95,7 @@ class ComposeSampleActivity : AppCompatActivity() {
val activityScopedViewModel: CounterViewModel = mavericksActivityViewModel()

val navScopedCount by navScopedViewModel.collectAsState(CounterState::count)
val activityScopedCount by activityScopedViewModel.collectAsState(CounterState::count)
val activityScopedCount by activityScopedViewModel.collectAsStateWithLifecycle(CounterState::count)

Column {
Text(title)
Expand Down
Loading

0 comments on commit 9e999be

Please sign in to comment.