I have a problem with Room that return LiveData.
I create Dao with function to returns list of data. I suppose to return as LiveData. But, it doesn't work as expected.
Dao function
#Transaction
#Query("SELECT * FROM AllocationPercentage WHERE id IN (:ids)")
fun getByIds(ids: List<Long>): LiveData<List<AllocationPercentageWithDetails>>
Here is how I observe it inside the ViewModel:
class AllocationViewModel(
private val getAllocationByIdUseCase: GetAllocationByIdUseCase,
private val getDetailByIdUseCase: GetAllocationPercentageByIdUseCase
) : ViewModel() {
var allocationUiState: LiveData<AllocationUiState> = MutableLiveData()
private set
var allocationPercentageUiState: LiveData<List<AllocationPercentageUiState>> = MutableLiveData()
private set
val mediatorLiveData = MediatorLiveData<List<AllocationPercentageUiState>>()
fun getAllocationById(allocationId: Long) = viewModelScope.launch(Dispatchers.IO) {
val result = getAllocationByIdUseCase(allocationId) // LiveData
allocationUiState = Transformations.map(result) {
AllocationUiState(allocation = it.allocation)
}
mediatorLiveData.addSource(result) { allocation ->
Log.d(TAG, "> getAllocationById")
val ids = allocation.percentages.map { percentage -> percentage.id }
val detailResult: LiveData<List<AllocationPercentageWithDetails>> =
getDetailByIdUseCase(ids) // LiveData
allocationPercentageUiState = Transformations.map(detailResult) { details ->
Log.d(TAG, ">> Transform : $details")
details.map {
AllocationPercentageUiState(
id = it.allocationPercentage.id,
percentage = it.allocationPercentage.percentage,
description = it.allocationPercentage.description,
currentProgress = it.allocationPercentage.currentProgress
)
}
}
}
}
}
The allocationPercentageUiState is observed by Fragment.
Log.d(TAG, "observeViewModel: ${it?.size}")
val percentages = it ?: return#observe
setAllocationPercentages(percentages) // update UI
}
allocationViewModel.mediatorLiveData.observe(viewLifecycleOwner) {}
And getDetailByIdUseCase just a function which directly return result from Dao.
class GetAllocationPercentageByIdUseCase(private val repository: AllocationPercentageRepository) {
operator fun invoke(ids: List<Long>): LiveData<List<AllocationPercentageWithDetails>> {
return repository.getAllocationPercentageByIds(ids)
}
}
Any idea why? Thank you.
Combining var with LiveData or MutableLiveData doesn't make sense. It defeats the purpose of using LiveData. If something comes along and observes the original LiveData that you have in that property, it will never receive anything. It will have no way of knowing there's a new LiveData instance it should be observing instead.
I can't exactly tell you how to fix it because your code above is incomplete, so I can't tell what you're trying to do in your mapping function, or whether it is called in some function vs. during ViewModel initialization.
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 am trying to get list of todos from database with livedata however, while debugging it always shows null for value. I have provided my files below.
My Dao:
#Query("SELECT * FROM todo_table WHERE IIF(:isCompleted IS NULL, 1, isCompleted = :isCompleted)")
fun getTodos(isCompleted: Boolean?): LiveData<List<Todo>>
My ViewModel:
private var _allTodoList = MutableLiveData<List<Todo>>()
var allTodoList: LiveData<List<Todo>> = _allTodoList
init {
viewModelScope.launch(Dispatchers.IO) {
val list = todoRepository.getTodos(null)
_allTodoList.postValue(list.value)
}
}
fun onFilterClick(todoType: Constants.TodoType) {
when (todoType) {
Constants.TodoType.ALL -> {
viewModelScope.launch(Dispatchers.IO) {
val list = todoRepository.getTodos(null)
_allTodoList.postValue(list.value)
}
}
Constants.TodoType.COMPLETED -> {
viewModelScope.launch(Dispatchers.IO) {
val list = todoRepository.getTodos(true)
_allTodoList.postValue(list.value)
}
}
Constants.TodoType.INCOMPLETE -> {
viewModelScope.launch(Dispatchers.IO) {
val list = todoRepository.getTodos(false)
_allTodoList.postValue(list.value)
}
}
}
}
My MainActivity:
val allTodoList = viewModel.allTodoList.observeAsState()
allTodoList.value?.run {//value is always null
if (!isNullOrEmpty()) {
...
} else {
...
}
}
While debugging I found that allTodoList.value is always null however, when I manually run same query in app inspection I the get the desired results.
You can simplify your code, see if it works.
ViewModel only needs this:
val allTodoList: LiveData<List<Todo>> = todoRepository.getTodos(null)
MainActivity:
val allTodoList by viewModel.allTodoList.observeAsState()
if (!allTodoList.isNullOrEmpty()) {
...
} else {
...
}
You are not observing the LiveData you get from Room.
YourDao.getTodos() and LiveData.getValue() are not suspend functions, so you get the current value, which is null because Room has not yet fetched the values from SQLite.
A possible solution would be to set the todo type as a live data itself and use a switchMap transformation in the ViewModel :
private val todoType = MutableLiveData<Constants.TodoType>(Constants.TodoType.ALL)
val allTodoList: LiveData<List<Todo>> = androidx.lifecycle.Transformations.switchMap(todoType) { newType ->
val typeAsBoolean = when(newType) {
Constants.TodoType.ALL -> null
Constants.TodoType.COMPLETED -> true
Constants.TodoType.INCOMPLETE -> false
else -> throw IllegalArgumentException("Not a possible value")
}
// create the new wrapped LiveData
// the transformation takes care of subscribing to it
// (and unsubscribing to the old one)
todoRepository.getTodos(typeAsBoolean)
}
fun onFilterClick(todoType: Constants.TodoType) {
// triggers the transformation
todoType.setValue(todoType)
}
This is in fact the exact use case demonstrated in the reference doc
I just want to know if it is possible for me to return activePodcastViewData. I get return not allow here anytime I tried to call it on the activePodcastViewData.Without the GlobalScope I do get everything working fine.However I updated my repository by adding suspend method to it.Hence I was getting Suspend function should only be called from a coroutine or another suspend function.
fun getPodcast(podcastSummaryViewData: PodcastViewModel.PodcastSummaryViewData): PodcastViewData? {
val repo = podcastRepo ?: return null
val url = podcastSummaryViewData.url ?: return null
GlobalScope.launch {
val podcast = repo.getPodcast(url)
withContext(Dispatchers.Main) {
podcast?.let {
it.feedTitle = podcastViewData.name ?: ""
it.imageUrl = podcastViewData.imageUrl ?: ""
activePodcastViewData = PodcastView(it)
activePodcastViewData
}
}
}
return null
}
class PodcastRepo {
val rssFeedService =RssFeedService.instance
suspend fun getPodcast(url:String):Podcast?{
rssFeedService.getFeed(url)
return Podcast(url,"No name","No Desc","No image")
}
I'm not sure that I understand you correctly but if you want to get activePodcastViewData from coroutine scope you should use some observable data holder. I will show you a simple example with LiveData.
At first, add implementation:
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0"
Now, in your ViewModel we need to create mutableLiveData to hold and emit our future data.
val podcastsLiveData by lazy { MutableLiveData<Podcast>() }
Here your method: (I wouldn't recommend GlobalScope, let's replace it)
fun getPodcast(podcastSummaryViewData: PodcastViewModel.PodcastSummaryViewData): PodcastViewData? {
val repo = podcastRepo ?: return null
val url = podcastSummaryViewData.url ?: return null
CoroutineScope(Dispatchers.IO).launch {
val podcast = repo.getPodcast(url)
withContext(Dispatchers.Main) {
podcast?.let {
it.feedTitle = podcastViewData.name ?: ""
it.imageUrl = podcastViewData.imageUrl ?: ""
activePodcastViewData = PodcastView(it)
}
}
}
podcastsLiveData.postValue(activePodcastViewData)
}
As you can see your return null is turned to postValue(). Now you finally can observe this from your Activity:
viewModel.podcastsLiveData.observe(this) {
val podcast = it
//Use your data
}
viewModel.getPodcast()
Now every time you call viewModel.getPodcast() method, code in observe will be invoked.
I hope that I helped some :D
I have connect my android application to firebase and am using it to retrieve Authentication details and data from firestone. I am using an MVVM architecture and live data for this. The problem is that I need to retrieve email address first and then used this data to query the firestone which contain documents with ID = emailID. You can see my viewmodel. The value for the emailID is null when every I run this. How can I accomplish this while following the MVVP style of coding ?
#Edit: I need to understand how can check if the live data has been initialised with a value in the case where one livedata value depends on the other.
class ProfileViewModel(): ViewModel() {
var random =""
private var _name = MutableLiveData<String>()
val userName
get()=_name
private var _post = MutableLiveData<String>()
val userPost
get()=_post
private var _imgUrl = MutableLiveData<Uri>()
val userImgUrl
get()=_imgUrl
private var _emailId = MutableLiveData<String>()
val userEmailId
get()=_emailId
init{
getUserDataFromProfile()
getUserPostFromFirestone()
}
private fun getUserPostFromFirestone() {
val mDatabaseInstance: FirebaseFirestore = FirebaseFirestore.getInstance()
// _emailId.observe(getApplication(), Observer {
//
// } )
if(_emailId.value!=null){
mDatabaseInstance.collection("users").document(_emailId.value)
.get()
.addOnCompleteListener { task ->
if (task.isSuccessful) {
_post.value = task.result?.data?.get("post").toString()
} else {
// Log.w("firestone", "Error getting documents.", task.exception)
_post.value = "Unable to Retrieve"
}
}
}
}
private fun getUserDataFromProfile() {
val mAuth = FirebaseAuth.getInstance()
val currentUser = mAuth.currentUser
random = currentUser?.displayName!!
_name.value = currentUser?.displayName
_post.value = "Unknown"
_imgUrl.value = currentUser?.photoUrl
_emailId.value = currentUser?.email
}
}
If you write a wrapper over the Firebase call and expose it as a LiveData (or, in this case, I'll pretend it's wrapped in a suspendCoroutineCancellable), in which case whenever you want to chain stuff, you either need MediatorLiveData to combine multiple LiveDatas into a single stream (see this library I wrote for this specific purpose) or just switchMap.
private val auth = FirebaseAuth.getInstance()
val imgUrl: LiveData<Uri> = MutableLiveData<Uri>(auth.currentUser?.photoUrl)
val emailId: LiveData<String> = MutableLiveData<String>(auth.currentUser?.email)
val post = emailId.switchMap { emailId ->
liveData {
emit(getUserByEmailId(emailId))
}
}
you can set observer to LiveData and remove it when you don't need it:
class ProfileViewModel : ViewModel() {
private val _email = MutableLiveData<String>()
private val emailObserver = Observer<String> { email ->
//email is here
}
init {
_email.observeForever(emailObserver)
}
override fun onCleared() {
_email.removeObserver(emailObserver)
super.onCleared()
}
}
Try using coroutines for the sequential execution of the code. so once you get the output of one and then the second one starts executing. If this isnt working Please let me know i can try help you.
init{
viewModelScope.launch{
getUserDataFromProfile()
getUserPostFromFirestone()
}
}
I'm making a crawling logic by using coroutines in Kotlin but i don't know this code is right.
this is model class
suspend fun parseYgosu() : Elements? {
var data:Elements? = null
var x : Deferred<Elements?> = CoroutineScope(Dispatchers.IO).async {
var doc = Jsoup.connect("https://www.ygosu.com/community/real_article").get()
data = doc.select("div.board_wrap tbody tr")
data
}
x.await()
Log.d(TAG, "$data")
return data
}
This code have problems. I do not want it be a suspend function.
And also I want to get data from this function by calling it from repository class.
could you help me?
You can use liveData builder
fun parseYgosu(): LiveData<Elements?> = liveData {
val element = withContext(Dispatchers.IO) {
Jsoup.connect("https://www.ygosu.com/community/real_article")
.get()
.select("div.board_wrap tbody tr")
}
emit(element)
}
and UI side:
// for fragment
viewModel.parseYgosu().observe(viewLifecycleOwner, Observer { element -> ... })
// or for activity
viewModel.parseYgosu().observe(this, Observer { element -> ... })
Sticking with the future Deferred, if you don't want it to be suspend then you can't have await() in it
// Not suspend
fun parseYgosuAsync() = CoroutineScope(Dispatchers.IO).async {
val doc = Jsoup.connect("https://www.ygosu.com/community/real_article").get()
val data = doc.select("div.board_wrap tbody tr")
Log.d(TAG, "$data")
data
}