How to override SnackbarData in Jetpack Compose? - kotlin

I need a custom parameter in the snackbar, but showSnackbar only allows passing three parameters (message, actionLabel and duration). I need to pass an Enum to decide which status to show.
#Composable
fun BaseScreen(
viewModel: BaseViewModel?,
content: #Composable () -> Unit
) {
val scaffoldState: ScaffoldState = rememberScaffoldState()
val snackbarHostState = remember { SnackbarHostState() }
val coroutineScope: CoroutineScope = rememberCoroutineScope()
Scaffold(
scaffoldState = scaffoldState,
snackbarHost = {
SnackbarHost(
hostState = snackbarHostState,
snackbar = { snackbarData ->
CustomSnackbar(
message = snackbarData.message,
// I can't get custom parameter
status = snackbarData.status
)
}
)
},
) { innerPadding ->
Column(modifier = Modifier.padding(innerPadding)) {
viewModel?.showSnackbar = { message ->
coroutineScope.launch {
snackbarHostState.showSnackbar(
message = message,
// I can't pass custom parameter
status = SnackbarStatusEnum.DANGER
)
}
}
content()
}
}
}

You can use something different.
Use a variable(state) to set the status and use a condition inside the Snackbar to change backgroundColor and actions.
Something like:
Scaffold(
scaffoldState = scaffoldState,
snackbarHost = {
SnackbarHost(it) { data ->
// custom snackbar with the custom colors
Snackbar(
backgroundColor = if (status) Color.Red else Color.Yellow,
//contentColor = ...,
snackbarData = data
)
}
},
)

Related

Jetpack compose custom snackbar material 3

How to achieve a custom snackbar in compose using material 3? I want to change the alignment of the snackbar. Also I want dynamic icon on the snackbar either on left or right side.
You can customize your snackbar using SnackBar composable and can change alignment using SnackbarHost alignment inside a Box if that's what you mean.
val snackState = remember { SnackbarHostState() }
val coroutineScope = rememberCoroutineScope()
Box(
modifier = Modifier
.fillMaxSize()
.padding(20.dp)
) {
Column(modifier = Modifier.fillMaxSize()) {
Button(
modifier = Modifier.fillMaxWidth(),
onClick = {
coroutineScope.launch {
snackState.showSnackbar("Custom Snackbar")
}
}) {
Text("Show Snackbar")
}
}
SnackbarHost(
modifier=Modifier.align(Alignment.BottomStart),
hostState = snackState
) { snackbarData: SnackbarData ->
CustomSnackBar(
R.drawable.baseline_swap_horiz_24,
snackbarData.visuals.message,
isRtl = true,
containerColor = Color.Gray
)
}
}
#Composable
fun CustomSnackBar(
#DrawableRes drawableRes: Int,
message: String,
isRtl: Boolean = true,
containerColor: Color = Color.Black
) {
Snackbar(containerColor = containerColor) {
CompositionLocalProvider(
LocalLayoutDirection provides
if (isRtl) LayoutDirection.Rtl else LayoutDirection.Ltr
) {
Row {
Icon(
painterResource(id = drawableRes),
contentDescription = null
)
Text(message)
}
}
}
}
Validated answer does not answer the question properly because the icon is provided in a static way, not dynamic. The icon is not passed to showSnackbar.
You can do it by having your own SnackbarVisuals :
// Your custom visuals
// Default values are the same than SnackbarHostState.showSnackbar
data class SnackbarVisualsCustom(
override val message: String,
override val actionLabel: String? = null,
override val withDismissAction: Boolean = false,
override val duration: SnackbarDuration = if (actionLabel == null) SnackbarDuration.Short else SnackbarDuration.Indefinite
// You can add custom things here (for you it's an icon)
#DrawableRes val drawableRes: Int
) : SnackbarVisuals
// The way you decide how to display your custom visuals
SnackbarHost(hostState = snackbarHostState) {
val customVisuals = it.visuals as SnackbarVisualsCustom
Snackbar {
// Here is your custom snackbar where you use your icon
}
}
// To display the custom snackbar
snackbarHostStateScope.launch {
snackbarHostState.showSnackbar(
SnackbarVisualsCustom(
message = "The message",
drawableRes = R.drawable.your_icon_id
)
)
}

Jetpack Compose AndroidView call Update

Changing the state in the following way can work just fine
#Composable
fun CustomView() {
val selectedItem = remember { mutableStateOf(0) }
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { context ->
CustomView(context).apply {
myView.setOnClickListener {
selectedItem.value = 1
}
}
},
update = { view ->
view.coordinator.selectedItem = selectedItem.value
}
)
}
What if I want to make a call?
Update the value by calling, not by changing the state
#Composable
fun CustomView() {
val selectedItem = remember { mutableStateOf(0) }
// I need to call some methods like below, but I can't get cropLayoutView
// cropLayoutView.flipImageHorizontally()
// cropLayoutView.flipImageVertically()
// cropLayoutView.rotateClockwise()
// val bitmap = cropLayoutView.croppedImage
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { context ->
CustomView(context).apply {
myView.setOnClickListener {
selectedItem.value = 1
}
}
},
update = { view ->
// How to make a call here?
view.setCoordinator()
}
)
}
What if I want to make a call?
Update the value by calling, not by changing the state

Why textfield not catch text inside and don't add to database

When I press button with onSendClicked is not adding text from textfield. I don't know where not catching text. I guess is somewhere mistake with viewmodel, becouse viewmodel don't get new value.
fun AddBar(
onSendClicked: () -> Unit
){
Row(Modifier.padding(5.dp)) {
var title by remember {
mutableStateOf("")
}
TextField(
value = title,
onValueChange = { title = it }
)
IconButton(onClick = {
onSendClicked()})
{
Icon(imageVector = Icons.Filled.ArrowForward, contentDescription = "Send Icon")
}
}
}
#Composable
fun MainScreen(
basketViewModel: BasketViewModel,
){
AddBar(onSendClicked = { basketViewModel.addToBasket() })
}
And viewModel
val id: MutableState<Int> = mutableStateOf(0)
val title: MutableState<String> = mutableStateOf("")
fun addToBasket(){
viewModelScope.launch(Dispatchers.IO) {
val basket = Basket(
title = title.value,
isChecked = false
)
repository.addToBasket(basket = basket)
}
}
Help....
You are never using the title state of the ViewModel. You are only updating the local title. For this to you have to stop using local title and just replace it with the title from viewModel. Something like that:
fun AddBar(
title: MutableState<String>,
onSendClicked: () -> Unit
){
Row(Modifier.padding(5.dp)) {
TextField(
value = title.value,
onValueChange = { title.value = it }
)
IconButton(onClick = {
onSendClicked()})
{
Icon(imageVector = Icons.Filled.ArrowForward, contentDescription = "Send Icon")
}
}
}
#Composable
fun MainScreen(
basketViewModel: BasketViewModel,
){
AddBar(
title = basketViewModel.title,
onSendClicked = { basketViewModel.addToBasket() }
)
}
You define the title both in view model and your main screen. Use the one in your view model.
fun AddBar(
title: String,
onValueChange: (String) -> Unit,
onSendClicked: () -> Unit
){
Row(Modifier.padding(5.dp)) {
TextField(
value = title,
onValueChange = { onValueChange(it) }
)
IconButton(
onClick = { onSendClicked() }
) {
Icon(
imageVector = Icons.Filled.ArrowForward,
contentDescription = "Send Icon"
)
}
}
}
#Composable
fun MainScreen(
basketViewModel: BasketViewModel,
){
AddBar(
title = basketViewModel.title,
onValueChange = { basketViewModel.changeTitle(it) }
onSendClicked = { basketViewModel.addToBasket() }
)
}
class BasketViewModel : ViewModel() {
var title by mutableStateOf("")
private set
fun changeTitle(value: String) {
title = value
}
fun addToBasket(){
viewModelScope.launch(Dispatchers.IO) {
val basket = Basket(
title = title.value,
isChecked = false
)
repository.addToBasket(basket = basket)
}
}
}

JetpackCompose Navigation Nested Graphs cause "ViewModelStore should be set before setGraph call" exception

I am trying to apply Jetpack Compose navigation into my application.
My Screens: Login/Register screens and Bottom navbar screens(call, chat, settings).
I already found out that the best way to do this is using nested graphs.
But I keep getting ViewModelStore should be set before setGraph call exception. However, I don't think this is the right exception.
My navigation is already in the latest version. Probably my nested graph logic is not right.
Requirement:
I want to be able to navigate from the Login or Register screen to any BottomBar Screen & reverse
#Composable
fun SetupNavGraph(
navController: NavHostController,
userViewModel: UserViewModel
) {
NavHost(
navController = navController,
startDestination = BOTTOM_BAR_GRAPH_ROUTE,
route = ROOT_GRAPH_ROUTE
) {
loginNavGraph(navController = navController, userViewModel)
bottomBarNavGraph(navController = navController, userViewModel)
}
}
NavGraph.kt
fun NavGraphBuilder.loginNavGraph(
navController: NavHostController,
userViewModel: UserViewModel
) {
navigation(
startDestination = Screen.LoginScreen.route,
route = LOGIN_GRAPH_ROUTE
) {
composable(
route = Screen.LoginScreen.route,
content = {
LoginScreen(
navController = navController,
loginViewModel = userViewModel
)
})
composable(
route = Screen.RegisterScreen.route,
content = {
RegisterScreen(
navController = navController,
loginViewModel = userViewModel
)
})
}
}
LoginNavGraph.kt
fun NavGraphBuilder.bottomBarNavGraph(
navController: NavHostController,
userViewModel: UserViewModel
) {
navigation(
startDestination = Screen.AppScaffold.route,
route = BOTTOM_BAR_GRAPH_ROUTE
) {
composable(
route = Screen.AppScaffold.route,
content = {
AppScaffold(
navController = navController,
userViewModel = userViewModel
)
})
}
}
BottomBarNavGraph.kt
#Composable
fun AppScaffold(
navController: NavHostController,
userViewModel: UserViewModel
) {
val scaffoldState = rememberScaffoldState()
Scaffold(
bottomBar = {
BottomBar(mainNavController = navController)
},
scaffoldState = scaffoldState,
) {
NavHost(
navController = navController,
startDestination = NavigationScreen.EmergencyCallScreen.route
) {
composable(NavigationScreen.EmergencyCallScreen.route) {
EmergencyCallScreen(
navController = navController,
loginViewModel = userViewModel
)
}
composable(NavigationScreen.ChatScreen.route) { ChatScreen() }
composable(NavigationScreen.SettingsScreen.route) {
SettingsScreen(
navController = navController,
loginViewModel = userViewModel
)
}
}
}
}
AppScaffold.kt
#Composable
fun BottomBar(mainNavController: NavHostController) {
val items = listOf(
NavigationScreen.EmergencyCallScreen,
NavigationScreen.ChatScreen,
NavigationScreen.SettingsScreen,
)
BottomNavigation(
elevation = 5.dp,
) {
val navBackStackEntry by mainNavController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
items.map {
BottomNavigationItem(
icon = {
Icon(
painter = painterResource(id = it.icon),
contentDescription = it.title
)
},
label = {
Text(
text = it.title
)
},
selected = currentRoute == it.route,
selectedContentColor = Color.White,
unselectedContentColor = Color.White.copy(alpha = 0.4f),
onClick = {
mainNavController.navigate(it.route) {
mainNavController.graph.startDestinationRoute?.let { route ->
popUpTo(route) {
saveState = true
}
}
restoreState = true
launchSingleTop = true
}
},
)
}
}
}
BottomBar.kt
const val ROOT_GRAPH_ROUTE = "root"
const val LOGIN_GRAPH_ROUTE = "login_register"
const val BOTTOM_BAR_GRAPH_ROUTE = "bottom_bar"
sealed class Screen(val route: String) {
object LoginScreen : Screen("login_screen")
object RegisterScreen : Screen("register_screen")
object AppScaffold : Screen("app_scaffold")
}
Screen.kt
sealed class NavigationScreen(val route: String, val title: String, #DrawableRes val icon: Int) {
object EmergencyCallScreen : NavigationScreen(
route = "emergency_call_screen",
title = "Emergency Call",
icon = R.drawable.ic_phone
)
object ChatScreen :
NavigationScreen(
route = "chat_screen",
title = "Chat",
icon = R.drawable.ic_chat)
object SettingsScreen : NavigationScreen(
route = "settings_screen",
title = "Settings",
icon = R.drawable.ic_settings
)
}
NavigationScreen.kt
After struggling some time with this issue, I made my way out by using two separated NavHost. It might not be the right way to do it but it works at the moment. You can find the example source code here:
https://github.com/talhaoz/JetPackCompose-LoginAndBottomBar
Hope they make the navigation easier on upcoming releases.
Nesting of NavHost is not allowed. It results in ViewModelStore should be set before setGraph call Exception. Generally, the bottom nav is outside of the NavHost, which is what the docs show. The recommended approach is a single NavHost, where you hide and show your bottom nav based on what destination you are on.
One NavHost, one NavHostController. Create a new NavHostController in front of the nested NavHost on AppScaffold.
Have similar issue when implement this common UI pattern:
HomePage(with BottomNavigationBar), this page is hosted by Inner nav controller
click some links of one page
navigate to a new page (with new Scaffold instance). This page is hosted by Outer nav controller.
Kinda hacked this issue by using 2 NavHost with 2 navController instance.
Basic idea is using some msg channel to tell the outer nav controller, a Channel in my case.
private val _pages: Channel<String> = Channel()
var pages = _pages.receiveAsFlow()
#Composable
fun Route() {
val navController1 = rememberNavController()
LaunchedEffect(true) {
pages.collect { page ->
navController1.navigate("detail")
}
}
NavHost(navController = navController1, startDestination = "home") {
composable("home") { MainPage() }
composable("detail") { DetailPage() }
}
}
#Composable
fun MainPage() {
val navController2 = rememberNavController()
val onTabSelected = { tab: String ->
navController2.navigate(tab) {
popUpTo(navController2.graph.findStartDestination().id) { saveState = true }
launchSingleTop = true
restoreState = true
}
}
Scaffold(topBar = { TopAppBar(title = { Text("Home Title") }) },
bottomBar = {
BottomNavigation {
val navBackStackEntry by navController2.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
BottomNavigationItem(
selected = currentDestination?.hierarchy?.any { it.route == "tab1" } == true,
onClick = { onTabSelected("tab1") },
icon = { Icon(imageVector = Icons.Default.Favorite, "") },
label = { Text("tab1") }
)
BottomNavigationItem(
selected = currentDestination?.hierarchy?.any { it.route == "tab2" } == true,
onClick = { onTabSelected("tab2") },
icon = { Icon(imageVector = Icons.Default.Favorite, "") },
label = { Text("tab2") }
)
BottomNavigationItem(
selected = currentDestination?.hierarchy?.any { it.route == "tab3" } == true,
onClick = { onTabSelected("tab3") },
icon = { Icon(imageVector = Icons.Default.Favorite, "") },
label = { Text("tab3") }
)
}
}
) { value ->
NavHost(navController = navController2, startDestination = "tab1") {
composable("tab1") { Home() }
composable("tab2") { Text("tab2") }
composable("tab3") { Text("tab3") }
}
}
}
class HomeViewModel: ViewModel()
#Composable
fun Home(viewModel: HomeViewModel = HomeViewModel()) {
Button(
onClick = {
viewModel.viewModelScope.launch {
_pages.send("detail")
}
},
modifier = Modifier.padding(all = 16.dp)
) {
Text("Home", modifier = Modifier.padding(all = 16.dp))
}
}
#Composable
fun DetailPage() {
Scaffold(topBar = { TopAppBar(title = { Text("Detail Title") }) }) {
Text("Detail")
}
}
Cons:
App needs to maintain UI stack information.
It's even harder to cope with responsive layout.
use rememberNavController() for your function
fun YourFunction(
navController: NavHostController = rememberNavController()
)
In my case, I had to create nav controller (for bottom bar) with in home screen.
#AndroidEntryPoint
class MainActivity: ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
setContent {
Theme {
Surface(modifier = Modifier.fillMaxSize()) {
AppContainer()
}
}
}
}
}
#Composable
fun AppContainer() {
val mainNavController = rememberNavController()
// This was causing the issue. I moved this to HomeScreen.
// val bottomNavController = rememberNavController()
Box(
modifier = Modifier.background(BackgroundColor)
) {
NavGraph(mainNavController)
}
}
#Composable
fun HomeScreen(mainNavController: NavController) {
val bottomBarNavController = rememberNavController()
}

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