-
Notifications
You must be signed in to change notification settings - Fork 28
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
Question: Plans on providing SavedStateHandle and Parcelize integration? #52
Comments
No concrete plans to support |
Hi, I really like how simple your multiplatform ViewModel works and would like to use your lib for my next KMP project :) Unfortunately, for my project it's necessary to pass arguments to some screens via ViewModel, e.g: ListScreen -> (user selects an item) -> DetailsScreen(id) I saw that you are already working on this, do you have an estimated release date for this feature? |
Thanks!
Could you describe more about your use case?
Not really an ETA for the SavedStateHandle support I am afraid. |
I'd like to find a multiplatform solution for ViewModels while still being able to use the SavedStateHandle on Android to survive system-initiated process death. I also want to be able to pass some parameters, like an ID to a ViewModel (e.g. the user navigates from a list of items to a specific subscreen)
Yes, that's true. But things get tricky when you use dependency injection frameworks, because the constructor arguments of the ViewModel are typically used for dependencies, not parameters. For example, on Android, when you use the Navigation Compose library, parameters such as IDs are passed to the ViewModel via the SavedStateHandle. MY SOLUTION: I found a generic solution to my use case using your library. I want to share my solution because I think it might help you think about how (or if) to integrate SavedStateHandle into your library. My shared expected ViewModel: import com.rickclephas.kmp.observableviewmodel.ViewModel as KMPViewModel
import kotlinx.coroutines.flow.MutableStateFlow
@OptIn(ExperimentalMultiplatform::class)
@OptionalExpectation
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.BINARY)
expect annotation class Parcelize()
@OptIn(ExperimentalMultiplatform::class)
@OptionalExpectation
@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.SOURCE)
expect annotation class Ignore()
expect interface Parcelable
expect class SavedStateHandle()
abstract class ViewModel<T : Parcelable>(
savedStateHandle: SavedStateHandle,
initialValue: T,
) : KMPViewModel() {
val state: MutableStateFlow<T> = stateFlow(savedStateHandle, initialValue)
}
expect fun <T> ViewModel<*>.stateFlow(
savedStateHandle: SavedStateHandle,
initialValue: T,
): MutableStateFlow<T> Actual Android implementation with real SavedStateHandle: import androidx.lifecycle.Observer
import com.rickclephas.kmp.observableviewmodel.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.withContext
actual typealias SavedStateHandle = androidx.lifecycle.SavedStateHandle
actual typealias Parcelize = kotlinx.android.parcel.Parcelize
actual typealias Parcelable = android.os.Parcelable
actual typealias Ignore = kotlinx.android.parcel.IgnoredOnParcel
actual fun <T> ViewModel<*>.stateFlow(
savedStateHandle: SavedStateHandle,
initialValue: T,
): MutableStateFlow<T> {
val liveData = savedStateHandle.getLiveData("__state", initialValue)
val stateFlow = MutableStateFlow(initialValue)
val observer = Observer<T> { value ->
if (value != stateFlow.value) {
stateFlow.value = value
}
}
liveData.observeForever(observer)
stateFlow
.onCompletion {
withContext(Dispatchers.Main.immediate) {
liveData.removeObserver(observer)
}
}
.onEach { value ->
withContext(Dispatchers.Main.immediate) {
if (liveData.value != value) {
liveData.value = value
}
}
}.launchIn(viewModelScope.coroutineScope)
return stateFlow
} Actual iOS implementation with a 'fake' SavedStateHandle: import com.rickclephas.kmp.observableviewmodel.MutableStateFlow
actual class SavedStateHandle actual constructor()
actual interface Parcelable
actual fun <T> ViewModel<*>.stateFlow(
savedStateHandle: SavedStateHandle,
initialValue: T,
) = MutableStateFlow(viewModelScope, initialValue) And here is an example ViewModel. import kotlinx.serialization.Serializable
@Serializable
data class CounterRoute(
val title: String = "Counter",
val initialCount: Int = 0,
)
@Parcelize
data class CounterState(
val title: String = "",
val count: Int = 0,
) : Parcelable
class CounterViewModel(
savedStateHandle: SavedStateHandle,
counterRoute: CounterRoute,
) : ViewModel<CounterState>(
savedStateHandle,
CounterState(
title = counterRoute.title,
count = counterRoute.initialCount,
),
) {
fun increment() {
state.value = state.value.copy(count = state.value.count + 1)
}
} Now, dependency injection is a bit tricky. I use the Kodein framework as follows: import org.kodein.di.DI
import org.kodein.di.LazyDelegate
import org.kodein.di.bindProvider
import org.kodein.di.instance
typealias ViewModelCreator<R, VM> = (SavedStateHandle, R) -> VM
typealias CounterViewModelCreator = ViewModelCreator<CounterRoute, CounterViewModel>
object DependencyInjector {
val di = DI.lazy {
// TODO Add dependencies
val counterViewModelCreator: CounterViewModelCreator = { savedStateHandle, counterRoute ->
CounterViewModel(savedStateHandle, counterRoute) // Add other dependencies ...
}
bindProvider { counterViewModelCreator }
}
/* ------------------------------ ViewModels for Android ------------------------------ */
inline fun <reified T : ViewModelCreator<*, *>> viewModelCreator(): LazyDelegate<T> {
return di.instance()
}
/* ----------------------------- ViewModels for iOS & Web ----------------------------- */
private val unused = SavedStateHandle()
fun createCounterViewModel(route: CounterRoute): CounterViewModel {
val creator: CounterViewModelCreator by di.instance()
return creator(unused, route)
}
} Now I need a custom ViewModelFactory on Android (I implemented it directly in MainActivity): private inline fun <reified VM : ViewModel, reified R : Any> NavBackStackEntry.getViewModel(): VM {
val creator: ViewModelCreator<R, VM> by DependencyInjector.viewModelCreator()
val viewModelFactory = GenericViewModelFactory { creator(savedStateHandle, toRoute()) }
return ViewModelProvider(viewModelStore, viewModelFactory)[VM::class.java]
}
private class GenericViewModelFactory<T : ViewModel>(private val creator: () -> T) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T = creator() as T
} And here is how I use it on Android: override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = CounterRoute("My Counter", 10)
) {
composable<CounterRoute> {
val viewModel = it.getViewModel<CounterViewModel, CounterRoute>()
CounterScreen(viewModel)
}
}
}
} And on iOS: @StateViewModel var viewModel = DependencyInjector.shared.createCounterViewModel(route: CounterRoute(title: "My Counter", initialCount: 32)) This is just a proof of concept, but I will soon be starting a large-scale real-world project based on this approach. Hope this helps you or anyone else using your awesome library :) |
Google helped us with this: https://android-review.googlesource.com/c/platform/frameworks/support/+/3176247 |
First of all, many thanks for your efforts in creating KMM-ViewModel :)
Are there any plans on providing integration of any kind with SavedStateHandle and Parcelize? In our project we have our own custom implementation of shared navigation logic that is integrated in our KMMViewModels. Having SavedStateHandle + Parcelize would make us able to use it to pass arguments/results while navigating
Edit: though it would probably require integration with something like navController so I'm not sure if SavedStateHandle in KMMViewModel would help us at all. But could come in handy in some other cases also.
The text was updated successfully, but these errors were encountered: