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
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'm facing an issue with my ViewModel that I use to hold user login data.
I update this ViewModel with user data from fragment A after a user logs in, but when I try to access the data from fragment B the data fields I just set are always null.
When fragment B is initialized the user LiveData field is never initially observed, however, when I trigger a change to the user object from fragment B the change is correctly observed within fragment B. It appears that the previous values of the fields in my ViewModel never reach fragment B, but new values do.
For a sanity check I made a simple string variable (not even a LiveData object) that I set to a value from fragment A, then, after navigating to fragment B I printed the value: it is uninitialized every time. It's as if the ViewModel I inject into fragment B is totally separate from the ViewModel I inject into fragment A.
What am I missing that causes the ViewModel observation in fragment B not to initially trigger with the last known value of user set from fragment A?
Fragment A
class FragmentA : Fragment() {
private val viewModel: LoginViewModel by viewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.user.observe(this, {
it?.let {
//Called successfully every time
navigateToFragmentB()
}
})
val mockUserData = User()
viewModel.loginSuccess(mockUserData)
}
}
Fragment B
class FragmentB : Fragment() {
private val viewModel: LoginViewModel by viewModel()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
...
viewModel.user.observe(viewLifecycleOwner, { user ->
user?.let {
binding.initialsBubble.text = user.getInitials()
} ?: navigateAway()
})
}
}
ViewModel
class LoginViewModel(
private val loginRepo: LoginRepo
) : ViewModel() {
private val _user = MutableLiveData<User?>()
val user: LiveData<User?> = _user
fun loginSuccess(result: AuthenticationResult) {
val user = loginRepo.login(result)
_user.postValue(user)
}
}
You should use sharedViewModel for both fragment.
Use these lines of code in both fragments
private val viewModel: LoginViewModel by activityViewModels()
instead of
private val viewModel: LoginViewModel by viewModel()
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)
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'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.