3

I am new to MVVM. so I have 2 requests to the server from my fragment/activity, the result from the first request will be used as an input parameter for the second request.

so first in my fragment, when a button is clicked then I make a request to check whether the user is banned or not, if not then this user can create a post.

so first I check if a user is banned or not using this code in my fragment

class CreateEventFragment : Fragment() {

    lateinit var model: CreateEventViewModel


    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {

        model = ViewModelProvider(this).get(CreateEventViewModel::class.java)

        button.setOnClickListener {
            model.checkIfUserIsBanned()
        }

    }


}

and here is the viewmodel

class CreateEventViewModel(application: Application) : AndroidViewModel(application) {

    val mUserIsBanned :MutableLiveData<Boolean> = UserClient.mUserIsBanned

    fun checkIfUserIsBanned(userID: String) {
        UserRepository.checkIfUserIsBanned(id)
    }


}

and here is the client ( I skip the repository for simplicity)

object UserClient {

    val mUserIsBanned = MutableLiveData<Boolean>()

    fun checkIfUserIsBanned(userID: String) {

        // perform networking, after getting the value then

        if (user.isBanned) {
            mUserIsBanned.postValue(true)
        } else {
            mUserIsBanned.postValue(false)
        }

    }



}

here is the problem, the second request needs the result of the first result, i.e the mUserIsBanned is need to check if the user is not banned then perform the second request (user create a post). my question is, where do I place this logic ? in viewmodel or in my fragment?

if (userIsBanned == false) {
   createPost()
}

from the tutorial I have seen, it seems the livedata is always observed in a fragment. so the first option is to place the logic in fragment like this

    model.mUserIsBanned.observe(viewLifecycleOwner, Observer { isBanned ->

        val userIsBanned = isBanned ?: return@Observer

        if (!userIsBanned) {
            model.createPost()
        }

    })

is it okay to place code checking like that in a fragment?

actually I don't need to observed the isBanned, I just need to check it once

or the second option is to check userIsBanned or not in viewmodel, but I don't know how to do livedata observation in viewmodel

or my approach is all wrong ? I am not sure using this MVVM

please help, java is also ok.

sarah
  • 3,819
  • 4
  • 38
  • 80
  • I am opinion, checking if user banned or not is something 1-time thing, it may not happen anytime so this make it no use of making it a live data and you can directly use it as a normal boolean and call create a post based on it. – Akshay Nandwana Mar 21 '20 at 03:53
  • yes you are right Akshay, this is just a Single Live Event, could you please show the code how to implement that ? I am confused – sarah Mar 21 '20 at 03:56
  • sure, could you explain why are you using the client? is it because you want to put all your API calling sperate from view model? check [here](https://pl.kotl.in/CODVOnPel) – Akshay Nandwana Mar 21 '20 at 04:23
  • @AkshayNandwana yes, to separate it from viewModel – sarah Mar 21 '20 at 04:32
  • @AkshayNandwana is it okay to just put API call in ViewModel ? I am new in MVVM and I just follow the tutorial I watch by separate the API call in client – sarah Mar 21 '20 at 04:36
  • yes, it is completely fine. It's all about how you architect the structure. You can have a repository class, view model talking to a repository to call a particular API from Data source where you mentioned all the API. – Akshay Nandwana Mar 21 '20 at 04:39
  • @AkshayNandwana so from the playground you share, all API call will be called in viewmodel and just store the result of userIsBanned as normal boolean in viewmodel and then call the second request method also from viewmodel ? am I right ? – sarah Mar 21 '20 at 05:02
  • yes, that's the one way – Akshay Nandwana Mar 21 '20 at 05:08

4 Answers4

5

You can try MediatorLiveData for your second operation. What MediatorLiveData does is, it creates a listenable container for your various LiveData objects & provide you callback once any of the underlying/observing value changes.

Example: Assume that LiveData<B> needs to be called on any value changes from LiveData<A>, here you can consider LiveData<B> as MediatorLiveData.

So declaration for LiveData<B> would be:

val bLiveData : LiveData<B> = MediatorLiveData<B>().apply {
    addSource(aLiveData) { aData ->
        value = convertADataToB(aData) //value is backing property for getValue()/setValue() method, use postValue() explicitly upon bg operation
    }
}

In your case, put this code inside your ViewModel:

val createPostLiveData: LiveData<Boolean> = MediatorLiveData<Boolean>().apply {
    addSource(mUserIsBanned) { flag ->
        if (!flag) {
            createPost() // Check whether you can return result from here and provide to this mediator livedata a value
        }
    }
}

Refer MediatorLiveData

Jeel Vankhede
  • 11,592
  • 2
  • 28
  • 58
  • 1
    as far as I know, LiveData (mediator or transformation) will not be triggered, if it is not observed (for example in fragment). will the block inside `addSource` be triggered if that createPostLiveData not observed ? cmiiw, https://stackoverflow.com/questions/60628371/why-my-map-livedata-is-not-called-even-though-i-have-change-the-value – sarah Mar 27 '20 at 04:14
  • Yes, that's right but shouldn't you observing `createPostLiveData` on UI to provide some meaningful error/success message or UI updates? – Jeel Vankhede Mar 27 '20 at 06:48
3

Working solution in 2021:

For observing LiveData inside ViewModel, use observeForever(observer).

class FirstViewModel(application: Application) : AndroidViewModel(application) {
    val country = MutableLiveData<String>()

    private val countryObserver = Observer<String> { country ->     //for removing it later
        //do your stuff
    }

    init {
        country.value = "xxx"

        country.observeForever(countryObserver)     //key point
    }

    override fun onCleared() {
        super.onCleared()

        country.removeObserver(countryObserver)     //【must remove!!】 See below
    }
}
Sam Chen
  • 7,597
  • 2
  • 40
  • 73
2

or the second option is to check userIsBanned or not in viewmodel, but I don't know how to do livedata observation in viewmodel

Don't, the documentation stated it to not use observe inside of any ViewModel

ViewModel objects are designed to outlive specific instantiations of views or LifecycleOwners. This design also means you can write tests to cover a ViewModel more easily as it doesn't know about view and Lifecycle objects. ViewModel objects can contain LifecycleObservers, such as LiveData objects. However ViewModel objects must never observe changes to lifecycle-aware observables, such as LiveData objects. If the ViewModel needs the Application context, for example to find a system service, it can extend the AndroidViewModel class and have a constructor that receives the Application in the constructor, since Application class extends Context.

https://developer.android.com/topic/libraries/architecture/viewmodel

Bitwise DEVS
  • 2,858
  • 4
  • 24
  • 67
1

We can think about three approaches,

  1. you have to fetch isBanned every time you try to create a post
  2. you fetch isBanned once or once in 5min (after 5min cache expires)
  3. you never check it, API will return error response when trying to create a post if user isBanned. API also returns specific model/http_code so you can also understand that user isBanned

Approach 1 is not OK, approach 2 is OK if isBanned is used in other places as well and if you store it locally (either until next app open or some period of time). Approach 3 has to be there always, isBanned must always be checked by server as well.

Approach 2:

View/Fragment/Activity:

// has no idea about detail; isBanned check or other kinds of validations
// new type of validations can be added without modifying this code (max post characters)
observeCreatePost()
viewModel.createPost(params)

ViewModel:

// does not know about validations, checks. But if you create Post only using this viewModel, then validation can be here as well

val createPostState = MutableLiveData<Result<Boolean>>()

fun createPost(params:String){
   createPostState.postValue(UIResult.Loading)

   createPostUseCase(params)
     // or .subscribe()
     .observe{ result->
        // ideally you convert Exceptions to (String) representation here
        createPostState.postValue(result.data)
     }
}

CreatePostUseCase:

operator fun invoke(params:String):Result<Boolean>{
    // validations are here, multiple ViewModels can use this UseCase
    // validate if params are valid, if not return Result.Error()

    // still does not know if userBanned comes from local data or API
    if(repository.isUserBanned()){
       return Result.Error()
    }else{
       return repository.createPost(params)
    }
}

PostRepository:

fun isUserBanned():Boolean{
   if(userBanned exist locally and not expired)
       return userBanned locally
   else
       isUserBanned = api.isUserBanned()
       storeLocal(isUserBanned)
       return isUserBanned
}

fun createPost(params):Result<Boolean>{
   response = api.createPost(params)
   // return Result wrapped response 
}
Jemshit
  • 9,501
  • 5
  • 69
  • 106