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

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

Related

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

What's the use of `MutableTransitionState`? Looks like mutableStateOf does all it can equally good

In https://developer.android.com/jetpack/compose/animation, it is mentioned that
We sometimes want to have an initial state different from the first target state. We can use updateTransition with MutableTransitionState to achieve this. For example, it allows us to start animation as soon as the code enters composition.
// Start in collapsed state and immediately animate to expanded
var currentState = remember { MutableTransitionState(BoxState.Collapsed) }
currentState.targetState = BoxState.Expanded
val transition = updateTransition(currentState)
// ...
(For full code, refer to full code example 1. below)
However, the above code is only working for initial animation but not subsequent (i.e. change from Expanded to Collapsed), as whenever the currentState.targetState changes, the composable function will run and retriggered currentState.targetState = BoxState.Expanded.
To fix the problem, I'll have to
var currentState by remember { mutableStateOf(BoxState.Collapsed) }
val transition = updateTransition(currentState, label = "")
LaunchedEffect(Unit) {
currentState.targetState = BoxState.Expanded
}
(For full code, refer to full code example 2. below)
This will make the animation start automatically from Collapsed to Expanded. Also subsequently upon change from Expanded to Collapse (e.g. through a button click), it will still work, as the LaunchedEffect is no longer triggered.
With the LaunchedEffect, we can also do it with normal mutableStateOf, i.e. without MutableTransitionState, and still, behave well.
var currentState by remember { mutableStateOf(BoxState.Collapsed) }
val transition = updateTransition(currentState, label = "")
LaunchedEffect(Unit) {
currentState = BoxState.Expanded
}
(For full code, refer to full code example 3. below)
So I cannot see any extra benefit using MutableTransitionState compare to the normal mutableStateOf. Anything I miss?
Full Code that describes the situation above
1. MutableTransitionState code follow document example that doesn't work
The code that doesn't works with just MutableTransitionState (i.e. no effect on button click, only having initial animation)
#Composable
fun Greeting() {
val currentState = remember { MutableTransitionState(BoxState.Collapsed) }
currentState.targetState = BoxState.Expanded
val transition = updateTransition(currentState, label = "")
val rect by transition.animateRect(label = "") { state ->
when (state) {
BoxState.Collapsed -> Rect(0f, 0f, 100f, 100f)
BoxState.Expanded -> Rect(100f, 100f, 300f, 300f)
}
}
Column {
Canvas(
modifier = Modifier.fillMaxWidth().height(500.dp)
.border(BorderStroke(1.dp, Color.Green))
) {
drawPath(Path().apply { addRect(rect) }, Color.Red)
}
Button(onClick = {
currentState.targetState =
if (currentState.targetState == BoxState.Expanded) BoxState.Collapsed
else BoxState.Expanded
}) {
Text("Click Me")
}
}
}
2. MutableTransitionState code with launched effect that works
The code that works with just MutableTransitionState (i.e. have effect on button click, and having initial animation)
#Composable
fun Greeting() {
val currentState = remember { MutableTransitionState(BoxState.Collapsed) }
val transition = updateTransition(currentState, label = "")
val rect by transition.animateRect(label = "") { state ->
when (state) {
BoxState.Collapsed -> Rect(0f, 0f, 100f, 100f)
BoxState.Expanded -> Rect(100f, 100f, 300f, 300f)
}
}
Column {
Canvas(
modifier = Modifier.fillMaxWidth().height(500.dp)
.border(BorderStroke(1.dp, Color.Green))
) {
drawPath(Path().apply { addRect(rect) }, Color.Red)
}
Button(onClick = {
currentState.targetState =
if (currentState.targetState == BoxState.Expanded) BoxState.Collapsed
else BoxState.Expanded
}) {
Text("Click Me")
}
}
LaunchedEffect(Unit) {
currentState.targetState = BoxState.Expanded
}
}
3. Just mutableStateOf code with launched effect that still works
The code that works with just mutableStateOf (i.e. have effect on button click, and having initial animation)
#Composable
fun Greeting() {
var currentState by remember { mutableStateOf(BoxState.Collapsed) }
val transition = updateTransition(currentState, label = "")
val rect by transition.animateRect(label = "") { state ->
when (state) {
BoxState.Collapsed -> Rect(0f, 0f, 100f, 100f)
BoxState.Expanded -> Rect(100f, 100f, 300f, 300f)
}
}
Column {
Canvas(
modifier = Modifier.fillMaxWidth().height(500.dp)
.border(BorderStroke(1.dp, Color.Green))
) {
drawPath(Path().apply { addRect(rect) }, Color.Red)
}
Button(onClick = {
currentState =
if (currentState == BoxState.Expanded) BoxState.Collapsed
else BoxState.Expanded
}) {
Text("Click Me")
}
}
LaunchedEffect(Unit) {
currentState = BoxState.Expanded
}
}
MutableTransitionState allows you to specify a different targetState than the initial state. Note it is not a MutableState.
If you do something like this:
val currentState = remember {
MutableTransitionState(BoxState.Collapsed).apply {targetState = BoxState.Expanded}
}
You can expect the Transition to start an animation going from BoxState.Collapsed to BoxState.Expanded as soon as the updateTransition is composed for the first time.
MutableTransitionState is designed to 1) trigger animations when a composable gets added to the tree (i.e. enter animations), and 2) allow observation of currentState vs targetState through the MutableTransitionState object like this: https://android.googlesource.com/platform/frameworks/support/+/e6095adbb8ffba6aede464fd06ef7302eac61860/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/AnimatedVisiblilityLazyColumnDemo.kt#127

How can be saved the destinations of nested navigation graph's id in the bottom navigation 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").

Resources