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

fix: hilt viewmodel onCleared not called #212

Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cafe.adriel.voyager.sample.hiltIntegration
import androidx.compose.runtime.Composable
import cafe.adriel.voyager.androidx.AndroidScreen
import cafe.adriel.voyager.hilt.getScreenModel
import cafe.adriel.voyager.hilt.getViewModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import cafe.adriel.voyager.sample.DetailsContent
Expand All @@ -17,15 +18,15 @@ data class HiltDetailsScreen(

// Uncomment version below if you want keep using ViewModel instead of to convert it to ScreenModel
// ViewModelProvider.Factory is not required. Until now Hilt has no support to Assisted Injection by default
/*val viewModel: HiltDetailsViewModel = getViewModel(
val viewModel: HiltDetailsViewModel = getViewModel(
viewModelProviderFactory = HiltDetailsViewModel.provideFactory(index)
)*/
)

// This version include more boilerplate because we are simulating support
// to Assisted Injection using ScreenModel. See [HiltListScreen] for a simple version
val viewModel = getScreenModel<HiltDetailsScreenModel, HiltDetailsScreenModel.Factory> { factory ->
/*val viewModel = getScreenModel<HiltDetailsScreenModel, HiltDetailsScreenModel.Factory> { factory ->
factory.create(index)
}
}*/

DetailsContent(viewModel, "Item #${viewModel.index}", navigator::pop)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package cafe.adriel.voyager.sample.hiltIntegration

import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider

Expand All @@ -8,6 +9,11 @@ import androidx.lifecycle.ViewModelProvider
class HiltDetailsViewModel(
val index: Int
) : ViewModel() {

override fun onCleared() {
Log.d(">> TAG <<", "HiltDetailsViewModel#$index is being cleared by onCleared()")
}

companion object {
fun provideFactory(
index: Int
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,32 @@
package cafe.adriel.voyager.sample.hiltIntegration

import android.util.Log
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import cafe.adriel.voyager.sample.sampleItems
import dagger.hilt.android.ViewModelLifecycle
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject

@HiltViewModel
class HiltListViewModel @Inject constructor(
private val handle: SavedStateHandle
private val handle: SavedStateHandle,
viewModelLifecycle: ViewModelLifecycle
) : ViewModel() {

init {
if (handle.get<List<String>>("items").isNullOrEmpty()) {
handle["items"] = sampleItems
}
viewModelLifecycle.addOnClearedListener {
Log.d(">> TAG <<", "$this is being cleared by addOnClearedListener")
}
}

val items: List<String>
get() = handle["items"] ?: error("Items not found")

override fun onCleared() {
Log.d(">> TAG <<", "$this is being cleared by onCleared()")
}
}
1 change: 1 addition & 0 deletions voyager-hilt/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ dependencies {
implementation(libs.compose.ui)
implementation(libs.lifecycle.savedState)
implementation(libs.lifecycle.viewModelKtx)
implementation(libs.lifecycle.viewModelCompose)
implementation(libs.hilt.android)
kapt(libs.hilt.compiler)

Expand Down
39 changes: 25 additions & 14 deletions voyager-hilt/src/main/java/cafe/adriel/voyager/hilt/ViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,41 +3,52 @@ package cafe.adriel.voyager.hilt
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.lifecycle.HasDefaultViewModelProviderFactory
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStore
import cafe.adriel.voyager.androidx.AndroidScreenLifecycleOwner
import cafe.adriel.voyager.core.lifecycle.ScreenLifecycleProvider
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.hilt.internal.componentActivity

/**
* A function to provide a [dagger.hilt.android.lifecycle.HiltViewModel] managed by voyager ViewModelLifecycleOwner
* instead of using Activity ViewModelLifecycleOwner.
* There is compatibility with Activity ViewModelLifecycleOwner too but it must be avoided because your ViewModels
* will be cleared when activity is totally destroyed only.
* A function to provide a [dagger.hilt.android.lifecycle.HiltViewModel] managed by the nearest [androidx.lifecycle.LifecycleOwner].
* The nearest [androidx.lifecycle.LifecycleOwner] is provide by [LocalLifecycleOwner]. So, the look up will be:
*
* 1. Getting the nearest [androidx.lifecycle.LifecycleOwner] provided by [LocalLifecycleOwner]
* 2. If there is no nearest and your [Screen] is an [cafe.adriel.voyager.androidx.AndroidScreen] than the [androidx.lifecycle.LifecycleOwner] will be [cafe.adriel.voyager.androidx.AndroidScreenLifecycleOwner]
* 3. If not an [cafe.adriel.voyager.androidx.AndroidScreen] than will look up for the topmost [LocalLifecycleOwner] available
*
* To avoid instances living more than the screen lifecycle, please provide a custom [androidx.lifecycle.LifecycleOwner] or
*
* @param viewModelProviderFactory A custom factory commonly used with Assisted Injection
* @return A new instance of [ViewModel] or the existent instance in the [ViewModelStore]
*/
@Suppress("UnusedReceiverParameter")
@Composable
public inline fun <reified T : ViewModel> Screen.getViewModel(
viewModelProviderFactory: ViewModelProvider.Factory? = null
): T {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
val viewModelStoreOwner = LocalViewModelStoreOwner.current
return remember(key1 = T::class) {
val activity = context.componentActivity
val lifecycleOwner = (this as? ScreenLifecycleProvider)
?.getLifecycleOwner() as? AndroidScreenLifecycleOwner
?: activity
val hasDefaultViewModelProviderFactory = requireNotNull(lifecycleOwner as? HasDefaultViewModelProviderFactory) {
"$lifecycleOwner is not a androidx.lifecycle.HasDefaultViewModelProviderFactory"
}
val viewModelStore = requireNotNull(viewModelStoreOwner?.viewModelStore) {
"$viewModelStoreOwner is null or have a null viewModelStore"
}
val factory = VoyagerHiltViewModelFactories.getVoyagerFactory(
activity = activity,
delegateFactory = viewModelProviderFactory ?: lifecycleOwner.defaultViewModelProviderFactory
activity = context.componentActivity,
delegateFactory = viewModelProviderFactory
?: hasDefaultViewModelProviderFactory.defaultViewModelProviderFactory
)
val provider = ViewModelProvider(
store = lifecycleOwner.viewModelStore,
store = viewModelStore,
factory = factory,
defaultCreationExtras = lifecycleOwner.defaultViewModelCreationExtras
defaultCreationExtras = hasDefaultViewModelProviderFactory.defaultViewModelCreationExtras
)
provider[T::class.java]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,8 @@ package cafe.adriel.voyager.hilt.internal
import android.content.Context
import android.content.ContextWrapper
import androidx.activity.ComponentActivity
import androidx.lifecycle.ViewModelProvider

// Unfortunately findOwner function is internal in activity-compose
// TODO: Maybe move to androidx module because we'll need this function when implement onCloseRequest support
internal inline fun <reified T> findOwner(context: Context): T? {
private inline fun <reified T> findOwner(context: Context): T? {
var innerContext = context
while (innerContext is ContextWrapper) {
if (innerContext is T) {
Expand All @@ -22,7 +19,3 @@ internal inline fun <reified T> findOwner(context: Context): T? {
internal val Context.componentActivity: ComponentActivity
get() = findOwner<ComponentActivity>(this)
?: error("Context must be a androidx.activity.ComponentActivity. Current is $this")

@PublishedApi
internal val Context.defaultViewModelProviderFactory: ViewModelProvider.Factory
get() = componentActivity.defaultViewModelProviderFactory