Lazycolumn with lazyrow that are loaded with api call - api

i'am new to jetpack compose and i would like to achieve a view but i'am really struggling with it. it a catalogue movie where you can chose film in different categories, so you get the all the categories listed in column and for each category you get multiples films in the row.
Here is what i want to do : i have a viewmodele to make call to my api. So i make a first call giving me the list of categories. then for each category have to make an other call of my view modele to get films of this category. Here is how i started :
class CatalogViewModel() : ViewModel() {
var loadCategories by mutableStateOf(false)
var categories: ArrayList<Category> = arrayListOf()
init {
fetchCategories()
}
fun fetchCategories() {
this.viewModelScope.launch {
Api.Call(
completionHandler = object : completionHandler<ArrayList<Category>, Error> {
override fun succeed(result: ArrayList<Category>?) {
loadCategories = true
if (result != null) {
categories = result
loadCategories = true
}
}
override fun failed(error: PKError?) {
println("failed")
loadCatalog = false
}
})
}
}
completionHandler is just and interface that we use to make our api call :
interface completionHandler<in T, in U> {
fun succeed(result: T?)
fun failed(error: U?)}
So first of all i don't really know how states work in jetpack compose so i'am not sure if i'am doing it good or not but it's work, now in my composable i just have to check
if (CatalogViewModel.loadCategories)
Then i can display my list of categories. But here is where i don't know how to do further : now for each category i have to make an other call to get the film that i need to display is the row, but i can't use the same technique because i'am not sur how much call i'll need to do, so i can't have a mutable state of boolean to observe in order to compose my row. So how can i do a call for each row and compose when the call is done ? also this has to be asynchrone because i can't wait all row to be loaded to display them.
And in a second time, i'll maybe get a lot of categories so i would like to only load those displayed on the screen.
I feel like i'am doing something wrong or maybe i didn't really get how to work with jetpack compose. So i would really like some help, thanks

Related

Take snapshot of MutableStateFlow that doesn't change with the state in Jetpack Compose

I'm trying to make an editor app that allows you to undo/redo changes. I want to achieve that by storing the ui state in stacks (ArrayDeque) and pop them back once the user hits undo/redo. But every time I stored the state in stack, after I made change to the state, the value in the stack is changed as well.
Is there a way to snapshot a state that won't be affected by future changes in the state flow?
My code looks something like this:
Data Class
class Data () {
var number: Int = 0
fun foo()
}
State Class
data class UiState(
val dataList: MutableList<Data> = mutableListOf(),
)
ViewModel
class UiViewModel : ViewModel() {
private val _uiState = MutableStateFlow(UiState())
private val undoStack: ArrayDeque<UiState> = ArrayDeque()
fun makeChangeToState() {
saveStateToUndoStack(_uiState.value)
Log.i("Test", undoStack.last().dataList[0].number) // 0
val dataList= _uiState.value.dataList.toMutableStateList()
dataList[0].number = 1
_uiState.update { currentState ->
currentState.copy(
dataList = dataList,
)
}
Log.i("Test", undoStack.last().dataList[0].number) // 1 because the _uiState changed
}
fun undo() {
val lastState = undoStack.last()
// Won't work because the data in lastState has already been updated with _uiState
_uiState.update { lastState }
}
}
Things I've tried:
Use _uiState.value.copy
Call saveStateToUndoStack(uiState: UiState) from Composable functions and pass in the viewModel.uiState.collectAsState()
Both doesn't seem to work, I play around for a few hours but don't have a clue.
The reason why the old value got updated is because all the objects in the list are references, not related to MutableStateFlow. I just need a deep copy of the list, see this post here:
https://www.baeldung.com/kotlin/deep-copy-data-class
Another thread worth reading: Deep copy of list with objects in Kotlin

Collecting Flow<List> and displaying it in Compose (Kotlin)

Hello guys I have list of movies that I call from MovieApi.
In movieRepo I did this:
override suspend fun getPopularMovies() : Flow<List<Movie>>{
val popularMovies : Flow<List<Movie>> = flow{
while(true){
val lastMovie = movieApi.getPopularMovies()
Log.i("EMIT", "${emit(lastMovie)}")
kotlinx.coroutines.delay(5000)
}
}
return popularMovies
}
In MovieViewModel:
init{
viewModelScope.launch {
repository.getPopularMovies().collect(){
Log.i("COLLECTED", "$it")
}
}
}
private suspend fun getPopularMovies() {
return repository.getPopularMovies().collect()
}
I know that collect gets all Movies I want, but I need to display it in my HomeScreen with viewModel when I call getPopularMovies.
I'm reading Flow docs but cant understan how this part works(news part is from Flow documentation):
newsRepository.favoriteLatestNews.collect { favoriteNews ->
// Update View with the latest favorite news
}
I have the same question too actually. Curious to see if you had found out anything.
I could be mistaken in this but I would like to gain a better understanding in this so I would appreciate for other to chime in as well.
Assuming you're using targeting a recyclerview.
For non-Viewmodel collection approach, the collection has to be done in the UI layer.
In collect block, you will need to pass movie list to adapter's submitList.
But if you still want to do collection in ViewModel, you will need to create a UIState as a StateFlow. Collect the movie list into a UI state.
In UI layer, collect the UI state and access the movie list from it

Why doesn't App crash when I use collect a flow from the UI directly from launch in Jetpack Compose?

I have read the article. I know the following content just like Image B.
Warning: Never collect a flow from the UI directly from launch or the launchIn extension function if the UI needs to be updated. These functions process events even when the view is not visible. This behavior can lead to app crashes. To avoid that, use the repeatOnLifecycle API as shown above.
But the Code A can work well without wrapped with repeatOnLifecycle, why?
Code A
#Composable
fun Greeting(handleMeter: HandleMeter,lifecycleScope: LifecycleCoroutineScope) {
Column(
modifier = Modifier.fillMaxSize()
) {
var my by remember { mutableStateOf(5)}
Text(text = "OK ${my}")
var dataInfo = remember { handleMeter.uiState }
lifecycleScope.launch {
dataInfo.collect { my=dataInfo.value }
}
}
class HandleMeter: ViewModel() {
val uiState = MutableStateFlow<Int>(0)
...
}
Image B
Code A will not work in real life. If you need to run some non-UI code in a composable function, use callbacks (like onClick) or LaunchedEffect (or other side effects).
LaunchedEffect {
dataInfo.collect {my=dataInfo.value}
}
Side effects are bound to composables, there is no need to specify the owner of their lifecycle directly.
Also, you can easily convert any flow to state:
val my = handleMeter.uiState.collectAsState()

Android RecyclerView NotifyDataSetChanged with LiveData

When instantiating the ListAdapter for my RecyclerView, I call:
viewModel.currentList.observe(viewLifecycleOwner){
adapter.submitList(it)
}
which is what I've seen done in the Android Sunflower project as the proper way to submit LiveData to a RecyclerView. This works fine, and when I add items to my database they are updated in the RecyclerView. However, if I write
viewModel.currentList.observe(viewLifecycleOwner){
adapter.submitList(it)
adapter.notifyDataSetChanged()
}
Then my adapters data observer always lags behind. I call
adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver(){
override fun onChanged() {
super.onChanged()
if (adapter.currentList.isEmpty()) {
empty_dataset_text_view.visibility = View.VISIBLE
} else {
empty_dataset_text_view.visibility = View.GONE
}
}
})
And empty_dataset_text_view appears one change after the list size reached zero, and likewise it disappears one change after the list size is non-zero.
Clearly, the observer is running the code before it has submitted it which in this case is LiveData queried from Room. I'd simply like to know what the workaround is for this: is there a way to "await" a return from my LiveData?

How to inform a Flux that I have an item ready to publish?

I am trying to make a class that would take incoming user events, process them and then pass the result to whoever subscribed to it:
class EventProcessor
{
val flux: Flux<Result>
fun onUserEvent1(e : Event)
{
val result = process(e)
// Notify flux that I have a new result
}
fun onUserEvent2(e : Event)
{
val result = process(e)
// Notify flux that I have a new result
}
fun process(e : Event): Result
{
...
}
}
Then the client code can subscribe to EventProcessor::flux and get notified each time a user event has been successfully processed.
However, I do not know how to do this. I tried to construct the flux with the Flux::generate function like this:
class EventProcessor
{
private var sink: SynchronousSink<Result>? = null
val flux: Flux<Result> = Flux.generate{ sink = it }
fun onUserEvent1(e : Event)
{
val result = process(e)
sink?.next(result)
}
fun onUserEvent2(e : Event)
{
val result = process(e)
sink?.next(result)
}
....
}
But this does not work, since I am supposed to immediately call next on the SynchronousSink<Result> passed to me in Flux::generate. I cannot store the sink as in the example:
reactor.core.Exceptions$ErrorCallbackNotImplemented:
java.lang.IllegalStateException: The generator didn't call any of the
SynchronousSink method
I was also thinking about the Flux::merge and Flux::concat methods, but these are static and they create a new Flux. I just want to push things into the existing flux, such that whoever holds it, gets notified.
Based on my limited understanding of the reactive types, this is supposed to be a common use case. Yet I find it very difficult to actually implement it. This brings me to a suspicion that I am missing something crucial or that I am using the library in an odd way, in which it was not intended to be used. If this is the case, any advice is warmly welcome.