Kotlin databinding shared viewmodel not updating both views - android

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()

Related

Bottom Navigation View Null

I am trying to set a badge to a BottomNavigationView by following this straightforward approach.
However, when I initialize the BottomNavigationView I get:
java.lang.IllegalStateException: view.findViewById(R.id.bottom_navigation_view) must not be null
I am initializing the BottomNativigationView from a fragment. I am guessing that is the issue, but I cannot figure out the solution.
private lateinit var bottomNavigation: BottomNavigationView
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_home, container, false)
bottomNavigation = view.findViewById(R.id.bottom_navigation_view)
}
Here is the BottomNavigationView xml for the Activity that sets up navigation for the fragments.
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="#+id/bottom_navigation_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#color/colorWhite"
app:itemIconTint="#color/navigation_tint"
app:itemTextColor="#color/navigation_tint"
app:labelVisibilityMode="labeled"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:menu="#menu/bottom_navigation" />
It feels like I am missing something simple, but I cannot figure out what. Thanks!
You have many options to communicate betwean fragments - activity and between fragment's itself..
You should not try access activity views from fragment.
Solution 1: Share data with the host activity
class ItemViewModel : ViewModel() {
private val mutableSelectedItem = MutableLiveData<Item>()
val selectedItem: LiveData<Item> get() = mutableSelectedItem
fun selectItem(item: Item) {
mutableSelectedItem.value = item
}
}
class MainActivity : AppCompatActivity() {
// Using the viewModels() Kotlin property delegate from the activity-ktx
// artifact to retrieve the ViewModel in the activity scope
private val viewModel: ItemViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.selectedItem.observe(this, Observer { item ->
// Perform an action with the latest item data
})
}
}
class ListFragment : Fragment() {
// Using the activityViewModels() Kotlin property delegate from the
// fragment-ktx artifact to retrieve the ViewModel in the activity scope
private val viewModel: ItemViewModel by activityViewModels()
// Called when the item is clicked
fun onItemClicked(item: Item) {
// Set a new item
viewModel.selectItem(item)
}
}
Solution 2: Receive results in the host activity
button.setOnClickListener {
val result = "result"
// Use the Kotlin extension in the fragment-ktx artifact
setFragmentResult("requestKey", bundleOf("bundleKey" to result))
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
supportFragmentManager
.setFragmentResultListener("requestKey", this) { requestKey, bundle ->
// We use a String here, but any type that can be put in a Bundle is supported
val result = bundle.getString("bundleKey")
// Do something with the result
}
}
}
There is many more ways but these are latest approaches from Google.
Check this reference: https://developer.android.com/guide/fragments/communicate
You can access the activity from its fragment by casting activity to your activity class, and inflate the views then.
bottomNavigation = (activity as MyActivityName).findViewById(R.id.bottom_navigation_view)

DialogFragment with view model not working with data binding

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

DataBinding & LiveData : two implementations (Kotlin & Java) and can't make the Java impl working

I'm facing issues while using DataBinding and LiveData in a Java projet. I followed a previous course in Kotlin and when I try to implement the same behaviors I just can't make it work. I'm clearly missing something in terms of understanding so I'd like to have you thoughts.
I'll paste the code from the Kotlin (working) example and then the Java (not working) one.
KOTLIN
score_fragment.xml
...
<data>
<variable
name="scoreViewModel"
type="com.example.android.guesstheword.screens.score.ScoreViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/score_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".screens.score.ScoreFragment">
<TextView
android:id="#+id/score_text"
...
android:text="#{String.valueOf(scoreViewModel.score)}"
.../>
...
ScoreFragment.kt
class ScoreFragment : Fragment() {
private lateinit var viewModelFactory: ScoreViewModelFactory
private lateinit var viewModel: ScoreViewModel
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate view and obtain an instance of the binding class.
val binding: ScoreFragmentBinding = DataBindingUtil.inflate(
inflater,
R.layout.score_fragment,
container,
false
)
// Get args using by navArgs property delegate
val scoreFragmentArgs by navArgs<ScoreFragmentArgs>()
viewModelFactory = ScoreViewModelFactory(scoreFragmentArgs.score)
viewModel = ViewModelProviders.of(this, viewModelFactory)
.get(ScoreViewModel::class.java)
binding.scoreViewModel = viewModel
binding.lifecycleOwner = this
return binding.root
}
}
ScoreViewModel.kt
class ScoreViewModel(finalScore: Int) : ViewModel() {
private val _score = MutableLiveData<Int>()
val score: LiveData<Int>
get() = _score
private val _eventPlayAgain = MutableLiveData<Boolean>()
val eventPlayAgain: LiveData<Boolean>
get() = _eventPlayAgain
init {
Timber.i("ScoreViewModel created")
_score.value = finalScore
}
fun onPlayAgain() {
_eventPlayAgain.value = true
}
fun onPlayAgainComplete() {
_eventPlayAgain.value = false
}
override fun onCleared() {
super.onCleared()
Timber.i("ScoreViewModel cleared")
}
}
Explanations : let's focus only on the score value. In ScoreViewModel the value is of type LiveData. When the fragment's launched, the value is correctly displayed on the screen through "#{String.valueOf(scoreViewModel.score)}". This works correctly.
JAVA
activity_main.xml
<data>
<variable
name="noteViewModel"
type="com.example.architectureapp.viewModel.NoteViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="#+id/textview"
android:text="#{noteViewModel.test}" />
</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
private NoteViewModel mNoteViewModel;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
binding = ActivityMainBinding.inflate(getLayoutInflater());
mNoteViewModel = ViewModelProviders.of(this).get(NoteViewModel.class);
binding.setNoteViewModel(mNoteViewModel);
binding.setLifecycleOwner(this);
}
}
NoteViewModel
public class NoteViewModel extends AndroidViewModel {
public MutableLiveData<String> test = new MutableLiveData<>("TesT");
public NoteViewModel(#NonNull Application application) {
super(application);
}
}
Explanations : here I'm setting a MutableLiveData test whith a value of "TesT" and then I intent to display it using android:text="#{noteViewModel.test}". But the text is never displayed and remains blank.
Obviously there is something wrong but despite the syntaxic differences between the two implementations I just can't figure out why the Java version is not displaying the value in the Textview.
EDIT
Thanks to Rajnish suryavanshi I was not getting my binding the right way, I had to only use :
binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
This one set the content view with the layout provided AND return the binding.
Alternatively you can do :
binding = ActivityMainBinding.inflate(getLayoutInflater()); (returns the binding but does not set the content view)
setContentView(binding.getRoot()); (set the content view with the binding root view)
I found this misleading -> https://developer.android.com/topic/libraries/data-binding/expressions
It states that we can replace
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
by
ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater());
which is not the same !
Happy to get the subtility now !
Take a look on this two lines.
setContentView(R.layout.activity_main);
binding = ActivityMainBinding.inflate(getLayoutInflater());
You are not creating your binding while inflating the layout. Instead of setContentView use DataBindingUtil.setContentView(this, R.layout.activity_main);
And now you can get the view using layout inflater
binding = ActivityMainBinding.inflate(getLayoutInflater());

Data Binding inflate layout inside another layout

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

Android: Call ViewModel function with Data Binding

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)
}

Categories

Resources