how to bind ViewModel life cycle to compose - kotlin

I'm using Jetpack Compose now.
I realized I could have ViewModel per my composable and init view model in composable just like this:
val myViewModel:MyViewModel = viewModel()
But there is a problem that these view models will never be destroyed, even when the composable is not shown.
for instance, I have a Main composable screen that loads some other screen based on user interaction, like this:
#Composable
fun MainAuthentication(viewModel: MainViewModel) {
val state = viewModel.state.value
val scope = rememberCoroutineScope()
val scaffoldState = rememberScaffoldState()
Scaffold(scaffoldState = scaffoldState)
{
//--------------------(login and sign up button)--------------------//
Row(
modifier = Modifier
.padding(top = 50.dp)
.fillMaxSize(),
verticalAlignment = Alignment.CenterVertically,
) {
if (!state.signUpFormIsVisible && !state.loginFormIsVisible) {
Button(
onClick = {
viewModel.onEvent(event = MainEvent.LoginButtonClick)
},
modifier = Modifier
.padding(10.dp)
.weight(0.5f)
) {
Text(text = stringResource(id = R.string.login))
}
Button(
onClick = { viewModel.onEvent(event = MainEvent.SignUpButtonClick) },
modifier = Modifier
.padding(10.dp)
.weight(0.5f)
) {
Text(text = stringResource(id = R.string.signup))
}
}
}
LoginForm(show = state.loginFormIsVisible) { msg ->
scope.launch {
scaffoldState.snackbarHostState.showSnackbar(
message = msg
)
}
}
SignUpForm(show = state.signUpFormIsVisible) { msg ->
scope.launch {
scaffoldState.snackbarHostState.showSnackbar(
message = msg
)
}
}
}
}
each of the login and sign up screens has its view model and is used like this:
#Composable
fun LoginForm(show: Boolean, snackBarMsg: (String) -> Unit) {
val viewModel: LoginViewModel = viewModel()
val state = viewModel.state.value
...
AnimatedVisibility(
visible = show,
enter = slideInVertically(),
exit = slideOutVertically()
) {
...
...
}
}
How can I bind each view model to its composable function if the composable is not visible, the view model gets destroyed?
Is it a good practice to destroy the view models if the respective composable is not visible?

In Compose you can use navigation, which is perfect for your needs: each route has its own view model scope, which is destroyed as soon as the route is removed from the navigation back stack.
You can use popBackStack to remove current screen from the stack before navigation to the new screen, the old screen will be destroyed with the corresponding view model. Check out this answer on how you can remove multiple items.
Compose Navigation is based on regular Android navigation, so its documentation is relevant for most of questions, in case the Compose Navigation documentation seems short to you.

ViewModels are meant to be independent of the UIs. If the UI gets destroyed due to a configuration change, the viewmodel should remain intact and retain the state of the UI when it gets recomposed. Binding a viewmodel to every composable makes no sense. Normally, you should have only one viewmodel per screen and all the composables on that screen should use that one. But that isn't a hard rule. There are certainly composables that can and should have their own viewmodels. But they need to be managed at a higher level so that they get destroyed when the screen they appear on is no longer in use. When you navigate from the current screen to a previous screen using the Back button, you normally will want to have all the viewmodels for that screen destroyed.
There are better approaches to how composables in your UI hierarchy can access viewmodels and have them destroyed. I developed one solution that manages your viewmodels for your screens and allows composables anywhere within the hierarchy to easily access them. There is also the feature that a viewmodel can stay alive even if the screen is destroyed when you return to the previous screen. A use-case for this is if you are developing an app that hosts video conferences and you want the camera and audio to continue while you navigate back without terminating the meeting. For this, the viewmodel needs to remain alive, even though the screen itself has been destroyed. Jetpack Compose has its own solutions but I was never satisfied with them. The two biggest drawbacks with the native approach is that you cannot pass objects between screens and you need to write code to detect device configuration settings and changes to adapt the screen. I solved these with the framework I developed. Check it out:
https://github.com/JohannBlake/Jetmagic

Related

Queue of snackbars jetpack Compose

When i call Snackbar multiple times, they create a queue and occure one by one. I want a new Snachbar to immediately close previous one. Here is my code:
Scaffold(
snackbarHost = {
SnackbarHost(it) { data ->
Snackbar(
actionColor = color5,
snackbarData = data
)
}
},...
That`s what documentation says:
SnackbarHostState guarantees to show at most one snackbar at a time. If this function is called while another snackbar is already visible, it will be suspended until this snack bar is shown and subsequently addressed. If the caller is cancelled, the snackbar will be removed from display and/or the queue to be displayed.
Any ideas how can i switch off this function of SnackbarHost?
You could dismiss the previous snack bar before showing a new one. I don't know whether it is intended for this purpose, but SnackbarData interface in SnackbarHostStateclass has a dismiss() method:
snackbarHostState.currentSnackbarData?.dismiss()

mutableStateOf holding the value after the recomposition without remember API in Jetpack Compose

Any time a state is updated a recomposition takes place.
but here, I haven't used the remember API, but after the recomposition also it's holding the value, is the mutableStateOf() will remember the value without remember API?
#Composable
fun MyChildUI() {
var count by mutableStateOf(1)
Button(onClick = {
count++
println(count)
}) {
Text(text = "$count")
}
}
This is because of scoped recomposition. Any Composable that is not inline and returns Unit is a scope. Compose only triggers recomposition in nearest scope. In your example it's Button's scope. You can check out this question which is very similar
Why does mutableStateOf without remember work sometimes?
In this particular example when you click the button, only lines 42-47 will be recomposed. You can verify this by adding a log statement in line 41.
When the whole MyChildUI composable recomposes, the value of the count will be reset to 1.
So, you should use remember to avoid issues.

Is Jetpack Compose Navigation good design for navigating?

The following code are from the official sample project.
There are two branches, main and end.
I found the Code main and the Code end using different ways to navigate.
Code main is simple and clear, and in other projects, it navigate based State just like Code A which is from the project.
Code end use NavHostController to navigate, but It seems that we need't to use Navigation again when we use Jetpack Compose, right?
Code main
#Composable
fun RallyApp() {
RallyTheme {
val allScreens = RallyScreen.values().toList()
var currentScreen by rememberSaveable { mutableStateOf(RallyScreen.Overview) }
Scaffold(
...
) { innerPadding ->
Box(Modifier.padding(innerPadding)) {
currentScreen.content(
onScreenChange = { screen ->
currentScreen = RallyScreen.valueOf(screen)
}
)
}
}
}
}
enum class RallyScreen(
val icon: ImageVector,
val body: #Composable ((String) -> Unit) -> Unit
) {
Overview(
icon = Icons.Filled.PieChart,
body = { OverviewBody() }
),
Accounts(
icon = Icons.Filled.AttachMoney,
body = { AccountsBody(UserData.accounts) }
),
Bills(
icon = Icons.Filled.MoneyOff,
body = { BillsBody(UserData.bills) }
);
#Composable
fun content(onScreenChange: (String) -> Unit) {
body(onScreenChange)
}
}
Code end
#Composable
fun RallyNavHost(navController: NavHostController, modifier: Modifier = Modifier) {
NavHost(
navController = navController,
startDestination = Overview.name,
modifier = modifier
) {
composable(Overview.name) {
OverviewBody(
...
)
}
composable(Accounts.name) {
...
}
composable(Bills.name) {
...
}
}
}
enum class RallyScreen(
val icon: ImageVector,
) {
Overview(
icon = Icons.Filled.PieChart,
),
Accounts(
icon = Icons.Filled.AttachMoney,
),
Bills(
icon = Icons.Filled.MoneyOff,
);
companion object {
fun fromRoute(route: String?): RallyScreen =
when (route?.substringBefore("/")) {
Accounts.name -> Accounts
Bills.name -> Bills
Overview.name -> Overview
null -> Overview
else -> throw IllegalArgumentException("Route $route is not recognized.")
}
}
Code A
fun CraneHomeContent(
...
) {
val suggestedDestinations by viewModel.suggestedDestinations.collectAsState()
val onPeopleChanged: (Int) -> Unit = { viewModel.updatePeople(it) }
var tabSelected by remember { mutableStateOf(CraneScreen.Fly) }
BackdropScaffold(
...
frontLayerContent = {
when (tabSelected) {
CraneScreen.Fly -> {
...
}
CraneScreen.Sleep -> {
...
}
CraneScreen.Eat -> {
...
}
}
}
)
}
I've worked with Compose since the early alpha stages and became quickly disappointed with Google's lame attempt at providing a more modern approach to navigating a single-activity app. When you consider that Android's view-based system was entirely replaced with the declaration approach that Compose uses, you have to seriously wonder why they would stick with a navigation controller that doesn't allow you pass objects from one screen to another. There was also the issue that adding animation when transitioning from one screen to another was an afterthought. There is an add-on that supports animation transitions.
But perhaps the worst thing about Compose was its lack of handling device configuration changes. Under the older view-based system, you defined your layouts in xml files and placed these in resource folders that had qualifiers in the folder name that would aid Android in picking the correct layout based on things like screen density, orientation, screen size, etc. That went out the window with Compose. Eventually Google did add APIs to handle composables that need to be selected based on screen sizes and densities. But ultimately, you end up writing this decision logic within your composable and your code starts to look like spaghetti. Google's Android team completely forgot about the most basic "Separation of Concerns" when they chose to mix UI layouts with the logic that determines which layout gets selected. Designing your composables is one thing and how they get selected is another. Don't mix the two. Your composables become increasingly less reusable when you integrate those APIs. The original developers of Android (who weren't from Google) knew well enough to have the operating system manage the layout selection based upon changes to device configurations.
I chose to create my own framework that handles navigation and manages composables in almost an identical way that the view-based system works. It also remedies the issue of not being able to pass objects from screen to screen. If you are interested, you can check it out at:
https://github.com/JohannBlake/Jetmagic
So to answer you question about whether Compose Navigation is good for navigating, I would have to say no. I have worked with it and have found it severely lacking. If you want to be just-another-run-of-the-mill-Android-developer, then use Compose Navigation. But if you want to chart your own path and liberate yourself from Google's poor design practices, then use Jetmagic or even better, create your own navigation framework. It isn't that hard.
To answer your question: Code Main is not the right way to navigate.
This is an example to get started. In that case you simply hide and show Composables that's it.
Why is it better to use navigation?
Lifecylce is being handled better. Imagine you want to start a cameraX compasable screen and then you want to return back to your initial composable. Navigation component will handle and release the resources.
Composable are added to the back stack. So when you press back it automatically returns to previous screen composable.
You get this nice animation and you do not just see the next screen instantly.
I am sure there are other points as well but those are some that came up to my mind right now....
It depends on the specific use case you want to implement. In general, yes, any sort of navigation library is more suitable for navigation. It is literally in the name. Most navigation libraries, including Navigation Component, provide such essentials as:
storing the whole navigation history (the backstack) and the saved state (rememberSaveable) of every entry in the backstack. The same destination may be several times in the backstack, but they would be stored as different backstack entries. This allows storing the separate saved states for each of them.
providing separate scoped Lifecycle, ViewModelStore and SavedStateRegistry for each of them. This may be useful for allocating/cleaning up resources while the destination is visible or while it is in the backstack.
Doing the simple switching between destinations as in your Code main or Code A MAY be a valid option, but it lacks the functionality mentioned above. All your destinations use the same Lifecycle, ViewModelStore and SavedStateRegistry of the parent composable. Also, rememberSaveable wouldn't save state when switching between destinations. In some cases it might be fine, but you should be aware of the difference.
On the question whether Navigation Component for Compose is good for navigation? It's fine, but there are flaws and complications. You may read a good article by CommonsWare on the topic: Navigating in Compose: Criteria
Considering all the weirdness of the official library, I've even crafted my own navigation library: Compose Navigation Reimagined

VueX wait for modal to close before reopening with new data

I'm trying to ensure a smooth transition with a vue modal (the one in the guide). My VueX store.js file contains this mutation:
swapModalView (state, view) {
state.modalView = 'none'
state.modalView = view
}
The modal closes when the view is set to "none," and reopens when it's anything else.
<VueModal v-if="modalView != 'none'"></VueModal>
It's probably pretty simple, but how can I change the view to "none," and then set it to the view being passed after the transition ends? I would think I could just do it here, in the mutation, right?
I found it. Just basic setting up an event listener for a css transition, which I never did without copying some code somewhere.
swapModalView (state, view) {
let modal = document.getElementById('locustModal')
var launchNew = function (event) {
state.modalView = view
modal.removeEventListener('transitionend', launchNew)
}
state.modalView = 'none'
modal.addEventListener('transitionend', launchNew, false)
}

How to tear down an enhanced fragment

I am working on an existing SPA where we replace components with Aurelia components step by step. We use the enhance API of the TemplatingEngine. That works pretty well but we also need to tear down those enhanced fragments (remove event listeners, ...) when moving to another part of the application (no page reload).
My idea is to keep the aurelia instance in the page and reuse it.
Currently I enhance fragments like this:
function enhanceFragment(targetElement) {
function proceed() {
let aurelia = window.DFAurelia;
let engine = aurelia.container.get(TemplatingEngine);
engine.enhance({
container: aurelia.container,
element: targetElement,
resources: aurelia.resources
});
}
if (!window.DFAurelia) {
bootstrap(async aurelia => {
aurelia.use
.defaultBindingLanguage()
.defaultResources()
.eventAggregator()
.developmentLogging()
.globalResources('app/df-element');
await aurelia.start();
window.DFAurelia = aurelia;
proceed();
});
} else {
proceed();
}
}
The HTML I enhance looks like:
<df-element></df-element>
I tried this in a function of the custom element itself (DfElement::removeMyself()):
let vs: ViewSlot = this.container.get(ViewSlot);
let view: View = this.container.get(View);
vs.remove(view);
vs.detached();
vs.unbind();
but I get an error when getting the view from the container (Cannot read property 'resources' of undefined). I called this function from a click handler.
Main question: how to manually trigger the unbind and detached hooks of the DfElement and its children?
Bonus questions: properties of my aurelia instance (window.DFAurelia) root and host are undefined: is that a bad thing? Do you see any potential issue with this way of enhancing (and un-enhancing) fragments in the page?
Use the View returned from the enhance() method.
The enhance() method returns the enhanced View object. It is a good practice to manage the teardown from the same location where you call enhance(), as you may not be able to trust an element to remember to tear itself down. However, you can always register the View instance with the enhance container to access it from within the custom element.
function proceed() {
let aurelia = window.DFAurelia;
let container = aurelia.container;
let engine = container.get(TemplatingEngine);
let view = engine.enhance({
container: container,
element: targetElement,
resources: aurelia.resources
});
container.registerInstance(View, view);
}
This will tell the DI container to respond to calls for View with this View.
import { inject, Aurelia, View } from 'aurelia-framework';
#inject(Aurelia, Element)
export class DFCustomElement {
// element is passed to the constructor
constructor(aurelia, element) {
this.container = aurelia.container;
this.element = element;
}
// but View is only available after attached
attached() {
this.view = this.container.get(View);
}
removeMyself() {
this.element.remove();
this.view.detached();
this.view.unbind();
}
}
Using the created(view) lifecycle method
A much better practice would be to use the created(view) lifecycle method in your custom element.
import { inject } from 'aurelia-framework';
#inject(Element)
export class DFCustomElement {
constructor(element) {
this.element = element;
}
created(view) {
this.view = view;
}
removeMyself() {
this.element.remove();
this.view.detached();
this.view.unbind();
}
}
This is a much more straightforward, best practices way of having a custom element grab its own View. However, when trying to write this answer for you, I tested nesting a custom element in a <compose> element. The result was that the View in my custom element was actually the View for my <compose> element, and removeMyself() removed the <compose> entirely.