so probably dumb question for someone with experience with Jetpack Compose, but for some reason my Toast are not displaying. Here's the snippet for it. I'm sending 3 toasts with .show() in different parts of the onCreate method, and still nothing happens. I also tried using LocalContext.current as context inside a #Composable function but same result, nothing is displayed. I see online many different examples, even videos on youtube in which this exact same could should run. Anybody knows why?
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Toast.makeText(this, " ASDASDASD ", Toast.LENGTH_LONG).show()
setContent {
Toast.makeText(this, " ASDASDASD ", Toast.LENGTH_LONG).show()
MyApplicationTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
Toast.makeText(this, " ASDASDASD ", Toast.LENGTH_LONG).show()
Greeting("Android")
}
}
}
}
A sample code to show Toast.
setContent {
val context = LocalContext.current
MyAppTheme {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize(),
) {
TextButton(
onClick = {
Toast.makeText(context, "Toast", Toast.LENGTH_LONG).show()
},
) {
Text(text = "Show Toast")
}
}
}
}
Let me break down the important things to look into in this code.
Toast should not be part of the composable code, rather it should be part of the side effect code.
(e.g. onClick(), LaunchedEffect, DisposableEffect, etc.)
Get the context inside the composable code using LocalContext.current. Note that you have to store it in a variable as the code inside onClick() does not have composable scope. (As mentioned above).
Composables recompose very frequently depending on the UI changes, we don't want to show a Toast every time the composable recomposes.
Refer to Side Effects Docs to understand it better.
You need to use a scaffold for displaying toasts
Code:
class SimpleFormActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val scaffoldState = rememberScaffoldState()
var inputFieldState by remember { mutableStateOf("") }
val scope = rememberCoroutineScope()
Scaffold(
modifier = Modifier.fillMaxSize(),
scaffoldState = scaffoldState
) { padding ->
Column(
Modifier
.fillMaxSize()
.padding(50.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
TextField(
value = inputFieldState,
label = {
Text(text = "Enter your name")
},
onValueChange = {
inputFieldState = it
},
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(20.dp))
Button(
onClick = {
scope.launch {
scaffoldState.snackbarHostState.showSnackbar(
inputFieldState,
duration = SnackbarDuration.Short
)
}
}) {
Text(text = "Click")
}
}
}
}
}
}
Demo:
Related
I have an OutlineTextField where I am editing and updating it by calling an API on click of Save button.
Now what I want if the user doesn't change the text, api call should not happen and then onclick of save , there should not be any api call and it should got to the previous screen.
Below is my code snippet:
OutlinedTextField(
value = value,
modifier = modifier,
onValueChange = onValueChange,
placeholder = PlaceholderComponent
)
I solved it by checking initial viewmodel text.
If I've understood the question clearly, as #Gabriele Mariotti suggested, you can store the previous value of the text field and then compare it with the actual in the text field itself on the button click.
You can arrange the code with a Composable function like so:
#Composable
fun SaveButton(
modifier: Modifier = Modifier
) {
var currentValue by remember { mutableStateOf("") }
var previousValue by remember { mutableStateOf(currentValue) }
val context = LocalContext.current
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly,
modifier = Modifier.fillMaxSize()
) {
OutlinedTextField(
value = currentValue,
modifier = modifier,
onValueChange = {
currentValue = it
}
)
Button(onClick = {
if (currentValue != previousValue) {
Toast.makeText(context, "API request started", Toast.LENGTH_SHORT).show()
previousValue = currentValue
// Handle API request
} else {
Toast.makeText(context, "The text has not changed. Returning to the previous screen...", Toast.LENGTH_LONG).show()
// Handle on back screen
}
}) {
Text(text = "Save")
}
}
}
I am working on nested column in jetpack compose. I have one list which is huge amount of data coming from server. I was checked in Layout Inspector and I see that whenever my item is added in list it recompose and increase counts. So my doubt is if I add 100 item in list one by one, so my Nested Column will be 100 times recompose ? If not can someone help me on this please?
ListViewComposableActivity.kt
class ListViewComposableActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AppBarScaffold(
displayHomeAsUpEnabled = true,
titleId = R.string.activity
) {
ListViewItemStateful()
}
}
}
}
ListViewItemStateful
#Composable
fun ListViewItemStateful(
viewModel: ListViewModel = koinViewModel(),
) {
ItemViewListStateless(
uiState = viewModel.uiState,
isEnable = viewModel.isEnable,
scanDeviceList = viewModel.scanResultList,
)
}
ItemViewListStateless
#Composable
fun ItemViewListStateless(
uiState: State,
isEnable: Boolean,
scanDeviceList: SnapshotStateList<ScanResults>,
) {
when (uiState) {
INITIAL,
FIRST -> {
ListContent(isEnable, scanDeviceList)
}
}
}
ListContent
#Composable
fun ListContent(isEnable: Boolean, scanDeviceList: SnapshotStateList<ScanResults>) {
AnimatedVisibility(true) {
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxSize()
.verticalScroll(rememberScrollState()),
) {
if (isEnable) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
DeviceList(
scanDeviceList,
modifier = Modifier.align(Alignment.Start),
)
}
}
}
}
}
DeviceList
#Composable
fun ColumnScope.DeviceList(
scanDeviceList: SnapshotStateList<ScanResults>,
modifier: Modifier = Modifier,
) {
Spacer(modifier = Modifier.height(32.dp))
AnimatedVisibility(
scanDeviceList.isNotEmpty(),
modifier = modifier
) {
Column {
Text(text = "Device List")
scanDeviceList.forEachIndexed { index, scanResults ->
Text(text = scanResults.device.name)
}
}
}
}
ListViewModel.kt
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.viewModelScope
import com.abc.app.common.BaseViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class ListViewModel : BaseViewModel() {
val scanResultList by lazy { mutableStateListOf<ScanResults>() }
var isEnable by mutableStateOf(false)
private set
var uiState by mutableStateOf<State>(State.INITIAL)
private set
init {
viewModelScope.launch {
(0..10).forEach {
delay(2000)
scanResultList.add(ScanResults(Device("item $it")))
}
}
isEnable = true
uiState = State.FIRST
}
}
data class ScanResults(val device: Device)
data class Device(val name: String)
enum class State {
INITIAL,
FIRST
}
I am adding few items in list to show in layout inspector
In above image you can see the DeviceList is recompose 10 times.
I checked in Jetpack Compose: Debugging recomposition around 6:40 min he tried to solve recompose issue and there skipped recomposition count is clear. So why it's showing count in my component tree in recomposition and skipped section? Many thanks
UPDATE
When I was changed to #Thracian answer it still recomposition skip
#Composable
fun ColumnScope.DeviceList(
scanDeviceList:()-> SnapshotStateList<ScanResults>,
modifier: Modifier = Modifier,
) {
Spacer(modifier = Modifier.height(32.dp))
AnimatedVisibility(
scanDeviceList().isNotEmpty(),
modifier = modifier
) {
Column {
Text(text = "Device List")
scanDeviceList().forEachIndexed { index, scanResults ->
Item(scanResults.device)
}
}
}
}
#Composable
private fun Item(device: Device) {
Text(
modifier = Modifier.border(2.dp, getRandomColor()),
text = device.name
)
}
fun getRandomColor() = Color(
red = Random.nextInt(256),
green = Random.nextInt(256),
blue = Random.nextInt(256),
alpha = 255
)
In your question when you add a new item to SnapshotStateList whole Column is composed because Column doesn't create a composition scope due to inline keyword. If you create a scope that scope is recomposed when the value it reads changes. You can refer this question and answer as well.
Jetpack Compose Smart Recomposition
Add an item where Text reads device
#Composable
private fun Item(device: Device) {
Text(
modifier = Modifier.border(2.dp, getRandomColor()),
text = device.name
)
}
Random color is something i use for displaying recomposition visually
fun getRandomColor() = Color(
red = Random.nextInt(256),
green = Random.nextInt(256),
blue = Random.nextInt(256),
alpha = 255
)
Your current setup
With Item Composable that creates scope.
#Composable
fun ColumnScope.DeviceList(
scanDeviceList: SnapshotStateList<ScanResults>,
modifier: Modifier = Modifier,
) {
Spacer(modifier = Modifier.height(32.dp))
AnimatedVisibility(
scanDeviceList.isNotEmpty(),
modifier = modifier
) {
Column {
Text(text = "Device List", color = getRandomColor())
scanDeviceList.forEachIndexed { index, scanResults ->
// Text(
// modifier = Modifier.border(2.dp, getRandomColor()),
// text = scanResults.device.name
// )
Item(scanResults.device)
}
}
}
}
And when you have many items, especially they don't fit viewport you can use LazyColumn instead of Column with verticalScroll to limit recomposition amount to number of items that are visible on viewport or visible area of LazyColumn
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.
here's my situation: I have to show in my app a detail of a record I receive from API. Inside this view, I may or may not need to show some data coming from another viewmodel, based on a field.
Here my code:
#OptIn(ExperimentalMaterial3Api::class)
#Composable
fun ViewDetail(viewModel: MainViewModel, alias: String?, otherViewModel: OtherViewModel) {
viewModel.get(alias)
Scaffold {
val isLoading by viewModel.isLoading.collectAsState()
val details by viewModel.details.collectAsState()
when {
isLoading -> LoadingUi()
else -> Details(details, otherViewModel)
}
}
}
#OptIn(ExperimentalMaterial3Api::class)
#Composable
private fun Details(details: Foo?, otherViewModel: OtherViewModel) {
details?.let { sh ->
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState),
) {
Text(sh.title, fontSize = 24.sp, lineHeight = 30.sp)
Text(text = sh.description)
if (sh.other.isNotEmpty()) {
otherViewModel.load(sh.other)
val others by otherViewModel.list.collectAsState()
Others(others)
}
}
}
}
#OptIn(ExperimentalMaterial3Api::class)
#Composable
private fun Others(others: Flow<PagingData<Other>>) {
val items: LazyPagingItems<Other> = others.collectAsLazyPagingItems()
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
contentPadding = PaddingValues(16.dp),
) {
items(items = items) { item ->
if (item != null) {
Text(text = item.title, fontSize = 24.sp)
Spacer(modifier = Modifier.height(4.dp))
Text(text = item.description)
}
}
if (items.itemCount == 0) {
item { EmptyContent() }
}
}
}
All the description here may be very long, both on the main Details body or in the Others (when present), so here's why the scroll behaviour requested.
Problem: I get this error:
Vertically scrollable component was measured with an infinity maximum height constraints, which is disallowed. One of the common reasons is nesting layouts like LazyColumn and Column(Modifier.verticalScroll()).
I hoped that .wrapContentHeight() inside LazyColumn would do the trick, but to no avail.
Is this the right way to do it?
Context: all packages are updated to the latest versions available on maven
The main idea here is to merge your Column with LazyColumn.
As your code is not runnable, I'm giving more a pseudo code, which should theoretically work.
Also calling otherViewModel.load(sh.other) directly from Composable builder is a mistake. According to thinking in compose, to get best performance your view should be side effects free. To solve this issue Compose have special side effect functions. Right now your code is gonna be called on each recomposition.
if (sh.other.isNotEmpty()) {
LaunchedEffect(Unit) {
otherViewModel.load(sh.other)
}
}
val others by otherViewModel.list.collectAsState()
LazyColumn(
modifier = Modifier
.fillMaxSize()
.wrapContentHeight(),
contentPadding = PaddingValues(16.dp),
) {
item {
Text(sh.title, fontSize = 24.sp, lineHeight = 30.sp)
Text(text = sh.description)
}
items(items = items) { item ->
if (item != null) {
Text(text = item.title, fontSize = 24.sp)
Spacer(modifier = Modifier.height(4.dp))
Text(text = item.description)
}
}
if (items.itemCount == 0) {
item { EmptyContent() }
}
}
You can use a system like the following
#Composable
fun Test() {
Box(Modifier.systemBarsPadding()) {
Details()
}
}
#Composable
fun Details() {
LazyColumn(Modifier.fillMaxSize()) {
item {
Box(Modifier.background(Color.Cyan).padding(16.dp)) {
Text(text = "Hello World!")
}
}
item {
Box(Modifier.background(Color.Yellow).padding(16.dp)) {
Text(text = "Another data")
}
}
item {
Others()
}
}
}
#Composable
fun Others() {
val values = MutableList(50) { it }
values.forEach {
Box(
Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(text = "Value = $it")
}
}
}
The result with scroll is:
I'm currently thinking about creating an initiative tracker app for a friend of mine and since I've been out of touch with app development for quite a while I'm trying to evaluate which tools would best suit my needs.
Since the App is going to be developed for Android, I'm pretty sure I'll be using Kotlin and therefore Jetpack Compose caught my eye.
After making a bit of research and going through the docs though, I'm unsure if it is capable of what I want to achieve:
I want to be able to create a dynamic list of entry cards sorted by a certain value asigned to each card. (Which I'm relatively certain LazyColumns will be able to handle).
The catch is this: Each of these cards needs to have buttons and several additional values that can be manipulated with these buttons.
I haven't been able to find descriptions of something like this in the docs or any examples using Jetpack Compose LazyColumns.
I created a (badly made) mockup to hopefully better describe what it is I want to do:
Would anyone be able to share insights on if Jetpack Compose is capable of these features or if not could share advice on what tool to use instead?
Thanks alot and Kind regards.
Yes, with Compose, you can quite easily make such an application.
Here is a basic example of such a UI.
class MainActivity : FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AppTheme {
ItemsScreen()
}
}
}
}
class Item(val title: String, value1: Int, value2: Int, value3: Int) {
val value1 = mutableStateOf(value1)
val value2 = mutableStateOf(value2)
val value3 = mutableStateOf(value3)
val valueToSortBy: Int
get() = value1.value + value2.value + value3.value
}
class ScreenViewModel : ViewModel() {
val items = List(3) {
Item(
"Item $it",
Random.nextInt(10),
Random.nextInt(10),
Random.nextInt(10),
)
}.toMutableStateList()
fun sortItems() {
items.sortByDescending { it.valueToSortBy }
}
fun addNewItem() {
items.add(
Item(
"Item ${items.count()}",
Random.nextInt(10),
Random.nextInt(10),
Random.nextInt(10),
)
)
}
}
#Composable
fun ItemsScreen() {
val viewModel: ScreenViewModel = viewModel()
LaunchedEffect(viewModel.items.map { it.valueToSortBy }) {
viewModel.sortItems()
}
Box(
Modifier
.fillMaxSize()
.padding(10.dp)
) {
LazyColumn(
contentPadding = PaddingValues(vertical = 10.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
items(viewModel.items) { item ->
ItemView(item)
}
}
FloatingActionButton(
onClick = viewModel::addNewItem,
modifier = Modifier.align(Alignment.BottomEnd)
) {
Text("+")
}
}
}
#Composable
fun ItemView(item: Item) {
Card(
elevation = 5.dp,
) {
Column(modifier = Modifier.padding(10.dp)) {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Text(
item.title,
style = MaterialTheme.typography.h5
)
Spacer(modifier = Modifier.weight(1f))
Text(
"Value to sort by: ${item.valueToSortBy}",
style = MaterialTheme.typography.body1,
fontStyle = FontStyle.Italic,
)
}
Spacer(modifier = Modifier.size(25.dp))
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
CounterView(value = item.value1.value, setValue = { item.value1.value = it })
CounterView(value = item.value2.value, setValue = { item.value2.value = it })
CounterView(value = item.value3.value, setValue = { item.value3.value = it })
}
}
}
}
#Composable
fun CounterView(value: Int, setValue: (Int) -> Unit) {
Row(verticalAlignment = Alignment.CenterVertically) {
CounterButton("+") {
setValue(value + 1)
}
Text(
value.toString(),
modifier = Modifier.padding(5.dp)
)
CounterButton("-") {
setValue(value - 1)
}
}
}
#Composable
fun CounterButton(text: String, onClick: () -> Unit) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.size(45.dp)
.background(Color.LightGray)
.clickable(onClick = onClick)
) {
Text(
text,
color = Color.White,
)
}
}
I'm sorting by sum of 3 values here. Note that sorting items like this may not do much performant in a production app, you should use a database to store items and request sorted items from it.