0

I have this rating button on my fragment that observes the post request on my viewmodel. The thing is, I would like to tell the user whether the post request was successful or not through a toast but as I have it now, I can only post the request and see if it was successful by the logs. How can I do that?

This is the button:

private fun ratingButton() {
       binding.btnRating.setOnClickListener {
           arguments?.getInt("Id")?.let {
               arguments?.getString("Session Id").let { it1 ->
                   if (it1 != null) {
                       viewModel.postRating(it, mapOf("value" to binding.ratingBar.rating), it1)
                   }
               }
           }
       }
   }

This is the view model:

class RatingViewModel constructor(
   private val remoteDataSource: MovieRemoteDataSource
): ViewModel() {


   val ratingSuccess = MutableLiveData<Boolean>()
   val ratingFailedMessage = MutableLiveData<String?>()

   private var _rating = MutableLiveData<Resource<RatingResponse>>()

   val rating: LiveData<Resource<RatingResponse>>
       get() = _rating

   fun postRating(rating:Int, id:Map<String,Float>, session_id:String){
       remoteDataSource.postRating(rating, id, session_id,  object: MovieRemoteDataSource.RatingCallBack<RatingResponse>{
           override fun onSuccess(value: Resource<RatingResponse>){
               ratingSuccess.postValue(true)
               _rating.value = value
           }
           override fun onError(message:String?){
               ratingSuccess.postValue(false)
               ratingFailedMessage.postValue(message)
           }
       })
   }
}

This is the remote data source:

interface RatingCallBack<T> {
       fun onSuccess(value: Resource<T>)
       fun onError(message: String?)
   }

   fun postRating(rating: Int, id:Map<String,Float>, session_id:String, ratingCallback: RatingCallBack<RatingResponse>) {
       val service = RetrofitService.instance
           .create(MovieService::class.java)

       val call = service.postRating(rating, session_id, id)

       call.enqueue(object : Callback<RatingResponse?> {
           override fun onResponse(
               call: Call<RatingResponse?>,
               response: Response<RatingResponse?>
           ) {
               if (response.isSuccessful) {
                   Log.d("d", "d")
               } else {
                   Log.d("d", "d")
               }
           }

           override fun onFailure(call: Call<RatingResponse?>, t: Throwable) {
               Log.d("d", "d")
           }
       })
   }

I added the resource because I think it might be helpful but it doesn't seem to be working:

data class Resource<out T> (
   val status: NetworkStatus,
   val data: T? = null,
   val message: String? = null
)

enum class NetworkStatus{
   LOADING,
   SUCCESS,
   ERROR
}
dazai
  • 766
  • 4
  • 25
  • in the remote datasource propagate the result using the ratingCallback object. ratingCallback.onSuccess(yourValue) or ratingCallback.onError(yourMessage) – Manuel Mato Aug 15 '21 at 14:31
  • I don't know about kotlin, but if you're returning a MutableLiveData from the view model method which indicates if the request succeeded or failed, you can put an observer on the method and check the boolean value after it finishes and returns the mutable live data – Omar Shawky Aug 15 '21 at 14:38
  • like here but change the List to Boolean, https://developer.android.com/topic/libraries/architecture/viewmodel#implement – Omar Shawky Aug 15 '21 at 14:39
  • Some related reading on MVVM and events vs state: https://stackoverflow.com/questions/53484781/android-mvvm-is-it-possible-to-display-a-message-toast-snackbar-etc-from-the – Richard Le Mesurier Jun 29 '23 at 11:07

3 Answers3

2

You don't call your ratingCallback in the dataSource once the postRating finishes, therefore the results don't get to the ViewModel. Do:

class MovieRemoteDataSource {
    fun postRating(ratingCallback: RatingCallBack<...>) {
        val service = RetrofitService.instance
            .create(MovieService::class.java)
        val call = service.postRating()

        call.enqueue(object : Callback<...> {
            override fun onResponse(
                call: Call<...>,
                response: Response<...>
            ) {
                if (response.isSuccessful) {
                    ratingCallback.onSuccess(...)
                } else {
                    ratingCallback.onError(...)
                }
            }

            override fun onFailure(call: Call<...>, t: Throwable) {
                ratingCallback.onError(...)
            }
        })
    }
}

If your toasts still won't show after adding the missing callbacks, you probably haven't set up your fragment to observe the liveData correctly yet. First, make sure your binding has LifecycleOwner assigned. Without it, liveData may not be observed:

binding = DataBindingUtil.inflate(...)
// add this line after you inflate the binding
binding.lifecycleOwner = viewLifecycleOwner

Then, observe your ratingSuccess liveData:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // make sure your viewModel has been created at this point, before calling observe()
        observe()
    }

    private fun observe() {
        viewModel.ratingSuccess.observe(viewLifecycleOwner, Observer { success ->
            if (success) {
                Toast.makeText(requireContext(), "Post rating succeeded", Toast.LENGTH_SHORT).show()
            } else {
                Toast.makeText(requireContext(), "Post rating failed", Toast.LENGTH_SHORT).show()
            }
        })
    }
Dat Pham Tat
  • 1,433
  • 7
  • 9
2

I just need to point out that using a LiveData like this for one-shot events will give you problems, unfortunately. LiveData is designed to always give each observer the most recent value, whereas what you want is to post a result, have one thing observe it once and then it's consumed.

Every time you register an observer on that LiveData, it'll get the last success or fail value, and pop a toast up. So whenever you rotate the device, move away from the app and come back etc, you get a message. And you probably don't want that!

It's unfortunately not a neatly solved problem at this point - here's some reading:

LiveData with SnackBar, Navigation and other events (the SingleLiveEvent case) (written by one of the Android team)

Android SingleLiveEvent Redux with Kotlin Flow (recommended by the above link, I've used this approach - uses Flows and Channels to create consumable single events)

Yes it's a huge pain

cactustictacs
  • 17,935
  • 2
  • 14
  • 25
0

You can observe (in fragment) the value changes of ratingSuccess which is a MutableLiveData but I would strongly suggest to use LiveData for observing purposes.

viewModel.ratingSuccess.observe(viewLifecycleOwner, Observer {
     if (it) {
          Toast.makeText(this, "Request succeeded", Toast.LENGTH_SHORT).show()
     } else {
          Toast.makeText(this, "Request failed", Toast.LENGTH_SHORT).show()
     }
})

OR if you're using the latest Kotlin version:

viewModel.ratingSuccess.observe(viewLifecycleOwner) {
     if (it) {
         Toast.makeText(this, "Request succeeded", Toast.LENGTH_SHORT).show()
     } else {
         Toast.makeText(this, "Request failed", Toast.LENGTH_SHORT).show()
     }
}
  • Thank you, that seems to be the right way to do this but I'm confused as to where I'm supposed to place it, Itried after viewModel.postRating but that doesn't trigger the toast, even though the post request has to be made first before evaluating if it was succesful or not.. i think – dazai Aug 15 '21 at 15:21
  • Please try putting your viewModel observers in Activity's onCreate() function. These observers are usually set once in activity / fragment lifecycle. – Buddhabhushan Naik Aug 15 '21 at 15:30
  • okay but when is this observer supposed to go, given that first I need to pass the viewModel.postRating with all the values? – dazai Aug 15 '21 at 15:33
  • Order should be like this: 1. activity created 2. observers are set 3. user inputs are stored / updated 4. postRating called on click of the button 5. observers are notified automatically – Buddhabhushan Naik Aug 15 '21 at 15:34