What the difference between observe and wrapper observeEvents (LiveData)? - android

There is a convenient wrapper that allows you to reduce the boilerplate when you work with LiveData - observeEvents.
open class Event<T>(value: T? = null) {
val liveData = MutableLiveData(value)
protected var hasBeenHandled = false
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled || liveData.value == null) {
null
} else {
hasBeenHandled = true
liveData.value
}
}
companion object {
fun <T> LifecycleOwner.observeEvents(event: Event<T>, body: (T?) -> Unit) {
event.liveData.observe(this) { body(event.getContentIfNotHandled()) }
}
}
}
class MutableEvent<T>(value: T? = null) : Event<T>(value) {
#MainThread
fun fireEvent(event: T) {
hasBeenHandled = false
liveData.value = event
}
#WorkerThread
fun postEvent(event: T) {
hasBeenHandled = false
liveData.postValue(event)
}
}
Next, we can see how to use it.
There is the following sealed class for specific events:
sealed class ProductEvent {
data class AddProduct(val data: SomeProduct) : ProductEvent()
data class RemoveProduct(val productId: String) : ProductEvent()
}
ViewModel code:
private val _productEvents = MutableEvent<ProductEvent>()
val productEvents = _productEvents
private fun addProduct() {
val product: SomeProduct = repository.getProduct()
_productEvents.fireEvent(ProductEvent.AddProduct(product)
}
Activity/Fragment code:
observeEvents(viewModel.productEvents) { event ->
event?.let {
when(event) {
is ProductEvent.AddProduct -> // add product
is ProductEvent.RemoveProduct-> // remove product
}
}
}
Everything works fine, but there is one thing.
For example, when we use registerForActivityResult:
private val result = registerForActivityResult(ActivityResultContracts.StartActivityForResult())
{ result: ActivityResult ->
if (result.resultCode == Activity.RESULT_OK) {
result.data?.getIntExtra(SomeActivity.SOME_RESULT, 0)?.let {
// do work that will call ProductEvent
// viewModel.addProduct() - for example
}
}
}
When SomeActivity finishes and we return here, this code will run before LifecycleOwner will be active and because of that a subscriber will not be called.
There is a solution (lifecycleScope.launchWhenResumed), but the fact is that if we define our LiveData as usual:
// viewModel
private val _product = MutableLiveData<SomeProduct>()
val product = _product
// Activity/Fragment
viewModel.product.observe(lifecycleOwner) {}
then the subscriber will work as expected.
I would like to know what the difference. observeEvents is merely a wrapper that does the same thing, but for some reason works a little differently.

Related

Get data asynchronously with Coroutine from Room Database

so i'm building an export function to export data from Room Database to external file (*.txt file), i'm trying to achieve this by using suspend function from Dao to ViewModel to get all the data, I think i don't really need LiveData because i dont observe it and just calling it one time. Here are the codes
ItemDao
#Dao
interface ItemDao {
#Query("SELECT * FROM item_table")
suspend fun readItemWithUnits_(): List<ItemModel>
}
ViewModel
#HiltViewModel
class HomeViewModel #Inject constructor (private val itemDao: ItemDao): ViewModel() {
fun readItemWithUnits_(): Deferred<List<ItemModel>> {
return viewModelScope.async(Dispatchers.IO) {
itemDao.readItemWithUnits_()
}
}
}
And calling the Deferred from a Fragment inside withContext(Dispatchers.Main)
Fragment
val viewModel: HomeViewModel by viewModels()
// Codes before //
private val writeExample = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == Activity.RESULT_OK) {
val userChosenUri = it.data?.data
val outStream = requireContext().contentResolver.openOutputStream(userChosenUri!!)
lifecycleScope.launchWhenCreated {
withContext(Dispatchers.Main) {
val listOfItemWithUnit = viewModel.readItemWithUnits_().await()
var exportContent = "#item_table\n"
listOfItemWithUnit.forEach { itemModel ->
exportContent += "${itemModel.itemId};${itemModel.itemName};${itemModel.itemNote}\n"
}
exportContent.byteInputStream().use { input ->
outStream.use { output ->
input.copyTo(output!!)
}
}
}
}
}
}
// Codes After //
For now these codes work just fine, the question is, am i doing it correctly? since i will be dealing with a lot of data or is there a better way?
Edit
I've tried something like this, change from lifecycleScope.launchWhenCreated to CoroutineScope(Dispatchers.IO).launch
private val writeExample = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == Activity.RESULT_OK) {
val userChosenUri = it.data?.data
val outStream = requireContext().contentResolver.openOutputStream(userChosenUri!!)
CoroutineScope(Dispatchers.IO).launch {
val listOfItemWithUnit = viewModel.readItemWithUnits_().await()
var exportContent = "#item_table\n"
listOfItemWithUnit.forEach { itemModel ->
exportContent += "${itemModel.item.itemId};${itemModel.item.itemName};${itemModel.item.itemNote}\n"
}
exportContent.byteInputStream().use { input ->
outStream.use { output ->
input.copyTo(output!!)
}
}
}
}
}
It return an error
java.lang.IllegalStateException: Method addObserver must be called on the main thread
And this, without withContext(Dispatchers.Main)
private val writeExample = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == Activity.RESULT_OK) {
val userChosenUri = it.data?.data
val outStream = requireContext().contentResolver.openOutputStream(userChosenUri!!)
lifecycleScope.launchWhenCreated {
val listOfItemWithUnit = viewModel.readItemWithUnits_().await()
var exportContent = "#item_table\n"
listOfItemWithUnit.forEach { itemModel ->
exportContent += "${itemModel.item.itemId};${itemModel.item.itemName};${itemModel.item.itemNote}\n"
}
exportContent.byteInputStream().use { input ->
outStream.use { output ->
input.copyTo(output!!)
}
}
}
}
}
It return an error
java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
Structure your code like this
CoroutineScope(Dispatchers.IO).launch{ // do your background tasks here
withContext(Dispatchers.Main){ //do tasks on the main thread that you want with that data
} }
Since it is a database operation without anything on the main thread the whole code will be in the CoroutineScope(Dispatchers.IO).launch block.

Android: Firebase Object is null when using kotlin flow

My problem is, that when I try to get a document out of my database, that this document aka the object is always null. I only have this problem when I use Kotlin Coroutines to get the document out of my database. Using the standard approach with listeners do work.
EmailRepository
interface EmailRepository {
suspend fun getCalibratePrice(): Flow<EmailEntity?>
suspend fun getRepairPrice(): Flow<EmailEntity?>
}
EmailRepository Implementation
class EmailRepositoryImpl #Inject constructor(private val db: FirebaseFirestore) : EmailRepository {
fun hasInternet(): Boolean {
return true
}
// This works! When using flow to write a document, the document is written!
override fun sendEmail(email: Email)= flow {
emit(EmailStatus.loading())
if (hasInternet()) {
db.collection("emails").add(email).await()
emit(EmailStatus.success(Unit))
} else {
emit(EmailStatus.failed<Unit>("No Email connection"))
}
}.catch {
emit(EmailStatus.failed(it.message.toString()))
}.flowOn(Dispatchers.Main)
// This does not work! "EmailEntity" is always null. I checked the document path!
override suspend fun getCalibratePrice(): Flow<EmailEntity?> = flow {
val result = db.collection("emailprice").document("Kalibrieren").get().await()
emit(result.toObject<EmailEntity>())
}.catch {
}.flowOn(Dispatchers.Main)
// This does not work! "EmailEntity" is always null. I checked the document path!
override suspend fun getRepairPrice(): Flow<EmailEntity?> = flow {
val result = db.collection("emailprice").document("Reparieren").get().await()
emit(result.toObject<EmailEntity>())
}.catch {
}.flowOn(Dispatchers.Main)
}
Viewmodel where I get the data
init {
viewModelScope.launch {
withContext(Dispatchers.IO) {
if (subject.value != null){
when(subject.value) {
"Test" -> {
emailRepository.getCalibratePrice().collect {
emailEntity.value = it
}
}
"Toast" -> {
emailRepository.getRepairPrice().collect {
emailEntity.value = it
}
}
}
}
}
}
}
private val emailEntity = MutableLiveData<EmailEntity?>()
private val _subject = MutableLiveData<String>()
val subject: LiveData<String> get() = _subject
Fragment
#AndroidEntryPoint
class CalibrateRepairMessageFragment() : EmailFragment<FragmentCalibrateRepairMessageBinding>(
R.layout.fragment_calibrate_repair_message,
) {
// Get current toolbar Title and send it to the next fragment.
private val toolbarText: CharSequence by lazy { toolbar_title.text }
override val viewModel: EmailViewModel by navGraphViewModels(R.id.nav_send_email) { defaultViewModelProviderFactory }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Here I set the data from the MutableLiveData "subject". I don't know how to do it better
viewModel.setSubject(toolbarText.toString())
}
}
One would say, that the Firebase rules are the problems here, but that should not be the case here, because the database is open and using the listener approach does work.
I get the subject.value from my CalibrateRepairMessageFragment. When I don't check if(subject.value != null) I get a NullPointerException from my init block.
I will use the emailEntitiy only in my viewModel and not outside it.
I appreciate every help, thank you.
EDIT
This is the new way I get the data. The object is still null! I've also added Timber.d messages in my suspend functions which also never get executed therefore flow never throws an error.. With this new approach I don't get a NullPointerException anymore
private val emailEntity = liveData {
when(subject.value) {
"Test" -> emailRepository.getCalibratePrice().collect {
emit(it)
}
"Toast" -> emailRepository.getRepairPrice().collect {
emit(it)
}
// Else block is never executed, therefore "subject.value" is either Test or toast and the logic works. Still error when using flow!
else -> EmailEntity("ERROR", 0F)
}
}
I check if the emailEntity is null or not with Timber.d("EmailEntity is ${emailEntity.value}") in one of my functions.
I then set the price with val price = MutableLiveData(emailEntity.value?.basePrice ?: 1000F) but because emailentity is null the price is always 1000
EDIT 2
I have now further researched the problem and made a big step forward. When observing the emailEntity from a fragment like CalibrateRepairMessageFragment the value is no longer null.
Furthermore, when observing emailEntity the value is also not null in viewModel, but only when it is observed in one fragment! So how can I observe emailEntity from my viewModel or get the value from my repository and use it in my viewmodel?
Okay, I have solved my problem, this is the final solution:
Status class
sealed class Status<out T> {
data class Success<out T>(val data: T) : Status<T>()
class Loading<T> : Status<T>()
data class Failure<out T>(val message: String?) : Status<T>()
companion object {
fun <T> success(data: T) = Success<T>(data)
fun <T> loading() = Loading<T>()
fun <T> failed(message: String?) = Failure<T>(message)
}
}
EmailRepository
interface EmailRepository {
fun sendEmail(email: Email): Flow<Status<Unit>>
suspend fun getCalibratePrice(): Flow<Status<CalibrateRepairPricing?>>
suspend fun getRepairPrice(): Flow<Status<CalibrateRepairPricing?>>
}
EmailRepositoryImpl
class EmailRepositoryImpl (private val db: FirebaseFirestore) : EmailRepository {
fun hasInternet(): Boolean {
return true
}
override fun sendEmail(email: Email)= flow {
Timber.d("Executed Send Email Repository")
emit(Status.loading())
if (hasInternet()) {
db.collection("emails").add(email).await()
emit(Status.success(Unit))
} else {
emit(Status.failed<Unit>("No Internet connection"))
}
}.catch {
emit(Status.failed(it.message.toString()))
}.flowOn(Dispatchers.Main)
// Sends status and object to viewModel
override suspend fun getCalibratePrice(): Flow<Status<CalibrateRepairPricing?>> = flow {
emit(Status.loading())
val entity = db.collection("emailprice").document("Kalibrieren").get().await().toObject<CalibrateRepairPricing>()
emit(Status.success(entity))
}.catch {
Timber.d("Error on getCalibrate Price")
emit(Status.failed(it.message.toString()))
}
// Sends status and object to viewModel
override suspend fun getRepairPrice(): Flow<Status<CalibrateRepairPricing?>> = flow {
emit(Status.loading())
val entity = db.collection("emailprice").document("Kalibrieren").get().await().toObject<CalibrateRepairPricing>()
emit(Status.success(entity))
}.catch {
Timber.d("Error on getRepairPrice")
emit(Status.failed(it.message.toString()))
}
}
ViewModel
private lateinit var calibrateRepairPrice: CalibrateRepairPricing
private val _calirateRepairPriceErrorState = MutableLiveData<Status<Unit>>()
val calibrateRepairPriceErrorState: LiveData<Status<Unit>> get() = _calirateRepairPriceErrorState
init {
viewModelScope.launch {
when(_subject.value.toString()) {
"Toast" -> emailRepository.getCalibratePrice().collect {
when(it) {
is Status.Success -> {
calibrateRepairPrice = it.data!!
_calirateRepairPriceErrorState.postValue(Status.success(Unit))
}
is Status.Loading -> _calirateRepairPriceErrorState.postValue(Status.loading())
is Status.Failure -> _calirateRepairPriceErrorState.postValue(Status.failed(it.message))
}
}
else -> emailRepository.getRepairPrice().collect {
when(it) {
is Status.Success -> {
calibrateRepairPrice = it.data!!
_calirateRepairPriceErrorState.postValue(Status.success(Unit))
}
is Status.Loading -> _calirateRepairPriceErrorState.postValue(Status.loading())
is Status.Failure -> _calirateRepairPriceErrorState.postValue(Status.failed(it.message))
}
}
}
price.postValue(calibrateRepairPrice.head!!.basePrice)
}
}
You can now observe the status in one of your fragments (but you dont need to!)
Fragment
viewModel.calibrateRepairPriceErrorState.observe(viewLifecycleOwner) { status ->
when(status) {
is Status.Success -> requireContext().toast("Price successfully loaded")
is Status.Loading -> requireContext().toast("Price is loading")
is Status.Failure -> requireContext().toast("Error, Price could not be loaded")
}
}
This is my toast extensions function:
fun Context.toast(text: String, duration: Int = Toast.LENGTH_SHORT) {
Toast.makeText(this, text, duration).show()
}

Android UI testing: Why LiveData's observers are not being called?

I have been trying, without success, to do some UI tests on Android.
My app follows the MVVM architecture and uses Koin for DI.
I followed this tutorial to properly set up a UI test for a Fragment with Koin, MockK and Kakao.
I created the custom rule for injecting mocks, setup the ViewModel, and on the #Before call, run the expected answers and returns with MockK. The problem is that, even when the fragment's viewmodel's LiveData object is the same as the testing class's LiveData object, the Observer's onChange is never triggered on the Fragment.
I run the test with the debugger and it seems the LiveData functions and MockK's answers are properly called. The logs show that the value hold by the LiveData objects is the same. The lifecycle of the Fragment when the test is running is Lifecycle.RESUMED. So why is the Observer's onChange(T) not being triggered?
The custom rule:
#VisibleForTesting(otherwise = VisibleForTesting.NONE)
abstract class FragmentTestRule<F : Fragment> :
ActivityTestRule<FragmentActivity>(FragmentActivity::class.java, true, true) {
override fun afterActivityLaunched() {
super.afterActivityLaunched()
activity.runOnUiThread {
val fm = activity.supportFragmentManager
val transaction = fm.beginTransaction()
transaction.replace(
android.R.id.content,
createFragment()
).commit()
}
}
override fun beforeActivityLaunched() {
super.beforeActivityLaunched()
val app = InstrumentationRegistry.getInstrumentation()
.targetContext.applicationContext as VideoWorldTestApp
app.injectModules(getModules())
}
protected abstract fun createFragment(): F
protected abstract fun getModules(): List<Module>
fun launch() {
launchActivity(Intent())
}
}
#VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun <F : Fragment> createRule(fragment: F, vararg module: Module): FragmentTestRule<F> =
object : FragmentTestRule<F>() {
override fun createFragment(): F = fragment
override fun getModules(): List<Module> = module.toList()
}
My test App:
#VisibleForTesting(otherwise = VisibleForTesting.NONE)
class VideoWorldTestApp: Application(){
companion object {
lateinit var instance: VideoWorldTestApp
}
override fun onCreate() {
super.onCreate()
instance = this
startKoin {
if (BuildConfig.DEBUG) androidLogger(Level.DEBUG) else EmptyLogger()
androidContext(this#VideoWorldTestApp)
modules(emptyList())
}
Timber.plant(Timber.DebugTree())
}
internal fun injectModules(modules: List<Module>) {
loadKoinModules(modules)
}
}
The custom test runner:
class CustomTestRunner: AndroidJUnitRunner() {
override fun newApplication(
cl: ClassLoader?,
className: String?,
context: Context?
): Application {
return super.newApplication(cl, VideoWorldTestApp::class.java.name, context)
}
}
The test:
#RunWith(AndroidJUnit4ClassRunner::class)
class HomeFragmentTest {
private val twitchViewModel: TwitchViewModel = mockk(relaxed = true)
private val userData = MutableLiveData<UserDataResponse>()
private val fragment = HomeFragment()
#get:Rule
var fragmentRule = createRule(fragment, module {
single(override = true) {
twitchViewModel
}
})
#get:Rule
var countingTaskExecutorRule = CountingTaskExecutorRule()
#Before
fun setup() {
val userResponse: UserResponse = mockk()
every { userResponse.displayName } returns "Rubius"
every { userResponse.profileImageUrl } returns ""
every { userResponse.description } returns "Soy streamer"
every { userResponse.viewCount } returns 5000
every { twitchViewModel.userData } returns userData as LiveData<UserDataResponse>
every { twitchViewModel.getUserByInput(any()) }.answers {
userData.value = UserDataResponse(listOf(userResponse))
}
}
#Test //This one is passing
fun testInitialViewState() {
onScreen<HomeScreen> {
streamerNameTv.containsText("")
streamerCardContainer.isVisible()
nameInput.hasEmptyText()
progressBar.isGone()
}
}
#Test //This one is failing
fun whenWritingAName_AndPressingTheImeAction_AssertTextChanges() {
onScreen<HomeScreen> {
nameInput.typeText("Rubius")
//nameInput.pressImeAction()
searchBtn.click()
verify { twitchViewModel.getUserByInput(any()) } //This passes
countingTaskExecutorRule.drainTasks(5, TimeUnit.SECONDS)
streamerNameTv.hasText("Rubius") //Throws exception
streamerDescp.hasText("Soy streamer")
streamerCount.hasText("Views: ${5000.formatInt()}}")
}
}
}
The fragment being tested:
class HomeFragment : BaseFragment<FragmentHomeBinding>(R.layout.fragment_home) {
override val bindingFunction: (view: View) -> FragmentHomeBinding
get() = FragmentHomeBinding::bind
val twitchViewModel: TwitchViewModel by sharedViewModel()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
twitchViewModel.getUserClips("")
binding.nameInput.setOnEditorActionListener { _, actionId, _ ->
if(actionId == EditorInfo.IME_ACTION_SEARCH) {
twitchViewModel.getUserByInput(binding.nameInput.text.toString())
hideKeyboard()
return#setOnEditorActionListener true
}
return#setOnEditorActionListener false
}
binding.searchBtn.setOnClickListener {
twitchViewModel.getUserByInput(binding.nameInput.text.toString() ?: "")
hideKeyboard()
}
twitchViewModel.userData.observe(viewLifecycleOwner, Observer { data ->
if (data != null && data.dataList.isNotEmpty()){
binding.streamerCard.setOnClickListener {
findNavController().navigate(R.id.action_homeFragment_to_clipsFragment)
}
val streamer = data.dataList[0]
Picasso.get()
.load(streamer.profileImageUrl)
.into(binding.profileIv)
binding.streamerLoginTv.text = streamer.displayName
binding.streamerDescpTv.text = streamer.description
binding.streamerViewCountTv.text = "Views: ${streamer.viewCount.formatInt()}"
}
else {
binding.streamerCard.setOnClickListener { }
}
})
twitchViewModel.errorMessage.observe(viewLifecycleOwner, Observer { msg ->
showSnackbar(msg)
})
twitchViewModel.progressVisibility.observe(viewLifecycleOwner, Observer { visibility ->
binding.progressBar.visibility = visibility
binding.cardContent.visibility =
if(visibility == View.VISIBLE)
View.GONE
else
View.VISIBLE
})
}
}
The ViewModel:
class TwitchViewModel(private val repository: TwitchRepository): BaseViewModel() {
private val _userData = MutableLiveData<UserDataResponse>()
val userData = _userData as LiveData<UserDataResponse>
private val _userClips = MutableLiveData<UserClipsResponse?>()
val userClips = _userClips as LiveData<UserClipsResponse?>
init {
viewModelScope.launch {
repository.authUser(this#TwitchViewModel)
}
}
fun currentUserId() = userData.value?.dataList?.get(0)?.id ?: ""
fun clipsListExists() = userClips.value != null
fun getUserByInput(input: String){
viewModelScope.launch {
_progressVisibility.value = View.VISIBLE
_userData.value = repository.getUserByName(input, this#TwitchViewModel)
_progressVisibility.value = View.GONE
}
}
/**
* #param userId The ID of the Streamer whose clips are gonna fetch. If null, resets
* If empty, sets the [userClips] value to null.
*/
fun getUserClips(userId: String){
if(userId.isEmpty()) {
_userClips.postValue(null)
return
}
if(userId == currentUserId() && _userClips.value != null) {
_userClips.postValue(_userClips.value)
return
}
viewModelScope.launch {
_userClips.value = repository.getUserClips(userId, this#TwitchViewModel)
}
}
}
When running the test with the normal ActivityRule and launching the Activity as it were a normal launch, the observers are triggering successfully.
I'm using a relaxed mock to avoid having to mock all functions and variables.
Finally found the problem and the solution with the debugger. Apparently, the #Before function call runs after the ViewModel is injected into the fragment, so even if the variables pointed to the same reference, mocked answer where executing only in the test context, not in the android context.
I changed the ViewModel initialization to the module scope like this:
#get:Rule
val fragmentRule = createRule(fragment, module {
single(override = true) {
makeMocks()
val twitchViewModel = mockViewModel()
twitchViewModel
}
})
private fun makeMocks() {
mockkStatic(Picasso::class)
}
private fun mockViewModel(): TwitchViewModel {
val userData = MutableLiveData<UserDataResponse>()
val twitchViewModel = mockk<TwitchViewModel>(relaxed = true)
every { twitchViewModel.userData } returns userData
every { twitchViewModel.getUserByInput("Rubius") }.answers {
updateUserDataLiveData(userData)
}
return twitchViewModel
}
And the Observer inside the Fragment got called!
Maybe it's not related, but I could not rebuild the gradle project if I have mockk(v1.10.0) as a testImplementation and as a debugImplementation.

Kotlin Interface can't be instantiated! Interface name: kotlinx.coroutines.Deferred

Im working on an Android App that is using MVVM with Koin and Retrofit.
Suddenly i keep receiving an error saying
Interface can't be instantiated! Interface name:
kotlinx.coroutines.Deferred
however i cannot find where's the problem.
The current structure is this.
ViewModel.
class ExchangeRateBanksViewModel (private val comercialBanksRepository: ComercialBanksRepository): ViewModel() , KoinComponent{
private val _uiState = MutableLiveData<CredentialsDataState>()
val uiState: LiveData<CredentialsDataState> get() = _uiState
init {
viewModelScope.launch {
runCatching {
emitUiState(showProgress = true)
comercialBanksRepository.getExchangeRateByToday()
}.onSuccess {root ->
val nameMap: MutableList<ExchangeRate> = root.data.map { it }.toMutableList()
emitUiState(result = Event(nameMap))
}.onFailure {
println(it.printStackTrace())
}
}
}
private fun emitUiState(showProgress: Boolean = false, result: Event<List<ExchangeRate>>? = null, error: Event<Int>? = null){
val dataState = CredentialsDataState(showProgress, result, error)
_uiState.value = dataState
}
data class CredentialsDataState(val showProgress: Boolean, val result: Event<List<ExchangeRate>>?, val error: Event<Int>?)
}
the interface
interface ComercialBanksInterface {
#GET("api/commercialBankExchangeRates/date/today")
suspend fun requestExchangeRateToday(): Deferred<RootExchangeRate>
}
the repository.
interface ComercialBanksRepository {
suspend fun getExchangeRateByToday(): RootExchangeRate
}
class ComercialBanksRepositoryImplement(val comercialBanksService: ComercialBanksInterface): ComercialBanksRepository{
override suspend fun getExchangeRateByToday(): RootExchangeRate {
return comercialBanksService.requestExchangeRateToday().await()
}
}
and finally the section where i call my viewModel.
exchangeRateBanksViewModel.uiState.observe(this, Observer {
val dataState = it ?: return#Observer
if (!dataState.showProgress) displayHideSkeleton(hide = true) else displayHideSkeleton(hide = false)
if (dataState.result != null && !dataState.result.consumed){
dataState.result.consume()?.let { result ->
println("RESULT: ${result}")
exchangeRateAdapter.submitList(result)
}
}
if (dataState.error != null && !dataState.error.consumed){
dataState.error.consume()?.let { error ->
Toast.makeText(applicationContext, resources.getString(error), Toast.LENGTH_LONG).show()
}
}
})
I can't find which part the interface has the problem...
Retrofit already handles suspend functions so you don't need to return Deferred. Just return RootExchangeRate and that's it.
Also your repository's function should also be suspendable, and the view model should return a LiveData, where the activity (or fragment) will observe it.

Problem in using viewModelScope with LiveData

I am using viewModelScope in the ViewModel which calls a suspend function in the repository as shown below:
ViewModel
class DeepFilterViewModel(val repo: DeepFilterRepository) : ViewModel() {
var deepFilterLiveData: LiveData<Result>? = null
fun onImageCompressed(compressedImage: File): LiveData<Result>? {
if (deepFilterLiveData == null) {
viewModelScope.launch {
deepFilterLiveData = repo.applyFilter(compressedImage)
}
}
return deepFilterLiveData
}
}
Repository
class DeepFilterRepository {
suspend fun applyFilter(compressedImage: File): LiveData<Result> {
val mutableLiveData = MutableLiveData<Result>()
mutableLiveData.value = Result.Loading
withContext(Dispatchers.IO) {
mutableLiveData.value = Result.Success("Done")
}
return mutableLiveData
}
}
I am observing the LiveData from the Fragment as shown below:
viewModel.onImageCompressed(compressedImage)?.observe(this, Observer { result ->
when (result) {
is Result.Loading -> {
loader.makeVisible()
}
is Result.Success<*> -> {
// Process result
}
}
})
The problem is I am getting no value from the LiveData. If I don't use viewModelScope.launch {} as shown below, then everything works fine.
class DeepFilterViewModel(val repo: DeepFilterRepository) : ViewModel() {
var deepFilterLiveData: LiveData<Result>? = null
fun onImageCompressed(compressedImage: File): LiveData<Result>? {
if (deepFilterLiveData == null) {
deepFilterLiveData = repo.applyFilter(compressedImage)
}
return deepFilterLiveData
}
}
I don't know what I am missing. Any help will be appreciated.
This code:
viewModelScope.launch {
deepFilterLiveData = repo.applyFilter(compressedImage)
}
returns immediately so when you first invoke the onImageCompressed() method you return null as deepFilterLiveData. Because in your UI you use ?. on the null return value of onImageCompressed() the when clause will not be reached. The code without the coroutine works because in that case you have sequential code, your ViewModel awaits for the repository call.
To solve this you could keep the LiveData for the ViewModel-UI interaction and return the values directly from the repository method:
class DeepFilterRepository {
suspend fun applyFilter(compressedImage: File) = withContext(Dispatchers.IO) {
Result.Success("Done")
}
}
And the ViewModel:
class DeepFilterViewModel(val repo: DeepFilterRepository) : ViewModel() {
private val _backingLiveData = MutableLiveData<Result>()
val deepFilterLiveData: LiveData<Result>
get() = _backingLiveData
fun onImageCompressed(compressedImage: File) {
// you could also set Loading as the initial state for _backingLiveData.value
_backingLiveData.value = Result.Loading
viewModelScope.launch {
_backingLiveData.value = repo.applyFilter(compressedImage)
}
}
}

Categories

Resources