I'm trying to use Data Binding for setting onClick listeners for buttons in my fragment.
The function that I need to be called every time "next" button is pressed is in a View Model.
I managed to bind data from View Model to my layout XML but I am still unable to call functions from a view model :/
I'm getting this error when trying to call ViewModel functions:
C:\Users\Michal\git\fitness-fatality\app\build\generated\source\kapt\debug\com\example\fitnessfatality\DataBinderMapperImpl.java:10: error: cannot find symbol
import com.example.fitnessfatality.databinding.FragmentWorkoutLoggingBindingImpl;
^
symbol: class FragmentWorkoutLoggingBindingImpl
location: package com.example.fitnessfatality.databinding
I've also tried calling view model functions like this:
android:onClick="#{viewModel.incrementIndex()}"
However, if I bind the entire fragment, I am able to call its functions.
This is how I've tried implementing on click binding with view model:
<?xml version="1.0" encoding="utf-8"?>
<layout
android:id="#+id/main_linear_container"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="com.example.fitnessfatality.ui.workoutTracking.viewModels.TrackingViewModel"/>
<import type="java.util.List"/>
<import type="com.example.fitnessfatality.ui.workoutTracking.TrackingFragment" />
<variable name="viewModel" type="TrackingViewModel" />
<variable name="fragment" type="TrackingFragment" />
</data>
<LinearLayout
android:orientation="vertical" android:layout_height="match_parent" android:layout_width="match_parent">
//More layouts
<Button
android:text="Next"
android:onClick="#{viewModel.incrementIndex}"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:id="#+id/btn_next" android:layout_weight="1"/>
</LinearLayout>
</layout>
And in my fragment I have
private lateinit var trackingViewModel: TrackingViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
trackingViewModel = ViewModelProviders.of(this).get(TrackingViewModel::class.java)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val binding =
DataBindingUtil.inflate<FragmentWorkoutLoggingBinding>(
inflater,
R.layout.fragment_workout_logging,
container,
false
)
binding.lifecycleOwner = this
binding.viewModel = trackingViewModel
binding.fragment = this
return binding.root
}
And my ViewModel:
class TrackingViewModel(application: Application): BaseViewModel(application) {
val workoutExercises: LiveData<List<WorkoutExercisePojo>>
private val workoutExerciseRepository: WorkoutExerciseRepository
val currentIndex: MutableLiveData<Int> = MutableLiveData()
val index: LiveData<Int> = currentIndex
init {
val db = AppDatabase.getDatabase(application, scope)
workoutExerciseRepository = WorkoutExerciseRepository(db.workoutExerciseDao())
workoutExercises = workoutExerciseRepository.allWorkoutExercises
currentIndex.value = 0
}
fun incrementIndex() {
currentIndex.value = currentIndex.value!!.plus(1)
}
}
With the custom BindingAdapter:
#BindingAdapter("onClick")
fun onClick(view: View, onClick: () -> Unit) {
view.setOnClickListener {
onClick()
}
}
You should be able to directly bind a viewmodel function like
app:onClick="#{viewModel::forgotPasswordClicked}"
in your XML. This would then lead to a viewmodel function like:
fun forgotPasswordClicked() {
TODO("ForgotPasswordClicked")
}
This way, you also don't have to import unnecessary Android-Dependencies into your viewmodel.
Managed to solved. The problem was that incremenetIndex function in ViewModel did not accept View as a parameter.
So now, the function in ViewModel looks like this:
fun incrementIndex(view: View) {
currentIndex.value = currentIndex.value!!.plus(1)
}
Related
I am trying to call a function in my fragment via expression binding from my XML file in "android:onclick...", but it will not work. The error is that the fragment is not attached to a context.
It is the
MaterialAlertDialogBuilder(requireContext())
which gives me headache.
How do I give the context to the fragment?
I have seen similar questions regarding that topic, but none that helped me.
Any help is much appreciated.
ItemDetailFragment.kt:
class ItemDetailFragment : Fragment() {
private lateinit var item: Item
private val viewModel: InventoryViewModel by activityViewModels {
InventoryViewModelFactory(
(activity?.application as InventoryApplication).database.itemDao()
)
}
private val navigationArgs: ItemDetailFragmentArgs by navArgs()
private var _binding: FragmentItemDetailBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentItemDetailBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val id = navigationArgs.itemId
binding.viewModel = viewModel
binding.fragment = ItemDetailFragment()
}
/**
* Displays an alert dialog to get the user's confirmation before deleting the item.
*/
fun showConfirmationDialog() {
MaterialAlertDialogBuilder(requireContext())
.setTitle(getString(android.R.string.dialog_alert_title))
.setMessage(getString(R.string.delete_question))
.setCancelable(false)
.setNegativeButton(getString(R.string.no)) { _, _ -> }
.setPositiveButton(getString(R.string.yes)) { _, _ ->
deleteItem()
}
.show()
}
/**
* Called when fragment is destroyed.
*/
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
fragment_item_detail.kt:
<?xml version="1.0" encoding="utf-8"?><!--
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type="com.example.inventory.InventoryViewModel" />
<variable
name="fragment"
type="com.example.inventory.ItemDetailFragment" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="#dimen/margin"
tools:context=".ItemDetailFragment">
<Button
android:id="#+id/delete_item"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="#dimen/margin"
android:onClick="#{()->fragment.showConfirmationDialog()}"
android:text="#string/delete"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/sell_item" />
</androidx.constraintlayout.widget.ConstraintLayout>
That is the error i am getting:
java.lang.IllegalStateException: Fragment ItemDetailFragment{e562873} (c6ab2144-3bdc-410b-91eb-e5668e8b617a) not attached to a context.
You should not pass your fragment instance as a data binding variable.
You could define a Boolean mutable live data variable in your InventoryViewModel and show the dialog when it changes:
private val _showConfirmation = MutableLiveData(false)
val showConfirmation
get() = _showConfirmation
fun onShowConfirmation() {
_showConfirmation.value = true
}
fun onConfirmationShown() {
_showConfirmation.value = false
}
Then, define an observer for this property in your ItemDetailFragment:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val id = navigationArgs.itemId
binding.viewModel = viewModel
binding.executePendingBindings()
viewModel.showConfirmation.observe(viewLifecycleOwner) {
if (it) {
showConfirmationDialog()
viewModel.onConfirmationShown()
}
}
}
Finally, remove the fragment variable from the XML and change your Button's onClick as:
<Button
...
android:onClick="#{() -> viewModel.onShowConfirmation()}"
/>
I'm trying to use a shared viewmodel for an activity and a fragment displayed in the activity because both need to be updated by the viewmodel. The fragment is constantly updated using MutableLiveData, but the activity is not and I don't really understand why. For readability reasons I did cut out the layout parameters in the .xml files which are irrelevant to the problem.
My activity code looks like the following:
class MainActivity : AppCompatActivity() {
private lateinit var _binding : MainActivityBinding
private val _viewModel : MySharedViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
_binding = DataBindingUtil.setContentView(this, R.layout.main_activity)
_binding.viewModel = _viewModel
_binding.lifecycleOwner = this
...
}
}
The lifecycle owner of the binding is set, yet MutableLiveData does not seem to update the activity.
In the activity layout file is the following:
<layout>
<data>
<variable
name="viewModel"
type="com.customApp.viewModels.MySharedViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.appbar.MaterialToolbar>
<TextView
android:id="#+id/totalTime"
android:text="#{viewModel.topBarText}"
.../>
</com.google.android.material.appbar.MaterialToolbar>
<com.google.android.material.bottomnavigation.BottomNavigationView
.../>
<androidx.fragment.app.FragmentContainerView
.../>
</androidx.constraintlayout.widget.ConstraintLayout>
So I have the topbar with the text view that always shall show the text, the bottom navigation view as tabbar and the fragment container view that contains the fragments.
The fragment code is:
class FirstFragment : Fragment() {
private val _viewModel : MySharedViewModel by viewModels()
private lateinit var _binding : FirstFragmentBinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = DataBindingUtil.inflate(inflater, R.layout.first_fragment, container, false)
_binding.viewModel = _viewModel
_binding.lifecycleOwner = viewLifecycleOwner
return _binding.root
}
}
Also here the lifecycleowner is set for the binding and in the fragment layout it is bound to the viewmodel:
<layout>
<data>
<variable
name="viewModel"
type="com.customApp.viewModels.MySharedViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout>
<TextView
...
android:text="#{viewModel.fragmentText}" />
<Button
android:id="#+id/startButton"
android:onClick="#{() -> viewModel.startIteration()}"
.../>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
And in my viewmodel I simply update texts that shall be displayed:
class MySharedViewModel : ViewModel() {
var topBarText : MutableLiveData<String> = MutableLiveData<String>("Hi")
var fragmentText : MutableLiveData<String> = MutableLiveData<String>("There")
private val stringsForTop = arrayOf("Hi","How", "You")
private val stringsForFragment = arrayOf("There","Are", "?")
private var index = 0
fun startIteration() {
kotlin.concurrent.fixedRateTimer(initialDelay = 1L, period = 1000L) {
viewModelScope.launch(Dispatchers.Main) {
topBarText.value = stringsForTop[index]
fragmentText.value = stringsForFragment[index]
index++
}
}
}
}
Now while the fragment text is updated, the text in the top bar is not updated and always just displays "Hi". I have the feeling, that the activity as lifecycleobserver for the view model is overwritten when the fragment is initialized and its viewLifecycleOwner is bound to the view model.
Am I missing something or is there another way to register both, activity and fragment with their lifecycleowners, at the viewmodel? Any help appreciated.
If you are using by viewmodels<>() in Fragment, it will create new instance of ViewModel coupled to Fragment life cycle. if you want same instance of ViewModel as it is Activity use by activityViewModels<>(), try below code in Fragment
private val _viewModel : MySharedViewModel by activityViewModels()
I've created one dialog fragemnt with view model (mvvm). Dialog consist of one button (custom view). when using view model with data binding, button click is not working when livedata change.I'm using boolean value to check if button is clicked or not. What is causing issue? Also suggest any other approach if needed.
profile_dialog_fragment.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewmodel"
type="com.test.ui.ProfileDialogViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.ProfileDialog">
<com.google.android.material.button.MaterialButton
android:id="#+id/login"
style="#style/TextAppearance.MaterialComponents.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Login"
android:onClick="#{() -> viewmodel.onLoginButtonClick()}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
ProfileDialog.kt
class ProfileDialog : DialogFragment() {
companion object {
fun newInstance() = ProfileDialog()
}
private val viewModel: ProfileDialogViewModel by viewModel()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding = ProfileDialogFragmentBinding.inflate(inflater, container, false)
.apply {
this.lifecycleOwner = this#ProfileDialog
this.viewmodel = viewmodel
}
return binding.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel.startLogin.observe(viewLifecycleOwner, Observer {
Log.d("insta", "This is working")
if (it == null) return#Observer
if(it) {
Log.d("insta", "This is not working")
val loginIntent = Intent(this.context, LoginActivity::class.java)
this.context?.startActivity(loginIntent)
}
})
}
}
ProfileDialogViewModel.kt
class ProfileDialogViewModel : ViewModel() {
private val _startLogin = MutableLiveData<Boolean>(false)
val startLogin: LiveData<Boolean>
get() = _startLogin
fun onLoginButtonClick() {
Log.d("insta", "This ain't working")
_startLogin.postValue(true)
}
}
Your viewmodel is defined in
private val viewModel: ProfileDialogViewModel by viewModel()
So, pay attention to viewModel. The problem located in
this.viewmodel = viewmodel
where this points to ProfileDialogFragmentBinding. Here you assinging ProfileDialogFragmentBinding.viewmodel = ProfileDialogFragmentBinding.viewmodel - that's why it's not working.
To solve problem, properly assign it like that:
this.viewmodel = viewModel
EDIT - I tried to set android:focusable and android:clickable attributes to true in the XML, but it didn't changed anything. Still looking!
I can't understand why my onClick handler isn't working on this particular case.
I want to make a CardView clickable, using databinding. I've seen a lot of code for onClickListener, but i decided to use this pattern, as described in the Android doc.
I have a class, named Recipe(), which contains 3 elements : an ID, a Title and an URL. The ultimate goal of this click handler is to navigate to a new fragment, passing the ID as a parameter to display new elements. I'll later get the ID in the Fragment using observer.
In this example, I already use data provided by the ViewModel in the XML (recipe.image and recipe.title), and it works just fine: data is correctly binded and displayed.
However, clicking on the CardView don't result in anything: as the Log isn't display, I suppose clicking don't trigger the onClick event.
You'll find element from the ViewModel and XML below. Thanks in advance!
class DefaultRecipeListViewModel : ViewModel() {
private val _recipeList = MutableLiveData<List<Recipe>>()
val recipeList: LiveData<List<Recipe>>
get() = _recipeList
//Defining coroutine
private var viewModelJob = Job()
private val coroutineScope = CoroutineScope(viewModelJob + Dispatchers.Main )
//Livedata observed in the Fragment
private var _navigateToRecipe = MutableLiveData<Int>()
val navigateToRecipe: LiveData<Int>
get() = _navigateToRecipe
private var _showSnackbarEvent = MutableLiveData<Boolean>()
val showSnackBarEvent: LiveData<Boolean>
get() = _showSnackbarEvent
init {
getRecipesForNewbie()
}
fun getRecipesForNewbie() {
coroutineScope.launch {
var getRecipes = service.getRecipe().await()
try {
_recipeList.value = getRecipes.results
} catch (e: Exception) {
Log.i("ViewModel","Error: $e")
}
}
}
fun onRecipeClicked(id: Int) {
_showSnackbarEvent.value = true
_navigateToRecipe.value = id
Log.i("ViewModel", "Item clicked, id: $id")
}
fun doneNavigating(){
Log.i("ViewModel", "done navigating, navigateToRecipe set to -1")
_navigateToRecipe.value = -1
}
fun doneShowingSnackbar() {
_showSnackbarEvent.value = false
}
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
}
Here's the XML
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="recipe"
type="com.example.recipesfornewbies.recipes.Recipe" />
<variable
name="viewModel"
type="com.example.recipesfornewbies.defaultrecipelist.DefaultRecipeListViewModel" />
</data>
<LinearLayout
android:layout_height="wrap_content"
android:layout_width="match_parent">
<androidx.cardview.widget.CardView
android:id="#+id/card_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginBottom="6dp"
android:layout_marginTop="6dp"
app:cardCornerRadius="10dp"
app:cardElevation="6dp"
android:onClick="#{()-> viewModel.onRecipeClicked(recipe.id)}">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="#+id/recipe_image"
android:layout_width="match_parent"
android:layout_height="100dp"
android:contentDescription="#{recipe.title}"
app:imageFromUrl="#{recipe.image}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:layout_editor_absoluteX="0dp" />
<TextView
android:id="#+id/recipe_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:text="#{recipe.title}"
android:textSize="24sp"
app:layout_constraintTop_toBottomOf="#id/recipe_image"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
</layout>
Fragment code:
class DefaultRecipeListFragment: Fragment(){
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding = FragmentDefaultRecipeListBinding.inflate(inflater)
val viewModel: DefaultRecipeListViewModel by lazy {
ViewModelProviders.of(this).get(DefaultRecipeListViewModel::class.java)
}
binding.viewModel = viewModel
// Allows Data Binding to Observe LiveData with the lifecycle of this Fragment
binding.setLifecycleOwner(this)
//Creating the RecyclerView
val manager = LinearLayoutManager(activity)
binding.recyclerRecipeList.layoutManager = manager
binding.recyclerRecipeList.adapter = RecipeListAdapter()
viewModel.navigateToRecipe.observe(this, Observer{id ->
if (id != -1){
Log.i("Fragment","Navigate to ${id}")
}
viewModel.doneNavigating()
})
viewModel.showSnackBarEvent.observe(this, Observer {
if (it == true) {
Snackbar.make(
activity!!.findViewById(android.R.id.content),
"Clicked!",
Snackbar.LENGTH_SHORT
).show()
viewModel.doneShowingSnackbar()
}
})
return binding.root
}
}
ViewHolder class, used in the RecyclerView Adapter
class RecipeViewHolder(private var binding: RecipeViewBinding):
RecyclerView.ViewHolder(binding.root) {
fun bind(Recipe: Recipe) {
val imageURI = "https://spoonacular.com/recipeImages/"
Recipe.image = imageURI + Recipe.image
binding.recipe = Recipe
// Forces the data binding to execute immediately,to correctly size RecyclerVieW
binding.executePendingBindings()
}
I finally decided to use this solution on my code, which works pretty well.
I stopped trying to access the function in my ViewModel directly from the XML: I actually listen for event on the Fragment, even if I find the solution to be less pretty that the one I wanted.
In the Fragment, this is how I handle the click:
binding.recyclerRecipeList.addOnItemTouchListener(RecyclerItemClickListener(this.context!!,
binding.recyclerRecipeList, object : RecyclerItemClickListener.OnItemClickListener {
override fun onItemClick(view: View, position: Int) {
viewModel.recipeList.value?.let {
val ident = it[position].id
findNavController().navigate(
DefaultRecipeListFragmentDirections.actionDefaultRecipeListFragmentToDetailedRecipeFragment(ident))
Log.i("Fragment", "id: $ident")
}
}
override fun onItemLongClick(view: View?, position: Int) {
TODO("do nothing")
}
}))
Still haven't understood why my first solution didn't work.
im trying to make a base bottom sheet dialog fragment class that supports data binding. here is my class:
abstract class RoundedBottomSheetDialogFragment<VM : BaseViewModel, DB : ViewDataBinding> :
BottomSheetDialogFragment() {
abstract val viewModel: VM
open lateinit var binding: DB
private fun init(inflater: LayoutInflater, container: ViewGroup?) {
binding = DataBindingUtil.inflate(inflater, getLayoutRes(), container, true)
}
abstract fun getLayoutRes(): Int
abstract fun configEvents()
abstract fun bindObservables()
/**
*
* You need override this method.
* And you need to set viewModel to binding: binding.viewModel = viewModel
*
*/
abstract fun initBinding()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val parentLayout = inflater.inflate(R.layout.rounded_bottom_sheet, container, false)
init(inflater, parentLayout.container)
showDialogAsExpanded()
return parentLayout
}
private fun showDialogAsExpanded() {
dialog?.setOnShowListener {
val bottomSheetInternal =
(dialog as BottomSheetDialog).findViewById<View>(R.id.design_bottom_sheet) ?: return#setOnShowListener
val behavior = BottomSheetBehavior.from(bottomSheetInternal)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
behavior.skipCollapsed = true
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
configEvents()
bindObservables()
}
}
if you see i'm inflating a layout inside this dialog fragment class and i want to use data binding inside that layout xml file.
this is an example of my xml file:
<layout>
<data>
<variable
name="vm"
type="com.mobtakerteam.walleto.ui.login.searchcountry.SearchCountryViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/search_list"
android:layout_width="match_parent"
android:layout_height="400dp"
app:data="#{vm.countries}"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="20"
tools:listitem="#layout/fragment_search_country_row" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
but the problem is that it is not working in the xml layout and i have to manually observe the live data objects inside kotlin class like this:
viewModel.countries.observe(this, Observer {
adapter.submitList(it)
})
so what is the problem?
Using LiveData with binding you have to set lifecycle owner like this
binding.lifecycleOwner = this