How to make ModalBottomSheetLayout to use the newest state for sheetContent? - android

I tried to show a timestamp (or any strings that can be updated in general) inside a sheetContent of the ModalBottomSheetLayout that is shown by clicking on a floating action button. However, that timestamp is only updated once: the first time that the sheet is shown. If I close the sheet (through onSave in the code) and open it again, the timestamp stays the same instead of showing the newest timestamp. I think I maybe missing "remember" or "mutableState" somehow but I am not able to get it to work.
class MainActivity : ComponentActivity() {
#ExperimentalMaterialApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AskQuestionTheme {
val modalBottomSheetState =
rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
val scope = rememberCoroutineScope()
Scaffold(
floatingActionButton = {
FloatingActionButton(
onClick = { scope.launch { modalBottomSheetState.show() } }) {
Icon(
Icons.Rounded.Add,
contentDescription = "add"
)
}
}) {
ModalBottomSheetLayout(
sheetState = modalBottomSheetState,
sheetContent = {
var t = Date().toString()
MySheet(
str = t,
onSave = {
scope.launch {
modalBottomSheetState.hide()
}
},
onCancel = { scope.launch { modalBottomSheetState.hide() } })
}
) {
Text("hello")
}
}
}
}
}
}
#Composable
fun MySheet(str: String, onSave: () -> Unit, onCancel: () -> Unit) {
Column {
Box(modifier = Modifier.fillMaxWidth()) {
Icon(
Icons.Rounded.Close,
contentDescription = "edit",
modifier = Modifier.align(Alignment.CenterStart).clickable { onCancel() })
Button(onClick = onSave, modifier = Modifier.align(Alignment.CenterEnd)) {
Text("save")
}
}
Text(str)
Spacer(Modifier.height(100.dp))
}
}

There are two ways for me. Up to you which select)
First variant. Added a new property var t by remember { mutableStateOf(Date().toString()) } and change its before showing ModalBottomSheetLayout. It is an update code
t = Date().toString().
class MainActivity : ComponentActivity() {
#ExperimentalMaterialApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AskQuestionTheme {
val modalBottomSheetState =
rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
val scope = rememberCoroutineScope()
var t by remember { mutableStateOf(Date().toString()) }
Scaffold(
floatingActionButton = {
FloatingActionButton(
onClick = {
scope.launch {
t = Date().toString()
modalBottomSheetState.show()
}
}) {
Icon(
Icons.Rounded.Add,
contentDescription = "add"
)
}
}) {
ModalBottomSheetLayout(
sheetState = modalBottomSheetState,
sheetContent = {
MySheet(
str = t,
onSave = {
scope.launch {
modalBottomSheetState.hide()
}
},
onCancel = { scope.launch { modalBottomSheetState.hide() } })
}
) {
Text("hello")
}
}
}
}
}
}
Second variant. Now you can see LaunchedEffect. Code inside this block is triggered when this value modalBottomSheetState.direction changes. Inside this block we update a date.
class MainActivity : ComponentActivity() {
#ExperimentalMaterialApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AskQuestionTheme {
val modalBottomSheetState =
rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
val scope = rememberCoroutineScope()
Scaffold(
floatingActionButton = {
FloatingActionButton(
onClick = { scope.launch { modalBottomSheetState.show() } }) {
Icon(
Icons.Rounded.Add,
contentDescription = "add"
)
}
}) {
ModalBottomSheetLayout(
sheetState = modalBottomSheetState,
sheetContent = {
var t by remember { mutableStateOf(Date().toString()) }
LaunchedEffect(modalBottomSheetState.direction) {
if (modalBottomSheetState.direction == -1f) {
t = Date().toString()
}
}
MySheet(
str = t,
onSave = {
scope.launch {
modalBottomSheetState.hide()
}
},
onCancel = { scope.launch { modalBottomSheetState.hide() } })
}
) {
Text("hello")
}
}
}
}
}
}

Related

How do I cancel an event after another one has started?

I am having an issue, where after a Snackbar appears the FAB won't perform navigation for the period of the Snackbar message. If I click it multiple times when the Snackbar is displayed it will stack and open multiple instances of one screen (only after the Snackbar is gone). How do I cancel showing the Snackbar on click of the FAB and preserve the functionality of the FAB at all times?
TasksList.kt:
#OptIn(ExperimentalMaterial3Api::class)
#Composable
fun TasksListScreen(
modifier: Modifier = Modifier,
onNavigate: (UiEvent.Navigate) -> Unit,
viewModel: TaskListViewModel = hiltViewModel()
) {
val tasks = viewModel.tasks.collectAsState(initial = emptyList())
val snackbarHostState = remember { SnackbarHostState() }
LaunchedEffect(key1 = true) {
viewModel.uiEvent.collect { event ->
when (event) {
is UiEvent.ShowSnackbar -> {
val result = snackbarHostState.showSnackbar(
message = event.message,
actionLabel = event.action,
duration = SnackbarDuration.Long,
)
if (result == SnackbarResult.ActionPerformed) {
viewModel.onEvent(TaskListEvent.OnUndoDeleteTask)
}
}
is UiEvent.Navigate -> onNavigate(event)
else -> Unit
}
}
}
Scaffold(
snackbarHost = {
SnackbarHost(snackbarHostState) { data ->
Snackbar(
shape = RoundedShapes.medium,
actionColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.background,
snackbarData = data
)
}
},
floatingActionButton = {
FloatingActionButton(
shape = RoundedShapes.medium,
onClick = { viewModel.onEvent(TaskListEvent.OnAddTask) },
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.background
) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = stringResource(R.string.fab_cd)
)
}
},
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.app_name)) },
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primary,
titleContentColor = MaterialTheme.colorScheme.background
)
)
},
) { padding ->
LazyColumn(
state = rememberLazyListState(),
verticalArrangement = spacedBy(12.dp),
contentPadding = PaddingValues(vertical = 16.dp),
modifier = modifier
.fillMaxSize()
.padding(padding)
.padding(horizontal = 16.dp),
) {
items(items = tasks.value, key = { task -> task.hashCode() }) { task ->
val currentTask by rememberUpdatedState(newValue = task)
val dismissState = rememberDismissState(confirmValueChange = {
if (it == DismissValue.DismissedToStart) {
viewModel.onEvent(TaskListEvent.OnDeleteTask(currentTask))
}
true
})
SwipeToDismiss(state = dismissState,
directions = setOf(DismissDirection.EndToStart),
background = { },
dismissContent = {
TaskItem(
task = task, modifier = modifier
)
})
}
}
}
}
TaskListViewModel:
#HiltViewModel
class TaskListViewModel #Inject constructor(private val repository: TaskRepositoryImplementation) :
ViewModel() {
val tasks = repository.getTasks()
private val _uiEvent = Channel<UiEvent>()
val uiEvent = _uiEvent.receiveAsFlow()
private var deletedTask: Task? = null
fun onEvent(event: TaskListEvent) {
when (event) {
is TaskListEvent.OnAddTask -> {
sendUiEvent(UiEvent.Navigate(Routes.ADD_EDIT_TASK))
}
is TaskListEvent.OnEditClick -> {
sendUiEvent(UiEvent.Navigate(Routes.ADD_EDIT_TASK + "?taskId=${event.task.id}"))
}
is TaskListEvent.OnDeleteTask -> {
val context = TaskApp.instance?.context
viewModelScope.launch(Dispatchers.IO) {
deletedTask = event.task
repository.deleteTask(event.task)
if (context != null) {
sendUiEvent(
UiEvent.ShowSnackbar(
message = context.getString(R.string.snackbar_deleted),
action = context.getString(R.string.snackbar_action)
)
)
}
}
}
is TaskListEvent.OnUndoDeleteTask -> {
deletedTask?.let { task ->
viewModelScope.launch {
repository.addTask(task)
}
}
}
}
}
private fun sendUiEvent(event: UiEvent) {
viewModelScope.launch(Dispatchers.IO) {
_uiEvent.send(event)
}
}
}

Bottom Sheet not being shown if hiding keyboard

If my keyboard is opened, then on clicking of button if i try to hide my keyboard and change sheetState to show() then my keyboard hides but my sheet is not visible.
Mimicing opening of sheetState on result from api call.
Note - Sheet is shown for first time only
setContent {
val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
val key = LocalSoftwareKeyboardController.current
ModalBottomSheetLayout(
sheetState = sheetState,
sheetContent = {
repeat(20) {
Text(text = "$it")
}
}
) {
Column {
var otpResponse by remember { mutableStateOf<Boolean?>(null) }
if (otpResponse == false) {
LaunchedEffect(key1 = Unit, block = {
delay(180)
otpResponse = true
})
}
if (otpResponse == true) {
LaunchedEffect(key1 = Unit, block = {
sheetState.show()
})
}
Column {
var string by remember { mutableStateOf("") }
TextField(value = string, onValueChange = { string = it })
Button(onClick = {
key?.hide()
otpResponse = false
}) {
Text(text = "TEST")
}
}
}
}
}
I have modified your code have look which is working without delay
#OptIn(ExperimentalMaterialApi::class, ExperimentalComposeUiApi::class)
#Preview(showBackground = true)
#Composable
fun DefaultPreview(){
val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
val key = LocalSoftwareKeyboardController.current
val scope = rememberCoroutineScope()
ModalBottomSheetLayout(
sheetState = sheetState,
sheetContent = {
repeat(20) {
Text(text = "$it")
}
}
) {
Column {
Column {
var string by remember { mutableStateOf("") }
TextField(value = string, onValueChange = { string = it })
Button(onClick = {
key?.hide()
scope.launch{
sheetState.show()
}
}) {
Text(text = "TEST")
}
}
}
}
}
thanks for my colleague to fix this

Why won't my Card Swipe right in this Test?

I am testing an app in myTest() and would like to simulate a swipeRight gesture on MyCard() composable.
When I use performTouchInput { swipeRight() } nothing happens. The card stays in the same place.
How can I simulate the swipe right gesture on the card? What am I missing?
Code
class MyTest {
#get:Rule
val composeRule = createComposeRule()
#Before
fun setUp() {
composeRule.setContent {
MyCard()
}
}
#Test
fun myTest() = runTest {
composeRule.onNodeWithTag("DraggableCard")
.performTouchInput { swipeRight() }
}
}
#SuppressLint("UnusedTransitionTargetStateParameter")
#Composable
fun MyCard() {
var swipeState by remember { mutableStateOf(false) }
val transitionState = remember { MutableTransitionState(swipeState).apply { targetState = !swipeState } }
val transition = updateTransition(transitionState, "cardTransition")
val offsetTransition by transition.animateFloat(
label = "cardOffsetTransition",
transitionSpec = { tween(durationMillis = 300) },
targetValueByState = { if (swipeState) 75f else 0f },)
Card(
modifier = Modifier
.testTag("DraggableCard")
.offset { IntOffset(offsetTransition.roundToInt(), 0) }
.pointerInput(Unit) {
detectHorizontalDragGestures { _, dragAmount ->
when {
dragAmount >= 6 -> { swipeState = true }
dragAmount < -6 -> { swipeState = false }
}
}
},
content = { Text(text = "Hello") }
)
}

Compose Test is Flaky

My some view that starts from an activity shows an alert dialog when pressing the back button using the BackHandler.
#Composable
fun PostEditContent(
title: String,
uiState: BasePostEditUiState
) {
var enabledBackHandler by remember { mutableStateOf(true) }
var enabledAlertDialog by remember { mutableStateOf(false) }
val snackBarHostState = remember { SnackbarHostState() }
val coroutineScope = rememberCoroutineScope()
val onBackPressedDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
val navigateToBack = { onBackPressedDispatcher?.onBackPressed() }
val forceNavigateToBack = {
coroutineScope.launch {
enabledBackHandler = false
awaitFrame()
navigateToBack()
}
}
if (enabledAlertDialog) {
PostEditAlertDialog(
onDismissRequest = { enabledAlertDialog = false },
onOkClick = { forceNavigateToBack() }
)
}
BackHandler(enabledBackHandler) {
enabledAlertDialog = true
}
...
It's working fine. But in the testing, it sometimes failed because the awaitFrame() is not working properly.
#Suppress("TestFunctionName")
#Composable
private fun PostEditScreenWithHomeBackStack(uiState: PostEditUiState) {
val navController = rememberNavController()
NavHost(navController, startDestination = "home") {
composable(route = "home") {
Text(HOME_STACK_TEXT)
}
composable(route = "edit") {
PostEditScreen(uiState = uiState)
}
}
SideEffect {
navController.navigate("edit")
}
}
...
#Test
fun navigateToUp_WhenSuccessSaving() {
composeTestRule.apply {
setContent {
PostEditScreenWithHomeBackStack(emptyUiState.copy(title = "a", content = "a"))
}
// The save button calls the `forceNavigateToBack()` that in the upper code block
onNodeWithContentDescription("save").performClick()
onNodeWithText(HOME_STACK_TEXT).assertIsDisplayed()
}
}
Is there a better way to show the alert dialog or fix the flaky test? Thanks.

trying to save value of my edit text field after changing to one activity to 2nd activity and then return to 1st activity in jetpack compose

I am trying to save my edit text value after changing from one activity to another
ex: just like in a register pager (first you have to fill all data in pages and in the end submit.
I am having trouble here is if i want to edit my first page data so i moved from 2nd page to 1st page but my 1st page doesn't show any data
I tried using save state and restore state of navigation but it wont help
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent { ScreenNavigation() }
}
}
#Composable
fun Page1(navController: NavHostController) {
var test = remember { mutableStateOf("") }
Column() {
TextField(value = test.value, onValueChange = { test.value = it })
Button(
onClick = {
navController.navigate(ScreenRoute.Page2.name) {
popUpTo(ScreenRoute.Page2.name) { saveState = true }
launchSingleTop = true
restoreState = true
}
}
) {}
Text(text = "Page1")
TextField(
value = test.value,
onValueChange = { test.value = it },
)
}
}
#Composable
fun Page2(navController: NavHostController) {
var test = remember { mutableStateOf("") }
Column() {
TextField(value = test.value, onValueChange = { test.value = it })
Button(
onClick = {
navController.navigate(ScreenRoute.Page1.name) {
popUpTo(ScreenRoute.Page1.name) { saveState = true }
launchSingleTop = true
restoreState = true
}
}
) {}
Text(text = "Page2")
}
}
// code for navigation
#Composable
fun ScreenNavigation() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = ScreenRoute.Page1.name) {
composable(ScreenRoute.Page1.name) { Page1(navController) }
composable(ScreenRoute.Page2.name) { Page2(navController) }
}
}
// code for screen route
enum class ScreenRoute {
Page1,
Page2,
}
Have both variables outside the functions and above class like this:
var test1 = mutableStateOf("")
var test2 = mutableStateOf("")
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent { ScreenNavigation() }
}
}
#Composable
fun Page1(navController: NavHostController) {
Column() {
TextField(value = test1.value, onValueChange = { test1.value = it })
Button(
onClick = {
navController.navigate(ScreenRoute.Page2.name) {
popUpTo(ScreenRoute.Page2.name) { saveState = true }
launchSingleTop = true
restoreState = true
}
}
) {}
Text(text = "Page1")
TextField(
value = test1.value,
onValueChange = { test1.value = it },
)
}
}
#Composable
fun Page2(navController: NavHostController) {
Column() {
TextField(value = test2.value, onValueChange = { test2.value = it })
Button(
onClick = {
navController.navigate(ScreenRoute.Page1.name) {
popUpTo(ScreenRoute.Page1.name) { saveState = true }
launchSingleTop = true
restoreState = true
}
}
) {}
Text(text = "Page2")
}
}
// code for navigation
#Composable
fun ScreenNavigation() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = ScreenRoute.Page1.name) {
composable(ScreenRoute.Page1.name) { Page1(navController) }
composable(ScreenRoute.Page2.name) { Page2(navController) }
}
}
// code for screen route
enum class ScreenRoute {
Page1,
Page2,
}
Have both variables outside the functions
Use rememberSaveable
remembersaveable save the state of value
val numberOfFamilyMember = rememberSaveable {
mutableStateOf("")
}
Try using rememberSaveable like this
#Composable
fun Page1(navController: NavHostController) {
var test by rememberSaveable { mutableStateOf("") }
Column() {
TextField(value = test.value, onValueChange = { test.value = it })
Button(
onClick = {
navController.navigate(ScreenRoute.Page2.name) {
popUpTo(ScreenRoute.Page2.name) { saveState = true }
launchSingleTop = true
restoreState = true
}
}
) {}
Text(text = "Page1")
TextField(
value = test.value,
onValueChange = { test.value = it },
)
}
}
Using variables outside Composable seems to be a side effect.
rememberSaveable should survive the activity change according to docs

Categories

Resources