How can be saved the destinations of nested navigation graph's id in the bottom navigation Jetpack Compose? - android-jetpack-compose

I am facing some problems with navigation's back stacks in Jetpack Compose. The following diagram is my desired scenario.
I have bottom bar and two items Home and Setting. And I want to make both as nested graphs. In HomeNavGraph, HomeRootScreen is startDestination and can navigate to HomeDetail1Screen and HomeDetail2. Setting tab also likes that. And I want to save those nested graph states like When I click HomeRoot -> HomeDetail1 -> HomeDetail2, at that time I click Setting tab in BottomBar. And click back to Home, I want my current Screen to HomeDeatil2Screen.
The followings are my codes. This is my Destination class.
sealed class Destinations(
val route: String
) {
object HomeRoot : Destinations(route = "home_root_screen")
object HomeDetail1 : Destinations(route = "home_detail1_screen")
object HomeDetail2 : Destinations(route = "home_detail2_screen")
object SettingRoot : Destinations(route = "setting_root_screen")
object SettingDetail1 : Destinations(route = "setting_detail1_screen")
object SettingDetail2 : Destinations(route = "setting_detail2_screen")
}
const val BOTTOM_NAV_ROUTE = "btn_nav"
const val HOME_ROUTE = "home"
const val SETTING_ROUTE = "setting"
This is my BottomBarNavGraph.
#Composable
fun BottomBarNavGraph(
navController: NavHostController
) {
NavHost(
navController = navController,
startDestination = HOME_ROUTE,
route = BOTTOM_NAV_ROUTE
) {
homeNavGraph(navController)
settingNavGraph(navController)
}
}
This is HomeNavGraph.
fun NavGraphBuilder.homeNavGraph(
navController: NavHostController
) {
navigation(
startDestination = Destinations.HomeRoot.route,
route = HOME_ROUTE
) {
composable(route = Destinations.HomeRoot.route) { HomeScreen(navController = navController) }
composable(route = Destinations.HomeDetail1.route + "/{argText}") {
val arg = it.arguments?.getString("argText") ?: "Nothing Typed"
HomeDetail1Screen(
text = arg,
navController = navController
)
}
composable(route = Destinations.HomeDetail2.route) { HomeDetail2Screen(navController = navController) }
}
}
This is SettingNavGraph.
fun NavGraphBuilder.settingNavGraph(
navController: NavHostController
) {
navigation(
startDestination = Destinations.SettingRoot.route,
route = SETTING_ROUTE
) {
composable(
route = Destinations.SettingRoot.route
) {
SettingScreen(navController = navController)
}
composable(
route = Destinations.SettingDetail1.route
) {
SettingDetail1Screen(navController = navController)
}
composable(
route = Destinations.SettingDetail2.route
) {
SettingDetail2Screen(navController = navController)
}
}
}
This is my BottomBarDestinations.
sealed class BottomBarDestinations(
val route: String,
val title: String,
val icon: ImageVector
) {
object Home : BottomBarDestinations(
route = HOME_ROUTE,
title = "Home",
icon = Icons.Default.Home
)
object Setting : BottomBarDestinations(
route = SETTING_ROUTE,
title = "Setting",
icon = Icons.Default.Settings
)
}
The followings are my setup of BottomBar in navHost.
#Composable
fun BottomBar(navController: NavHostController) {
val screens = listOf(
BottomBarDestinations.Home,
BottomBarDestinations.Setting
)
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestinations = navBackStackEntry?.destination
BottomNavigation {
screens.forEach {
AddItem(
screen = it,
currentDestinations = currentDestinations,
navController = navController
)
}
}
}
#Composable
fun RowScope.AddItem(
screen: BottomBarDestinations,
currentDestinations: NavDestination?,
navController: NavHostController
) {
BottomNavigationItem(
label = {
Text(text = screen.title)
},
icon = {
Icon(
imageVector = screen.icon,
contentDescription = "Nav Icon"
)
},
selected = currentDestinations?.hierarchy?.any { it.route == screen.route } == true,
unselectedContentColor = LocalContentColor.current.copy(
alpha = ContentAlpha.disabled
),
onClick = {
navController.navigate(screen.route) {
popUpTo(navController.graph.findStartDestination().id)
launchSingleTop = true
restoreState = true
}
}
)
}
#Composable
fun MainScreen(
) {
val navController = rememberNavController()
Scaffold(bottomBar = {
BottomBar(navController = navController)
}) {
BottomBarNavGraph(navController = navController)
}
}
Please help me with this. I stack with this. If you want to check the source code. You can check this in branch
bottom_nav_graph
https://github.com/kyawlinnthant/JetpackComposeNavigationComponent/tree/nested_graph

If you need to save state of route (Screen) or graph, you need to use following technique:
navController.navigate(screen.route) {
popUpTo(navController.graph.findStartDestination().id){
inclusive = true
saveState = true
}
launchSingleTop = true
restoreState = true
}
BTW, I can't use
navController.graph.findStartDestination().id
I specify the start destination by myself (in your case will be "home_root_screen").

Related

How to add a value coming from inside a compose function into a dataStore

I am building an Android app that uses Compose and Navigation. On one screen I have several form fields each in its own composable function, of which I want to store the values. I have managed to do so for a single form field that is in the main screen function as in this example like this:
#Composable
fun Screen1(navController: NavController) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val dataStoreName = StoreName(context)
val savedName = dataStoreName.getValue.collectAsState(initial = "")
Column( ) {
val patientName = remember { mutableStateOf("") }
Button(onClick = {scope.launch {dataStoreName.saveValue(patientName.value)}
navController.navigate(Screen.Screen2.route) }) {
Text(text = "Save & Next")}
OutlinedTextField( value = patientName.value,
label = { Text(text = "Name") },
onValueChange = { patientName.value = it })
Text(text = "Current information stored: " + savedName.value) } }
However, it is not clear to me how to adapt it when I have several fields each in it's own composable function. When I place the mutableState variable inside the textfield function it is not recognized by the code in the save button, and when I place it outside, the variable is not recognized by the textfield function...
This is one of the textfield function as I have them now, which I would call inside the column in the Screen1 function:
#Composable
fun PatientNameField() {
val patientName by remember { mutableStateOf(TextFieldValue("")) }
OutlinedTextField(
value = patientName.value,
label = { Text(text = "Name") },
onValueChange = { patientName.value = it } )
}
For your current problem you can pass the patientName as a parameter to your PatientNameField composable as shown below. This will help to maintain the state of patientName across your Screen1 and PatientNameField composables.
#Composable
fun Screen1(navController: NavController) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val dataStoreName = StoreName(context)
val savedName = dataStoreName.getValue.collectAsState(initial = "")
Column {
val patientName = remember { mutableStateOf("") }
Button(onClick = {
scope.launch { dataStoreName.saveValue(patientName.value) }
navController.navigate(Screen.Screen2.route)
}) {
Text(text = "Save & Next")
}
//pass the patientName as a parameter to your Composable
PatientNameField(patientName)
Text(text = "Current information stored: " + savedName.value)
}
}
#Composable
fun PatientNameField(patientName: MutableState<String>) {
OutlinedTextField(
value = patientName.value,
label = { Text(text = "Name") },
onValueChange = { patientName.value = it } )
}

Jetpack Compose - TextField loses focus after typing

I have an AlertDialog with dynamic height. It has a TextField, on which some basic validation is performed, such as making sure the content is not empty. A warning message is displayed below it if validation fails. Once the user enters text in the text field, the warning message automatically disappears.
The problem is, when the user starts typing after the warning message is already displayed, the TextField loses focus for some reason. Does anyone know why, and how to prevent this from happening? Relevant code is below the gif. Currently using compose:1.2.0-alpha04
AlertDialog
AlertDialog(
properties = DialogProperties(usePlatformDefaultWidth = false),
modifier = Modifier.width(250.dp),
onDismissRequest = { showAddMatchDialog = false },
buttons = {
var player1Name by rememberSaveable { mutableStateOf("") }
var player1NameError by rememberSaveable { mutableStateOf(false) }
var player1Score by rememberSaveable { mutableStateOf("") }
var player1ScoreError by rememberSaveable { mutableStateOf(false) }
Column(
modifier = Modifier.padding(top = 8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = stringResource(R.string.add_match_dialog_title),
fontSize = 20.sp,
fontWeight = FontWeight.SemiBold
)
Spacer(Modifier.height(4.dp))
PlayerRow(
nameLabel = stringResource(R.string.player1_name),
name = player1Name,
isNameError = player1NameError,
onNameClear = { player1Name = "" },
onNameChange = {
player1Name = it
player1NameError = false
},
score = player1Score,
isScoreError = player1ScoreError,
onScoreChange = {
player1Score = it
player1ScoreError = false
}
)
Spacer(Modifier.height(8.dp))
// Same thing for player 2
}
}
)
Player Row
#Composable
fun PlayerRow(
nameLabel: String,
name: String,
isNameError: Boolean,
onNameClear: () -> Unit,
onNameChange: (String) -> Unit,
score: String,
isScoreError: Boolean,
onScoreChange: (String) -> Unit
) {
Column {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
// Name TextField
Column {
Text(
text = nameLabel,
style = MaterialTheme.typography.subtitle2
)
Spacer(Modifier.height(4.dp))
Box {
BasicTextField(
modifier = Modifier
.width(TEXT_FIELD_WIDTH.dp)
.height(TEXT_FIELD_HEIGHT.dp)
.background(
color = GrayLight,
shape = roundedCornerShape
)
.then(
if (isNameError) {
Modifier.border(
width = 1.dp,
color = Warning,
shape = roundedCornerShape
)
} else {
Modifier
}
)
.padding(start = 8.dp, end = 8.dp, top = 6.dp),
value = name,
onValueChange = onNameChange,
singleLine = true,
)
Icon(
modifier = Modifier
.padding(start = (TEXT_FIELD_WIDTH - 25).dp, top = 3.dp)
.ripplelessClickable { onNameClear() },
imageVector = Icons.Default.Clear,
contentDescription = "",
tint = Gray
)
}
}
// Score TextField
Column {
Text(
text = stringResource(R.string.score),
style = MaterialTheme.typography.subtitle2
)
Spacer(Modifier.height(4.dp))
BasicTextField(
modifier = Modifier
.size(TEXT_FIELD_HEIGHT.dp)
.background(
color = GrayLight,
shape = roundedCornerShape
)
.then(
if (isScoreError) {
Modifier.border(
width = 1.dp,
color = Warning,
shape = roundedCornerShape
)
} else {
Modifier
}
)
.padding(start = 6.dp, end = 6.dp, top = 6.dp),
value = score,
onValueChange = onScoreChange,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number
),
singleLine = true,
)
}
}
if (isNameError) {
Text(
modifier = Modifier.padding(start = 20.dp),
text = stringResource(R.string.enter_player_name),
color = Warning,
fontSize = 14.sp
)
}
if (isScoreError) {
Text(
modifier = Modifier.padding(start = 20.dp),
text = stringResource(R.string.enter_player_score),
color = Warning,
fontSize = 14.sp
)
}
}
}
I had the exact same issue. The problem is this:
else {
Modifier
}
You cannot assign "Modifier" by itself when using .then()

Inserting an AnnotatedString into an EditText (Jetpack Compose)

I'm trying to solve the following issue. For example, I'm writing this text - "Hello *world*". After I stop writing, for example, after a second, the word "*world*" should be replaced by "world" in bold.
I've tried to do this, but so far it doesn't work.
val originalText = MutableStateFlow("")
val resultText = originalText
.debounce(1000)
.distinctUntilChanged()
.flatMapLatest { text ->
val result = formatText(text) // create AnnotatedString
flow { emit(result) }
}
And trying to insert to EditText:
val resultText by viewModel.resultText.collectAsState(AnnotatedString(""))
OutlinedTextField(
value = TextFieldValue(resultText),
onValueChange = {
viewModel.originalText.value = it.text
},
label = { Text("Description") },
modifier = Modifier
.fillMaxHeight()
.fillMaxWidth()
)
The problem is that I can't achieve the following result: we write text to "EditText" and after a second it is formatted and inserted into the same "EditText".
Could someone tell me, please, how can I solve this issue?
I found out a solution, but I'm sorry for code. It's definitely worth improving.
ViewModel methods:
private var _wordList = mutableListOf<String>()
val wordList = _wordList
// Remove words that are not in the string
fun updateWordList(text: String) {
_wordList.forEach {
if(!text.contains(it)) {
_wordList.remove(it)
}
}
}
fun getWords(text: String) : List<String> {
val regex = Regex("\\*(.*?)[\\*]")
val matches = regex.findAll(text)
return matches.map { it.groupValues[1] }.toList()
}
fun addWords(text: String) {
val words = getWords(text)
words.forEach { word ->
if(!_wordList.contains(word)) _wordList.add(word)
}
}
A method which create an AnnotatedString:
fun getAnnotatedString(text: String, words: List<String>): AnnotatedString = buildAnnotatedString {
append(text)
words.forEach { word ->
if (text.contains(word)) {
val offsetStart = text.indexOf(word)
val offsetEnd = offsetStart + word.length
addStyle(
style = SpanStyle(fontWeight = FontWeight.Bold),
start = offsetStart,
end = offsetEnd
)
}
}
}
After that we need to create the following variables:
val words = viewModel.getWords(description)
viewModel.addWords(description)
val descResult = if (words.isEmpty()) description else description.replace("*", "")
val formattedString = formatString(descResult, viewModel.wordList)
var textFieldValueState by remember {
mutableStateOf(TextFieldValue(annotatedString = formattedString))
}
val textFieldValue = textFieldValueState.copy(annotatedString = formattedString)
And finally, we define the OutlinedTextField:
OutlinedTextField(
value = textFieldValue,
onValueChange = {
viewModel.updateWordList(it.text)
if (tmp == it.text) {
textFieldValueState = it
return#OutlinedTextField
}
description = it.text
textFieldValueState = it
},
label = { Text("Description") }
)

Prevent closing bottom sheet with background click with accompanist library

I have tried accompanist library for navigation from this article and I want to prevent bottom sheet to be closed when I click on the background of bottom sheet (the gray area) and to make it non clickable at all, how can I achieve this?
This is the code from the link
#Composable
fun MyApp() {
val navController = rememberNavController()
val bottomSheetNavigator = rememberBottomSheetNavigator()
navController.navigatorProvider += bottomSheetNavigator
ModalBottomSheetLayout(
bottomSheetNavigator = bottomSheetNavigator
) {
NavHost(navController, startDestination = "home") {
composable(route = "home") {
Button(onClick = { navController.navigate("sheet") }) {
Text("Click me to see something cool!")
}
}
bottomSheet(route = "sheet") {
Text("This is a cool bottom sheet!")
Button(onClick = { navController.navigate("home") }) {
Text("Take me back, please!")
}
Spacer(modifier = Modifier.padding(200.dp))
}
}
}
}
ModalBottomSheetLayout has this sheetState parameter set as following:
sheetState: ModalBottomSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
You can prevent the dismiss behaviour by passing this parameter as following:
// This prevents dismissing the ModalBottomSheet
val sheetState = rememberModalBottomSheetState(
initialValue = ModalBottomSheetValue.Hidden,
confirmStateChange = { false }
)

Jetpack Compose list diffs animation

Is there a way to get an animation effect on a list (column/row) changes in Compose that looks something like recyclerview animations with setItemAnimator?
There is not currently a way to do this with LazyColumn/LazyRow. This is something that is likely to be added eventually (though as always with predictions about the future: no promises), but it's currently a lower priority than getting more fundamental features working.
Note: I work on the team that implemented these components. I'll update this answer if the situation changes.
The Modifier API called Modifier.animateItemPlacement() was implemented and merged and will probably be released in an upcoming Compose version. Tweet: https://twitter.com/CatalinGhita4/status/1455500904690552836?s=20
At the moment, you'll need to manage the enter/exit transition of the changed items explicitly. You could use AnimatedVisibility for that like this example.
Here's an example for dealing with item additions/removals at least:
#ExperimentalAnimationApi
#Suppress("UpdateTransitionLabel", "TransitionPropertiesLabel")
#SuppressLint("ComposableNaming", "UnusedTransitionTargetStateParameter")
/**
* #param state Use [updateAnimatedItemsState].
*/
inline fun <T> LazyListScope.animatedItemsIndexed(
state: List<AnimatedItem<T>>,
enterTransition: EnterTransition = expandVertically(),
exitTransition: ExitTransition = shrinkVertically(),
noinline key: ((item: T) -> Any)? = null,
crossinline itemContent: #Composable LazyItemScope.(index: Int, item: T) -> Unit
) {
items(
state.size,
if (key != null) { keyIndex: Int -> key(state[keyIndex].item) } else null
) { index ->
val item = state[index]
val visibility = item.visibility
androidx.compose.runtime.key(key?.invoke(item.item)) {
AnimatedVisibility(
visibleState = visibility,
enter = enterTransition,
exit = exitTransition
) {
itemContent(index, item.item)
}
}
}
}
#Composable
fun <T> updateAnimatedItemsState(
newList: List<T>
): State<List<AnimatedItem<T>>> {
val state = remember { mutableStateOf(emptyList<AnimatedItem<T>>()) }
LaunchedEffect(newList) {
if (state.value == newList) {
return#LaunchedEffect
}
val oldList = state.value.toList()
val diffCb = object : DiffUtil.Callback() {
override fun getOldListSize(): Int = oldList.size
override fun getNewListSize(): Int = newList.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
oldList[oldItemPosition].item == newList[newItemPosition]
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
oldList[oldItemPosition].item == newList[newItemPosition]
}
val diffResult = calculateDiff(false, diffCb)
val compositeList = oldList.toMutableList()
diffResult.dispatchUpdatesTo(object : ListUpdateCallback {
override fun onInserted(position: Int, count: Int) {
for (i in 0 until count) {
val newItem = AnimatedItem(visibility = MutableTransitionState(false), newList[position + i])
newItem.visibility.targetState = true
compositeList.add(position + i, newItem)
}
}
override fun onRemoved(position: Int, count: Int) {
for (i in 0 until count) {
compositeList[position + i].visibility.targetState = false
}
}
override fun onMoved(fromPosition: Int, toPosition: Int) {
// not detecting moves.
}
override fun onChanged(position: Int, count: Int, payload: Any?) {
// irrelevant with compose.
}
})
if (state.value != compositeList) {
state.value = compositeList
}
val initialAnimation = Animatable(1.0f)
initialAnimation.animateTo(0f)
state.value = state.value.filter { it.visibility.targetState }
}
return state
}
data class AnimatedItem<T>(
val visibility: MutableTransitionState<Boolean>,
val item: T,
) {
override fun hashCode(): Int {
return item?.hashCode() ?: 0
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as AnimatedItem<*>
if (item != other.item) return false
return true
}
}
suspend fun calculateDiff(
detectMoves: Boolean = true,
diffCb: DiffUtil.Callback
): DiffUtil.DiffResult {
return withContext(Dispatchers.Unconfined) {
DiffUtil.calculateDiff(diffCb, detectMoves)
}
}

Resources