I'm trying to change the text of a button based on user interaction. The text is bound to a boolean variable in the view model and is supposed to observe changes in that variable. When the variable changes, the text is supposed to switch. But it's not.
<?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"
xmlns:bind="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="userDataViewModel"
type="com.mysite.myapp.viewModels.UserDataViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/change_group_location_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".fragments.GroupLocationFragment">
<Button
android:id="#+id/select_location_button"
android:layout_width="match_parent"
android:layout_height="64dp"
android:text='#{userDataViewModel.userData.groupChanged ? "changed" : "not changed"}'/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
Here is the part of the fragment where I bind the view model:
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_group_location, container, false)
binding.lifecycleOwner = this
userDataViewModel = activity?.run {
ViewModelProviders
.of(this, UserDataViewModelFactory(prefs, userDataFetcherService))
.get(UserDataViewModel::class.java)
} ?: throw Exception("Invalid Activity")
userDataViewModel
.getUserData()
.observe(this, Observer {
binding.userDataViewModel = userDataViewModel
})
return binding.root
}
I can see that the boolean variable in the view model toggles to 'true', but the button text still says 'not changed'. I just don't see what I am overlooking.
EDIT
Adding in the ViewModel class in case the issue is there.
class UserDataViewModel(private val prefs: SharedPreferences): ViewModel() {
val userData: MutableLiveData<UserData> by lazy {
MutableLiveData<UserData>().also {
val userDataString = prefs.getString(UserData.USER_DATA_SHARED_PREFERENCE_KEY, "")
it.value = Gson().fromJson(userDataString, UserData::class.java)
}
}
fun getUserData(): LiveData<UserData> {
return userData
}
}
Might be an issue with a button text which you set.
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/change_group_location_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".fragments.GroupLocationFragment">
<Button
android:id="#+id/select_location_button"
android:layout_width="match_parent"
android:layout_height="64dp"
android:text='#{userDataViewModel.userData.groupChanged ? `changed` : `not changed`}'/>
change it with back quote (`) sign.
You can use string from string.xml also like below
<Button
android:id="#+id/select_location_button"
android:layout_width="match_parent"
android:layout_height="64dp"
android:text="#{userDataViewModel.userData.groupChanged ? #string/txt_changed : #string/txt_not/-changed}"
Related
I've switched from using LiveData to StateFlow in my app and also applied it in Data Binding. But after changing the value of StateFlow, the UI is not updating. From what I understand from the StateFlow docs, I only have to assign a new value to the value property of a StateFlow to update its consumers.
Below, I have a time field which I update the value by using an MaterialTimePicker dialog. But after I change the time and assign a new value to the selectedAlarm property (StateFlow) of my viewmodel, the UI is not updating for the new value. I'm not sure where is my mistake or what is lacking.
Below is my AlarmFormFragment:
#AndroidEntryPoint
class AlarmFormFragment : Fragment() {
private val alarmsViewModel: AlarmsViewModel by activityViewModels()
private var _binding: FragmentAlarmFormBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentAlarmFormBinding.inflate(inflater, container, false)
binding.alarmsViewModel = alarmsViewModel
binding.lifecycleOwner = viewLifecycleOwner
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
setUpTimeField()
}
private fun setUpTimeField() {
alarmsViewModel.selectedAlarm.value.let { alarm ->
binding.llTimeField.setOnClickListener {
// Create and then show time picker
val picker =
MaterialTimePicker.Builder()
.setTimeFormat(TimeFormat.CLOCK_12H)
.setTitleText("Set Time")
.setHour(alarm.time.get(Calendar.HOUR_OF_DAY))
.setMinute(alarm.time.get(Calendar.MINUTE))
.build()
picker.addOnPositiveButtonClickListener {
alarm.time.set(Calendar.HOUR_OF_DAY, picker.hour)
alarm.time.set(Calendar.MINUTE, picker.minute)
alarmsViewModel.selectedAlarm.value = alarm
}
picker.show(childFragmentManager, "TIME_INPUT_DIALOG")
}
}
}
}
Below is the xml file for AlarmFormFragment:
<?xml version="1.0" encoding="utf-8"?>
<layout>
<data>
<import type="com.example.myclock.utilities.Utility" />
<import type="com.example.myclock.utilities.SnoozeUtils"/>
<variable
name="alarmsViewModel"
type="com.example.myclock.viewmodels.AlarmsViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
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"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".fragments.AlarmFormFragment">
<!-- Form Fields -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent">
<!-- Time Field -->
<LinearLayout
android:id="#+id/llTimeField"
style="#style/FormFieldLayoutStyle">
<TextView
style="#style/FormFieldTextStyle.Label"
android:text="#string/label_time" />
<TextView
android:id="#+id/tvTime"
style="#style/FormFieldTextStyle.Value"
android:text="#{Utility.INSTANCE.timeToString(alarmsViewModel.selectedAlarm.value.time)}" />
</LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
And below is my AlarmsViewModel:
#HiltViewModel
class AlarmsViewModel #Inject constructor(
private val alarmsRepository: AlarmsRepository
) : ViewModel() {
private val _alarms = MutableStateFlow<List<Alarm>>(emptyList())
val alarms: StateFlow<List<Alarm>> get() = _alarms
var selectedAlarm = MutableStateFlow(Alarm())
...
}
Below is class for the data Alarm:
#Parcelize
#Entity
class Alarm(
#PrimaryKey(autoGenerate = true) val id: Int = 0,
var time: Calendar = Calendar.getInstance(),
...
) : Parcelable {
...
}
I want the value to increase when I press the button and write it in the textview.
However, the code below shows the increasing value as Log
But, on screen, the value in textview does not change.
I'm not sure what's wrong. Please let me know if you know.
Fragment.kt
class SettingFragment : Fragment(), View.OnClickListener {
companion object {
fun newInstance() = SettingFragment()
private val TAG = "SettingFragment"
}
private lateinit var viewModel: SettingViewModel
private lateinit var binding: FragmentSettingBinding
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_setting, container, false)
return binding.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProvider(this).get(SettingViewModel::class.java)
binding.lifecycleOwner = this
binding.button.setOnClickListener(this)
}
override fun onClick(v: View?) {
viewModel.increase()
Log.d(TAG, "Log Data" + viewModel.testInt.value)
}
}
FragmentViewModel
class SettingViewModel : ViewModel() {
val testInt: MutableLiveData<Int> = MutableLiveData()
init {
testInt.value = 0
}
fun increase() {
testInt.value = testInt.value?.plus(1)
}
}
fragment.xml
<?xml version="1.0" encoding="utf-8"?>
<layout>
<data>
<variable
name="viewModel"
type="com.example.ui.setting.SettingViewModel" />
</data>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".ui.setting.SettingFragment">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="#{String.valueOf(viewModel.testInt)}" />
<Button
android:id="#+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
</LinearLayout>
</layout>
<variable
name="viewModel"
type="com.example.ui.setting.SettingViewModel" />
This viewModel variable doesn't bind to a certain variable programmatically. So, when the value incremented, it won't reflect to the layout.
To fix this set the fragment's viewModel to this value:
viewModel = ViewModelProvider(this).get(SettingViewModel::class.java)
binding.viewModel = viewModel // <<<<< Here is the change
binding.lifecycleOwner = this
binding.button.setOnClickListener(this)
Side note:
You shouldn't use onActivityCreated() as it's is deprecated as of API level 28; you can normally its part of code to onCreateView(). And you can check here for other alternatives.
Why is val rv: RecyclerView = findViewById(R.id.material_list_rv) returning null?
MainActivity
class MainActivity : AppCompatActivity() {
var adaptor: MaterialListAdaptor = MaterialListAdaptor()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (savedInstanceState == null) {
supportFragmentManager.commit {
setReorderingAllowed(true)
add<MaterialListFragment>(R.id.fragment_container_view)
}
}
initRecyclerView()
setRecyclerViewData()
}
private fun initRecyclerView(){
val rv: RecyclerView = findViewById(R.id.material_list_rv)
rv.layoutManager = LinearLayoutManager(this#MainActivity)
rv.adapter = adaptor
}
private fun setRecyclerViewData(){
val l = DataSource.createMaterialList()
adaptor.setDataSet(l)
}
}
activity_main
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
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"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.fragment.app.FragmentContainerView
android:id="#+id/fragment_container_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
MaterialListFragment
class MaterialListFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return inflater.inflate(R.layout.fragment_material_list, container, false)
}
}
fragment_material_list
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MaterialListFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/material_list_rv"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:listitem="#layout/material_list_rv_list_item" />
</androidx.constraintlayout.widget.ConstraintLayout>
The problem
You placed the call to findViewById in MainActivity.onCreate. It's too early for that, the Fragment's view hasn't been created at that point.
You should be looking for the view in your MaterialListFragment. If you call findViewById in onCreateView, it will work. Like this:
val root = inflater.inflate(R.layout.fragment_material_list, container, false)
val rv = root.findViewById(R.id. material_list_rv)
// Do whatever (eg assign rv to a field in the Fragment to use it later)
return root
Some advice
Even if your code worked, try to have Activities, Fragments and custom Views manage their own children.
If the upper containers dive deep into their children, it's very easy to make mistakes (like the one here!) and you lose the ability to update internal details of Fragments and Views without breaking the containing Activity.
Activity/Fragment lifecycle chart
To orient yourself, keep this in mind (though this diagram is EXTREMELY simplified):
You are looking for RecylerView in MainActivity, where it does not exist.
Instead you should try to access it in FragmentActivity.
FragmentHome load layout_home.xml, and layout_home.xml displays a recyclerview and a button named btnMain
recyclerview include the item layout layout_voice_item.xml, it displays a button named btnChild。
I use displayCheckBox : LiveData<Boolean> to control whether both btnMain and btnChild are shown or not with the code android:visibility="#{!aHomeViewModel.displayCheckBox? View.VISIBLE: View.GONE}".
I find btnMain can be shown or not when I change the value of displayCheckBox, but btnChild keep to show, why?
FragmentHome.kt
class FragmentHome : Fragment() {
private lateinit var binding: LayoutHomeBinding
private val mHomeViewModel by lazy {
getViewModel {
HomeViewModel(provideRepository(mContext))
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(
inflater, R.layout.layout_home, container, false
)
binding.lifecycleOwner = this.viewLifecycleOwner
binding.aHomeViewModel=mHomeViewModel
val adapter = VoiceAdapters(mHomeViewModel)
binding.mvoiceList.adapter=adapter
mHomeViewModel.listVoiceBySort.observe(viewLifecycleOwner){
adapter.submitList(it)
}
...
return binding.root
}
}
HomeViewModel.kt
class HomeViewModel(private val mDBVoiceRepository: DBVoiceRepository) : ViewModel() {
private val _displayCheckBox = MutableLiveData<Boolean>(true)
val displayCheckBox : LiveData<Boolean> = _displayCheckBox
fun setCheckBox(isDisplay:Boolean){
_displayCheckBox.value = isDisplay
}
...
}
VoiceAdapters.kt
class VoiceAdapters (private val aHomeViewModel: HomeViewModel):
ListAdapter<MVoice, VoiceAdapters.VoiceViewHolder>(MVoiceDiffCallback()) {
...
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VoiceViewHolder {
return VoiceViewHolder(
LayoutVoiceItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
}
override fun onBindViewHolder(holder: VoiceViewHolder, position: Int) {
val aMVoice = getItem(position)
holder.bind(aHomeViewModel, aMVoice)
}
inner class VoiceViewHolder (private val binding: LayoutVoiceItemBinding):
RecyclerView.ViewHolder(binding.root) {
fun bind(mHomeViewModel: HomeViewModel, aMVoice: MVoice) {
binding.aHomeViewModel = mHomeViewModel
binding.executePendingBindings()
}
}
...
}
class MVoiceDiffCallback : DiffUtil.ItemCallback<MVoice>() {
...
}
layout_home.xml
<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>
<import type="android.view.View" />
<variable name="aHomeViewModel"
type="info.dodata.voicerecorder.viewcontrol.HomeViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/mvoice_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="#layout/layout_voice_item"
/>
<Button
android:id="#+id/btnMain"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="#{!aHomeViewModel.displayCheckBox? View.VISIBLE: View.GONE}"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
layout_voice_item.xml
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View" />
<variable name="aHomeViewModel"
type="info.dodata.voicerecorder.viewcontrol.HomeViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="#+id/btnChild"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="#{!aHomeViewModel.displayCheckBox? View.VISIBLE: View.GONE}"
/>
</LinearLayout>
</layout>
Giving a LifecycleOwner to LayoutVoiceItemBinding might helps.
voiceViewHolder.binding.lifecycleOwner = yourLifecycleOwner
The view holder pattern was not always part of android and before its wide adoption, a naive list implementation would have resulted in an inflated view rendered for each value in the submitted list. That is extremely memory inefficient.
With the adoption of a view holder pattern, views (the visual items) are only inflated up to the maximum that can be visible on the screen (and one or two extra for smooth scrolling)
The adapter however needs to be made aware of changes in the list's data.
https://developer.android.com/guide/topics/ui/layout/recyclerview#Adapter
If the list needs an update, call a notification method on the
RecyclerView.Adapter object, such as notifyItemChanged(). The layout
manager then rebinds any affected view holders, allowing their data to
be updated.
Observing the displayCheckBox : LiveData<Boolean> and calling adapter.notifyItemChanged might be the issue at hand.
I leave with another reference reiterating that view binding will not observe the live data but reduces the boilerplate code.
https://medium.com/androiddevelopers/android-data-binding-recyclerview-db7c40d9f0e4
What’s Left?
All the boilerplate from the RecyclerView is now handled
and all you have left to do is the hard part: loading data off the UI
thread, notifying the adapter when there is a data change, etc.
Android Data Binding only reduces the boring part.
I have an object that will fill some fields in my xml, but a mutablelivedata return a T value and I can't use the T properties. What I need to do to use the T object in my xml?
I don't want to create a mutablelivedata for each object member.
Sorry for my bad english..
class ExampleFragment: Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val viewModel = ViewModelProviders.of(this).get(MyVm::class.java)
val binding = DataBindingUtil.inflate<FragmentExampleBinding>(
inflater,
R.layout.fragment_house,
container,
false
)
binding.myVm = viewModel
binding.lifecycleOwner = this
return binding.root
}
}
class MyVm: ViewModel(){
val house: MutableLiveData<House> by lazy { MutableLiveData<House>().apply{ value =
House().apply{
door = "First door"
window = "My window"
}
} }
val thing: MutableLiveData<String> by lazy { MutableLiveData<String>().apply{ value = "something" } }
}
class House(){
var door: String = ""
var window: String = ""
}
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable name="my_vm" type="br.com.MyVm"/>
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
orientation="vertical"
>
<!--I need this work-->
<TextView
android:layout_width="wrap_parent"
android:layout_height="wrap_parent"
android:text="my_vm.house.door"
/>
<!--I need this work-->
<TextView
android:layout_width="wrap_parent"
android:layout_height="wrap_parent"
android:text="my_vm.house.window"
/>
<TextView
android:layout_width="wrap_parent"
android:layout_height="wrap_parent"
android:text="my_vm.thing"
/>
</LinearLayout>
<layout>
This code is correcty! A tried run and worked, I thounght it didn't work because the android studio didn't show the attributes when press ctrl + space.