Queue of snackbars jetpack Compose - kotlin

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()

Related

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

how to bind ViewModel life cycle to compose

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

v-navigation-drawer drops into a runaway loop on window resize

First, let me say that the v-navigation-drawer works as intended, i.e.:
On clicking the hamburger menu the TOGGLE_DRAWER mutation is committed, and it toggles open/closed, updating the state.
On window resize it opens/closes at a designated breakpoint
So it works.
BUT the window resize does not properly toggle the mutation and I keep getting a Vuex mutation error when I resize the window:
I understand why I'm getting this error - the $store.state.ui.drawer is being modified outside of the mutator (it's the v-navigation-drawer's v-model):
<v-navigation-drawer
v-model="$store.state.ui.drawer"
app
clipped
>
I get it's bad form to bind the state to the v-model. But when I try to make a drawer computed property with a get() and set() method that properly gets/commits a mutation, the browser crashes (presumably because the set method triggers an endless loop of commits toggling drawer true/false into infinity):
computed: {
drawer: {
get () {
return this.$store.state.ui.drawer
},
set () {
this.$store.commit('TOGGLE_DRAWER') // <--crashes the browser
}
}
}
I've searched endlessly for a solution to this problem. It's bugging me even though it visually appears to be working.
I've considered running the v-navigation-drawer in stateless mode and handling all the window resize events and state updates manually. I've also considered disabling 'Strict' mode in Vuex (which would hide the errors). But the former is a lot more complexity and the latter is a bandaid that costs me debugging insight in development.
This sounds like a perfect candidate for Lodash's debounce function. If you need to stick with using setter/getter while applying this effect, have a look at this post; otherwise, this one for sequential event subscription on any of the lifecycle hooks.
After spending some time with this, I think I have a solution. Wanted to share for anyone else that may be facing the same issue with VNavigationDrawer using Vuex state to control visibility.
The #input event passes a val parameter, which includes the state of the drawer after the window resizes. I created a new action that is called by the below function:
<v-navigation-drawer
:value="$store.state.ui.drawer"
app
clipped
#input="updateDrawer($event)"
>
Here is the action being dispatched:
methods: {
updateDrawer(event) {
if (event !== this.drawer) { // avoids dispatching duplicate actions; checks for unique window resize event
this.$store.dispatch('updateDrawer',event)
}
}
},
And the action commits the new val to the Vuex store.
Basically, the input event is able to watch for updates to the drawer, and subsequently update the drawer state if it's necessary.
You'll also see above that I stubbornly accepted using :value as the docs suggest, even though I think this should be controlled by a v-model.
Seems to be working - with the right events called and the state being updated appropriately.

I am using Backhander in react native with react-native-router-flux but its reacting on all screens where I want to make it work for screen specific

I am using Backhander in react native with react-native-router-flux but it's reacting on all screens where I want to make it work for screen-specific, but when I am trying to get the current route name in the onBackPress method, it's giving me first screen name in router name.
componentDidMount() {
BackHandler.addEventListener('hardwareBackPress', this.onBackPress);
}
componentWillUnmount() {
BackHandler.removeEventListener('hardwareBackPress', this.onBackPress);
}
onBackPress = () => {
alert(this.props.navigation.state.routeName)
}
First of all - BackHandlers in React Native are global and not screen specific. But you can achieve your wanted behavior.
Some background
With BackHandler.addEventListener you push an event listener on a Stack of event listeners, with BackHandler.removeEventListener you remove the given listener from the Stack. When the BackButton is pressed, the top listener from the stack is called and the code is executed. Then the next listener is called and so on. This stops when the first listener returns true.
For your specific problem
You should ensure that you add an event listener on the page you want it to (like you are doing in your code example)
You should ensure that your event listener returns true
You should ensure that your listener gets removed when unmounting the view (like you do)
Now you BackHandler should work for the view you have implemented it in (lets call it view1). But you have to think about all the other views. Especially when you are pushing views on top of view1. Ether you can implement an "onFocus" and "onBlur" method for view1 and use this methods instead of componentDidMount and componentWillUnmount for adding and removing event listeners, or you have to add event listeners for the back handler for all views that are pushed on top of view1.
Hope that helps :-)
Source: https://facebook.github.io/react-native/docs/backhandler
If you want backHandler to act differently for specific screen then you can use Actions.currentScene in your onBackPress function :
onBackPress = () => {
if(Actions.currentScene === 'SceneKey'){
return true;
}
Actions.pop();
return true;
}

How do I prevent a view from being placed on the navigation stack in Prism

I have the case in Prism where I support the SearchContract with View/ViewModel combination.
The problem is this: when I leave the Search Panel open and perform multiple searches, each individual search is placed on the Navigation Stack. This means that when the user hits the "GoBack" button, they see each previous search result until they reach the original form that was being displayed before search was initiated.
I want the GoBack button to navigate back to the first page prior to Search.
I see a couple of possibilities:
Change GoBack to unwind the stack until the first non-search view is
reached.
Do something in NavigateFrom to pop the current view from
the stack before Navigating away form the active search.
Do the same thing in #2 only on NavigateTo
Use Regions?
One approach is to navigate.GoBack() on the SearchPage.OnNavigatedTo event when the navigation mode is 'Back.
public override async void OnNavigatedTo(object navigationParameter, Windows.UI.Xaml.Navigation.NavigationMode navigationMode, System.Collections.Generic.Dictionary<string, object> viewModelState)
{
if (navigationMode != NavigationMode.Back)
{
base.OnNavigatedTo(navigationParameter, navigationMode, viewModelState);
// ... and so on ...
}
else
{
if (this.navigationService.CanGoBack())
{
// this call must be run on the dispatcher due to using the async-void signature on this event
await Window.Current.CoreWindow.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => this.navigationService.GoBack());
}
}
}
What's interesting here is the need to use the Dispatcher. Without it, the .GoBack() function fails. I'm not convinced this is the best answer, but it's an answer.