UseCases or Interactors with Kt Flow and Retrofit - android

Context
I started working on a new project and I've decided to move from RxJava to Kotlin Coroutines. I'm using an MVVM clean architecture, meaning that my ViewModels communicate to UseCases classes, and these UseCases classes use one or many Repositories to fetch data from network.
Let me give you an example. Let's say we have a screen that is supposed to show the user profile information. So we have the UserProfileViewModel:
#HiltViewModel
class UserProfileViewModel #Inject constructor(
private val getUserProfileUseCase: GetUserProfileUseCase
) : ViewModel() {
sealed class State {
data SuccessfullyFetchedUser(
user: ExampleUser
) : State()
}
// ...
val state = SingleLiveEvent<UserProfileViewModel.State>()
// ...
fun fetchUserProfile() {
viewModelScope.launch {
// ⚠️ We trigger the use case to fetch the user profile info
getUserProfileUseCase()
.collect {
when (it) {
is GetUserProfileUseCase.Result.UserProfileFetched -> {
state.postValue(State.SuccessfullyFetchedUser(it.user))
}
is GetUserProfileUseCase.Result.ErrorFetchingUserProfile -> {
// ...
}
}
}
}
}
}
The GetUserProfileUseCase use case would look like this:
interface GetUserProfileUseCase {
sealed class Result {
object ErrorFetchingUserProfile : Result()
data class UserProfileFetched(
val user: ExampleUser
) : Result()
}
suspend operator fun invoke(email: String): Flow<Result>
}
class GetUserProfileUseCaseImpl(
private val userRepository: UserRepository
) : GetUserProfileUseCase {
override suspend fun invoke(email: String): Flow<GetUserProfileUseCase.Result> {
// ⚠️ Hit the repository to fetch the info. Notice that if we have more
// complex scenarios, we might require zipping repository calls together, or
// flatmap responses.
return userRepository.getUserProfile().flatMapMerge {
when (it) {
is ResultData.Success -> {
flow { emit(GetUserProfileUseCase.Result.UserProfileFetched(it.data.toUserExampleModel())) }
}
is ResultData.Error -> {
flow { emit(GetUserProfileUseCase.Result.ErrorFetchingUserProfile) }
}
}
}
}
}
The UserRepository repository would look like this:
interface UserRepository {
fun getUserProfile(): Flow<ResultData<ApiUserProfileResponse>>
}
class UserRepositoryImpl(
private val retrofitApi: RetrofitApi
) : UserRepository {
override fun getUserProfile(): Flow<ResultData<ApiUserProfileResponse>> {
return flow {
val response = retrofitApi.getUserProfileFromApi()
if (response.isSuccessful) {
emit(ResultData.Success(response.body()!!))
} else {
emit(ResultData.Error(RetrofitNetworkError(response.code())))
}
}
}
}
And finally, the RetrofitApi and the response class to model the backend API response would look like this:
data class ApiUserProfileResponse(
#SerializedName("user_name") val userName: String
// ...
)
interface RetrofitApi {
#GET("api/user/profile")
suspend fun getUserProfileFromApi(): Response<ApiUserProfileResponse>
}
Everything has been working fine so far, but I've started to run into some issues when implementing more complex features.
For example, there's a use case where I need to (1) post to a POST /send_email_link endpoint when the user first signs in, this endpoint will check if the email that I send in the body already exists, if it doesn't it will return a 404 error code, and (2) if everything goes okay, I'm supposed to hit a POST /peek endpoint that will return some info about the user account.
This is what I've implemented so far for this UserAccountVerificationUseCase:
interface UserAccountVerificationUseCase {
sealed class Result {
object ErrorVerifyingUserEmail : Result()
object ErrorEmailDoesNotExist : Result()
data class UserEmailVerifiedSuccessfully(
val canSignIn: Boolean
) : Result()
}
suspend operator fun invoke(email: String): Flow<Result>
}
class UserAccountVerificationUseCaseImpl(
private val userRepository: UserRepository
) : UserAccountVerificationUseCase {
override suspend fun invoke(email: String): Flow<UserAccountVerificationUseCase.Result> {
return userRepository.postSendEmailLink().flatMapMerge {
when (it) {
is ResultData.Success -> {
userRepository.postPeek().flatMapMerge {
when (it) {
is ResultData.Success -> {
val canSignIn = it.data?.userName == "Something"
flow { emit(UserAccountVerificationUseCase.Result.UserEmailVerifiedSuccessfully(canSignIn)) }
} else {
flow { emit(UserAccountVerificationUseCase.Result.ErrorVerifyingUserEmail) }
}
}
}
}
is ResultData.Error -> {
if (it.exception is RetrofitNetworkError) {
if (it.exception.errorCode == 404) {
flow { emit(UserAccountVerificationUseCase.Result.ErrorEmailDoesNotExist) }
} else {
flow { emit(UserAccountVerificationUseCase.Result.ErrorVerifyingUserEmail) }
}
} else {
flow { emit(UserAccountVerificationUseCase.Result.ErrorVerifyingUserEmail) }
}
}
}
}
}
}
Issue
The above solution is working as expected, if the first API call to the POST /send_email_link ever returns a 404, the use case will behave as expected and return the ErrorEmailDoesNotExist response so the ViewModel can pass that back to the UI and show the expected UX.
The problem as you can see is that this solution requires a ton of boilerplate code, I thought using Kotlin Coroutines would make things simpler than with RxJava, but it hasn't turned out like that yet. I'm quite sure that this is because I'm missing something or I haven't quite learned how to use Flow properly.
What I've tried so far
I've tried to change the way I emit the elements from the repositories, from this:
...
override fun getUserProfile(): Flow<ResultData<ApiUserProfileResponse>> {
return flow {
val response = retrofitApi.getUserProfileFromApi()
if (response.isSuccessful) {
emit(ResultData.Success(response.body()!!))
} else {
emit(ResultData.Error(RetrofitNetworkError(response.code())))
}
}
}
...
To something like this:
...
override fun getUserProfile(): Flow<ResultData<ApiUserProfileResponse>> {
return flow {
val response = retrofitApi.getUserProfileFromApi()
if (response.isSuccessful) {
emit(ResultData.Success(response.body()!!))
} else {
error(RetrofitNetworkError(response.code()))
}
}
}
..
So I can use the catch() function like I'd with RxJava's onErrorResume():
class UserAccountVerificationUseCaseImpl(
private val userRepository: UserRepository
) : UserAccountVerificationUseCase {
override suspend fun invoke(email: String): Flow<UserAccountVerificationUseCase.Result> {
return userRepository.postSendEmailLink()
.catch { e ->
if (e is RetrofitNetworkError) {
if (e.errorCode == 404) {
flow { emit(UserAccountVerificationUseCase.Result.ErrorEmailDoesNotExist) }
} else {
flow { emit(UserAccountVerificationUseCase.Result.ErrorVerifyingUserEmail) }
}
} else {
flow { emit(UserAccountVerificationUseCase.Result.ErrorVerifyingUserEmail) }
}
}
.flatMapMerge {
userRepository.postPeek().flatMapMerge {
when (it) {
is ResultData.Success -> {
val canSignIn = it.data?.userName == "Something"
flow { emit(UserAccountVerificationUseCase.Result.UserEmailVerifiedSuccessfully(canSignIn)) }
} else -> {
flow { emit(UserAccountVerificationUseCase.Result.ErrorVerifyingUserEmail) }
}
}
}
}
}
}
}
This does reduce the boilerplate code a bit, but I haven't been able to get it working because as soon as I try to run the use case like this I start getting errors saying that I shouldn't emit items in the catch().
Even if I could get this working, still, there's way too much boilerplate code here. I though doing things like this with Kotlin Coroutines would mean having much more simple, and readable, use cases. Something like:
...
class UserAccountVerificationUseCaseImpl(
private val userRepository: AuthRepository
) : UserAccountVerificationUseCase {
override suspend fun invoke(email: String): Flow<UserAccountVerificationUseCase.Result> {
return flow {
coroutineScope {
val sendLinksResponse = userRepository.postSendEmailLink()
if (sendLinksResponse is ResultData.Success) {
val peekAccount = userRepository.postPeek()
if (peekAccount is ResultData.Success) {
emit(UserAccountVerificationUseCase.Result.UserEmailVerifiedSuccessfully())
} else {
emit(UserAccountVerificationUseCase.Result.ErrorVerifyingUserEmail)
}
} else {
if (sendLinksResponse is ResultData.Error) {
if (sendLinksResponse.error == 404) {
emit(UserAccountVerificationUseCase.Result.ErrorEmailDoesNotExist)
} else {
emit(UserAccountVerificationUseCase.Result.ErrorVerifyingUserEmail)
}
} else {
emit(UserAccountVerificationUseCase.Result.ErrorVerifyingUserEmail)
}
}
}
}
}
}
...
This is what I had pictured about working with Kotlin Coroutines. Ditching RxJava's zip(), contact(), delayError(), onErrorResume() and all those Observable functions in favor of something more readable.
Question
How can I reduce the amount of boilerplate code and make my use cases look more Coroutine-like?
Notes
I know some people just call the repositories directly from the ViewModel layer, but I like having this UseCase layer in the middle so I can contain all the code related to switching streams and handling errors here.
Any feedback is appreciated! Thanks!
Edit #1
Based on #Joffrey response, I've changed the code so it works like this:
The Retrofit API layer keeps returning suspendable function.
data class ApiUserProfileResponse(
#SerializedName("user_name") val userName: String
// ...
)
interface RetrofitApi {
#GET("api/user/profile")
suspend fun getUserProfileFromApi(): Response<ApiUserProfileResponse>
}
The repository now returns a suspendable function and I've removed the Flow wrapper:
interface UserRepository {
suspend fun getUserProfile(): ResultData<ApiUserProfileResponse>
}
class UserRepositoryImpl(
private val retrofitApi: RetrofitApi
) : UserRepository {
override suspend fun getUserProfile(): ResultData<ApiUserProfileResponse> {
val response = retrofitApi.getUserProfileFromApi()
return if (response.isSuccessful) {
ResultData.Success(response.body()!!)
} else {
ResultData.Error(RetrofitNetworkError(response.code()))
}
}
}
The use case keeps returning a Flow since I might also plug calls to a Room DB here:
interface GetUserProfileUseCase {
sealed class Result {
object ErrorFetchingUserProfile : Result()
data class UserProfileFetched(
val user: ExampleUser
) : Result()
}
suspend operator fun invoke(email: String): Flow<Result>
}
class GetUserProfileUseCaseImpl(
private val userRepository: UserRepository
) : GetUserProfileUseCase {
override suspend fun invoke(email: String): Flow<GetUserProfileUseCase.Result> {
return flow {
val userProfileResponse = userRepository.getUserProfile()
when (userProfileResponse) {
is ResultData.Success -> {
emit(GetUserProfileUseCase.Result.UserProfileFetched(it.toUserModel()))
}
is ResultData.Error -> {
emit(GetUserProfileUseCase.Result.ErrorFetchingUserProfile)
}
}
}
}
}
This looks much more clean. Now, applying the same thing to the UserAccountVerificationUseCase:
interface UserAccountVerificationUseCase {
sealed class Result {
object ErrorVerifyingUserEmail : Result()
object ErrorEmailDoesNotExist : Result()
data class UserEmailVerifiedSuccessfully(
val canSignIn: Boolean
) : Result()
}
suspend operator fun invoke(email: String): Flow<Result>
}
class UserAccountVerificationUseCaseImpl(
private val userRepository: UserRepository
) : UserAccountVerificationUseCase {
override suspend fun invoke(email: String): Flow<UserAccountVerificationUseCase.Result> {
return flow {
val sendEmailLinkResponse = userRepository.postSendEmailLink()
when (sendEmailLinkResponse) {
is ResultData.Success -> {
val peekResponse = userRepository.postPeek()
when (peekResponse) {
is ResultData.Success -> {
val canSignIn = peekResponse.data?.userName == "Something"
emit(UserAccountVerificationUseCase.Result.UserEmailVerifiedSuccessfully(canSignIn)
}
else -> {
emit(UserAccountVerificationUseCase.Result.ErrorVerifyingUserEmail)
}
}
}
is ResultData.Error -> {
if (sendEmailLinkResponse.isNetworkError(404)) {
emit(UserAccountVerificationUseCase.Result.ErrorEmailDoesNotExist)
} else {
emit(UserAccountVerificationUseCase.Result.ErrorVerifyingUserEmail)
}
}
}
}
}
}
This looks much more clean and it works perfectly. I still wonder if there's any more room for improvement here.

The most obvious problem I see here is that you're using Flow for single values instead of suspend functions.
Coroutines makes the single-value use case much simpler by using suspend functions that return plain values or throw exceptions. You can of course also make them return Result-like classes to encapsulate errors instead of actually using exceptions, but the important part is that with suspend functions you are exposing a seemingly synchronous (thus convenient) API while still benefitting from asynchronous runtime.
In the provided examples you're not subscribing for updates anywhere, all flows actually just give a single element and complete, so there is no real reason to use flows and it complicates the code. It also makes it harder to read for people used to coroutines because it looks like multiple values are coming, and potentially collect being infinite, but it's not the case.
Each time you write flow { emit(x) } it should just be x.
Following the above, you're sometimes using flatMapMerge and in the lambda you create flows with a single element. Unless you're looking for parallelization of the computation, you should simply go for .map { ... } instead. So replace this:
val resultingFlow = sourceFlow.flatMapMerge {
if (something) {
flow { emit(x) }
} else {
flow { emit(y) }
}
}
With this:
val resultingFlow = sourceFlow.map { if (something) x else y }

Related

How to implement loadstate with adapter, recyclerView?

I would like to implement a feature like loadStateFlow in Paging 3.
I do not use pagination in my implementation and it is not necessary in my case.
Could I make it another way?
I have found something like LoadingStateAdapter library
https://developer.android.com/reference/kotlin/androidx/paging/LoadStateAdapter
For now I get a list using method in the fragment:
private fun collectNotificationItems() {
vm.notificationData.collectWith(viewLifecycleOwner) {
notificationAdapter.items = it
}
}
This is implementation I would like to achieve, example is in paging3:
private fun collectItems() {
vm.items.collectWith(viewLifecycleOwner, adapter::submitData)
adapter.loadStateFlow.collectWith(viewLifecycleOwner) { loadState ->
vm.setLoadingState(loadState.refresh is LoadState.Loading)
val isEmpty =
loadState.source.refresh is LoadState.NotLoading && loadState.append.endOfPaginationReached && archiveAdapter.itemCount < 1
vm.setEmptyStateVisible(isEmpty)
}
}
Where methods are:
in ViewModel
fun setLoadingState(isLoading: Boolean) {
_areShimmersVisible.value = isLoading && !_isSwipingToRefresh.value
if (!isLoading) _isSwipingToRefresh.value = false
}
areShimmers and isSwiping are MutableStateFlow
Could you recommend any other options?
EDIT:
I have the whole implementation a little bit different.
I have use case to make it
class GetListItemDetailsUseCase #Inject constructor(private val dao: Dao): BaseFlowUseCase<Unit, List<ItemData>>() {
override fun create(params: Unit): Flow<List<ItemData>> {
return flow{
emit(dao.readAllData())
}
}
}
For now it looks like the code above.
How to use DateState in that case?
EDIT2:
class GetNotificationListItemDetailsUseCase #Inject constructor(private val notificationDao: NotificationDao): BaseFlowUseCase<Unit, DataState<List<NotificationItemsResponse.NotificationItemData>>>() {
override fun create(params: Unit): Flow<DataState<List<NotificationItemsResponse.NotificationItemData>>> {
return flow{
emit(DataState.Loading)
try {
emit(DataState.Success(notificationDao.readAllDataState()))
} catch(e: Exception) {
emit(DataState.Error(e)) // error, and send the exception
}
}
}
}
DAO
#Query("SELECT * FROM notification_list ORDER BY id ASC")
abstract suspend fun readAllDataState(): DataState<List<NotificationItemsResponse.NotificationItemData>>
/\ error beacause of it:
error: Not sure how to convert a Cursor to this method's return type
fragment
private suspend fun collectNotificationItems() {
vm.notificationData.collectLatest { dataState ->
when(dataState) {
is DataState.Error -> {
collectErrorState()
Log.d("collectNotificationItems", "Collect ErrorState")
}
DataState.Loading -> {
Log.d("collectNotificationItems", "Collect Loading")
}
is DataState.Success<*> -> {
vm.notificationData.collectWith(viewLifecycleOwner) {
notificationAdapter.items = it
notificationAdapter.notifyDataSetChanged()
Log.d("collectNotificationItems", "Collect Sucess")
}
}
}
}
You could use a utility class (usually called DataState or something like that).
sealed class DataState<out T> {
data class Success<out T>(val data: T) : DataState<T>()
data class Error(val exception: Exception) : DataState<Nothing>()
object Loading : DataState<Nothing>()
}
Then, you change your flow's return type from Flow<YourObject> to Flow<DataState<YourObject>> and emit the DataStates within a flow {} or channelFlow {} block.
val notificationsFlow: Flow<DataState<YourObject>> get() = flow {
emit(DataState.Loading) // when you collect, you will receive this DataState telling you that it's loading
try {
// networking/database stuff
emit(DataState.Success(yourResultObject))
} catch(e: Exception) {
emit(DataState.Error(e)) // error, and send the exception
}
}
Finally, just change your collect {} to be like:
notificationsFlow.collectLatest { dataState ->
when(dataState) {
is DataState.Error -> { } // error occurred, deal with it here
DataState.Loading -> { } // it's loading, show progress bar or something
is DataState.Success -> { } // data received from the flow, access it with dataState.data
}
}
For more information on this regard, check this out.

Elegant way of handling error using Retrofit + Kotlin Flow

I have a favorite way of doing network request on Android (using Retrofit). It looks like this:
// NetworkApi.kt
interface NetworkApi {
#GET("users")
suspend fun getUsers(): List<User>
}
And in my ViewModel:
// MyViewModel.kt
class MyViewModel(private val networkApi: NetworkApi): ViewModel() {
val usersLiveData = flow {
emit(networkApi.getUsers())
}.asLiveData()
}
Finally, in my Activity/Fragment:
//MyActivity.kt
class MyActivity: AppCompatActivity() {
private viewModel: MyViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel.usersLiveData.observe(this) {
// Update the UI here
}
}
}
The reason I like this way is because it natively works with Kotlin flow, which is very easy to use, and has a lot of useful operations (flatMap, etc).
However, I am not sure how to elegantly handle network errors using this method. One approach that I can think of is to use Response<T> as the return type of the network API, like this:
// NetworkApi.kt
interface NetworkApi {
#GET("users")
suspend fun getUsers(): Response<List<User>>
}
Then in my view model, I can have an if-else to check the isSuccessful of the response, and get the real result using the .body() API if it is successful. But it will be problematic when I do some transformation in my view model. E.g.
// MyViewModel.kt
class MyViewModel(private val networkApi: NetworkApi): ViewModel() {
val usersLiveData = flow {
val response = networkApi.getUsers()
if (response.isSuccessful) {
emit(response.body()) // response.body() will be List<User>
} else {
// What should I do here?
}
}.map { // it: List<User>
// transform Users to some other class
it?.map { oneUser -> OtherClass(oneUser.userName) }
}.asLiveData()
Note the comment "What should I do here?". I don't know what to do in that case. I could wrap the responseBody (in this case, a list of Users) with some "status" (or simply just pass through the response itself). But that means that I pretty much have to use an if-else to check the status at every step through the flow transformation chain, all the way up to the UI. If the chain is really long (e.g. I have 10 map or flatMapConcat on the chain), it is really annoying to do it in every step.
What is the best way to handle network errors in this case, please?
You should have a sealed class to handle for different type of event. For example, Success, Error or Loading. Here is some of the example that fits your usecases.
enum class ApiStatus{
SUCCESS,
ERROR,
LOADING
} // for your case might be simplify to use only sealed class
sealed class ApiResult <out T> (val status: ApiStatus, val data: T?, val message:String?) {
data class Success<out R>(val _data: R?): ApiResult<R>(
status = ApiStatus.SUCCESS,
data = _data,
message = null
)
data class Error(val exception: String): ApiResult<Nothing>(
status = ApiStatus.ERROR,
data = null,
message = exception
)
data class Loading<out R>(val _data: R?, val isLoading: Boolean): ApiResult<R>(
status = ApiStatus.LOADING,
data = _data,
message = null
)
}
Then, in your ViewModel,
class MyViewModel(private val networkApi: NetworkApi): ViewModel() {
// this should be returned as a function, not a variable
val usersLiveData = flow {
emit(ApiResult.Loading(true)) // 1. Loading State
val response = networkApi.getUsers()
if (response.isSuccessful) {
emit(ApiResult.Success(response.body())) // 2. Success State
} else {
val errorMsg = response.errorBody()?.string()
response.errorBody()?.close() // remember to close it after getting the stream of error body
emit(ApiResult.Error(errorMsg)) // 3. Error State
}
}.map { // it: List<User>
// transform Users to some other class
it?.map { oneUser -> OtherClass(oneUser.userName) }
}.asLiveData()
In your view (Activity/Fragment), observe these state.
viewModel.usersLiveData.observe(this) { result ->
// Update the UI here
when(result.status) {
ApiResult.Success -> {
val data = result.data <-- return List<User>
}
ApiResult.Error -> {
val errorMsg = result.message <-- return errorBody().string()
}
ApiResult.Loading -> {
// here will actually set the state as Loading
// you may put your loading indicator here.
}
}
}
//this class represent load statement management operation
/*
What is a sealed class
A sealed class is an abstract class with a restricted class hierarchy.
Classes that inherit from it have to be in the same file as the sealed class.
This provides more control over the inheritance. They are restricted but also allow freedom in state representation.
Sealed classes can nest data classes, classes, objects, and also other sealed classes.
The autocomplete feature shines when dealing with other sealed classes.
This is because the IDE can detect the branches within these classes.
*/
ٍٍٍٍٍ
sealed class APIResponse<out T>{
class Success<T>(response: Response<T>): APIResponse<T>() {
val data = response.body()
}
class Failure<T>(response: Response<T>): APIResponse<T>() {
val message:String = response.errorBody().toString()
}
class Exception<T>(throwable: Throwable): APIResponse<T>() {
val message:String? = throwable.localizedMessage
}
}
create extention file called APIResponsrEX.kt
and create extextion method
fun <T> APIResponse<T>.onSuccess(onResult :APIResponse.Success<T>.() -> Unit) : APIResponse<T>{
if (this is APIResponse.Success) onResult(this)
return this
}
fun <T> APIResponse<T>.onFailure(onResult: APIResponse.Failure<*>.() -> Unit) : APIResponse<T>{
if (this is APIResponse.Failure<*>)
onResult(this)
return this
}
fun <T> APIResponse<T>.onException(onResult: APIResponse.Exception<*>.() -> Unit) : APIResponse<T>{
if (this is APIResponse.Exception<*>) onResult(this)
return this
}
merge it with Retrofit
inline fun <T> Call<T>.request(crossinline onResult: (response: APIResponse<T>) -> Unit) {
enqueue(object : retrofit2.Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
if (response.isSuccessful) {
// success
onResult(APIResponse.Success(response))
} else {
//failure
onResult(APIResponse.Failure(response))
}
}
override fun onFailure(call: Call<T>, throwable: Throwable) {
onResult(APIResponse.Exception(throwable))
}
})
}

Flow exception transparency is violated : Emission from another coroutine is detected

I'm tring to implement NetworkBoundResource class in my project and this is what i'm trying. Everything work correctly getting response, caching but when im emiting value inside flowBuilder then it crashes and showing this error.
error i'm getting:
Emission from another coroutine is detected.
Child of ProducerCoroutine{Active}#df26eb9, expected child of FlowCoroutine{Active}#a0bb2fe.
FlowCollector is not thread-safe and concurrent emissions are prohibited.
To mitigate this restriction please use 'channelFlow' builder instead of 'flow')' has been detected.
Emissions from 'catch' blocks are prohibited in order to avoid unspecified behaviour, 'Flow.catch' operator can be used instead.
For a more detailed explanation, please refer to Flow documentation.
NetworkBoundResource class:
abstract class NetworkBoundResource<ResultType, RequestType> {
fun invoke(): Flow<Resource<ResultType>> = flow {
val rawData = loadFromDb()
if (shouldFetch(rawData)) {
fetchDataFromServer()
.onStart { emit(Resource.loading(rawData)) } // emit() causing issue
.catch { emit(Resource.error(it, null)) } // emit() causing issue
.collectLatest { }
}
}
// Save API response result into the database
protected abstract suspend fun cacheInDb(items: RequestType)
// Need to fetch data from server or not.
protected abstract fun shouldFetch(data: ResultType?): Boolean
// Show cached data from the database.
protected abstract suspend fun loadFromDb(): ResultType
// Fetch the data from server.
protected abstract suspend fun fetchDataFromServer(): Flow<ApiResponse<List<Category>>>
// when the fetch fails.
protected open fun onFetchFailed() {}
}
Repository class:
fun getCategories(): Flow<Resource<List<Category>>> {
return object : NetworkBoundResource<List<Category>, List<Category>>() {
override suspend fun cacheInDb(items: List<Category>) {
withContext(Dispatchers.IO) { database.getCategories().insert(items) }
}
override fun shouldFetch(data: List<Category>?): Boolean {
return true
}
override suspend fun loadFromDb(): List<Category> {
return withContext(Dispatchers.IO) { database.getCategories().read() }
}
override suspend fun fetchDataFromServer(): Flow<ApiResponse<List<Category>>> {
return flow { emit(RetrofitModule.getCategories()) }
}
}.invoke()
}
myViewModelClass:
init {
viewModelScope.launch {
repository.getCategories().collectLatest {
if(it.data!=null){
_categories.value = it.data
Log.d("appDebug", " ViewModel : $it")
}
}
}
}
As the exception says, cold flows doesn't allow to emit() concurrently.
You have two options:
Replace flow { } with channelFlow { } and send values with send() (Probably easier in your case)
Make sure no emit() is called concurrently

using countDownLatch.await() to make sure result is delivered

Full source code can be found here : https://github.com/alirezaeiii/SavingGoals-Cache
This is LocalDataSource class :
#Singleton
class QapitalLocalDataSource #Inject constructor(
private val goalsDao: GoalsDao
) : LocalDataSource {
override fun getSavingsGoals(): Single<List<SavingsGoal>> =
Single.create { singleSubscriber ->
goalsDao.getGoals()
.subscribe {
if (it.isEmpty()) {
singleSubscriber.onError(NoDataException())
} else {
singleSubscriber.onSuccess(it)
}
}
}
}
Above Method has been used in Repository class :
#Singleton
class GoalsRepository #Inject constructor(
private val remoteDataSource: QapitalService,
private val localDataSource: LocalDataSource,
private val schedulerProvider: BaseSchedulerProvider
) {
private var cacheIsDirty = false
fun getSavingsGoals(): Observable<List<SavingsGoal>> {
lateinit var goals: Observable<List<SavingsGoal>>
if (cacheIsDirty) {
goals = getGoalsFromRemoteDataSource()
} else {
val latch = CountDownLatch(1)
var disposable: Disposable? = null
disposable = localDataSource.getSavingsGoals()
.observeOn(schedulerProvider.io())
.doFinally {
latch.countDown()
disposable?.dispose()
}.subscribe({
goals = Observable.create { emitter -> emitter.onNext(it) }
}, { goals = getGoalsFromRemoteDataSource() })
latch.await()
}
return goals
}
}
As you see I am using countDownLatch.await() to make sure result is emmited in subscribe or error block. Is there any better solution than using CountDownLatch while using RxJava?
latch.await() blocks the thread which kinda defeats the whole point of using an async API like RxJava.
RxJava has APIs like onErrorResumeNext to handle exceptions and toObservable to convert a Single result to an Observable result.
Also, RxJava types like this are typically intended to be cold (they don't run or figure anything out until you subscribe) so I'd recommend not checking cacheIsDirty until the subscription happens.
I'd go with something like:
fun getSavingsGoals(): Observable<List<SavingsGoal>> {
return Observable
.fromCallable { cacheIsDirty }
.flatMap {
if (it) {
getGoalsFromRemoteDataSource()
} else {
localDataSource.getSavingsGoals()
.toObservable()
.onErrorResumeNext(getGoalsFromRemoteDataSource())
}
}
}
Btw, if you are already using Kotlin, I highly recommend coroutines. Then you async code ends up reading just like regular sequential code.

Sharing same MutableLiveData between Repository and ViewModel

I'm in the process of wrapping my head around Architecture Components / MVVM.
Let's say I have a repository, a ViewModel and a Fragment. I'm using a Resource class as a wrapper to expose network status, like suggested in the Guide to architecture components.
My repository currently looks something like this (simplified for brevity):
class MyRepository {
fun getLists(organizationId: String) {
var data = MutableLiveData<Resource<List<Something>>>()
data.value = Resource.loading()
ApolloClient().query(query)
.enqueue(object : ApolloCall.Callback<Data>() {
override fun onResponse(response: Response<Data>) {
response.data()?.let {
data.postValue(Resource.success(it))
}
}
override fun onFailure(exception: ApolloException) {
data.postValue(Resource.exception(exception))
}
})
}
Then in the ViewModel, I also declare a MutableLiveData:
var myLiveData = MutableLiveData<Resource<List<Something>>>()
fun getLists(organizationId: String, forceRefresh: Boolean = false) {
myLiveData = myRepository.getLists(organizationId)
}
Finally, the Fragment:
viewModel.getLists.observe(this, Observer {
it?.let {
if (it.status.isLoading()) showLoading() else hideLoading()
if (it.status == Status.SUCCESS) {
it.data?.let {
adapter.replaceData(it)
setupViews()
}
}
if (it.status == Status.ERROR) {
// Show error
}
}
})
As you see, there will be an issue with the observer not being triggered, since the LiveData variable will be reset in the process (the Repository creates a new instance).
I'm trying to figure out the best way to make sure that the same LiveData variable is used between the Repository and ViewModel.
I thought about passing the LiveData from the ViewModel to the getLists method, so that the Repository would be using the object from the ViewModel, but even if it works, it seems wrong to do that.
What I mean is something like that:
ViewModel
var myLiveData = MutableLiveData<Resource<List<Something>>>()
fun getLists(organizationId: String, forceRefresh: Boolean = false) {
myRepository.getLists(myLiveData, organizationId)
}
Repository
fun getLists(data: MutableLiveData<Resource<List<Something>>>, organizationId: String) {
...
}
I think I figured out how to do it, thanks to #NSimon for the cue.
My repository stayed the same, and my ViewModel looks like this:
class MyViewModel : ViewModel() {
private val myRepository = MyRepository()
private val organizationIdLiveData = MutableLiveData<String>()
private val lists = Transformations.switchMap(organizationIdLiveData) { organizationId -> myRepository.getLists(organizationId) }
fun getLists() : LiveData<Resource<MutableList<Something>>> {
return lists
}
fun fetchLists(organizationId: String, forceRefresh: Boolean = false) {
if (organizationIdLiveData.value == null || forceRefresh) {
organizationIdLiveData.value = organizationId
}
}
}
I observe getLists() in my fragment, and call viewModel.fetchLists(id) when I want the data. Seems legit?

Categories

Resources