My task is to get whole Article with provided title from RecyclerView.
When I click on specific Article i get title from it.
Room database:
#Query("SELECT * FROM article_table WHERE title = :title")
fun getArticleDetails(title: String): Flow<ArticleLocal>
Repository:
fun getArticleDetails(title: String): Flow<ArticleLocal> {
return articleDao.getArticleDetails(title)
}
ViewModel:
val articleDetail = MutableStateFlow<ArticleLocal>(ArticleLocal("","","","",""))
fun getArticle(title: String) {
viewModelScope.launch {
articleRepository.getArticleDetails(title).collect {
articleDetail.emit(it)
}
}
}
MainActivity:
lifecycleScope.launch {
viewModel.getArticle(title)
viewModel.articleDetail.collect {
Log.d(TAG, "onCreate: $it")
}
}
Problem with this code is that articleDetail on first touch gives me empty ArticleLocal e.g. title = "" I defined in ViewModel, later I get good result.
EDIT: With MyActivity .collet I get whole object but cannot access propert like it.title
Use a SharedFlow so it doesn't have to publish a default result. The flow won't emit anything until it receives its first value. Use replay = 1 to get similar behavior as StateFlow as far as new subscribers getting the most recent value immediately.
You also need to consider that if the title changes, it should not keep publishing values with the old title. Currently, you have it collecting from more and more flows each time the title changes.
If you use another MutableSharedFlow just for the title, you can get it to automatically cancel unnecessary collection of those old title flows. It also allows you to get the benefit of SharingStarted.WhileSubscribed to avoid unnecessary collection from the repository when there are no subscribers.
In ViewModel:
private val articleTitle = MutableSharedFlow<String>(bufferOverflow = BufferOverflow.DROP_OLDEST)
val articleDetail = articleTitle.flatMapLatest { articleRepository.getArticleDetails(it) }
.shareIn(viewModelScope, SharingStarted.WhileSubscribed(5000), replay = 1)
fun getArticle(title: String) {
articleTitle.tryEmit(title)
}
You can get rid of additional flow to emit data and use the flow returned from the repository directly.
ViewModel:
fun getArticle(title: String): Flow<ArticleLocal> {
return articleRepository.getArticleDetails(title)
}
MainActivity:
lifecycleScope.launch {
viewModel.getArticle(title).collect {
Log.d(TAG, "onCreate: $it")
}
}
Related
In my application I want update data with SharedFlow and my application architecture is MVI .
I write below code, but just update one of data!
I have 2 spinners and this spinners data fill in viewmodel.
ViewModel code :
class MyViewModel #Inject constructor(private val repository: DetailRepository) : ViewModel() {
private val _state = MutableStateFlow<MyState>(MyState.Idle)
val state: StateFlow<MyState> get() = _state
fun handleIntent(intent: MyIntent) {
when (intent) {
is MyIntent.CategoriesList -> fetchingCategoriesList()
is MyIntent.PriorityList -> fetchingPrioritiesList()
}
}
private fun fetchingCategoriesList() {
val data = mutableListOf(Car, Animal, Color, Food)
_state.value = DetailState.CategoriesData(data)
}
private fun fetchingPrioritiesList() {
val data = mutableListOf(Low, Normal, High)
_state.value = DetailState.PriorityData(data)
}
}
With below codes I filled spinners in fragment :
lifecycleScope.launch {
//Send
viewModel.handleIntent(MyIntent.CategoriesList)
viewModel.handleIntent(MyIntent.PriorityList)
//Get
viewModel.state.collect { state ->
when (state) {
is DetailState.Idle -> {}
is DetailState.CategoriesData -> {
categoriesList.addAll(state.categoriesData)
categorySpinner.setupListWithAdapter(state.categoriesData) { itItem ->
category = itItem
}
Log.e("DetailLog","1")
}
is DetailState.PriorityData -> {
prioritiesList.addAll(state.prioritiesData)
prioritySpinner.setupListWithAdapter(state.prioritiesData) { itItem ->
priority = itItem
}
Log.e("DetailLog","2")
}
}
When run application not show me number 1 in logcat, just show number 2.
Not call this line : is DetailState.CategoriesData
But when comment this line viewModel.handleIntent(MyIntent.PriorityList) show me number 1 in logcat!
Why when use this code viewModel.handleIntent(MyIntent.CategoriesList) viewModel.handleIntent(MyIntent.PriorityList) not show number 1 and 2 in logcat ?
The problem is that a StateFlow is conflated, meaning if you rapidly change its value faster than collectors can collect it, old values are dropped without ever being collected. Therefore, StateFlow is not suited for an event-like system like this. After all, it’s in the name that it is for states rather than events.
It’s hard to suggest an alternative because your current code looks like you shouldn’t be using Flows at all. You could simply call a function that synchronously returns data that you use synchronously. I don’t know if your current code is a stepping stone towards something more complicated that really would be suitable for flows.
I'm trying to retrieve a single Client item from my Room database. Every client is displayed in a list, and each client has an edit button on them. When the button is pressed, I would like to retrieve that client from the database by their id. Their details will then be displayed on an edit screen.
My problem arises in actually getting the client from the database. So far I have tried 2 approaches:
Coroutines based approach
I have tried to retrieve the item using coroutine based functions with Room. This approach does "work" to an extent, but in its current form the coroutine ends up retrieving the newly searched for client ***after*** the edit screen has been displayed. This makes it so that when you edit a client, you end up editing the one you tried to edit previously.
I have tried to counteract this by using .join(), using viewModelScope.async rather than launch and then attempting to use .await, and a few other ideas, but none of them have worked.
ClientDao.kt
#Dao
interface ClientDao {
#Query("SELECT * FROM tblClient WHERE id = :id")
suspend fun getClientToEdit(id: Int): List<Client>
}
ClientRepository.kt
class ClientRepository(private val clientDao: ClientDao) {
val clientSearchResults = MutableLiveData<List<Client>>()
suspend fun getClientToEdit(id: Int) {
clientSearchResults.value = clientDao.getClientToEdit(id)
}
}
ClientViewModel.kt
class ClientViewModel(application: Application): ViewModel() {
private val repository: ClientRepository
val clientSearchResults: MutableLiveData<List<Client>>
init {
val clientDB = ManagementDatabase.getDatabase(application)
val clientDao = clientDB.clientDao()
repository = ClientRepository(clientDao)
clientSearchResults = repository.clientSearchResults
}
fun getClientToEdit(clientId: Int) = viewModelScope.launch {
repository.getClientToEdit(clientId)
}
}
ManagementApp.kt
ClientScreen(
onEditClient = { id ->
clientViewModel.getClientToEdit(id)
val editClientList: List<Client>? = clientViewModel.clientSearchResults.value
//This looks awful but it works
// It just gets the client details of the selected client
if (editClientList != null) {
if (editClientList.firstOrNull() != null) {
selectedClient = editClientList[0]
If I could just find a way to make it so that clientViewModel.getClientToEdit(id) fully executed before running the rest of the code in ManagementApp.kt, it would work. The problem is I'm not sure how.
Flow based approach:
I didn't really think this approach would work, but it was worth a shot. I have tried to retrieve the item using a flow list, in the same way I have been retrieving the whole list.
ClientDao.kt
#Dao
interface ClientDao {
#Query("SELECT * FROM tblClient WHERE id = :id")
fun getClientToEdit(id: Int): Flow<List<Client>>
}
ClientRepository.kt
class ClientRepository(private val clientDao: ClientDao) {
fun getClientSearchResults(id: Int): Flow<List<Client>> =
clientDao.getClientToEdit(id)
}
ClientViewModel.kt
class ClientViewModel(application: Application): ViewModel() {
private val repository: ClientRepository
init {
val clientDB = ManagementDatabase.getDatabase(application)
val clientDao = clientDB.clientDao()
repository = ClientRepository(clientDao)
}
fun getClientToEdit(clientId: Int): LiveData<List<Client>> {
return repository.getClientSearchResults(id = clientId).asLiveData()
}
}
ManagementApp.kt
ClientScreen(
onEditClient = { id ->
val editClientList by clientViewModel.getClientToEdit(id).observeAsState(listOf())
//This looks awful but it works
// It just gets the client details of the selected client
if (editClientList != null) {
if (editClientList.firstOrNull() != null) {
selectedClient = editClientList[0]
The problem with this approach is that .observeAsState gives me the '#Composable invocations can only happen from the context of a #Composable function' error (Although the snippet of code above is actually within a #Composable function).
If anyone could provide some much needed help I would greatly appreciate it. I'm new to Android and have struggled with Room quite a bit, so my apologies if the code isn't really up to scratch. Thank you.
When the button is pressed, I would like to retrieve that client from the database by their id. Their details will then be displayed on an edit screen.
If by "edit screen" you mean proper screen with ViewModel to which you navigate using for example androidx.navigation, better approach would be to just pass the id to that new screen and do the loading in its ViewModel.
If I could just find a way to make it so that clientViewModel.getClientToEdit(id) fully executed before running the rest of the code in ManagementApp.kt
You can do that by making getClientToEdit suspend fun and then doing something like this:
val scope = rememberCoroutineScope()
ClientScreen(
onEditClient = { id ->
scope.launch {
clientViewModel.getClientToEdit(id)
// now getClientToEdit was executed
}
}
)
I would also suggest returning Client directly from the getClientToEdit, using LiveData for that is not necessary
Although the snippet of code above is actually within a #Composable function
It's not, you are trying to call it from onClick callback and onClick is not marked with #Composable, so you cannot call composable functions from there.
To sum it up:
If the result of your action is navigation to another screen, you can do one of these:
Pass just the id to that other screen and do the loading there, as I suggested.
Launch coroutine inside onEditClient callback, load the client and navigate from there as shown above.
Load the client in ViewModel, update some state there and navigate based on that state, something like:
// ViewModel
val actions = MutableSharedFlow<Action>()
fun editClient(id: Int) = viewModelScope.launch {
val client = repository.getClientToEdit(clientId)
actions.emit(NavigateToEditScreen(client))
}
// Screen
val action by clientViewModel.actions.collectAsState()
LaunchedEffect(action) {
if (action is NavigateToEditScreen) {
// do the navigation using action.client
}
}
ClientScreen(
onEditClient = { id ->
clientViewModel.editClient(id)
}
)
I started building my app using Room, Flow, LiveData and Coroutines, and have come across something odd: what I'm expecting to be a value flow actually has one null item in it.
My setup is as follows:
#Dao
interface BookDao {
#Query("SELECT * FROM books WHERE id = :id")
fun getBook(id: Long): Flow<Book>
}
#Singleton
class BookRepository #Inject constructor(
private val bookDao: BookDao
) {
fun getBook(id: Long) = bookDao.getBook(id).filterNotNull()
}
#HiltViewModel
class BookDetailViewModel #Inject internal constructor(
savedStateHandle: SavedStateHandle,
private val bookRepository: BookRepository,
private val chapterRepository: ChapterRepository,
) : ViewModel() {
val bookID: Long = savedStateHandle.get<Long>(BOOK_ID_SAVED_STATE_KEY)!!
val book = bookRepository.getBook(bookID).asLiveData()
fun getChapters(): LiveData<PagingData<Chapter>> {
val lastChapterID = book.value.let { book ->
book?.lastChapterID ?: 0L
}
val chapters = chapterRepository.getChapters(bookID, lastChapterID)
return chapters.asLiveData()
}
companion object {
private const val BOOK_ID_SAVED_STATE_KEY = "bookID"
}
}
#AndroidEntryPoint
class BookDetailFragment : Fragment() {
private var queryJob: Job? = null
private val viewModel: BookDetailViewModel by viewModels()
override fun onResume() {
super.onResume()
load()
}
private fun load() {
queryJob?.cancel()
queryJob = lifecycleScope.launch() {
val bookName = viewModel.book.value.let { book ->
book?.name
}
binding.toolbar.title = bookName
Log.i(TAG, "value: $bookName")
}
viewModel.book.observe(viewLifecycleOwner) { book ->
binding.toolbar.title = book.name
Log.i(TAG, "observe: ${book.name}")
}
}
}
Then I get a null value in lifecycleScope.launch while observe(viewLifecycleOwner) gets a normal value.
I think it might be because of sync and async issues, but I don't know the exact reason, and how can I use LiveData<T>.value to get the value?
Because I want to use it in BookDetailViewModel.getChapters method.
APPEND: In the best practice example of Android Jetpack (Sunflower), LiveData.value (createShareIntent method of PlantDetailFragment) works fine.
APPEND 2: The getChapters method returns a paged data (Flow<PagingData<Chapter>>). If the book triggers an update, it will cause the page to be refreshed again, confusing the UI logic.
APPEND 3: I found that when I bind BookDetailViewModel with DataBinding, BookDetailViewModel.book works fine and can get book.value.
LiveData.value has extremely limited usefulness because you might be reading it when no value is available yet.
You’re checking the value of your LiveData before it’s source Flow can emit its first value, and the initial value of a LiveData before it emits anything is null.
If you want getChapters to be based on the book LiveData, you should do a transformation on the book LiveData. This creates a LiveData that under the hood observes the other LiveData and uses that to determine what it publishes. In this case, since the return value is another LiveData, switchMap is appropriate. Then if the source book Flow emits another version of the book, the LiveData previously retrieved from getChapters will continue to emit, but it will be emitting values that are up to date with the current book.
fun getChapters(): LiveData<PagingData<Chapter>> =
Transformations.switchMap(book) { book ->
val lastChapterID = book.lastChapterID
val chapters = chapterRepository.getChapters(bookID, lastChapterID)
chapters.asLiveData()
}
Based on your comment, you can call take(1) on the Flow so it will not change the LiveData book value when the repo changes.
val book = bookRepository.getBook(bookID).take(1).asLiveData()
But maybe you want the Book in that LiveData to be able to be changed when the repo changes, and what you want is that the Chapters LiveData retrieved previously does not change? So you need to manually get it again if you want it to be based on the latest Book? If that's the case, you don't want to be using take(1) there which would prevent the book from appearing updated in the book LiveData.
I would personally in that case use a SharedFlow instead of LiveData, so you could avoid retrieving the values twice, but since you're currently working with LiveData, here's a possible solution that doesn't require you to learn those yet. You could use a temporary Flow of your LiveData to easily get its current or first value, and then use that in a liveData builder function in the getChapters() function.
fun getChapters(): LiveData<PagingData<Chapter>> = liveData {
val singleBook = book.asFlow().first()
val lastChapterID = singleBook.lastChapterID
val chapters = chapterRepository.getChapters(bookID, lastChapterID)
emitSource(chapters)
}
I have a huge understanding problem here, I have a ecommerce app and I cannot properly calculate value of users cart.
The problem is, my solution works well to the point but I have an issue when there are no products in the cart. Obviously LiveData observer or switchMap will not get executed when it's value is empty.
It seems like something trivial, only thing I want to do here is handle the situation when user have no products in the cart. Is the livedata and switchMap a wrong approach here?
I get userCart from the repo -> I calculate its value in the viewModel and expose it to the view with dataBinding.
#HiltViewModel
class CartFragmentViewModel
#Inject
constructor(
private val repository: ProductRepository,
private val userRepository: UserRepository,
private val priceFormatter: PriceFormatter
) : ViewModel() {
private val user = userRepository.currentUser
val userCart = user.switchMap {
repository.getProductsFromCart(it.cart)
}
val cartValue = userCart.switchMap {
calculateCartValue(it)
}
private fun calculateCartValue(list: List<Product>?): LiveData<String> {
val cartVal = MutableLiveData<String>()
var cartValue = 0L
list?.let { prods ->
prods.forEach {
cartValue += it.price
}
cartVal.postValue(priceFormatter.formatPrice(cartValue))
} ?: cartVal.postValue(priceFormatter.formatPrice(0))
return cartVal
}
fun removeFromCart(product: Product) {
userRepository.removeFromCart(product)
getUserData()
}
private fun getUserData() {
userRepository.getUserData()
}
init {
getUserData()
}
}
Default value is to solve the "initial" empty cart.
Now if you need to trigger it when there's no data... (aka: after you remove items and the list is now empty), I'd use a sealed class to wrap the actual value.
(names and code are pseudo-code, so please don't copy-paste)
Something like this:
Your Repository should expose the cart, user, etc. wrapped in a sealed class:
sealed class UserCartState {
object Empty : UserCartState()
data class HasItems(items: List<things>)
object Error(t: Throwable) :UserCartState() //hypotetical state to signal problems
}
In your CartFragmentViewModel, you observe and use when (for example), to determine what did the repo responded with.
repo.cartState.observe(...) {
when (state) {
is Empty -> //deal with it
is HasItems -> // do what it takes to convert it, calculate it, etc.
is Error -> // handle it
}
}
When the user removes the last item in the cart, your repo should emit Empty.
The VM doesn't care how that happened, it simply reacts to the new state.
The UI cares even less. :)
You get the idea (I hope).
That's how I would look into it.
You can even use a flow of cart items, or the new "FlowState" thingy (see the latest Google I/O 21) to conserve resources when the lifecycle owner is not ready.
I suppose that this part of code creates the problem
list?.let { prods ->
prods.forEach {
cartValue += it.price
}
cartVal.postValue(priceFormatter.formatPrice(cartValue))
} ?: cartVal.postValue(priceFormatter.formatPrice(0))
Probably, list is not null but is empty. Please try this:
if (list.isNullOrEmpty) {
list.forEach {
cartValue += it.price
}
cartVal.postValue(priceFormatter.formatPrice(cartValue))
} else {
cartVal.postValue(priceFormatter.formatPrice(0))
}
I've one LiveData named sortOrder and then I've another variable named myData that observes any change to sortOrder and populates data accordingly.
class TestViewModel #ViewModelInject constructor() : ViewModel() {
private val sortOrder = MutableLiveData<String>()
val myData = sortOrder.map {
Timber.d("Sort order changed to $it")
"Sort order is $it"
}
init {
sortOrder.value = "year"
}
}
Observing in Activity
class TestActivity : AppCompatActivity() {
private val viewModel: TestViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_test)
// Observing data
viewModel.myData.observe(this) {
Timber.d("Data is : $it")
}
}
}
Question
How can I replace the above scenario with Flow/StateFlow APIs without any change in output?
If you fail to convert the mapped cold Flow into a hot Flow, it will restart the flow every time you collect it (like when your Activity is recreated). That's how cold flows work.
I have a feeling they will flesh out the transform functions for StateFlow/SharedFlow, because it feels very awkward to map them to cold flows and have to turn them back into hot flows.
The public Flow has to be a SharedFlow if you don't want to manually map the first element distinctly because the stateIn function requires you to directly provide an initial state.
private val sortOrder = MutableStateFlow("year")
val myData = sortOrder.map {
Timber.d("Sort order changed to $it")
"Sort order is $it"
}.shareIn(viewModelScope, SharingStarted.Eagerly, 1)
Or you could create a separate function that is called within map and also in a stateIn function call.
private val sortOrder = MutableSharedFlow<String>()
private fun convertSortOrder(order: String): String {
Log.d("ViewModel", "Sort order changed to $order")
return "Sort order is $order"
}
val myData = sortOrder.map {
convertSortOrder(it)
}.stateIn(viewModelScope, SharingStarted.Eagerly, convertSortOrder("year"))
From the point of the fragment/activity, you have to create a job that collects the flow in onStart() and cancel it in onStop(). Using the lifecycleScope.launchWhenStarted will keep the flow active even in the background.
Use the bindin library to migrate to Flow with ease. I'm biased, tho.
See an article on medium about it.