I'm trying to use Kotlin Coroutines + Retrofit to make my network calls, but my current implementation has two problems.
A) It only returns once my loop has completed.
B) it seems to wait for each call in my loop to complete before making the next one.
The API I'm interacting with requires me to make an initial fetch, returning an array of itemId's
[ 1234, 3456, 3456 ... ]
and for each item in the above response, fetch that item with id
{ id: 1234, "name": "banana" ... }
My current implementation is as follows, what am I doing wrong?
suspend operator fun invoke(feedType: String): NetworkResult<List<MyItem>> = withContext(Dispatchers.IO) {
val itemList: MutableList< MyItem > = mutableListOf()
val result = repository.fetchItems()
when (result) {
is NetworkResult.Success -> {
itemList.addAll(result.data)
for (i in itemList) {
val emptyItem = result.data[i]
val response = repository.fetchItem(emptyItem.id)
when (response) {
is NetworkResult.Success -> {
val item = response.data
emptyItem.setProperties(item)
}
}
}
}
is NetworkResult.Error -> return#withContext result
}
return#withContext NetworkResult.Success(itemList)
}
I would like to propose you to use async to process every item separately:
suspend operator fun invoke(feedType: String): NetworkResult<List<MyItem>> = withContext(Dispatchers.IO) {
when (val result = repository.fetchItems()) { // 1
is NetworkResult.Success -> {
result.data
.map { async { fetchItemData(it) } } // 2
.awaitAll() // 3
NetworkResult.Success(result.data)
}
is NetworkResult.Error -> result
}
}
private suspend fun fetchItemData(item: MyItem) {
val response = repository.fetchItem(item.id)
if (response is NetworkResult.Success) {
item.setProperties(response.data)
}
}
In this code, at first, we make a call to fetchItems to get the items ids (1). Then we make a call to fetchItem for every item at the same time (2). It can be easily done with coroutines and async. Then we wait until all data will be fetched (3).
Related
I have the following setup.
I have a screen with a list of items (PlantsScreen). When clicking on an item from the list I will be navigated to another screen (AddEditPlantScreen). After editing and saving the item and navigating back to the listScreen, I want to show the updated list of items. But the list is not displaying the updated list but the list before the edit of the item.
In order to have a single source of truth, I am fetching the data from a node.js Back-End and then saving it to the local repository (Room). I think I need to refresh the state in the ViewModel to fetch the updated list from my repository.
I know I can use a Job to do this, but it throws me an error. Is this the correct approach when returning a Flow?
If yes, how can I achieve this.
If not, what alternative approach do I have?
plantsListViewModel.kt
private val _state = mutableStateOf<PlantsState>(PlantsState())
val state: State<PlantsState> = _state
init {
getPlants(true, "")
}
private fun getPlants(fetchFromBackend: Boolean, query: String) {
viewModelScope.launch {
plantRepository.getPlants(fetchFromBackend, query)
.collect { result ->
when (result) {
is Resource.Success -> {
result.data?.let { plants ->
_state.value = state.value.copy(
plants = plants,
)
}
}
}
}
}
}
Here is my repository where I fetch the items in the list from.
// plantsRepository.kt
override suspend fun getPlants(
fetchFromBackend: Boolean,
query: String
): Flow<Resource<List<Plant>>> {
return flow {
emit(Resource.Loading(true))
val localPlants = dao.searchPlants(query)
emit(
Resource.Success(
data = localPlants.map { it.toPlant() },
)
)
val isDbEmpty = localPlants.isEmpty() && query.isBlank()
val shouldLoadFromCache = !isDbEmpty && !fetchFromBackend
if (shouldLoadFromCache) {
emit(Resource.Loading(false))
return#flow
}
val response = plantApi.getPlants().plants
dao.clearPlants()
dao.insertPlants(
response.map { it.toPlantEntity() }
)
emit(Resource.Success(
data = dao.searchPlants("").map { it.toPlant() }
))
emit(Resource.Loading(false))
}
}
The full code for reference can be found here:
https://gitlab.com/fiehra/plants
Thank you!
You actually have two sources of truth: One is the room database, the other the _state object in the view model.
To reduce this to a single source of truth you need to move the collection of the flow to the compose function where the data is needed. You will do this using the extension function StateFlow.collectAsStateWithLifecycle() from the artifact androidx.lifecycle:lifecycle-runtime-compose. This will automatically subscribe and unsubscribe the flow when your composable enters and leaves the composition.
Since you want the business logic to stay in the view model you have to apply it before the flow is collected. The idea is to only transform the flow in the view model:
class PlantsViewModel {
private var fetchFromBackend: Boolean by mutableStateOf(true)
private var query: String by mutableStateOf("")
#OptIn(ExperimentalCoroutinesApi::class)
val state: StateFlow<PlantsState> =
snapshotFlow { fetchFromBackend to query }
.flatMapLatest { plantRepository.getPlants(it.first, it.second) }
.mapLatest(PlantsState::of)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = PlantsState.Loading,
)
// ...
}
If you want other values for fetchFromBackend and query you just need to update the variables; the flow will automatically recalculate the state object. It can be as simple as just calling something like this:
fun requestPlant(fetchFromBackend: Boolean, query: String) {
this.fetchFromBackend = fetchFromBackend
this.query = query
}
The logic to create a PlantsState from a result can then be done somewhere else in the view model. Replace your PlantsViewModel.getPlants() with this and place it at file level outside of the PlantsViewModel class:
private fun PlantsState.Companion.of(result: Resource<List<Plant>>): PlantsState = when (result) {
is Resource.Success -> {
result.data?.let { plants ->
PlantsState.Success(
plants = plants,
)
} ?: TODO("handle case where result.data is null")
}
is Resource.Error -> {
PlantsState.Error("an error occurred")
}
is Resource.Loading -> {
PlantsState.Loading
}
}
With the PlantsState class replaced by this:
sealed interface PlantsState {
object Loading : PlantsState
data class Success(
val plants: List<Plant> = emptyList(),
val plantOrder: PlantOrder = PlantOrder.Name(OrderType.Descending),
val isOrderSectionVisible: Boolean = false,
) : PlantsState
data class Error(
val error: String,
) : PlantsState
companion object
}
Then, wherever you need the state (in PlantsScreen f.e.), you can get a state object with
val state by viewModel.state.collectAsStateWithLifecycle()
Thanks to kotlin flows state will always contain the most current data from the room database, and thanks to the compose magic your composables will always update when anything in the state object updates, so that you really only have one single source of truth.
Additionally:
PlantRepository.getPlants() should not be marked as a suspend function because it just creates a flow and won't block; long running data retrieval will be done in the collector.
You will need to manually import androidx.compose.runtime.getValue and the androidx.compose.runtime.setValue for some of the delegates to work.
After #Leviathan was able to point me in the right direction i refactored my code by changing the return types of my repository functions, implementing use cases and returning a Flow<List<Plant>> instead of Flow<Resource<List<Plant>>> for simplicity purposes.
Further removed the suspend marker of the functions in the PlantDao.kt and PlantRepository.kt as pointed out by Leviathan.
// PlantRepositoryImplementation.kt
override fun getPlants(
fetchFromBackend: Boolean,
query: String
): Flow<List<Plant>> {
return flow {
if (fetchFromBackend) {
val response = plantApi.getPlants().plants
dao.clearPlants()
dao.insertPlants(
response.map { it.toPlantEntity() }
)
val localPlants = dao.searchPlants(query)
localPlants.collect { plants ->
emit(plants.map { it.toPlant() })
return#collect
}
} else {
val localPlants = dao.searchPlants(query)
localPlants.collect { plants ->
emit(plants.map { it.toPlant() })
return#collect
}
}
}
}
I started using a Job and GetPlants usecase in my viewModel like this:
// PlantsViewModel.kt
private fun getPlants(plantOrder: PlantOrder, fetchFromBackend: Boolean, query: String) {
getPlantsJob?.cancel()
getPlantsJob = plantUseCases.getPlants(plantOrder, fetchFromBackend, query)
.onEach { plants ->
_state.value = state.value.copy(
plants = plants,
plantOrder = plantOrder
)
}.launchIn(viewModelScope)
I also had to remove the suspend in the PlantDao.kt
// PlantDao.kt
fun searchPlants(query: String): Flow<List<PlantEntity>>
This is the code for my GetPlants usecase:
// GetPlantsUsecase.kt
class GetPlants
(
private val repository: PlantRepository,
) {
operator fun invoke(
plantOrder: PlantOrder = PlantOrder.Name(OrderType.Descending),
fetchFromBackend: Boolean,
query: String
): Flow<List<Plant>> {
return repository.getPlants(fetchFromBackend, query).map { plants ->
when (plantOrder.orderType) {
is OrderType.Ascending -> {
// logic for sorting
}
}
is OrderType.Descending -> {
// logic for sorting
}
}
}
}
}
I recently started working with coroutines.
The task is that I need to check the priority parameter from the List and make a request to the server, if the response from the server is OK, then stop the loop.
var minPriority = 0
list.forEach { model ->
if (model.priority > minPriority) {
makeRequest(model.value)
minPriority = model.priority
}
}
private fun makeRequest(value: String) {
scope.launch() {
val response = restApi.makeRequest()
if response.equals("OK") {
**stop list foreach()**
}
}
}
In RxJava, this was done using the retryWhen() operator, tell me how to implement this in Coroutines?
I suggest making your whole code suspendable, not only the body of makeRequest() function. This way you can run the whole operation in the background, but internally it will be sequential which is easier to code and maintain.
It could be something like this:
scope.launch() {
var minPriority = 0
list.forEach { model ->
if (model.priority > minPriority) {
val response = restApi.makeRequest()
if response.equals("OK") {
return#forEach
}
minPriority = model.priority
}
}
}
Of if you need to keep your makeRequest() function separate:
fun myFunction() {
scope.launch() {
var minPriority = 0
list.forEach { model ->
if (model.priority > minPriority) {
if (makeRequest(model.value)) {
return#forEach
}
minPriority = model.priority
}
}
}
}
private suspend fun makeRequest(value: String): Boolean {
val response = restApi.makeRequest()
return response.equals("OK")
}
I have a requirement after I call the local database(room) and get the data then only I will fire the network call.
If local data is present then network call will not happen otherwise network call will happen and it will store the data.
I have tried the same with a strategy class but network call is happening before it checks the local database.
DataAccessStrategy.kt
fun <T, A> fetchCategory(databaseQuery: () -> LiveData<T>,
networkCall: suspend () -> Resource<A>,
saveCallResult: suspend (A) -> Unit,
clearDB: suspend () -> Unit): LiveData<Resource<T>> =
liveData(Dispatchers.IO) {
emit(Resource.loading())
val categoryList = databaseQuery().map { it ->
Resource.success(it)
}
emitSource(categoryList)
if (categoryList.value?.data == null || Preferences(MainApplication.getContext())
.getBooleanPreference(MainApplication.getContext(), Preferences.APP_CATEGORY_UPDATE)) {
val networkCallStatus = networkCall()
if (networkCallStatus.status == SUCCESS) {
if (Preferences(MainApplication.getContext()).getBooleanPreference(MainApplication.getContext(),
Preferences.APP_CATEGORY_UPDATE)) {
clearDB()
}
saveCallResult(networkCallStatus.data!!)
Preferences(MainApplication.getContext()).storeBooleanPreference(MainApplication.getContext(),
Preferences.APP_CATEGORY_UPDATE, false)
} else if (networkCallStatus.status == ERROR) {
emit(Resource.error(networkCallStatus.message!!))
emitSource(categoryList)
}
}
}
ProjectRepository
class ProjectRepository constructor(
private val homePagePostDAO: HomePagePostDAO,
private val categoryDAO: CategoryDAO,
private val homeRemoteDataSource: HomeRemoteDataSource
) {
val TAG = "ProjectRepository"
val getCategoryFromDB = categoryDAO.getAllCategory()
fun getAllCategoryList() = fetchCategory(
databaseQuery = { categoryDAO.getAllCategory() },
networkCall = { homeRemoteDataSource.getCategory() },
saveCallResult = { categoryDAO.insertAllCategory(it.data) },
clearDB = { categoryDAO.deleteAllCategory() }
)
}
So the main requirement is until I get the data from the room db it
should not execute the next line.
I want to make a sequential call using a coroutine. That means I have
two calls one is db select query another one is a retrofit network
call. Retrofit network call will start only if db call is finish.
I am fairly new to this kotlin-coroutine thing and i have an issue with job-scheduling.In this code below, first i fetch topic names from user's cache in the fragment.(topicsList)
And then, i need to fetch these topics from API one by one. What i want to do is loop through the topicsList, make a request for each topic and get all the responses once at the completion of all requests. In order to achieve that, in getEverything() method(which fires up a request), i am adding the responses into an arraylist for every time.(responseList)
In for loop, i am firing up all the requests. After the completion of the job, job.invokeOnCompletion{} is called and i set my liveData to responseList. However, this approach doesn't work. Problem is, i am updating the liveData before the setting the responseList. I don't know how can it be possible. Could anybody help me about this?
Here is my CoroutineScope in myFragment:
val topicsList = dataMap["topics"] // GOT THE TOPICS
topicsList?.let {
var job: Job
CoroutineScope(Dispatchers.Main).launch {
job = launch {
for (topic in topicsList) {
mViewModel.getEverything(topic, API_KEY)
}
}
job.join()
job.invokeOnCompletion {
mViewModel.updateLiveData()
}
}
} ?: throw Exception("NULL")
getEverything() method in viewModel:
suspend fun getEverything(topic: String, apiKey: String) {
viewModelScope.launch {
_isLoading.value = true
withContext(Dispatchers.IO) {
val response = api.getEverything(topic, apiKey)
withContext(Dispatchers.Main) {
if (response.isSuccessful) {
if (response.body() != null) {
responseList.add(response.body()!!)
println("Response is successful: ${response.body()!!}")
_isLoading.value = false
_isError.value = false
}
}
else {
Log.d(TAG, "getEverything: ${response.errorBody()}")
_isError.value = true
_isLoading.value = false
}
}
}
}
}
And, updateLiveData method:
fun updateLiveData() {
_newsResponseList.value = responseList
println("response list : ${responseList.size}")
responseList.clear()
}
And this is how it looks in the logs: Logs
Logs for you who cannot open the image :
I/System.out: response list : 0
I/System.out: Response is successful: NewsResponse(articleList=[Article(source=Source(id=wired, ...
I/System.out: Response is successful: NewsResponse(articleList=[Article(source=Source(id=techcrunch, ...
I/System.out: Response is successful: NewsResponse(articleList=[Article(source=Source(id=wired, ...
I/System.out: Response is successful: NewsResponse(articleList=[Article(source=Source(id=the-verge, ...
Btw data is fetched without an error and its correct. I've no issue with that.
The issue is that getEverything uses launch to create a background job, then returns before it knows the job is complete.
To fix this, have getEverything return the data directly:
suspend fun getEverything(topic: String, apiKey: String): Response? {
_isLoading.value = true
val response = withContext(Dispatchers.IO) {
api.getEverything(topic, apiKey)
}
_isLoading.value = false
return response.takeIf { it.isSuccessful }?.body()?.let { body ->
println("Response is successful: $body")
}.also {
_isError.value = it == null
}
}
In your Fragment, request the results and assign them:
lifecycleScope.launch {
_responseList.value = topicsList.mapNotNull { topic ->
model.getResponse(topic, apiKey)
}
}
In my app, I have this flow:
ClickListender in my fragment:
search_button.setOnClickListener {
if(search_input.text.isNullOrEmpty())
Toast.makeText(activity, "Input Error", Toast.LENGTH_LONG).show()
else
viewModel.onSearchButtonClicked(search_input.text.toString())
}
onSearchButtonClicked inside viewModel:
fun onSearchButtonClicked(input: String) {
coroutineScope.launch {
repo.insertToDatabase(input)
}
}
insertToDatabase inside Repository:
suspend fun insertToDatabase(string: String) {
withContext(Dispatchers.IO) {
val dataList =
ExternalApi.retrofitCall.getData(string).await()
if (dataList.intialDataResult < 1) {
//show error
} else {
//all good
database.myDataBase.insertAll(dataList)
}
}
}
I need to show error message if intialDataResult is less then one.
I thought about create MutableLiveData inside my repository with initial value of false and listen from the fragment through the viewModel, but it's not good approach because I have no way to set the LiveData to "false" again after I show error message.
I also tried to return bool from the insertToDatabase function and decide if to show error or not, with no success.
Any ideas how can I solve this?
Why not create a LiveData to manage your work's result state?
Create a class to store result of work why sealed class?
sealed class ResultState{
object Success: ResultState() // this is object because I added no params
data class Failure(val message: String): ResultState()
}
Create a LiveData to report this result
val stateLiveData = MutableLiveData<ResultState>()
Make insertToDatabase() return a result
suspend fun insertToDatabase(input: String): ResultState {
return withContext<ResultState>(Dispatchers.IO) {
val dataList =
ExternalApi.retrofitCall.getData(string).await()
if (dataList.intialDataResult < 1) {
return#withContext ResultState.Failure("Reason of error...")
} else {
database.myDataBase.insertAll(dataList)
return#withContext ResultState.Success
}
}
}
Now, report result to UI
fun onSearchButtonClicked(input: String) {
coroutineScope.launch {
val resultState = repo.insertToDatabase(input)
stateLiveData.value = resultState
}
}
In UI,
viewModel.stateLiveData.observe(viewLifeCycleOwner, Observer { state ->
when (state) {
is ResultState.Success -> { /* show success in UI */ }
is ResultState.Failure -> { /* show error in UI with state.message variable */ }
}
})
Similarly, you can add a ResultState.PROGRESS to show that a task is running in the UI.
If you have any queries, please add a comment.