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.
Related
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 using nested recyclerview.
In the picture, the red box is the Routine Item (Parent Item), and the blue box is the Detail Item (Child Item) in the Routine Item.
You can add a parent item dynamically by clicking the ADD ROUTINE button.
Similarly, child items can be added dynamically by clicking the ADD button of the parent item.
As a result, this function works just fine.
But the problem is in the code I wrote.
I use a ViewModel to observe and update parent item addition/deletion.
However, it does not observe changes in the detail item within the parent item.
I think it's because LiveData only detects additions and deletions to the List.
So I put _items.value = _items.value code to make it observable when child items are added and deleted.
This way, I didn't even have to use update code like notifyDataSetChanged() in the child adapter.
In the end it is a success, but I don't know if this is the correct code.
Let me know if you have additional code you want!
In Fragment.kt
class WriteRoutineFragment : Fragment() {
private var _binding : FragmentWriteRoutineBinding? = null
private val binding get() = _binding!!
private lateinit var adapter : RoutineAdapter
private val vm : WriteRoutineViewModel by viewModels { WriteRoutineViewModelFactory() }
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View? {
_binding = FragmentWriteRoutineBinding.inflate(inflater, container, false)
adapter = RoutineAdapter(::addDetail, ::deleteDetail)
binding.rv.adapter = this.adapter
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
getTabPageResult()
// RecyclerView Update
vm.items.observe(viewLifecycleOwner) { updatedItems ->
adapter.setItems(updatedItems)
}
}
private fun getTabPageResult() {
val navController = findNavController()
navController.currentBackStackEntry?.also { stack ->
stack.savedStateHandle.getLiveData<String>("workout")?.observe(
viewLifecycleOwner, Observer { result ->
vm.addRoutine(result) // ADD ROUTINE
stack.savedStateHandle?.remove<String>("workout")
}
)
}
}
private fun addDetail(pos: Int) {
vm.addDetail(pos)
}
private fun deleteDetail(pos: Int) {
vm.deleteDetail(pos)
}
}
ViewModel
class WriteRoutineViewModel : ViewModel() {
private var _items: MutableLiveData<ArrayList<RoutineModel>> = MutableLiveData(arrayListOf())
val items: LiveData<ArrayList<RoutineModel>> = _items
fun addRoutine(workout: String) {
val item = RoutineModel(workout, "TEST")
_items.value?.add(item)
// _items.value = _items.value
}
fun addDetail(pos: Int) {
val detail = RoutineDetailModel("TEST", "TEST")
_items.value?.get(pos)?.addSubItem(detail) // Changing the parent item's details cannot be observed by LiveData.
_items.value = _items.value // is this right way?
}
fun deleteDetail(pos: Int) {
if(_items.value?.get(pos)?.getSubItemSize()!! > 1)
_items.value?.get(pos)?.deleteSubItem() // is this right way?
else
_items.value?.removeAt(pos)
_items.value = _items.value // is this right way?
}
}
This is pretty standard practice when using a LiveData with a mutable List type. The code looks like a smell, but it is so common that I think it's acceptable and people who understand LiveData will understand what your code is doing.
However, I much prefer using read-only Lists and immutable model objects if they will be used with RecyclerViews. It's less error prone, and it's necessary if you want to use ListAdapter, which is much better for performance than a regular Adapter. Your current code reloads the entire list into the RecyclerView every time there is any change, which can make your UI feel laggy. ListAdapter analyzes automatically on a background thread your List for which items specifically changed and only rebinds the changed items. But it requires a brand new List instance each time there is a change, so it makes sense to only use read-only Lists if you want to support using it.
I'm building an Android app that has different pages that mainly have some EditText. My goal is to handle the click on the EditText and shows a DialogAlert with an EditText, then the user can put the text, click "save" and the related field in the database (I'm using Room and I've tested the queries and everything works) will be updated. Now I was able to handle the text from the DialogFragment using interface but I don't know how to say that the text retrieved is related to the EditText that I've clicked. What is the best approach to do this?
Thanks in advance for your help.
Let's take this fragment as example:
class StaticInfoResumeFragment : Fragment(), EditNameDialogFragment.OnClickCallback {
private val wordViewModel: ResumeStaticInfoViewModel by viewModels {
WordViewModelFactory((requireActivity().application as ManagementCinemaApplication).resumeStaticInfoRepo)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
val root = inflater.inflate(R.layout.fragment_static_info_resume, container, false)
wordViewModel.resumeStaticInfo.observe(viewLifecycleOwner) { words ->
println("test words: $words")
}
val testView = root.findViewById<TextInputEditText>(R.id.textInputEditText800)
testView.setOnClickListener{
val fm: FragmentManager = childFragmentManager
val editNameDialogFragment = EditNameDialogFragment.newInstance("Some Title")
editNameDialogFragment.show(fm, "fragment_edit_name")
}
resumeStaticInfoViewModel.firstName.observe(viewLifecycleOwner, Observer {
testView.setText(it)
})
return root
}
override fun onClick(test: String) {
println("ciao test: $test")
wordViewModel.updateFirstName(testa)
}}
Then I've the ViewModel:
class ResumeStaticInfoViewModel(private val resumeStaticInfoRepo: ResumeStaticInfoRepo): ViewModel() {
val resumeStaticInfo: LiveData<ResumeStaticInfo> = resumeStaticInfoRepo.resumeStaticInfo.asLiveData()
fun updateFirstName(resumeStaticInfoFirstName: String) = viewModelScope.launch {
resumeStaticInfoRepo.updateFirstName(resumeStaticInfoFirstName)
}
....
And the DialogFragment:
class EditNameDialogFragment : DialogFragment() {
private lateinit var callback: OnClickCallback
interface OnClickCallback {
fun onClick(test: String)
}
override fun onAttach(context: Context) {
super.onAttach(context)
try {
callback = parentFragment as OnClickCallback
} catch (e: ClassCastException) {
throw ClassCastException("$context must implement UpdateNameListener")
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val title = requireArguments().getString("title")
val alertDialogBuilder: AlertDialog.Builder = AlertDialog.Builder(requireContext())
alertDialogBuilder.setTitle(title)
val layoutInflater = context?.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
val alertCustomView = layoutInflater.inflate(R.layout.alert_dialog_edit_item, null)
val editText = alertCustomView.findViewById<EditText>(R.id.alert_edit)
alertDialogBuilder.setView(alertCustomView)
alertDialogBuilder.setPositiveButton(
"Save",
DialogInterface.OnClickListener { dialog, which ->
callback.onClick(editText.text.toString())
})
alertDialogBuilder.setNegativeButton("No") { _: DialogInterface, _: Int -> }
return alertDialogBuilder.create()
}
companion object {
fun newInstance(title: String?): EditNameDialogFragment {
val frag = EditNameDialogFragment()
val args = Bundle()
args.putString("title", title)
frag.arguments = args
return frag
}
}
}
Do you mean you just want to show a basic dialog for entering some text, and you want to be able to reuse that for multiple EditTexts? And you want a way for the dialog to pass the result back, but also have some way of identifying which EditText it was created for in the first place?
The thing about dialogs is they can end up being recreated (like if the app is destroyed in the background, and then restored when the user switches back to it) so the only real configuration you can do on it (without getting into some complexity anyway) is through its arguments, like you're doing with the title text.
So one approach you could use is send some identifier parameter to newInstance, store that in the arguments, and then pass it back in the click listener. So you're giving the callback two pieces of data in onClick - the text entered and the reference ID originally passed in. That way, the activity can handle the ID and decide what to do with it.
An easy value you could use is the resource ID of the EditText itself, the one you pass into findViewById - it's unique, and you can easily use it to set the text on the view itself. You're using a ViewModel here, so it should be updating automatically when you set a value in that, but in general it's a thing you could do.
The difficulty is that you need to store some mapping of IDs to functions in the view model, so you can handle each case. That's just the nature of making the dialog non-specific, but it's easier than making a dialog for each property you want to update! You could make it a when block, something like:
// you don't need the #ResId annotation but it can help you avoid mistakes!
override fun onClick(text: String, #ResId id: Int) {
when(id) {
R.id.coolEditText -> viewModel.setCoolText(text)
...
}
}
where you list all your cases and what to call for each of them. You could also make a map like
val updateFunctions = mapOf<Int, (String) -> Unit>(
R.id.coolEditText to viewModel::setCoolText
)
and then in your onClick you could call updateFunctions[id]?.invoke(text) to grab the relevant function for that EditText and call it with the data. (Or use get which throws an exception if the EditText isn't added to the map, which is a design error you want to get warned about, instead of silently ignoring it which is what the null check does)
Im working on a project and implementing the MVVM model with databinding and navigation. My button is on a fragment that opens with a drawer menu item, the thing is when i click on the button it does nothing, the debugger doesn't go into the navigate method, I really don't know what I did wrong, can someone help?
MYACCOUNT CLASS:
class MyAccountFragment : BaseFragment() {
private val vm: MyAccountViewModel by viewModel()
override fun getViewModel(): BaseViewModel = vm
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding = FragmentMyAccountBinding.inflate(inflater, container, false)
context ?: return binding.root
injectFeature()
setToolbar(binding)
subscribeUi(binding)
return binding.root
}
/**
* set toolbar
* **/
private fun setToolbar(binding: FragmentMyAccountBinding) {
binding.appBarLayout.backClickListener = (activity as MainActivity).createOnBackClickListener()
}
/**
* set ui
* **/
private fun subscribeUi(binding: FragmentMyAccountBinding) {
binding.viewModel = vm
}
}
MYACCVIEWMODEL
class MyAccountViewModel constructor() : BaseViewModel() {
fun onAddRoomClick()
{
navigate(MyAccountFragmentDirections.actionMyAccountFragmentToAddRoomFragment())
}
}
and in the xml i implemented the
android:onClick="#{() -> viewModel.onAddRoomClick()}"
Im using this pattern for all my Fragments and ViewModels, and i really dont know why it doesn't do anything, the vm initializes. On the other drawermenu fragment I also have the onClick method and it navigates to the other fragment. So if anyone knows the solution that would be helpful, thank you in advance.
the answer was in the initialization of the viewModel.
the onClick method in xml is in a content_layout that is included in a fragment_layout and instead of binding.viewModel = vm I had to do binding.content_layout.viewModel = vm.
private fun subscribeUi(binding: FragmentMyAccountBinding) {
binding.contentMyAccount.viewModel = vm
}
ViewModel is not supposed to handle any kind of navigation, it will just receive the event from the UI and pass it to the controller (which might be a fragment or activity) and the latter will handle the navigation...
So one way to solve your issue is to do the following:
ViewModel
class MyAccountViewModel constructor() : BaseViewModel() {
private val _navigateToRoomFragEvent = MutableLiveData<Boolean>(false)
val navigateToRoomFragEvent:LiveData<Boolean>
get()=_navigateToRoomFragEvent
fun onAddRoomClick()
{
_navigateToRoomFragEvent.value=true
}
fun resetNavigation(){
_navigateToRoomFragEvent.value=false
}
}
Controller (Activity or Fragment)
inside **onCreate() (if it is an activity)**
viewModel.navigateToRoomFragEvent.observe(this,Observer{navigate->
//boolean value
if(navigate){
navController.navigate(//action)
}
viewModel.resetNavigation() //don't forget to reset the event
})
onActivityCreated(if it is a fragment)
viewModel.navigateToRoomFragEvent.observe(viewLifeCycleOwner,Observer{navigate->
//boolean value
if(navigate){
navController.navigate(//action)
}
viewModel.resetNavigation() //don't forget to reset the event
})
Hope it helps,
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.