Skip to content

Commit

Permalink
feat: androidx view model scoped support too
Browse files Browse the repository at this point in the history
  • Loading branch information
Thiago dos Santos authored and Thiago dos Santos committed Sep 24, 2023
1 parent 5e1cd06 commit a6690ff
Show file tree
Hide file tree
Showing 16 changed files with 474 additions and 231 deletions.
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
package cafe.adriel.voyager.sample.androidViewModel

import androidx.compose.runtime.Composable
import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.androidx.AndroidScreen
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import cafe.adriel.voyager.sample.DetailsContent
import org.koin.androidx.compose.getViewModel
import org.koin.core.parameter.parametersOf

data class AndroidDetailsScreen(
val index: Int
Expand All @@ -15,7 +14,7 @@ data class AndroidDetailsScreen(
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val viewModel = getViewModel<AndroidDetailsViewModel> { parametersOf(index) }
val viewModel = viewModel { AndroidDetailsViewModel(index) }

DetailsContent(viewModel, "Item #${viewModel.index}", navigator::pop)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,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 +17,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 cleared with index: $index")
}

companion object {
fun provideFactory(
index: Int
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.SavedStateHandle
import androidx.lifecycle.ViewModel
import cafe.adriel.voyager.sample.sampleItems
Expand All @@ -19,4 +20,8 @@ class HiltListViewModel @Inject constructor(

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

override fun onCleared() {
Log.d(">> TAG <<", "HiltListViewModel cleared")
}
}
Original file line number Diff line number Diff line change
@@ -1,123 +1,32 @@
package cafe.adriel.voyager.androidx

import android.app.Activity
import android.app.Application
import android.content.Context
import android.content.ContextWrapper
import android.os.Bundle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.ProvidedValue
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.platform.LocalSavedStateRegistryOwner
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.HasDefaultViewModelProviderFactory
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.SAVED_STATE_REGISTRY_OWNER_KEY
import androidx.lifecycle.SavedStateViewModelFactory
import androidx.lifecycle.VIEW_MODEL_STORE_OWNER_KEY
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory
import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.ViewModelStoreOwner
import androidx.lifecycle.enableSavedStateHandles
import androidx.lifecycle.viewmodel.CreationExtras
import androidx.lifecycle.viewmodel.MutableCreationExtras
import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
import androidx.savedstate.SavedStateRegistry
import androidx.savedstate.SavedStateRegistryController
import androidx.savedstate.SavedStateRegistryOwner
import cafe.adriel.voyager.core.lifecycle.ScreenLifecycleOwner
import cafe.adriel.voyager.core.lifecycle.ScreenLifecycleStore
import cafe.adriel.voyager.core.screen.Screen
import java.util.concurrent.atomic.AtomicReference

public class AndroidScreenLifecycleOwner private constructor() :
ScreenLifecycleOwner,
LifecycleOwner,
ViewModelStoreOwner,
SavedStateRegistryOwner,
HasDefaultViewModelProviderFactory {
public class AndroidScreenLifecycleOwner : ScreenLifecycleOwner {

override val lifecycle: LifecycleRegistry = LifecycleRegistry(this)

override val viewModelStore: ViewModelStore = ViewModelStore()

private val atomicContext = AtomicReference<Context>()

private val controller = SavedStateRegistryController.create(this)

private var deactivateLifecycleListener: (() -> Unit)? = null

private var isCreated: Boolean by mutableStateOf(false)

override val savedStateRegistry: SavedStateRegistry
get() = controller.savedStateRegistry

override val defaultViewModelProviderFactory: ViewModelProvider.Factory
get() = SavedStateViewModelFactory(
application = atomicContext.get()?.applicationContext?.getApplication(),
owner = this
)

override val defaultViewModelCreationExtras: CreationExtras
get() = MutableCreationExtras().apply {
val application = atomicContext.get()?.applicationContext?.getApplication()
if (application != null) {
set(AndroidViewModelFactory.APPLICATION_KEY, application)
}
set(SAVED_STATE_REGISTRY_OWNER_KEY, this@AndroidScreenLifecycleOwner)
set(VIEW_MODEL_STORE_OWNER_KEY, this@AndroidScreenLifecycleOwner)

/* TODO if (getArguments() != null) {
extras.set<Bundle>(DEFAULT_ARGS_KEY, getArguments())
}*/
}

init {
controller.performAttach()
enableSavedStateHandles()
}

private fun onCreate(savedState: Bundle?) {
check(!isCreated) { "onCreate already called" }
isCreated = true
controller.performRestore(savedState)
initEvents.forEach {
lifecycle.handleLifecycleEvent(it)
}
}

private fun onStart() {
startEvents.forEach {
lifecycle.handleLifecycleEvent(it)
}
}

private fun onStop() {
deactivateLifecycleListener?.invoke()
deactivateLifecycleListener = null
stopEvents.forEach {
lifecycle.handleLifecycleEvent(it)
}
}
private var voyagerAndroidLifecycleOwner: VoyagerAndroidLifecycleOwner? = null

@Composable
override fun ProvideBeforeScreenContent(
provideSaveableState: @Composable (suffixKey: String, content: @Composable () -> Unit) -> Unit,
content: @Composable () -> Unit
) {
if (voyagerAndroidLifecycleOwner == null) {
voyagerAndroidLifecycleOwner = VoyagerAndroidLifecycleOwner(LocalContext.current)
}

provideSaveableState("lifecycle") {
LifecycleDisposableEffect()
voyagerAndroidLifecycleOwner!!.LifecycleDisposableEffect()

val hooks = getHooks()

Expand All @@ -128,93 +37,26 @@ public class AndroidScreenLifecycleOwner private constructor() :
}

override fun onDispose(screen: Screen) {
val context = atomicContext.getAndSet(null) ?: return
if (context is Activity && context.isChangingConfigurations) return
viewModelStore.clear()
disposeEvents.forEach {
lifecycle.handleLifecycleEvent(it)
}
}

private fun performSave(outState: Bundle) {
controller.performSave(outState)
voyagerAndroidLifecycleOwner?.dispose()
voyagerAndroidLifecycleOwner = null
}

@Composable
private fun getHooks(): List<ProvidedValue<*>> {
atomicContext.compareAndSet(null, LocalContext.current)
val androidLifecycleOwner = checkNotNull(voyagerAndroidLifecycleOwner) {
"ProvideBeforeScreenContent was not called before getHooks"
}

return remember(this) {
listOf(
LocalLifecycleOwner provides this,
LocalViewModelStoreOwner provides this,
LocalSavedStateRegistryOwner provides this
LocalLifecycleOwner provides androidLifecycleOwner,
LocalViewModelStoreOwner provides androidLifecycleOwner,
LocalSavedStateRegistryOwner provides androidLifecycleOwner
)
}
}

private fun registerLifecycleListener(outState: Bundle) {
val activity = atomicContext.get()?.getActivity()
if (activity != null && activity is LifecycleOwner) {
val observer = object : DefaultLifecycleObserver {
override fun onStop(owner: LifecycleOwner) {
performSave(outState)
}
}
val lifecycle = activity.lifecycle
lifecycle.addObserver(observer)
deactivateLifecycleListener = { lifecycle.removeObserver(observer) }
}
}

@Composable
private fun LifecycleDisposableEffect() {
val savedState = rememberSaveable { Bundle() }
if (!isCreated) {
onCreate(savedState) // do this in the UI thread to force it to be called before anything else
}

DisposableEffect(this) {
registerLifecycleListener(savedState)
onStart()
onDispose {
performSave(savedState)
onStop()
}
}
}

private tailrec fun Context.getActivity(): Activity? = when (this) {
is Activity -> this
is ContextWrapper -> baseContext.getActivity()
else -> null
}

private tailrec fun Context.getApplication(): Application? = when (this) {
is Application -> this
is ContextWrapper -> baseContext.getApplication()
else -> null
}

public companion object {

private val initEvents = arrayOf(
Lifecycle.Event.ON_CREATE
)

private val startEvents = arrayOf(
Lifecycle.Event.ON_START,
Lifecycle.Event.ON_RESUME
)

private val stopEvents = arrayOf(
Lifecycle.Event.ON_PAUSE,
Lifecycle.Event.ON_STOP
)

private val disposeEvents = arrayOf(
Lifecycle.Event.ON_DESTROY
)
public fun get(screen: Screen): ScreenLifecycleOwner {
return ScreenLifecycleStore.register(screen) { AndroidScreenLifecycleOwner() }
}
Expand Down
Loading

0 comments on commit a6690ff

Please sign in to comment.