I'm trying to implement Grid Layout with 2 columns using Compose, but LazyVertical Grid does not work for me. I searched for some workarounds to fulfill the task, but nothing was rendered on a screen. Any ideas?
val state = rememberLazyListState()
LazyVerticalGrid(
cells = GridCells.Fixed(2),
state = state,
content = {
items(bookList.books){
bookList.books.map {
BookUI(book = it, onClick = {})
}
}
}
)
I tried using LazyVerticalGrid this way, but it does not render a list, while LazyColumn renders it
You don't need a map when using items.
Change
items(bookList.books){
bookList.books.map {
BookUI(book = it, onClick = {})
}
}
to
items(bookList.books){ book ->
BookUI(book = it, onClick = {})
}
Don't forget to import,
import androidx.compose.foundation.lazy.items
try to use the following code:
#OptIn(ExperimentalFoundationApi::class)
#Composable
fun MyGrid(items: List<String>) {
LazyVerticalGrid(
cells = GridCells.Fixed(count = 2)
) {
items(items) { text ->
Text(text = text)
}
}
}
Few things you should pay attention to:
the items(*) {} function need to be imported from androidx.compose.foundation.lazy.items
You added #OptIn(ExperimentalFoundationApi::class)
rememberLazyListState() is actually a default param so no need to add it.
For the above example you can use something like this:
#OptIn(ExperimentalFoundationApi::class)
#Composable
fun Content() {
MyGrid(
items = listOf(
"Item A",
"Item B",
"Item C",
"Item D",
"Item E",
"Item F"
)
)
}
And you will get this:
Implementation has changed since the time your question was created, so the following code snipped should work in your case:
val state = rememberLazyGridState()
LazyVerticalGrid(
state = state,
columns = GridCells.Fixed(2)
) {
items(bookList.books) { books ->
BookUI(book = it, onClick = {})
}
}
Related
In classic Android programming (before composites) to show list on recyclerview I need adapter and if I want to give any action like clickable element, where logic will be provide in fragment/viewmodel I had to through argument create such parameter like
(Int) -> Unit
how does it look in jetpack compose when I create lazy column? Should it look the same and just create paramaters in Screens/UI then configure logic in the same way in fragment/viewmodel or there is some other approachs?
Getting rid of all the bloat of recyclerview is one of my favorite advantages of compose. Let's assume you'd like to include the following list in your screen:
#Composable
private fun TestList(
myItems: List<String>,
onClick: (String) -> Unit,
modifier: Modifier = Modifier,
) {
LazyColumn(modifier.fillMaxSize()) {
items(myItems) { item ->
TestItemView(
text = item,
onClick = onClick
)
}
}
}
#Composable
private fun TestItemView(
text: String,
onClick: (String) -> Unit,
modifier: Modifier = Modifier,
) {
Surface(modifier.fillMaxWidth()) {
Button(
onClick = { onClick(text) },
content = { Text(text) }
)
}
}
Option A: keep the state in the composable.
#Composable
private fun TestScreenA() {
val myItems = remember { mutableStateOf(listOf("A", "B", "C", "D")) }
TestList(
myItems = myItems.value,
onClick = { clickedItem ->
// for demonstration purposes we remove item on click
myItems.value = myItems.value.filterNot { it == clickedItem }
}
)
}
Option B: Keep the state in the viewmodel (like before)
class TestViewModelB: ViewModel() {
private val _myItems = MutableStateFlow(listOf("A", "B", "C", "D"))
val myItems = _myItems.asStateFlow()
fun onItemClicked(clickedItem: String){
// for demonstration purposes we remove item on click
_myItems.update { items -> items.filterNot { it == clickedItem } }
}
}
#Composable
private fun TestScreenB(
// inject viewModel here using your favorite DI-Framework
viewModel: TestViewModelB
) {
TestList(
// you might want to use collectAsStateWithLifecycle in the future
// see https://medium.com/androiddevelopers/consuming-flows-safely-in-jetpack-compose-cde014d0d5a3
myItems = viewModel.myItems.collectAsState().value,
onClick = viewModel::onItemClicked
)
}
Both options are viable (especially when using rememberSavable).
However, i suggest to use optionB for people just migrating to compose, as its more similar to what they're used. I personally use OptionA for simple states, and OptionB for more complex ones (like a list)
I've been checking out the Performance best practices for Jetpack Compose Google I/O, in there it's stated that this code should only re-execute the Text() function, since only this function reads a value that changes.
private class NameHolder(var name: String)
#Composable
private fun LittleText(nameHolder: NameHolder) {
Box {
Text(text = "Nombre: ${nameHolder.name}")
println("compose 2")
}
println("compose 1")
}
however when I run it I can see that for every change both prints execute as well.
I also tested with something like this:
#Composable
private fun LittleText(name: String) {
Box {
Text(text = "Nombre: $name")
println("compose 2")
}
println("compose 1")
}
With the same result, I'm changing the text with a TextField, like this:
var name by remember { mutableStateOf("name") }
TextField(
value = name,
onValueChange = {
name = it
}
)
LittleText(name)
What I'm I doing wrong? How can I achieve this behaviour and have only the Text re-executing the composition?
I found an answer that cover this:
#Composable
fun TestingCompose() {
Column {
TestView()
println("compose 1")
}
}
#Composable
fun TestView() {
val textFieldValue = remember { mutableStateOf(TextFieldValue("")) }
TextField(textFieldValue)
println("compose 2")
}
#Composable
fun TextField(textFieldValue: MutableState<TextFieldValue>) {
TextField(
value = textFieldValue.value,
onValueChange = { textFieldValue.value = it }
)
println("compose 3")
}
I'm still trying to fully understand it, so any insight would be greatly appreciated, but checking the log while testing this shows that only the composable containing the TextField gets re-executed with every character.
I am using Jetpack Compose, along with Paging 3 library & Jetpack Navigation. The issue I am facing is I have a LazyList which is fetching data from remote source using paging library.
ViewModel
fun getImages(): Flow<PagingData<ObjectImage>> = Pager(
PagingConfig(PAGE_SIZE, enablePlaceholders = false)
) { DataHome(RANDOM) }.flow.cachedIn(viewModelScope)
HomeView
val images = viewModelHome.getImages().collectAsLazyPagingItems()
LazyColumn {
...
}
Now whats happening is when I navigate to another View using navHostController.navigate() and then press back to get to HomeView... the LazyColumn resets itself & start loading items again from network.
So I am stuck with this issue. I tried manually caching in viewModel variable... though it works but it screws up SwipeRefresh (which stops showing refresh state)
data.apply {
when {
// refresh
loadState.refresh is LoadState.Loading -> {
ItemLoading()
}
// reload
loadState.append is LoadState.Loading -> {...}
// refresh error
loadState.refresh is LoadState.Error -> {...}
// reload error
loadState.append is LoadState.Error -> {...}
}
}
implementation("androidx.paging:paging-runtime-ktx:3.1.0")
implementation("androidx.paging:paging-compose:1.0.0-alpha14")
Is this an issue with PagingLibrary which is still in alpha??
Update 1 (I am not sure if this is a good solution, but I am solving
the swipe refresh issue as follows)
// get images
var images: Flow<PagingData<ObjectImage>> = Pager(PagingConfig(PAGE_SIZE)) {
DataHome(RANDOM)
}.flow.cachedIn(viewModelScope)
// reload items
fun reload(){
images = Pager(PagingConfig(PAGE_SIZE)) {
DataHome(RANDOM)
}.flow.cachedIn(viewModelScope)
}
// and rather than calling .refresh() method on lazy items... I am calling viewModel.reload()
The problem is that you are creating new Pager every time you call getImages(), which is every time your composable recomposes, that's not how it's supposed to be done.
You should make it a val items = Pager(... for the caching to work.
For the screwed up SwipeRefresh, how do you implement it? There is a refresh() method on LazyPagingItems, you should use that.
EDIT: Ok, so based on the coments and edits to your question:
In your viewmodel, do as I suggested before:
val items = Pager( // define your pager here
Your composable can then look like this:
#Composable
fun Screen() {
val items = viewModel.items.collectAsLazyPagingItems()
val state = rememberSwipeRefreshState(
isRefreshing = items.loadState.refresh is LoadState.Loading,
)
SwipeRefresh(
modifier = Modifier.fillMaxSize(),
state = state,
// use the provided LazyPagingItems.refresh() method,
// no need for custom solutions
onRefresh = { items.refresh() }
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
) {
// display the items only when loadState.refresh is not loading,
// as you wish
if (items.loadState.refresh is LoadState.NotLoading) {
items(items) {
if (it != null) {
Text(
modifier = Modifier.padding(16.dp),
text = it,
)
}
}
// you can also add item for LoadState.Error, anything you want
if (items.loadState.append is LoadState.Loading) {
item {
Box(modifier = Modifier.fillMaxWidth()) {
CircularProgressIndicator(
modifier = Modifier
.align(Alignment.Center)
.padding(16.dp)
)
}
}
}
}
// if the loadState.refresh is Loading,
// display just single loading item,
// or nothing at all (SwipeRefresh already indicates
// refresh is in progress)
else if (items.loadState.refresh is LoadState.Loading) {
item {
Box(modifier = Modifier.fillParentMaxSize()) {
Text(
text = "Refreshing",
modifier = Modifier.align(Alignment.Center))
}
}
}
}
}
}
I want get Data(with an api) from internet and set the data in LazyColumn
So I use this
#Composable
fun LazyColumn() {
val list by remember { mutableStateOf(arrayListOf<HelloData>()) }
Thread {
for (index in 0..100) {
list.add(HelloData(JSONObject(URL("https://ovooa.com/API/sjtx/api?form=%E5%A5%B3%E5%A4%B4").readText()).getString("text")))
}
}.start()
LazyColumn(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(4.dp)) {
items(list) { item ->
Image(
painter = rememberImagePainter(item.url, builder = {
crossfade(true)
diskCachePolicy(CachePolicy.DISABLED)
memoryCachePolicy(CachePolicy.DISABLED)
}),
contentDescription = null,
modifier = Modifier.size(200.dp)
)
}
}
}
data class HelloData(val url: String)
It's not working.
I just want get the data from my api,then put them in an Image.
I can achieve it easily in traditional Android Development.
Use mutableStateListOf instead of mutableStateOf. mutableStateListOf detects changes to the list items whereas mutableStateOf will only detect changes to the list object.
I'm trying to update a LazyColumn items using a subscriber to a RxAndroid Flowable. The state variable I'm using for the image list is called simply "list"
This is my LazyColumn code:
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
) {
items(list) { image ->
Text(text = image.title ?: "Title")
}
}
If for example, I run this test coroutine, the list is updated and shows the correct amount of test images:
GlobalContext.run {
val testList = SnapshotStateList<Image>()
for (i in 1..100) {
testList.add(Image(i, null, null, null, null))
}
list = testList
}
But if I try the same method using my subscription to a Flowable, it updates the variable value but the recomposition is not triggered. This is my code:
val observer = remember {
disposable.add(
viewModel.imagesObservable().subscribe(
{ images ->
val snapList = SnapshotStateList<Image>()
images.forEach {
snapList.add(Image(it.id, it.albumId, it.title, it.url, it.thumbnailUrl))
}
list = snapList
},
{ Log.d("dasal", "Error: Can't load images") }
)
)
}
How do I handle a Flowable with a Composable?
Fixed it. I was using this declaration
var list = remember { mutableStateListOf<Image>() }
I changed it to this one instead
val list = remember { mutableStateOf(listOf<Image>()) }
Now I can use the list.value property to update/read the current value.