Get Data from internet and set the data in LazyColumn - android

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.

Related

Android common ViewModel for two Compose screens

I have an issue with refreshing Compose Lazy List, based on changes in persistence.
The business case - I have a screen (Fragment) with MyObject list contains all objects, there is another screen with only favorites MyObjects. Both use the same Composable as a list element with name, description and "heart" icon to set/unset favorite flag.
On "all" list setting and unsetting favorite flag works well - click on IconToggleButton sets boolean in DB and then switching to Favorite screen shows new item. Unset favorite on "all" screen sets flag to false as expected and when navigates to Favorite screen removes item.
But toggling favorite icon on Favorite screen change boolean in DB - BUT does not refresh and recompose LazyList content. I have to manually switch few times between screens, then eventually both are, let's say, synchronized with DB.
Unset favorite on the last element on Favorite list does not refresh it at all - I have to "like" another object on "all" list, then the Favorite list content is recompose with replacing items.
Moreover - there are some cases, that items on both lists disappears, while they are still in DB. I need to dig it deeper and debug this case, but maybe is related.
Some code's details:
There is a simple Entity
#Entity(tableName = "my_objects")
data class MyObject(
#PrimaryKey(autoGenerate = true)
var id: Long = 0,
#ColumnInfo(name = "name")
val name: String,
#ColumnInfo(name = "favorite")
val favorite: Boolean = false
)
Then there are also DAO, Provider and Repository with Domain Model. In DAO there are methods:
#Query("SELECT * FROM my_objects")
fun getAll(): List<MyObject>
#Query("SELECT * FROM my_objects WHERE favorite = 1")
fun getFavorites(): List<MyObject>
called in Provider and then in Repository.
In MyObjectListViewModel (with mapping from DB model do domain model):
#HiltViewModel
class MyObjectListViewModel #Inject constructor(
private val updateMyObject: UpdateMyObject,
private val getOrderedMyObjectList: GetOrderedMyObjectList,
private val dispatchers: CoroutineDispatcherProvider
) : ViewModel() {
private val mutableMyObjects = MutableLiveData<List<ItemMyObjectModel>>()
val myObjects: LiveData<List<ItemMyObjectModel>> = mutableMyObjects
fun loadMyObjects() {
viewModelScope.launch(dispatchers.io) {
val myObjectListResult = getOrderedMyObjectList()
withContext(dispatchers.main) {
when (myObjectListResult) {
is MyObjectListResult.Success -> {
val viewModelList = myObjectListResult.list.map {
ItemMyObjectModel(it)
}
mutableMyObjects.postValue(viewModelList)
}
}
}
}
}
fun switchFavoriteFlag(itemMyObjectModel: ItemMyObjectModel) {
val myObject = itemMyObjectModel.itemMyObject
myObject.favorite = !myObject.favorite
viewModelScope.launch(dispatchers.io) {
val updatedObject = updateMyObject(myObject) //save via DAO
}
}
}
MyObjectFavoriteListViewModel looks exactly the same, except that load function calls loadFavoriteMyObjects() and it uses GetOrderedFavoriteMyObjectList Repository. BTW - maybe it could be aggregate to one ViewModel, but with pair of LiveData and load function - one pair for all item and one for favorites?
Last but not least - Composables:
#OptIn(ExperimentalMaterial3Api::class)
#Composable
fun MyObjectFavoriteListScreen(
viewModel: MyObjectFavoriteListViewModel,
navigator: MyObjectNavigator
) {
val list by viewModel.myObjects.observeAsState()
val lazyListState = rememberLazyListState()
Scaffold(
floatingActionButton = {
MyObjectListFloatingActionButton(
extended = lazyListState.isScrollingUp() //local extension
) { navigator.openNewMyObjectFromObjectList() }
}
) { padding ->
if (list != null) {
LazyColumn(
contentPadding = PaddingValues(
horizontal = dimensionResource(id = R.dimen.margin_normal),
vertical = dimensionResource(id = R.dimen.margin_normal)
),
state = lazyListState,
modifier = Modifier.padding(padding)
) {
items(list!!) { item ->
MyObjectListItem( // with Card() includes Text() and IconToggleButton()
item = item,
onCardClick = { myObjectId -> navigator.openMyObjectDetailsFromFavouriteList(myObjectId) },
onFavoriteClick = { itemMyObjectModel -> viewModel.switchFavoriteFlag(itemMyObjectModel) }
)
}
}
} else {
Box(
modifier = Modifier
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(text = stringResource(id = "No objects available"))
}
}
}
}
I think that one issue could be related with if (list != null) {} (list is observed as State<T?>).
But for sure there is something wrong with states, I am pretty sure that the list should be triggered to recompose, but there is no(?) state to do so.
Any ideas?

Fetching words from Firestore does not update LazyColumn Jetpack Compose

#Composable
fun getData() {
var wordData = arrayListOf<Word>()
db.get().addOnSuccessListener { documents ->
for (document in documents) {
wordData.add(document.toObject(Word::class.java))
}
}
LazyColumn {
items(wordData) { word ->
WordCard(word = word)
}
}
}
I wanted to use Lazy Column to show all of my words data, these are my WordCard codes.
#Composable
fun WordCard(word: Word) {
Text(text = word.word, color = Color.Black, modifier = Modifier.background(Color.Gray).padding(12.dp))
}
Not sure if this is a firebase issue, but I notice this.
var wordData = arrayListOf<Word>()
You are just adding elements to a standard collection structure, not something that compose can observe state changes for it to update your LazyColumn.
So please change it to this using SnapshotStateList and wrap it to a remember{…} so the list won't re-initialize on succeeding re-compositions
val wordData = remember { mutableStateListOf<Word>() }

Sticky Headers with paging 3 jetpack compose

I am trying to implement sticky headers as shown in the example here, with paging using Jetpack compose LazyColumn as shown bellow :
LazyColumn(
modifier = modifier.fillMaxSize(),
contentPadding = PaddingValues(20.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
// TODO (HADI) enhance later
notifications.itemSnapshotList.items.groupBy { extractNotificationHeader(it) }
.forEach { (header, messages) ->
stickyHeader {
NotificationHeader(
modifier = Modifier.fillMaxWidth(),
value = header
)
}
items(
items = messages,
key = { message -> message.notificationId },
) {
NotificationMessage(
notification = it,
onNotificationClick = { onNotificationClick(it) }
)
}
}
}
but I get requests from back-end with only 2 pages as I am using code like :
// API service method
#GET("notifications")
suspend fun getNotifications(
#Query("page") page: Int,
#Query("page_size") pageSize: Int = 30
): GenericResponse<NotificationsJson>
// data source impl
class NotificationDataSourceImpl #Inject constructor(private val notificationApiService: NotificationApiService) : NotificationDataSource {
override val invalidatingFactory = InvalidatingPagingSourceFactory {
NotificationMediator(notificationApiService)
}
override fun loadNotifications(): Flow<PagingData<NotificationCenterMessage>> {
return Pager(
config = PagingConfig(
pageSize = 30,
enablePlaceholders = false,
),
pagingSourceFactory = invalidatingFactory
).flow
}
override fun invalidateNotifications() {
invalidatingFactory.invalidate()
}
}
the problem is I have 7 more pages, but they are not loaded when I reach the bottom position of the lazy column items, I've tried to use only items without looping and sticky headers and it's working fine.
I've looked up for many solutions like here, but nothing worked as required.
Also I am trying to mark notification at specific position when the user click it to be read, but when I use invalidateNotifications()
the page reload and I lose the position even when I use animateScrollToItem() with rememberLazyListState()
Loads are triggered by the LazyPagingItems.get method and you are not calling it anywhere. Modify your extractNotificationHeader function to return indices of the header and messages instead of the models and then inside stickyHeader and items, use them like notifications.get(headerIndex).

Paging 3 list auto refresh on navigation back in jetpack compose navigation

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))
}
}
}
}
}
}

Jetpack Compose does not update my list with RxAndroid

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.

Categories

Resources