Android ViewState using RxJava or kotlin coroutines - android

I'm trying to learn how to use RxJava in Android, but have run into a dead end. I have the following DataSource:
object DataSource {
enum class FetchStyle {
FETCH_SUCCESS,
FETCH_EMPTY,
FETCH_ERROR
}
var relay: BehaviorRelay<FetchStyle> = BehaviorRelay.createDefault(FetchStyle.FETCH_ERROR)
fun fetchData(): Observable<DataModel> {
return relay
.map { f -> loadData(f) }
}
private fun loadData(f: FetchStyle): DataModel {
Thread.sleep(5000)
return when (f) {
FetchStyle.FETCH_SUCCESS -> DataModel("Data Loaded")
FetchStyle.FETCH_EMPTY -> DataModel(null)
FetchStyle.FETCH_ERROR -> throw IllegalStateException("Error Fetching")
}
}
}
I want to trigger an update downstream, whenever I change the value of relay, but this doesn't happen. It works when the Activity is initialized, but not when I'm updating the value. Here's my ViewModel, from where I update the value:
class MainViewModel : ViewModel() {
val fetcher: Observable<UiStateModel> = DataSource.fetchData().replay(1).autoConnect()
.map { result -> UiStateModel.from(result) }
.onErrorReturn { exception -> UiStateModel.Error(exception) }
.startWith(UiStateModel.Loading())
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
fun loadSuccess() {
DataSource.relay.accept(DataSource.FetchStyle.FETCH_SUCCESS)
}
fun loadEmpty() {
DataSource.relay.accept(DataSource.FetchStyle.FETCH_EMPTY)
}
fun loadError() {
DataSource.relay.accept(DataSource.FetchStyle.FETCH_ERROR)
}
}
This is the code from the Activity that does the subsciption:
model.fetcher
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
uiState -> mainPresenter.loadView(uiState)
})

Ended up using kotlin coroutines instead, as I was unable to re-subscribe to ConnectableObservable and start a new fetch.
Here's the code for anyone interested.
The presenter:
class MainPresenter(val view: MainView) {
private lateinit var subscription: SubscriptionReceiveChannel<UiStateModel>
fun loadSuccess(model: MainViewModel) {
model.loadStyle(DataSource.FetchStyle.FETCH_SUCCESS)
}
fun loadError(model: MainViewModel) {
model.loadStyle(DataSource.FetchStyle.FETCH_ERROR)
}
fun loadEmpty(model: MainViewModel) {
model.loadStyle(DataSource.FetchStyle.FETCH_EMPTY)
}
suspend fun subscribe(model: MainViewModel) {
subscription = model.connect()
subscription.subscribe { loadView(it) }
}
private fun loadView(uiState: UiStateModel) {
when(uiState) {
is Loading -> view.isLoading()
is Error -> view.isError(uiState.exception.localizedMessage)
is Success -> when {
uiState.result != null -> view.isSuccess(uiState.result)
else -> view.isEmpty()
}
}
}
fun unSubscribe() {
subscription.close()
}
}
inline suspend fun <E> SubscriptionReceiveChannel<E>.subscribe(action: (E) -> Unit) = consumeEach { action(it) }
The view:
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
launch(UI) {
mainPresenter.subscribe(model)
}
btn_load_success.setOnClickListener {
mainPresenter.loadSuccess(model)
}
btn_load_error.setOnClickListener {
mainPresenter.loadError(model)
}
btn_load_empty.setOnClickListener {
mainPresenter.loadEmpty(model)
}
}
override fun onDestroy() {
super.onDestroy()
Log.d("View", "onDestroy()")
mainPresenter.unSubscribe()
}
...
The model:
class MainViewModel : ViewModel() {
val TAG = this.javaClass.simpleName
private val stateChangeChannel = ConflatedBroadcastChannel<UiStateModel>()
init {
/** When the model is initialized we immediately start fetching data */
fetchData()
}
override fun onCleared() {
super.onCleared()
Log.d(TAG, "onCleared() called")
stateChangeChannel.close()
}
fun connect(): SubscriptionReceiveChannel<UiStateModel> {
return stateChangeChannel.openSubscription()
}
fun fetchData() = async {
stateChangeChannel.send(UiStateModel.Loading())
try {
val state = DataSource.loadData().await()
stateChangeChannel.send(UiStateModel.from(state))
} catch (e: Exception) {
Log.e("MainModel", "Exception happened when sending new state to channel: ${e.cause}")
}
}
internal fun loadStyle(style: DataSource.FetchStyle) {
DataSource.style = style
fetchData()
}
}
And here's a link to the project on github.

Related

How to until wait 2 parallel retrofit calls both finish?

I want to call 2 retrofit services in parallel and then do an action only when both of them finished, but I don't seem to figuer it out how.
I have a viewModel where I have defined my services:
var config= List<Configuration>
fun getClientProducts() {
getClientClientConfigUseCase
.build(this)
.executeWithError({ config ->
config = config
}, {
})
}
var therapies = List<DtoTherapy>
fun getTherapies() {
getTherapiesUseCase
.build(this)
.executeWithError({ config ->
therapies = it
}, {
})
}
And then I want to call both services in parallel in my fragment:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupUi(view)
loadUserData()
viewModel.getClientProducts()
viewModel.getTherapies()
}
And when both variables config and therapies have the value do an action. But as I said maybe one service take 1 sec to respond and another 4 secs, and I want only to perfom an action when both have finished. Any help with be appreciated.
Here is the class I use to build the use case call:
abstract class SingleUseCase<T> : UseCase() {
private lateinit var single: Single<T>
private lateinit var useCaseInterface: UseCaseInterface
private var withLoader: Boolean = false
private var withErrorMessage: Boolean = false
internal abstract fun buildUseCaseSingle(): Single<T>
fun build(useCaseInterface: UseCaseInterface): SingleUseCase<T> {
this.withLoader = false
this.withErrorMessage = false
this.useCaseInterface = useCaseInterface
this.single = buildUseCaseSingle()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doAfterSuccess { useCaseInterface.onSuccess(it) }
return this
}
fun withLoader(): SingleUseCase<T> {
this.withLoader = true
return this
}
fun withErrorMessage(): SingleUseCase<T> {
this.withErrorMessage = true
return this
}
fun single(): Single<T> {
return this.single
}
fun execute(onSuccess: ((t: T) -> Unit)) {
useCaseInterface.onPrepareRequest(withLoader)
buildObservable(onSuccess)
}
private fun buildObservable(onSuccess: ((t: T) -> Unit)) {
disposeLast()
lastDisposable = single
.doFinally { useCaseInterface.onFinishRequest(this.withLoader) }
.subscribe(
{ onSuccess(it) },
{
useCaseInterface.onError(mapError(it), withErrorMessage)
})
lastDisposable?.let {
compositeDisposable.add(it)
}
}
fun executeWithError(onSuccess: ((success: T) -> Unit), onError: ((error: ApiError ) -> Unit)) {
useCaseInterface.onPrepareRequest(withLoader)
buildObservable(onSuccess, onError)
}
private fun buildObservable(onSuccess: ((success: T) -> Unit), onError: ((error: ApiError ) -> Unit)) {
disposeLast()
lastDisposable = single
.doFinally { useCaseInterface.onFinishRequest(this.withLoader) }
.subscribe(
{ onSuccess(it) },
{
onError(mapError(it))
useCaseInterface.onError(mapError(it), withErrorMessage)
})
lastDisposable?.let {
compositeDisposable.add(it)
}
}
private fun mapError(t: Throwable): ApiError {
return if(t is HttpException) {
val apiError = t.response()?.errorBody()?.string()
try {
ApiError (t.code(), t.response()?.errorBody()?.string(), Gson().fromJson(apiError, GenericError::class.java))
} catch(e: Exception) {
ApiError (-2, "Unkown error")
}
} else ApiError (-1, "Unkown error")
}
}
And this is a specific usecase class:
class GetClientConfigUseCase #Inject constructor(private val repository: UserRepository) :
SingleUseCase<ClientConfigResponse>() {
override fun buildUseCaseSingle(): Single<ClientConfigResponse> {
return repository.getUserConfig()
}
}
I guess you need zip operation. With zip operation you can have a result of two observable in one place when both of them received data.
Observable<List<ClientProducts>> observable1 = ...
Observable<List<DtoTherapy>> observable2 = ...
Observable.zip(observable1, observable2, new BiFunction<List<ClientProducts>, List<DtoTherapy>, Result>() {
#Override
public Result apply(List<ClientProducts> products, List<DtoTherapy> therapies) throws Exception
{
// here you have both of your data
// do operations on products and therapies
// then return the result
return result;
}
});

MutableStateFlow: Cannot invoke setValue on a background thread from Coroutine on ActivityLifecycleCallbacks

Problem
I got error message Cannot invoke setValue on a background thread when using MutableStateFlow on ActivityLifecycleCallback, i got this error after updating lifecycle jetpack to 2.4.0 and use repeatOnLifecycle. Calling network interfaces using viewmodel that injected by koin to activity responding in ActivityLifecycleCallback.
Implementation
Callback
class ActivityLifecycleCallback :
Utils.ActivityLifecycleCallbacks(),
KoinComponent {
private val mPref by inject<SecuredPrefUseCase>()
private var mViewModel: Lazy<AccountViewModel>? = null
override fun onActivityCreated(activity: Activity) {
super.onActivityCreated(activity)
if (activity is AppCompatActivity) {
mViewModel = activity.viewModel()
if (activity is AccountObserver.Interfaces) {
mViewModel?.let {
activity.lifecycle.addObserver(
AccountObserver(activity, it.value)
)
activity.supportFragmentManager.fragments.forEach { frg ->
if (frg is AccountObserver.Interfaces) {
frg.viewLifecycleOwner.lifecycle.addObserver(
AccountObserver(frg, it.value)
)
}
}
}
}
}
}
override fun onActivityStarted(activity: Activity) {
if (mPref.membership != MembershipType.GUEST) {
mViewModel?.value?.getMembership()
}
super.onActivityStarted(activity)
}
}
ViewModel
class AccountViewModel(
private val useCase: AccountUseCase,
) : BaseViewModel() {
private val _membership = MutableStateFlow<State.Single<Membership.Data?>>(State.Single.Idle())
val mMembership get() = _membership.asImmutable()
suspend fun resetMembership() {
_membership.emit(State.Single.Idle())
}
fun getMembership() = viewModelScope.launch(ioContext) {
requestSingle(_membership) {
useCase.getMembership()
}
}
suspend inline fun <R> BaseViewModel.requestSingle(
state: MutableStateFlow<State.Single<R>>,
delay: Long = Constant.MEDIUM_DELAY,
crossinline block: suspend () -> R,
) = mutex.withLock {
flow {
emit(State.Single.Loading())
val result = block.invoke()
delay(delay)
emit(State.Single.Success(result))
}.catch {
state.emit(State.Single.Failed(it))
}.collect {
state.emit(it)
}
}
}
Observer
class AccountObserver(
private val view: Interfaces,
private val viewModel: AccountViewModel,
) : DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) {
super.onCreate(owner)
owner.initObserver {
viewModel.mMembership.observeSingle(this) {
when (it) {
is State.Single.Idle -> view.onMembershipIdle()
is State.Single.Loading -> view.onMembershipLoading()
is State.Single.Success -> {
view.onMembershipSuccess(it.data)
viewModel.resetMembership()
}
is State.Single.Failed -> {
view.onMembershipFailed(it.throwable)
viewModel.resetMembership()
}
}
}
}
}
}
inline fun LifecycleOwner.initObserver(
crossinline block: suspend CoroutineScope.() -> Unit,
) {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
block.invoke(this)
}
}
}
suspend inline fun <R> StateFlow<State.Single<R>>.observeSingle(
coroutineScope: CoroutineScope,
crossinline block: suspend (State.Single<R>) -> Unit,
) {
coroutineScope.launch {
collect {
block.invoke(it)
}
}
}

Nested Callback function with coroutines

I want to get a response from callback function async/await style of javascript using kotlin coroutines.
Here is my callback functions
offlineCatalog.findOfflineVideoById(id, object : OfflineCallback<Video> {
override fun onSuccess(video: Video?) {
video?.let {
//Return This Video
} ?: kotlin.run {
findVideoOnline(id, state)
}
}
override fun onFailure(throwable: Throwable?) {
findVideoOnline(id, state)
}
})
onlineCatalog.findVideoByID(id, object : VideoListener() {
override fun onVideo(video: Video?) {
video?.let {
//Return This Video
} ?: kotlin.run {
Log.e("Return Error")
}
}
override fun onError(errors: MutableList<CatalogError>) {
super.onError(errors)
Log.e("Return Error")
}
})
I want to call function that will return video object from OfflineCatalog if error in OfflineCatalog then search from OnlineCatalog.
such as
try{
val video:Video? = getVideo(id:String)
//do something
}catch(throwable:Throwable){
Log.e("Video not found")
}
Update: My Implementation
this is what I came up with
suspend fun getVideo(id: String): Video? = withContext(Dispatchers.IO) {
var video = getVideoOffline(id)
video?.let { video } ?: kotlin.run { getVideoOnline(id) }
}
suspend fun getVideoOffline(id: String): Video? = suspendCancellableCoroutine { cont ->
(offlineCatalog.findOfflineVideoById(id, object : OfflineCallback<Video> {
override fun onSuccess(video: Video?) = cont.resume(video)
override fun onFailure(throwable: Throwable?) = cont.resume(null)
}))
}
suspend fun getVideoOnline(id: String): Video? = suspendCancellableCoroutine { cont ->
catalog.findVideoByID(id, object : VideoListener() {
override fun onVideo(video: Video?) = cont.resume(video)
override fun onError(errors: MutableList<CatalogError>) = cont.resume(null)
})
}
Usage-
CoroutineScope(Dispatchers.Main).launch {
getVideo(id)?.let {
//Do Something
} ?: kotlin.run{
//Video Not Found
}
}
you have to do something like this
#ExperimentalCoroutinesApi
suspend fun getVideo(id: String): Video? = coroutineScope {
val offlineVideo: Video? = suspendCancellableCoroutine { cont ->
offlineCatalog.findOfflineVideoById(id, object : OfflineCallback<Video> {
override fun onSuccess(video: Video?) {
cont.resume(video)
}
override fun onFailure(throwable: Throwable?) {
cont.resume(null)
}
})
}
offlineVideo ?: suspendCancellableCoroutine { cont ->
// offlineVideo not found so search from onlineCatalog
onlineCatalog.findVideoByID(id, object : VideoListener() {
override fun onVideo(video: Video?) {
cont.resume(video)
}
override fun onError(errors: MutableList<CatalogError>) {
super.onError(errors)
cont.resumeWithException(someException)
}
})
}
}
then you can call it as you wanted
someScope.launch {
try {
val video: Video? = getVideo(id)
//do something
} catch (throwable: Throwable) {
Log.e("Video not found")
}
}
Read more about suspendCancellableCoroutine here

How can I convert LiveData to 2 pieces of liveData?

I have configLiveData:LiveData<Response<ConfigFile>> where Response is
sealed class Response<out T> {
data class Success<out T>(val data: T) : Response<T>()
data class Failure<out T>(val message: String) : Response<T>()
}
now in ViewModel I want to transform configLiveData to two different
LiveDatas 1.LiveData<ConfigFile> and 2. LiveData<String> and as a result of transformation one of them will be empty.
but I want to get ride of LiveData<Response<ConfigFile>> and have instead of it LiveData<ConfigFile>
override suspend fun fetchConfigFile(): Response<ConfigFile> {
return suspendCoroutine { cont ->
EnigmaRiverContext.getHttpHandler().doHttp(AppConstants.getPath().append("config/appConfig.json").toURL(),
JsonHttpCall("GET"), object : JsonReaderResponseHandler() {
override fun onSuccess(jsonReader: JsonReader) {
try {
val successObject = ApiConfigFile(jsonReader)
cont.resume(Response.Success(successObject.toPresenterModel()))
} catch (e: IOException) {
cont.resume(Response.Failure(e.message))
} catch (e: Exception) {
cont.resume(Response.Failure(e.message ))
}
}
override fun onError(error: Error?) {
cont.resume(Response.Failure("error.message"))
}
})
}
}
It is how my Repository looks like
private fun fetchConfig() {
uiScope.launch {
when (val result = homeRepository.fetchConfigFile()) {
is Response.Success<ConfigFile> -> {
postValue(Response.Success(result.data))
}
is Response.Failure -> postValue(Response.Failure(result.message))
}
}
}
class ConfigFileLiveData #Inject constructor(val homeRepository: IHomeRepository) : LiveData<Response<ConfigFile>>() {
private val liveDataJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + liveDataJob)
override fun onActive() {
fetchConfig()
}
private fun fetchConfig() {
viewModelScope.launch {
when (val result = homeRepository.fetchConfigFile()) {
is Response.Success<ConfigFile> -> {
postValue(Response.Success(result.data))
}
is Response.Failure -> postValue(Response.Failure(result.message))
}
}
}
}
I have `ConfigFileLiveData` which is singleton and I want to use this liveData in other viewModels as I need to fetch config once and use it in different ViewModels
class ConfigFileLiveData #Inject constructor(val homeRepository: IHomeRepository) : LiveData<Response<ConfigFile>>() {
override fun onActive() {
fetchConfig()
}
private fun fetchConfig() {
viewModelScope.launch {
when (val result = homeRepository.fetchConfigFile()) {
is Response.Success<ConfigFile> -> {
postValue(Response.Success(result.data))
}
is Response.Failure -> postValue(Response.Failure(result.message))
}
}
}
}
In Viewmodel Define two LiveData variable.
private var configLiveData = MutableLiveData<ConfigFile>()
private var stringLiveData = MutableLiveData<String>()
Modify this method
private fun fetchConfig() {
uiScope.launch {
when (val result = homeRepository.fetchConfigFile()) {
is Response.Success<ConfigFile> -> {
configLiveData.value = Response.Success(result.data)
}
is Response.Failure -> {
stringLiveData.value = Response.Failure(result.message)
}
}
}
}

How to show ProgressDialog when fetching data from ViewModel

I want to show ProgressDialog while fetching data from ViewModel and it works fine when I fetch data for the first time, but when I want to refresh the data from API the ProgressDialog starts and does not stops
I create MutableLiveData<Boolean>() and try to manage the visibility but it's not working
This is how i refresh my data from my Activity
private fun loadNorthTram() {
val model =
ViewModelProviders.of(this#MainActivity).get(MyViewModelNorth::class.java)
model.isNorthUpdating.observe(
this#MainActivity,
Observer { b ->
if (b!!)
AppUtil.showProgressSpinner(this#MainActivity)
else
AppUtil.dismissProgressDialog()
})
model.getNorthTrams().observe(this#MainActivity, Observer
{
if (it != null) {
setData(it)
}
})
}
Below is my ViewModel class
class MyViewModelNorth : ViewModel() {
private lateinit var mtoken: String
private val apiService: ApiInterface = ApiClient.client.create(ApiInterface::class.java)
private lateinit var trams: MutableLiveData<TramModel>
val isNorthUpdating = MutableLiveData<Boolean>().default(false)
fun getNorthTrams(): MutableLiveData<TramModel> {
isNorthUpdating.value = true
if (!::trams.isInitialized) {
trams = MutableLiveData()
callTokenAPI()
}
return trams
}
private fun callTokenAPI() {
val tokenObservable: Observable<TokenModel> = apiService.fetchToken()
tokenObservable
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnNext { self ->
mtoken = self.responseObject[0].DeviceToken
callTramAPI()
}
.subscribe(getTokenObserver())
}
private fun callTramAPI() {
val apiService: ApiInterface = ApiClient.client.create(ApiInterface::class.java)
val observable: Observable<TramModel> = apiService.fetchTrams(AppUtil.NORTH_TRAMS, mtoken)
observable
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getTramObserver())
}
private fun getTokenObserver(): Observer<TokenModel> {
return object : Observer<TokenModel> {
override fun onComplete() {}
override fun onSubscribe(d: Disposable) {}
override fun onNext(tokenModel: TokenModel) {}
override fun onError(e: Throwable) {
if (e is HttpException) {
val errorBody = e.response().errorBody()
HttpErrorUtil(e.code()).handelError()
}
}
}
}
private fun getTramObserver(): Observer<TramModel> {
return object : Observer<TramModel> {
override fun onComplete() {
isNorthUpdating.value = false
}
override fun onSubscribe(d: Disposable) {}
override fun onNext(t: TramModel) {
if (!t.hasError && t.hasResponse)
trams.value = t
else if (t.errorMessage.isBlank())
applicationContext().showToast(t.errorMessage)
else
applicationContext().showToast(applicationContext().getString(R.string.something_wrong))
}
override fun onError(e: Throwable) {
if (e is HttpException) {
val errorBody = e.response().errorBody()
HttpErrorUtil(e.code()).handelError()
}
}
}
}
public fun getIsNothUpdating(): LiveData<Boolean> {
return isNorthUpdating
}
fun <T : Any?> MutableLiveData<T>.default(initialValue: T) = apply { setValue(initialValue) }
}
I have not tested your code but I think your problem is in function getNorthTrams() in viewmodel.
First time when you fetch data, trams is not initialized, your api call is happening and at only onCompleted, you are setting isNorthUpdating.value = false. This code works.
But when you refresh data, trams is already initialized. So, there is no case for isNorthUpdating.value = false, which is causing progress dialog to not dismiss.
So I think you should handle else case in your viewmodel.
fun getNorthTrams(): MutableLiveData<TramModel> {
isNorthUpdating.value = true
if (!::trams.isInitialized) {
trams = MutableLiveData()
callTokenAPI()
}else{
//do your thing for refresh
isNorthUpdating.value = false
}
return trams
}
Also, in api call, if error occur, you should set isNorthUpdating to false and show some error message. Otherwise, progress dialog will always be showing even if some error occur in api call.

Categories

Resources