Starting to Learn JetPack Compose. I'm struggling now with State hosting. I have this simple example to press a Button to show in a Text component the content of an array. I'm able to make it work if the variable is inside my #Composable. When applying State Hoisting (taking the variable for my composable) I'm finding some issues.
This is the code that is working fine
var listadoNumeros = listOf<Int>(1, 2, 3, 4, 5)
NextValue(listadoNumeros)
#Composable
fun NextValue(listado: List<Int>) {
var position by rememberSaveable {mutableStateOf (0)}
Column(
modifier = Modifier
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceAround
){
Text(text = "Value in Array ${listado[position]}")
Button(onClick = { position += 1 }) {
Text(text = "Next")
}}}
This is the code that is not working correctly
var listadoNumeros = listOf<Int>(1, 2, 3, 4, 5)
var n by rememberSaveable {mutableStateOf (0)}
NextValue(position = n,listadoNumeros)
#Composable
fun NextValue(position:Int,listado: List<Int>) {
Column(
modifier = Modifier
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceAround
) {
Text(text = "Value in Array ${listado[position]}")
Button(onClick = { position += 1 }) {
Text(text = "Next")
}}}
Error message, as you can expect, is "position can't be reassigned". I see why, but don't know how to fix it. I read about onValueChanged in TextField, etc, but don't know if it's applicable here.
position += 1 is equivalent to position = position + 1. In Kotlin, function arguments are val and cannot be reassigned within the scope of the function. That is why the compiler will complain and prevent you from doing that.
What you want to do is to add an extra event callback within the function and perform this addition at the function call site.
#Composable
fun NextValue(position: Int, listado: List<Int>, onPositionChange: (Int) -> Unit) {
Column(
modifier = Modifier
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceAround
) {
Text(text = "Value in Array ${listado[position]}")
Button(onClick = { onPositionChange(position + 1) }) {
Text(text = "Next")
}
}
}
You can use the above composable as
val listadoNumeros = listOf<Int>(1, 2, 3, 4, 5)
var n by remember { mutableStateOf(0) }
NextValue(position = n, listadoNumeros, onPositionChange = { newPosition -> n = newPosition})
Whenever the user clicks on the button, an event is sent back up to the caller and the caller decides what to do with the updated information. In this case, recreate the composable with an updated value.
Related
I have implemented a discrete slider like this:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
YourProjectNameTheme(darkTheme = false) {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(all = 4.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
MyUI()
}
}
}
}
}
}
#Composable
private fun MyUI() {
var sliderValue by remember {
mutableStateOf(1f)
}
Slider(
value = sliderValue,
onValueChange = { sliderValue_ ->
sliderValue = sliderValue_
},
onValueChangeFinished = {
// this is called when the user completed selecting the value
},
valueRange = 1f..21f,
steps = 6
)
Text(text = sliderValue.toString())
}
The output:
I'm expecting exact numbers (like 3, 6, 9, 12, 15, 18) when I tap on the tick marks. But, it is showing the nearest float value. How to fix this?
The steps attribute if greater than 0, specifies the amounts of discrete values, evenly distributed between across the whole value range.
In you case you have to use valueRange = 0f..21f
Slider(
//...
valueRange = 0f..21f,
steps = 6
)
That's because you are starting from 1 instead of 0. Please plan the value and the steps accordingly,
#Preview
#Composable
private fun MyUI() {
var sliderValue by remember {
mutableStateOf(0)
}
Slider(value = sliderValue.toFloat(), onValueChange = { sliderValue_ ->
sliderValue = sliderValue_.toInt()
}, onValueChangeFinished = {
// this is called when the user completed selecting the value
}, valueRange = 0f..21f, steps = 6
)
Text(text = sliderValue.toString())
}
I have a 3 Column. In 1st Column of components are 2nd and 3rd Column. In 2nd Column there are so many components inside that. In last 3rd Column I have a few items and I am sticking at the bottom of screen. I have done with the help of this answer. In smaller screen item is going behind, so my supervisor mention that all item will automatically scroll of 2nd Column which is clearly above of 3rd Column.
#Composable
fun Xyz(){
Theme {
Column(
modifier = Modifier
.padding(dimensionResource(R.dimen.margin_screen_edge_sides))
.fillMaxSize()
.verticalScroll(rememberScrollState()),
// verticalArrangement = Arrangement.Top
or
// verticalArrangement = Arrangement.Arrangement.SpaceBetween
) {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally,
) {
// so many item in here.
// If here items is behind of 3rd column then automatically scroll the item when user land of this this screen
}
Column {
Button()
// may be more item in here
}
}
}
}
Actual Output
Expected Output
Scenario 1
Note:- Item will be increase in 2nd Column i.e. I added logic in AnimatedVisibility so when recompose it will added the item.
Scenario 2
When no item is going behind the 3rd Column then my screen will not scroll anything
if you have question please ask me. Many Thanks
UPDATE
#Composable
fun Xyz(){
Theme {
val scrollState = rememberScrollState()
LaunchedEffect(
keyOneIsTrue,
keyTwoIsTrue
) {
val newValue = scrollState.maxValue
scrollState.animateScrollTo(newValue)
}
Column(
modifier = Modifier
.padding(dimensionResource(R.dimen.margin_screen_edge_sides))
.fillMaxSize()
.verticalScroll(rememberScrollState()),
// verticalArrangement = Arrangement.Top
or
// verticalArrangement = Arrangement.Arrangement.SpaceBetween
) {
Column(
modifier = Modifier
.verticalScroll(scrollState)
.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally,
) {
// so many item in here.
// If here items is behind of 3rd column then automatically scroll the item when user land of this this screen
}
Column {
Button()
// may be more item in here
}
}
}
}
I have so many keys but I am giving you few example in my LaunchedEffect. When keyOneIsTrue it goes inside the LaunchedEffect and then newValue always return 0 value. This is same happening in keyTwoIsTrue and nothing will scroll :(
Note when any key change it means I am changing visibility of items in 2nd Column by the help of AnimatedVisibility
UPDATE 2
I am adding real time example which is auto scrolling are not working when item is added in the list.
#Preview(showBackground = true, widthDp = 250, heightDp = 320)
#Composable
fun Xyz() {
Theme {
var itemClicked by remember { mutableStateOf(0) }
val favourites = remember { mutableStateListOf<String>() }
val scrollState = rememberScrollState()
LaunchedEffect(favourites.size > 0) {
scrollState.animateScrollTo(scrollState.maxValue)
}
Column(
modifier = Modifier
.padding(dimensionResource(R.dimen.margin_screen_edge_sides))
.fillMaxSize()
.verticalScroll(rememberScrollState()),
) {
Column(
modifier = Modifier
.verticalScroll(scrollState)
.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally,
) {
favourites.forEach { text ->
Text(
text = text,
fontSize = 30.sp,
color = Red
)
}
}
Column {
Button(onClick = {
itemClicked++
favourites.add("item clicked $itemClicked")
}) {
Text(text = "Add me")
}
}
}
}
}
You can clearly see in video when item add in the list the auto scroll is not working. It only scrolling when user do manually.
You can use the ScrollState to scroll the content of the Column.
val scrollState = rememberScrollState()
Column(
//...
){
Column(
Modifier
.verticalScroll(scrollState)
//....
}
Then you can use your favorite logic to scroll the content.
For example when the screen is loaded:
LaunchedEffect(Unit){
scrollstate.animateScrollTo(scrollstate.maxValue)
}
If you have a list and you want to scroll when a new item is loaded inside the scrollable Column:
val favourites = remember { mutableStateListOf<String>() }
val scrollState = rememberScrollState()
LaunchedEffect(favourites.size) {
scrollState.animateScrollTo(scrollState.maxValue)
}
I've a data class:
data class Feed_Status(val img:Int, val name_id: String)
I've a class:
class Feed_helper {
fun Image_getter(): List<() -> Feed_Status> {
val Images = listOf {
Feed_Status(R.drawable.image_demo1, "name1")
Feed_Status(R.drawable.image_demo2, "name2")
Feed_Status(R.drawable.image_demo3, "name3")
Feed_Status(R.drawable.image_demo4, "name4")
Feed_Status(R.drawable.image_demo5, "name5")
Feed_Status(R.drawable.image_demo6, "name6")
Feed_Status(R.drawable.image_demo7, "name7")
Feed_Status(R.drawable.image_demo8, "name8")
Feed_Status(R.drawable.image_demo9, "name9")
Feed_Status(R.drawable.image_demo10, "name10")
Feed_Status(R.drawable.image_demo11, "name11")
Feed_Status(R.drawable.image_demo12, "name12")
Feed_Status(R.drawable.image_demo13, "name13")
Feed_Status(R.drawable.image_demo14, "name14")
Feed_Status(R.drawable.image_demo15, "name15")
Feed_Status(R.drawable.image_demo16, "name16")
Feed_Status(R.drawable.image_demo17, "name17")
Feed_Status(R.drawable.image_demo18, "name18")
Feed_Status(R.drawable.image_demo19, "name19")
Feed_Status(R.drawable.image_demo20, "name20")
Feed_Status(R.drawable.image_demo21, "name21")
Feed_Status(R.drawable.image_demo22, "name22")
Feed_Status(R.drawable.image_demo23, "name23")
Feed_Status(R.drawable.image_demo24, "name24")
Feed_Status(R.drawable.image_demo25, "name25")
Feed_Status(R.drawable.image_demo25, "name26")
}
return Images
}
}
through which I'm calling items() in lazyRow
#Composable
fun feed() {
LazyColumn(
reverseLayout = false,
modifier = Modifier
.fillMaxSize(),
userScrollEnabled = true
) {
// Status(es)
item {
LazyRow(
reverseLayout = false,
modifier = Modifier
.fillMaxWidth()
.height(100.dp),
horizontalArrangement = Arrangement.SpaceBetween,
userScrollEnabled = true
) {
val statuses = Feed_helper().Image_getter()
items(statuses) { status ->
Column(
verticalArrangement = Arrangement.Center,
modifier = Modifier
.width(80.dp)
) {
Card(
shape = CircleShape,
modifier = Modifier
.padding(8.dp)
.size(64.dp)
) {
Image(
painterResource(id = status.img),
contentDescription = status.name_id + "'s status",
contentScale = ContentScale.Crop
)
}
Text(
text = status.name_id,
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
)
}
}
}
}
}
}
But whenever I'm calling element from statuses through statuses in items() it is giving me Reference Not found!
Callers:
painterResource(id = status.img) in Image()
contentDescription = status.name_id + "'s status" in Image()
text = status.name_id in Text
All callers are in items(statuses){ status ->
I've been trying to solve this for hours. So any help will be very apreciated.
If you find any typo please update it or tell me to fix.
PS: This is my first time here and I have an almost zero experience on android development and Kotlin. I developed terminal apps and worked on ML kinda work in Python, C++, C. So I may need more information in explanation. I started learning Android Development a week ago only.
Edit: You can ask me any more information.
Peace
Change the return type of Image_getter to List<Feed_status> instead of it having a type of lambda, and change the braces to parenthesis when you declare the list of them.
class Feed_helper {
fun Image_getter(): List<Feed_Status> { // change it to this instead of type of lambda `() -> Feed_status`
val Images = listOf ( // change it to this instead of braces
...
)
return Images
}
}
and import
import androidx.compose.foundation.lazy.items
I'm starting to learn about Jetpack Compose. I put together this app where I explore different day-to-day use cases, each of the feature modules within this project is supposed to tackle different scenarios.
One of this feature modules – the chatexample feature module, tries to implement a simple ViewPager where each of the pages is a Fragment, the first page "Messages" is supposed to display a paginated RecyclerView wrapped around a SwipeRefreshLayout. Now, the goal is to implement all this using Jetpack Compose. This is the issue I'm having right now:
The PullRefreshIndicator that I'm using to implement the Pull-To-Refresh action works as expected and everything seems pretty straightforward so far, but I cannot figure out why the ProgresBar stays there on top.
So far I've tried; Carrying on the Modifier from the parent Scaffold all the way through. Making sure I explicitly set the sizes to fit the max height and width. Add an empty Box in the when statement - but nothing has worked so far, I'm guessing I could just remove the PullRefreshIndicator if I see that the ViewModel isn't supposed to be refreshing, but I don't think that's the right thing to do.
To quickly explain the Composables that I'm using here I have:
<Surface>
<Scaffold> // Set with a topBar
<Column>
<ScrollableTabRow>
<Tab/> // Set for the first "Messages" tab
<Tab/> // Set for the second "Dashboard" tab
</ScrollableTabRow>
<HorizontalPager>
// ChatExampleScreen
<Box> // A Box set with the pullRefresh modifier
// Depending on the ChatExamleViewModel we might pull different composables here
</PullRefreshIndicator>
</Box>
// Another ChatExampleScreen for the second tab
</HorizontalPager>
</Column>
<Scaffold>
</Surface>
Honestly, I don't get how the PullRefreshIndicator that is in a completely different Composable (ChatExampleScreen) gets to overlap with the ScrollableTabRow that is outside.
Hope this makes digesting the UI a bit easier. Any tip, advice, or recommendation is appreciated. Thanks! 🙇
Edit: Just to be completely clear, what I'm trying to achieve here is to have a PullRefreshIndicator on each page. Something like this:
On each page, you pull down, see the ProgressBar appear, and when it is done, it goes away, within the same page. Not overlapping with the tabs above.
A comparatively easier solution in my case was to simply give the Box that contains my vertically scrollable Composable and my PullRefreshIndicator a zIndex of -1f:
Box(Modifier.fillMaxSize().zIndex(-1f)) {
LazyColumn(...)
PullRefreshIndicator(...)
}
And that already did the trick for me. I have a very similar setup to the OP, a Scaffold containing a ScrollableTabRow and a HorizontalPager with refreshable lists on the individual tabs.
I want to leave my first answer as I feel it will still be useful to future readers, so heres another one you might consider.
One of the Box in the tabs has a scroll modifier though, because according to the Accompanist Docs and the actual functionality.
… The content needs to be 'vertically scrollable' for SwipeRefresh()
to be able to react to swipe gestures. Layouts such as LazyColumn are
automatically vertically scrollable, but others such as Column or
LazyRow are not. In those instances, you can provide a
Modifier.verticalScroll modifier…
It's from accompanist documentation about the migration of the API but it still applies to this current one in compose framework.
The way I understand it is a scroll event should be present for the PullRefresh to get activated manually (i.e a layout/container with a vertical scroll modifier or a LazyColumn), something that will consume a drag/swipe event in the screen.
Here's the short working sample. All of these are copy-and-paste-able.
Activity:
class PullRefreshActivity: ComponentActivity() {
private val viewModel: MyViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyAppTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = { TopAppBarSample() }
) {
MyScreen(
modifier = Modifier.padding(it),
viewModel = viewModel
)
}
}
}
}
}
}
Some data classes:
data class MessageItems(
val message: String = "",
val author: String = ""
)
data class DashboardBanner(
val bannerMessage: String = "",
val content: String = ""
)
ViewModel:
class MyViewModel: ViewModel() {
var isLoading by mutableStateOf(false)
private val _messageState = MutableStateFlow(mutableStateListOf<MessageItems>())
val messageState = _messageState.asStateFlow()
private val _dashboardState = MutableStateFlow(DashboardBanner())
val dashboardState = _dashboardState.asStateFlow()
fun fetchMessages() {
viewModelScope.launch {
isLoading = true
delay(2000L)
_messageState.update {
it.add(
MessageItems(
message = "Hello First Message",
author = "Author 1"
),
)
it.add(
MessageItems(
message = "Hello Second Message",
author = "Author 2"
)
)
it
}
isLoading = false
}
}
fun fetchDashboard() {
viewModelScope.launch {
isLoading = true
delay(2000L)
_dashboardState.update {
it.copy(
bannerMessage = "Hello World!!",
content = "Welcome to Pull Refresh Content!"
)
}
isLoading = false
}
}
}
Tab Screen Composables:
#Composable
fun MessageTab(
myViewModel : MyViewModel
) {
val messages by myViewModel.messageState.collectAsState()
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
items(messages) { item ->
Column(
modifier = Modifier
.fillMaxWidth()
.border(BorderStroke(Dp.Hairline, Color.DarkGray)),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = item.message)
Text(text = item.author)
}
}
}
}
#Composable
fun DashboardTab(
myViewModel: MyViewModel
) {
val banner by myViewModel.dashboardState.collectAsState()
Box(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState()),
contentAlignment = Alignment.Center
) {
Column {
Text(
text = banner.bannerMessage,
fontSize = 52.sp
)
Text(
text = banner.content,
fontSize = 16.sp
)
}
}
}
Finally, the composable that contains the PullRefresh and the Pager/Tab components, and all of them are direct children of a ConstraintLayout. So to achieve a PullRefresh behind the Tabs but still on top of the HorizontalPager, first I had to put the HorizontalPager as the first child, the PullRefresh as the second and the Tabs as the last one, constraining them accordingly to preserve the visual arrangement of a Tab Pager.
#OptIn(ExperimentalMaterialApi::class, ExperimentalPagerApi::class)
#Composable
fun MyScreen(
modifier : Modifier = Modifier,
viewModel: MyViewModel
) {
val refreshing = viewModel.isLoading
val pagerState = rememberPagerState()
val pullRefreshState = rememberPullRefreshState(
refreshing = refreshing,
onRefresh = {
when (pagerState.currentPage) {
0 -> {
viewModel.fetchMessages()
}
1 -> {
viewModel.fetchDashboard()
}
}
},
refreshingOffset = 100.dp // just an arbitrary offset where the refresh will animate
)
ConstraintLayout(
modifier = modifier
.fillMaxSize()
.pullRefresh(pullRefreshState)
) {
val (pager, pullRefresh, tabs) = createRefs()
HorizontalPager(
count = 2,
state = pagerState,
modifier = Modifier.constrainAs(pager) {
top.linkTo(tabs.bottom)
start.linkTo(parent.start)
end.linkTo(parent.end)
bottom.linkTo(parent.bottom)
height = Dimension.fillToConstraints
}
) { page ->
when (page) {
0 -> {
MessageTab(
myViewModel = viewModel
)
}
1 -> {
DashboardTab(
myViewModel = viewModel
)
}
}
}
PullRefreshIndicator(
modifier = Modifier.constrainAs(pullRefresh) {
top.linkTo(parent.top)
start.linkTo(parent.start)
end.linkTo(parent.end)
},
refreshing = refreshing,
state = pullRefreshState,
)
ScrollableTabRow(
modifier = Modifier.constrainAs(tabs) {
top.linkTo(parent.top)
start.linkTo(parent.start)
end.linkTo(parent.end)
},
selectedTabIndex = pagerState.currentPage,
indicator = { tabPositions ->
TabRowDefaults.Indicator(
modifier = Modifier.tabIndicatorOffset(
currentTabPosition = tabPositions[pagerState.currentPage],
)
)
},
) {
Tab(
selected = pagerState.currentPage == 0,
onClick = {},
text = {
Text(
text = "Messages"
)
}
)
Tab(
selected = pagerState.currentPage == 1,
onClick = {},
text = {
Text(
text = "Dashboard"
)
}
)
}
}
}
output:
<Surface>
<Scaffold>
<ConstraintLayout>
// top to ScrollableTabRow's bottom
// start, end, bottom to parent's start, end and bottom
// 0.dp (view), fillToConstraints (compose)
<HorizontalPager>
<PagerScreens/>
</HorizontalPager>
// top, start, end of parent
<PullRefreshIndicator/>
// top, start and end of parent
<ScrollableTabRow>
<Tab/> // Set for the first "Messages" tab
<Tab/> // Set for the second "Dashboard" tab
</ScrollableTabRow>
</ConstraintLayout>
<Scaffold>
</Surface>
I think there's nothing wrong with the PullRefresh api and the Compose/Accompanist Tab/Pager api being used together, it seems like the PullRefresh is just respecting the placement structure of the layout/container it is put into.
Consider this code, no tabs, no pager, just a simple set-up of widgets that is identical to your set-up
Column(
modifier = Modifier.padding(it)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(80.dp)
.background(Color.Blue)
)
val pullRefreshState = rememberPullRefreshState(
refreshing = false,
onRefresh = { viewModel.fetchMessages() }
)
Box(
modifier = Modifier.pullRefresh(pullRefreshState)
) {
PullRefreshIndicator(
modifier = Modifier.align(Alignment.TopCenter),
refreshing = false,
state = pullRefreshState,
)
}
}
What it looks like.
The PullRefresh is placed inside a component(Box) that is placed below another component in a Column vertical placement, and since it's below another widget, its initial position will not be hidden like the image sample.
With your set-up, since I noticed that the ViewModel is being shared by the tabs and also the reason why I was confirming if you are decided with your architecture is because the only fix I can think of is moving the PullRefresh up in the sequence of the composable widgets.
First changes I made is in your ChatExampleScreen composable, which ended up like this, all PullRefresh components are removed.
#Composable
fun ChatExampleScreen(
chatexampleViewModel: ChatExampleViewModel,
modifier: Modifier = Modifier
) {
val chatexampleViewModelState by chatexampleViewModel.state.observeAsState()
Box(
modifier = modifier
.fillMaxSize()
) {
when (val result = chatexampleViewModelState) {
is ChatExampleViewModel.State.SuccessfullyLoadedMessages -> {
ChatExampleScreenSuccessfullyLoadedMessages(
chatexampleMessages = result.list,
modifier = modifier,
)
}
is ChatExampleViewModel.State.NoMessagesFetched -> {
ChatExampleScreenEmptyState(
modifier = modifier
)
}
is ChatExampleViewModel.State.NoInternetConnectivity -> {
NoInternetConnectivityScreen(
modifier = modifier
)
}
else -> {
// Agus - Do nothing???
Box(modifier = modifier.fillMaxSize())
}
}
}
}
and in your Activity I moved all the setContent{…} scope into another function named ChatTabsContent and placed everything inside it including the PullRefresh components.
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun ChatTabsContent(
modifier : Modifier = Modifier,
viewModel : ChatExampleViewModel
) {
val chatexampleViewModelIsLoadingState by viewModel.isLoading.observeAsState()
val pullRefreshState = rememberPullRefreshState(
refreshing = chatexampleViewModelIsLoadingState == true,
onRefresh = { viewModel.fetchMessages() }
)
Box(
modifier = modifier
.pullRefresh(pullRefreshState)
) {
Column(
Modifier
.fillMaxSize()
) {
val pagerState = rememberPagerState()
ScrollableTabRow(
selectedTabIndex = pagerState.currentPage,
indicator = { tabPositions ->
TabRowDefaults.Indicator(
modifier = Modifier.tabIndicatorOffset(
currentTabPosition = tabPositions[pagerState.currentPage],
)
)
}
) {
Tab(
selected = pagerState.currentPage == 0,
onClick = { },
text = {
Text(
text = "Messages"
)
}
)
Tab(
selected = pagerState.currentPage == 1,
onClick = { },
text = {
Text(
text = "Dashboard"
)
}
)
}
HorizontalPager(
count = 2,
state = pagerState,
modifier = Modifier.fillMaxWidth(),
) { page ->
when (page) {
0 -> {
ChatExampleScreen(
chatexampleViewModel = viewModel,
modifier = Modifier.fillMaxSize()
)
}
1 -> {
ChatExampleScreen(
chatexampleViewModel = viewModel,
modifier = Modifier.fillMaxWidth()
)
}
}
}
}
PullRefreshIndicator(
modifier = Modifier.align(Alignment.TopCenter),
refreshing = chatexampleViewModelIsLoadingState == true,
state = pullRefreshState,
)
}
}
which ended up like this
setContent {
TheOneAppTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = { TopAppBarSample() }
) {
ChatTabsContent(
modifier = Modifier.padding(it),
viewModel = viewModel
)
}
}
}
}
Result:
Structural changes.
<Surface>
<Scaffold> // Set with a topBar
<Box>
<Column>
<ScrollableTabRow>
<Tab/> // Set for the first "Messages" tab
<Tab/> // Set for the second "Dashboard" tab
</ScrollableTabRow>
<HorizontalPager>
<Box/>
</HorizontalPager>
</Column>
// pull refresh is now at the most "z" index of the
// box, overlapping the content (tabs/pager)
<PullRefreshIndicator/>
</Box>
<Scaffold>
</Surface>
I haven't explored this API yet, but it looks like it should be used directly in a z-oriented layout/container parent such as Box as the last child.
I just want to share more details about the issue here and what the solution is. I appreciate a lot the solutions shared above and these were definitely key to figuring the problem out.
The bare-minimum solution here is to replace the Box with a ConstraintLayout in the ChatScreenExample composable:
Why? Because as #z.y shared above the PullRefreshIndicator needs to be contained on a "vertically scrollable" composable, and while the Box composable can be set with the vericalScroll() modifier we need to make sure we constraint the height of the content, that's why we had to change to a ConstraintLayout.
Feel free to correct me if I'm missing something.
There is yet another solution to this problem, which is using a .clipToBounds() modifier over the tab content container.
I am showing a list of rows with one word in it, inside a LazyColumn. On clicking the row, an edit form opens. The data is coming from a room database.
Since the row is on a separate composable function, I can open many different edit forms together (one in each row). But I want to show only one edit form in the whole list at a time. If I click one row to open an edit form, the rest of the open forms on the other rows should be closed. How can I do that?
Here is the code:
val words: List<Word> by wordViewModel.allWords.observeAsState(listOf())
var newWord by remember { mutableStateOf("") }
val context = LocalContext.current
val keyboardController = LocalSoftwareKeyboardController.current
LazyColumn(
modifier = Modifier
.weight(1f)
.padding(vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
items(words) { word ->
WordItemLayout(
word = word,
onSaveUpdatedWord = { onUpdateWord(it) },
onTrashClicked = { onDeleteWord(it) }
)
}
}
#Composable
fun WordItemLayout(word: Word, onSaveUpdatedWord: (Word) -> Unit, onTrashClicked: (Word) -> Unit) {
var showEditForm by remember { mutableStateOf(false) }
var editedWord by remember { mutableStateOf(word.word) }
val context = LocalContext.current
Column {
Row(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colors.primaryVariant)
.padding(vertical = 12.dp, horizontal = 24.dp)
.clickable {
showEditForm = !showEditForm
editedWord = word.word
},
verticalAlignment = Alignment.CenterVertically,
) {
Image(painter = painterResource(R.drawable.ic_star), contentDescription = null)
Text(
text = word.word,
color = Color.White,
fontSize = 20.sp,
modifier = Modifier
.padding(start = 16.dp)
.weight(1f)
)
// Delete Button
IconButton(
onClick = {
showEditForm = false
onTrashClicked(word)
Toast.makeText(context, "Word deleted", Toast.LENGTH_SHORT).show()
},
modifier = Modifier.size(12.dp)
) {
Icon(
imageVector = Icons.Filled.Delete,
contentDescription = "Delete Word",
tint = Color.White
)
}
}
// word edit form
if (showEditForm) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.Bottom
) {
TextField(
value = editedWord,
onValueChange = { editedWord = it },
modifier = Modifier.weight(1f),
colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.White) // TextField Background Color
)
// Update Button
Button(
onClick = {
val updatedWord: Word = word
if (updatedWord.word != editedWord.trim()) {
updatedWord.word = editedWord.trim()
onSaveUpdatedWord(updatedWord)
Toast.makeText(context, "Word updated", Toast.LENGTH_SHORT).show()
}
showEditForm = false
},
modifier = Modifier.padding(start = 8.dp)
) {
Icon(imageVector = Icons.Filled.Done, contentDescription = "Update Word")
}
}
}
}
}
Thanks for your help!
An approach: In your view model, declare an openRowIndex state (this will store the index of the opened row, you can initialize it to -1 for example).
Define a method that can change this state, for example updateOpenRowIndex
I'm not sure what kind of state holder you are using in your view model. I will use StateFlow for this answer. In your view model declare the new state and method:
private val _openRowIndex = MutableStateFlow(-1)
val openRowIndex: StateFlow<Int> = _openRowIndex
fun updateOpenRowIndex(updatedIndex: Int) {
_openRowIndex.value = updatedIndex
}
For each row compisable, pass in the index of it inside the LazyColumn. You can get the indices using the itemsIndexed method. Also collect your openRowIndex, and pass that to the composable as well. Pass in also the method that updates the open row index:
itemsIndexed(words) { index, word ->
//get the current opened row state and collect it (might look different for you if you are not using StateFlow):
val openRowIndex = wordViewModel.openRowIndex.collectAsState()
WordItemLayout(
word = word,
onSaveUpdatedWord = { onUpdateWord(it) },
onTrashClicked = { onDeleteWord(it) },
index = index, //new parameter!
openRowIndex = openRowIndex.value //new parameter!
onUpdateOpenedRow = wordViewModel::updateOpenRowIndex //new parameter!
)
}
Now, in the row composable, simply check if the index and openRowIndex match, and display an opened row only if they match. Now to update the open row: make the Row clickable, and on click use view models updateOpenRowIndex method to update state to index. Compose will handle the rest and recompose when the state changes with the newly opened row!
fun WordItemLayout(
word: Word,
onSaveUpdatedWord: (Word) -> Unit,
onTrashClicked: (Word), -> Unit,
index: Int, //new parameters
openRowIndex: Int,
onUpdateOpenedRow: (Int) -> Unit
) {
if(index == openRowIndex) {
//display this row as opened
} else {
//display this row as closed
}
}
As I said, make the row clickable and call the update function:
Row(
modifier = Modifier.clickable {
onUpdateOpenedRow(index)
//additional instructions for what to happen when row is clicked...
}
//additional row parameters...
)