Firebase: How to check if document write was successful - android

I want to check if my database write was successful in order to show the user an error message.
My current approach doesn't work as it says "Type mismatch, required Unit found EmailStatus"
Current approach
class EmailRepositoryImpl : EmailRepository {
private val db = Firebase.firestore
override fun sendEmail(email: Email): EmailStatus<Nothing> {
db.collection("emails").document().set(email).addOnCompleteListener {
if (it.isSuccessful) return#addOnCompleteListener EmailStatus.Success<Nothing>
if (it.isCanceled) return#addOnCompleteListener EmailStatus.Error(it.exception!!)
}
}
}
Status Sealed Class
sealed class EmailStatus<out T> {
data class Success<out T>(val data: T) : EmailStatus<T>()
data class Error(val exception: Exception) : EmailStatus<Nothing>()
}
Is it even possible to write something like this? As far as I know there is a generic firebase error type but I didn't found anything related to kotlin or android...
I appreciate every help, thank you
Edit
I've tried getting my document, but I am just getting null: (When I use the listener approach, everything works fine)
Interface
interface EmailRepository {
suspend fun getEmail(): Flow<EmailEntity?>
}
Interface Implementation
override suspend fun getEmail(): Flow<EmailEntity?> = flow {
val result = db.collection("emailprice").document("Email").get().await()
emit(result.toObject<EmailEntity>())
}
ViewModel
private val emailEntity = liveData<EmailEntity?>(Dispatchers.IO) {
emailRepository.getCalibratePrice()
}

The problem is that addOnCompleteListener callback does not return anything (Unit) and you are trying to return an EmailStatus from that scope.
You have three approaches:
Create an interface that will populate the value and return that EmailStatus down to your caller layer
Use Coroutines to suspend this function when the async call to firebase is done and then return that value
Use Flow to offer the data when it's ready to process
I think the easiest way to do this one shot operation is to use Coroutines; I have written an article about that.

Okay, this is the final solution, thanks to #Gastón Saillén and #Doug Stevenson :
EmailRepository
interface EmailRepository {
fun sendEmail(email: Email): Flow<EmailStatus<Unit>>
}
EmailRepository Implementation
class EmailRepositoryImpl #Inject constructor(
private val db: FirebaseFirestore
) : EmailRepository {
override fun sendEmail(email: Email)= flow<EmailStatus<Unit>> {
db.collection("emails").add(email).await()
emit(EmailStatus.success(Unit))
}.catch {
emit(EmailStatus.failed(it.message.toString()))
}.flowOn(Dispatchers.Main)
}
ViewModel
fun sendEmail(): LiveData<EmailStatus<Unit>> {
val newEmail = createEmail()
return emailRepository.sendEmail(newEmail).asLiveData()
}
Fragment
btn.setOnClickListener {
viewModel.sendEmail().observe(viewLifecycleOwner) {
when(it) {
is EmailStatus.Success -> {
valid = true
navigateTo(next, bundleNext)
Toast.makeText(requireContext(), "Success", Toast.LENGTH_SHORT).show()
}
is EmailStatus.Failure -> {
valid = false
Toast.makeText(requireContext(), "Failed ${it.message}", Toast.LENGTH_SHORT).show()
}
}
}
}
The only problem I currently have is that my "faield state" does not work like it should.
It should fail, if the user has no internet access. Currently, the write to the db never fails and Firebase just waits till the user has internet access. The problem here is that when I click multiple times, the write is executed multiple times. But I think I have to implement a bit more logic here and the above written code is fine like it currently is.

Related

How to use collectAsState() when getting data from Firestore?

A have a screen where I display 10 users. Each user is represented by a document in Firestore. On user click, I need to get its details. This is what I have tried:
fun getUserDetails(uid: String) {
LaunchedEffect(uid) {
viewModel.getUser(uid)
}
when(val userResult = viewModel.userResult) {
is Result.Loading -> CircularProgressIndicator()
is Result.Success -> Log.d("TAG", "You requested ${userResult.data.name}")
is Result.Failure -> Log.d("TAG", userResult.e.message)
}
}
Inside the ViewModel class, I have this code:
var userResult by mutableStateOf<Result<User>>(Result.Loading)
private set
fun getUser(uid: String) = viewModelScope.launch {
repo.getUser(uid).collect { result ->
userResult = result
}
}
As you see, I use Result.Loading as a default value, because the document is heavy, and it takes time to download it. So I decided to display a progress bar. Inside the repo class I do:
override fun getUser(uid: String) = flow {
try {
emit(Result.Loading)
val user = usersRef.document(uid).get().await().toObject(User::class.java)
emit(Result.Success(user))
} catch (e: Exception) {
emit(Result.Failure(e))
}
}
I have two questions, if I may.
Is there something wrong with this code? As it works fine when I compile.
I saw some questions here, that recommend using collectAsState() or .collectAsStateWithLifecycle(). I tried changing userResult.collectAsState() but I cannot find that function. Is there any benefit in using collectAsState() or .collectAsStateWithLifecycle() than in my actual code? I'm really confused.
If you wish to follow Uncle Bob's clean architecture you can split your architecture into Data, Domain and Presentation layers.
For android image below shows how that onion shape can be simplified to
You emit your result from Repository and handle states or change data, if you Domain Driven Model, you store DTOs for data from REST api, if you have db you keep database classes instead of passing classes annotated with REST api annotation or db annotation to UI you pass a UI.
In repository you can pass data as
override fun getUser(uid: String) = flow {
val user usersRef.document(uid).get().await().toObject(User::class.java)
emit(user)
}
In UseCase you check if this returns error, or your User and then convert this to a Result or a class that returns error or success here. You can also change User data do Address for instance if your business logic requires you to return an address.
If you apply business logic inside UseCase you can unit test what you should return if you retrieve data successfully or in case error or any data manipulation happens without error without using anything related to Android. You can just take this java/kotlin class and unit test anywhere not only in Android studio.
In ViewModel after getting a Flow< Result<User>> you can pass this to Composable UI.
Since Compose requires a State to trigger recomposition you can convert your Flow with collectAsState to State and trigger recomposition with required data.
CollectAsState is nothing other than Composable function produceState
#Composable
fun <T : R, R> Flow<T>.collectAsState(
initial: R,
context: CoroutineContext = EmptyCoroutineContext
): State<R> = produceState(initial, this, context) {
if (context == EmptyCoroutineContext) {
collect { value = it }
} else withContext(context) {
collect { value = it }
}
}
And produceState
#Composable
fun <T> produceState(
initialValue: T,
key1: Any?,
key2: Any?,
#BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit
): State<T> {
val result = remember { mutableStateOf(initialValue) }
LaunchedEffect(key1, key2) {
ProduceStateScopeImpl(result, coroutineContext).producer()
}
return result
}
As per discussion in comments, you can try this approach:
// Repository
suspend fun getUser(uid: String): Result<User> {
return try {
val user = usersRef.document(uid).get().await().toObject(User::class.java)
Result.Success(user)
} catch (e: Exception) {
Result.Failure(e)
}
}
// ViewModel
var userResult by mutableStateOf<Result<User>?>(null)
private set
fun getUser(uid: String) {
viewModelScope.launch {
userResult = Result.Loading // set initial Loading state
userResult = repository.getUser(uid) // update the state again on receiving the response
}
}

Saving and deleting data from firestore async [duplicate]

I have created an app with Kotlin and Firebase Firestore. Now I need to implement coroutines as there is so much work on the main thread. But I'm also a beginner so it's something new to me. I've watched some tutorials on this but I didn't find complete tutorials on Firestore with coroutines. So I need some help to implement coroutines in my app In such parts like these (I tried by myself but didn't get it).
Retrieving posts from Firestore.
private fun retrievePosts() {
FirebaseFirestore.getInstance().collection("Posts")
.orderBy("timeStamp", Query.Direction.DESCENDING)
.get()
.addOnSuccessListener { queryDocumentSnapshots ->
postList?.clear()
for (documentSnapshot in queryDocumentSnapshots) {
val post = documentSnapshot.toObject(Post::class.java)
postList?.add(post)
}
postAdapter?.notifyDataSetChanged()
postAdapter?.setOnPostClickListener(this)
if (isRefreshed) {
swipe_refresh_home?.setRefreshing(false)
isRefreshed = false
}
swipe_refresh_home?.visibility = VISIBLE
progress_bar_home?.visibility = GONE
}.addOnFailureListener { e ->
Log.d(TAG, "UserAdapter-retrieveUsers: ", e)
swipe_refresh_home?.visibility = VISIBLE
progress_bar_home?.visibility = GONE
}
}
Getting user data into an adapter
private fun userInfo( fullName: TextView, profileImage: CircleImageView,
about: TextView, uid: String,
userLocation: TextView, itemRoot: LinearLayout ) {
val userRef = FirebaseFirestore.getInstance().collection("Users").document(uid)
userRef.get()
.addOnSuccessListener {
if (it != null && it.exists()) {
val user = it.toObject(User::class.java)
Glide.with(mContext).load(user?.getImage()).placeholder(R.drawable.default_pro_pic).into(profileImage)
fullName.text = user?.getFullName().toString()
about.text = user?.getAbout()
if (user?.getLocation() != ""){
userLocation.visibility = VISIBLE
userLocation.text = user?.getLocation()
}
if (profileImage.drawable == null){
itemRoot.visibility = GONE
}
else{
itemRoot.visibility = VISIBLE
}
}
}
}
And this Save post button in an adapter.
private fun savedPost(postId: String, saveButton: ImageView?) {
FirebaseFirestore.getInstance().collection("Users").document(currentUserID)
.collection("Saved Posts").document(postId)
.get()
.addOnSuccessListener {
if (it.exists()) {
saveButton?.setImageResource(drawable.ic_bookmark)
} else {
saveButton?.setImageResource(drawable.bookmark_post_ic)
}
}
}
As I see your code, you are using the following query:
val queryPostsByTimestamp = FirebaseFirestore.getInstance().collection("Posts")
.orderBy("timeStamp", Query.Direction.DESCENDING)
Most probably to get a list of Post objects from your "Posts" collection.
In order to use Kotlin Coroutines, don't forget to add the following dependencies in the Gradle (app) file:
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.3.9"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1"
I'll provide you a solution using the MVVM architecture pattern. So we'll use a repository class and a ViewModel class. For the asynchronous calls to Firestore, we'll use Flow.
For the response that we get from the database call, we need a sealed class that looks like this:
sealed class Response<out T> {
class Loading<out T>: Response<T>()
data class Success<out T>(
val data: T
): Response<T>()
data class Failure<out T>(
val errorMessage: String
): Response<T>()
}
Assuming that you have a "Post" class, let's create in the repository class the following function:
fun getPostsFromFirestore() = flow {
emit(Loading())
emit(Success(queryPostsByTimestamp.get().await().documents.mapNotNull { doc ->
doc.toObject(Post::class.java)
}))
}. catch { error ->
error.message?.let { errorMessage ->
emit(Failure(errorMessage))
}
}
So we'll emit an object according to the state. When first-time calling the function, we emit a loading state using emit(Loading(), when we get the data we emit the List<Post> and if we get an error, we emit the error message using Failure(errorMessage).
Now we need to call this function, from the ViewModel class:
fun getPosts() = liveData(Dispatchers.IO) {
repository.getPostsFromFirestore().collect { response ->
emit(response)
}
}
With the above function, we collect the data that we get from the getPostsFromFirestore() function call, and we emit the result further as a LiveData object so it can be observed in the activity/fragment like this:
private fun getPosts() {
viewModel.getPosts().observe(this, { response ->
when(response) {
is Loading -> //Load a ProgessBar
is Success -> {
val postList = response.data
//Do what you need to do with your list
//Hide the ProgessBar
}
is Failure -> {
print(response.errorMessage)
//Hide the ProgessBar
}
}
})
}
That's pretty much of it!
I don't know Firebase, so I may miss something, but generally speaking you don't need a special support in the library to use it with coroutines. If you start a background coroutine and then execute your above code in it, then Firebase will probably run within your coroutine without any problems.
The only problematic part could be listeners. Some libs invoke callbacks in the thread that was used to execute them, but some dispatch callbacks to a specific thread. In the case of Firebase it seems by default it runs listeners in the main thread. If this is not what you want, you can pass an executor to run callbacks within coroutines as well, e.g.:
.addOnSuccessListener(Dispatchers.Default.asExecutor()) { ... }

Kotlin Coroutine Flow: When does wasting resource happen when using Flow

I am reading this article to fully understand the dos and donts of using Flow while comparing it to my implementation, but I can't grasp clearly how to tell if you are wasting resource when using Flow or flow builder. When is the time a flow is being release/freed in memory and when is the time that you are wasting resource like accidentally creating multiple instances of flow and not releasing them?
I have a UseCase class that invokes a repository function that returns Flow. In my ViewModel this is how it looks like.
class AssetViewModel constructor(private val getAssetsUseCase: GetAssetsUseCase) : BaseViewModel() {
private var job: Job? = null
private val _assetState = defaultMutableSharedFlow<AssetState>()
fun getAssetState() = _assetState.asSharedFlow()
init {
job = viewModelScope.launch {
while(true) {
if (lifecycleState == LifeCycleState.ON_START || lifecycleState == LifeCycleState.ON_RESUME)
fetchAssets()
delay(10_000)
}
}
}
fun fetchAssets() {
viewModelScope.launch {
withContext(Dispatchers.IO) {
getAssetsUseCase(
AppConfigs.ASSET_BASE_URL,
AppConfigs.ASSET_PARAMS,
AppConfigs.ASSET_SIZES[AppConfigs.ASSET_LIMIT_INDEX]
).onEach {
when(it){
is RequestStatus.Loading -> {
_assetState.tryEmit(AssetState.FetchLoading)
}
is RequestStatus.Success -> {
_assetState.tryEmit(AssetState.FetchSuccess(it.data.assetDataDomain))
}
is RequestStatus.Failed -> {
_assetState.tryEmit(AssetState.FetchFailed(it.message))
}
}
}.collect()
}
}
}
override fun onCleared() {
job?.cancel()
super.onCleared()
}
}
The idea here is we are fetching data from remote every 10 seconds while also allowing on demand fetch of data via UI.
Just a typical useless UseCase class
class GetAssetsUseCase #Inject constructor(
private val repository: AssetsRepository // Passing interface not implementation for fake test
) {
operator fun invoke(baseUrl: String, query: String, limit: String): Flow<RequestStatus<AssetDomain>> {
return repository.fetchAssets(baseUrl, query, limit)
}
}
The concrete implementation of repository
class AssetsRepositoryImpl constructor(
private val service: CryptoService,
private val mapper: AssetDtoMapper
) : AssetsRepository {
override fun fetchAssets(
baseUrl: String,
query: String,
limit: String
) = flow {
try {
emit(RequestStatus.Loading())
val domainModel = mapper.mapToDomainModel(
service.getAssetItems(
baseUrl,
query,
limit
)
)
emit(RequestStatus.Success(domainModel))
} catch (e: HttpException) {
emit(RequestStatus.Failed(e))
} catch (e: IOException) {
emit(RequestStatus.Failed(e))
}
}
}
After reading this article which says that using stateIn or sharedIn will improve the performance when using a flow, it seems that I am creating new instances of the same flow on-demand. But there is a limitation as the stated approach only works for variable and not function that returns Flow.
stateIn and shareIn can save resources if there are multiple observers, by avoiding redundant fetching. And in your case, you could set it up to automatically pause the automatic re-fetching when there are no observers. If, on the UI side you use repeatOnLifecycle, then it will automatically drop your observers when the view is off screen and then you will avoid wasted fetches the user will never see.
I think it’s not often described this way, but often the multiple observers are just observers coming from the same Activity or Fragment class after screen rotations or rapidly switching between fragments. If you use WhileSubscribed with a timeout to account for this, you can avoid having to restart your flow if it’s needed again quickly.
Currently you emit to from an external coroutine instead of using shareIn, so there’s no opportunity to pause execution.
I haven't tried to create something that supports both automatic and manual refetching. Here's a possible strategy, but I haven't tested it.
private val refreshRequest = Channel<Unit>(Channel.CONFLATED)
fun fetchAssets() {
refreshRequest.trySend(Unit)
}
val assetState = flow {
while(true) {
getAssetsUseCase(
AppConfigs.ASSET_BASE_URL,
AppConfigs.ASSET_PARAMS,
AppConfigs.ASSET_SIZES[AppConfigs.ASSET_LIMIT_INDEX]
).map {
when(it){
is RequestStatus.Loading -> AssetState.FetchLoading
is RequestStatus.Success -> AssetState.FetchSuccess(it.data.assetDataDomain)
is RequestStatus.Failed -> AssetState.FetchFailed(it.message)
}
}.emitAll()
withTimeoutOrNull(100L) {
// drop any immediate or pending manual request
refreshRequest.receive()
}
// Wait until a fetch is manually requested or ten seconds pass:
withTimeoutOrNull(10000L - 100L) {
refreshRequest.receive()
}
}
}.shareIn(viewModelScope, SharingStarted.WhileSubscribed(4000L), replay = 1)
To this I would recommend not using flow as the return type of the usecase function and the api call must not be wrapped inside a flow builder.
Why:
The api call actually is happening once and then again after an interval it is triggered by the view model itself, returning flow from the api caller function will be a bad usage of powerful tool that is actually meant to be called once and then it must be self-reliant, it should emit or pump in the data till the moment it has a subscriber/collector.
One usecase you can consider when using flow as return type from the room db query call, it is called only once and then the room emits data into it till the time it has subscriber.
.....
fun fetchAssets() {
viewModelScope.launch {
// loading true
val result=getusecase(.....)
when(result){..process result and emit on state..}
// loading false
}
}
.....
suspend operator fun invoke(....):RequestStatus<AssetDomain>{
repository.fetchAssets(baseUrl, query, limit)
}
.....
override fun fetchAssets(
baseUrl: String,
query: String,
limit: String
):RequestStatus {
try {
//RequestStatus.Loading()//this can be managed in viewmodel itself
val domainModel = mapper.mapToDomainModel(
service.getAssetItems(
baseUrl,
query,
limit
)
)
RequestStatus.Success(domainModel)
} catch (e: HttpException) {
RequestStatus.Failed(e)
} catch (e: IOException) {
RequestStatus.Failed(e)
}
}

Kotlin Multiplatform Mobile: Ktor - how to cancel active coroutine (network request, background work) in Kotlin Native (iOS)?

In my project I write View and ViewModel natively and share Repository, Db, networking.
When user navigates from one screen to another, I want to cancel all network requests or other heavy background operations that are currently running in the first screen.
Example function in Repository class:
#Throws(Throwable::class)
suspend fun fetchData(): List<String>
In Android's ViewModel I can use viewModelScope to automatically cancel all active coroutines. But how to cancel those tasks in iOS app?
Lets suppose that the object session is a URLSession instance, you can cancel it by:
session.invalidateAndCancel()
I didn't find any first party information about this or any good solution, so I came up with my own. Shortly, it will require turning repository suspend functions to regular functions with return type of custom interface that has cancel() member function. Function will take action lambda as parameter. On implementation side, coroutine will be launched and reference for Job will be kept so later when it is required to stop background work interface cancel() function will cancel job.
In addition, because it is very hard to read type of error (in case it happens) from NSError, I wrapped return data with custom class which will hold error message and type. Earlier I asked related question but got no good answer for my case where ViewModel is written natively in each platform.
If you find any problems with this approach or have any ideas please share.
Custom return data wrapper:
class Result<T>(
val status: Status,
val value: T? = null,
val error: KError? = null
)
enum class Status {
SUCCESS, FAIL
}
data class KError(
val type: ErrorType,
val message: String? = null,
)
enum class ErrorType {
UNAUTHORIZED, CANCELED, OTHER
}
Custom interface
interface Cancelable {
fun cancel()
}
Repository interface:
//Convert this code inside of Repository interface:
#Throws(Throwable::class)
suspend fun fetchData(): List<String>
//To this:
fun fetchData(action: (Result<List<String>>) -> Unit): Cancelable
Repository implementation:
override fun fetchData(action: (Result<List<String>>) -> Unit): Cancelable = runInsideOfCancelableCoroutine {
val result = executeAndHandleExceptions {
val data = networkExample()
// do mapping, db operations, etc.
data
}
action.invoke(result)
}
// example of doing heavy background work
private suspend fun networkExample(): List<String> {
// delay, thread sleep
return listOf("data 1", "data 2", "data 3")
}
// generic function for reuse
private fun runInsideOfCancelableCoroutine(task: suspend () -> Unit): Cancelable {
val job = Job()
CoroutineScope(Dispatchers.Main + job).launch {
ensureActive()
task.invoke()
}
return object : Cancelable {
override fun cancel() {
job.cancel()
}
}
}
// generic function for reuse
private suspend fun <T> executeAndHandleExceptions(action: suspend () -> T?): Result<T> {
return try {
val data = action.invoke()
Result(status = Status.SUCCESS, value = data, error = null)
} catch (t: Throwable) {
Result(status = Status.FAIL, value = null, error = ErrorHandler.getError(t))
}
}
ErrorHandler:
object ErrorHandler {
fun getError(t: Throwable): KError {
when (t) {
is ClientRequestException -> {
try {
when (t.response.status.value) {
401 -> return KError(ErrorType.UNAUTHORIZED)
}
} catch (t: Throwable) {
}
}
is CancellationException -> {
return KError(ErrorType.CANCELED)
}
}
return KError(ErrorType.OTHER, t.stackTraceToString())
}
}
You probably have 3 options:
If you're using a some sort of reactive set up iOS side (e.g. MVVM) you could just choose to ignore cancellation. Cancellation will only save a minimal amount of work.
Wrap your iOS calls to shared code in an iOS reactive framework (e.g. combine) and handle cancellation using the iOS framework. The shared work would still be done, but the view won't be updated as your iOS framework is handling cancellation when leaving the screen.
Use Flow with this closable helper

Confused about error handling in network layer when implementing MVVM in android, How to notify user something is wrong?

I've been struggling with MVVM pattern and android-architecture-components for last couple of months.
In my last project although I did try to decouple some of my app logic but it ended with a lot of code mess. Every fragment did lots of work, from handling UI to handling network requests and ...
In this new App I followed best practices for android app architecture and till now it's going well. But the thing is, I don't know how to handle network errors, and I don't get it how should I notify user if some network calls fail.
After searching and reading some blog posts I ended up with the following code (SafeApiCall and SafeApiResutl) functions to handle network requests in one place, but the thing is, All my Network Requests are done using retrofit and a NetworkDataSource class, Then I pass The NetworkDataSource and Dao to the RepositoryImpl class which is an implementation of my Repository Interface. then I pass the Repository to the viewModel, So ViewModel knows nothing about network or Dao or what so ever. So here is the problem, How can I notify user in case of any network errors ? I thought about creating a LiveData<> and pass errors to it in network layer, but in this case, Repository Must observe this, and also let's say create a LiveData in repository so viewModel observe that and so on ... But this is too much chaining, I dont like the idea of doing that. I also did take a look at the GoogleSamples Todo-MVVM-live-kotlin project, but honestly I didn't understand what is going on.
suspend fun <T : Any> safeApiCall(call: suspend () -> Response<BasicResponse<T>>, errorMessage: String): T? {
return when (val result = safeApiResult(call)) {
is NetworkResult.Success -> {
Timber.tag("safeApiCall").d("data is ${result.serverResponse.data}")
result.serverResponse.data
}
is NetworkResult.Error -> {
Timber.tag("SafeApiCall").e("$errorMessage & Exception - ${result.exception}")
null
}
else -> TODO()
}
}
private suspend fun <T : Any> safeApiResult(
call: suspend () -> Response<BasicResponse<T>>
): NetworkResult<T> {
return try {
val response = call.invoke()
Timber.tag("SafeApiResult")
.d("response code : ${response.code()}, server value : ${response.body()!!.status}, server message: ${response.body()!!.message}")
if (response.isSuccessful) {
return when (ServerResponseStatus.fromValue(response.body()!!.status)) {
ServerResponseStatus.SUCCESS -> NetworkResult.Success(response.body()!!)
ServerResponseStatus.FAILED -> TODO()
ServerResponseStatus.UNKNOWN -> TODO()
}
} else {
TODO()
}
} catch (exception: Exception) {
Timber.tag("SafeApiResultFailed").e(exception)
NetworkResult.Error(exception)
}
}
sealed class NetworkResult<out T : Any> {
data class Success<out T : Any>(val serverResponse: BasicResponse<out T>) : NetworkResult<T>()
data class Error(val exception: Exception) : NetworkResult<Nothing>()
}
You are in the correct path. I would place both methods in the NetworkDataSource. All the calls executed should call those methods to handle the errors.
The NetworkDataSource will return the NetworkResult to the repository, and it will return the result to the ViewModel.
As you say, you can use a LiveData to notify the Activity/Fragment. You can create an error data class:
data class ErrorDialog(
val title: String,
val message: String
)
And declare a LiveData<ErrorDialog> that will be observed from your view. Then when you receive notifications in your view, you can implement logic in a BaseActivity/BaseFragment to show a Dialog or Toast or whatever type of view to indicate the error.

Categories

Resources