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>.
Related
So here's the deal . This is the ApiInterface:
interface ApiInterface {
#GET("api/fetch-some-info")
suspend fun fetchSomeInfo(
#Query("some_query_param") queryParam: String,
): Call<Data>
}
Here is how I generate it
new Retrofit.Builder()
.client(mOkHttpClient)
.addConverterFactory(MoshiConverterFactory.create(Moshi.Builder().build()))
.baseUrl(url)
.build().create(AdMediationV2.class);
here is Data class
#JsonClass(generateAdapter = true)
data class Data(
#Json(name = "data")
val info: String)
Now I am trying to call the function and receive data using KotlinExtensions for Retrofit, I am using implementation 'com.squareup.retrofit2:retrofit:2.9.0'
This is how I am trying to call this function. I want to keep a reference of the job so I might be able to cancel it later.
val job = CoroutineScope(Dispatchers.IO).async{
api.fetchSomeInfo("Param").await().let { data->
//handle response}
But I get this error
java.lang.IllegalArgumentException: Unable to create converter for retrofit2.Call<Data>
According to documentation I dont even need to add Call to interface, but unless I do I cannot call .await(). It is marked as red and this is what it says
Unresolved reference. None of the following candidates is applicable because of receiver type mismatch:
public suspend fun <T : Any> Call<TypeVariable(T)>.await(): TypeVariable(T) defined in retrofit2
public suspend fun <T : Any> Call<TypeVariable(T)?>.await(): TypeVariable(T)? defined in retrofit2
What am I doing wrong?
Note: The Api functions properly, when used in java code, call.enqueue() it works fine.
It also works just fine in this code :
var call : Deferred<Data>? = null
MainScope().launch {
call = async(Dispatchers.IO) {
api.fetchSomeInfo("Param")
}
call?.await().let {//handle reponse}
The function in your interface should return Data instead of Call<Data> since it's a suspend function. And since it's a suspend function, there will be no need for calling await() on anything. You can just call fetchSomeInfo() directly in a coroutine, without any concern about what dispatcher it is called from either.
interface ApiInterface {
#GET("api/fetch-some-info")
suspend fun fetchSomeInfo(
#Query("some_query_param") queryParam: String,
): Data
}
//...
val job = someCoroutineScope.launch {
try {
val data = api.fetchSomeInfo("Param")
// handle data
} catch (exception: Exception) {
// handle errors
}
}
Alternatively, you could remove the suspend keyword from your function so it does have to return a Call<Data> that you will then have to call await() on, but that's just adding convolution, so don't do that.
A couple notes about your coroutine... it is a code smell to create a coroutine scope just to launch a single coroutine and then immediately lose the reference. You should be launching from a persistent CoroutineScope that you presumably will be cancelling when you want it to go out of scope.
And when you call a Retrofit API (either by using a suspend function or calling await() on a Call object), you must wrap it in try/catch to handle any of the many possible errors that might occur.
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)
Let me start with example code snippets
suspend fun executeLive(result: MutableLiveData<Person>) {
val response = ... //suspend api request
mediatorLiveData.removeSource(response)
mediatorLiveData.addSource(response) {
result.value = sortData(it) // sortData is also suspend function which sortData at Dispatcher.Default
}
}
In this example, sortData can't call under lambda function(in this case addSource).And also I already declare executeLive as suspend, that why suspend api request can start at first. But sortData function show compile time error
Suspend function can only be called from a coroutine body
So how do I change my code structure to solve this problems?
Update: Is there any article about this?
A lambda is generally a callback function. Callback functions are so called because we wrap a block of code in a function, and pass it to someone else (or some place else) to be executed. It is a basic inversion of control where the code is not for you to execute, but someone else to do it (example the framework).
For example when you set a onClickListener on a button, we don't know when it will get called, we pass a lambda for the framework which takes care of the user interaction to call the specified action.
In your case similarly the suspend function is not calling the sortdata, it is passing it to the mediatorLiveData object to call it in its own context. It is not necessary the lambda you passed would be called from a coroutine body, as such this is not allowed.
You can solve this by converting the mediatorLiveData.addSource call into a suspending call itself with suspendCoroutine:
suspend fun executeLive(result: MutableLiveData<Person>) {
val response = ... //suspend api request
mediatorLiveData.removeSource(response)
val data = suspendCoroutine<TypeOfData> { cont ->
mediatorLiveData.addSource(response) { cont.resume(it) }
}
result.value = sortData(data)
}
I've used TypeOfData as a placeholder for whatever the type of data emitted by response is. Note that this will only work if the you're intending for a single emission to happen, though.
If you need to track multiple values, you can experiment with callbackFlow:
suspend fun executeLive(result: MutableLiveData<Person>) {
val response = ... //suspend api request
mediatorLiveData.removeSource(response)
callbackFlow<TypeOfData> {
mediatorLiveData.addSource(response) { offer(it) }
awaitClose { mediatorLiveData.removeSource(response) }
}
.collect { result.value = sortData(it) }
}
I have a BehaviourSubject as a callback for my Retrofit method.
private val loadCompleted = BehaviourSubject.create<List<String>>()
Inside my retrofit OnResponse/onFailure, I call
loadCompleted.onNext(myList) //inside retrofit onResponse and
loadCompleted.onError("Error") // inside retrofit onFailure
I am subscribed to a function which returns loadCompleted.
fun loadingCompleted() : Observable<List<String>>{return loadCompleted}
and then I subscribe to the loadingCompleted as
loadingCompleted.subscribe{list ->
//only called once
anotherfun(list)
}
The first time my Retrofit function gets called, I am able to get my subscribe called but subsequent calls to the same function doesn't fire the subscribe. I am assuming the calls normally return the same value because it is just a refresh and the data might not have changed. However, I still need to get the subscribe called so I can react accordingly. I have tried both BS and ReplaySubject but the result is the same. How do I use an observable to ensure I always get the subscribe called whenever I call onNext(x)/onComplete(x) even though x might not have changed?
You are probably finishing your BehaviorSubject stream with onComplete/onError. If you don't want to do that, wrap x/error to some kind of Result which is sealed class and has Success and Failure subclasses. Then always use subject.onNext() to emit.
sealed class Result<T> {
class Success<T>(val t: T) : Result<T>()
class Failure<T>(val e: Throwable) : Result<T>()
}
class Test {
val subject: BehaviorSubject<Result<List<String>>> = BehaviorSubject.create()
fun call() {
subject.onNext(Result.Success(listOf("example")))
subject.onNext(Result.Failure(RuntimeException("error")))
subject.subscribe{result->
when(result){
is Result.Success -> print(result.t)
is Result.Failure -> print(result.e.message)
}
}
}
}
My intention is to create an Observable that can be made to emit an item inside a class.
My goal is to create a ViewModel, which can expose a stream to a View in Android, reactively delivering items.
My approach is as follows:
class MyMVVM {
private lateinit var timeSelectedEmitter: ObservableEmitter<Int>
val timeSelectedObservable: Observable<Int> = Observable.create { emitter -> timeSelectedEmitter = emitter }
}
This should, in theory and as far as I understand it currently, give me access to the emitter, which then can be called at any point to deliver time-events to subscribers of timeSelectedObservable (subscribers which are inside the View)
1) Is this a correct approach or should this be done in another way?
2) Is there a way for the emitter to be created on Observable creation instead of when a subscriber is subscribing to the Observable (to avoid nullpointer exceptions or Kotlin exceptions)?
Thanks.
I usually create the Observable once and return the same Observable to anything that wants to subscribe to it. This way, if there are multiple subscribers, a new Observable doesn't need to be created each time.
So something like:
class ViewModel {
private val timeObservable = PublishSubject.create<Int>()
fun timeObservable(): Observable<Int> = timeObservable
fun update() {
// ...
timeObservable.onNext(time)
}
}
class Activity {
...
viewModel.timeObservable()
.subscribe {
time -> ...
})
}