Compose: Is there an EmptyView similar to Swiftui? - android

On iOS there is EmptyView here https://developer.apple.com/documentation/swiftui/emptyview. But I don't know how to implement it on Compose. If I have it, for some code is much easier for me. For example,
myList.map { item ->
if item is XItem -> EmptyView()
....
}
Don't tell me I need not it, I just know how to implement it. Thanks.

Compose is built much different than SwiftUI.
In SwiftUI you need to use EmptyView in two cases:
When you have a genetic parameter and it should be empty in some cases - e.g. you need to define some default type in case when the parameter is not specified.
When the context requires you to return some view.
On the other side, Compose doesn't have such problems in the first place, that's why no such view exists.
In cases when SwiftUI will give you an error around an empty #ViewBuilder block, Compose will be totally fine.
In your example you can use Unit:
myList.map { item ->
if item is XItem -> Unit
....
}
Or just empty braces:
myList.map { item ->
if item is XItem -> { }
....
}
If you'll find a case when you really need some empty view, you can use Box(Modifier) - it'll be an empty view with zero size.

I think you can use a Spacer component to display an empty space.
Spacer accepts Modifier object as a parameter, you can then use this modifier to set Spacer’s width or height or both.
For instance, you can draw a Spacer in your code but this needs to be done in a composable context or inside another composable.
#Composable
fun MyComposable(){
myList.map { item ->
if item is XItem -> Spacer(modifier = Modifier.size(100.dp, 100.dp))
....
}
}

You can easily create one yourself:
#Composable
fun EmptyView() {
}
which can be replaced / inlined by
{}

Related

Manage condition logic in stateless compose in jetpack compose

I am learning State hosting in jetpack compose. I have created two separated function ContentStateful and ContentStateLess. In my ContentStateLess there is a lot of view inside them and I am checking some condition and change view accordingly. I am guessing that there is no condition/business logic inside Stateful compose. So what is the proper way of doing this kind of logic in here.
ContentStateful
#Composable
fun ContentStateful(
viewModel: PairViewModel = getViewModel()
) {
ContentStateLess(viewModel)
}
ContentStateLess
#Composable
fun ContentStateLess(
viewModel: PairViewModel
) {
Text()
Text()
Image()
if (viewModel.isTrue) {
Image()
// more item here
} else {
Text()
// more item here
}
Image()
}
So what is the best recommendation for this if - else logic in ContentStateLess(). Many Thanks
If you are building stateless Composables it's better not to pass anything like ViewModel. You can pass Boolean parameter instead. When you wish to move your custom Composable to another screen or another project you will need to move ViewModel either.
The reason Google recommends stateless Composables is it's difficult to test, you can easily test a Composable with inputs only.
Another thing you experience the more states inner composables have to more exposure you create for your composable being in a State that you might not anticipate.
When you build simple Composables with one, two, three layers might not be an issue but with more states and layers state management becomes a serious issue. And if you somehow forget or miss a state inside a Composable you might end up with a behavior that's not expected. So to minimize risks and make your Composables testable you should aim to manage your states in one place and possible in a state holder class that wraps multiple states.
#Composable
fun ContentStateLess(
firstOneTrue: Boolean
) {
Text()
Text()
Image()
if (firstOneTrue) {
Image()
// more item here
} else {
Text()
// more item here
}
Image()
}

LazyColumn is showing wrong display when deleting an item

I'm creating an app that, among other things, enables the user to add a Consumer, and then remove him later. The consumers are shown in cards with a remove button in the end.
Adding a consumer works fine. However, when I try to remove a consumer, the one removed in the app screen is always the last one. I know this is not a logic implementation mistake, because I stopped the Debugger right before the items() call, and in any recomposition the list holding the consumers has the correct consumer removed! The following image shows the result after clicking the Remove button from the "B" card (the card removed is "C"!):
Look what the debugger shows right before the recomposition takes place:
The relevant code is below.
The ViewModel and Model (relevant part) definitions:
class ConsumidoresViewModel : ViewModel() {
var lista = mutableStateListOf<Consumidor>()
fun add(consumidor: Consumidor){
lista += consumidor
}
fun remove(consumidor: Consumidor){
lista.remove(consumidor)
}
}
data class Consumidor(var nome: String)
...
The main composable, called directly from .onCreate():
fun UsersView() {
var consumidores: ConsumidoresViewModel = viewModel()
...
LazyColumn() {
items(items = consumidores.lista) { consumidor ->
CardNome(consumidor, consumidores)
}
}
The fucntion call of the Remove button:
IconButton(onClick = { consumidorViewModel.remove(consumidor) }) { ... }
I can't figure out what I'm doing wrong. I'm fairily new with Android Programming / Compose, but I have been programming for decades (not professionaly). Can someone point me to a direction? It probably has something to do with my Sates / View Model implementation, but I can't find out what, as the SnapshotStateList on the debugger clearly shows "A" and "C" cards present, and "B" gone!
Based on the official docs.
By default, each item's state is keyed against the position of the item in the list or grid. However, this can cause issues if the data set changes, since items which change position effectively lose any remembered state. If you imagine the scenario of LazyRow within a LazyColumn, if the row changes item position, the user would then lose their scroll position within the row.
So it's usually a good set up when your data class has a unique property like an id if you plan to manipulate a collection of it (like your removal operation), you can then use it as a key = {...} for the LazyColumn so it knows not to use the index as a unique identifier for its item elements, and that could be the reason why your'e having a wrong display of items after removing an element from the list.
LazyColumn() {
items(items = consumidorList, key = { it.id }) { consumidorItem ->
...
}
}
Update:
Linking my another answer for a movableContentOf{...} sample.

LazyColumn Acknowledging Key inside another Column?

basically I have a giant Lazy Column, but I only want some components inside another component to launch the animation when that child component is visible. I am having so much trouble with the keys, I want to put a key ONLY on a certain composable within the LazyColumn. My issue is that the 'isItemWithKeyInView' is TRUE once the entire Column is showing, so the user is not able to see the animation since its not on the screen yet, but the same Column is visible.
I tried putting a LazyVerticalGrid instead of a Column but it had weird conflicting scrolling with being in a LazyColumn.
val isItemWithKeyinView by remember {
derivedStateOf{
listState.isScrollingInProgress && listState.layoutInfo.visibleItemsInfo.any { it.key == "animate"}
}
}
LazyColumn {
item{ Column{ //Other content }}
item(key="animate"){
Column {
Text()
Image()
// Lots of other composables
AnimateBar{ // Can I put a KEY specifically here???
// I want to launch this composable once it comes into view
}
}
item{ Column{ //Other content }}
}
}

Jetpack Compose: Explain to me why the list content is not updated?

I know that in Jetpack Compose you have to change the state of the passed in data in order to trigger a recomposition of the UI to update the UI with any changes. I have also read the documentation about Jetpack Compose state and ViewModels here. But that's a very simple example and does not cover the use case below.
Below is a conceptual scenario where I want to update the state of the list, by updating just one item's state that I wish to be reflected in the Jetpack Compose rendered part. I know I must assign a new list as data, which should trigger the recomposition and below I am using toMutableList() to try to achieve this. But this does not work. When I run this kind of code, recomposition does not happen and the single item's state is not updated in the list.
Could someone please explain to me why this does not work and how should I approach this?
I already know of mutableStateListOf(), but how should I approach this if I want to keep my view models compatible with other non-Jetpack Compose parts of my app, and thus I only want to use LiveData in my view models?
class Model : ViewModel() {
private val _items = MutableLiveData(listOf<Something>())
val items: LiveData<String> = _items
fun update(item: Something) {
_items.value = _items.value!!.toMutableList().map {
if (it == item) {
// Update item. But it's not reflected in Jetpack Compose
}
}
}
}
#Composable
fun ListComponent(model: Model) {
val items by model.items.observeAsState(emptyList())
LazyColumn {
items(items) { item ->
...
}
}
}
I think it's because you are mutating array instead of copying it. Compose needs stable equality when talking about recomposition avoidance, here i believe it can only use reference. Try copying array and then mutating the new one. I believe if you do map without toMutableList() it will create a copy and do exactly what you want

How to force jetpack compose to recompose?

Say that, I'm building a custom compose layout and populating that list as below
val list = remember { dataList.toMutableStateList()}
MyCustomLayout{
list.forEach { item ->
key(item){
listItemCompose( data = item,
onChange = { index1,index2 -> Collections.swap(list, index1,index2)})
}
}
This code is working fine and the screen gets recomposed whenever onChange lambda function is called, but when it comes to any small change in any item's property, it does not recompose, to elaborate that let's change the above lambda functions to do the following
{index1,index2 -> list[index1].propertyName = true}
Having that lambda changing list item's property won't trigger the screen to recompose. I don't know whether this is a bug in jetpack compose or I'm just following the wrong approach to tackle this issue and I would like to know the right way to do it from Android Developers Team. That's what makes me ask if there is a way to force-recomposing the whole screen.
You can't force a composable function to recompose, this is all handled by the compose framework itself, there are optimizations to determine when something has changed that would invalidate the composable and to trigger a recomposition, of only those elements that are affected by the change.
The problem with your approach is that you are not using immutable classes to represent your state. If your state changes, instead of mutating some deep variable in your state class you should create a new instance of your state class (using Kotin's data class), that way (by virtue of using the equals in the class that gets autogenerated) the composable will be notified of a state change and trigger a recomposition.
Compose works best when you use UDF (Unidirectional Data Flow) and immutable classes to represent the state.
This is no different than, say, using a LiveData<List<Foo>> from the view system and mutating the Foos in the list, the observable for this LiveData would not be notified, you would have to assign a new list to the LiveData object. The same principle applies to compose state.
you can recreate an entire composition using this
val text = remember { mutableStateOf("foo") }
key(text.value) {
YourComposableFun(
onClick = {
text.value = "bar"
}
) {
}
}
call this
currentComposer.composition.recompose()

Categories

Resources