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
Related
I want to know if it is possible to capture an suspend lambda with generic result with MockK and JUnit5.
I've tried several ways, and in the most recent I'm having a KotlinNullPointerException when I try to run the test.
Here is the code, with the complete class dependencies and test:
class Test {
data class Result<out T>(val status: ResultStatus, val data: T?, val exception: Exception?) {
companion object {
fun <T> success(data: T?): Result<T> {
return Result(ResultStatus.SUCCESS, data, null)
}
fun <T> error(exception: Exception): Result<T> {
return Result(ResultStatus.ERROR, null, exception)
}
fun <T> loading(data: T? = null): Result<T> {
return Result(ResultStatus.LOADING, data, null)
}
}
}
class Dep1() {
fun <R> methodToMock(viewModelScope: CoroutineScope, block: suspend CoroutineScope.() -> R): MutableLiveData<Result<R>> {
val result = MutableLiveData<Result<R>>()
result.value = Result.loading()
viewModelScope.launch {
try {
var asyncRequestResult: R? = null
withContext(Dispatchers.Default) {
asyncRequestResult = block()
}
result.value = Result.success(asyncRequestResult)
} catch (cancellationException: CancellationException) {
} catch (exception: Exception) {
result.value = Result.error(exception)
}
}
return result
}
}
class Dep2() {
fun methodToAssert(): Boolean {
println("Called")
return true
}
}
class ClassToTest(private val dep1: Dep1, private val dep2: Dep2) {
fun methodToCall(coroutineScope: CoroutineScope): MutableLiveData<Result<Boolean>> {
return dep1.methodToMock(coroutineScope) {
dep2.methodToAssert()
}
}
}
private val dep1: Dep1 = mockk()
private val dep2: Dep2 = mockk(relaxed = true)
private val mViewModelScope: CoroutineScope = GlobalScope
#Test
fun `Check if is calling the required methods correctly`() {
val classToTest = ClassToTest(dep1, dep2)
val transactionLambda = slot<suspend CoroutineScope.() -> Boolean>()
coEvery { dep1.methodToMock(mViewModelScope, capture(transactionLambda)) } coAnswers {
MutableLiveData(Result.success(transactionLambda.captured.invoke(mViewModelScope)))
}
classToTest.methodToCall(mViewModelScope)
verify { dep2.methodToAssert() }
}
}
If anyone is also having this problem, I was able to solve it using every instead of coEvery and calling the coInvoke() slot method:
every { dep1.methodToMock(mViewModelScope, capture(transactionLambda)) } answers {
MutableLiveData(Result.success(transactionLambda.coInvoke(mViewModelScope)))
}
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)
}
}
}
}
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.
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.
I'm using Room + LiveData in my Android project. Following to Google Blueprints, I've implemented data layer of my application.
This is how my Dao looks like:
#Query("SELECT * FROM events WHERE id=:arg0")
fun loadSingle(id: String): LiveData<Event>
I'm calling it from my EventRepository:
fun loadSingle(eventId: String): LiveData<RequestReader<Event>> {
return object: NetworkManager<Event, Event>(appExecutors!!) {
override fun loadLocal(): LiveData<Event> {
val item = eventLocal!!.loadSingle("Title 1")
Crashlytics.log(Log.VERBOSE, TAG, "loadFromServer::loadLocal=$item")
return item
}
override fun isUpdateForced(data: Event?): Boolean {
Crashlytics.log(Log.VERBOSE, TAG, "loadFromServer::isUpdateForced")
return data == null || requestTimeout.isAllowed(UNDEFINED_KEY.toString())
}
override fun makeRequest(): LiveData<ApiResponse<Event>> {
Crashlytics.log(Log.VERBOSE, TAG, "loadFromServer::makeRequest")
return Database.createService(EventRemote::class.java).load(eventId)
}
override fun onSuccess(item: Event) {
eventLocal?.save(item)
}
override fun onFail() {
Crashlytics.log(Log.VERBOSE, TAG, "loadFromServer::onFail")
requestTimeout.reset(UNDEFINED_KEY.toString())
}
}.liveData
}
Where NetworkManager class is (has been "taken" from here):
abstract class NetworkManager<ResultType, RequestType> #MainThread constructor(val appExecutors: AppExecutors) {
companion object {
private val TAG = "TAG_NETWORK_MANAGER"
}
val liveData: MediatorLiveData<RequestReader<ResultType>> = MediatorLiveData()
init {
liveData.value = RequestReader.loading(null)
val localSource: LiveData<ResultType> = loadLocal()
Log.d(TAG, "before add::localSource=${localSource.value}")
liveData.addSource(localSource, { data ->
Log.d(TAG, "data=$data")
liveData.removeSource(localSource)
if (isUpdateForced(data)) {
loadRemote(localSource)
} else {
liveData.addSource(localSource, { reusedData -> liveData.value = RequestReader.success(reusedData)})
}
})
}
private fun loadRemote(localSource: LiveData<ResultType>) {
val remoteSource = makeRequest()
liveData.addSource(localSource, {
liveData.value = RequestReader.success(it)
})
liveData.addSource(remoteSource) { response ->
liveData.removeSource(localSource)
liveData.removeSource(remoteSource)
if (response!!.isSuccessful) {
appExecutors.diskIO.execute {
onSuccess(processResponse(response))
appExecutors.mainThread.execute {
liveData.addSource(localSource, {
liveData.value = RequestReader.success(it)
})
}
}
} else {
onFail()
liveData.addSource(localSource, {
liveData.value = RequestReader.error("Error: ${response.errorMessage}", it)
})
}
}
}
#MainThread
protected abstract fun loadLocal(): LiveData<ResultType>
#MainThread
protected abstract fun isUpdateForced(data: ResultType?): Boolean
#MainThread
protected abstract fun makeRequest(): LiveData<ApiResponse<RequestType>>
#WorkerThread
protected abstract fun onSuccess(item: RequestType)
#MainThread
protected abstract fun onFail()
#WorkerThread
protected fun processResponse(response: ApiResponse<RequestType>): RequestType {
return response.body!!
}
}
And after i expect to get my LiveData in ViewModel:
open class EventSingleViewModel: ViewModel(), RepositoryComponent.Injectable {
companion object {
private val TAG = "TAG_EVENT_SINGLE_VIEW_MODEL"
}
#Inject lateinit var eventRepository: EventRepository
var eventSingle: LiveData<RequestReader<Event>>? = null
override fun inject(repositoryComponent: RepositoryComponent) {
repositoryComponent.inject(this)
eventSingle = MutableLiveData<RequestReader<Event>>()
}
fun load(eventId: String) {
Crashlytics.log(Log.VERBOSE, TAG, "starts to loadList::eventId=$eventId")
eventSingle = eventRepository.loadSingle(eventId)
}
}
The problem.
I'm getting a list of events the same way (it works!) I've described above, but with a single event (this event is already in database) it doesn't work. I've found out that localSource.value is null (in NetworkManager). Maybe my query is bad or.. something else.
Check again your DAO implementation, the argument must be the same in both, the function parameter and the annotation arg.
Change this:
#Query("SELECT * FROM events WHERE id=:arg0")
fun loadSingle(id: String): LiveData<Event>
To:
#Query("SELECT * FROM events WHERE id=:id ")
fun loadSingle(id: String): LiveData<Event>