Is Jetpack Compose Navigation good design for navigating? - kotlin

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

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

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

The purpose of StyleSheet.create in React Native

I wanted to ask the community about the changes in StyleSheet.create in React Native.
Before:
I have reviewed the past questions about this topic, such as this question, but they all have been answered pretty a long time ago (apart from this answer, but I wanted to have something definite) and a lot has changed since.
Before StyleSheet was creating a unique id for the styles, mainly for performance optimisations. If you wanted to get the styles out of the created styles object, you should have used the flatten method. The majority of the answers reference this flatten method and you could not access styles property as if it was a normal object.
E.g.
const styles = StyleSheet.create({
modalContainer: {
width: 100,
backgroundColor: 'white',
padding: 5,
},
You could not access the padding styles like styles.modalContainer.padding;
Currently:
However, the behaviour of this has changed. This is the source code of StyleSheet from the React Native team. Just copying the create method:
create<+S: ____Styles_Internal>(obj: S): $ObjMap<S, (Object) => any> {
// TODO: This should return S as the return type. But first,
// we need to codemod all the callsites that are typing this
// return value as a number (even though it was opaque).
if (__DEV__) {
for (const key in obj) {
StyleSheetValidation.validateStyle(key, obj);
if (obj[key]) {
Object.freeze(obj[key]);
}
}
}
return obj;
},
};
Which is just returning the object passed to create without doing anything to it. So you can actually access the styles as styles.modalContainer.padding;
Maybe I don't understand clearly the TODO, but this method has been coded this way at least since RN 0.57 and I don't know whether they are going to change it back.
My question:
Is there any sense in using StyleSheet.create anymore?
Thanks in advance for sharing your opinions!
Stylesheet is generally used to create a global style in react native and add it to the respective views which requires to style the objects.
Some widgets like TextInput, Text, Button cannot apply almost all the css styles in the react native.
So, in those cases what you can do is you can wrap those widgets with and then can create global StyleSheets using StyleSheet.create() method to globally use and reduce your headache.
So the conclusion for your question can be summarized as the Stylesheet.create() can be helpful to improve the performance while styling your multiple views using the same style will create a new object every time for each one.
While Stylesheet.create() will act as a single global object for all the views which are using it to style themselves resulting performance/memory optimisation.
I've never heard of this flatten() being necessary like you described. In fact, in the React Native repo in the very first commit, there was an example provided:
Examples/Movies/MovieCell.js:
https://github.com/facebook/react-native/commit/a15603d8f1ecdd673d80be318293cee53eb4475d#diff-4712aeb2165b3c0ce812bef903be3464
In this example, you can see var styles = StyleSheet.create({..}); being used in its present flavor and at that moment in 2016 you can see styles being referenced in the components as styles.styleName.
Additionally in the StyleSheet class here is create from the initial commit:
class StyleSheet {
static create(obj) {
var result = {};
for (var key in obj) {
StyleSheet.validateStyle(key, obj);
result[key] = StyleSheetRegistry.registerStyle(obj[key]);
}
return result;
}
// ...
}
As you see, no call to flatten on the initial commit, neither inside the create method, nor from the user using create.
In summary it seems this never changed and you could always access the styles using the dot operator.
As for whether to use it I don't think you have a choice. It clearly has some sort of validation code inside of it, it's also using type checking and the react team recommends using it. I don't see any other methods that do what it does. How could you use the class without create, just using some sort of init or constructor method? I don't see one on the class. There is no StyleSheet({...}); To get obj returned you need to call create.
Your editor could not give you IntelliSense if you strip away the validation behavior and make it a plain object. You won't know when you're making typos or referencing styles that don't exist, you won't have autocompletion. You'd need to create your own interfaces and use TypeScript. Thus you should use create because otherwise at a minimum you're breaking your IDE.

How to use ionViewDidEnter in directives

I am trying to create a directive where I animate a fab-button when the view is shown.
The animation works if it is inside ngOnInit, but due to ionic route strategy the animation doesn't work when I leave the page and go back. Putting it in ionViewDidEnter didn't work because I presume that ionViewDidEnter doesn't work inside the directive. So is there any approach I can take to solve this?
<ion-fab vertical="bottom" horizontal="end" slot="fixed">
<ion-fab-button mode="md" appAnimateFab>
<ion-icon name="create" mode="md"></ion-icon>
</ion-fab-button>
</ion-fab>`
#Directive({
selector: 'ion-fab-button[appAnimateFab]'
})
export class AnimateFabDirective implements OnInit {
constructor(
private animationBuilder: AnimationBuilder,
private element: ElementRef
) { }
ngOnInit() {
}
ionViewDidEnter() {
console.log(this.element);
const factory = this.animationBuilder.build([
style({transform: 'rotate(-45deg)'}),
animate('5s ease-in', style({transform: 'rotate(0deg)'}))
]);
const anim = factory.create(this.element.nativeElement);
anim.play();
}
}
This is an interesting question. I got halfway through writing out a detailed reply yesterday when I realised that you were actually asking about directives and not custom components... so all my research was wrong haha.
Today I have had another look. The tutorials all seem to conveniently miss having a requirement to deal with pages changing backwards and forwards and just lean on ngOnInit.
After scratching my head for a bit I started to wonder how else it could be triggered and I'm thinking: what about the Intersection Observer API?
I really like the way Alligator.io explain things:
Using the Intersection Observer API to Trigger Animations and Transitions
Their example shows the animation being triggered every time you scroll down to view.
If you are flipping pages then it feels like it should trigger as coming into view, but I haven't tested this out with code.
For a more Ionic-focused example with Intersection Observer API, Josh has a tutorial:
Animating List Items in Ionic with the Intersection Observer API | joshmorony - Learn Ionic & Build Mobile Apps with Web Tech
Maybe you can adapt this to use your animation code?

How to navigate to a different view that is not a child?

I am new to react native and new to iOS (not programming) so please excuse me if this question is a simple one. I am trying to navigate from one view to another (with a transition), however they are not related so I do not need the back navigation. I actually do not have a navigation bar at all. When using the Navigator component it seems to not support this at all. I am not sure if there is a separate way to do this but I am not able to figure it out without implementing my own hack.
If I use the navigator component and keep pushing on the views then it just keeps them all in memory and I do not want that. I can transition from one view to another and then pop but I may end up going to the wrong view in that case. I can also replace the view but it seems that does not allow for transitions.
To give you a scenario think of it like this:
Application starts and loads a "Loading" screen.
When initial loading is complete it will then go to the "Login" screen.
There is a button on the "Login" screen to "Register" or "Retrieve Password".
If they click "Register" it will take them there with a button back to "Login".
If they click "Retrieve Password" it will take them to a page with buttons to go back to "Login" or "Register".
So by this example you can see that there is no way to pop because if you were on the login screen and went to the register screen and then wanted to go the retrieve password screen then pop just simply wouldn't work. I do not want any navigation controls on the screen I just want to be able to do a smooth transition between these screens.
Now I was able to find a way to do this but I had to add a method to the Navigator class and hack code in using some of there core methods which seems like its not a good idea at all but here is the code (note this is really just a hack to see if it would work):
Navigator.prototype.pushWithUnmount = function(route) {
var activeLength = this.state.presentedIndex + 1;
var activeStack = this.state.routeStack.slice(0, activeLength);
var activeAnimationConfigStack = this.state.sceneConfigStack.slice(0, activeLength);
var nextStack = activeStack.concat([route]);
var destIndex = nextStack.length - 1;
var nextAnimationConfigStack = activeAnimationConfigStack.concat([
this.props.configureScene(route),
]);
this._emitWillFocus(nextStack[destIndex]);
this.setState({
routeStack: nextStack,
sceneConfigStack: nextAnimationConfigStack,
}, () => {
this._enableScene(destIndex);
this._transitionTo(
destIndex,
null, // default velocity
null, // no spring jumping
() => {
this.replaceAtIndex(nextStack[destIndex], 0);
this.setState({
presentedIndex: 0,
});
}
);
});
}
By using the code provided above I am now able to do:
this.props.navigator.pushWithUnmount({ component: SomeComponent });
With this code the views are pushed onto the stack with a transition and the old views are unmounted when its finished.
Please tell me that I am doing something wrong and that there is a better way to do this?
The default router with React Native is pretty limited. I'd check out React Native Router Flux. We just switched to it a few weeks ago in our product and have really liked it. It does exactly what you want.