I have 2 Groups in my layout which control the visibility of my Views.
However, I cannot set their visibility via DataBinding:
<layout>
<data>
<import type="android.view.View"/>
<variable
name="viewModel"
type="co.aresid.book13.fragments.trackinglist.TrackingListViewModel"
/>
</data>
<androidx.constraintlayout.widget.ConstraintLayout>
...
<androidx.constraintlayout.widget.Group
android:id="#+id/content_group"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="#{viewModel.hideLoadingAndShowContent ? View.VISIBLE : View.GONE, default=gone}"
app:constraint_referenced_ids="tracking_list_recycler_view"
/>
<androidx.constraintlayout.widget.Group
android:id="#+id/loading_group"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="#{viewModel.hideLoadingAndShowContent ? View.GONE : View.VISIBLE, default=visible}"
app:constraint_referenced_ids="progress_circular"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
The hideLoadingAndShowContent variable is a LiveData which gets its value from a corresponding MutableLiveData in my ViewModel:
private val _hideLoadingAndShowContent = MutableLiveData<Boolean>()
val hideLoadingAndShowContent: LiveData<Boolean>
get() = _hideLoadingAndShowContent
This LiveData is only set in the ViewModel and does not occur in the Fragment class.
In the Fragment class, I have also set the binding.lifecycleOwner:
binding.lifecycleOwner = viewLifecycleOwner
What detail am I missing out?
I forgot to pass the ViewModel to the layout binding in my Fragment class:
binding.viewModel = viewModel
Because that android:visibility does not support binding variable observation,
You can create BindingAdapter this way
#BindingAdapter("mutableVisibility")
fun setMutableVisibility(view: View, visibility: MutableLiveData<Boolean>) {
val owner = (view.getParentActivity() ?: view.context) as LifecycleOwner
if (owner != null) {
visibility.observe(
owner,
Observer { value ->
view.visibility = if(value) View.VISIBLE else View.GONE
})
}
}
Utility function for getting activity from view.
fun View.getParentActivity(): AppCompatActivity?{
var context = this.context
while (context is ContextWrapper) {
if (context is AppCompatActivity) {
return context
}
context = context.baseContext
}
return null
}
Then in your XML you can do it like
<androidx.constraintlayout.widget.Group
android:id="#+id/content_group"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="#{viewModel.hideLoadingAndShowContent}"
app:constraint_referenced_ids="tracking_list_recycler_view"
/>
Related
I have one relative layout and one ImageView. I want to set visibility based on Image loading like if image loads successfully then imageview is visible and if some error occurs relative layout is visible. How can I manage this scenario in data binding using BindingAdapter ?
Your question is not clear so I don't know if it will help you.
These are steps to implement
1: Create parameters in BindingAdapter.kt
#BindingAdapter("showLoading")
fun View.showLoading(loading: Boolean) {
if (loading) {
visible()
} else {
gone()
}
}
#BindingAdapter("showError")
fun View.showError(error: Boolean) {
if (error) {
visible()
} else {
gone()
}
}
fun View.gone() {
this.visibility = View.GONE
}
fun View.visible() {
this.visibility = View.VISIBLE
}
2: Use parameters [app:showLoading="#{viewModel.showLoading}"] and [app:showError="#{viewModel.showError}"] created in file 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">
<data>
<variable
name="viewModel"
type="com.xxx.xxx.MainViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#EAEAE2"
android:orientation="vertical">
<Button
android:id="#+id/btnAdd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ShowLoading" />
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:showLoading="#{viewModel.showLoading}" />
<RelativeLayout
android:id="#+id/viewError"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#color/colorPrimary"
app:showError="#{viewModel.showError}" />
</LinearLayout>
</layout>
3: Create showError and showLoading variables in Viewmodel.
And assign the viewModel variable to the binding.
class MainViewModel: ViewModel() {
val showError: MutableLiveData<Boolean> = MutableLiveData()
val showLoading: MutableLiveData<Boolean> = MutableLiveData()
init {
showError.postValue(false)
showLoading.postValue(true)
}
}
class MainActivity : AppCompatActivity() {
private val viewModel = MainViewModel()
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.lifecycleOwner = this
binding.viewModel = viewModel
initViews()
}
private fun initViews() {
binding.btnAdd.setOnClickListener {
viewModel.showError.postValue(true)
viewModel.showLoading.postValue(false)
}
}
}
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.
Visibility property not working
<data>
<import type="android.view.View"/>
<variable
name="viewmodel"
type="com.wiesoftware.spine.ui.auth.LoginViewModel" />
</data>
This is my progressbar code
<ProgressBar
android:id="#+id/pbLogin"
android:visibility="#{viewmodel.isVisibles ? View.VISIBLE : View.GONE, default=gone}"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="#+id/editTextTextPersonName2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="#+id/editTextTextPersonName" />
I am changing the value of isVisibles in viewModel but it doesn't reflect please give a solution. This is the class where I change isVisibles value.
class LoginViewModel(
private val authRepositry: AuthRepositry
):ViewModel() {
var email:String?=null
var password:String?=null
var loginEventListener:LoginEventListener?=null
var isVisibles:Boolean=false
fun loginButtonClicked(view: View){
isVisibles=true
if (email.isNullOrEmpty() || password.isNullOrEmpty()){
loginEventListener?.onLoginFailed("Please enter credentials.")
isVisibles=false
return
}
}
}```
> I have updated viewmodel class please check and let me know where is the issue I am using Kotlin Please provide me solution thanks
try add this
binding.lifecycleOwner = this // activity
I have implemented RecyclerView in my app with Kotlin using Refrofit, MVVM, DataBinding, Coroutines. The problem is i have some nested arrays which i want to parse using viewModel. Here is the structure of the api:
What i want is to parse the inner services arrays inside the categoryService array.
The xml items layout is like this:
<data>
<variable
name="services"
type="com.techease.fitness.models.categoryServices.Service" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_margin="#dimen/tenDP"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#drawable/round_card_view"
android:orientation="vertical"
android:padding="#dimen/tenDP"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="#+id/ivService"
android:layout_width="150dp"
android:layout_height="150dp"
android:scaleType="fitXY"
android:padding="#dimen/tenDP"
android:src="#drawable/test_image_24"
bind:avatar="#{services.attachment}" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:hint="#string/user_name"
android:text="#{services.title}"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:hint="#string/user_name"
android:text="#{services.duration}" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
Here is my existing viewModel which only returns the last item from categoryService[last] and displaying the services items.
private lateinit var job: Job
private val catServices = MutableLiveData<List<Service>>()
val discoverServices: MutableLiveData<List<Service>>
get() = catServices
fun discoverServices(context: Context) {
job = CoroutinesIO.ioThenMain(
{
repository.discoverServices(context)
}, {
for (i in 0 until it!!.data.categoryServices.size) {
for (services in 0 until it.data.categoryServices.get(i).services.size) {
catServices.value = it.data.categoryServices.get(i).services
}
}
}
)
}
i have nested for loops in viewModel which is parsing the target array correctly but only displaying the last item service array. Here is the recyclerView implementation:
val apiRepository = ApiRepository()
factory = DiscoverViewModelFactory(apiRepository)
viewModel = ViewModelProvider(this, factory).get(DiscoverServiceViewModel::class.java)
binding.viewModel = viewModel
viewModel.discoverServices(this)
viewModel.discoverServices.observe(this, Observer { services ->
rvFeatured.also {
it.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
it.setHasFixedSize(true)
if (services != null) {
it.adapter = DiscoverServicesAdapter(services, this)
}
}
})
And here is the adapter which is fully binded:
private val services: List<Service>,
private val listenerHomeServices: DiscoverServicesClickListener
) : RecyclerView.Adapter<DiscoverServicesAdapter.ServicesViewHolder>() {
override fun getItemCount() = services.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
ServicesViewHolder(
DataBindingUtil.inflate(
LayoutInflater.from(parent.context),
R.layout.custom_discover_layout,
parent,
false
)
)
override fun onBindViewHolder(holder: ServicesViewHolder, position: Int) {
holder.recyclerViewServicesBinding.services = services[position]
}
class ServicesViewHolder(
val recyclerViewServicesBinding: CustomDiscoverLayoutBinding
) : RecyclerView.ViewHolder(recyclerViewServicesBinding.root)}
Again i want to parse the nested service array, but currently only the last categoryService1 service array items are displayed by recyclerView. Hope this make sense.
I have created sample app to demo my question:
A TextView and a Button, text view visibility is bound to viewModel.bar
I want the button to switch the value of viewModel.bar when clicked and the UI to get updated as well.
However, this is not happening. The value is changed but the UI is not updated.
Layout File:
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View"/>
<variable name="viewModel"
type="com.example.bindingone.MainViewModel"/>
</data>
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="#+id/text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World"
android:visibility="#{viewModel.bar ? View.VISIBLE : View.GONE}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<Button
android:text="Update UI"
android:onClick="clicked"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</android.support.constraint.ConstraintLayout>
</layout>
MainActivity File:
class MainActivity : AppCompatActivity() {
private val viewModel = MainViewModel()
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
binding.viewModel = viewModel
}
fun clicked(v: View) {
viewModel.bar.value = viewModel.bar.value?.not() ?: false
}
}
MainViewModel file:
class MainViewModel : ViewModel() {
var bar = MutableLiveData<Boolean>()
}
After creating binding, add this line binding.setLifecycleOwner(this).
/**
* Sets the {#link LifecycleOwner} that should be used for observing changes of
* LiveData in this binding. If a {#link LiveData} is in one of the binding expressions
* and no LifecycleOwner is set, the LiveData will not be observed and updates to it
* will not be propagated to the UI.
*
* #param lifecycleOwner The LifecycleOwner that should be used for observing changes of
* LiveData in this binding.
*/
#MainThread
public void setLifecycleOwner(#Nullable LifecycleOwner lifecycleOwner)