I'm working on an app where a data source is defined in a Provider/Manager. This class (let's call it InfoProvider) is pretty much just a black box - it has some properties, and calls that when executed, result in change of these properties (similar to how a Repository work, but instead of calls returning values, they execute an async call that will result in the change of one or more properties of the provider).
This setup is specifically for Bluetooth Low Energy - we all know how badly managed it is on Android, and I wanted to make it as asynchronous as possible, and use databinding+livedata+viewmodels to achieve a fully responsive architecture.
With Xamarin this would be easy, just define the InfoProvider as a field in the ViewModel, and bind to its fields. However I don't necessarily want to expose all fields in all viewmodels (some might only need the battery status of the device, some might need full access, some might just execute functions without waiting for a response). For functions, it's easy to proxy, but for LiveData<T> I haven't found much information. How would I go forward and "pass around" the LiveData field?
Example:
class InfoProvider {
var batteryPercent = MutableLiveData<Int>()
public fun requestBatteryUpdate() {
[...]
batteryPercent.value = newValue
}
}
// ViewModel for accessing device battery, inheriting from architecture ViewModel
class DeviceBatteryViewModel: ViewModel() {
var batteryPercentage = MutableLiveData<Int>()
val infoProvider: InfoProvider by inject()
init {
// TODO: Subscribe this.batteryPercentage to infoProvider.batteryPercent
fun onButtonClick() {
infoProvider.requestBatteryUpdate()
}
}
class DeviceBatteryFragment: Fragment() {
val ViewModel: DeviceBatteryViewModel by inject()
private lateinit var binding: DeviceBatteryBinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
DeviceBatteryBinding.inflate(inflater, container, false).also { binding = it }.root
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.viewModel = this.ViewModel
}
}
// res/layout/fragment_devicebattery.xml
<layout [namespaces]>
<data class="bindings.DeviceBatteryBinding>
<variable android:name="viewModel" type=".DeviceBatteryViewModel />
</data>
<WhatEverLayout [...]>
<TextView [...] android:text="#{viewModel.batteryPercentage.toString()}" />
<Button [...] android:onClick="#{() -> viewModel.onButtonClick()}" />
</WhatEverLayout>
</layout>
What I'd like to avoid is the Rx-style .observe(() -> {}), .subscribe(() -> {}) etc. kind of exchanges. Can this be done (i.e. if I assign the value of infoProvider.batteryPercent to the VM's batteryPercentage field, will it also receive updates), or should I bind directly to the infoProvider?
There is no way to "pass around" the LiveData field without calling batteryPercent.observe(...). Additionally, you will need to use a Lifecycler Owner to Observe the field (unless you want to ObserveForever which is not a recommended solution).
My suggestion would be something like this:
InfoProvider {
val repositoryBatteryUpdate = BehaviorSubject.create<Int>()
fun observeRepositoryBatteryUpdate(): Observable<Int> {
return repositoryBatteryUpdate
}
fun requestBatteryUpdate(){
// Pseudo code for actually update
// Result goes into repositoryBatteryUpdate.onNext(...)
}
}
ViewModel{
val status: MutableLiveData<Int>
init{
repository.observeRepositoryItems()
.subscribe( update -> status.postValue(status))
}
fun update(){
repository.requestBatteryUpdate()
}
}
Fragment{
viewModel.status.observe() // <-- Here you observe Updates
viewModel.update()
}
Note that you will have to dispose the subscription in the ViewModel onCleared.
Note that all of this is pseudo code and it should be done a lot cleaner than this.
Related
I need a Factory to support Viewmodel WITH ARGUMENTS. I tried to implement the class which is commended "AbstractSavedStateViewModelFactory". I applied the trouble in the short way.
I don't know if I am failing at ShopFragment. Some ideas please.
I am working with HandleSavedState and a customizable ModelFactory which receive parameters, extending AbstractSavedStateViewModelFactory, but I am not currently be able to SAVE VALUE.
!! BY THE WAY: I don't know if this line(ShopFragment.kt) is correct:
ShopViewModelFactory(requireNotNull(requireActivity()).application, this, quantity, idupdate) maybe it is
I had tried harder but always receive null in Log. I don't want Inject dependencies with some responses that I have seen like "Hilt"
dependencies
implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.0-alpha02"
ViewModelfactory.kt
class ShopViewModelFactory(
val application: Application,
val owner: SavedStateRegistryOwner,
val quantity: Int, val idupdate: Int
) : AbstractSavedStateViewModelFactory(owner, null){
override fun <T : ViewModel?> create( key: String, modelClass: Class<T>,
handle: SavedStateHandle ): T {
if (modelClass.isAssignableFrom(ShopViewModel::class.java)) {
#Suppress("UNCHECKED_CAST")
return ShopViewModel(application, handle, quantity,idupdate) as T
}
throw IllegalArgumentException("Unable to construct SHOPviewmodel")
}
}
In the Fragment: ShopFragment.kt
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? {
bind = DataBindingUtil.inflate(inflater, R.layout.fragment_shop, container, false)
var quantity = ShopFragmentArgs.fromBundle(requireArguments()).quantity
var idupdate = ShopFragmentArgs.fromBundle(requireArguments()).idproduct
var factory = ShopViewModelFactory(requireNotNull(requireActivity()).application,
this,
quantity,
idupdate)
vmShop =ViewModelProvider(this, factory).get(ShopViewModel::class.java)
In ShopViewModel.kt:
class ShopViewModel (application: Application,
private val savedStateHandle: SavedStateHandle,
quantity: Int, idupdate: Int): ViewModel() {
private val memoria2 : MutableLiveData<String> = savedStateHandle.getLiveData("user","init ")
init{
Log.i("DICE","RECUPERADO ${memoria2}")
addToCart()
}
private fun addToCart(){
memoria2.value += "Some data"
// that previous sentence suppose to save persist data
// !!if I add savedStateHandle.set("user", memoria2.value) still is not working
}
}
This supposed to initialize if savedInstanceState.value is null:
savedStateHandle.getLiveData("user","init ")
Expected response:
a. I/DICE RECUPERADO init Some data
b. I/DICE RECUPERADO init Some data Some data
When you're using a SavedStateHandle and getLiveData for a particular key, you're not supposed to set the value on that LiveData - you're supposed to set it on the SavedStateHandle using the same key. That way, the data is stored, and the LiveData updates automatically (because getLiveData links it to the stored state)
So you want to do it this way:
private fun addToCart(){
savedStateHandle["user"] = "Some data"
}
and it should just work!
Remember that SavedStateHandles are lost when the app is explicitly closed. They only maintain the state while the app is running, so it can survive things like Activities getting destroyed, or the app getting closed in the background and recreated by the system - it's like savedInstanceState in an Activity or Fragment. If you need to store data between sessions, you need to persist it in something like SharedPreferences or a database
I'm a rookie Android developer, and could use a little guidance regarding traversing a LiveData List in the ViewModel.
I am basing my app on the MVVM design, and it is simply scanning folders for images, and adding some folders to a favourites list I store in a database. During the scans, I need to check with the stored favourites to see if any of the scanned folders are favourites.
It is the "check against the stored favourites" part that gives me trouble.
Here are the relevant bits from my fragment:
class FoldersFragment : Fragment(), KodeinAware {
override val kodein by kodein()
private val factory: FoldersViewModelFactory by instance()
private var _binding: FragmentFoldersBinding? = null
private val binding get() = _binding!!
private lateinit var viewModel: FoldersViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentFoldersBinding.inflate(inflater, container, false)
val root: View = binding.root
return root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(this, factory).get(FoldersViewModel::class.java)
binding.rvFolderList.layoutManager = GridLayoutManager(context, gridColumns)
val adapter = FolderItemAdapter(listOf(), viewModel)
binding.rvFolderList.adapter = adapter
viewModel.getFolderList().observe(viewLifecycleOwner, {
adapter.folderItems = it
binding.rvFolderList.adapter = adapter // Forces redrawing of the recyclerview
})
...
}
Now, that observer work just fine - it picks up changes and my RecyclerView responds with delight; all is well.
Here are the relevant bits from my RecyclerView adapter:
class FolderItemAdapter(var folderItems: List<FolderItem>, private val viewModel: FoldersViewModel):
RecyclerView.Adapter<FolderItemAdapter.FolderViewHolder>() {
private lateinit var binding: FolderItemBinding
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FolderViewHolder {
binding = FolderItemBinding.inflate(LayoutInflater.from(parent.context))
val view = binding.root
return FolderViewHolder(view)
}
override fun onBindViewHolder(holder: FolderViewHolder, position: Int) {
val currentItem = folderItems[position]
...
if (viewModel.isFavourite(currentItem)) {
// do stuff
}
...
}
}
And with that, my problem; the check viewModel.isFavourite(currentItem)always returns false.
The implementation in my ViewModel is:
class FoldersViewModel(private val repository: FoldersRepository) : ViewModel() {
fun getImageFolders() = repository.getImageFolders()
fun isFavourite(item: FolderItem): Boolean {
var retval = false
getImageFolders().value?.forEach {
if (it.path == item.path) {
retval = true
}
}
}
}
The `getImageFolders() function is straight from the repository, which again is straight from the Dao:
#Dao
interface FoldersDao {
#Query("SELECT * FROM image_folders")
fun getImageFolders(): LiveData<List<FolderItem>>
}
My problem is that I simply can't traverse that list of favourites in the ViewModel. The isFavourite(item: FolderItem) function always returns false because getImageFolders().value always is null. When I check getImageFolders() it is androidx.room.RoomTrackingLiveData#d0d6d31.
And the conundrum; the observer is doing the exact same thing? Or isn't it?
I suspect I am not understanding something basic here?
Your getImageFolders() function retrieves something asynchronously from the database, because you specified that it returns a LiveData. When you get the LiveData back, it will not immediately have a value available. That's why your .value?.forEach is never called. value is still null because you're trying to read it immediately. A LiveData is meant to be observed to obtain the value when it arrives.
There are multiple ways to make a DAO function return something without blocking the current thread. (Handy table here.) Returning a LiveData is one way, but it's pretty awkward to use if you only want one value back. Instead, you should use something from the One-shot read row in the linked table.
If you aren't using RxJava or Guava libraries, that leaves a Kotlin coroutines suspend function as the natural choice.
That would make your Dao look like:
#Dao
interface FoldersDao {
#Query("SELECT * FROM image_folders")
suspend fun getImageFolders(): List<FolderItem>
}
And then your ViewModel function would look like:
suspend fun isFavourite(item: FolderItem): Boolean {
return getImageFolders().any { it.path == item.path }
}
Note that since it is a suspend function, it can only be called from a coroutine. This is necessary to avoid blocking the main thread. If you're not ready to learn coroutines yet, you can replace this function with a callback type function like this:
fun isFavoriteAsync(item: FolderItem, callback: (Boolean)->Unit) {
viewModelScope.launch {
val isFavorite = getImageFolders().any { it.path == item.path }
callback(isFavorite)
}
}
and at the call site use it like
viewModel.isFavoriteAsync(myFolderItem) { isFavorite ->
// do something with return value when it's ready here
}
your getImageFolder() is an expensive function so
getImageFolders().value?.forEach {
if (it.path == item.path) {
retval = true
}
}
in this part the value is still null that is why it returns false.
the solution is to make sure the value is not null. Do not check null inside isFavorite function instead call isFavorite() function only when getImageFolder() is done the operation.
What you should do is something like this
observe the liveData of imageFolders
ondatachange check if the data is null or not
if it is not null update UI and use isFavourite() function
I am working with the MVVM architecture.
The code
When I click a button, the method orderAction is triggered. It just posts an enum (further logic will be added).
ViewModel
class DashboardUserViewModel(application: Application) : SessionViewModel(application) {
enum class Action {
QRCODE,
ORDER,
TOILETTE
}
val action: LiveData<Action>
get() = mutableAction
private val mutableAction = MutableLiveData<Action>()
init {
}
fun orderAction() {
viewModelScope.launch(Dispatchers.IO) {
// Some queries before the postValue
mutableAction.postValue(Action.QRCODE)
}
}
}
The fragment observes the LiveData obj and calls a method that opens a new fragment. I'm using the navigator here, but I don't think that the details about it are useful in this context. Notice that I'm using viewLifecycleOwner
Fragment
class DashboardFragment : Fragment() {
lateinit var binding: FragmentDashboardBinding
private val viewModel: DashboardUserViewModel by lazy {
ViewModelProvider(this).get(DashboardUserViewModel::class.java)
}
private val observer = Observer<DashboardUserViewModel.Action> {
// Tried but I would like to have a more elegant solution
//if (viewLifecycleOwner.lifecycle.currentState == Lifecycle.State.RESUMED)
it?.let {
when (it) {
DashboardUserViewModel.Action.QRCODE -> navigateToQRScanner()
DashboardUserViewModel.Action.ORDER -> TODO()
DashboardUserViewModel.Action.TOILETTE -> TODO()
}
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentDashboardBinding.inflate(inflater, container, false)
binding.viewModel = viewModel
binding.lifecycleOwner = this
viewModel.action.observe(viewLifecycleOwner, observer)
// Tried but still having the issue
//viewModel.action.reObserve(viewLifecycleOwner, observer)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
// Tried but still having the issue
//viewModel.action.removeObserver(observer)
}
private fun navigateToQRScanner() {
log("START QR SCANNER")
findNavController().navigate(LoginFragmentDirections.actionLoginToPrivacy())
}
}
The problem
When I close the opened fragment (using findNavController().navigateUp()), the Observe.onChanged of DashboardFragment is immediately called and the fragment is opened again.
I have already checked this question and tried all the proposed solutions in the mentioned link (as you can see in the commented code). Only this solution worked, but it's not very elegant and forces me to do that check every time.
I would like to try a more solid and optimal solution.
Keep in mind that in that thread there was no Lifecycle implementation.
The issue happens because LiveData always post the available data to the observer if any data is readily available. Afterwords it will post the updates. I think it is the expected working since this behaviour has not been fixed even-though bug raised in issue tracker.
However there are many solutions suggested by developers in SO, i found this one easy to adapt and actually working just fine.
Solution
viewModel.messagesLiveData.observe(viewLifecycleOwner, {
if (viewLifecycleOwner.lifecycle.currentState == Lifecycle.State.RESUMED) {
//Do your stuff
}
})
That's how LiveData works, it's a value holder, it holds the last value.
If you need to have your objects consumed, so that the action only triggers once, consider wrapping your object in a Consumable, like this
class ConsumableValue<T>(private val data: T) {
private val consumed = AtomicBoolean(false)
fun consume(block: ConsumableValue<T>.(T) -> Unit) {
if (!consumed.getAndSet(true)) {
block(data)
}
}
}
then you define you LiveData as
val action: LiveData<ConsumableValue<Action>>
get() = mutableAction
private val mutableAction = MutableLiveData<ConsumableValue<Action>>()
then in your observer, you'd do
private val observer = Observer<ConsumableValue<DashboardUserViewModel.Action>> {
it?.consume { action ->
when (action) {
DashboardUserViewModel.Action.QRCODE -> navigateToQRScanner()
DashboardUserViewModel.Action.ORDER -> TODO()
DashboardUserViewModel.Action.TOILETTE -> TODO()
}
}
}
UPDATE
Found a different and still useful implementation of what Frances answered here. Take a look
I’m facing some issues with databinding and livedata, when I have a custom object.
For example:
I have a MutableLiveData
val user = MutableLiveData<User>()
and I’m using two way databinding with
#={viewModel.user.name}
But my observer its not been fired inside Fragment with
viewModel.user.observer.
When I put a breakpoint inside FragmentBinding generated class, I can see setValue been called and userLiveData’s user values with data.
The problem is with observer not been fired inside Fragment.
Anyone knows what I am doing wrong there?
EDIT 1
Below is my fragment code:
val infoPessoalViewModel: InfoPessoalViewModel by viewModel()
lateinit var bindingView: FragmentInfoPessoalBinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
bindingView = DataBindingUtil.inflate(inflater, R.layout.fragment_info_pessoal, container, false)
return bindingView.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
bindingView.apply {
lifecycleOwner = this#InfoPessoalFragment
viewModel = infoPessoalViewModel
}
infoPessoalViewModel.user.observe(this, Observer { user ->
user.confirmEmail?.let {
//NOT FIRED HERE
Log.d("LiveData","Fired!")
}
})
}
EDIT 2
Sorry, I was giving a example variables with diff names.
Your BindingAdapter logic should be able to convert EditText input into a User instance and vice versa. Let's say User instance looks like this:
User.kt
class User(val username: String)
Then an example adapter should be:
MyBindingAdapters.kt
/**
* Convert EditText input into a User instance.
*/
#InverseBindingAdapter(attribute = "android:text")
fun getUser(view: EditText): User {
return User(view.text.toString())
}
/**
* Convert a User instance into EditText text
*/
#BindingAdapter("android:text")
fun setUser(view: EditText, newUser: User?) {
if (newUser?.username != view.text.toString()) {
view.setText(newUser?.username)
}
}
In your layout file, bind to userLiveData
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="#={viewModel.userLiveData}"/>
Notice how the adapters are bijective, meaning one User is paired with exactly one String and vice versa. If User class is more complex, then two-way binding to MutableLiveData<User> doesn't really make sense. In such case, you should bind it to MutableLiveData<String> instead and manually update User instance in the view model.
I have an activity, TabBarActivity that hosts a fragment, EquipmentRecyclerViewFragment. The fragment receives the LiveData callback but the Activity does not (as proofed with breakpoints in debugging mode). What's weird is the Activity callback does trigger if I call the ViewModel's initData method. Below are the pertinent sections of the mentioned components:
TabBarActivity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initVM()
setContentView(R.layout.activity_nav)
val equipmentRecyclerViewFragment = EquipmentRecyclerViewFragment()
supportFragmentManager
.beginTransaction()
.replace(R.id.frameLayout, equipmentRecyclerViewFragment, equipmentRecyclerViewFragment.TAG)
.commit()
navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener)
}
var eVM : EquipmentViewModel? = null
private fun initVM() {
eVM = ViewModelProviders.of(this).get(EquipmentViewModel::class.java)
eVM?.let { lifecycle.addObserver(it) } //Add ViewModel as an observer of this fragment's lifecycle
eVM?.equipment?.observe(this, loadingObserver)// eVM?.initData() //TODO: Not calling this causes Activity to never receive the observed ∆
}
val loadingObserver = Observer<List<Gun>> { equipment ->
...}
EquipmentRecyclerViewFragment
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
columnCount = 2
initVM()
}
//MARK: ViewModel Methods
var eVM : EquipmentViewModel? = null
private fun initVM() {
eVM = ViewModelProviders.of(this).get(EquipmentViewModel::class.java)
eVM?.let { lifecycle.addObserver(it) } //Add ViewModel as an observer of this fragment's lifecycle
eVM?.equipment?.observe(this, equipmentObserver)
eVM?.initData()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_equipment_list, container, false)
if (view is RecyclerView) { // Set the adapter
val context = view.getContext()
view.layoutManager = GridLayoutManager(context, columnCount)
view.adapter = adapter
}
return view
}
EquipmentViewModel
class EquipmentViewModel(application: Application) : AndroidViewModel(application), LifecycleObserver {
var equipment = MutableLiveData<List<Gun>>()
var isLoading = MutableLiveData<Boolean>()
fun initData() {
isLoading.setValue(true)
thread { Thread.sleep(5000) //Simulates async network call
var gunList = ArrayList<Gun>()
for (i in 0..100){
gunList.add(Gun("Gun "+i.toString()))
}
equipment.postValue(gunList)
isLoading.postValue(false)
}
}
The ultimate aim is to have the activity just observe the isLoading MutableLiveData boolean, but since that wasn't working I changed the activity to observe just the equipment LiveData to minimize the number of variables at play.
To get same reference of ViewModel of your Activity you need to pass the same Activity instance, you should use ViewModelProviders.of(getActivity). When you pass this as argument, you receive instance of ViewModel that associates with your Fragment.
There are two overloaded methods:
ViewModelProvider.of(Fragment fragment)
ViewModelProvider.of(FragmentActivity activity)
For more info Share data between fragments
I put this code inside the onActivityCreated fragment, don't underestimate getActivity ;)
if (activity != null) {
globalViewModel = ViewModelProvider(activity!!).get(GlobalViewModel::class.java)
}
globalViewModel.onStop.observe(viewLifecycleOwner, Observer { status ->
Log.d("Parent Viewmodel", status.toString())
})
This code helps me to listening Parent ViewModel changes in fragment.
Just for those who are confused between definitions of SharedViewModel vs Making two fragments use one View Model:
SharedViewModel is used to share 'DATA' (Imagine two new instances being created and data from view model is being send to two fragments) where it is not used for observables since observables look for 'SAME' instance to take action. This means you need to have one viewmodel instance being created for two fragments.
IMO: Google should somehow mention this in their documentation since I myself thought that under the hood they are same instance where it is basically not and it actually now makes sense.
EDIT : Solution in Kotlin: 11/25/2021
In Your activity -> val viewModel : YourViewModel by viewModels()
In Fragment 1 - >
val fragmentViewModel =
ViewModelProvider(requireActivity() as YourActivity)[YourViewModel::class.java]
In Fragment 2 - >
val fragmentViewModel =
ViewModelProvider(requireActivity() as YourActivity)[YourViewModel::class.java]
This Way 2 fragments share one instance of Activity viewmodel and both fragments can use listeners to observe changes between themselves.
When you create fragment instead of getting viewModel object by viewModels() get it from activityViewModels()
import androidx.fragment.app.activityViewModels
class WeatherFragment : Fragment(R.layout.fragment_weather) {
private lateinit var binding: FragmentWeatherBinding
private val viewModel: WeatherViewModel by activityViewModels() // Do not use viewModels()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentWeatherBinding.inflate(inflater, container, false)
binding.viewModel = viewModel
// Observing for testing & Logging
viewModel.cityName.observe(viewLifecycleOwner, Observer {
Log.d(TAG, "onCreateView() | City name changed $it")
})
return binding.root
}
}
Kotlin Answer
Remove these two points in your function if you are using:
= viewModelScope.launch { }
suspend