Refactoring Observer on Android - android

I'm building an App and i made a contact type page with a couple of input text and a button to send a message. Now, i wanted to make the button enabled only when some criteria are met, which are having the three most important fields filled up with some data (Service, Object and Message).
With MVVM pattern and two way databinding, it works just fine but when I observe the data in the fragment the view is linked to, I have some ugly code which looks like this:
contactPageViewModel.serviceToContact.observe(viewLifecycleOwner, Observer { service ->
contactPageViewModel.objectContact.observe(viewLifecycleOwner, Observer { objectContact ->
contactPageViewModel.message.observe(viewLifecycleOwner, Observer { message ->
contact_send_btn.isEnabled = !service.isNullOrEmpty() && !objectContact.isNullOrEmpty() && !message.isNullOrEmpty()
})
})
})
Basically what it does is that it checks if the three fields are full of data, and if its the case it activates the button, which works well.
But my problem is this: Its a nested observer, two times even. So, is it possible to make it look cleaner without having a nested observer to check if the conditions are met ?
Thanks.

You should never have nested observers. It doesn't only look bad, this is a big leak because you're repeatedly creating duplicate observers every time an outer observer gets triggered.
An alternative that keeps your view model as is is to separate the observers and call a function that updates the button in each of them.
contactPageViewModel.serviceToContact.observe(viewLifecycleOwner, Observer { service ->
updateSendButtonEnabled()
})
contactPageViewModel.objectContact.observe(viewLifecycleOwner, Observer { objectContact ->
updateSendButtonEnabled()
})
contactPageViewModel.message.observe(viewLifecycleOwner, Observer { message ->
updateSendButtonEnabled()
})
fun updateSendButtonEnabled() {
val service = contactPageViewModel.serviceToContact.value
val objectContact = contactPageViewModel.objectContact.value
val message = contactPageViewModel.message.value
contact_send_btn.isEnabled = !service.isNullOrEmpty() && !objectContact.isNullOrEmpty() && !message.isNullOrEmpty()
}
BUT! If you want to be as true as possible to the MVVM pattern, this is all still bad because you're doing logic in your view. 100% true to MVVM is to give your viewmodel a single livedata variable that tells the view whether or not to enable the button. Then the only observer and only thing it is doing should look like this:
contactPageViewModel.sendButtonEnabled.observe(viewLifecycleOwner, Observer { isEnabled ->
contact_send_btn.isEnabled = isEnabled
})

Related

LiveData update does not cause Recompostion of my Composables

In my ViewModel I have a few LiveData objects like this one:
val invitations = MutableLiveData(listOf<Session>())
I have set up multiple Firestore Snapshot Listeners that should update my LiveData. Therefore I give the function that instantiates the listener a lambda like this:
repository.getInvitations {
invitations.value = it
Log.d("ONLINE", "Receiving Invitations.")
}
Here is the repository function:
override fun getInvitations(onDataUpdate: (List<Session>) -> Unit) {
val invitationsQuery = db.collection("invitations").whereEqualTo("player_id", auth.currentUser?.uid)
val listener = invitationsQuery.addSnapshotListener{ querySnapshot, exception ->
if (exception != null){
Log.w("ONLINE", "Error handling Snapshot",exception)
return#addSnapshotListener
}
val sessions = mutableListOf<Session>()
querySnapshot?.documents?.forEach{
sessions.add(it.toSession())
}
onDataUpdate(sessions)
}
listeners.add(listener)
}
In my Composable I observe the LiveData with observeAsState() from the viewModel
val invitations = viewModel.invitations.observeAsState()
In my UI I use the received state as usual with invitation.value.
That worked quite fine for a while. Before I had another normal firestore query running in the init block of my viewModel that updated some other LiveData. But since I did not needed that function anymore and deleted it, my UI is not updating anymore.
My instant suggestion is that the lambda callback in the snapshot listener is not updating my liveData correctly. Which I more or less proved wrong by adding a Text element under the rest of my UI where I also use the same state. That text somehow can detect the change in the LiveData.
My other UI code is quite complex but here is quick overview:
#Composable
fun Screen(viewModel: MyviewModel = koinViewModel()){
val invitations = viewModel.invitations.observeAsState()
MyUIElement(invitations.value)
Text("${invitations.value}")
}
If more of the UI code is needed I can give it.
I also tried asking ChatGPT about this. It mentioned that snapshot listeners are running in background threads and the LiveData can only detect data changes in the Main thread. But it did not seem to be helpful to just add a runBlocking(Dispatchers.Main) {} to update it in the Main thread.
Also this cant really be the problem for my UI to not update, because my Text is showing all my changes to the firestore data if I update it in the online console.
This was bothering me for a while now. I really dont want to refactor all my Code now. A quick fix would be appreciatd. :)

Jetpack Compose: How to prevent first composition when screen should be created by a certain asynchronous state

I am still new to compose and I am curious how people treat this kind of thing.
Let's imagine that we have a screen that has two variants, one variant with some views, another variant with other views. That variant should be dictated by a persisted flag, which I have stored using DataStore (the new SharedPrefs). The only issues is that unlike SharedPrefs, DataStore is asynchronous and is made to work with coroutines. So here's what happens, the screen gets rendered in default state (variant A) for just a split second, atfer about 100-200ms the viewModel successfully reads the value from DataStore on a coroutine and posts it on a mutableStateOf(), which as a result triggers recomposition with the variant B of the screen that is saved in the prefs. This transition is visible and the entire behavior looks glitchy. How do you fix this? I don't want the screen to compose the default state before the viewModel has time to read the stored value, I want the screen to await those 100-200ms without doing anything and composing the views only after the reading from DataStore.
The code looks like:
#Composable
fun MyScreen(){
val viewModel = hiltViewModel<ScreenViewModel>()
val state = viewModel.uiState
if(state == MyScreenState.A){
[...] // some view here
} else {
[...] // other view here
}
}
#HiltViewModel
class ScreenViewModel #Inject constructor(
private val dataStoreService: DataStoreService
) : BaseViewModel() {
var uiState by mutableStateOf(MyScreenState.A)
init {
viewModelScope.launch {
dataStoreService.flag().collect { flag ->
uiState = if(flag) MyScreenState.A else MyScreenState.B
}
}
}
}
For simplicity, MyScreenState is just a simple enum in this case. One of the things I thought about is defaulting the uiState to null instead of variant A and in my screen check if the state is null and if so returning a Unit (basically rendering nothing). If the state is not null, render the screen accordingly. But the truth is that I don't feel like making that uiState nullable, I avoid working with nulls at all cost because they make the code just a little less readable and needs extra handling. What's your solution on this? Thanks.
Instead of creating nullable state create another state that represents nothing being happening. I generally use Idle state to set as initial or a UI state when nothing should happen. I also use this approach for fire once events after event is invoked and processed.
var uiState by mutableStateOf(MyScreenState.Idle)
it will be a loading or a blank screen depending on your UI

How to get data from a MutableLiveData

I'm trying to get data from a MutableLiveData; however, it seems like something is wrong with the code, can you guys check for me please?
I can get the object, but I failed to add the object to a mutableList
properties = ArrayList()
propertyViewModel.propertyItemLiveData.observe(
viewLifecycleOwner,
Observer { propertyItems ->
for (property in propertyItems){
var p:Property = Property(property.id,property.address
,property.price,property.phone,property.lat,property.lon)
println(p)// i can display data
properties.add(p)//when i add to properties, the properties still null. Why?
}
}
)
if (properties.isEmpty()){
println("null")
}
The code in the observer will only run when propertyItemLiveData pushes a new value, or if it already has a value when you first observe it. But from the docs:
As soon as an app component is in the STARTED state, it receives the most recent value from the LiveData objects it’s observing. This only occurs if the LiveData object to be observed has been set.
So you won't actually get a value until your Activity or Fragment hits the onStart() callback, meaning your observer code won't run until then. If the code you posted is running earlier than that (say in onCreate), then what you're doing is:
creating an empty list
adding an observer that will add stuff to that list (but it won't run until later)
checking if the list is still empty (it definitely is)
Because of the observer pattern, where your code reacts to new data/events being pushed to it, whatever you need to do with that populated list should be part of the observer code. It should react to the new value and take action - update a list view, alert the user, start an API call, whatever
propertyViewModel.propertyItemLiveData.observe(viewLifecycleOwner) { propertyItems ->
// handle the propertyItems, add them to your list etc
// then do whatever needs to happen with the list, e.g. display it
updateDisplay(propertyList)
}
btw if Property is a data class and you're just copying all its data, you can add to your list like this:
properties.addAll(propertyItems.map { it.copy() })
// or propertyItems.map(Property::copy)
hello first of all in kotlin in general you have to use mutableList and the check of empty or any other instruction should inside the call back like this :
properties = mutableListOf<YourClass>()
propertyViewModel.propertyItemLiveData.observe(
viewLifecycleOwner,
Observer { propertyItems ->
for (property in propertyItems){
var p:Property = Property(property.id,property.address
,property.price,property.phone,property.lat,property.lon)
println(p)// i can display data
properties.add(p)//when i add to properties, the properties
}
if (properties.isEmpty()){
println("null")
}
}
)

Activity Launcher(File picker) is loading multiple times in single event - Jetpack compose

I am using a file picker inside a HorizontalPager in jetpack compose. When the corresponding screen is loaded while tapping the button, the launcher is triggered 2 times.
Code snippet
var openFileManager by remember {
mutableStateOf(false)
}
if (openFileManager) {
launcher.launch("*/*")
}
Button(text = "Upload",
onClick = {
openFileManager = true
})
Edited: First of all Ian's point is valid why not just launch it in the onClick directly? I also assumed that maybe you want to do something more with your true false value. If you want nothing but launch then all these are useless.
The screen can draw multiple times when you click and make openFileManager true so using only condition won't prevent it from calling multiple times.
You can wrap your code with LaunchedEffect with openFileManager as a key. The LaunchedEffect block will run only when your openFileManager change.
if (openFileManager) {
LaunchedEffect(openFileManager) {
launcher.launch("*/*")
}
}
You should NEVER store such important state inside a #Composable. Such important business logic is meant to be stored in a more robust holder like the ViewModel.
ViewModel{
var launch by mutableStateOf (false)
private set
fun updateLaunchValue(newValue: Boolean){
launch = newValue
}
}
Pass these to the Composable from the main activity
MyComposable(
launchValue = viewModel.launch
updateLaunchValue = viewModel::updateLaunchValue
)
Create the parameters in the Composable as necessary
#Comoosable
fun Uploader(launchValue: Boolean, onUpdateLaunchValue: (Boolean) -> Unit){
LaunchedEffect (launchValue){
if (launchValue)
launcher.launch(...)
}
Button { // This is onClick
onUpdateLaunchValue(true) // makes the value true in the vm, updating state
}
}
If you think it is overcomplicated, you're in the wrong paradigm. This is the recommended AND CORRECT way of handling state in Compose, or any declarative paradigm, really afaik. This keeps the code clean, while completely separating UI and data layers, allowing controlled interaction between UI and state to achieve just the perfect behaviour for the app.

Use observe for a variable that updated inside another observe in Kotlin

I am trying first handle the response from API by using observe. Later after observing the handled variable I want to save it to database.
The variable tokenFromApi is updated inside tokenResponseFromApi's observer. Is it possible to observe tokenFromApi outside the observer of tokenResponseFromApi? When debugged, the code did not enter inside tokenFromApi observer when the app started.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
var tokenResponseFromApi: LiveData<String>? = MutableLiveData<String>()
var tokenFromApi: LiveData<TokenEntity>? = MutableLiveData<TokenEntity>()
tokenResponseFromApi?.observe(viewLifecycleOwner, Observer {
tokenResponseFromApi ->
if (tokenResponseFromApi != null) {
tokenFromApi = viewModel.convertTokenResponseToEntity(tokenResponseFromApi, dh.asDate)
}
})
tokenFromApi?.observe(viewLifecycleOwner, Observer {
tokenFromApi ->
if (tokenFromApi != null) {
viewModel.saveTokenToDB(repo, tokenFromApi)
}
})
}
Your problem is that you're registering the observer on tokenFromApi during setup, and when you get your API response, you're replacing tokenFromApi without registering an observer on it. So if it ever emits a value, you'll never know about it. The only observer you have registered is the one on the discarded tokenFromApi which is never used by anything
Honestly your setup here isn't how you're supposed to use LiveData. Instead of creating a whole new tokenFromApi for each response, you'd just have a single LiveData that things can observe. When there's a new value (like an API token) you set that on the LiveData, and all the observers see it and react to it. Once that's wired up, it's done and it all works.
The way you're doing it right now, you have a data source that needs to be taken apart, replaced with a new one, and then everything reconnected to it - every time there's a new piece of data, if you see what I mean.
Ideally the Fragment is the UI, so it reacts to events (by observing a data source like a LiveData and pushes UI events to the view model (someone clicked this thing, etc). That API fetching and DB storing really belongs in the VM - and you're already half doing that with those functions in the VM you're calling here, right? The LiveDatas belong in the VM because they're a source of data about what's going on inside the VM, and the rest of the app - they expose info the UI needs to react to. Having the LiveData instances in your fragment and trying to wire them up when something happens is part of your problem
Have a look at the App Architecture guide (that's the UI Layer page but it's worth being familiar with the rest), but this is a basic sketch of how I'd do it:
class SomeViewModel ViewModel() {
// private mutable version, public immutable version
private val _tokenFromApi = MutableLiveData<TokenEntity>()
val tokenFromApi: LiveData<TokenEntity> get() = _tokenFromApi
fun callApi() {
// Do your API call here
// Whatever callback/observer function you're using, do this
// with the result:
result?.let { reponse ->
convertTokenResponseToEntity(response, dh.asDate)
}?.let { token ->
saveTokenToDb(repo, token)
_tokenFromApi.setValue(token)
}
}
private fun convertTokenResponseToEntity(response: String, date: Date): TokenEntity? {
// whatever goes on in here
}
private fun saveTokenToDb(repo: Repository, token: TokenEntity) {
// whatever goes on in here too
}
}
so it's basically all contained within the VM - the UI stuff like fragments doesn't need to know anything about API calls, whether something is being stored, how it's being stored. The VM can update one of its exposed LiveData objects when it needs to emit some new data, update some state, or whatever - stuff that's interesting to things outside the VM, not its internal workings. The Fragment just observes whichever one it's interested in, and updates the UI as required.
(I know the callback situation might be more complex than that, like saving to the DB might involve a Flow or something. But the idea is the same - in its callback/result function, push a value to your LiveData as appropriate so observers can receive it. And there's nothing wrong with using LiveData or Flow objects inside the VM, and wiring those up so a new TokenEntity gets pushed to an observer that calls saveTokenToDb, if that kind of pipeline setup makes sense! But keep that stuff private if the outside world doesn't need to know about those intermediate steps

Categories

Resources