With my navigation graph I have three composable views that all share the same view model SellViewModel as shown and all the three views are interacting with the car object inside the sell view model
class SellViewModel : ViewModel() {
var car = mutableStateOf(UsedCar())
fun getMoldBrands() {...}
fun getMoldSpecs() {...}
And my navigation graph :
#Composable()
fun NavGraph() {
val navController = rememberNavController()
val vm = SellViewModel()
NavHost(navController = navController, startDestination = "splash") {
composable("splash") { Splash(navController) }
composable("home") { Home(navController) }
sellGraph(navController, vm)
}
}
fun NavGraphBuilder.sellGraph(navController: NavController , vm : SellViewModel) {
navigation(startDestination = "mold_brands", route = "sell") {
composable("mold_brands") { Brands(nav = navController, vm = vm) }
composable("Mold_specs") { Specs(nav = navController, vm = vm) }
composable("mold_images") { Images(nav = navController, vm = vm) }
}
}
I noticed that when I navigate back to the home screen my sell view model is never destroyed or disposed. and when I navigate for example again to the Brands view the car object still have its previous properties, how can i dispose this view model when i back to the home screen
Update : I updated my sell graph like below
fun NavGraphBuilder.sellGraph(navController: NavController) {
val vm = SellViewModel()
navigation(startDestination = "mold_brands", route = "sell") {
composable("mold_brands") { Brands(nav = navController, vm = vm) }
composable("mold_specs") { Specs(nav = navController, vm = vm) }
composable("mold_images") { Images(nav = navController, vm = vm) }
}
And I use in navigation to the sell views with popUpTo
nav.navigate("sell"){
popUpTo("home")
}
But the view model still in memory and never disposed !
Any ideas, help will be much appreciated
Related
Folks I am stuck engineering a proper solution to access a viewModel scoped to a nav graph , from a button that exists in the TopAppBar in a compose application
Scaffold{
TopAppBar-> Contains the Save Button
Body->
BioDataGraph() -> Contains 5 screens to gather biodata information , and a viewmodel scoped to the graph
}
}
My BioDataViewModel looks like this
class BioDataViewModel{
fun gatherPersonalInformation()
fun gatherPhotos()
...
fun onSaveEverything()
}
The issue i came across is as i described above , how should i go about access the BioDataViewModel , such that i can invoke onSaveEverything when save is clicked in the TopAppBar.
What I have tried
private val performSave by mutableStateOf(false)
Scaffold(
topBar = {
TopAppBar(currentDestination){
//save is clicked.
performSave = true
}
})
{
NavHost(
navController = navController,
startDestination = homeNavigationRoute,
modifier = Modifier
.padding(padding)
.consumedWindowInsets(padding),
) {
composable(route = bioDataRoute) {
val viewModel = hiltViewModel<BioDataViewModel>()
if (performSave){
viewModel.onSaveEverything()
}
BioDataScreen(
viewModel
)
}
}
}
The problem with the approach above is that how and when should i reset the state of performSave ? . Because if i do not set it to false; on every recomposition onSaveEverything would get called.
What would be the best way to engineer a solution for this ? . I checked to see if a similar situation was tackled in jetpack samples , but i found nothing there .
I'm not sure if I understand you correctly, but you can define the BioDataViewModel in activity level, and you can access it in the TopAppBar like this
class MyActivity: ComponentActivity() {
// BioDataViewModel definition here
private val viewModel: BioDataViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Scaffold(
topBar = {
TopAppBar(currentDestination) {
//save is clicked.
viewModel.onSaveEverything() // call onSaveEverything here
}
})
{
...
...
}
...
...
Edit:
If you want to have the same instance of ViewModel from activity and NavGraph level, you can consider this, a reference from my other answer.
You can define the ViewModelStoreOwner in the navigation graph level.
NavHost(
navController = navController,
startDestination = homeNavigationRoute,
modifier = Modifier
.padding(padding)
.consumedWindowInsets(padding),
) {
val viewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
"LocalViewModelStoreOwner not available"
}
composable(route = bioDataRoute) {
val viewModel = hiltViewModel<BioDataViewModel>(viewModelStoreOwner)
if (performSave){
viewModel.onSaveEverything()
}
BioDataScreen(
viewModel
)
}
}
I have a composable function named 'Page' as a basic composable to hold NavHost for my app, please see architecture below:
#Composable
fun Page(viewModel: LdvToolViewModel = hiltViewModel(), scaffoldState: ScaffoldState, navController: NavHostController){
val statusBarMode = viewModel.statusBarUiState
val uiController = rememberSystemUiController()
LaunchedEffect(statusBarMode){
uiController.run {
if(statusBarMode.isDarkContent){
setStatusBarColor(color = Color.White, darkIcons = true)
}else{
setStatusBarColor(color = LdvOrange, darkIcons = false)
}
}
}
val navBuilder: NavGraphBuilder.() -> Unit = {
composable(LdvPages.SEARCHING.name) { SearchUi(viewModel, scaffoldState = scaffoldState) }
composable(LdvPages.ERROR.name) { ErrorUi(viewModel,scaffoldState = scaffoldState) }
composable(LdvPages.PANEL.name) { PanelUi(scaffoldState,viewModel, mBaseViewModel) }
composable(LdvPages.PrivacyPolicy.name){ PrivacyPolicy(scaffoldState)}
composable(LdvPages.TermsOfUse.name){ TermsOfUse(scaffoldState)}
composable(LdvPages.OpenSourceLicense.name){ OpenSourceLicense(scaffoldState)}
composable(LdvPages.DebugPage.name){ DebugPage(viewModel)}
}
val start by derivedStateOf {
if (...){
LdvPages.PANEL.name }else if(...){
LdvPages.ERROR.name
}else{LdvPages.SEARCHING.name}
}
NavHost(navController = navController, startDestination = start, builder = navBuilder)
if(!isNfcEnable){
viewModel.setNfcDisableContent()
ErrorDialog(viewModel = viewModel){
startActivity(Intent(Settings.ACTION_NFC_SETTINGS));
}
}
}
As you can see that 'LdvToolViewModel' has been injected to 'Page' as hiltViewModel. To keep 'LdvToolViewModel' as one instance among lifecycles of nested-composable functions in navBuilder, I have to pass it as parameter to those functions. Is there any better way like I can somehow inject 'LdvToolViewModel' in those functions as hiltViewModel and meanwhile I can still have the injected hiltViewModel as a same instance?
Imagine you have a "HomeGraph", with "Home" as a parent destination, and few destination screens that should share the same ViewModel instance.
First get a NavBackStackEntry, by passing your parent route
val parentEntry: NavBackStackEntry = remember(navBackStackEntry) {
navController.getBackStackEntry(Destination.HomeGraph.route)
}
Then get an instance of a ViewModel by passing the parent NavBackStackEntry
val userViewModel = hiltViewModel<HomeViewModel>(parentEntry)
Also, remember that if you navigate to Destination.HomeGraph.route either from nested navigation or from a different graph a new instance of ViewModel will be created, so if you navigate within a single graph, navigate to startDestination e.g Destination.Home.route - this way you will keep the same ViewModel instance.
I don't thing we have a well-defined ViewModel sharing in compose as we had with a view system e.g by activityViewModels(), but keeping ViewModel state in graphs while user is not accessing them is a bad practice.
You can always pass the ViewModel in one of the graph extension function if necessary.
fun NavGraphBuilder.homeGraph(navController: NavHostController) {
navigation(
startDestination = Destination.Home.route,
route = Destination.HomeGraph.route
) {
composable(Destination.Home.route) { navBackStackEntry ->
val parentEntry = remember(navBackStackEntry) {
navController.getBackStackEntry(Destination.HomeGraph.route)
}
val homeViewModel = hiltViewModel<HomeViewModel>(parentEntry)
HomeRoute(
viewModel = homeViewModel,
onNavigate = { dest ->
navController.navigate(dest.route)
})
}
composable(Destination.Search.route) { navBackStackEntry ->
val parentEntry = remember(navBackStackEntry) {
navController.getBackStackEntry(Destination.HomeGraph.route)
}
val homeViewModel = hiltViewModel<HomeViewModel>(parentEntry)
UserSupportRoute(
viewModel = userViewModel,
onNavigate = { dest ->
navController.navigate(dest.route) {
popUpTo(Destination.Search.route) {
inclusive = true
}
}
})
}
}
Consider this example.
For authentication, we'll be using 2 screens - one screen to enter phone number and the other to enter OTP.
Both these screens were made in Jetpack Compose and the for the NavGraph, we are using compose navigation.
Also I have to mention that DI is being handled by Koin.
val navController = rememberNavController()
NavHost(navController) {
navigation(
startDestination = "phone_number_screen",
route = "auth"
) {
composable(route = "phone_number_screen") {
// Get's a new instance of AuthViewModel
PhoneNumberScreen(viewModel = getViewModel<AuthViewModel>())
}
composable(route = "otp_screen") {
// Get's a new instance of AuthViewModel
OTPScreen(viewModel = getViewModel<AuthViewModel>())
}
}
}
So how can we share the same viewmodel among two or more composables in a Jetpack compose NavGraph?
You can to pass your top viewModelStoreOwner to each destination
directly passing to .viewModel() call, composable("first") in my example
overriding LocalViewModelStoreOwner for the whole content, so each composable inside CompositionLocalProvider will have access to the same view models, composable("second") in my example
val viewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
"No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
}
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "first") {
composable("first") {
val model = viewModel<SharedModel>(viewModelStoreOwner = viewModelStoreOwner)
}
composable("second") {
CompositionLocalProvider(
LocalViewModelStoreOwner provides viewModelStoreOwner
) {
SecondScreen()
}
}
}
In the second case, you can get your model at any level of the composition tree, which is inside the CompositionLocalProvider:
#Composable
fun SecondScreen() {
val model = viewModel<SharedModel>()
SomeView()
}
#Composable
fun SomeView() {
val model = viewModel<SharedModel>()
}
Using Hilt you could do something like the below. But since you are using Koin I don't know the way of Koin yet.
#Composable
fun MyApp() {
NavHost(navController, startDestination = startRoute) {
navigation(startDestination = innerStartRoute, route = "Parent") {
// ...
composable("exampleWithRoute") { backStackEntry ->
val parentEntry = remember {
navController.getBackStackEntry("Parent")
}
val parentViewModel = hiltViewModel<ParentViewModel>(
parentEntry
)
ExampleWithRouteScreen(parentViewModel)
}
}
}
}
Official doc: https://developer.android.com/jetpack/compose/libraries#hilt
Here is an other way with Koin.
It strictly do the same than the validated answer but simpler to write. It will have exactly the same viewModelStoreOwner without having to write it explicitly. Please tell me if i'm wrong.
val navController = rememberNavController()
val sharedViewModel = getViewModel()
NavHost(navController = navController, startDestination = "first") {
composable("first") {
// You can use sharedViewModel
}
composable("second") {
// You can use sharedViewModel
}
}
I have used sample project from KooberApp (it is Raywenderlich book advanced architectures example project) and tried to replace custom Inversion of Control Containers and Dependency Injection code with usage of some framework.
I think most popular Dependency Injection framework for iOS is Swinject.
There I can register components for services.
I would like to resemble original app components lifespan. After some run and try, application seems to work correctly. But I am not 100% sure that approach I have used is the best, and that I haven't missed something important. I think I can still have some inconsistency with components lifespan i.e. usage of scopes .container, .graph, .transient, .weak
I someone could advise whether this Container implementation is correct or something should be fixed, changed, modified, do better?
App Container
import Swinject
import SwinjectAutoregistration
public class DIAppContainer {
public class func get() -> Container {
let container = Container()
container.register(Container.self, name: "main") { r in
return DIMainContainer.get(parent: container)
}
container.autoregister(UserSessionCoding.self, initializer: UserSessionPropertyListCoder.init)
container.autoregister(AuthRemoteAPI.self, initializer: FakeAuthRemoteAPI.init)
#if USER_SESSION_DATASTORE_FILEBASED
container.autoregister(UserSessionDataStore.self, initializer: FileUserSessionDataStore.init)
#else
container.autoregister(UserSessionDataStore.self, initializer: KeychainUserSessionDataStore.init)
#endif
container.autoregister(UserSessionRepository.self, initializer: KooberUserSessionRepository.init)
container.autoregister(MainViewModel.self, initializer: MainViewModel.init).inObjectScope(.container)
container.register(LaunchViewModel.self) { r in
return LaunchViewModel(userSessionRepository: r.resolve(UserSessionRepository.self)!, notSignedInResponder: r.resolve(MainViewModel.self)!, signedInResponder: r.resolve(MainViewModel.self)!)
}.inObjectScope(.transient)
container.register(LaunchViewController.self) { r in
let vc = LaunchViewController(viewModel: r.resolve(LaunchViewModel.self)!)
return vc
}
container.register(MainViewController.self) { r in
let vc = MainViewController( viewModel: r.resolve(MainViewModel.self)!,
launchViewController: r.resolve(LaunchViewController.self)!,
mainContainer: r.resolve(Container.self, name: "main")! )
return vc
}
return container
}
}
Main Container
public class DIMainContainer {
public class func get(parent: Container) -> Container {
let container = Container(parent: parent, defaultObjectScope: .container)
container.register(Container.self, name: "onboarding") { r in
return DIOnboardingContainer.get(parent: container)
}
container.register(Container.self, name: "signedin") { r in
return DISignedInContainer.get(parent: container)
}
container.autoregister(OnboardingViewModel.self, initializer: OnboardingViewModel.init).inObjectScope(.weak)
container.register(OnboardingViewController.self) { r in
return OnboardingViewController(viewModel: r.resolve(OnboardingViewModel.self)!, onboardingContainer: r.resolve(Container.self, name: "onboarding")! )
}.inObjectScope(.transient)
container.autoregister(SignedInViewModel.self, initializer: SignedInViewModel.init).inObjectScope(.weak)
container.register(SignedInViewController.self) { (r : Resolver, userSession : UserSession) in
return SignedInViewController(viewModel: r.resolve(SignedInViewModel.self)!, userSession: userSession, signedinContainer: r.resolve(Container.self, name: "signedin")!)
}.inObjectScope(.transient)
return container
}
}
Signed In Container
public class DISignedInContainer {
public class func get(parent: Container) -> Container {
let container = Container(parent: parent)
container.register(Container.self, name: "pickmeup") { r in
return DIPickMeUpContainer.get(parent: container)
}
//container.autoregister(SignedInViewModel.self, initializer: SignedInViewModel.init)
container.autoregister(ImageCache.self, initializer: InBundleImageCache.init)
container.autoregister(Locator.self, initializer: FakeLocator.init)
// Getting Users Location
container.register(DeterminedPickUpLocationResponder.self) { r in
return r.resolve(SignedInViewModel.self)!
}
container.register(GettingUsersLocationViewModel.self) { r in
return GettingUsersLocationViewModel(determinedPickUpLocationResponder: r.resolve(DeterminedPickUpLocationResponder.self)!, locator: r.resolve(Locator.self)!)
}
container.register(GettingUsersLocationViewController.self) { r in
return GettingUsersLocationViewController(viewModel: r.resolve(GettingUsersLocationViewModel.self)!)
}
// Pick Me Up
container.register(PickMeUpViewController.self) { (r: Resolver, location: Location) in
return PickMeUpViewController(location: location, pickMeUpContainer: r.resolve(Container.self, name: "pickmeup")! )
}
// Waiting For Pickup
container.register(WaitingForPickupViewModel.self) { r in
return WaitingForPickupViewModel(goToNewRideNavigator: r.resolve(SignedInViewModel.self)!)
}
container.register(WaitingForPickupViewController.self) { r in
return WaitingForPickupViewController(viewModel: r.resolve(WaitingForPickupViewModel.self)!)
}
// Profile
container.register(NotSignedInResponder.self) { r in
return r.resolve(MainViewModel.self)!
}
container.register(DoneWithProfileResponder.self) { r in
return r.resolve(SignedInViewModel.self)!
}
container.register(ProfileViewModel.self) { (r: Resolver, userSession: UserSession) in
return ProfileViewModel(userSession: userSession, notSignedInResponder: r.resolve(NotSignedInResponder.self)!, doneWithProfileResponder: r.resolve(DoneWithProfileResponder.self)!, userSessionRepository: r.resolve(UserSessionRepository.self)!)
}
container.register(ProfileContentViewController.self) { (r: Resolver, userSession: UserSession) in
return ProfileContentViewController(viewModel: r.resolve(ProfileViewModel.self, argument: userSession)!)
}
container.register(ProfileViewController.self) { (r: Resolver, userSession: UserSession) in
return ProfileViewController(contentViewController: r.resolve(ProfileContentViewController.self, argument: userSession)!)
}
return container
}
}
Onboarding Container
public class DIOnboardingContainer {
public class func get(parent: Container) -> Container {
let container = Container(parent: parent)
container.register(GoToSignUpNavigator.self) { r in
return r.resolve(OnboardingViewModel.self)!
}
container.register(GoToSignInNavigator.self) { r in
return r.resolve(OnboardingViewModel.self)!
}
container.autoregister(WelcomeViewModel.self, initializer: WelcomeViewModel.init).inObjectScope(.transient)
container.autoregister(WelcomeViewController.self, initializer: WelcomeViewController.init).inObjectScope(.weak)
container.register(SignedInResponder.self) { r in
return r.resolve(MainViewModel.self)!
}
container.register(SignInViewModel.self) { r in
return SignInViewModel(userSessionRepository: r.resolve(UserSessionRepository.self)!, signedInResponder: r.resolve(SignedInResponder.self)!)
}.inObjectScope(.transient)
container.autoregister(SignInViewController.self, initializer: SignInViewController.init).inObjectScope(.transient)
container.register(SignUpViewModel.self) { r in
return SignUpViewModel(userSessionRepository: r.resolve(UserSessionRepository.self)!, signedInResponder: r.resolve(SignedInResponder.self)!)
}.inObjectScope(.transient)
container.autoregister(SignUpViewController.self, initializer: SignUpViewController.init)
return container
}
}
Here App Container is created in AppDelegate, child containers are registered in parent container and init injected to view controllers and then stored in properties and used to initialize this child view controllers
let container : Container = DIAppContainer.get()
Here is MainViewController example with injected child container (Main-Scoped)
// MARK: - Main-Scoped Container
let mainContainer: Container
// MARK: - Properties
// View Model
let viewModel: MainViewModel
// Child View Controllers
let launchViewController: LaunchViewController
var signedInViewController: SignedInViewController?
var onboardingViewController: OnboardingViewController?
// State
let disposeBag = DisposeBag()
// MARK: - Methods
public init(viewModel: MainViewModel,
launchViewController: LaunchViewController,
mainContainer: Container) {
self.viewModel = viewModel
self.launchViewController = launchViewController
self.mainContainer = mainContainer
super.init()
}
I doing an application with various views types : MvxViewController, MvxTabBarViewController, ...
But when I want to do it, I meet difficulties : Following initial directives (http://bit.ly/1hLNMF3, http://bit.ly/1hNNY2g), I loose navigation back button among others.
So, I want to mix simple views and tabbed view without loosing Back button and without recode it (with NavigationItem.SetLeftBarButtonItem : http://bit.ly/1fsqGEC). Inspired by these solutions, I've do this :
A list of items (simple MvxTableController)
A detailed view of my items - MvxTabBarViewController, who have 3 tabs (1 view attached to each tab)
For the MvxTabBarViewController - main controller for item details :
public partial class SecondView : MvxTabBarViewController {
private int _count = 0;
public SecondView() {
ViewDidLoad();
}
public new SecondViewModel ViewModel {
get { return (SecondViewModel)base.ViewModel; }
set { base.ViewModel = value; }
}
public override void ViewDidLoad() {
base.ViewDidLoad();
if (ViewModel == null) return;
var viewControllers = new UIViewController[] {
CreateTabFor ("tab 1", "t1", ViewModel.Tab1);
CreateTabFor ("tab 2", "t2", ViewModel.Tab2);
CreateTabFor ("tab 3", "t3", ViewModel.Tab3);
}
ViewControllers = viewControllers;
CustomizableViewControllers = new UIViewController[0] { }
SelectedViewController = ViewControllers [0]
}
private UIViewController CreateTabFor (string tabTitle, string tabImage, IMvxViewModel viewModel) {
var controller = new UITabViewController ();
var screen = this.CreateViewControllerFor(viewModel) as UIViewController;
controller.TabBarItem = new UITabBarItem (tabTitle, UIImage.FromBundle("Images/" + tabImage + ".png"), _count);
_count++;
controller.Add (screen.View);
return controller;
}
}
Moreover, I don't need to change Setup class.