1
votes

I'm trying to display an error message to the users when something went wrong with a Retrofit API call. I'm using Kotlin Coroutines, Kodein and MVVM pattern. I have trouble displaying the error message in a toast message given that the MutableLiveData exceptionMessage is not really observed in the fragment (I'm guessing this has something to do with the nature of the asynchronous function to get the data via the API (suspend fun)).

ViewModel:

class BarcodeViewModel(
    private val barcodeRepository: BarcodeRepository,
    private val productRepository: ProductRepository
) : ViewModel() {

    var exceptionMessage: MutableLiveData<String> = MutableLiveData()

    private val handler = CoroutineExceptionHandler { _, exception ->
        exceptionMessage.value = exception.localizedMessage
    }

    fun getBarcodeData(barcode: String) {
        CoroutineScope(Dispatchers.Main).launch(handler) {
            val currentArticle = barcodeRepository.getProductData(barcode)

            for (article in currentArticle.products) {
                val articleToAdd =
                    Product(...)

                val articleDb = productRepository.getProduct(barcode)
                if (articleDb.isEmpty()) {
                    productRepository.addProduct(articleToAdd)
                    exceptionMessage.value = ""
                } else {
                    exceptionMessage.value = "Product already exists"
                }
            }
        }
    }
}

Fragment:

class ArticleAddFragment : Fragment(), LifecycleOwner, KodeinAware {
    override val kodein: Kodein by kodein()
    private val factory: BarcodeViewModelFactory by instance()
    private lateinit var viewModel: BarcodeViewModel

override fun onCreateView(
    inflater: LayoutInflater, container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {
    val view = inflater.inflate(R.layout.fragment_article_add, container, false)

    viewModel = ViewModelProviders.of(this, factory).get(BarcodeViewModel::class.java)

    ...

    return view
}

private fun processResult(firebaseVisionBarcodes: List<FirebaseVisionBarcode>) {
    if (firebaseVisionBarcodes.isNotEmpty()) {
        for (item in firebaseVisionBarcodes) {
            when (item.valueType) {
                FirebaseVisionBarcode.TYPE_PRODUCT -> {
                    viewModel.getBarcodeData(item.rawValue!!)
                    viewModel.exceptionMessage.observe(viewLifecycleOwner, Observer {
                        it?.let {
                            if (!it.isBlank()) {
                                Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
                            }
                        }
                    })
                    ...
                }
            }
        }
    }
}

What's the smartest way to display the error message in a toast?

1
When you call processResult? I think you should observe LiveData inside onViewCreated, it means fragment already create viewLifecycleOwner. - Công Hải
In the onCreate method another method is called that starts the camera, if the camera finds a barcode in the live feed from the camera processResult is called. I've already tried observing LiveData in onViewCreated but it's always null even when the exception in the coroutine is thrown :/ - masterR0shi
CoroutineScope(Dispatchers.Main).launch(handler) does this job is finishing or causing any exception and also Do you want show exception message.value = "Product already exists"? - sunil

1 Answers

0
votes

I would suggest using Sealed Classes or Enum to update and observe the state.

State.kt

sealed class State<T> {
    class Loading<T> : State<T>()

    data class Success<T>(val data: T) : State<T>()

    data class Error<T>(val message: String) : State<T>()

    companion object {

        /**
         * Returns [State.Loading] instance.
         */
        fun <T> loading() = Loading<T>()

        /**
         * Returns [State.Success] instance.
         * @param data Data to emit with status.
         */
        fun <T> success(data: T) = Success(data)

        /**
         * Returns [State.Error] instance.
         * @param message Description of failure.
         */
        fun <T> error(message: String) = Error<T>(message)
    }

}

The classes are generic, so you can put the data accordingly on the basis of success and failure.