diff --git a/gradle.properties b/gradle.properties index 255511a..a408753 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ kotlin.mpp.enableGranularSourceSetsMetadata=true kotlin.native.enableDependencyPropagation=false #Android -android.compileSdk=31 +android.compileSdk=32 android.targetSdk=31 android.minSdk=21 @@ -19,4 +19,6 @@ version.coreKtx=1.7.0 version.appcompat=1.4.0 version.material=1.4.0 version.constraint=2.1.2 -version.lifecycleRuntimeKtx=2.4.0 \ No newline at end of file +version.lifecycleRuntimeKtx=2.4.0 +version.viewModelKtx=2.4.0 +version.composeViewModel=2.6.0-alpha01 \ No newline at end of file diff --git a/modo-render-android-compose/build.gradle.kts b/modo-render-android-compose/build.gradle.kts index 8c4a19e..ec23a6a 100644 --- a/modo-render-android-compose/build.gradle.kts +++ b/modo-render-android-compose/build.gradle.kts @@ -20,6 +20,7 @@ android { kotlinOptions { jvmTarget = "1.8" + freeCompilerArgs += "-Xjvm-default=all" } buildFeatures { @@ -36,6 +37,8 @@ dependencies { implementation("androidx.compose.ui:ui:${properties["version.compose"]}") implementation("androidx.compose.foundation:foundation:${properties["version.compose"]}") implementation("org.jetbrains.kotlin:kotlin-parcelize-runtime:${properties["version.kotlin"]}") + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:${properties["version.viewModelKtx"]}") + implementation("androidx.lifecycle:lifecycle-viewmodel-compose:${properties["version.composeViewModel"]}") } val sourceJar by tasks.registering(Jar::class) { diff --git a/modo-render-android-compose/src/main/java/com/github/terrakok/modo/android/compose/ComposeRender.kt b/modo-render-android-compose/src/main/java/com/github/terrakok/modo/android/compose/ComposeRender.kt index fbda0b6..bb1e45d 100644 --- a/modo-render-android-compose/src/main/java/com/github/terrakok/modo/android/compose/ComposeRender.kt +++ b/modo-render-android-compose/src/main/java/com/github/terrakok/modo/android/compose/ComposeRender.kt @@ -4,9 +4,11 @@ import android.app.Activity import androidx.compose.runtime.* import androidx.compose.runtime.saveable.SaveableStateHolder import androidx.compose.runtime.saveable.rememberSaveableStateHolder +import androidx.lifecycle.ViewModelStoreOwner import com.github.terrakok.modo.NavigationRender import com.github.terrakok.modo.NavigationState import com.github.terrakok.modo.Screen +import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner typealias RendererContent = @Composable ComposeRendererScope.() -> Unit @@ -51,17 +53,37 @@ open class ComposeRenderImpl( private val lastStackEvent: MutableState = mutableStateOf(ScreenTransitionType.Idle) private val removedScreens = mutableSetOf() + private var viewModel: ModoViewModel? = null + override fun invoke(state: NavigationState) { if (state.chain.isEmpty()) { exitAction() } lastStackEvent.value = getTransitionType(this.state.value.chain, state.chain) removedScreens.addAll(calculateRemovedScreens(this.state.value.chain, state.chain)) + + val oldState = this.state.value.chain + val newState = state.chain + if (oldState.size > newState.size) { + oldState.lastOrNull()?.let { + viewModel?.clear(it.id) + } + } + this.state.value = state } @Composable override fun Content() { + val screen = state.value.chain.lastOrNull() ?: return + if (viewModel == null) { + val owner = checkNotNull(LocalViewModelStoreOwner.current) { + "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner" + } + viewModel = ModoViewModel.getInstance(owner.viewModelStore) + } + val owner = ViewModelStoreOwner { viewModel!!.getViewModelStore(screen.id) } + val stateHolder: SaveableStateHolder = rememberSaveableStateHolder() DisposableEffect(key1 = state.value) { onDispose { @@ -69,14 +91,13 @@ open class ComposeRenderImpl( } } CompositionLocalProvider( - LocalSaveableStateHolder provides stateHolder + LocalSaveableStateHolder provides stateHolder, + LocalViewModelStoreOwner provides owner ) { - state.value.chain.lastOrNull()?.let { screen -> - require(screen is ComposeScreen) { - "ComposeRender works with ComposeScreen only! Received $screen" - } - ComposeRendererScope(screen, lastStackEvent.value).content() + require(screen is ComposeScreen) { + "ComposeRender works with ComposeScreen only! Received $screen" } + ComposeRendererScope(screen, lastStackEvent.value).content() } } diff --git a/modo-render-android-compose/src/main/java/com/github/terrakok/modo/android/compose/ModoViewModel.kt b/modo-render-android-compose/src/main/java/com/github/terrakok/modo/android/compose/ModoViewModel.kt new file mode 100644 index 0000000..c34f7dc --- /dev/null +++ b/modo-render-android-compose/src/main/java/com/github/terrakok/modo/android/compose/ModoViewModel.kt @@ -0,0 +1,53 @@ +package com.github.terrakok.modo.android.compose + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelStore +import androidx.lifecycle.get + +internal class ModoViewModel : ViewModel() { + + private val viewModelStores = mutableMapOf() + + fun clear(key: String) { + val viewModelStore = viewModelStores.remove(key) + viewModelStore?.clear() + } + + override fun onCleared() { + viewModelStores.values.forEach { it.clear() } + viewModelStores.clear() + } + + fun getViewModelStore(key: String): ViewModelStore { + var viewModelStore = viewModelStores[key] + if (viewModelStore == null) { + viewModelStore = ViewModelStore() + viewModelStores[key] = viewModelStore + } + return viewModelStore + } + + override fun toString(): String { + val sb = StringBuilder("ViewModelStores (") + val viewModelStoreIterator: Iterator = viewModelStores.keys.iterator() + while (viewModelStoreIterator.hasNext()) { + sb.append(viewModelStoreIterator.next()) + if (viewModelStoreIterator.hasNext()) { + sb.append(", ") + } + } + sb.append(')') + return sb.toString() + } + + companion object { + private val factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class) = ModoViewModel() as T + } + + fun getInstance(viewModelStore: ViewModelStore): ModoViewModel = + ViewModelProvider(viewModelStore, factory).get() + } +} diff --git a/sample-android-compose/build.gradle.kts b/sample-android-compose/build.gradle.kts index 99a82a1..0add542 100644 --- a/sample-android-compose/build.gradle.kts +++ b/sample-android-compose/build.gradle.kts @@ -43,5 +43,6 @@ dependencies { implementation("androidx.compose.material:material:${properties["version.compose"]}") implementation("androidx.compose.ui:ui-tooling:${properties["version.compose"]}") implementation("androidx.lifecycle:lifecycle-runtime-ktx:${properties["version.lifecycleRuntimeKtx"]}") + implementation("androidx.lifecycle:lifecycle-viewmodel-compose:${properties["version.composeViewModel"]}") implementation("androidx.activity:activity-compose:${properties["version.composeActivity"]}") } \ No newline at end of file diff --git a/sample-android-compose/src/main/java/com/github/terrakok/androidcomposeapp/saveable/DetailsScreen.kt b/sample-android-compose/src/main/java/com/github/terrakok/androidcomposeapp/saveable/DetailsScreen.kt index 39a0c03..5780b6c 100644 --- a/sample-android-compose/src/main/java/com/github/terrakok/androidcomposeapp/saveable/DetailsScreen.kt +++ b/sample-android-compose/src/main/java/com/github/terrakok/androidcomposeapp/saveable/DetailsScreen.kt @@ -1,14 +1,17 @@ package com.github.terrakok.androidcomposeapp.saveable +import android.util.Log import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.lifecycle.ViewModel import com.github.terrakok.modo.android.compose.ComposeScreen import com.github.terrakok.modo.android.compose.uniqueScreenKey import kotlinx.parcelize.Parcelize +import androidx.lifecycle.viewmodel.compose.viewModel @Parcelize class DetailsScreen( @@ -24,10 +27,25 @@ class DetailsScreen( } @Composable -fun ProfileDetailsScreen(id: String) { +fun ProfileDetailsScreen( + id: String, + viewModel: DetailsViewModel = viewModel() +) { Box { Column(Modifier.align(Alignment.Center)) { Text(text = "Profile details $id") } } -} \ No newline at end of file +} + +class DetailsViewModel : ViewModel() { + + init { + Log.d(this.javaClass.name, "init") + } + + override fun onCleared() { + super.onCleared() + Log.d(this.javaClass.name, "onCleared") + } +}