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))
}
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.
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")
}
}
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 ViewModel that has a MutableLiveData of an arraylist of class Course
private var coursesList: MutableLiveData<ArrayList<Course>> = MutableLiveData()
This coursesList is filled with data got from an API (by Retrofit): coursesList.postValue(response.body())
Now, a user can search for a course by its name. The function that I have for searching is that I iterate through the elements of the coursesList and check if its name is equal to what a user typed. It returns an arrayList with the courses that start with the name typed (this list is later sent to a fragment which passes it to an adapter to be shown in a recyclerview):
fun getCoursesList(): MutableLiveData<ArrayList<Course>> {
return coursesList
}
fun searchCourses(searchString: String): ArrayList<Course> {
val resultsList: ArrayList<Course> = ArrayList()
if (getCoursesList().value == null) return resultsList
if (getCoursesList().value!!.size > 0) {
for (course in getCoursesList().value!!.iterator()) {
if (course.name.toLowerCase(Locale.ROOT).startsWith(searchString)) {
resultsList.add(course)
}
}
}
resultsList.sortBy { it.price }
return resultsList
}
This function works and all but my instructor asked me to use LiveData for searching without giving any additional hints on how to do that.
So my question is how to use LiveData for searching? I tried to search for answers, I saw that some used LiveDataTransformations.switchMap but they were all using RoomDAOs and I couldn't adapt it to the code that I have.
Any help would be appreciated very much. Thanks in advance.
Maybe that can help you a little bit,
class YourViewModel(
private val courcesRepository: CourcesRepository
) : ViewModel() {
// Private access - mutableLiveData!
private val _coursesList = MutableLiveData<ArrayList<Course>>()
// Public access - immutableLiveData
val coursesList: LiveData<ArrayList<Course>>
get() = _coursesList
init {
// mutableLiveData initialize, automatic is immutable also initialize
_coursesList.postValue(getCourses())
}
// Here you get your data from repository
private fun getCourses(): ArrayList<Course> {
return courcesRepository.getCources()
}
// Search function
fun searchCourses(searchString: String) {
// you hold your data into this methode
val list: ArrayList<Course> = getCources()
if (searchString.isEmpty()) {
// here you reset the data if search string is empty
_coursesList.postValue(list)
} else {
// here you can search the list and post the new one to your LiveData
val filterList = list.filter {
it.name.toLowerCase(Locale.ROOT).startsWith(searchString)
}
filterList.sortedBy { it.price }
_coursesList.postValue(filterList)
}
}
}
The first tip is you should use LiveData like below, that is also recommended from google's jet pack team. The reason is so you can encapsulate the LivaData.
The second tip is you should use kotlin's idiomatic way to filter a list. Your code is readable and faster.
At least is a good idea to make a repository class to separate the concerns in your app.
And some useful links for you:
https://developer.android.com/jetpack/guide
https://developer.android.com/topic/libraries/architecture/livedata
I hope that's helpful for you
Ii is hard to guess the desired outcome, but a possible solution is to use live data for searched string also. And then combine them with coursesList live data into live data for searched courses, like this for example.
val searchStringLiveData: MutableLiveData<String> = MutableLiveData()
val coursesListLiveData: MutableLiveData<ArrayList<Course>> = MutableLiveData()
val searchedCourses: MediatorLiveData<ArrayList<Course>> = MediatorLiveData()
init {
searchedCourses.addSource(searchStringLiveData) {
searchedCourses.value = combineLiveData(searchStringLiveData, coursesListLiveData)
}
searchedCourses.addSource(coursesListLiveData) {
searchedCourses.value = combineLiveData(searchStringLiveData, coursesListLiveData)
}
}
fun combineLiveData(searchStringLiveData: LiveData<String>, coursesListLiveData: LiveData<ArrayList<Course>> ): ArrayList<Course> {
// your logic here to filter courses
return ArrayList()
}
I haven't run the code so I am not 100% sure that it works, but the idea is that every time either of the two live data changes value, searched string or courses, the combine function is executed and the result is set as value of the searchedCourses mediator live data. Also I omitted the logic of the filtering for simplicity.
I am working on an Android App using the MVVM pattern along LiveData (possibly Transformations) and DataBinding between View and ViewModel. Since the app is "growing", now ViewModels contain lots of data, and most of the latter are kept as LiveData to have Views subscribe to them (of course, this data is needed for the UI, be it a Two-Way Binding as per EditTexts or a One-Way Binding). I heard (and googled) about keeping data that represents the UI state in the ViewModel. However, the results I found were just simple and generic. I would like to know if anyone has hints or could share some knowledge on best practices for this case. In simple words, What could be the best way to store the state of an UI (View) in a ViewModel considering LiveData and DataBinding available? Thanks in advance for any answer!
I struggled with the same problem at work and can share what is working for us. We're developing 100% in Kotlin so the following code samples will be as well.
UI state
To prevent the ViewModel from getting bloated with lots of LiveData properties, expose a single ViewState for views (Activity or Fragment) to observe. It may contain the data previously exposed by the multiple LiveData and any other info the view might need to display correctly:
data class LoginViewState (
val user: String = "",
val password: String = "",
val checking: Boolean = false
)
Note, that I'm using a Data class with immutable properties for the state and deliberately don't use any Android resources. This is not something specific to MVVM, but an immutable view state prevents UI inconsistencies and threading problems.
Inside the ViewModel create a LiveData property to expose the state and initialize it:
class LoginViewModel : ViewModel() {
private val _state = MutableLiveData<LoginViewState>()
val state : LiveData<LoginViewState> get() = _state
init {
_state.value = LoginViewState()
}
}
To then emit a new state, use the copy function provided by Kotlin's Data class from anywhere inside the ViewModel:
_state.value = _state.value!!.copy(checking = true)
In the view, observe the state as you would any other LiveData and update the layout accordingly. In the View layer you can translate the state's properties to actual view visibilities and use resources with full access to the Context:
viewModel.state.observe(this, Observer {
it?.let {
userTextView.text = it.user
passwordTextView.text = it.password
checkingImageView.setImageResource(
if (it.checking) R.drawable.checking else R.drawable.waiting
)
}
})
Conflating multiple data sources
Since you probably previously exposed results and data from database or network calls in the ViewModel, you may use a MediatorLiveData to conflate these into the single state:
private val _state = MediatorLiveData<LoginViewState>()
val state : LiveData<LoginViewState> get() = _state
_state.addSource(databaseUserLiveData, { name ->
_state.value = _state.value!!.copy(user = name)
})
...
Data binding
Since a unified, immutable ViewState essentially breaks the notification mechanism of the Data binding library, we're using a mutable BindingState that extends BaseObservable to selectively notify the layout of changes. It provides a refresh function that receives the corresponding ViewState:
Update: Removed the if statements checking for changed values since the Data binding library already takes care of only rendering actually changed values. Thanks to #CarsonHolzheimer
class LoginBindingState : BaseObservable() {
#get:Bindable
var user = ""
private set(value) {
field = value
notifyPropertyChanged(BR.user)
}
#get:Bindable
var password = ""
private set(value) {
field = value
notifyPropertyChanged(BR.password)
}
#get:Bindable
var checkingResId = R.drawable.waiting
private set(value) {
field = value
notifyPropertyChanged(BR.checking)
}
fun refresh(state: AngryCatViewState) {
user = state.user
password = state.password
checking = if (it.checking) R.drawable.checking else R.drawable.waiting
}
}
Create a property in the observing view for the BindingState and call refresh from the Observer:
private val state = LoginBindingState()
...
viewModel.state.observe(this, Observer { it?.let { state.refresh(it) } })
binding.state = state
Then, use the state as any other variable in your layout:
<layout ...>
<data>
<variable name="state" type=".LoginBindingState"/>
</data>
...
<TextView
...
android:text="#{state.user}"/>
<TextView
...
android:text="#{state.password}"/>
<ImageView
...
app:imageResource="#{state.checkingResId}"/>
...
</layout>
Advanced info
Some of the boilerplate would definitely benefit from extension functions and Delegated properties like updating the ViewState and notifying changes in the BindingState.
If you want more info on state and status handling with Architecture Components using a "clean" architecture you may checkout Eiffel on GitHub.
It's a library I created specifically for handling immutable view states and data binding with ViewModel and LiveData as well as glueing it together with Android system operations and business use cases.
The documentation goes more in depth than what I'm able to provide here.
Android Unidirectional Data Flow (UDF) 2.0
Update 12/18/2019: Android Unidirectional Data Flow with LiveData — 2.0
I've designed a pattern based on the Unidirectional Data Flow using Kotlin with LiveData.
UDF 1.0
Check out the full Medium post or YouTube talk for an in-depth explanation.
Medium - Android Unidirectional Data Flow with LiveData
YouTube - Unidirectional Data Flow - Adam Hurwitz - Medellín Android Meetup
Code Overview
Step 1 of 6 — Define Models
ViewState.kt
// Immutable ViewState attributes.
data class ViewState(val contentList:LiveData<PagedList<Content>>, ...)
// View sends to business logic.
sealed class ViewEvent {
data class ScreenLoad(...) : ViewEvent()
...
}
// Business logic sends to UI.
sealed class ViewEffect {
class UpdateAds : ViewEffect()
...
}
Step 2 of 6 — Pass events to ViewModel
Fragment.kt
private val viewEvent: LiveData<Event<ViewEvent>> get() = _viewEvent
private val _viewEvent = MutableLiveData<Event<ViewEvent>>()
override fun onCreate(savedInstanceState: Bundle?) {
...
if (savedInstanceState == null)
_viewEvent.value = Event(ScreenLoad(...))
}
override fun onResume() {
super.onResume()
viewEvent.observe(viewLifecycleOwner, EventObserver { event ->
contentViewModel.processEvent(event)
})
}
Step 3 of 6 — Process events
ViewModel.kt
val viewState: LiveData<ViewState> get() = _viewState
val viewEffect: LiveData<Event<ViewEffect>> get() = _viewEffect
private val _viewState = MutableLiveData<ViewState>()
private val _viewEffect = MutableLiveData<Event<ViewEffect>>()
fun processEvent(event: ViewEvent) {
when (event) {
is ViewEvent.ScreenLoad -> {
// Populate view state based on network request response.
_viewState.value = ContentViewState(getMainFeed(...),...)
_viewEffect.value = Event(UpdateAds())
}
...
}
Step 4 of 6 — Manage Network Requests with LCE Pattern
LCE.kt
sealed class Lce<T> {
class Loading<T> : Lce<T>()
data class Content<T>(val packet: T) : Lce<T>()
data class Error<T>(val packet: T) : Lce<T>()
}
Result.kt
sealed class Result {
data class PagedListResult(
val pagedList: LiveData<PagedList<Content>>?,
val errorMessage: String): ContentResult()
...
}
Repository.kt
fun getMainFeed(...)= MutableLiveData<Lce<Result.PagedListResult>>().also { lce ->
lce.value = Lce.Loading()
/* Firestore request here. */.addOnCompleteListener {
// Save data.
lce.value = Lce.Content(ContentResult.PagedListResult(...))
}.addOnFailureListener {
lce.value = Lce.Error(ContentResult.PagedListResult(...))
}
}
Step 5 of 6 — Handle LCE States
ViewModel.kt
private fun getMainFeed(...) = Transformations.switchMap(repository.getFeed(...)) {
lce -> when (lce) {
// SwitchMap must be observed for data to be emitted in ViewModel.
is Lce.Loading -> Transformations.switchMap(/*Get data from Room Db.*/) {
pagedList -> MutableLiveData<PagedList<Content>>().apply {
this.value = pagedList
}
}
is Lce.Content -> Transformations.switchMap(lce.packet.pagedList!!) {
pagedList -> MutableLiveData<PagedList<Content>>().apply {
this.value = pagedList
}
}
is Lce.Error -> {
_viewEffect.value = Event(SnackBar(...))
Transformations.switchMap(/*Get data from Room Db.*/) {
pagedList -> MutableLiveData<PagedList<Content>>().apply {
this.value = pagedList
}
}
}
Step 6 of 6 — Observe State Change!
Fragment.kt
contentViewModel.viewState.observe(viewLifecycleOwner, Observer { viewState ->
viewState.contentList.observe(viewLifecycleOwner, Observer { contentList ->
adapter.submitList(contentList)
})
...
}