Retrofit's `enqueue()` doesn't re-assign value of object in time for the return statement? - kotlin

I'm making a call to the API and the response body is assigned to an object inside Retrofit's enqueue(), the problem is that enqueue finishes too quickly for the value to be assigned before the return statement of the function body is called.
Previously, I was using MutableLiveData before and it took care of that because it's always observing the data and when it changes it assigns it with no problem but now I don't want to use any MutableLiveData or Observables because I'm trying to prepare the data before any UI is actually drawn on the screen.
fun getResponse(
weatherLocationCoordinates: WeatherLocation
): RequestResponse {
weatherApiService.getCurrentWeather(
weatherLocationCoordinates.latitude,
weatherLocationCoordinates.longitude
).enqueue(object : Callback<WeatherResponse> {
override fun onResponse(
call: Call<WeatherResponse>,
response: Response<WeatherResponse>
) {
if (response.isSuccessful) {
// This where I do the assigning
requestResponse = RequestResponse(response.body(), true)
}
}
override fun onFailure(call: Call<WeatherResponse>, t: Throwable) {
requestResponse = RequestResponse(null, false)
}
})
// When this is called, enqueue is still not finished
// therefore I get the wrong value, I get the previously set initialization value of the obj.
return requestResponse
}
Should I be using Callbacks or something else? I'm not sure on how to implement the callback.

Following up on the comments here's an approach with callbacks:
Let's suppose we change the method signature to:
fun getResponse(
weatherLocationCoordinates: WeatherLocation,
onSuccess: (WeatherResponse) -> Unit = {},
onError: (Throwable) -> Unit = {}
) {
weatherApiService.getCurrentWeather(
weatherLocationCoordinates.latitude,
weatherLocationCoordinates.longitude
).enqueue(object : Callback<WeatherResponse> {
override fun onResponse(
call: Call<WeatherResponse>,
response: Response<WeatherResponse>
) {
if (response.isSuccessful) {
onSuccess(response.body())
} else {
onError(CustomHttpExceptionWithErrorDescription(response))
}
}
override fun onFailure(call: Call<WeatherResponse>, t: Throwable) {
onError(t)
}
})
}
CustomHttpExceptionWithErrorDescription will have to be something you code that can simply parse the error gotten from the server. Anything that is not 2XX status code
This method accepts 2 extra parameters - one gets called upon success the other on error. The idea is to call it like:
getResponse(
weatherLocationCoordinates,
onSuccess = {
// do something with response
},
onError = {
// do something with the error
}
)
Because they have default parameters you actually don't need to specify both callbacks. Just the one you are interested in. Examples:
// react only to successes
getResponse(
weatherLocationCoordinates,
onSuccess = {
// do something with response
}
)
// react only to errors
getResponse(weatherLocationCoordinates) {
// do something with the error
}
// just call the network calls and don't care about success or error
getResponse(weatherLocationCoordinates)

Related

Kotlin flows SharedFlow not received in collectInLifeCycle inside Fragment

I am observing inside a fragment the events of a sharedflow such as this:
myEvent.collectInLifeCycle(viewLifecycleOwner) { event ->
when (state) {
//check the event. The event emited form onStart is never reached here :(
}
}
Whereas in the viewmodel I have
private val _myEvent = MutableSharedFlow<MyEvent>()
val myEvent: SharedFlow<MyEvent> = _myEvent
fun loadData() =
viewModelScope.launch {
getDataUseCase
.safePrepare(onGenericError = { _event.emit(Event.Error(null)) })
.onStart { _event.emit(Event.Loading) }
.onEach { result ->
result.onSuccess { response ->
_event.emit(Event.Something)
}
}
.launchIn(viewModelScope)
}
So the problem is that only the Event.Something is the one being properly collected from the fragment, whereas _event.emit(Event.Loading) is not being collected... If I debug it goes to the onStart, but it is never called in the fragment.
Your SharedFlow needs to have a replay so that collectors always get at least the most recent value. Otherwise, if you emit to the Flow before the collector is registered, it will never see anything emitted. Do this:
private val _myEvent = MutableSharedFlow<MyEvent>(replay = 1)
Personally, unless I'm missing some detail here that would change my mind, I would simplify all your code to avoid having to manually call loadData(). Something like this but I'm guessing a bit because I don't know all your types and functions.
val myEvent: SharedFlow<MyEvent> = flow {
emit(Event.Loading)
emitAll(
getDataUseCase
.transform { result ->
result.onSuccess { response ->
emit(Event.Something)
}
}
.catch { error -> emit(Event.Error(null)) }
)
}.shareIn(viewModelScope, SharingStarted.Lazily, replay = 1)

Getting data from Datastore for injection

I am trying to retrieve the base url from my proto datastore to be used to initialize my ktor client instance I know how to get the data from the datastore but I don't know how to block execution until that value is received so the client can be initialized with the base url
So my ktor client service asks for a NetworkURLS class which has a method to return the base url
Here is my property to retrieve terminalDetails from my proto datastore
val getTerminalDetails: Flow<TerminalDetails> = cxt.terminalDetails.data
.catch { e ->
if (e is IOException) {
Log.d("Error", e.message.toString())
emit(TerminalDetails.getDefaultInstance())
} else {
throw e
}
}
Normally when I want to get the values I would do something like this
private fun getTerminalDetailsFromStore() {
try {
viewModelScope.launch(Dispatchers.IO) {
localRepository.getTerminalDetails.collect {
_terminalDetails.value = it
}
}
} catch(e: Exception) {
Log.d("AdminSettingsViewModel Error", e.message.toString()) // TODO: Handle Error Properly
}
}
but in my current case what I am looking to do is return terminalDetails.backendHost from a function and that where the issue comes in I know I need to use a coroutine scope to retrieve the value so I don't need to suspend the function but how to a prevent the function returning until the coroutine scope has finished?
I have tried using async and runBlocking but async doesn't work the way I would think it would and runBlocking hangs the entire app
fun backendURL(): String = runBlocking {
var url: String = "localhost"
val job = CoroutineScope(Dispatchers.IO).async {
repo.getTerminalDetails.collect {
it.backendHost
}
}
url
}
Can anyone give me some assistance on getting this to work?
EDIT: Here is my temporary solution, I do not intend on keeping it this way, The issue with runBlocking{} turned out to be the Flow<T> does not finish so runBlocking{} continues to block the app.
fun backendURL(): String {
val details = MutableStateFlow<TerminalDetails>(TerminalDetails.getDefaultInstance())
val job = CoroutineScope(Dispatchers.IO).launch {
repo.getTerminalDetails.collect {
details.value = it
}
}
runBlocking {
delay(250L)
}
return details.value.backendHost
}
EDIT 2: I fully fixed my issue. I created a method with the same name as my val (personal decision) which utilizes runBlocking{} and Flow<T>.first() to block while the value is retrieve. The reason I did not replace my val with the function is there are places where I need the information as well where I can utilize coroutines properly where I am not initializing components on my app
val getTerminalDetails: Flow<TerminalDetails> = cxt.terminalDetails.data
.catch { e ->
if (e is IOException) {
Log.d("Error", e.message.toString())
emit(TerminalDetails.getDefaultInstance())
} else {
throw e
}
}
fun getTerminalDetails(): TerminalDetails = runBlocking {
cxt.terminalDetails.data.first()
}

LiveData Observer isn't triggered for the second time

I'm expecting that the observer will be triggered when I'm hitting API by clicking one of the side menu. When I clicked one of the menu, Retrofit actually gave me the response with the correct value. The problem is, the Observer isn't getting triggered for the second time. I've trace the problem and find out that my Repository isn't returning a value even though my Retrofit already update the MutableLiveData.
RemoteDataSource.kt
override fun getDisastersByFilter(filter: String?): LiveData<ApiResponse<DisastersDTO?>> {
val result = MutableLiveData<ApiResponse<DisastersDTO?>>()
apiService.getDisastersByFilter(filter).enqueue(object : Callback<DisastersResponse> {
override fun onResponse(
call: Call<DisastersResponse>,
response: Response<DisastersResponse>
) {
if(response.isSuccessful) {
val data = response.body()
data?.disastersDTO?.let {
result.postValue(ApiResponse.Success(it))
Log.d("RemoteDataSource", "$it")
} ?: run {
result.postValue(ApiResponse.Error("Bencana alam tidak ditemukan"))
}
} else {
result.postValue(ApiResponse.Error("Terjadi kesalahan!"))
}
}
override fun onFailure(call: Call<DisastersResponse>, t: Throwable) {
result.postValue(ApiResponse.Error(t.localizedMessage!!))
Log.d("RemoteDataSource", t.localizedMessage!!)
}
})
return result
}
Repository.kt
override fun getDisastersByFilter(filter: String?): LiveData<Resource<List<Disaster>>> =
remoteDataSource.getDisastersByFilter(filter).map {
when (it) {
is ApiResponse.Empty -> Resource.Error("Terjadi error")
is ApiResponse.Error -> Resource.Error(it.errorMessage)
is ApiResponse.Loading -> Resource.Loading()
is ApiResponse.Success -> Resource.Success(
DataMapper.disastersResponseToDisasterDomain(
it.data
)
)
}
}
SharedViewModel.kt
fun getDisastersByFilter(filter: String? = "gempa"): LiveData<Resource<List<Disaster>>> =
useCase.getDisastersByFilter(filter)
Here's the **MapsFragment**
private val viewModel: SharedViewModel by activityViewModels()
viewModel.getDisastersByFilter("gempa").observe(viewLifecycleOwner) {
when (it) {
is Resource.Success -> {
Log.d("MapsFragmentFilter", "${it.data}")
it.data?.let { listDisaster ->
if(listDisaster.isNotEmpty()) {
map.clear()
addGeofence(listDisaster)
listDisaster.map { disaster ->
placeMarker(disaster)
addCircle(disaster)
}
}
}
}
is Resource.Error -> Toast.makeText(context, "Filter Error", Toast.LENGTH_SHORT).show()
is Resource.Loading -> {}
}
}
Here's the MainActivity that triggers the function to hit API
private val viewModel: SharedViewModel by viewModels()
binding.navViewMaps.setNavigationItemSelectedListener { menu ->
when (menu.itemId) {
R.id.filter_gempa -> viewModel.getDisastersByFilter("gempa")
R.id.filter_banjir -> viewModel.getDisastersByFilter("banjir")
R.id.about_us -> viewModel.getDisasters()
}
binding.drawerLayoutMain.closeDrawers()
true
}
I can't be sure from what you've posted, but your menu options call getDisastersByFilter on your SharedViewModel, and it looks like that eventually calls through to getDisastersByFilter in RemoteDataSource.
That function creates a new LiveData and returns it, and all your other functions (including the one in viewModel) just return that new LiveData. So if you want to see the result that's eventually posted to it, you need to observe that new one.
I don't know where the fragment code you posted is from, but it looks like you're just calling and observing viewModel.getDisastersByFilter once. So when that first happens, it does the data fetch and you get a result on the LiveData it returned. That LiveData won't receive any more results, from the looks of your code - it's a one-time, disposable thing that receives a result later, and then it's useless.
If I've got that right, you need to rework how you're handling your LiveDatas. The fragment needs to get the result of every viewModel.getDisastersByFilter call, so it can observe the result - it might be better if your activity passes an event to the fragment ("this item was clicked") and the fragment handles calling the VM, and it can observe the result while it's at it (pass it to a function that wires that up so you don't have to keep repeating your observer code)
The other approach would be to have the Fragment observe a currentData livedata, that's wired up to show the value of a different source livedata. Then when you call getDisastersByFilter, that source livedata is swapped for the new one. The currentData one gets any new values posted to this new source, and the fragment only has to observe that single LiveData once. All the data gets piped into it by the VM.
I don't have time to do an example, but have a look at this Transformations stuff (this is one of the developers' blogs): https://medium.com/androiddevelopers/livedata-beyond-the-viewmodel-reactive-patterns-using-transformations-and-mediatorlivedata-fda520ba00b7
What I believe you are doing wrong is using LiveData in the first place while using a retrofit.
You are getting a response asynchronously while your code is running synchronously. So, you need to make use of suspending functions by using suspend.
And while calling this function from ViewModel, wrap it with viewModelScope.launch{}
fun getDisastersByFilter(filter: String? = "gempa") = viewModelScope.launch {
useCase.getDisastersByFilter(filter).collect{
// do something....
// assign the values to MutableLiveData or MutableStateFlows
}
}
You should either be using RxJava or CallbackFlow.
I prefer Flows, given below is an example of how your code might look if you use callback flow.
suspend fun getDisastersByFilter(filter: String?): Flow<ApiResponse<DisastersDTO?>> =
callbackFlow {
apiService.getDisastersByFilter(filter)
.enqueue(object : Callback<DisastersResponse> {
override fun onResponse(
call: Call<DisastersResponse>,
response: Response<DisastersResponse>
) {
if (response.isSuccessful) {
val data = response.body()
data?.disastersDTO?.let {
trySend(ApiResponse.Success(it))
// result.postValue(ApiResponse.Success(it))
Log.d("RemoteDataSource", "$it")
} ?: run {
trySend(ApiResponse.Error("Bencana alam tidak ditemukan"))
// result.postValue(ApiResponse.Error("Bencana alam tidak ditemukan"))
}
} else {
trySend(ApiResponse.Error("Terjadi kesalahan!"))
// result.postValue(ApiResponse.Error("Terjadi kesalahan!"))
}
}
override fun onFailure(call: Call<DisastersResponse>, t: Throwable) {
trySend(ApiResponse.Error(t.localizedMessage!!))
// result.postValue(ApiResponse.Error(t.localizedMessage!!))
Log.d("RemoteDataSource", t.localizedMessage!!)
}
})
awaitClose()
}

Create a callback function with another callback inside

My apologies for the bad title, I'm fairly new to callbacks and I'm not sure how to explain what I'm trying to achieve.
I have a class called MyClass that has a function connectToService inside of it.
The function connectToService does some calculations and then calls a function with a callback, like this:
fun connectToService() {
//Whatever calculations
val a = 7
var b = 3
var c = a + b
val token = MyToken()
token.actionCallback = object: SuperSecretObject {
override fun onSuccess(asyncActionToken: MyToken) {
c++
}
override fun onFailure(asyncActionToken: MyToken) {
c--
}
}
}
I want to create another class, YourClass which creates an object of MyClass and then calls the connectToService function. When the connectToService function finishes either the onSuccess or onFailurefunctions, I want to do something depending on which one was triggered (something different each time, thats why I can't put it inside the onSuccess or onFailure blocks of code).
Something like this:
//Inside `yourClass`
private fun myFunc() {
val yourClassObj = YourClass()
youClassObj.connectToService {
if(onSuccess)
reinventTheWheel()
else
squareIt()
}
youClassObj.connectToService {
combAWatermelon()
}
youClassObj.connectToService {
sharpenMyHammer()
}
}
Is this possible? If so, how can I achieve it? If it's not, what would be the closest solution to this requirement?
EDIT:
More detailed information has been requested, so while I can't provide exact details, I'll do my best to explain what's going on.
I'm basically working on a library to simplify petitions. For example, MQTT petitions. This is something tht resembles what I want to achieve:
/**
* Subscribes to a list of topics and handles the results
*/
fun subscribe(client: MqttAndroidClient, list: MutableList<String>, onMsg: ((String, MqttMessage)->Unit)?=null, conLost: ((Throwable)->Unit)?=null, delComp: ((IMqttDeliveryToken)->Unit)?=null) {
if (client.isConnected) { //Assert connection
for(x in list.iterator()) { //Subscribe to events
client.subscribe(x, 0)
}
client.setCallback(object : MqttCallback {
override fun connectionLost(cause: Throwable) { //Lost connection
Log.i("TAG", "Connection lost")
conLost?.let { it(cause) }
}
#Throws(java.lang.Exception::class)
override fun messageArrived(topic: String, message: MqttMessage) { //Arrived message
Log.i("TAG", "Message arrived: topic => $topic, message => $message")
onMsg?.let { it(topic, message) }
}
override fun deliveryComplete(token: IMqttDeliveryToken) { //Delivery complete
Log.i("TAG", "Delivery complete")
delComp?.let { it(token) }
}
})
}
}
The messageArrived function must have a behaviour that can be customized depending on the app it's being used on.
For example, on one app I want the onMsg() function to be like this:
when(topic) {
"firstTopic" -> {
localVariable++
}
"secondTopic" -> {
localMethod()
}
"thirdTopic" -> {
localClass.variable.method()
}
}
If I'm using it on an Android device, I'd like to be able to update the interface, doing Android API calls, etc.
I'm not sure I got your question correctly. I think what you are looking for is passing lambdas.
fun connectToService(onSucc: ()->Unit, onFail: ()->Unit) {
//Whatever calculations
MyToken().actionCallback = object: SuperSecretObject {
override fun onSuccess(asyncActionToken: MyToken) {
onSucc()
}
override fun onFailure(asyncActionToken: MyToken) {
onFail()
}
}
}
Then you can call the function like this:
connectToService({ /* Something */ }, { /* Something else */ })

Android Kotlin Coroutines: what is the difference between flow, callbackFlow, channelFlow,... other flow constructors

I have code that should change SharedPreferences into obsarvable storage with flow so I've code like this
internal val onKeyValueChange: Flow<String> = channelFlow {
val callback = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
coroutineScope.launch {
//send(key)
offer(key)
}
}
sharedPreferences.registerOnSharedPreferenceChangeListener(callback)
awaitClose {
sharedPreferences.unregisterOnSharedPreferenceChangeListener(callback)
}
}
or this
internal val onKeyValueChange: Flow<String> = callbackFlow {
val callback = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
coroutineScope.launch {
send(key)
//offer(key)
}
}
sharedPreferences.registerOnSharedPreferenceChangeListener(callback)
awaitClose {
sharedPreferences.unregisterOnSharedPreferenceChangeListener(callback)
}
}
Then I observe this preferences for token, userId, companyId and then log into but there is odd thing as I need to build app three times like changing token not causes tokenFlow to emit anything, then second time new userId not causes userIdFlow to emit anything, then after 3rd login I can logout/login and it works. On logout I am clearing all 3 properties stores in prefs token, userId, companyId.
For callbackFlow:
You cannot use emit() as the simple Flow (because it's a suspend function) inside a callback. Therefore the callbackFlow offers you a synchronized way to do it with the trySend() option.
Example:
fun observeData() = flow {
myAwesomeInterface.addListener{ result ->
emit(result) // NOT ALLOWED
}
}
So, coroutines offer you the option of callbackFlow:
fun observeData() = callbackFlow {
myAwesomeInterface.addListener{ result ->
trySend(result) // ALLOWED
}
awaitClose{ myAwesomeInterface.removeListener() }
}
For channelFlow:
The main difference with it and the basic Flow is described in the documentation:
A channel with the default buffer size is used. Use the buffer
operator on the resulting flow to specify a user-defined value and to
control what happens when data is produced faster than consumed, i.e.
to control the back-pressure behavior.
The trySend() still stands for the same thing. It's just a synchronized way (a non suspending way) for emit() or send()
I suggest you to check Romans Elizarov blog for more detailed information especially this post.
Regarding your code, for callbackFlow you wont' be needing a coroutine launch:
coroutineScope.launch {
send(key)
//trySend(key)
}
Just use trySend()
Another Example, maybe much concrete:
private fun test() {
lifecycleScope.launch {
someFlow().collectLatest {
Log.d("TAG", "Finally we received the result: $it")
// Cancel this listener, so it will not be subscribed anymore to the callbackFlow. awaitClose() will be triggered.
// cancel()
}
}
}
/**
* Define a callbackFlow.
*/
private fun someFlow() = callbackFlow {
// A dummy class which will run some business logic and which will sent result back to listeners through ApiCallback methods.
val service = ServiceTest() // a REST API class for example
// A simple callback interface which will be called from ServiceTest
val callback = object : ApiCallback {
override fun someApiMethod(data: String) {
// Sending method used by callbackFlow. Into a Flow we have emit(...) or for a ChannelFlow we have send(...)
trySend(data)
}
override fun anotherApiMethod(data: String) {
// Sending method used by callbackFlow. Into a Flow we have emit(...) or for a ChannelFlow we have send(...)
trySend(data)
}
}
// Register the ApiCallback for later usage by ServiceTest
service.register(callback)
// Dummy sample usage of callback flow.
service.execute(1)
service.execute(2)
service.execute(3)
service.execute(4)
// When a listener subscribed through .collectLatest {} is calling cancel() the awaitClose will get executed.
awaitClose {
service.unregister()
}
}
interface ApiCallback {
fun someApiMethod(data: String)
fun anotherApiMethod(data: String)
}
class ServiceTest {
private var callback: ApiCallback? = null
fun unregister() {
callback = null
Log.d("TAG", "Unregister the callback in the service class")
}
fun register(callback: ApiCallback) {
Log.d("TAG", "Register the callback in the service class")
this.callback = callback
}
fun execute(value: Int) {
CoroutineScope(Dispatchers.IO).launch {
if (value < 2) {
callback?.someApiMethod("message sent through someApiMethod: $value.")
} else {
callback?.anotherApiMethod("message sent through anotherApiMethod: $value.")
}
}
}
}