Weird function behaviour inside composable function - kotlin

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?

Related

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

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

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")
})

viewModelScope blocks UI in Jetpack Compose

viewModelScope blocks UI in Jetpack Compose
I know viewModelScope.launch(Dispatchers.IO) {} can avoid this problem, but how to use viewModelScope.launch(Dispatchers.IO) {}?
This is my UI level code
#Composable
fun CountryContent(viewModel: CountryViewModel) {
SingleRun {
viewModel.getCountryList()
}
val pagingItems = viewModel.countryGroupList.collectAsLazyPagingItems()
// ...
}
Here is my ViewModel, Pager is my pagination
#HiltViewModel
class CountryViewModel #Inject constructor() : BaseViewModel() {
var countryGroupList = flowOf<PagingData<CountryGroup>>()
private val config = PagingConfig(pageSize = 26, prefetchDistance = 1, initialLoadSize = 26)
fun getCountryList() {
countryGroupList = Pager(config) {
CountrySource(api)
}.flow.cachedIn(viewModelScope)
}
}
This is the small package
#Composable
fun SingleRun(onClick: () -> Unit) {
val execute = rememberSaveable { mutableStateOf(true) }
if (execute.value) {
onClick()
execute.value = false
}
}
I don't use Compose much yet, so I could be wrong, but this stood out to me.
I don't think your thread is being blocked. I think you subscribed to an empty flow before replacing it, so there is no data to show.
You shouldn't use a var property for your flow, because the empty original flow could be collected before the new one replaces it. Also, it defeats the purpose of using cachedIn because the flow could be replaced multiple times.
You should eliminate the getCountryList() function and just directly assign the flow. Since it is a cachedIn flow, it doesn't do work until it is first collected anyway. See the documentation:
It won't execute any unnecessary code unless it is being collected.
So your view model should look like:
#HiltViewModel
class CountryViewModel #Inject constructor() : BaseViewModel() {
private val config = PagingConfig(pageSize = 26, prefetchDistance = 1, initialLoadSize = 26)
val countryGroupList = Pager(config) {
CountrySource(api)
}.flow.cachedIn(viewModelScope)
}
}
...and you can remove the SingleRun block from your Composable.
You are not doing anything that would require you to specify dispatchers. The default of Dispatchers.Main is fine here because you are not calling any blocking functions directly anywhere in your code.

Why does Jetpack Compose LazyColumn MutableLiveData only recompose on first button click?

Learning Kotlin and Jetpack Compose in Android Studio. Trying to understand recomposition.
The following code only recomposes on the first button click - why?
Output is shown below my code. Basically, the first time I click the Add New Item button the screen recomposes perfectly, but then it does not recompose again. However, if you look at the LogCat, you can see that it's still working, just not updating the screen. What am I missing?
View Model
class MainViewModel : ViewModel() {
val listLiveData: LiveData<List<Int>>
get() = newListLiveData
private val newList = ArrayList<Int>()
private val newListLiveData = MutableLiveData<List<Int>>()
fun addNewListItem(listItem: Int) {
newList.add(listItem)
newListLiveData.value = newList
println("**** addNewListItem($listItem)")
}
}
Home Screen Scaffolding and Content
#Composable
fun HomeScreen(
navController: NavHostController,
model: MainViewModel
) {
val stuff by model.listLiveData.observeAsState(emptyList())
Scaffold(
content = { Content(model, stuff) }
)
}
#Composable
fun Content(
model: MainViewModel = MainViewModel(),
stuff: List<Int>
) {
LazyColumn() {
items(items = stuff){ index ->
Text("$index")
}
}
Button(
onClick = { model.addNewListItem((0..10).random()) },
) {
Text(text = "Add Random Item to List")
}
}
LogCat Output
I/System.out: **** addNewListItem(0) <-- Recomposes fine here
I/System.out: **** addNewListItem(2) <-- Does not recompose
I/System.out: **** addNewListItem(5) <-- Does not recompose
I/System.out: **** addNewListItem(6) <-- Does not recompose
I/System.out: **** addNewListItem(4) <-- Does not recompose
Screenshot
Thank you
Honestly speaking, as long as you do not have to do with interoperability with the View system, AVOID using anything other than the built-for-Compose MutableState<T> objetcs. The ultra-boilerplate code of yours can simply be replaced by
ViewModel{
val cleanList by mutableStateListOf<Item>() // Initialized as an Empty list of 'Item's
fun addItem(item: Item){
cleanList.add
}
}
Within Composables,
#Composable
MyComposable(
list: List<Item>,
onAddItem: (Item) -> Unit // Receive Item, return Unit
) {
Button { // This is on-Click, assume
onAddItem(/*Add Item Here, per logic*/)
}
}
Call it anywhere like so
MyComposable(list = viewModel.cleanList, onAddItem = viewModel::addItem)
This is some fine code here. Easy to read, free of boilerplate, bafflingly beautiful and clean as a crystal.
Leverage the beauty of Compose, and ditch the ugly legacy crap.

Can't add a suspended function to a setOnClickListener in my AlertDialog

The add_button in the songToAddDialog() method won't accept my suspended method positiveButtonClick() into its setOnClickListener. I have been looking to this for hours and I do not know what to do.
// Check the GenreKey of the current playlist to use later to create the Song
suspend fun playlistGenreCheck(id: Long): Long {
val playlist = dataSource.getPlaylist(id)
val playlistGenre = playlist.playlistGenre
return playlistGenre
}
// When you press the add button in the AlertDialog it will add the song to the database and closes the AlertDialog afterwards
suspend fun positiveButtonClick(builder: AlertDialog){
val song = Song(title = builder.dialogTitleEt.toString(), artist = builder.dialogArtistEt.toString(), playlist = arguments.playlistKey, key = builder.dialogKeyEt.toString(), genre = playlistGenreCheck(arguments.playlistKey))
songsModel.addSong(song)
builder.dismiss()
}
// When you press the cancel button the AlertDialog will bet dismissed
fun negativeButtonClick(builder: AlertDialog){
builder.dismiss()
}
// This creates the AlertDialog and adds the two functions above to the buttons
fun songToAddDialog(){
val mDialogView = LayoutInflater.from(requireContext()).inflate(R.layout.add_song_dialog, null)
val mBuilder = AlertDialog.Builder(requireContext()).setView(mDialogView).setTitle("Add a Song")
val mAlertDialog = mBuilder.show()
mDialogView.add_button.setOnClickListener{positiveButtonClick(mAlertDialog)}
mDialogView.cancel_button.setOnClickListener{negativeButtonClick(mAlertDialog)}
}
// Makes the add-button inside the songslistview observable
songsModel.addButton.observe(viewLifecycleOwner, androidx.lifecycle.Observer{
if (it) {
songToAddDialog()
}
})
The Suspend function can only be called from a CoroutineScope. If you have the lifecycle dependency then use:
mDialogView.add_button.setOnClickListener{
lifecyclescope.launch{
positiveButtonClick(mAlertDialog)
}
}
If you don't have lifecycle dependencies then calling CoroutineScope like this should also work:
mDialogView.add_button.setOnClickListener{
CoroutineScope(Dispatchers.IO).launch{
positiveButtonClick(mAlertDialog)
}
}
Do tell if you still have some problem regarding this :)