Add multiple source to MediatorLiveData and change its value - android

Basically I have a screen, and there are a few EditTexts and a Button.
Users have to fill in all fields otherwise the Button is disabled.
I am using DataBinding to achieve this. Below is my code in the viewmodel.
val isNextEnabled = MediatorLiveData<Boolean>()
isNextEnabled.apply {
addSource(field1LiveData) {
isNextEnabled.value =
it != null
&& field2LiveData.value != null
&& field3LiveData.value != null
}
addSource(field2LiveData) {
isNextEnabled.value =
it != null
&& field1LiveData.value != null
&& field3LiveData.value != null
}
addSource(field3LiveData) {
isNextEnabled.value =
it != null
&& field2LiveData.value != null
&& field1LiveData.value != null
}
}
In the xml
<Button
android:enabled="#{viewmodel.isNextEnabled}"
.
.
.
</Button>
Everything works fine as expected. But the logic above looks cumbersome. What if I have more EditText ? The code would be painful to write/maintain.
Is there any way I can simplify it?

Ultimately you have a UseCase/Logic where you decide when the next button is enabled.
I think you should separate the logic into useCases where it makes sense.
E.g.
// update these when they change in the UI for e.g.
val field1Flow: Flow<Boolean> = flow { ... }
val field2Flow: Flow<Boolean> = flow { ... }
val nextButtonState = combine(field1Flow, field2Flow) { f1, f2 ->
f1 && f2
}.collect { state ->
// use your state.
}
Now... if you need special logic and not just two-boolean algebra here, you can always extract it into use-cases that return more flows.
Or map it or various operations you could do:
E.g.
class YourUseCase() {
operator fun invoke(field1: Boolean, field2: Boolean) {
// Your Logic
return field1 && field2
}
}
// And now...
val _nextButtonState = combine(field1Flow, field2Flow) { f1, f2 ->
YourUseCase(f1, f2)
}
val _uiState = _nextButtonState.transformLatest {
emit(it) // you could add a when(it) { } and do more stuff here
}
// And if you don't want to change your UI to use flows, you can expose this as live data
val uiState = _uiState.asLiveData()
Keep in mind this is Pseudo-code written on SO.. not even Notepad ;)
I hope that makes a bit of sense. The idea is to separate the bits into use-cases (that you can ultimately test in isolation) and to have a flow of data. When buttons change state, the fieldNFlow emits the values and this triggers the whole chain for you.
If you have the latest Coroutines (2.4.0+) you can use the new operators to avoid using LiveData, but overall, I'd try to think in that direction.
Lastly, your liveData code with a mediator is not bad, I'd at the very least, extract the "logic" into 3 different useCases so it's not all together in a series of if/else statements.
A word of caution: I haven't used Databinding in over 3(?) years, I'm personally not a fan of it so I cannot tell you if it would cause a problem with this approach.

Related

java.lang.IllegalStateException: You cannot access the NavBackStackEntry's ViewModels until it is added to the NavController's back stack

I have created a composable called ResolveAuth. ResolveAuth is the first screen when user opens the app after Splash. All it does is check whether an email is present in Datastore or not. If yes redirect to main screen and if not then redirect to tutorial screen
Here is my composable and viewmodel code
#Composable
fun ResolveAuth(resolveAuthViewModel: ResolveAuthViewModel, navController: NavController) {
Scaffold(content = {
ProgressBar()
when {
resolveAuthViewModel.userEmail.value != "" -> {
navController.navigate(Screen.Main.route) {
popUpTo(0)
}
resolveAuthViewModel.userEmail.value = null
}
resolveAuthViewModel.userEmail.value == "" -> {
navController.navigate(Screen.Tutorial.route) {
popUpTo(0)
}
resolveAuthViewModel.userEmail.value = null
}
}
})
}
#HiltViewModel
class ResolveAuthViewModel #Inject constructor(
private val dataStoreManager: DataStoreManager): ViewModel(){
val userEmail = MutableLiveData<String>()
init {
viewModelScope.launch{
val job = async {dataStoreManager.email.first()}
val email = job.await()
if(email != ""){
userEmail.value = email
}
}
}
}
But I keep getting an exception saying
java.lang.IllegalStateException: You cannot access the NavBackStackEntry's ViewModels until it is added to the NavController's back stack (i.e., the Lifecycle of the NavBackStackEntry reaches the CREATED state).
I am using below jetpack lib for navigation
implementation("androidx.navigation:navigation-compose:2.4.0-rc01")
There is no issue in my Main and Tutorial screen as I tried to run them separately and it works fine.
Easily resolvable, just add this when call to a Side-Effect instead.
LaunchedEffect(Unit){
while(!isNavStackReady) // Hold execution while the NavStack populates.
delay(16) // Keeps the resources free for other threads.
when {
resolveAuthViewModel.userEmail.value != "" -> {
navController.navigate(Screen.Main.route) {
popUpTo(0)
}
resolveAuthViewModel.userEmail.value = null
}
resolveAuthViewModel.userEmail.value == "" -> {
navController.navigate(Screen.Tutorial.route) {
popUpTo(0)
}
resolveAuthViewModel.userEmail.value = null
}
}
}
Here, the call to navigate is made only after the currentBackStackEntry has been completely filled, so it yields no error. The original error occurred since you were calling navigate before the concerned composable was even made available to the nav stack.
As for how to update the isNavStackReady variable to reflect the correct state of the navStack, it is fairly simple. Create the variable at a top-level declaration, such that only the required components may access it. May as well throw it inside a viewModel if you please. Set the default value of the var to false, for obvious reasons. Here's the update mechanism.
#Composable
fun StartDestination(){
isNavStackReady = true
}
That's it, that's really it. If you could successfully navigate to your start destination that you define in the nav graph, it means the navStack has likely been populated well. Hence, you just update this variable here, and the LaunchedEffect block up there will respond to this update, and the while loop that's been holding execution off, will finally break. It will then call the navigate on the appropriate destination route. Remember, however, that the isNavStackReady variable, for this mechanism to work, needs to be a state-holder, i.e., initialised with mutableStateOf(false). Using delegates, of course, is completely fine (personally encouraged).
Now, all this is fine, but actually, it's not quite the right implementation. You see, this entire thing is taken care of completely internally by the navigation APIs for us, but it breaks because we are trying to do its job, and we suck at it.
We are creating an intermediate route to land on, at the start of the app, and from there, immediately navigating to another screen based on calculations. So, all we want is to open the app at a desired page, that is, start the navigator on a desired page when it is first created. We have a handy parameter called startDestination, just for that.
Hence, the ideal, simple, beautiful solution would be to just
startDestination = when {
resolveAuthViewModel.userEmail.value != "" -> {
navController.navigate(Screen.Main.route) {
popUpTo(0)
}
resolveAuthViewModel.userEmail.value = null
}
resolveAuthViewModel.userEmail.value == "" -> {
navController.navigate(Screen.Tutorial.route) {
popUpTo(0)
}
resolveAuthViewModel.userEmail.value = null
}
}
in your NavBuilder's arguments. Tiniest silliest logical flaw, that so many people couldn't get. It's intriguing to think how the human mind works...
Happy New Year,

How to combine livedata and kotlin flow

Is this good to put the collect latest inside observe?
viewModel.fetchUserProfileLocal(PreferencesManager(requireContext()).userName!!)
.observe(viewLifecycleOwner) {
if (it != null) {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
viewModel.referralDetailsResponse.collect { referralResponseState ->
when (referralResponseState) {
State.Empty -> {
}
is State.Failed -> {
Timber.e("${referralResponseState.message}")
}
State.Loading -> {
Timber.i("LOADING")
}
is State.Success<*> -> {
// ACCESS LIVEDATA RESULT HERE??
}}}}
I'm sure it isn't, my API is called thrice too as the local DB changes, what is the right way to do this?
My ViewModel looks like this where I'm getting user information from local Room DB and referral details response is the API response
private val _referralDetailsResponse = Channel<State>(Channel.BUFFERED)
val referralDetailsResponse = _referralDetailsResponse.receiveAsFlow()
init {
val inviteSlug: String? = savedStateHandle["inviteSlug"]
// Fire invite link
if (inviteSlug != null) {
referralDetail(inviteSlug)
}
}
fun referralDetail(referral: String?) = viewModelScope.launch {
_referralDetailsResponse.send(State.Loading)
when (
val response =
groupsRepositoryImpl.referralDetails(referral)
) {
is ResultWrapper.GenericError -> {
_referralDetailsResponse.send(State.Failed(response.error?.error))
}
ResultWrapper.NetworkError -> {
_referralDetailsResponse.send(State.Failed("Network Error"))
}
is ResultWrapper.Success<*> -> {
_referralDetailsResponse.send(State.Success(response.value))
}
}
}
fun fetchUserProfileLocal(username: String) =
userRepository.getUserLocal(username).asLiveData()
You can combine both streams of data into one stream and use their results. For example we can convert LiveData to Flow, using LiveData.asFlow() extension function, and combine both flows:
combine(
viewModel.fetchUserProfileLocal(PreferencesManager(requireContext()).userName!!).asFlow(),
viewModel.referralDetailsResponse
) { userProfile, referralResponseState ->
...
}.launchIn(viewLifecycleOwner.lifecycleScope)
But it is better to move combining logic to ViewModel class and observe the overall result.
Dependency to use LiveData.asFlow() extension function:
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0"
it certainly is not a good practice to put a collect inside the observe.
I think what you should do is collect your livedata/flows inside your viewmodel and expose the 'state' of your UI from it with different values or a combined state object using either Flows or Livedata
for example in your first code block I would change it like this
get rid of "userProfile" from your viewmodel
create and expose from your viewmodel to your activity three LiveData/StateFlow objects for your communityFeedPageData, errorMessage, refreshingState
then in your viewmodel, where you would update the "userProfile" update the three new state objects instead
this way you will take the business logic of "what to do in each state" outside from your activity and inside your viewmodel, and your Activity's job will become to only update your UI based on values from your viewmodel
For the specific case of your errorMessage and because you want to show it only once and not re-show it on Activity rotation, consider exposing a hot flow like this:
private val errorMessageChannel = Channel<CharSequence>()
val errorMessageFlow = errorMessageChannel.receiveAsFlow()
What "receiveAsFlow()" does really nicely, is that something emitted to the channel will be collected by one collector only, so a new collector (eg if your activity recreates on a rotation) will not receive the message and your user will not see it again

Synchronizing value change in Coroutines

I have a ViewModel in android and I am trying to validate whether the person has entered his name and age before moving on to the next page.
Here is my code:
fun onContinueClick() {
val navigateNextPage: (Int) -> Unit = lambda#{ validation ->
if (validation < 2)
return#lambda
nextPageUseCase().onEach {
_navigationNotify.value = it // Moving on to the next page
}
}
viewModelScope.launch {
var valid = 0
getName().collect { name ->
if (name != null) navigateNextPage(++valid)
}
getAge().collect { age ->
if (age != null) navigateNextPage(++valid)
}
}
}
Although this is working as expected, is it efficient to perform this operation? I think that if both the ++valid happen at the same time, I wouldn't be able to go to the next page.
I want to know how to synchronize the code to avoid that situation.

Android Logic Based On Multiple Live Data Values

I am using the Android data binding library to make reactive views with LiveData
I make a repo request for a list of jobs
var jobsRequest: LiveData<Resource<List<Job>>>
= Transformations.switchMap(position) { repo.getJobsWithStatus(it) }
Then I have 3 more LiveData based on the above, like so
First, to check whether the request has completed
private val requestComplete: LiveData<Boolean>
= Transformations.map(jobsRequest) {
it.status == Status.SUCCESS || it.status == Status.ERROR
}
Next, to transform to a list of jobs without the resource wrapper
var jobs: LiveData<List<Job>>
= Transformations.map(jobsRequest) { it.data }
Lastly, to check if that job list is empty
val jobsEmpty: LiveData<Boolean>
= Transformations.map(jobs) { (it ?: emptyList()).isEmpty() }
In the layout I want to show a loading spinner if the request has not completed and the jobs list is empty and need a variable in my view model to dictate this
I have tried the code below and, as expected, it does not work
val spinnerVisible: LiveData<Boolean>
= Transformations.map(requestComplete) {
!(requestComplete.value ?: false) && (jobsEmpty.value ?: true)
}
What is the correct practice for having a LiveData variable based on the state of 2 others - I want to keep all logic in the view model, not in the activity or layout.
Is the jobsEmpty observer needed? Seems like you could reuse the jobs one for it.
Anway, to your question:
For this there is a MediatorLiveData. It does what you need: it can merge multiple (in your case: 2) LiveData objects and can determine another livedata value based on that.
Some pseudo-code:
MediatorLiveData showSpinner = new MediatorLiveData<Boolean>()
showSpinner.addSource(jobsEmpty, { isEmpty ->
if (isEmpty == true || requestComplete.value == true) {
// We should show!
showSpinner.value = true
}
// Remove observer again
showSpinner.removeSource(jobsEmpty);
})
showSpinner.addSource(requestComplete, { isCompleted ->
if (isCompleted == true && jobsEmpty == true) {
// We should show!
showSpinner.value = true
}
// Remove observer again
showSpinner.removeSource(requestComplete);
})
return showSpinner
Note that you need to return the mediatorlivedata as result, as this is the object you are interested in for your layout.
Additionally, you can check the documentation on the MediatorLiveData, it has some more examples: https://developer.android.com/reference/android/arch/lifecycle/MediatorLiveData

Kotlin - equivalence to Swift's combination of "if let + cast"

I'm trying to find out how to achieve the combination of "if let + cast" in kotlin:
in swift:
if let user = getUser() as? User {
// user is not nil and is an instance of User
}
I saw some documentation but they say nothing regarding this combination
https://medium.com/#adinugroho/unwrapping-sort-of-optional-variable-in-kotlin-9bfb640dc709
https://kotlinlang.org/docs/reference/null-safety.html
One option is to use a safe cast operator + safe call + let:
(getUser() as? User)?.let { user ->
...
}
Another would be to use a smart cast inside the lambda passed to let:
getUser().let { user ->
if (user is User) {
...
}
}
But maybe the most readable would be to just introduce a variable and use a smart cast right there:
val user = getUser()
if (user is User) {
...
}
Kotlin can automatically figure out whether a value is nil or not in the current scope based on regular if statements with no need for special syntax.
val user = getUser()
if (user != null) {
// user is known to the compiler here to be non-null
}
It works the other way around too
val user = getUser()
if (user == null) {
return
}
// in this scope, the compiler knows that user is not-null
// so there's no need for any extra checks
user.something
In Kotlin you can use the let:
val user = getUser()?.let { it as? User } ?: return
This solution is closest to guard but it may be useful.
In Kotlin you can use:
(getUser() as? User)?.let { user ->
// user is not null and is an instance of User
}
as? is a 'safe' cast operator that returns null instead of throwing an error on failure.
What about this one?
val user = getUser() ?: return

Categories

Resources