Ui is not updating from viewmodel kotlin flow - android-jetpack-compose

I am quite new in Android Flow and JetPack compose,
I am trying to update my UI when mutable state is being changed , but this is not calling our composable , here is my code
#Composable
fun Grid() {
val mainViewModel by viewModels<DashBoardViewModel>()
mainViewModel.getData()
when (val result = mainViewModel.mutableState.value) {
is Resource.Success -> {
LazyVerticalGrid(
cells = GridCells.Adaptive(100.dp)
) {
items(result.device.items.first().devices.count()) {
EachItem(it)
}
}
}
is Resource.Error -> { Text(text = result.message) }
Resource.Loading -> { CircularProgressIndicator() }
Resource.Empty -> {}
else -> { CircularProgressIndicator() }
}
}
ViewModel:
#HiltViewModel
class DashBoardViewModel #Inject constructor(
private val dashBoardRepository: DashBoardRepository
) : ViewModel() {
val mutableState = MutableLiveData<Resource>()
fun getData() = viewModelScope.launch {
flow {
emit(Resource.Loading)
try {
val mResponse = dashBoardRepository.getDevice()
emit(Resource.Success(mResponse))
} catch (e: Exception) {
e.printStackTrace()
emit(Resource.Error("Error"))
}
}.flowOn(Dispatchers.IO).collect {
mutableState.value = it
}
}
}

There are two problems in your code:
mainViewModel.mutableState.value gets only the current value from your mutable state. Your composable will not be notified when this value changes and thus it cannot reflect the change. If you want to use LiveData in your viewmodel, you have to use observeAsState() extension function which converts LivaData to State that can be automatically observed by composable function. Other option is to have (Mutable)State directly in your viewmodel. See this state explanation.
Your mainViewModel.getData() function will be called every time your Grid() function recomposes, which will be every time your mainViewModel.mutableState changes (once you observe it correctly). You definitely don't want that. Better solution would be to call getData() from your viewModel's init block, or, if you really need to call it from your composable function, use LaunchedEffect.
And, as a side note, the way you are creating flow and then collecting it into LiveData is really odd and unnecessary. You can do something like this instead:
fun getData() = viewModelScope.launch {
mutableState.value = Resource.Loading
try {
val mResponse = dashBoardRepository.getDevice()
mutableState.value = Resource.Success(mResponse)
} catch (e: Exception) {
e.printStackTrace()
mutableState.value = Resource.Error("Error")
}
}

Related

How can I remember the list position in a Compose LazyColumn using Paging 3 LazyPagingItems?

I have a function like:
#Composable
fun LazyElementList(data: Flow<PagingData<Element>>) {
val scrollState = rememberLazyListState()
val elements = data.collectAsLazyPagingItems()
LazyColumn(state = scrollState) {
items(elements) {
DisplayElement(it)
}
}
}
I would like when navigating to another screen and back to maintain the place in the list.
Unexpectedly, the value of scrollState is maintained when visiting child screens. If it wasn't, it should be hoisted, probably into the ViewModel.
In the code in the question scrollState will be reset to the beginning of the list because there are no items in the list on the first composition. You need to wait to display the list until the elements are loaded.
#Composable
fun LazyElementList(data: Flow<PagingData<Element>>) {
val scrollState = rememberLazyListState()
val elements = data.collectAsLazyPagingItems()
if (elements.isLoading) {
DisplayLoadingMessage()
} else {
LazyColumn(state = scrollState) {
items(elements) {
DisplayElement(it)
}
}
}
}
fun LazyPagingItems.isLoading(): Boolean
get() = loadState.refresh is LoadState.Loading

How to show a composable just for e few seconds?

I have a Text composable within a Box:
Box(modifier = Modifier)
) {
Text(text = "BlaBla" )
}
How can show the Box/Text for a few seconds, only?
You can use LaunchedEffect and delay with a boolean flag and set it false after time specified
#Composable
private fun TimedLayout() {
var show by remember { mutableStateOf(true) }
LaunchedEffect(key1 = Unit){
delay(5000)
show = false
}
Column(modifier=Modifier.fillMaxSize()) {
Text("Box showing: $show")
if(show){
Box{
Text(text = "BlaBla" )
}
}
}
}
There are tons of way you can achieve this, for instance:
You can use AnimatedVisibility in case you want to choose which animation you want.
Example:
AnimatedVisibility(visible = yourFlag) {
Box{
Text(text = "BlaBla")
}
}
If you are using a ViewModel or any pattern and you can observe you can create two states and then inside of the ViewModel / Presenter / Whatever you start the timer you want (doing this you can test it):
sealed class TextState {
object Visible: TextState()
object Invisible: TextState()
}
And then inside the #Composable you do this
val state by presenter.uiState.collectAsState()
when (val newState = state) {
Visible -> {}
Invisible -> {}
}
You can change it also playing with the Alpha
Modifier.alpha(if(isVisible) 1f else 0f)
Another valid option is what #Thracian commented, using Side-Effects
for example :
#Composable
fun YourComposable() {
LaunchedEffect(key1 = Unit, block = {
try {
initTimer(SECONDS_HERE) {
//Timer has passed so you can show or hide
}
} catch(e: Exception) {
//Something wrong happened with the timer
}
})
}
suspend fun initTimer(time: Long, onEnd: () -> Unit) {
delay(timeMillis = time)
onEnd()
}

How to navigate to another screen after call a viemodelscope method in viewmodel - Jetpack Compose

Hello developers i have a problem i calling an api from viewmodel but is a coroutine and i do not how to wait to api response to make login .... i have this method in viewmodel:
fun createUser(context: Context){
viewModelScope.launch {
val jsonObject= JSONObject()
jsonObject.put("correo", register.value.correo)
jsonObject.put("password", register.value.password)
jsonObject.put("nombre", register.value.nombre)
jsonObject.put("apellido", register.value.apellido)
jsonObject.put("direccion",register.value.direccion)
jsonObject.put("telefono", register.value.telefono)
var bodyrequest=jsonObject.toString().toRequestBody()
val result: RegisterResponse = registerService.register(bodyrequest)
response.value=result
Log.d("response",result.message)
}
}
and this is onClick event from button in a #Composable function:
onClick = {
if (vm.validateCredentials()=="ok"){
vm.createUser(context)
Log.d("result from compose","${vm.response.value.message}")
navController.navigate(Screen.login.route){
popUpTo(Screen.login.route){
inclusive=true
}
}
}else{
showPopUp = !showPopUp
}
}
The big problem is that when the navigation is triggered, the coroutine has not finished yet and therefore I cannot find out if the api response was successful or not. How could I do this?
You can let createUser return the Job, and then join it in onClick. Like this:
fun createUser(context: Context) = viewModelScope.launch {...}
...
onClick = {
scope.launch {
viewModel.createUser(context).join()
navController.navigate(Screen.login.route){
popUpTo(Screen.login.route){
inclusive=true
}
}
}
}
But this makes the code difficult to maintain. I suggest having the ViewModel layer send an Event when createUser is done and the View layer collects it and navigate to a new screen. Like this:
interface Event
abstract class BaseViewModel<E : Event> : ViewModel() {
private val _event = Channel<E>()
val event = _event.receiveAsFlow().shareIn(viewModelScope, SharingStarted.Lazily)
protected suspend fun sendEvent(event: E) = _event.send(event)
protected fun sendEventSync(event: E) = viewModelScope.launch { _event.send(event) }
}
#Composable
fun <E : Event> OnEvent(event: Flow<E>, onEvent: (E) -> Unit) {
LaunchedEffect(Unit) {
event.collect(onEvent)
}
}
sealed interface LoginEvent : Event {
object LoginSuccess : LoginEvent
data class LoginFailure(val exception: Throwable) : LoginEvent
}
class LoginViewModel() : BaseViewModel<LoginEvent>() {
fun createUser(context: Context){
viewModelScope.launch {
repository.loign()
.onSuccess { sendEvent(LoginEvent.LoginSuccess) }
.onFailure { sendEvent(LoginEvent.LoginFailure(it)) }
}
}
}
#Composable
fun Screen() {
...
OnEvent(viewModel.event) {
when (it) {
LoginEvent.LoginSuccess ->
navController.navToXXXScreen()
is LoginEvent.LoginFailure ->
context.toast(it.exception.localizedMessage ?: "")
}
}
}

How to pass a composable content parameter in data class

I need to pass a compose content parameter in data class. For example a button can render when added into this content.
data class ContentData {
val content: #Composable ()-> Unit
}
This is working but when I get the app background I am getting parcelable exception. How to solve this problem.
One possible explanation I think that will occur related with a parcelable error, happens if you try to pass such object between activities as extras through Intent. Consider not use Composable as parameters in objects. Instead, try to represent the parameters of your Composable with a model which contains the parameters.
// your compose function
#Composable
fun Item(content: String = "Default", padding: Dp){
// ...
}
// Ui Model which contains your data (instead of have a weird composable reference) as a parcelable.
data class ContentData(
val content: String = "Default",
val paddingRaw: Int = 0
) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.readString().orEmpty(),
parcel.readInt()
) {
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(content)
parcel.writeInt(paddingRaw)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<ContentData> {
override fun createFromParcel(parcel: Parcel): ContentData {
return ContentData(parcel)
}
override fun newArray(size: Int): Array<ContentData?> {
return arrayOfNulls(size)
}
}
}
// Example if you need the model between activities through the intent as an extra.
val data = ContentData("your content", 11)
val intent = Intent().apply {
putExtra("keyContentData", data)
}
//The way of get and use your model.
val contentData = intent.extras?.get("keyContentData") as ContentData
#Composable
fun ParentComponent(){
// ...
Item(
contentData?.content.orEmpty(),
contentData?.paddingRaw?.dp ?: 0.dp
)
// ...
}

Jetpack Compose - How can we call a #Composable function inside an onClick()

I know its not possible to call composable functions inside onClick.
#Composable invocations can only happen from the context of a #Composable function
Compose version - alpha06
But I'm stuck with the below requirement.
The requirement is,
Call a server api call inside an onClick.
LazyColumnFor(items = list) { reports ->
Box(Modifier.clickable(
onClick = {
//API call
val liveDataReportsDetails =
viewModel.getReportDetails("xxxx")
LiveDataComponentForReportsDetails(liveDataReportsDetails)
}
)) {
ReportListItem(
item = reports
)
}
}
So you're right, composable functions cannot be called from within onClicks from either a button or a modifier. So you need to create a value like:
private val showDialog = mutableStateOf(false)
When set to true you want to invoke the composable code, like:
if(showDialog.value) {
alert()
}
Alert being something like:
#Composable
fun alert() {
AlertDialog(
title = {
Text(text = "Test")
},
text = {
Text("Test")
},
onDismissRequest = {
},
buttons = {
Button(onClick = { showDialog.value = false }) {
Text("test")
}
}
)
}
Now finish with changing the boolean where intended, like:
Box(Modifier.clickable(
onClick = {
showDialog.value = true
}
))
I hope this explanation helps, of course the value doesn't have to be a boolean, but you get the concept :).

Resources