I'm having a little trouble adding a form inside a Bottom sheet because every time I open the bottomSheet, the previous values continue there. I'm trying to make something like this
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun BottomSheet() {
val bottomSheetScaffoldState = rememberBottomSheetScaffoldState(
bottomSheetState = BottomSheetState(BottomSheetValue.Collapsed)
)
val coroutineScope = rememberCoroutineScope()
BottomSheetScaffold(
scaffoldState = bottomSheetScaffoldState,
sheetContent = {
Form {
// save foo somewhere
coroutineScope.launch {
bottomSheetScaffoldState.bottomSheetState.collapse()
}
}
},
sheetPeekHeight = 0.dp
) {
Button(onClick = {
coroutineScope.launch {
bottomSheetScaffoldState.bottomSheetState.expand()
}
}) {
Text(text = "Expand")
}
}
}
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun Form(
onSaveFoo: (String) -> Unit
) {
var foo by remember { mutableStateOf("") }
Column {
Button(onClick = {
onSaveFoo(foo)
}) {
Text(text = "Save")
}
OutlinedTextField(value = foo, onValueChange = { foo = it })
}
}
There is a way to "clean" my form every time the bottom sheet collapses without manually setting all values to "" again?
Something like the BottomShettFragment. If I close and reopen the BottomSheetFragment, the previous values will not be there.
Firstly, they say that it is better to control your state outside of a composable function (in a viewmodel) and pass it as a parameter.
You may clear the textField value, when you decide to collapse your bottomSheet, for example in onSaveFoo function.
Add a MutableStateFlow to your viewmodel, subscribe to its updates via collectAsState extension in your composable. You can get a viewmodel by a composable function viewModel(ViewModelClass::class.java).
In onSaveFoo function update your state with new string or empty string if that's the behaviour you want to achieve. State updates should happen inside viewmodel. So create a method in your viewmodel to update your state and call it when you want to collapse your bottomsheet to clear the text contained in your state.
And another thing, remember saves the value across recompositions. The value will be lost only if your Composable is removed from the Composition. It will happen if you change the content of your bottomSheet.
Something like this:
sheetContent = {
if(bottomSheetScaffoldState.bottomSheetState.isExpanded){
Form {
// save foo somewhere
coroutineScope.launch {
bottomSheetScaffoldState.bottomSheetState.collapse()
}
}
}else{
Spacer(modifier=Modifier.height(16.dp).background(Color.White)//or some other composable
}
},
Related
I am building a simple app which has a TabLayout with two tabs. One tab is a list of items which you can click and make a note about the item. The other tab is a list of notes you made in the first tab.
I have created a TabLayout using TabRows and HorizontalPager here is the code for that section:
#OptIn(ExperimentalPagerApi::class)
#Composable
fun HorizontalPagerTabLayout(
modifier: Modifier
) {
val tabData = listOf("Records" to Icons.Default.Phone, "Notes" to Icons.Default.Star)
val pagerState = rememberPagerState(
initialPage = 0
)
val tabIndex = pagerState.currentPage
val coroutineScope = rememberCoroutineScope()
Column {
TabRow(
selectedTabIndex = tabIndex,
modifier = modifier
) {
tabData.forEachIndexed { index, _ ->
Tab(
icon = {
Icon(imageVector = tabData[index].second, contentDescription = null)
},
selected = tabIndex == index,
onClick = {
coroutineScope.launch {
pagerState.animateScrollToPage(index)
}
},
text = {
Text(
tabData[index].first,
color = if (pagerState.currentPage == index) Color.White else Color.LightGray
)
})
}
}
HorizontalPager(state = pagerState, count = tabData.size) { page ->
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
when (page) {
0 -> RecordsScreen(modifier = Modifier)
1 -> NotesScreen(modifier = Modifier)
}
}
}
}
}
In RecordScreen I have a logic that will make a Note about the Record and store it in RoomDB. That works as expected.
In NotesScreen I have a viewModel that will pull all the notes from RoomDB and diplay them in the NotesScreen tab.
Here is the code for NotesScreen:
#Composable
fun NotesScreen(
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
modifier: Modifier,
notesViewModel: NotesViewModel = hiltViewModel()
) {
val notesList by notesViewModel.notesStateData.collectAsState()
// FAILED SOLUTION NUMBER 2
// LaunchedEffect(Unit){
// notesViewModel.getAllNotes()
// }
// FAILED SOLUTION NUMBER 3
// DisposableEffect(lifecycleOwner) {
// val observer = LifecycleEventObserver { _, event ->
// when (event) {
// Lifecycle.Event.ON_RESUME -> {
// notesViewModel.getAllNotes()
// }
// else -> {}
// }
// }
// lifecycleOwner.lifecycle.addObserver(observer)
// onDispose {
// lifecycleOwner.lifecycle.removeObserver(observer)
// }
// }
LazyColumn(
contentPadding = (PaddingValues(horizontal = 16.dp, vertical = 8.dp)),
modifier = modifier
) {
items(items = notesList.orEmpty()) { note ->
NoteItem(note)
}
}
}
From the code you can see that I commented out some failed solutions. My first solution is to include notesViewModel.getAllNotes() in init{} block of viewModel. But this calls the function only once, and not every time I get back to NotesScreen.
My second solution is to create a LaunchedEffect(Unit) because I want to call this viewModel function every time the NotesScreen is displayed. But it didn't work, it also calls the getAllNotes() only once.
My third solution is to create a DisposableEffect with observer that will tell me every time ON_RESUME happens, but this also didn't work.
I used debugger and logged some behaviours:
When I use init block, the init block happens as soon as I open the app and not when I open the screen. It seems like the HorizontalPager loads the screen immediately.
When I use second solution the LaunchEffect block happens when I navigate to the screen, but it doesn't get called again even though I switched tabs multiple times.
When I use DisposableEffect all the lifecycle states happen as soon as I open the app, ON_START, ON_CREATE, and ON_RESUME are logged as soon as app is opened even though I am not on NotesScreen yet. This means that the viewModel function got called only once, and now I can't see the new Notes I created untill I restart the app.
My desired behaviour: Every time I open NotesScreen I want to load all the notes from RoomDB, and not only once on app start. I am trying to trigger VM function that gets notes from DB automatically without refresh layouts or buttons but I can't seem to find a solution.
Any help would be heavily appreciated.
You can use snapshotFlow for add page change callback to Pager. It Creates a Flow from observable Snapshot state. Here is a sample code
// Page change callback
LaunchedEffect(pagerState) {
snapshotFlow { pagerState.currentPage }.collect { page ->
when (page) {
0 -> viewModel.getAllNotes() // First page
1 -> // Second page
else -> // Other pages
}
}
}
I am using compose navigation with single activity and no fragments.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MobileComposePlaygroundTheme {
Surface(color = MaterialTheme.colors.background) {
val navController = rememberNavController()
NavHost(navController, startDestination = "main") {
composable("main") { MainScreen(navController) }
composable("helloScreen") { HelloScreen() }
}
}
}
}
}
}
#Composable
private fun MainScreen(navController: NavHostController) {
val count = remember {
Log.d("TAG", "inner remember, that is, initialized")
mutableStateOf(0)
}
LaunchedEffect("fixedKey") {
Log.d("TAG", "inner LaunchedEffect, that is, initialized")
}
Column {
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = {
count.value++
Log.d("TAG", "count: ${count.value}")
},
modifier = Modifier.padding(8.dp)
) {
Text(text = "Increase Count ${count.value}")
}
Button(
onClick = { navController.navigate("helloScreen") },
modifier = Modifier.padding(8.dp)
) {
Text(text = "Go To HelloScreen")
}
}
}
#Composable
fun HelloScreen() {
Log.d("TAG", "HelloScreen")
Text("Hello Screen")
}
MainScreen -> HelloScreen -> back button -> MainScreen
After pop HelloScreen by back button, MainScreen restart composition from scratch. That is, not recomposition but initial composition. So remember and LaunchedEffect is recalculated.
I got rememberSaveable for maintaining states on this popping upper screen case. However how can I prevent re-execute LaunchedEffect? In addition, docs saying rememberSavable makes value to survive on configuration change but this is not the exact case.
I expected that LowerScreen is just hidden when UpperScreen is pushed, and LowerScreen reveal again when UpperScreen is popped, like old Android's onPause(), onResume(), etc.
In Compose, is this not recommended?
ps.
Lifecycle of Composable is not tied with ViewModel but with Activity
It needs more care about initialization of ViewModel
Why Compose team design like this?
Can you recommend good architecture sample code?
I'm building a composable screen, say PostScreen where multiple posts are shown in GridView and when user click on any of them, I'll navigate to DetailScreen where posts are shown in larger box with multiple buttons associated (like, comment).
My logic is, when user click on any post in PostScreen, use an index from PostScreen to scroll to that index in DetailScreen. Issue is, when user click on any post (and arrive to DetailScreen), then move up (or down) wards and then click on action (for example, like a post), a coroutine operation is launched but index is getting reset and DetailScreen scroll to original index instead of staying at liked post. How would i resolve this? (I know about rememberLazyListState())
#Composable
fun DetailScreen(
viewModel: MyViewModel,
index: Int? // index is coming from navGraph
) {
val postIndex by remember { mutableStateOf(index) }
val scope = rememberCoroutineScope()
val posts = remember(viewModel) { viewModel.posts }.collectAsLazyPagingItems()
Scaffold(
topBar = { MyTopBar() }
) { innerPadding ->
JustNestedScreen(
modifier = Modifier.padding(innerPadding),
posts = posts,
onLike = { post ->
// This is causing index to reset, maybe due to re-composition
scope.launch {
viewModel.toggleLike(
postId = post.postId,
isLiked = post.isLiked
)
}
},
indexToScroll = postIndex
)
}
}
#Composable
fun JustNestedScreen(
modifier: Modifier = Modifier,
posts: LazyPagingItems<ExplorePost>,
onLike: (Post) -> Unit,
indexToScroll: Int? = null
) {
val scope = rememberCoroutineScope()
val listState = rememberLazyListState()
LazyColumn(
modifier = modifier
.fillMaxSize()
.background(MaterialTheme.colors.background),
state = listState
) {
items(posts) { post ->
post?.let {
// Display image in box and some buttons
FeedPostItem(
post = it,
onLike = onLike,
)
}
}
indexToScroll?.let { index ->
scope.launch {
listState.scrollToItem(index = index)
}
}
}
}
Use LaunchedEffect. LaunchedEffect's block is only run the first time and then every time keys are changed. If you only want to run it once, use Unit or listState as a key:
LaunchedEffect(listState) {
indexToScroll?.let { index ->
listState.scrollToItem(index = index)
}
}
Here is my problem;
When I add MyText composable in my Screen, I see all Logs (value1, value2, value3) which means it is recomposing every part of my code.
However when I comment the MyText line, I see only value3 on Logcat
How can I fix this ? I know it is not a big problem here but just imagine we have a scrollable Column here and we are trying to pass ScrollState.value to My Text component. Because of this situation, our list becomes so laggy.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Screen()
}
}
}
#Composable
fun Screen(){
var counter by remember {
mutableStateOf(0)
}
Log.i("RECOMPOSE","VALUE1")
Column() {
Text(text = "Just a text")
Log.i("RECOMPOSE","VALUE2")
Button(onClick = { counter = counter.plus(1) }) {
Text(text = counter.toString())
Log.i("RECOMPOSE","VALUE3")
}
MyText(counter)
}
}
#Composable
fun MyText(counter:Int){
Text(text = counter.toString())
}
EDIT
There is main problem, with Scrollable Column;
#Composable
fun Screen(){
val scrollState = rememberScrollState()
Box() {
Column(modifier = Modifier
.verticalScroll(scrollState)
.padding(top = 50.dp)) {
//Some Static Column Elements with images etc.
}
MyText(scrollStateValue = scrollState.value) //Doing some UI staff in this component
}
}
#Composable
fun MyText(scrollStateValue:Int){
Text(text = scrollStateValue.toString())
}
This behaviour is totally expected.
Compose is trying to reduce number of recompositions as much as possible. When you comment out MyText, the only view that depends on counter is Button content, so this is the only view that needs to be recomposed.
By the same logic you shouldn't see VALUE1 logs more than once, but the difference here is that Column is inline function, so if it content needs to be recomposed - it gets recomposed with the containing view.
Using this knowledge, you can easily prevent a view from being recomposed: you need to move part, which doesn't depends on the state, into a separate composable. The fact that it uses scrollState won't make it recompose, only reading state value will trigger recomposition.
#Composable
fun Screen(){
val scrollState = rememberScrollState()
Box() {
YourColumn(scrollState)
MyText(scrollStateValue = scrollState.value) //Doing some UI staff in this component
}
}
#Composable
fun YourColumn(scrollState: ScrollState){
Column(modifier = Modifier
.verticalScroll(scrollState)
.padding(top = 50.dp)) {
//Some Static Column Elements with images etc.
}
}
In this app, I have a screen where you can enter a title and content for a Note.
The screen has two composables DetailScreen() and DetailScreenContent.
Detailscreen has the scaffold and appbars and calls DetailScreenContents() which has two TextFields and a button.
I'm expecting the user to enter text in these fields and then press the button which will package the text into a NOTE object. My question is, how to pass the NOTE to the upper composable which is DETAILSCREEN() with a callback like=
onclick: -> Note or any other efficient way?
#Composable
fun DetailScreen(navCtl : NavController, mviewmodel: NoteViewModel){
Scaffold(bottomBar = { TidyBottomBar()},
topBar = { TidyAppBarnavIcon(
mtitle = "",
onBackPressed = {navCtl.popBackStack()},
)
}) {
DetailScreenContent()
}
}
#Composable
fun DetailScreenContent() {
val titleValue = remember { mutableStateOf("")}
val contentValue = remember { mutableStateOf("")}
val endnote by remember{ mutableStateOf(Note(
Title = titleValue.value,
Content = contentValue.value))}
Column(modifier = Modifier.fillMaxSize()) {
OutlinedTextField(value = titleValue.value,
onValueChange = {titleValue.value = it},
singleLine = true,
label = {Text("")}
,modifier = Modifier
.fillMaxWidth()
.padding(start = 3.dp, end = 3.dp),
shape = cardShapes.small
)
OutlinedTextField(value = contentValue.value, onValueChange = {
contentValue.value = it
},
label = {Text("Content")}
,modifier = Modifier
.fillMaxWidth()
.padding(start = 3.dp, end = 3.dp, top = 3.dp)
.height(200.dp),
shape = cardShapes.small,
)
Row(horizontalArrangement = Arrangement.End,
modifier = Modifier.fillMaxWidth()){
Button(onClick = {
/**return the object to the upper composable**/
}, shape = cardShapes.small) {
Text(text = stringResource(R.string.Finish))
}
}
}
You could use state hoisting. Using lambdas is the most common way of hoisting state here.
Ok so here's DetailScreenContent(), say
fun DetailScreenContent(
processNote: (Note) -> Unit
){
Button( onClick = { processNote(/*Object to be "returned"*/) }
}
We are not literally returning anything, but we are hoisting the state up the hierarchy. Now, in DetailsScreen
fun DetailScreen(navCtl : NavController, mviewmodel: NoteViewModel){
Scaffold(bottomBar = { TidyBottomBar()},
topBar = { TidyAppBarnavIcon(
mtitle = "",
onBackPressed = {navCtl.popBackStack()},
)
}) {
DetailScreenContent(
processNote = {note -> //This is the passed object
/*Perform operations*/
}
)
//You could also extract the processNote as a variable, like so
/*
val processNote = (Note) {
Reference the note as "it" here
}
*/
}
}
This assumes that there is a type Note (something like a data class or so, the object of which type is being passed up, get it?)
That's how we hoist our state and hoist it up to the viewmodel. Remember, compose renders state based on variables here, making it crucial to preserve the variables, making sure they are not modified willy nilly and read from random places. There should be, at a time, only one instance of the variables, which should be modified as and when necessary, and should be read from a common place. This is where viewmodels are helpful. You store all the variables (state) inside the viewmodel, and hoist the reads and modifications to there. It must act as a single source of truth for the app.