How to share different layouts depending on the current page in kotlin jetpack compose? - kotlin

I am relatively new to kotlin with jetpack compose and I am having a hard time figuring out how to share the same layout of a page among different pages. The thing is that the layout is conditional to whether the user has logged in or not (among other things).
The basic implementation I have looks something like the following code.
Now, when I click in a LoginButton in the LoginPage, it changes a isLoggedIn variable in the ViewModels, which in turns triggers recomposition and the proper layout is displayed is wrapping the actual page contents. Another example is if the "create account" button is clicked, then the SignUpLayout is wrapping the page contents.
Now, another way I can think (and like better than triggering the recomposition in all the application), is for the LoginButton to call the navigate method in the NavHostController. However I am unsure how to implement the "wrapper layouts" for this other way of doing things.
So my questions are:
Is the current way of navigating around layouts fine? Even though it triggers recomposition from the top most composable (aka ClientApp)?
If I wanted for the LoginButton to make navigation by calling the NavHostController.navigate method instead of changing a variable that triggers recomposition, how will the different layouts be handled?
Is there anything else I am missing? Maybe there is another way of accomplishing this that I am not aware of and I am just over engineering things?
enum class Page(
#StringRes val title: Int,
val layout: String,
val route: String
) {
Profile(
title = R.string.profile_page,
layout = "main",
route = "/profile"),
Home(
title = R.string.home_page,
layout = "main",
route = "/home"),
Login(
title = R.string.login_page,
layout = "none",
route = "/login"),
SignUpUserDetails(
title = R.string.user_details_page,
layout = "sign-up",
route = "/sign-up/user-details"),
SignUpAccountDetails(
title = R.string.account_details_page,
layout = "sign-up",
route = "/sign-up/account-details"),
}
#Composable
fun PageNavHost(
navHostController: NavHostController,
modifier: Modifier = Modifier
) {
NavHost(
navController = navHostController,
startDestination = Page.Home.route,
modifier = modifier
) {
composable(route = Page.Profile.route) {
ProfilePage()
}
composable(route = Page.Home.route) {
HomePage()
}
composable(route = Page.Login.route) {
val viewModel = hiltViewModel<LoginViewModel>()
LoginPage(viewModel, navHostController)
}
composable(route = Page.SignUpUser.route) {
SignUpUserPage()
}
}
}
#Composable
fun ClientApp() {
val navHostController: NavHostController = rememberNavController()
val mainViewModel = hiltViewModel<MainViewModel>()
val signUpViewModel = hiltViewModel<SignUpViewModel>()
val loginViewModel = hiltViewModel<LoginViewModel>()
val isLoggedIn by mainViewModel.sessionHolder.isLoggedIn.collectAsState()
if(!isLoggedIn) {
LoginPage(loginViewModel, navHostController)
} else {
val currentPage = navHostController.currentPage()
when (currentPage?.layout) {
"main" -> MainLayout(mainViewModel, navHostController) { content() }
"sign-up" -> SignUpLayout(navHostController, signUpViewModel) { content() }
else -> content()
}
}
}

Related

Jetpack Compose Navigation Is Triggering Recomposition

I have a simple Jetpack Compose application that uses the Navigation Component.
My UI is comprised of the following composables (redacted UI to be concise):
NavHost:
#Composable
fun NavHost(
navController: NavHostController = rememberNavController(),
startDestination: String = "main"
) {
NavHost(
navController = navController,
startDestination = startDestination) {
composable("main") {
SomeList(onNavigateToItemView = {
navController.navigate("listItem")
})
}
composable("listItem") {
ItemView()
}
}
}
SomeList:
#Composable
fun SomeList(onNavigateToItemView: () -> Unit) {
Column {
Row ( Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
Text(text = Constants.APP_TITLE, fontSize = 30.sp, fontWeight = FontWeight.Bold)
}
Box(modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
LazyColumn(
horizontalAlignment = Alignment.CenterHorizontally
) {
items(items) { item->
ItemCard(item, onNavigateToItemView)
}
}
}
}
}
ItemCard:
#Composable
fun ItemCard(item: ItemModel, onNavigateToItemView: () -> Unit) {
Card(
border = BorderStroke(2.dp, Color.Cyan),
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 5.dp)
.clickable {
onNavigateToItemView()
}
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
)
{
....
}
}
}
Now, whenever the user clicks on an ItemCard, he/she transitions to an ItemView.
What I am seeing is that the ItemView is being recomposed several times when navigating to it and also when navigating back from it.
According to the guide linked above,
The NavController's navigate function modifies the NavController's
internal state. To comply with the single source of truth principle as
much as possible, only the composable function or state holder that
hoists the NavController instance and those composable functions that
take the NavController as a parameter should make navigation calls.
Navigation events triggered from other composable functions lower in
the UI hierarchy need to expose those events to the caller
appropriately using functions.
And as you can see above, I am following that practice.
So, am I doing something wrong in my implementation or is this just how navigation and Jetpack Compose work together?
The multiple recomposition calls are affecting my UI unnecessarily.
Short Answer
Yes, navigating using compose navigation will recompose your composable each time you are navigating to it.
Workaround
When you are using navController.navigate() it will automatically recompose the targeted route. However, you can use an option to save the current state of the screen that you leave and restore the state of the screen that you target.
For example :
A->B->A->B
The first time you will load A & B you will recompose because you have no state saved yet. The moment you go from B to A (B->A) the state will be restored so you'll not recompose. Same things occurs the second time you go from A to B (A->B)
Use
Accord to documentation, you can make an extension function like this
fun NavController.popUpTo(destination: String) = navigate(destination) {
popUpTo(graph.findStartDestination().id) {
saveState = true
}
// Restore state when reselecting a previously selected item
restoreState = true
}
And use it like this
SomeList(onNavigateToItemView = {
navController.popUpTo("listItem")
})

how create custom shape in android jetpack compose?

How can I make a shape like this?
I see there two ways how I would've done this:
You can create #Composable function with Canvases (official guideline, article on Medium), if you need to use tab of this folder-like shape
// Usage:
#Composable
fun Somewhere() {
FolderLikeCard(
topTailContent = {
// for example, your tab will just be, without content, only for shape
Box(modifier = Modifier.size(64.dp, 12.dp))
},
mainContent = {
// main content
}
)
}
// Implementation:
#Composable
fun FolderLikeCard(
topTailContent: #Composable () -> Unit,
mainContent: #Composable () -> Unit
) {
val cornerSize = 4.dp // for example
Column {
Row {
Spacer(modifier = Modifier.weight(1f))
Box {
Canvas {
TODO("draw with help of links above tab's outline (using drawArc maybe)")
}
Box(
modifier = Modifier.padding(left = cornerSize, top = cornerSize, right = cornerSize),
content = topTailContent
)
}
}
Box {
Canvas {
TODO("draw main part outline")
}
Box(
modifier = Modifier.padding(left = cornerSize, bottom = cornerSize, right = cornerSize),
content = mainContent
)
}
}
}
+ I feel it can be refactored with help of Modifier.drawBehind method
Create an actual Shape for using it in Modifier.background(color = yourColor, shape = FolderLikeShape(tabHeight, tabWidth, ...)) method, link to the article that I read some time ago, + linked question helps, but I don’t see how you can then put the content there so far, if you need it, then I hope the following commentators will help with this.

Weird function behaviour inside composable function

Whenever I tell my NavGraph to start on this composable screen:
#Composable
fun LiveTrainingScreen(viewModel: LiveTrainingViewModel = viewModel(), navController: NavController) {
viewModel.context = LocalContext.current
viewModel.scope = rememberCoroutineScope()
viewModel.navController = navController
val largeVideoPlayerHandler = viewModel.InitLargeVideoDisplay(CameraLink.cockPitRight) //exoplayer saved within this handler and refrences is made within the viewModel
val smallVideoPlayerHandler = viewModel.InitSmallVideoDisplay(CameraLink.navigationAndAltitudeDisplay) //exoplayer saved within this handler and refrences is made within the viewModel
//lots of code
}
and when I want to switch the mediaType/video that is being displayed of the Exoplayer this function will work:
class LiveTrainingViewModel(application: Application) : AndroidViewModel(application) {
fun SwitchLargeVideoDisplay(cameraLinkObject: CameraLinkObject) {
UpdateLargeVideoDisplayCameraLinkObject(cameraLinkObject)
largeVideoDisplay.videoPlayer.setMediaItem(MediaItem.fromUri(cameraLinkObject.url))
largeVideoDisplay.videoPlayer.prepare()
}
}
But whenever I load this Screen from another screen this large screen update function won't work for some reason?
Does anybody know why?

Update State outside the composable function. (Jetpack compose)

I am trying to implement redux with Jetpack compose. The scenario looks like this:
I have a list view where I need to show data, in composable function.
#Composable
fun CreateListView(text: String) {
val listdata = state { store.state }
LazyColumn {
//some listview code here
}
}
above, I want to use the data that I got from the redux store. but the store. The subscription method is standalone, and outside the composable. where, though I am able to update the state through new data, but the changes are not reflecting back to composable listview:
// activity page outside composable
private fun storeSubscription(){
viewModel.storeSubscription = store.subscribe {
when (store.state) {
store.state = // list data from some source
}
}
}
Is it possible to update the composable, like above, from outside the function, and without sending any parameter? Since the redux store is a global one, so it should work I think.
You can use MutableLiveData outside of composable function. Use observeAsState() in composable to recompose when data changes.
private val myLive = MutableLiveData<String>()
fun nonComposableScope(){
myLive.postValue("hello")
}
#Composable
fun MyScreen(textLive:LiveData<String>){
val text: String? by textLive.observeAsState()
// use text here
}
Try something like,
#Composable
fun <T> Store<T>.asState(): State<T> {
val result = remember { mutableStateOf(store.state) }
DisposableEffect {
val unsubscribe = store.subscribe {
result.value = store.state
}
onDispose { unsubscribe() }
}
return result
}
#Composable
fun CreateListView(text: String) {
val listdata by store.asState()
LazyColumn {
//some listview code here
}
}
The exact code might differ as I don't know what redux implementation you are using.
This creates an observable state object that will be updated whenever the lambda passed to subscribe is called. Also, it will automatically unsubscribe when CreateListView is no longer part of the composition.
You have to follow the state hosting pattern
From Android Domcumentaiton
Key Term: State hoisting is a pattern of moving state up the tree to
make a component stateless.
When applied to composables, this often means introducing two
parameters to the composable:
value: T: the current value to display. onValueChange: (T) -> Unit: an
event that requests the value to change where T is the proposed new
value.
So in your case you will save the state in the upper Composable that needs to access it, and pass the value of the state and a lambda function to change it to the other Composable, you can learn more from the Official Documentation.
You could simply use a lambda like so:
(An example from an app I am working on.)
#OptIn(ExperimentalMaterial3Api::class)
#Composable
fun RumbleSearchResult(rumbleSearchResult: RumbleSearchResult, onClick: () -> Unit) {
ListItem(
headlineText = {
rumbleSearchResult.title?.let { title ->
Text(title)
}
},
supportingText = {
rumbleSearchResult.channel.let { creator ->
val text = when {
rumbleSearchResult.views > 0 -> {
"${creator.name}, ${rumbleSearchResult.views} views"
}
else -> {
creator.name ?: ""
}
}
Row {
Text(text)
if (creator.isVerified) {
Icon(
painter = painterResource(R.drawable.ic_baseline_verified_24),
tint = Color.Cyan,
contentDescription = stringResource(id = R.string.mainActivity_verified_content_description)
)
}
}
}
},
leadingContent = {
AsyncImage(
rumbleSearchResult.thumbnailSrc,
contentDescription = null,
modifier = Modifier.size(100.dp, 100.dp)
)
},
modifier = Modifier.clickable {
onClick.invoke()
}
)
Divider()
}
Main composable:
LazyColumn {
items(viewModel.searchResults) {
RumbleSearchResult(rumbleSearchResult = it) {
openDialog = true
}
}
}

tornadofx listview is creating one additional null listcellfragment than items in the list

I have a ViewModel for a ListView with 3 players in it:
object PlayerListViewModel : ViewModel() {
lateinit var players : ObservableList<Player>
init{
}
fun loadPlayers(){
players = Engine.selectedGame.players.asObservable()
}
}
class PlayerListView : View() {
private val vm = PlayerListViewModel
override val root = VBox()
init {
vm.loadPlayers()
root.replaceChildren {
style {
spacing = 25.px
alignment = Pos.CENTER
padding = box(0.px, 15.px)
}
listview(vm.players){
style{
background = Background.EMPTY
prefWidth = 300.px
}
isFocusTraversable = false
isMouseTransparent = true
cellFragment(PlayerCardFragment::class)
}
}
}
}
For some reason the listview is creating 4 PlayerCardFragments, with the first having a null item property and the last 3 having the correct Player item reference. This is the PlayerCardFragment definition:
class PlayerCardFragment : ListCellFragment<Player>() {
private val logger = KotlinLogging.logger { }
private val vm = PlayerViewModel().bindTo(this)
private lateinit var nameLabel : Label
private lateinit var scoreLabel : Label
override val root = hbox {
addClass(UIAppStyle.playerCard)
nameLabel = label(vm.name) { addClass(UIAppStyle.nameLabel) }
scoreLabel = label(vm.score) { addClass(UIAppStyle.scoreLabel) }
}
init {
logger.debug { "Initializing fragment for ${this.item} and ${vm.name.value}" }
EventBus.channel(EngineEvent.PlayerChanged::class)
.observeOnFx()
.subscribe() {
vm.rollback() //force viewmodel (PlayerViewModel) refresh since model (Player) does not implement observable properties
logger.debug { "${vm.name.value}'s turn is ${vm.myTurn.value}" }
root.toggleClass(UIAppStyle.selected, vm.myTurn)
}
}
When running the application, the PlayerCardFragment initializations print out "Initializing fragment for null and null" four times, but the list appears perfectly correctly with the 3 Player items. Later during execution, wnen there is an Engine.PlayerChanged event received, the Oberver function prints:
"null's turn is false"
"Adam's turn is false"
"Chad's turn is true"
"Kyle's turn is false"
These are the correct players, with the correct turn statuses. The listview appears perfectly well with the styling changes. I'm just not sure where that first null ListCellFragment is coming from.
It seems like you're trying to give ItemViewModel functionality to a view model by having everything around it change. Why not change the PlayerViewModel to have the functionality instead? Easiest way to imagine is to create bindings out of generic properties, then have them all changed and committed with by listening to the itemProperty:
class Player(var foo: String?, var bar: Int?)
class PlayerViewModel() : ItemViewModel<Player>() {
val foo = bind { SimpleStringProperty() }
val bar = bind { SimpleIntegerProperty() }
init {
itemProperty.onChange {
foo.value = it?.foo
bar.value = it?.bar
}
}
override fun onCommit() {
item?.let { player ->
player.foo = foo.value
player.bar = bar.value?.toInt()
}
}
}
Is it pretty? No. Does it keep you from having to implement an event system? Yes.