How to dynamically create menu in jetpack compose - kotlin

I'm wanting the menu to load the data that comes from the database. This is being static manually, does anyone have any idea how I can do this? Here's my current structure. Anything if you need more code details I can show you
My method:
#Composable
private fun DrawerContent(
scope: CoroutineScope,
scaffoldState:ScaffoldState,
navController: NavController
){
val items = listOf(
Screen.Entry1,
Screen.Entry2,
Screen.Entry3
)
Column(modifier= Modifier
.background(colorResource(id = R.color.pastel_green))
.fillMaxWidth()
.fillMaxHeight(),
horizontalAlignment = Alignment.CenterHorizontally) {
Text(
"Menu", style = MaterialTheme.typography.h5,
modifier = Modifier.padding(bottom = 20.dp,top=20.dp),
color = colorResource(id = R.color.marron)
)
val current by navController.currentBackStackEntryAsState()
val currentRoute = current?.destination
items.forEach { screen ->
var selected = currentRoute?.hierarchy?.any { it.route == screen.route } == true
val selectedColor = if (selected) colorResource(id = R.color.cinza) else Color.Transparent
val colorfont = if (selected) colorResource(id = R.color.white) else colorResource(id = R.color.cinza)
Row(modifier = Modifier
.fillMaxWidth()
.height(32.dp)
.background(selectedColor)
.clickable {
selected =
currentRoute?.hierarchy?.any { it.route == screen.route } == true
scope.launch { scaffoldState.drawerState.close() }
navController.navigate(screen.route) {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
// on the back stack as users select items
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
// Avoid multiple copies of the same destination when
// reselecting the same item
launchSingleTop = true
// Restore state when reselecting a previously selected item
restoreState = true
}
}) {
Text(stringResource(screen.resourceString),fontSize = 20.sp,
color= colorfont)
}
}
}
}
Class Screen:
sealed class Screen(val route: String, #StringRes val resourceString: Int,#IdRes val resourceId:Int) {
object Entry1 : Screen("entry1", R.string.entry1, R.id.entry1)
object Entry2 : Screen("entry2", R.string.entry2, R.id.entry2)
object Entry3 : Screen("entry3", R.string.entry3, R.id.entry3)
}

solved
My Code:
NavHost(
navController = navController,
// 1st change: Set startDestination to the exact string of route
startDestination = "entry${items[0].category_id}/{$ARG_ID}", // NOT "dynamic/1", provide arguments via defaultValue
) {
items.forEach { screen ->
composable(
route = "entry${screen.category_id}/{$ARG_ID}",
// 2nd change: Set startDestination argument via defaultValue
arguments = listOf(navArgument(ARG_ID) {
type = NavType.IntType; defaultValue =
screen.category_id
}),
) {
entryAbstract.Entry1(id = it.arguments?.getInt(ARG_ID))
}
}
}
companion object
{
const val ARG_ID = "id"
}
My method composable
#Composable
private fun DrawerContent(scope: CoroutineScope,scaffoldState:ScaffoldState
,navController: NavController
) {
Column(
modifier = Modifier
.background(colorResource(id = R.color.pastel_green))
.fillMaxWidth()
.fillMaxHeight(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
"Menu", style = MaterialTheme.typography.h5,
modifier = Modifier.padding(bottom = 20.dp, top = 20.dp),
color = colorResource(id = R.color.marron)
)
val current by navController.currentBackStackEntryAsState()
val currentRoute = current?.destination
items.forEach { screen ->
var selected = currentRoute?.hierarchy?.any { it.route == "entry${screen.category_id}/{$ARG_ID}" } == true
val selectedColor = if (selected) colorResource(id = R.color.cinza) else Color.Transparent
val colorfont = if (selected) colorResource(id = R.color.white) else colorResource(id = R.color.cinza)
Row(modifier = Modifier
.fillMaxWidth()
.height(32.dp)
.background(selectedColor)
.clickable {
selected =
currentRoute?.hierarchy?.any { it.route == "entry${screen.category_id}/{$ARG_ID}" } == true
scope.launch { scaffoldState.drawerState.close() }
navController.navigate("entry${screen.category_id}/${screen.category_id}") {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
// on the back stack as users select items
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
// Avoid multiple copies of the same destination when
// reselecting the same item
launchSingleTop = true
// Restore state when reselecting a previously selected item
restoreState = true
}
}) {
Text(screen.category_name, fontSize = 20.sp, color = colorfont)
}
}
}
}

Related

How to load bitmap Images in a List faster, Using jetpack compose Android

I am trying to load a list of object in a column using jetpack compose.
The problem is the image is making screen lag.
Here's my code - single list item composable function.
fun SoulProfilePreviewSingleItem(
soul: Souls,
soulWinningViewModel: SoulWinningViewModel,
onClick: () -> Unit
) {
val lazyListState = rememberLazyListState()
/* val localImage = if (soul.localImage.isNotBlank())
rememberImagePainter(getBase64StringToImage(soul.localImage)) else null*/
LaunchedEffect(key1 = soulWinningViewModel.nurtureVsFollowUpTabIndex.value) {
lazyListState.animateScrollToItem(0)
// if (soul.localImage.isNotBlank()){
// }
}
val isManaging = soulWinningViewModel.isManagingList.value
var isDeleted by remember {
mutableStateOf(false)
}
val setVisibility: (Boolean) -> Unit = {
isDeleted = it
}
AnimatedVisibility(
visible = isDeleted.not() &&
if (soulWinningViewModel.userIsSearchingFor.value.isNotEmpty() && isManaging.not())
soul.name.contains(soulWinningViewModel.userIsSearchingFor.value, true) ||
soul.followUpActions.toString()
.contains(soulWinningViewModel.userIsSearchingFor.value, true) else
true
) {
with(soul) {
Column {
Box(
modifier = Modifier
.height(60.dp)
.background(MaterialTheme.colorScheme.background)
.fillMaxWidth(),
contentAlignment = Alignment.BottomEnd
) {
Row(
Modifier
.fillMaxSize(),//.clickable { onClick() },
verticalAlignment = Alignment.CenterVertically
) {
Row(
verticalAlignment = Alignment.Top,
modifier = Modifier
.padding(start = brc_DP_small)
.padding(bottom = 6.dp, top = 6.dp)
) {
/*if (soul.localImage.isNotBlank()) {
GlideImage(
model = ,
// Crop, Fit, Inside, FillHeight, FillWidth, None
contentScale = ContentScale.Crop,
// shows an image with a circular revealed animation.
)
Log.e("BITMAP","onResourceReady test")
val image = loadPicture(url = soul.localImage, defaultImage = R.drawable.ic_brc_logo).value
image?.let { img->
Image(
// Can crash this
bitmap =img.asImageBitmap(),
contentDescription = null,
modifier = Modifier
.clip(RoundedCornerShape(50))
.size(30.dp),
contentScale = ContentScale.Crop
)
}
}*/
if (soul.localImage.isNotBlank()) {
// val imageLoader = rememberImagePainter(getBase64StringToImage(soul.localImage))
Image(
// painter = imageLoader
// Can crash this
bitmap = (
getBase64StringToImage(soul.localImage).asImageBitmap()
),
contentDescription = null,
modifier = Modifier
.clip(RoundedCornerShape(50))
.size(30.dp),
contentScale = ContentScale.Crop
)
} else {
Image(
// Can crash this
painterResource(id = R.drawable.ic_brc_logo),
contentDescription = null,
modifier = Modifier
.clip(RoundedCornerShape(50))
.size(30.dp),
contentScale = ContentScale.Crop
)
}
}
Column(
modifier = Modifier
.padding(top = brc_DP_small)
.weight(1f)
.fillMaxWidth()
.fillMaxHeight()
.padding(start = brc_DP_smaller),
verticalArrangement = Arrangement.SpaceBetween
) {
Column(
modifier = Modifier
.weight(1f)
.fillMaxSize()
) {
// This is the place where signle soul is getting showed......
Text(
text = name,
style = if (isManaging.not()) MaterialTheme.typography.labelSmall.copy(
fontWeight = FontWeight(500),
letterSpacing = 0.7.sp
)
else MaterialTheme.typography.labelSmall
.copy(
fontSize = 10.sp
),
modifier = Modifier,
maxLines = 1,
)
if (isManaging.not()) {
Text(
text = SimpleDateFormat(
"dd-MMM-yyyy",
Locale.UK
).format(
createdOn
),
style = MaterialTheme.typography.bodySmall.copy(fontSize = 9.sp),
color = Color.Gray,
modifier = Modifier
)
}
}
if (isBrcMember) {
Text(
text = "BRC Member",
style = MaterialTheme.typography.labelSmall.copy(
fontSize = 7.sp,
fontStyle = FontStyle.Italic
),
color = brc_blue_color,
modifier = Modifier.padding(bottom = brc_DP_smallest)
)
}
}
}
var externClick by remember {
mutableStateOf(false)
}
Column(modifier = Modifier.padding(brc_DP_small)) {
AddSoulFollowUpActionButton(
soul = soul,
soulWinningViewModel = soulWinningViewModel,
visible = soulWinningViewModel.nurtureVsFollowUpTabIndex.value == 1,
externClick = externClick,
onFinished = { externClick = false }
) {
setVisibility(it)
}
}
//TODO:Check Click area ...
Row(modifier = Modifier.fillMaxSize()) {
Spacer(
modifier = Modifier
.fillMaxSize()
.weight(1.4f)
.clickable(
interactionSource = MutableInteractionSource(),
indication = null
) { onClick() })
if (soulWinningViewModel.nurtureVsFollowUpTabIndex.value == 1 || isManaging) {
Spacer(
modifier = Modifier
.fillMaxSize()
.weight(0.6f)
.clickable(
interactionSource = MutableInteractionSource(),
indication = null
) { externClick = !externClick })
}
}
}
Spacer(
modifier = Modifier
.padding(start = 28.dp)
.fillMaxWidth() //.then(if (isManaging.not()) Modifier.width(150.dp) else Modifier.fillMaxWidth())
.height(0.3.dp)
.background(Color.LightGray)
)
}
}
}
}
caller of list composable function.
forEach {
SoulProfilePreviewSingleItem(
soul = it,
soulWinningViewModel
) {
soulWinningViewModel.currentSoul.value = it
navController.navigate(route = SoulWinningRoutes.PROFILE)
}
}
Whenever I am trying to show Image() of my soul object I am getting glitches and my screens takes more time to load itself.
I am storing image as string in local room database and trying to retrieve it back converting it to bitmapImage.
Code for conversation.
fun getBase64StringToImage(base64String: String): Bitmap {
val decodedString: ByteArray = Base64.decode(base64String, Base64.DEFAULT)
val decodedByte = BitmapFactory.decodeByteArray(decodedString, 0, decodedString.size)
return decodedByte
// this is taking a lot of time and making my Ui performance dull.
}
Is there any way I can achieve this functionality with good performance. Like loading Image in separate scope or anything else.
I have already tried using rememberImagePainter and Glide Coil. rememberImagePainter making my performance worst.
Please feel free to give me suggestions, I am still in learning stage.
Thanks in advance.

Observing viewmodel's liveData as state doesn't trigger a recomposition of the composable

A colleague of mine has written the following code:
Viewmodel:
class CallMeBackViewModel(
private val ewayControllerUseCase: EwayControllerUseCase,
private val accountControllerUseCase: AccountControllerUseCase
) : ViewModel() {
val accountDetails = MutableLiveData<AccountDetailsResponse>()
val contactForm = CallMeBackForm()
init {
val accountId = App.instance.appContext.getPreferenceThroughKeystore(PreferenceKeys.ACCOUNT_ID.key)
contactForm.timeframe = "09:00 - 11:00"
if (accountId != "null") {
getAccountDetails(accountId)
}
}
private fun getAccountDetails(accountId: String) {
viewModelScope.launch(Dispatchers.IO) {
when (val response = accountControllerUseCase.getAccountDetails(accountId)) {
is ResponseResult.Success -> {
accountDetails.postValue(response.data!!)
fillContactFormByAccount(response.data!!)
}
is ResponseResult.Error -> {
Log.d("GET_ACCOUNT_DETAILS_ERROR", "Network Call Failed With Exception: " + response.exception.message)
}
}
}
}
private fun fillContactFormByAccount(accountDetailsResponse: AccountDetailsResponse) {
contactForm.firstName = accountDetailsResponse.firstName!!
contactForm.lastName = accountDetailsResponse.lastName!!
contactForm.phoneNumber = accountDetailsResponse.phone1!!
contactForm.accountId = accountDetailsResponse.accountId!!
}
#ExperimentalFoundationApi
#ExperimentalMaterialApi
fun submitCallbackForm(activity: MainActivity) {
viewModelScope.launch(Dispatchers.IO) {
when (val response = ewayControllerUseCase.submitCallbackForm(contactForm)) {
is ResponseResult.Success -> {
onSuccessfulSubmission(activity)
}
is ResponseResult.Error -> {
Log.d("CALLBACK_FORM_SUBMISSION_ERROR", "Network Call Failed With Exception: " + response.exception.message)
}
}
}
}
#ExperimentalFoundationApi
#ExperimentalMaterialApi
private suspend fun onSuccessfulSubmission(activity: MainActivity) {
withContext(Dispatchers.Main) {
activity.onBackPressed()
}
}
}
He basically fetches some form data and when received, updates the accountDetails live data with them. Now in the composable screen he observes this field as a state like so :
Screen:
#ExperimentalFoundationApi
#ExperimentalMaterialApi
#Composable
fun ContactCallMeBack() {
val viewModel: CallMeBackViewModel = remember { getKoin().get() }
val accountDetails by viewModel.accountDetails.observeAsState(null)
val canProceed = remember { mutableStateOf(false) }
val textChangedToggle = remember { mutableStateOf(false) }
val activity = LocalContext.current as MainActivity
LaunchedEffect(textChangedToggle.value) {
canProceed.value = canProceed(viewModel.contactForm)
}
Column(modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.verticalScroll(rememberScrollState())
) {
TopBarPadding(true)
Spacer(modifier = Modifier.padding(24.dp))
if (accountDetails != null) {
Form(accountDetails, viewModel, textChangedToggle)
}
else {
Form(accountDetails, viewModel, textChangedToggle)
}
Spacer(modifier = Modifier.padding(42.fixedDp()))
MyButton(text = "Καλέστε με",
buttonType = if (canProceed.value) {
MyButtonType.PRIMARY
} else {
MyButtonType.DISABLED
},
onClick = { viewModel.submitCallbackForm(activity) })
Spacer(modifier = Modifier.padding(49.fixedDp()))
}
}
#Composable
private fun Form(
accountDetails: AccountDetailsResponse?,
viewModel: CallMeBackViewModel,
textChangedToggle: MutableState<Boolean>
) {
Column(modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(13.dp))
.background(Color.White)
.padding(start = 16.dp, end = 16.dp, top = 22.dp, bottom = 24.dp)
) {
InputField(label = "Κωδικός Συνδρομητή",
initialValue = accountDetails?.accountId ?: "",
onTextChangeCallback = {
viewModel.contactForm.accountId = it
triggerCheck(textChangedToggle)
}
)
Spacer(modifier = Modifier.padding(18.fixedDp()))
InputField(label = "Όνομα",
initialValue = accountDetails?.firstName ?: "",
onTextChangeCallback = {
viewModel.contactForm.firstName = it
triggerCheck(textChangedToggle)
}
)
Spacer(modifier = Modifier.padding(18.fixedDp()))
InputField(label = "Επώνυμο",
initialValue = accountDetails?.lastName ?: "",
onTextChangeCallback = {
viewModel.contactForm.lastName = it
triggerCheck(textChangedToggle)
}
)
Spacer(modifier = Modifier.padding(18.fixedDp()))
InputField(label = "Τηλέφωνο",
initialValue = accountDetails?.phone1 ?: "",
onTextChangeCallback = {
viewModel.contactForm.phoneNumber = it
triggerCheck(textChangedToggle)
}
)
Spacer(modifier = Modifier.padding(18.fixedDp()))
MyDropdown(label = "Διαθεσιμότητα", list = mapOf(
1 to "09:00 - 11:00",
2 to "11:00 - 13:00",
3 to "13:00 - 15:00",
4 to "15:00 - 17:00"
),
defaultSelected = 1,
disableSelectionReset = true,
onSelectionChangeCallbackValue = { viewModel.contactForm.timeframe = it }
)
Spacer(modifier = Modifier.padding(18.fixedDp()))
InputField(label = "Μήνυμα", singleLine = false,
onTextChangeCallback = {
viewModel.contactForm.reason = it
triggerCheck(textChangedToggle)
}
)
}
}
private fun canProceed(form: CallMeBackForm): Boolean {
return (form.firstName != "" &&
form.lastName != "" &&
form.phoneNumber != "" &&
form.reason != "")
}
private fun triggerCheck(state: MutableState<Boolean>) {
state.value = !state.value
}
InputField composable:
#Composable
fun InputField(
label: String,
enabled: Boolean = true,
isPassword: Boolean = false,
infoButton: (() -> Unit)? = null,
placeholder: String = LocalContext.current.getString(R.string.input_placeholder),
canReveal: Boolean = false,
phonePrefix: Boolean = false,
singleLine: Boolean = true,
optional: Boolean = false,
initialValue: String = "",
onTextChangeCallback: (String) -> Unit = { },
numbersOnly: Boolean = false
) {
Column {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(bottom = 10.fixedDp())
) {
Text(
text = label,
fontSize = 16.sp,
lineHeight = 20.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(end = 10.fixedDp())
)
if (optional) {
Text(
text = LocalContext.current.getString(R.string.optional),
fontSize = 16.sp,
lineHeight = 20.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(end = 10.fixedDp()),
color = colorResource(id = R.color.input_field_optional_text)
)
}
if (infoButton != null) {
InfoButton(
buttonColor = colorResource(id = R.color.Orange_100),
onClick = infoButton
)
}
}
Spacer(modifier = Modifier.padding(10.fixedDp()))
val textFieldValue = remember { mutableStateOf(TextFieldValue(initialValue)) }
val textFieldPasswordMode = remember { mutableStateOf(isPassword) }
val modifier = if (singleLine) {
Modifier
.clip(RoundedCornerShape(6.dp))
.fillMaxWidth()
.background(if (enabled) Color.White else colorResource(id = R.color.Grey_10))
} else {
Modifier
.clip(RoundedCornerShape(6.dp))
.fillMaxWidth()
.requiredHeight(100.dp)
.background(if (enabled) Color.White else colorResource(id = R.color.Grey_10))
}
var isTextFieldFocused by remember { mutableStateOf(false) }
ShadowWrapper(
cardElevation = 1.dp,
border = BorderStroke(
1.dp,
if (isTextFieldFocused && enabled) colorResource(id = R.color.Orange_100)
else colorResource(id = R.color.my_card_border)
),
shadowShapeRadius = 6.dp,
shadowElevation = 1.dp
) {
TextField(
value = textFieldValue.value,
onValueChange = {
textFieldValue.value = it
onTextChangeCallback.invoke(it.text)
},
modifier = modifier
.onFocusChanged { isTextFieldFocused = it.hasFocus },
keyboardOptions = if (numbersOnly) {
KeyboardOptions(keyboardType = KeyboardType.Phone)
} else {
KeyboardOptions(keyboardType = KeyboardType.Text)
},
enabled = enabled,
colors = TextFieldDefaults.textFieldColors(
disabledTextColor = colorResource(id = R.color.Grey_60),
focusedIndicatorColor = Color.Transparent,
backgroundColor = if (enabled) colorResource(id = R.color.white) else colorResource(
id = R.color.Grey_10
),
textColor = colorResource(id = R.color.black),
unfocusedIndicatorColor = colorResource(id = R.color.white),
disabledIndicatorColor = colorResource(id = R.color.white),
cursorColor = colorResource(id = R.color.black)
),
singleLine = singleLine,
placeholder = {
Text(
text = placeholder,
fontStyle = FontStyle.Italic,
fontSize = 16.sp,
lineHeight = 20.sp,
overflow = TextOverflow.Visible
)
},
readOnly = !enabled,
visualTransformation = if (textFieldPasswordMode.value) PasswordVisualTransformation() else VisualTransformation.None,
leadingIcon = if (phonePrefix) {
{
PhonePrefix(R.drawable.greek_flag, "+30")
}
} else null,
trailingIcon = if (canReveal) {
{
RevealingEye(state = textFieldPasswordMode)
}
} else null,
shape = RoundedCornerShape(6.dp),
)
}
}
}
I noticed the seemingly useless if statement around the form, but it seems to be the only thing forcing the recomposition of the Form composable. I thought that when a new accountDetails instance is posted inside an observable state, that this would trigger any composables that depend on it to recompose. What am I missing?

How can select one out of 4 composabel in compose?

Suppose I have four Button or Composable in a row, I want to select one and deselect the other, not like the radio button but behave exactly like that.
#Composable
fun ButtonSet(modifier: Modifier = Modifier) {
Row(
modifier = modifier
.padding(start = 60.dp, end = 60.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly,
) {
val button1 = remember { mutableStateOf(false) }
val button2 = remember { mutableStateOf(false) }
val button3 = remember { mutableStateOf(false) }
val button4 = remember { mutableStateOf(false) }
val buttonList = listOf<Any>(button1, button2, button3, button4)
val activeButton = remember {
mutableStateOf(true)
}
if (button1.value || button2.value || button3.value || button4.value) {
activeButton.value = false
} else if (button1.value) {
button1.value = !button1.value
} else if (button2.value) {
button2.value = !button2.value
} else if (button3.value) {
button3.value = !button3.value
} else if (button4.value) {
button4.value = !button4.value
}
GoalkeeperButton(button1.value)
GoalkeeperButton(button2.value)
GoalkeeperButton(button3.value)
GoalkeeperButton(button4.value)
}
}
#Composable
fun GoalkeeperButton(
active: Boolean = false,
#SuppressLint("ModifierParameter") modifier: Modifier = Modifier,
) {
val buttonState = remember {
mutableStateOf(active)
}
if (!buttonState.value) {
ButtonUnActive() {}
} else {
ButtonActive()
}
RadioButton(selected =, onClick = { /*TODO*/ })
}
#Composable
fun ButtonActive(active: Boolean = true) {
val button = painterResource(id = R.drawable.bttn_pressed)
val gloves = painterResource(id = R.drawable.blue_hands)
val buttonState = remember {
mutableStateOf(active)
}
Column(
modifier = Modifier
.wrapContentWidth()
.height(130.dp)
) {
Image(
painter = gloves,
contentDescription = null,
contentScale = ContentScale.FillBounds,
modifier = Modifier
.size(width = 135.dp, height = 70.dp)
)
Image(
painter = button,
contentDescription = null,
contentScale = ContentScale.FillBounds,
modifier = Modifier
.clickable(onClick = {})
.size(width = 107.dp, height = 65.dp)
)
}
}
#Composable
fun ButtonUnActive(
state: Boolean = false,
onclick: () -> Unit,
) {
val button = painterResource(id = R.drawable.bttn)
var unActiveState by remember {
mutableStateOf(state)
}
Image(
painter = button,
contentDescription = null,
contentScale = ContentScale.FillBounds,
modifier = Modifier
.clickable(
onClick = {
onclick()
},
)
.size(width = 107.dp, height = 65.dp)
)
}
This is what I tried, but I know it's very bad code, please suggest a better and best way.

Compose navigation title is not updating

I am trying to update the title of the TopAppBar based on a live data in the ViewModel, which I update on different screens. It looks like the live data is getting updated properly, but the update is not getting reflected on the title of the TopAppBar. Here is the code:
class MainActivity : ComponentActivity() {
#ExperimentalFoundationApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
LazyVerticalGridActivityScreen()
}
}
}
#ExperimentalFoundationApi
#Composable
fun LazyVerticalGridActivityScreen(destinationViewModel: DestinationViewModel = viewModel()) {
val navController = rememberNavController()
var canPop by remember { mutableStateOf(false) }
// getting the latest title value from the view model
val title: String by destinationViewModel.title.observeAsState("")
Log.d("MainActivity_title", title) // not getting called
navController.addOnDestinationChangedListener { controller, _, _ ->
canPop = controller.previousBackStackEntry != null
}
val navigationIcon: (#Composable () -> Unit)? =
if (canPop) {
{
IconButton(onClick = { navController.popBackStack() }) {
Icon(imageVector = Icons.Filled.ArrowBack, contentDescription = null)
}
}
} else {
null
}
Scaffold(
topBar = {
TopAppBar(title = { Text(title) }, navigationIcon = navigationIcon) // updating the title
},
content = {
NavHost(navController = navController, startDestination = "home") {
composable("home") { HomeScreen(navController) }
composable("details/{listId}") { backStackEntry ->
backStackEntry.arguments?.getString("listId")?.let { DetailsScreen(it, navController) }
}
}
}
)
}
#ExperimentalFoundationApi
#Composable
fun HomeScreen(navController: NavHostController, destinationViewModel: DestinationViewModel = viewModel()) {
val destinations = DestinationDataSource().loadData()
// updating the title in the view model
destinationViewModel.setTitle("Lazy Grid Layout")
LazyVerticalGrid(
cells = GridCells.Adaptive(minSize = 140.dp),
contentPadding = PaddingValues(8.dp)
) {
itemsIndexed(destinations) { index, destination ->
Row(Modifier.padding(8.dp)) {
ItemLayout(destination, index, navController)
}
}
}
}
#Composable
fun ItemLayout(
destination: Destination,
index: Int,
navController: NavHostController
) {
Column(
verticalArrangement = Arrangement.Center,
modifier = Modifier
.background(MaterialTheme.colors.primaryVariant)
.fillMaxWidth()
.clickable {
navController.navigate("details/$index")
}
) {
Image(
painter = painterResource(destination.photoId),
contentDescription = stringResource(destination.nameId),
modifier = Modifier.fillMaxWidth(),
contentScale = ContentScale.Crop
)
Text(
text = stringResource(destination.nameId),
color = Color.White,
fontSize = 14.sp,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp)
)
}
}
#Composable
fun DetailsScreen(
index: String,
navController: NavController,
destinationViewModel: DestinationViewModel = viewModel()
) {
val dataSource = DestinationDataSource().loadData()
val destination = dataSource[index.toInt()]
val destinationName = stringResource(destination.nameId)
val destinationDesc = stringResource(destination.descriptionId)
val destinationImage = painterResource(destination.photoId)
// updating the title in the view model
destinationViewModel.setTitle("Destination Details")
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
.verticalScroll(rememberScrollState())
) {
Image(
painter = destinationImage,
contentDescription = destinationName,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxWidth()
)
Column(modifier = Modifier.padding(horizontal = 16.dp)) {
Text(
text = destinationName,
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(vertical = 16.dp)
)
Text(text = destinationDesc, lineHeight = 24.sp)
Row(horizontalArrangement = Arrangement.End, modifier = Modifier.fillMaxWidth()) {
OutlinedButton(
onClick = {
navController.navigate("home") {
popUpTo("home") { inclusive = true }
}
},
modifier = Modifier.padding(top = 24.dp)
) {
Image(
imageVector = Icons.Filled.ArrowBack,
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colors.primaryVariant),
modifier = Modifier.size(20.dp)
)
Text("Back to Destinations", modifier = Modifier.padding(start = 16.dp))
}
}
}
}
}
EDIT: The ViewModel
class DestinationViewModel : ViewModel() {
private var _title = MutableLiveData("")
val title: LiveData<String>
get() = _title
fun setTitle(newTitle: String) {
_title.value = newTitle
Log.d("ViewModel_title", _title.value.toString())
Log.d("ViewModelTitle", title.value.toString())
}
}
Can anyone please help to find the bug? Thanks!
Edit:
Here is the GitHub link of the project: https://github.com/rawhasan/compose-exercise-lazy-vertical-grid
The reason it's not working is because those are different objects created in different scopes.
When you're using a navController, each destination will have it's own scope for viewModel() creation. By the design you may have a view model for each destination, like HomeViewModel, DestinationViewModel, etc
You can't access an other destination view model from current destination scope, as well as you can't access view model from the outer scope(which you're trying to do)
What you can do, is instead of trying to retrieve it with viewModel(), you can pass outer scope view model to your composable:
composable("details/{listId}") { backStackEntry ->
backStackEntry.arguments?.getString("listId")?.let { DetailsScreen(it, navController, destinationViewModel) }
}
Check out more details about viewModel() in the documentation
Another problem with your code is that you're calling destinationViewModel.setTitle("Lazy Grid Layout") inside composable function. So this code will be called many times, which may lead to recomposition.
To call any actions inside composable, you need to use side-effects. LaunchedEffect in this case:
LaunchedEffect(Unit) {
destinationViewModel.setTitle("Destination Details")
}
This will be called only once after view appear. If you need to call it more frequently, you need to specify key instead of Unit, so it'll be recalled each time when the key changes
You might wanna have a look here https://stackoverflow.com/a/68671477/15880865
Also, you do not need to paste all the code in the question. Just the necessary bit. We shall ask for it if something is missing in the question. Just change your LiveData to mutableStateOf and then let me know if that fixed your problem. Also, you do not need to call observeAsState after modifying the type. Just refer to the link it contains all the info.
Thanks

Integrating Braintree dropin UI into Kotlin Jetpack Compose

I'm trying to convert the android/java dropin UI code from here https://developer.paypal.com/braintree/docs/guides/drop-in/setup-and-integration#starting-drop-in into a jetpack compose app. So far I have
#Composable
fun Account(user: FinUser) {
val context = LocalContext.current
val customerToken = user.userData["customerToken"] as String
val dropInRequest = DropInRequest()
.clientToken(customerToken)
val dropInHintLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartIntentSenderForResult()
) {
print("pause here")
}
val dropInIntent = dropInRequest.getIntent(context)
val dropInPendingIntent = PendingIntent.getBroadcast(
context, 200, dropInIntent,
PendingIntent.FLAG_UPDATE_CURRENT
)
Column(){
Column(
Modifier
.padding(top = 0.dp)
.clickable { launchDropInUi(
dropInHintLauncher=dropInHintLauncher,
dropInPendingIntent=dropInPendingIntent) }) {
Divider(color = Color.LightGray, thickness = 1.dp)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(20.dp, 10.dp)
) {
Column() {
Text("Payment", color = Color.Gray)
Text("*********9999", color = Color.Black)
}
Spacer(modifier = Modifier.fillMaxWidth())
}
Divider(color = Color.LightGray, thickness = 1.dp)
}
}
}
fun launchDropInUi(dropInHintLauncher: ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult>, dropInPendingIntent: PendingIntent){
dropInHintLauncher.launch(
IntentSenderRequest.Builder(dropInPendingIntent).build()
)
}
When I click on my row there's no dropin UI popup but it does register the click and runs over the launchDropInUi function.
I found the issue. I needed to use
val dropInHintLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
)
Giving
#Composable
fun Account(user: FinUser) {
val context = LocalContext.current
val customerToken = user.userData["customerToken"] as String
val dropInRequest = DropInRequest()
.clientToken(customerToken)
val dropInHintLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
){ result: ActivityResult ->
if (result.resultCode == Activity.RESULT_OK) {
// you will get result here in result.data
val data: DropInResult? = result.data?.getParcelableExtra(DropInResult.EXTRA_DROP_IN_RESULT)
}else{
print("throw error popup")
}
}
val dropInIntent = dropInRequest.getIntent(context)
Column(){
Column(
Modifier
.padding(top = 0.dp)
.clickable { launchDropInUi(
dropInHintLauncher =dropInHintLauncher,
dropInIntent =dropInIntent) }) {
Divider(color = Color.LightGray, thickness = 1.dp)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(20.dp, 10.dp)
) {
Column() {
Text("Payment", color = Color.Gray)
Text("*********9999", color = Color.Black)
}
Spacer(modifier = Modifier.fillMaxWidth())
}
Divider(color = Color.LightGray, thickness = 1.dp)
}
}
}
fun launchDropInUi(dropInHintLauncher: ManagedActivityResultLauncher<Intent, ActivityResult>, dropInIntent: Intent){
dropInHintLauncher.launch(dropInIntent)
}