Should livedata be always used in ViewModel? - android

It seems like recommended pattern for fields in viewmodel is:
val selected = MutableLiveData<Item>()
fun select(item: Item) {
selected.value = item
}
(btw, is it correct that the selected field isn't private?)
But what if I don't need to subscribe to the changes in the ViewModel's field. I just need passively pull that value in another fragment.
My project details:
one activity and a bunch of simple fragments replacing each other with the navigation component
ViewModel does the business logic and carries some values from one fragment to another
there is one ViewModel for the activity and the fragments, don't see the point to have more than one ViewModel, as it's the same business flow
I'd prefer to store a value in one fragment and access it in the next one which replaces the current one instead of pass it into a bundle and retrieve again and again manually in each fragment
ViewModel:
private var amount = 0
fun setAmount(value: Int) { amount = value}
fun getAmount() = amount
Fragment1:
bnd.button10.setOnClickListener { viewModel.setAmount(10) }
Fragment2:
if(viewModel.getAmount() < 20) { bnd.textView.text = "less than 20" }
Is this would be a valid approach? Or there is a better one? Or should I just use LiveData or Flow?
Maybe I should use SavedStateHandle? Is it injectable in ViewModel?

To answer your question,
No, It is not mandatory to use LiveData always inside ViewModel, it is just an observable pattern to inform the caller about updates in data.
If you have something which won't be changed frequently and can be accessed by its instance. You can completely ignore wrapping it inside LiveData.
And anyways ViewModel instance will be preserved and so are values inside it.
And regarding private field, MutableLiveData should never be exposed outside the class, as the data flow is always from VM -> View which is beauty of MVVM pattern
private val selected = MutableLiveData<Item>()
val selectedLiveData : LiveData<Item>
get() = selected
fun select(item: Item) {
selected.value = item
}

Related

How can I directly get data from ViewModel in Activity or Fragment in a nice way?

How can I directly get data from ViewModel in Activity or Fragment?
For example, I save data in ViewModel
private val _isPaypalPay = MutableLiveData(false)
val isPaypalPay: LiveData<Boolean>
get() = _isPaypalPay
And I want to use a logic in Activity.
R.id.nsbtn_pay_confirm -> {
if (!isDeliveryInfoFine()) return
if (!viewModel.isPaypalPay.value!! && !isCreditInfoFilledOut()) return
updateAndPay()
}
I have two options
viewModel.isPaypalPay.value!!
make a function that returns _isPaypalPay.value!!
fun getIsPaypalPay() = _isPaypalPay.value!!
Or is there any better way?
The whole point of livedata is that you can listen to changes in the fragment and that it will be lifecycleaware. If you are trying to directly access the data in livedata, you are doing it wrong.
Try reading here: https://developer.android.com/topic/libraries/architecture/livedata

How should I go about implementing MVVM architecture pattern in my project?

I know this is a very documented topic, but I couldn't find a way to implement it in my project, even after spending hours trying to figure it out.
My root problem is that I have a RecyclerView with an Adapter whose content isn't updating as I'd like. I'm a beginner in Android, so I didn't implement any MVVM or such architecture, and my project only contains a repository, fetching data from Firebase Database, and passing it to a list of ShowModel, a copy of said list being used in my Adapter to display my shows (In order to filter/sort them without modifying the list with all shows).
However, when adding a show to the database from another Activity, my Adapter isn't displaying the newly added show (as detailed here)
I was told to use LiveData and ViewModel, but even though I started understanding how it works after spending time researching it, I don't fully get how I should use it in order to implement it in my project.
Currently I have the following classes:
The Adapter:
class ShowAdapter(private val context: MainActivity, private val layoutId: Int, private val textNoResult: TextView?) : RecyclerView.Adapter<ShowAdapter.ViewHolder>(), Filterable {
var displayList = ArrayList(showList)
class ViewHolder(view : View) : RecyclerView.ViewHolder(view){
val showName: TextView = view.findViewById(R.id.show_name)
val showMenuIcon: ImageView = view.findViewById(R.id.menu_icon)
}
#SuppressLint("NewApi")
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(layoutId, parent, false)
return ViewHolder(view)
}
#SuppressLint("NewApi", "WeekBasedYear")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val currentShow = displayList[position]
val index = holder.adapterPosition
holder.showName.text = currentShow.name
holder.itemView.setOnClickListener{ // Display show content
val intent = Intent(context, DetailsActivity::class.java)
intent.putExtra("position", index)
startActivity(context, intent, null)
}
holder.showMenuIcon.setOnClickListener{
val popupMenu = PopupMenu(context, it)
popupMenu.menuInflater.inflate(R.menu.show_management_menu, popupMenu.menu)
popupMenu.show()
popupMenu.setOnMenuItemClickListener {
when(it.itemId){
R.id.edit -> { // Edit show
val intent = Intent(context, AddShowActivity::class.java)
intent.putExtra("position", index)
startActivity(context, intent, null)
return#setOnMenuItemClickListener true
}
R.id.delete -> { // Delete show
val repo = ShowRepository()
repo.deleteShow(currentShow)
displayList.remove(currentShow)
notifyItemRemoved(index)
return#setOnMenuItemClickListener true
}
else -> false
}
}
}
}
override fun getItemCount(): Int = displayList.size
// Sorting/Filtering methods
}
The fragment displaying the adapter:
class HomeFragment : Fragment() {
private lateinit var context: MainActivity
private lateinit var verticalRecyclerView: RecyclerView
private lateinit var buttonAddShow: Button
private lateinit var showsAdapter: ShowAdapter
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_home, container, false)
context = getContext() as MainActivity
buttonAddShow = view.findViewById(R.id.home_button_add_show)
buttonAddShow.setOnClickListener{ // Starts activity to add a show
startActivity(Intent(context, AddShowActivity::class.java))
}
verticalRecyclerView = view.findViewById(R.id.home_recycler_view)
showsAdapter = ShowAdapter(context, R.layout.item_show, null)
verticalRecyclerView.adapter = showsAdapter
return view
}
}
The MainActivity:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
loadFragment(HomeFragment())
}
private fun loadFragment(fragment: Fragment){
val repo = ShowRepository()
if(showsListener != null) databaseRef.removeEventListener(showsListener!!)
repo.updateData{
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(R.id.fragment_container, fragment)
transaction.addToBackStack(null)
if(supportFragmentManager.isStateSaved)transaction.commitAllowingStateLoss()
else transaction.commit()
}
}
}
The repository:
class ShowRepository {
object Singleton{
val databaseRef = FirebaseDatabase.getInstance().getReference("shows")
val showList = arrayListOf<ShowModel>()
var showsListener: ValueEventListener? = null
}
fun updateData(callback: () -> Unit){
showsListener = databaseRef.addValueEventListener(object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
showList.clear()
for(ds in snapshot.children){
val show = ds.getValue(ShowModel::class.java)
if(show != null) showList.add(show)
}
callback()
}
override fun onCancelled(p0: DatabaseError) { }
})
}
fun insertShow(show: ShowModel){
databaseRef.child(show.id).setValue(show)
}
fun deleteShow(show: ShowModel){
databaseRef.child(show.id).removeValue()
}
}
From what I understand of LiveData and ViewModel, what I should do is creating a ShowViewModel containing a MutableLiveData<List<ShowModel>> containing the shows, and then observe it in my HomeFragment and update the adapter depending on the changes happening. However, everytime I start something to implement it, I encounter a situation where I'm lost and don't know what I should do, which leads me back to square one once again. I've been trying this for more than a week without progressing even a little bit, and that's why I'm here, hoping for some insight.
Sorry for the silly question and the absurd amount of informations, and hoping someone will be able to help me understand what I do wrong/should do.
(this ended up longer than I meant it to be - hope it's not too much! There's a lot to learn, but you don't have to make it super complicated at first)
Broadly, working backwards, it should go like this:
Adapter
displays whatever the Fragment tells it to (some kind of setData function that updates its internal list and refreshes)
passes events to the Fragment (deleteItem(item), showDetails(item) etc.) - don't have the Adapter doing things like starting Activites, that's not its responsibility
Fragment
grabs a reference to any ViewModels (only certain components like Fragments and Activities can actually "own" them)
observes any LiveData (or collects Flows if you're doing it that way) on the VM, and updates stuff in the UI in response
e.g. model.shows.observe(viewLifecycleOwner) { shows -> adapter.setData(shows) }
handles UI events and calls methods on the VM in response, e.g. click listeners, events from the Adapter
ViewModel
acts as a go-between for the UI (the Fragment) and the data layer (the repository)
exposes methods for handling events like deleting items, interacts with the data layer as required (e.g. calling the appropriate delete function)
exposes data state for the UI to observe, so it can react to changes/updates (e.g. a LiveData containing the current list of shows that the data layer has provided)
That's the basic setup - the VM exposes data which the UI layer observes and reacts to, by displaying it. The UI layer also produces events (usually down to user interaction) which are passed to the VM. You can read more about this general approach in this guide about app architecture - it's worth reading because not only is it recommended as a way to build apps, a lot of the components you use in modern Android are designed with this kind of approach in mind (like the reactive model of wiring stuff up).
You could handle the Adapter events like this:
// in your Adapter
var itemDeletedListener: ((Item) -> Unit)? = null
// when the delete event happens for an item
itemDeletedListener?.invoke(item)
// in your Fragment
adapter.itemDeletedListener = { viewModel.deleteItem(it) }
which is easier than implementing an interface, and lets you wire up your Adapter similar to doing setOnClickListener on a button. Notice we're passing the actual Item object here instead of a list index - generally this is easier to work with, you don't need to maintain multiple copies of a list just so you can look up an index given to you by something else. Passing a unique ID can make sense though, especially if you're working with a database! But usually the object itself is more useful and consistent
The data layer is the tricky bit - the ViewModel needs to communicate with that to get the current state. Say you delete an item - you then need to get the current, updated list of shows. You have three approaches:
Call the delete function, immediately after fetch the current data, and set it on the appropriate LiveData
This can work, but it's not very reactive - you're doing one action, then immediately doing another because you know your data is stale. It would be better if the new data just arrived automatically and you could react to that by pushing it out. The other issue is that calling the delete function might not have an immediate effect - if you fetch the current data, nothing might have changed yet. It's better if the data layer is responsible for announcing updates.
This is the simplest approach though, and probably a good start! You could run this task in a coroutine (viewModelScope.launch { // delete and fetch and update LiveData }) so any slowness doesn't block the current thread.
Have the data layer's functions return the current, updated data that results
Similar to above, you're just sort of pushing the fetching into the data layer. This requires all those functions to be written to return the current state, which could take a while! And depending on what data you want, this might be impossible - if you have an active query on some data, how does the function know what specific data to return?
Make the ViewModel observe the data it wants, so when the data layer updates, you get the results automatically
This is the recommended reactive approach - again it's that two-way idea. The VM calling a function on the data layer is completely separate from the VM receiving new data. One thing just happens as a natural consequence of the other, they don't need to be tied together. You just need to wire them up right!
How do you actually do that though? If you're working with something like Room, that's already baked in. Queries can return async data providers like LiveData or Flows - your VM just needs to observe those and expose the results, or just expose them directly. That way, when a table is updated, any queries (like the current shows) push a new value, and the observers receive it and do whatever they need to do, like telling the Adapter to display the data. It all Just Works once it's wired up.
Since you have your own repo, you need to expose your own data sources. You could have a currentShows LiveData or (probably preferably) the flow equivalent, StateFlow. When the repo initialises, and when any data is changed, it updates that currentShows data. Anything observing that (e.g. the VM, the Fragment through a LiveData/Flow that the VM exposes) will automatically get the new values. So broadly:
// Repo
// this setup is exactly the same as your typical LiveData, except you need an initial value
private val _currentShows = MutableStateFlow<List<Show>>(emptyList()) // or whatever default
val currentShows: StateFlow<List<Show>> = _currentShows
fun deleteItem(item: Item) {
// do the deletion
// get the updated show list
_currentShows.value = updatedShowList
}
// ViewModel
// one way of doing things - you have a lot of options! This literally just exposes
// the state from the data layer, and turns it into a LiveData (if you want that)
val currentShows = repo.currentShows.asLiveData()
// Fragment
// wire things up so you handle new data as it arrives
viewModel.currentShows.observe(viewLifecycleOwner) { shows -> adapter.setData(shows) }
That's basically it. I've skimmed over a lot because honestly, there's a lot to learn with this - especially about Flows and coroutines if you're not already familiar with those. But hopefully that gives you an overview of the general idea, and don't be afraid to take shortcuts (like just updating your data in the ViewModel by setting its LiveData values) while you're learning and getting the hang of it. Definitely give that app architecture guide a read, and also the guides for ViewModels and LiveData. It'll start to click when you get the general idea!

Coroutines and ViewModel best practice for separation of concers

I needed some direction on being able to observe some flow as live data in my ViewModel class.
For example: The ViewModel class has the field userDataFlow below which combines a few streams of Data Flow. I want to be able to extract out the work of that field into a separate class and let all of the inner working take place there and just want to observe the LiveData to the field in the ViewModel. I would need to pass in few things in the Parameter of that class from the ViewModel which the Flow would need in order to work. Not sure if this is a good practice. Basically, let my ViewModel observe the result and pass it along to the View.
val userDataFlow: Flow<List<UserData>> =
combine(
familyChannel.asFlow(),
userRealTimeData.asFlow,
).asLiveData()
}
Sounds like you need a UseCase/Interactor which in short processes data coming from different repositories.
For example suppose you want a list of your friends that live in countries with a COVID-19 infection rate above a certain value:
class GetFriendsInDangerUseCase(
private val friendsRepository: FriendsRepository,
private val countryRepository: CountryRepository)
fun invoke(threshold: Float) = friendsRepository.friendsFlow
.combine(countryRepository.countriesFlow) { friends, countries ->
val dangerousCountries = countries.filter { it.infectionRate >= threshold }
friends.filter { it.country in dangerousCountries }
}
Then use it like this from your VM:
val friendsInDangerFlow = getFriendsInDanger(0.5)

LiveData is not getting observed for one specific scenario

I have 3 LiveData objects in my ViewModel, I'm applying transformations to these, the problem is 2 LiveData are getting observed while the other one is not, I've tried different solutions like changing the way ViewModel is initialized or the way LiveData is initialized but nothing has worked for me.
class MyClass : ViewModel() {
init {
_originallist.value = Instance.getOrignalList()
}
// observed in Fragment A
val a: LiveData<List<A>> = Transformations.map(_searchText, ::extractA)
// observed in Fragment B
val b: LiveData<List<B>> = Transformations.map(_originallist, ::extractB)
// observed in Fragment C
val c: LiveData<List<C>> = Transformations.map(_originalList, ::extractC)
// Called from Fragment D to change the _searchText liveData
fun setSearchText(text: String) {
_searchText.value = text
}
fun extractA(text: String): List<A> {
val temp = ArrayList<A>()
list.foreach {
if (it.contains(text, false) temp . add (it)
}
return temp
}
fun extractB(list: List<B>): List<B> {
// do something
}
fun extractC(list: List<C>): List<C> {
// do something
}
}
If you have noticed that the LiveData b and c are getting initialized just once hence I'm able to see the data in my RecyclerView, but for the LiveData A, the search text can change based on user input, this is where my fragment is not observing this live data.
Things to note: This is a common ViewModel for my 3 viewPager fragments, LiveData a is observed in one fragment, B in another and C in another.
Eventually, I have to apply the search for other 2 fragments as well.
When I was debugging the observer lines in my fragment was getting skipped, another thing I would like to point out is that the code in all 3 fragments is same except that they are observing different LiveData
EDIT: What i have noticed now is that, since i'm calling the setSearchText() from Fragment D i'm able to observe the changes of LiveData A in Fragment D but i want to observe that in Fragment A but not able to.
I have a search bar in fragment D and bottom of that i have a view pager with 3 fragments, all 4 fragments have a common viewModel, Am i doing something wrong here or is this not the way to implement this?
TIA
Finally found the root cause, the problem was that the viewModel was getting its own lifecycle owner in each of fragment, the solution to this was to declare and initialize the viewModel object in the parent activity of the fragments and use its instace in the fragment to observe the LiveData
The problem is:
Your function extractA in
val a: LiveData<List<A>> = Transformations.map(_searchText, ::extractA)
will only be executed when the value of _searchText will change.
That's how Transformations work, they apply the given function whenever the value changes.

Updating Immutable View State Values in Android Unidirectional Data Flow

Question
I'm looking to refactor an immutable view state's values in the Android ViewModel (VM) in order to do the following:
Update the view state in the VM cleanly without copying the entire view state
Keep the view state data immutable to the view observing updates
I've built an Android Unidirectional Data Flow (UDF) pattern using LiveData to update the view state changes in the VM that are observed in the view.
See: Android Unidirectional Data Flow with LiveData — 2.0
Full sample code: Coinverse Open App
Implementation
The existing implementation uses nested LiveData.
One LiveData val to store the view state in the VM
Nested LiveData for the view state attributes as immutable vals
// Stored as viewState LiveData val in VM
data class FeedViewState(
val contentList: LiveData<PagedList<Content>>
val anotherAttribute: LiveData<Int>)
The view state is created in the VM's init{...}.
Then, in order to update the view state it must be copied and updated with the given attribute because it is an immutable val. If the attribute were to be mutable, it could be reassigned cleanly without the copy in the VM. However, being immutable is important to make sure the view cannot unintentionally change the val.
class ViewModel: ViewModel() {
val viewState: LiveData<FeedViewState> get() = _viewState
private val _viewState = MutableLiveData<FeedViewState>()
init {
_viewState.value = FeedViewState(
contentList = getContentList(...)
anotherAttribute = ...)
}
override fun swipeToRefresh(event: SwipeToRefresh) {
_viewState.value = _viewState.value?.copy(contentList = getContentList(...))
}
}
I am not sure if having "nested LiveData" is okay. When we work with any event-driven design implementation (LiveData, RxJava, Flow) we usually required to assume that the discrete data events are immutable and operations on these events are purely functional. Being immutable is NOT synonymous with being read-only(val). Immutable means immutable. It should be time-invariant and should work exactly the same way under any circumstances. That is one reason why I feel strange to have LiveData or ArrayList members in the data class, regardless of whether they are defined read-only or not.
Another, technical reason why one should avoid nested streams: it is almost impossible to observe them correctly. Every time there is a new data event emitted through the outer stream, the developers must make sure to remove inner subscriptions before observing the new inner stream, otherwise it can cause all sorts of problems. What's the point of having life-cycle aware observers, when the developers need to manually unsubscribe them?
In almost all scenarios, nested streams can be converted to one layer of stream. In your case:
class ViewModel: ViewModel() {
val contentList: LiveData<PagedList<Content>>
val anotherAttribute: LiveData<Int>
private val swipeToRefreshTrigger = MutableLiveData<Boolean>(true)
init {
contentList = Transformations.switchMap(swipeToRefreshTrigger) {
getContentList(...)
}
anotherAttribute = ...
}
override fun swipeToRefresh(event: SwipeToRefresh) {
swipeToRefreshTrigger.postValue(true)
}
}
Notes on PagedList:
PagedList is also mutable, but I guess it is something we just have to live with. PagedList usage is another topic so I won't be discussing it here.
Use Kotlin StateFlow - 7/21/20 Update
Rather than having two state classes with LiveData, one private and mutable, the other public and immutable, with the Kotlin coroutines 1.3.6 release a StateFlow value can be updated in the ViewModel, and rendered in the view's activity/fragment through an interface method.
See: Android Model-View-Intent with Kotlin Flow
Remove Nested LiveData, Create State Classes - 2/11/20
Approach: Store immutable LiveData state and effects in a view state and view effect class inside the ViewModel that is publicly accessible.
The view state and view effects attributes could be LiveData values directly in the VM. However, I'd like to organize the view state and effects into separate classes in order for the view to know whether it observing a view state or a view effect.
class FeedViewState(
_contentList: MutableLiveData<PagedList<Content>>,
_anotherAttribute: MutableLiveData<Int>
) {
val contentList: LiveData<PagedList<Content>> = _contentList
val anotherAttribute: LiveData<Int> = _anotherAttribute
}
The view state is created in the VM.
class ViewModel: ViewModel() {
val feedViewState: FeedViewState
private val _contentList = MutableLiveData<PagedList<Content>>()
private val _anotherAttribute = MutableLiveData<Int>()
init {
feedViewState = FeedViewState(_contentList, _anotherAttribute)
}
...
fun updateContent(){
_contentList.value = ...
}
fun updateAnotherAttribute(){
_anotherAttribute.value = ...
}
}
Then, the view state attributes would be observed in the activity/fragment.
class Fragment: Fragment() {
private fun observeViewState() {
feedViewModel.feedViewState.contentList(viewLifecycleOwner){ pagedList: PagedList<Content> ->
adapter.submitList(pagedList)
}
feedViewModel.feedViewState.anotherAttribute(viewLifecycleOwner){ anotherAttribute: Int ->
//TODO: Do something with other attribute.
}
}
}

Categories

Resources