I can receive data from network by retrofit.this code is hosted in repository class:
fun getQuestionsFromWebServer() {
val webService = AppModule()
val call = webService.provideModirfaniService().getQuestions()
call.enqueue(object : Callback<List<Question>> {
override fun onResponse(
call: Call<List<Question>>,
response: Response<List<Question>>
) {
Log.d("amin", response.body().toString())
val statusCode = response.code()
val questionList= response.body()
if(response.isSuccessful) {
if (questionList != null) {
//This is a suspend function
questionDao.updateQuestions(questionList)
}
}
}
override fun onFailure(call: Call<List<Question>>, t: Throwable) {
Log.d("amin", "failed to get questions from network")
}
})
}
Data should be inserted/updated to DB after receive. update method in DO:
#Update
suspend fun updateQuestions(list: List<Question>)
If the code was in my viewModel I could run suspend function inside viewModelScope.launch{} ,I need to know how to run a suspending function inside Retrofit onResponse method when it is in repository?
(I have not access to viewModel or Activity/Fragment instance inside repository class )
GlobalScope.launch seems to be the best fit, since you dont have access to any LifeCycleAware component. from the docs :
Global scope is used to launch top-level coroutines which are
operating on the whole application lifetime and are not cancelled
prematurely.
Please note that any coroutines launched with viewModelScope will get canceled when ViewModel is cleared, but no such cancellation happens with GlobalScope coroutines.
if that data is going to be stored for using somewhere else, you might use CoroutineScope. If data is reload and used only in the same fragment, and is no shared, use lifeCycleScope.
Related
I'm using Kotlin for the first time . I was able to make api calls in these 2 ways . What's the difference between them ?
With suspend function .
interface RetrofitInterface {
#GET("/posts")
suspend fun getUserData(): Response<List<User>>
}
I will call my api with val result = apiCaller.getUserData()
Using Call<> object
interface RetrofitInterface {
#GET("/posts")
fun getUserDataWithCall(): Call<User>
}
The api will be called via
val result2 = apiCaller.getUserDataWithCall().enqueue(object :Callback<User>{
override fun onResponse(call: Call<User>, response: Response<User>) {}
override fun onFailure(call: Call<User>, t: Throwable) {}
})
suspend is the coroutines keyword
By using Call<> you are making the Retrofit request and response asynchronous - with call.enqueue . This is the best approach since network calls can take a while and block the Main Thread.
Without using Call<> :- In this case the network operation will execute on the Main thread which will lead to blocking the Main thread. so, in order to make the network call asynchronously we mark it as a suspend function to make it so if it is used in a coroutine synchronously the main thread will not be blocked.
In order to make your code more readable, use the suspend keyword
viewModelScope.launch{
val responseFromServer = retrofitApiInterface.getUserData();
if(responseFromServer.isSuccesful){
}
}
Now your asynchronous code behaves like synchronized
However, suspending functions can only be invoked by another suspending function or within a coroutine.
I have a use case whenever generic error like internet lost or unknown error occurs in API call need to show error UI with retry button. when user press retry previously failed API should call and resume the user flow.
API interface before migrating coroutines method:
interface TodoService {
#POST("todo/create")
fun createTodo(#Body request: TodoRequest): Call<TodoResponse>
}
API client:
fun <T> fetch(call: Call<T>, completion: (result: NetworkBoundResource<T>) -> Unit) {
call.enqueue(object : Callback<T> {
override fun onFailure(call: Call<T>, t: Throwable) {
// I have the mechanism save call object and completion and show error UI
// when user press retry fetch(call.clone(), completion
}
override fun onResponse(call: Call<T>, response: Response<T>) {
}
})
}
This was not a problem before migrating retrofit interface to coroutines suspend methods. Because I can just clone the retrofit call object (call.clone()) and retry the API call as explained in the code comments.
API interface after migrating coroutines method:
interface TodoService {
#POST("todo/create")
suspend fun createTodo(#Body request: TodoRequest): TodoResponse
}
Now how can i achieve same feature without Call object?
You can handle the retry by saving the action outside of the Coroutine job e. g. adding a dispatch on a button.
Here is simple example, but not completed:
class ViewModel {
val context = CoroutineScope(Dispatchers.Main)
var dispatchRetry: (() -> Unit)? = null
fun createTodo(requestData: TodoRequest) {
context.launch() {
try {
todoService.createTodo(requestData)
} catch (t: Throwable) {
dispatchRetry = { todoService.createTodo(requestData) }
}
}
}
fun retry() {
dispatchRetry?.invoke()
}
}
use repositories and view model to get response through live data using coroutines and then in your activity user observers to get date this is the best way to use coroutines also best practice for MVVM
I want to use reactive paradigm using Kotlin Flow in my Android project. I have an external callback-based API so my choice is using callbackFlow in my Repository class.
I've already read insightfully some proper docs with no help:
callbackFlow documentation
Callbacks and Kotlin Flows by Roman Elizarov
What I want to achieve:
Currently my Repository class looks like this (simplified code):
lateinit var callback: ApiCallback
fun someFlow() = callbackFlow<SomeModel> {
callback = object : ApiCallback {
override fun someApiMethod() {
offer(SomeModel())
}
}
awaitClose { Log.d("Suspending flow until methods aren't invoked") }
}
suspend fun someUnfortunateCallbackDependentCall() {
externalApiClient.externalMethod(callback)
}
Problem occurs when someUnfortunateCallbackDependentCall is invoked faster than collecting someFlow().
For now to avoid UninitializedPropertyAccessException I added some delays in my coroutines before invoking someUnfortunateCallbackDependentCall but it is kind of hack/code smell for me.
My first idea was to use by lazy instead of lateinit var as this is what I want - lazy initialization of callback object. However, I couldn't manage to code it altogether. I want to emit/offer/send some data from someApiMethod to make a data flow but going outside of callbackFlow would require ProducerScope that is in it. And on the other hand, someUnfortunateCallbackDependentCall is not Kotlin Flow-based at all (could be suspended using Coroutines API at best).
Is it possible to do? Maybe using some others Kotlin delegates? Any help would be appreciated.
To answer your question technically, you can of course intialise a callback lazyily or with lateinit, but you can't do this AND share the coroutine scope (one for the Flow and one for the suspend function) at the same time - you need to build some kind of synchronisation yourself.
Below I've made some assumptions about what you are trying to achieve, perhaps they are not perfect for you, but hopefully give some incite into how to improve.
Since it is a Repository that you are creating, I will first assume that you are looking to store SomeModel and allow the rest of your app to observe changes to it. If so, the easiest way to do this is with a MutableStateFlow property instead of a callbackFlow:
interface Repository {
val state: Flow<SomeModel>
suspend fun reload()
}
class RepositoryImpl(private val service: ApiService) : Repository {
override val state = MutableStateFlow(SomeModel())
override suspend fun reload() {
return suspendCoroutine { continuation ->
service.callBackend(object : ApiCallback {
override fun someApiMethod(data: SomeModel) {
state.value = data
if (continuation.context.isActive)
continuation.resume(Unit)
}
})
}
}
}
interface ApiCallback {
fun someApiMethod(data: SomeModel)
}
data class SomeModel(val data: String = "")
interface ApiService {
fun callBackend(callback: ApiCallback)
}
The downside to this solution is that you have to call reload() in order to actually make a call to your backend, collecting the Flow alone is not enough.
myrepository.state.collect {}
myrepository.reload()
Another solution, again depending on what exactly you are trying to achieve, is to provide two ways to call your backend:
interface Repository {
fun someFlow(): Flow<SomeModel>
suspend fun reload(): SomeModel
}
class RepositoryImpl(private val service: ApiService) : Repository {
override fun someFlow() = callbackFlow<SomeModel> {
service.callBackend(object : ApiCallback {
override fun someApiMethod(data: SomeModel) {
offer(data)
}
})
awaitClose {
Log.d("TAG", "Callback Flow is closed")
}
}
override suspend fun reload(): SomeModel {
return suspendCoroutine<SomeModel> { continuation ->
service.callBackend(object : ApiCallback {
override fun someApiMethod(data: SomeModel) {
if (continuation.context.isActive)
continuation.resume(data)
}
})
}
}
}
interface ApiCallback {
fun someApiMethod(data: SomeModel)
}
data class SomeModel(val data: String = "")
interface ApiService {
fun callBackend(callback: ApiCallback)
}
Now you can either call reload() or someFlow() to retrieve SomeModel() and the Repository holds no "state".
Note that the reload() function is simply a 'coroutine' version of the callbackFlow idea.
I have developed 2 functions for the login.
The first "loginOne" works when I use the ViewModel scope.
The other one doesn't work when I use the LiveData scope.
Do you have an idea? I want to make "loginTwo" work.
API
interface LoginAPI {
#POST("login")
suspend fun getUser(#Body loginRequest: LoginRequest): User
}
Repository
class LoginRepository(private val loginAPI: LoginAPI) {
suspend fun getUser(loginRequest: LoginRequest) = loginAPI.getUser(loginRequest)
}
ViewModel
class LoginViewModel(private val loginRepository: LoginRepository) : ViewModel() {
private var user: LiveData<User>? = null
fun loginOne(username: String, password: String) {
viewModelScope.launch {
// i can enter here and get the user :)
val user = loginRepository.getUser(LoginRequest(username, password))
user
}
}
fun loginTwo(username: String, password: String) {
user = liveData(Dispatchers.IO) {
// i never enter inside.. why ?
val user = loginRepository.getUser(LoginRequest(username, password))
emit(user)
}
}
fun getUser(): LiveData<User>? = user
}
Fragment, my viewModel is injected with Koin
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
loginViewModel.getUser()?.observe(this, Observer { user ->
Log.d(LoginFragment::class.java.name, "User : $user ")
})
loginViewModel.loginOne("user","pcw123")
loginViewModel.loginTwo("user","pcw123")
}
Make sure that you created Scope in the right way. Also, that you are using appropriate Dispatchers to achieve wanted results.
You can additionally check if the call is being executed when you wanted to postValue.
Check if Job is still alive.
Check this thing.
Your emmit call looks suspicious.
When using LiveData, you might need to calculate values asynchronously. For example, you might want to retrieve a user's preferences and serve them to your UI. In these cases, you can use the liveData builder function to call a suspend function, serving the result as a LiveData object.
Each emit() call suspends the execution of the block until the LiveData value is set on the main thread.
In the example below, loadUser() is a suspend function declared elsewhere. Use the liveData builder function to call loadUser() asynchronously, and then use emit() to emit the result:
val user: LiveData<User> = liveData {
val data = database.loadUser() // loadUser is a suspend function.
emit(data)
}
EDIT: MutableLiveData for user variable - resolved the issue.
From the documentation:
The liveData building block serves as a structured concurrency
primitive between coroutines and LiveData. The code block starts
executing when LiveData becomes active and is automatically canceled
after a configurable timeout when the LiveData becomes inactive.
So, in your case, the 'user' liveData is already activated when you observing it from fragment. Because you called loginTwo() after liveData has been observed, the emit function will not triggered anymore. Try to call loginTwo() before observing liveData to get emit value from liveData ktx.
I'm trying out a basic implementation of Architecture Component's Live Data with Kotlin like this:
class MarketFragment : LifecycleFragment(){
......
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel=ViewModelProviders.of(this).get(MarketViewModel::class.java)
viewModel.book?.observe(this, Observer { book-> //updateUI })
....
My ViewModel class is created like this:
class MarketViewModel : ViewModel()
{
var book: MutableLiveData<Book>? =MutableLiveData()
var repository: Repository?= Repository()
init {
update("parameter")
}
fun update(s: String)
{
book=repository?.getBook(s)
}
}
And My Repository:
fun getBook(bookSymbol:String):MutableLiveData<Book>
{
val book=MutableLiveData<Book>()
......
call . enqueue (object : Callback<Book> {
override fun onResponse(call: Call<Book>?, response: Response<Book>?) {
book.value=response?.body()
}
.....
})
return book
}
}
And all of this works great and UI is updated as it should but only for the first time. If i try to make manual calls to update the viewModel from a UI action, the retrofit call still works as expected but the new data is not sent to the Observer in the Fragment:
//this doesn't work:
viewModel.update("string")
//This returns false:
viewModel.book.hasActiveObservers()
Is it the expected behaviour for an Observer to become inactive after the first trigger?
You are creating a new MutableLiveData instance each time you are calling getBooks
Hence your observer is not observing the right LiveData anymore.
To solve this
Your ViewModel should keep only one (immutable) LiveData instance
That immutable LiveData instance could either be:
A MediatorLiveData, which source is the repository's LiveData
A transformation of the repository's LiveData
That implies the repository method getBooks is only called once on initialization of the ViewModel, and that either refreshes itself OR you have to call another method on the repository to trigger a refresh.