Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bind viewModel to composable lifecycle #19

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
version.lifecycleRuntimeKtx=2.4.0
version.viewModelKtx=2.4.0
version.composeViewModel=2.6.0-alpha01
3 changes: 3 additions & 0 deletions modo-render-android-compose/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ android {

kotlinOptions {
jvmTarget = "1.8"
freeCompilerArgs += "-Xjvm-default=all"
}

buildFeatures {
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -51,32 +53,51 @@ open class ComposeRenderImpl(
private val lastStackEvent: MutableState<ScreenTransitionType> = mutableStateOf(ScreenTransitionType.Idle)
private val removedScreens = mutableSetOf<Screen>()

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 {
clearStateHolder(stateHolder)
}
}
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()
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, ViewModelStore>()

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<String> = 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 <T : ViewModel> create(modelClass: Class<T>) = ModoViewModel() as T
}

fun getInstance(viewModelStore: ViewModelStore): ModoViewModel =
ViewModelProvider(viewModelStore, factory).get()
}
}
1 change: 1 addition & 0 deletions sample-android-compose/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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"]}")
}
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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")
}
}
}
}

class DetailsViewModel : ViewModel() {

init {
Log.d(this.javaClass.name, "init")
}

override fun onCleared() {
super.onCleared()
Log.d(this.javaClass.name, "onCleared")
}
}