android coroutines flow manual retry - android

I'm using stateFlow in my viewModel to get the result of api calls with a sealed class like this :
sealed class Resource<out T> {
data class Loading<T>(val data: T?): Resource<T>()
data class Success<T>(val data: T?): Resource<T>()
data class Error<T>(val error: Exception, val data: T?, val time: Long = System.currentTimeMillis()): Resource<Nothing>()
}
class VehicleViewModel #ViewModelInject constructor(application: Application, private val vehicleRepository: VehicleRepository): BaseViewModel(application) {
val vehiclesResource: StateFlow<Resource<List<Vehicle>>> = vehicleRepository.getVehicles().shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1)
}
I would like to propose in my UI a button so that if the api call fails, the user can retry the call. I know there is a retry method on flows but it can not be called manually as it is only triggered when an exception occurs.
A common use case would be: The user have no internet connection, when the api call returns a network exception, I show a message to the user telling him to check its connection, then with a retry button (or by detecting that the device is now connected, whatever), I retry the flow.
But I can't figure a way to do it as you can not, like call flow.retry(). The actual retry method will be called as soon as an exception occurs. I don't want to retry immediately without asking the user to check it's connection, it wouldn't make sense.
Actually the only solution I found is to recreate the activity when the retry button is pressed so the flow will be reseted, but it's terrible for performances of course.
Without flows the solution is simple and there is plenties of examples , you just have to relaunch the job, but I can't find a way to do it properly with flows. I have logic in my repository between the local room database and the remote service and the flow api is really nice so I would like to use it for this use case too.

I recommend keep the vehicleResource as a field in viewmodel and call a function to make an API call to fetch data.
private val _vehiclesResource = MutableStateFlow<Resource<List<Vehicle>>>(Resource.Loading(emptyList()))
val vehiclesResource: StateFlow<Resource<List<Vehicle>>> = _vehiclesResource.asStateFlow()
init {
fetchVehicles()
}
private var vehicleJob : Job? = null
fun fetchVehicles() {
vehicleJob?.cancel()
vehicleJob = viewModelScope.launch {
vehicleRepository.getVehicles().collect {
_vehiclesResource.value = it
}
}
}
The API will be called in the constructor of viewmodel. And also you can call this function via the view (button's click listener) for error state.
P.S: I should mention that this line of your code has issue and SharedFlow couldn't be cast to StateFlow.
val vehiclesResource: StateFlow<Resource<List<Vehicle>>> = vehicleRepository.getVehicles().shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1)

Related

one-shot operation with Flow in Android

I'm trying to show a user information in DetailActivity. So, I request a data and get a data for the user from server. but in this case, the return type is Flow<User>. Let me show you the following code.
ServiceApi.kt
#GET("endpoint")
suspend fun getUser(#Query("id") id: Int): Response<User>
Repository.kt
fun getUser(id: Int): Flow<User> = flow<User> {
val userResponse = api.getUser(id = id)
if (userResponse.isSuccessful) {
val user = userResponse.body()
emit(user)
}
}
.flowOn(Dispatchers.IO)
.catch { // send error }
DetailViewModel.kt
class DetailViewModel(
private val repository : Repository
) {
val uiState: StateFlow<User> = repository.getUser(id = 369).stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = User() // empty user
)
}
DetailActivity.kt
class DetailActivity: AppCompatActivity() {
....
initObersevers() {
lifecycleScope.launch {
// i used the `flowWithLifecycle` because the data is just a single object.
viewModel.uiState.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).collect { state ->
// show data
}
}
}
...
}
But, all of sudden, I just realized that this process is just an one-shot operation and thought i can use suspend function and return User in Repository.kt.
So, i changed the Repository.kt.
Repository.kt(changed)
suspend fun getUser(id: Int): User {
val userResponse = api.getUser(id = id)
return if(userResponse.isSuccessful) {
response.body()
} else {
User() // empty user
}
}
And in DetailViewModel, i want to convert the User into StateFlow<User> because of observing from DetailActivity and I'm going to use it the same way as before by using flowWithLifecycle.
the concept is... i thought it's just one single data and i dind't need to use Flow in Repository. because it's not several items like List.
is this way correct or not??
Yeap, this one-time flow doesn't make any sense - it emits only once and that's it.
You've got two different ways. First - is to create a state flow in your repo and emit there any values each time you're doing your GET request. This flow will be exposed to the use case and VM levels. I would say that it leads to more difficult error handling (I'm not fond of this way, but these things are always arguable, haha), but it also has some pros like caching, you can always show/get the previous results.
Second way is to leave your request as a simple suspend function which sends a request, returns the result of it back to your VM (skipping error handling here to be simple):
val userFlow: Flow<User>
get() = _userFlow
private val _userFlow = MutableStateFlow(User())
fun getUser() = launch(viewModelScope) {
_userFlow.value = repository.getUser()
}
This kind of implementation doesn't provide any cache out of scope of this VM's lifecycle, but it's easy to test and use.
So it's not like there is only one "the-coolest-way-to-do-it", it's rather a question what suits you more for your needs.

Android Kotlin - Unit test a delayed coroutine that invokes a lambda action when delay is finished

Ive been struggling with this for quite some time now, perhaps someone could help...
I have this function in my class under test:
fun launchForegroundTimer(context: Context) {
helper.log("AppRate", "[$TAG] Launching foreground count down [10 seconds]")
timerJob = helper.launchActionInMillisWithBundle(Dispatchers.Main, TimeUnit.SECOND.toMillis(10), context, this::showGoodPopupIfAllowed)
}
So in that function, I first write to some log and then I call a coroutine function that expects a Dispatcher param, how long to wait before running the action, Any object that I would like to pass on to the action and the actual action function that is invoked when time has passed.
So in this case, the this::showGoodPopupIfAllowed which is a private method in the class, gets called when the 10,000 ms have passed.
Here is that function:
private fun showGoodPopupIfAllowed(context: Context?) {
if (isAllowedToShowAppRate()) {
showGoodPopup(context)
}
}
In that first if, there are a bunch of checks that occur before I can call showGoodPopup(context)
Now, here is the helper.launchActionInMillisWithBundle function:
fun <T> launchActionInMillisWithBundle(dispatcher: CoroutineContext, inMillis: Long, bundle: T, action: (T) -> Unit): Job = CoroutineScope(dispatcher).launchInMillisWithBundle(inMillis, bundle, action)
And here is the actual CoroutineScope extension function:
fun <T> CoroutineScope.launchInMillisWithBundle(inMillisFromNow: Long, bundle: T, action: (T) -> Unit) = this.launch {
delay(inMillisFromNow)
action(bundle)
}
What I am trying to achieve is a UnitTest that calls the launchForegroundTimer function, calls the helper function with the appropriate arguments and also continue through and call that lambda showGoodPopupIfAllowed function where I can also provide mocked behaviour to all the IF statments that occur in isAllowedToShowAppRate.
Currently my test stops right after the launchActionInMillisWithBundle is called and the test just ends. I assume there is no real call to any coroutine because I am mocking the helper class... not sure how to continue here.
I read a few interesting articles but none seems to resolve such state.
My current test function looks like this:
private val appRaterManagerHelperMock = mockkClass(AppRaterManagerHelper::class)
private val timerJobMock = mockkClass(Job::class)
private val contextMock = mockkClass(Context::class)
#Test
fun `launch foreground timer`() {
every { appRaterManagerHelperMock.launchActionInMillisWithBundle(Dispatchers.Main, TimeUnit.SECOND.toMillis(10), contextMock, any()) } returns timerJobMock
val appRaterManager = AppRaterManager(appRaterManagerHelperMock)
appRaterManager.launchForegroundTimer(contextMock)
verify(exactly = 1) { appRaterManagerHelperMock.log("AppRate", "[AppRaterManager] Launching foreground count down [10 seconds]") }
}
I'm using mockk as my Mocking lib.
AppRaterManager is the class under test
I'd like to also mention that, in theory I could have moved the coroutine invocation outside the class under test. So an external class like activity.onResume() could launch some sort of countdown and then call directly a function that checks showGoodPopupIfAllowed(). But currently, please assume that I do not have any way to change the calling code so the timer and coroutine should remain in the class under test domain.
Thank you!
Alright, I read a bit deeper into capturing/answers over at https://mockk.io/#capturing and saw there is a capture function.
So I captured the lambda function in a slot which enables me invoke the lambda and then the actual code continues in the class under test. I can mock the rest of the behavior from there.
Here is my test function for this case (for anyone who gets stuck):
#Test
fun `launch foreground timer, not participating, not showing good popup`() {
val slot = slot<(Context) -> Unit>()
every { appRaterManagerHelperMock.launchActionInMillisWithBundle(Dispatchers.Main, TimeUnit.SECOND.toMillis(10), contextMock, capture(slot)) } answers {
slot.captured.invoke(contextMock)
timerJobMock
}
every { appRaterManagerHelperMock.isParticipating() } returns false
val appRaterManager = AppRaterManager(appRaterManagerHelperMock)
appRaterManager.launchForegroundTimer(contextMock)
verify(exactly = 1) { appRaterManagerHelperMock.log("AppRate", "[AppRaterManager] Launching foreground count down [10 seconds]") }
verify(exactly = 1) { appRaterManagerHelperMock.isParticipating() }
verify(exactly = 0) { appRaterManagerHelperMock.showGoodPopup(contextMock, appRaterManager) }
}
So what's left now is how to test the coroutine actually invokes the lambda after the provided delay time is up.

Using Delegates.observable with StateFlow in Kotlin(Android)

I have a class which monitors the network state.
I'm using Flow to be able to collect the network state updates.
However, I also need to leave an option to use manual listeners that programmers can "hook" onto to be able to receive the network changes.
My code is simple :
private val networkTypeState = MutableStateFlow<NetworkState>(NetworkState.Unknown)
val networkTypeAsFlow: StateFlow<NetworkState> by notifyDelegate(networkTypeState)
private fun <T> notifyDelegate(init: T) =
Delegates.observable(init) { prop, _, new ->
Lg.i("notify subscribers of network update: ${prop.name} = $new")
notifySubscribers()
}
sealed class NetworkState {
object Unknown: NetworkState()
object Disconnected: NetworkState()
data class Connected(val isCellularOn: Boolean, val isWifiOn: Boolean): NetworkState()
}
Then when I update the state,
for example :
networkTypeState.value = NetworkState.Disconnected ,
the delegates.observable does not get called.
Worth noting, when I use networkTypeAsFlow.collect { .. } , this works well, meaning - the networkTypeAsFlow does get updated, it just doesn't call the delegates.observable
The Observable delegate monitors changes to the property itself. It is futile to use Observable for a val property, because a val property is never set to a new value. Mutating the object pointed at by the property is completely invisible to the property delegate.
If you want to observe changes, you can launch and collect:
private val networkTypeState = MutableStateFlow<NetworkState>(NetworkState.Unknown)
val networkTypeAsFlow: StateFlow<NetworkState> = networkTypeState
init {
viewModelScope.launch {
networkTypeAsFlow.collect {
Lg.i("notify subscribers of network update: $it")
notifySubscribers()
}
}
}
An additional benefit here is that notifySubscribers will always be called from the same dispatcher, regardless of which thread the network state was changed from.

Suscribing to observable: RxJava + Retrofit + Repository pattern

I am trying to set up retrofit and rxjava for my app to make api calls to a webservice. I am having some trouble subscribing to my observable object in the main activity. The architecture follows the viewmodel/repository pattern, here is my code
Repository
class WisproRepository() {
val request = ServiceBuilder.buildService(JsonPayments::class.java)
val apicall: Observable<Payment> = request.getPostsV2(2, 100, "authorizationcode")
fun getPayments(): Observable<Payment?>? {
return apicall.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
}
}
ViewModel
class PaymentsViewModel : ViewModel() {
var wisproRepository = WisproRepository()
var payments_rx: Observable<Payment?>? = wisproRepository.getTopContributors()
}
MainActivity
class MainActivity : AppCompatActivity() {
var adapter: MyRecyclerViewAdapter? = null
private var textView: TextView? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//Getting payment object and updating view
val view_model: PaymentsViewModel by viewModels()
view_model.payments_rx?.subscribe(....)
}
Now, instead of observing the livedata I would have to subscribe to the Observable and setup recyclerview when the data arrives but I don't understand how to do that in the subscribe method. But I don't understand how to do that inside the subscribe method. Also, how can I access the data of the Payment inside the observable object ?
Also I do not understand these lines right here,
val request = ServiceBuilder.buildService(JsonPayments::class.java)
val apicall: Observable<Payment> = request.getPostsV2(2,100, "authorization code")
apicall.subscribeOn(Schedulers.newThread()).observeOn(AndroidSchedulers.mainThread())
Are subscribeOn and observeOn performing the call? And if they are, how does it work (I am used to enqueue method)? Am I missing something? How can I setup onResponse and onFailure callbacks?
If you use retrofit2:adapter-rxjava2 it automatically wrap your API call to Cold Observable and auto-execute API Request when you subscribe.
Are subscribeOn and observeOn performing the call
It doesn't perform call API, API call when you call subscribe it makes your API request run in Background Thread and handles response in Main Thread
How can I setup onResponse and onFailure callbacks?
In subscribe method you can pass Consumer to handle Response or Error
view_model.payments_rx?.subscribe({
// response
}, {
// error
})
An observable is basically a stream of events, each event is of the type that you're observing. In this case Payment. Every time you get a Payment object, the onNext() method of your observable is called. Once the stream is done emitting events onComplete will be called, and in case of an error, onError (both of these are terminal events)
You haven't posted the JsonPayments class so I can't say for sure, but you need to be creating an observable in getPostsV2() and returning it so you can observe it or subscribe to it.
If you want onResponse and onFailure callbacks, you could essentially wrap your Payment object to Result<Payment> where Result has both of those properties. Your final observable will be Observable<Result<Payment>> where each event will be of type Result<Payment>.

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