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

feat: added support to have navigator scoped ViewModel #217

Open
wants to merge 5 commits into
base: main
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
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package cafe.adriel.voyager.sample.androidViewModel

import androidx.compose.runtime.Composable
import androidx.lifecycle.viewmodel.compose.viewModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey
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 @@ -18,7 +17,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 @@ -4,7 +4,7 @@ import androidx.compose.runtime.Composable
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.core.screen.uniqueScreenKey
import cafe.adriel.voyager.hilt.getScreenModel
import cafe.adriel.voyager.hilt.hiltViewModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import cafe.adriel.voyager.sample.DetailsContent
Expand All @@ -19,14 +19,15 @@ data class HiltDetailsScreen(
val navigator = LocalNavigator.currentOrThrow

// 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<HiltDetailsViewModel, HiltDetailsViewModel.Factory> { factory -> factory.create(index) }
val viewModel = hiltViewModel<HiltDetailsViewModel, HiltDetailsViewModel.Factory>(
viewModelFactory = { factory -> factory.create(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 dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
Expand All @@ -11,6 +12,10 @@ class HiltDetailsViewModel @AssistedInject constructor(
@Assisted val index: Int
) : ViewModel() {

override fun onCleared() {
Log.d(">> TAG <<", "HiltDetailsViewModel cleared with index: $index")
}

@AssistedFactory
interface Factory {
fun create(index: Int): HiltDetailsViewModel
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.core.screen.Screen
import cafe.adriel.voyager.hilt.getViewModel
import cafe.adriel.voyager.hilt.hiltViewModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import cafe.adriel.voyager.sample.ListContent
Expand All @@ -12,7 +12,7 @@ class HiltListScreen : Screen {
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val viewModel: HiltListViewModel = getViewModel()
val viewModel: HiltListViewModel = hiltViewModel()

// Uncomment version below if you want to use ScreenModel
// val viewModel: HiltListScreenModel = getScreenModel()
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")
}
}
3 changes: 3 additions & 0 deletions voyager-core/api/android/voyager-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ public final class cafe/adriel/voyager/androidx/AndroidScreenLifecycleOwner$Comp
public final fun get (Lcafe/adriel/voyager/core/screen/Screen;)Lcafe/adriel/voyager/core/lifecycle/ScreenLifecycleOwner;
}

public final class cafe/adriel/voyager/androidx/ContextExtKt {
}

public abstract interface annotation class cafe/adriel/voyager/core/annotation/ExperimentalVoyagerApi : java/lang/annotation/Annotation {
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package cafe.adriel.voyager.androidx

import android.app.Application
import android.content.Context
import android.content.ContextWrapper
import androidx.activity.ComponentActivity
import cafe.adriel.voyager.core.annotation.InternalVoyagerApi

@InternalVoyagerApi
public inline fun <reified T> Context.findOwner(
noinline nextFunction: (Context) -> Context? = { (it as? ContextWrapper)?.baseContext }
): T? = generateSequence(seed = this, nextFunction = nextFunction).mapNotNull { context ->
context as? T
}.firstOrNull()

@InternalVoyagerApi
public val Context.application: Application?
get() = findOwner<Application> { it.applicationContext }

@InternalVoyagerApi
public val Context.componentActivity: ComponentActivity?
get() = findOwner<ComponentActivity>()
7 changes: 3 additions & 4 deletions voyager-hilt/api/voyager-hilt.api
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ public abstract interface annotation class cafe/adriel/voyager/hilt/ScreenModelK
public abstract fun value ()Ljava/lang/Class;
}

public final class cafe/adriel/voyager/hilt/ScreenModelKt {
}

public final class cafe/adriel/voyager/hilt/VoyagerHiltViewModelFactories {
public static final field $stable I
public static final field INSTANCE Lcafe/adriel/voyager/hilt/VoyagerHiltViewModelFactories;
Expand All @@ -38,10 +41,6 @@ public final class cafe/adriel/voyager/hilt/VoyagerHiltViewModelFactories_Intern
public static fun newInstance (Ljava/util/Set;Ldagger/hilt/android/internal/builders/ViewModelComponentBuilder;)Lcafe/adriel/voyager/hilt/VoyagerHiltViewModelFactories$InternalViewModelFactory;
}

public final class cafe/adriel/voyager/hilt/internal/ContextExtKt {
public static final fun getComponentActivity (Landroid/content/Context;)Landroidx/activity/ComponentActivity;
}

public class hilt_aggregated_deps/_cafe_adriel_voyager_hilt_HiltWrapper_VoyagerHiltViewModelFactories_ViewModelFactoryEntryPoint {
public fun <init> ()V
}
Expand Down
118 changes: 69 additions & 49 deletions voyager-hilt/src/main/java/cafe/adriel/voyager/hilt/ScreenModel.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,65 @@
package cafe.adriel.voyager.hilt

import androidx.activity.ComponentActivity
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import cafe.adriel.voyager.androidx.componentActivity
import cafe.adriel.voyager.core.annotation.InternalVoyagerApi
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.rememberNavigatorScreenModel
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.hilt.internal.componentActivity
import cafe.adriel.voyager.navigator.Navigator
import dagger.hilt.android.EntryPointAccessors

@Composable
@PublishedApi
@InternalVoyagerApi
internal fun getActivity(): ComponentActivity {
val context = LocalContext.current
return remember(context) {
checkNotNull(context.componentActivity) {
"No androidx.activity.ComponentActivity found in the context: $context"
}
}
}

@PublishedApi
@InternalVoyagerApi
internal inline fun <reified T : ScreenModel> createScreenModel(
activity: ComponentActivity
): T {
val screenModels = EntryPointAccessors
.fromActivity(activity, ScreenModelEntryPoint::class.java)
.screenModels()
val model = screenModels[T::class.java]?.get()
?: error(
"${T::class.java} not found in hilt graph.\nPlease, check if you have a Multibinding " +
"declaration to your ScreenModel using @IntoMap and " +
"@ScreenModelKey(${T::class.qualifiedName}::class)"
)
return model as T
}

@PublishedApi
@InternalVoyagerApi
internal inline fun <reified T : ScreenModel, reified F : ScreenModelFactory> createScreenModelUsingFactory(
activity: ComponentActivity,
noinline factory: (F) -> T
): T {
val screenFactories = EntryPointAccessors
.fromActivity(activity, ScreenModelEntryPoint::class.java)
.screenModelFactories()
val screenFactory = screenFactories[F::class.java]?.get()
?: error(
"${F::class.java} not found in hilt graph.\nPlease, check if you have a Multibinding " +
"declaration to your ScreenModelFactory using @IntoMap and " +
"@ScreenModelFactoryKey(${F::class.qualifiedName}::class)"
)
return factory.invoke(screenFactory as F)
}

/**
* Provide a [ScreenModel] getting from Hilt graph.
*
Expand All @@ -19,18 +69,9 @@ import dagger.hilt.android.EntryPointAccessors
public inline fun <reified T : ScreenModel> Screen.getScreenModel(
tag: String? = null
): T {
val context = LocalContext.current
return rememberScreenModel(tag) {
val screenModels = EntryPointAccessors
.fromActivity(context.componentActivity, ScreenModelEntryPoint::class.java)
.screenModels()
val model = screenModels[T::class.java]?.get()
?: error(
"${T::class.java} not found in hilt graph.\nPlease, check if you have a Multibinding " +
"declaration to your ScreenModel using @IntoMap and " +
"@ScreenModelKey(${T::class.qualifiedName}::class)"
)
model as T
val activity = getActivity()
return rememberScreenModel(tag = tag) {
createScreenModel(activity)
}
}

Expand All @@ -45,18 +86,12 @@ public inline fun <reified T : ScreenModel, reified F : ScreenModelFactory> Scre
tag: String? = null,
noinline factory: (F) -> T
): T {
val context = LocalContext.current
return rememberScreenModel(tag) {
val screenFactories = EntryPointAccessors
.fromActivity(context.componentActivity, ScreenModelEntryPoint::class.java)
.screenModelFactories()
val screenFactory = screenFactories[F::class.java]?.get()
?: error(
"${F::class.java} not found in hilt graph.\nPlease, check if you have a Multibinding " +
"declaration to your ScreenModelFactory using @IntoMap and " +
"@ScreenModelFactoryKey(${F::class.qualifiedName}::class)"
)
factory.invoke(screenFactory as F)
val activity = getActivity()
return rememberScreenModel(tag = tag) {
createScreenModelUsingFactory<T, F>(
activity = activity,
factory = factory
)
}
}

Expand All @@ -69,18 +104,9 @@ public inline fun <reified T : ScreenModel, reified F : ScreenModelFactory> Scre
public inline fun <reified T : ScreenModel> Navigator.getNavigatorScreenModel(
tag: String? = null
): T {
val context = LocalContext.current
return rememberNavigatorScreenModel(tag) {
val screenModels = EntryPointAccessors
.fromActivity(context.componentActivity, ScreenModelEntryPoint::class.java)
.screenModels()
val model = screenModels[T::class.java]?.get()
?: error(
"${T::class.java} not found in hilt graph.\nPlease, check if you have a Multibinding " +
"declaration to your ScreenModel using @IntoMap and " +
"@ScreenModelKey(${T::class.qualifiedName}::class)"
)
model as T
val activity = getActivity()
return rememberNavigatorScreenModel(tag = tag) {
createScreenModel(activity = activity)
}
}

Expand All @@ -96,17 +122,11 @@ public inline fun <reified T : ScreenModel, reified F : ScreenModelFactory> Navi
tag: String? = null,
noinline factory: (F) -> T
): T {
val context = LocalContext.current
return rememberNavigatorScreenModel(tag) {
val screenFactories = EntryPointAccessors
.fromActivity(context.componentActivity, ScreenModelEntryPoint::class.java)
.screenModelFactories()
val screenFactory = screenFactories[F::class.java]?.get()
?: error(
"${F::class.java} not found in hilt graph.\nPlease, check if you have a Multibinding " +
"declaration to your ScreenModelFactory using @IntoMap and " +
"@ScreenModelFactoryKey(${F::class.qualifiedName}::class)"
)
factory.invoke(screenFactory as F)
val activity = getActivity()
return rememberNavigatorScreenModel(tag = tag) {
createScreenModelUsingFactory<T, F>(
activity = activity,
factory = factory
)
}
}
Loading