I would like to have a system to call API (Retrofit) with cache (in Room), with just coroutines (without LiveData and NetworkBoundResource).
So worflow is:
Check data in db
if present show it
if not:
Call API
Save data in db
show data
Problem app blocked in "Call API" step, here the stack
nativePollOnce:-1, MessageQueue (android.os) next:326, MessageQueue
(android.os) loop:160, Looper (android.os) main:6669, ActivityThread
(android.app) invoke:-1, Method (java.lang.reflect) run:493,
RuntimeInit$MethodAndArgsCaller (com.android.internal.os) main:858,
ZygoteInit (com.android.internal.os)
Retrofit service:
interface ProductService {
#GET("products")
suspend fun getProducts(): Response<List<Product>>
}
DAO Room:
#Dao
interface ProductDao {
#Query("SELECT * FROM Product ORDER BY price")
suspend fun getProducts(): List<Product>
#Transaction
#Insert(entity = Product::class)
suspend fun insertProducts(products: List<Product>)
}
My fragment:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
productService = createProductService()
productDao = MyDatabase.getDatabase(requireContext()).productDao()
CoroutineScope(Dispatchers.Main).launch {
getProducts()
}
}
private suspend fun getProducts() {
progressBar.visibility = View.VISIBLE
recyclerViewProducts.visibility = View.GONE
var products = withContext(Dispatchers.IO){ productDao.getProducts() }
if(products.isEmpty()) {
val response = withContext(Dispatchers.IO) { productService.getProducts() }
if(response.isSuccessful && response.body() != null) {
products = response.body()!!
withContext(Dispatchers.IO) { productDao.insertProducts(products) }
}
}
withContext(Dispatchers.Main) {
progressBar.visibility = View.GONE
recyclerViewProducts.visibility = View.VISIBLE
recyclerViewProducts.apply {
layoutManager = LinearLayoutManager(context)
// set the custom adapter to the RecyclerView
adapter = ProductsAdapter(products, this#ListProductFragment)
}
}
}
This is not clean architecture. You should have a Database layer (that you have) and a Repository and a Viewmodel. So when framgnet is created calls viewmodel to observe data from repository that also observe data from Db. If data from db is empty then it creates the api call in a coroutine scope and in the same thread it saves data to DB. So automatically viewmodel gets informed for new data
I recommend using MVVM Design Pattern. You must do what you want in the repository pattern.
The repository pattern is a design pattern that isolates data sources
from the rest of the app. A repository mediates between data sources
(such as persistent models, web services, and caches) and the rest of
the app. The diagram below shows how app components such as activities
that use LiveData might interact with data sources by way of a
repository. To implement a repository, you use a repository class,
such as the VideosRepository class that you create in the next task.
The repository class isolates the data sources from the rest of the
app and provides a clean API for data access to the rest of the app.
Using a repository class is a recommended best practice for code
separation and architecture. Advantages of using a repository. A
repository module handles data operations and allows you to use
multiple backends. In a typical real-world app, the repository
implements the logic for deciding whether to fetch data from a network
or use results that are cached in a local database. This helps make
your code modular and testable. You can easily mock up the repository
and test the rest of the code.
I suggest you check it out for the error code.
Control
Related
I'm trying to use MVVM with ViewModel, ViewBinding, Retrofit2 in Android/Kotlin.
I don't know how to move application logic from ViewModel. I can't just move methods with logic because they run on viewModelScope and put results into observable objects in my ViewModel.
Or maybe I can?
For example I have some ArrayList (to show on some ListView).
// items ArrayList
private val _stocktakings =
MutableLiveData<ArrayList<InventoryStocktakingWithCountsDto?>>(ArrayList())
val stocktakings : LiveData<ArrayList<InventoryStocktakingWithCountsDto?>> get() =
_stocktakings
// selected item
private val _selected_stocktaking = MutableLiveData<Int>>
val selected_stocktaking : LiveData<Int> get() = _selected_stocktaking
And a function that is called from my fragment:
public fun loadStocktakings() {
viewModelScope.launch {
Log.d(TAG, "Load stocktakings requested")
clearError()
try {
with(ApiResponse(ApiAdapter.apiClient.findAllStocktakings())){
if (isSuccessful && body != null){
Log.d(TAG, "Load Stocktakings done")
setStocktakings(body)
} else {
val e = "Can't load stocktakings, API error: {${errorMessage}}"
Log.e(TAG, e)
HandleError("Can't load stocktakings, API error: {${e}}") // puts error message into val lastError MutableLiveData...
}
}
} catch (t : Throwable) {
Log.e(TAG, "Can't load stocktakings, connectivity error: ${t.message}")
HandleError("Can't load stocktakings, API error: {${e}}") // puts error message into val lastError MutableLiveData...
}
}
}
Now I want to add another function that changes some field in one of stocktakings. Maybe something like:
public fun setSelectedStocktakingComplete() {
stocktakings.value[selected_stocktaking.value].isComplete = true;
// call some API function... another 15 lines of code?
}
How to do it properly?
I feel I have read wrong tutorials... This will end with fat viewmodel cluttered with viewModelScope.launch, error handling and I can't imagine what will happen when I start adding data/form validation...
Here, some tip for that
Make sure the ViewModel is only responsible for holding and managing
UI-related data.
Avoid putting business logic in the ViewModel. Instead, encapsulate
it in separate classes, such as Repository or Interactor classes.
Use LiveData to observe data changes in the ViewModel and update the
UI accordingly.
Avoid making network or database calls in the ViewModel. Instead,
use the Repository pattern to manage data operations and provide the
data to the ViewModel through a LiveData or other observable object.
Make sure the ViewModel does not hold context references, such as
Activity or Fragment.
Use a ViewModel factory to provide dependencies to the ViewModel, if
necessary.
you can ensure that your ViewModel is simple, easy to test,
and scalable. It also makes it easier to maintain your codebase, as
the business logic is separated from the UI logic.
hope you understand
I have an repository that contains an in-memory cache list inside a StateFlow. The problem is that whenever the user logs out and logs into another account, the old data from the previous user is still there.
object Repository {
private lateinit var remoteDataSource: RemoteDataSource
operator fun invoke(remoteDataSource: remoteDataSource) {
this.remoteDataSource = remoteDataSource
return this
}
private val myList = MutableStateFlow(listOf<myData>())
suspend fun getData(): Flow<List<myData>> =
withContext(Dispatchers.IO) {
if (myList.value.isEmpty()) {
val response = remoteDataSource.getData()
if (response != null) {
myList.value = response.map { it.toMyData() }
}
}
myList
}
suspend fun addData(newData: MyData) =
withContext(Dispatchers.IO) {
myList.value = myList.value.plus(newData)
remoteDataSource.addData(myData.toMyDataRequest())
}
}
This repository is used by multiple ViewModels. The list itself is only observed by one screen (let's call it myFragment), but other screens can add new elements to it. I've tried to clear the repository on myFragment's onDestroyView, but it clears the list whenever the user navigates away from myFragment (even when it's not a logout).
We could observe whenever the user logs out in an userRepository, but i don't know how to observe data in one repository from another repository (there's nothing like viewModelScope.launch to collect flows or something like that).
What approach can be used to solve this? And how would it clear the list?
i don't know how to observe data in one repository from another repository
I'd argue you shouldn't in this case.
You have a use-case: Logout.
When you invoke this use-case, you should perform al the necessary operations that your app requires. In this case, you should call your repository to let it know.
suspend fun clearData() =
withContext(Dispatchers.IO) {
// clear your data
}
I'd argue that you shouldn't hardcode the Dispatcher, since you'll likely test this at some point; in your tests you're going to use TestDispatcher or similar, and if you hardcode it, it will be harder to test. You write tests, right?
So now your use case..
class LogoutUseCase(repo: YourRepo) {
operator fun invoke() {
repo.clearData()
//do the logout
}
}
That's how I would think about this.
Your scope for all this is the UI that initiated the logout...
I am building a client application which uses Firebase for two things:
User Authentication
Using a realtime database
I have managed to set up everything correctly on my client and on my backend server (using Firebase's Admin SDK) and am able to correctly authenticate users and allow them to read/write to the database.
I am also using Retrofit2 to send requests from the client to the backend.
As part of allowing users access to the database, it is needed to send the user's token to the backend so the user can be verified.
To do this, I have the following logic:
val user = FirebaseAuth.getInstance().currentUser
if (user != null) {
user.getIdToken(false).addOnCompleteListener {
if (it.isSuccessful) {
val token = it.result?.token
//retrofit logic to send request happens from here
}
}
As you can see, getting the Id token of the user is an asynchronous call and in the current code base that I have, I have this code block for each one of my calls to the backend (duplication).
I want to know how I can export this snippet to a function (maybe a suspend method?) so that it can be reused for every call to the backend
I have searched online and have seen many SO questions, but none that fit this scenario.
I have thought about passing in a callback, but I have several methods that communicate to the backend, and each of them will require a different callback method.
The solution I am looking for looks something like this:
fun fetchDataFromDB() {
getIdTokenForUser()
//wait till it finishes and then
//perform request to DB
}
fun updateDataInDB() {
getIdTokenForUser()
//wait till it finishes and then
//perform request to DB
}
//......
I have tried reading about and implementing coroutines, but I lack the knowledge to do so correctly.
EDIT
Thanks to #Doug Stevenson for his answer and direction, I have managed to construct the following:
private suspend fun getUserIdToken(user: FirebaseUser) = coroutineScope {
val job = async {
user.getIdToken(false).result?.token
}
job.await()
}
And I use it in this fashion:
fun updateDB(context: Context) = runBlocking {
val user = FirebaseAuth.getInstance().currentUser
if (user != null) {
val token = getUserIdToken(user)
}
}
Is this the correct approach? Since the answers given below present a different implementation.
getIdToken is asynchronous returns a Task object. If you want to use a Task object in a Kotlin coroutine, you can use the library kotlinx-coroutines-play-services to add an extension method await() to the Task that makes it usable in a coroutine. With that, you can write something like this:
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.3.9"
import kotlinx.coroutines.tasks.await
suspend fun getIdTokenForUser(user: FirebaseUser): GetTokenResult {
return try {
user.getIdToken(false).await()
}
catch (e: Exception) {
// handle error
}
}
You might have to update the types here - I didn't try to compile or test this.
See also:
Android kotlin task to be executed using coroutines
Coroutines And Firebase: How to Implement Javascript-like Promise.all()
Using Firebase with Kotlin coroutines
In order to go from a callback based API like the following one:
val myCallback = object : ServiceCallback() {
override fun onResult(theobject: Something) {
// your callback code here
}
override fun onFailure(ex: Throwable) {
// error handling
}
}
theService.enqueue(callback)
You can use suspendCoroutine
What it does is that it suspends execution until the continuation is satified by the callback. So you can write a KTX like the following:
suspend fun Service.getSomething(): Something = suspendCoroutine{ cont ->
val callback = object : ServiceCallback(){
override fun onSuccess(data: Something): Unit = cont.resume(data)
override fun onFailure(ex: Throwable): Unit = cont.resume(ex)
}
this.enqueue(callback)
}
There is a lot of information out there on architecture components, kotlin and coroutines but nowhere I can find an example using all those things together.
I'm struggling on how to use android's architecture components as described here together with coroutines. I have an idea but feel uncertain if it's the correct way of implementating this architectural style.
I'm trying to use the view model + repository pattern together with retro fit and coroutines.
I have the following repository:
class FooRepostiroy(private val fooHttpService: FooHttpService) {
suspend fun someMethod() : SomeResult {
val response = fooHttpService.someRemotCall() // which is also a suspending method using retrofit-2
// process response, store it using room and return SomeResult data object
Then I use the FooRepository from my ViewModel but because someMethod is a suspending method I need to wrap it in a coroutine scope:
class FooViewModel(private val fooRepositoru : FooRepository) : ViewModel() {
private var someMethodJob : Job? = null
val result : MutableLiveData<SomeResult> = MutableLiveData()
fun someMethod() {
someMethodJob = viewModelScope.launch {
result.value = fooRepositoru.someMethod()
}
}
override fun onCleared() {
super.onCleared()
someMethodJob?.cancel()
}
Then in the fragment or activity I can observe the view model result
fooViewModel.result.observe(viewLifecycleOwner, Observer {
Starting from my repository layer and below everything can be a suspending function. Then from the view model I can call any suspending function but never have a publicly exposed suspending function in my view model.
Is this the correct or proper way to incorporate coroutines with the view model architecture ?
Is this the correct or proper way to incorporate coroutines with the view model architecture?
Yes!
Every instance of ViewModel has its own ViewModelScope.
The purpose of ViewModelScope is to run the jobs during the life cycle of that ViewModel and take care of automatic cancelation of running coroutine jobs in case the parent Activity/Fragment of ViewModel is destroyed.
Any running jobs under ViewModelScope will be canceled when the ViewModel will be destroyed.
Read more here
private var someMethodJob : Job? = null
val result : MutableLiveData<SomeResult> = MutableLiveData()
fun someMethod() {
someMethodJob = viewModelScope.launch {
result.value = fooRepositoru.someMethod()
}
}
You can ditch all of that and just say
val result: LiveData<SomeResult> = liveData {
emit(fooRepository.someMethod())
}
And then observe result.
So, I recently started experimentation with coroutines, I switched from Rxjava2 to coroutines, I haven't got a grasp of it yet but still, I ran into a condition where I needed to observe my database change and update the UI corresponding to that.
RxJava used to provide me with Flowables, Completeable etc. using that I would be able to observe changes in Db.
abstract fun insert(data: SomeData): Long
#Query("SELECT * FROM somedata_table")
abstract fun getData(): Flowable<List<SomeData>>
So here now I used to subscribe to getData and always used to observe changes
Now Enter coroutines, I am using a suspended function with a deferred result to return my responses
#Insert(onConflict = OnConflictStrategy.IGNORE)
abstract fun insert(data: SomeData): Long
#Query("SELECT * FROM somedata_table")
abstract fun getData(): List<SomeData>
suspend fun getAllSomeData():Deferred<List<SomeData>>{
return GlobalScope.async (context= coroutineContext){
database.myDao().getData()
}
}
Now I have no way to listen for updates, Channels in coroutines might be the right answer? but I am not sure how to use it with Room.
Use Room 2.2.0 Flows and kotlin coroutines. It's contentious but I dislike LiveData as it gives you results on the UI thread. If you have to do any data parsing you'll have to push everything back to another IO thread. It's also cleaner than using channels directly as you have to do extra openSubscription().consumeEach { .. } calls every time you want to listen to events.
Flow approach Requires the following versions:
// this version uses coroutines and flows in their non-experimental version
org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2
androidx.room:room-runtime:2.2.0
androidx.room:room-compiler:2.2.0
Dao:
#Dao
interface MyDao {
#Query("SELECT * FROM somedata_table")
fun getData(): Flow<List<SomeData>>
}
class to do observation:
launch {
dao.getData().collect { data ->
//handle data here
}
}
if your calling class is not itself a CoroutineScope you'd have to call launch with the context of something that is. That can be GlobalScope or some other class you create. Here I'm using lifecycleScope assuming we're in an Activity class.
lifecycleScope.launch {
dao.getData().collect { data ->
//handle data here
}
}
the collect lambda will receive every udpate to the table much like an Rx onNext call.
Currently, there are two different ways of doing that. The first is to use a liveData builder function. To make this work, you need to update lifecycle to androidx.lifecycle:*:2.2.0-alpha01 or any newer version. The LiveData builder function will be used to call getData() asynchronously, and then use emit() to emit the result. Using this method, you will modify your Room getData() function to a suspend function and make the return type wrapped as a LiveData, replacing the Flowable used before.
#Query("SELECT * FROM somedata_table")
abstract suspend fun getData(): LiveData<List<SomeData>>
In your viewmodel you create a liveData which references your Room database
val someData: LiveData<SomeData> = liveData {
val data = database.myDao().getData()
emit(data)
}
The second approach is to get data from our DB as Flow. To use this, you need to update Room to androidx.room:room-*:2.2.0-alpha02 (currently the latest) or a newer version. This update enables #Query DAO methods to be of return type Flow The returned Flow will re-emit a new set of values if the observing tables in the query are invalidated. Declaring a DAO function with a Channel return type is an error
#Query("SELECT * FROM somedata_table")
abstract fun getData(): Flow<List<SomeData>?>
The return type is a flow of a nullable list. The list is nullable because Room will return null when the query has no data fetched.
To fetch data from the flow we will use the terminal operator collect{ } in our Presenter/ViewModel. It is preferable to do this in the ViewModel since it comes with a ViewModelScope. The solution given below assumes we are doing this in a ViewModel where we have a provided viewModelScope.
fun loadData(){
viewModelScope.launch {
database.myDao()
.getData()
.distinctUntilChanged().
.collect{
it?.let{ /** Update your obsevable data here **/
}
}
Gradle dependencies:
dependencies {
compile group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-reactive', version: '1.1.1'
}
Room Dao
#Dao
interface HistoryDao : BaseDao<HistoryEntity> {
#Query("select * from History order by time desc")
fun observe(): Flowable<List<HistoryEntity>>
...
}
Interactor (browserHistoryInteractor below) (layer between dao and Fragment/Presenter)
// To get channel of List<HistoryEntity>:
import kotlinx.coroutines.reactive.openSubscription
fun observe() = historyDao.observe().openSubscription() // convert list to Coroutines channel
Presenter/Fragment/Activity (end point (in my case it is lifecycle-aware presenter))
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
private val compositeJob = Job() // somewhat equivalent "compositeDisposable" in rx
override fun onCreate() {
super.onCreate()
launch(compositeJob) { // start coroutine
val channel = browserHistoryInteractor.observe()
for (items in channel) { // waits for next list of items (suspended)
showInView { view?.setItems(items) }
}
}
}
override fun onDestroy() {
compositeJob.cancel() // as in rx you need to cancel all jobs
super.onDestroy()
}
https://www.youtube.com/watch?v=lh2Vqt4DpHU&list=PLdb5m83JnoaBqMWF-qqhZY_01SNEhG5Qs&index=5 at 29:25