In my mind, I should add binding.lifecycleOwner=this when I use viewModel.
I find many projects such as Code A doesn't add binding.lifecycleOwner=this, why?
The Code A is from the project https://github.com/enpassio/Databinding
Code A
class AddToyFragment : androidx.fragment.app.Fragment() {
private lateinit var binding: AddToyBinding
...
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(
inflater, R.layout.fragment_add_toy, container, false
)
setHasOptionsMenu(true)
return binding.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
(requireActivity() as AppCompatActivity).supportActionBar?.setDisplayHomeAsUpEnabled(true)
//If there is no id specified in the arguments, then it should be a new toy
val chosenToy : ToyEntry? = arguments?.getParcelable(CHOSEN_TOY)
//Get the view model instance and pass it to the binding implementation
val factory = AddToyViewModelFactory(provideRepository(requireContext()), chosenToy)
mViewModel = ViewModelProviders.of(this, factory).get(AddToyViewModel::class.java)
binding.viewModel = mViewModel
binding.fab.setOnClickListener {
saveToy()
}
binding.lifecycleOwner=this //I think it should add
}
binding.lifecycleOwner used for observing LiveData with data binding.
Kind of android:text=#{viewModel.text} where val text:LiveData<String>. View will observe text changes at runtime.
as far as I understand it,
binding.lifecycleOwner= this
used in one side to make a subscription to receive messages when liveData is changed (so information in view to be consistent), and in another side to delete observer from list when view and fragment is destroyed (to prevent memory leaks). Fragment's viewLifecycleOwner is more suitable to use as lifecycleOwner for binding in this way, isn't it?
Related
From a Google Codelab (can't remember which one), they adviced doing the following for fragments:
class MyFragment : Fragment() {
private var _binding: MyFragmentBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View {
_binding = MyFragmentBinding.inflate(inflater, container, false)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
And then accessing the views with e.g. binding.button1.
Is there a specific reason for doing it like this, with _binding and binding? Are there better methods? Perhaps an extension for Fragments - like a BaseFragment - to avoid code duplication.
It's not recommended to use BaseFragment or BaseActivity or BaseViewModel... it will just add boilerplate code to your project.
For binding you can just use it like this:
Declaration:
private var binding: MyFragmentBinding? = null
onCreateView:
binding = MyFragmentBinding.inflate(inflater, container, false)
binding?.root
Usage:
binding?.button...
binding?.text...
binding?.cardView...
onDestroyView:
binding = null
And everything is going to work just fine but we use the null check a lot (?) and it's making the code messy and we need to get a lot of null checks if we need something from a certain view, so we are sure that between onCreateView and onDestroyView, the binding is not null so we have _binding and binding:
private var _binding: MyFragmentBinding? = null
private val binding get() = _binding!!
We make _binding mutable with var so we can give it a value, and we make it nullable so we can clear it later.
And we have binding that have a custom getter so that means that each time we call binding it's going to return the latest value from _binding and force that it's not null with !!.
Now we seperate our variables, we have _binding to initialize and clear our binding, and we have binding that is immutable and not nullable to use it only for accessing views without null check ?
See this question for some answers about the reason why binding needs to be nullable in a fragment.
See this answer of mine where I linked some articles about the problems with BaseFragments. You can usually achieve the code reuse without the drawbacks of inheritance by using extension properties and functions.
Here is an example of a property delegate that takes care of releasing the ViewBinding reference when necessary and rebuilding it when necessary. If you use this, all you need is a single binding property. Example is from the article about this tool.
class FirstFragment: Fragment(R.layout.first_fragment) {
private val binding by viewBinding(FirstFragmentBinding::bind)
override fun onViewCreated(view: View, bundle: Bundle?) {
super.onViewCreated(view, bundle)
binding.buttonPressMe.onClick {
showToast("Hello binding!")
}
}
I just saw that CommonsWare has adressed this issue in this post.
Here is the parent class:
abstract class ViewBindingFragment<Binding : ViewBinding>(
private val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> Binding
) : Fragment() {
private var binding: Binding? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View {
return bindingInflater(inflater, container, false).apply { binding = this }.root
}
override fun onDestroyView() {
binding = null
super.onDestroyView()
}
protected fun requireBinding(): Binding = binding
?: throw IllegalStateException("You used the binding before onCreateView() or after onDestroyView()")
protected fun useBinding(bindingUse: (Binding) -> Unit) {
bindingUse(requireBinding())
}
}
He then subclasses ViewBindingFragment like so:
class ListFragment :
ViewBindingFragment<TodoRosterBinding>(TodoRosterBinding::inflate) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
useBinding { binding ->
binding.items.layoutManager = LinearLayoutManager(context)
}
}
}
Though I am not sure it will eventually lead to less code, if useBinding { binding -> } needs to be called in several functions.
I'm playing around with Kotlin on Android and one thing makes me confused.
When I converted few Fragments from Java to Kotlin I got this:
class XFragment : Fragment() {
private var binding: FragmentXBinding? = null
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = FragmentUhfReadBinding.inflate(inflater, container, false)
return binding!!.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding!!.slPower.addOnChangeListener(this)
binding!!.btnClearTagList.setOnClickListener(this)
}
// ...
private fun updateUi(){
binding!!.someTextView.text = getSomeTextViewText()
binding!!.someSlider.value = getSomeSliderValue()
}
}
I can't make binding non-nullable, because it has to be initialized after XFragment class constructor, in onCreateView() or later.
So with this approach it has to be nullable and I have to put !! everywhere.
Is there some way to avoid these !!?
The official documentation suggests this strategy:
private var _binding: FragmentXBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
Ultimately, it becomes just like requireActivity() and requireContext(). You just need to remember not to use it in a callback that might get called outside the view lifecycle.
Note, you can create your view using the super-constructor layout parameter and then bind to the pre-existing view in onViewCreated. Then you might not even need to have it in a property. I rarely need to do anything with it outside onViewCreated() and functions directly called by it:
class XFragment : Fragment(R.layout.fragment_x) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val binding = FragmentXBinding.bind(view)
binding.slPower.addOnChangeListener(this)
binding.btnClearTagList.setOnClickListener(this)
}
}
I will try to explain my issue step by step.
I created data variable within xml file
<data>
<variable
name="SLC90RViewModel"
type="com.example.poy.ui.questionnaires.tools.SCL90RViewModel"/>
</data>
In my ViewModel i need to retrieve string array from resources (which is stored in xml file), so i passed app context into ViewModel constructor. Now it looks like this:
class SCL90RViewModel(val context: Context) : ViewModel() {
val item = context.getResources().getStringArray(R.array.firstArray)
}
Finally, in my Fragment i want to assign LifecycleOwner and ViewModel to the binding:
class QuestionnaireFragment : Fragment(R.layout.fragment_questionnaire) {
private var binding: FragmentQuestionnaireBinding? = null
private val scl90rViewModel: SCL90RViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val fragmentBinding = FragmentQuestionnaireBinding.inflate(inflater, container, false)
binding = fragmentBinding
return fragmentBinding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding?.apply {
lifecycleOwner = viewLifecycleOwner
SCL90RViewModel = scl90rViewModel
}
}
}
However, SCL90RViewModel in binding?.apply{} is outlined with red and message "Classifier 'SCL90RViewModel' does not have a companion object, and thus must be initialized here" pops up. How can i fix it?
SCL90RViewModel is ambiguous here, it can be the binding variable or the ViewModel class. So use this inside the apply block to refer to the binding variable.
binding?.apply {
lifecycleOwner = viewLifecycleOwner
this.SCL90RViewModel = scl90rViewModel
}
binding is a data binding, I set the lifecycle owner to the lifecycle of the view.
Is it necessary ?
It seems that the app can work well if I remove binding.lifecycleOwner = this.viewLifecycleOwner.
Code
class FragmentHome : Fragment() {
private lateinit var binding: LayoutHomeBinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(
inflater, R.layout.layout_home, container, false
)
// Set the lifecycle owner to the lifecycle of the view
binding.lifecycleOwner = this.viewLifecycleOwner //Must I set lifecycleOwner for a data binding?
...
}
}
If you are using a LiveData object with your binding class, it is required to set a lifecycle owner to define the scope of the LiveData object.
So if you have a LiveData object like:
private val _name = MutableLiveData("John")
val name: LiveData<String> = _name
And you use it for binding like:
android:text="#{vm.name}"
Then you need to specify a lifecycle owner.
I have a ParentFragment and a ChildFragment. They are working pretty fine.
My problem is that in the future I might create many child fragments and this makes me write this boilerplate code for every single child fragment. Thus, I would like to optimize my ParentFragment so that I do not have to write boilerplate code for every single new child framment I create in the future.
ParentFragment
abstract class ParentFragment<T: ViewDataBinding>: Fragment() {
#LayoutRes
abstract fun getLayoutResId(): Int
protected lateinit var binding: T
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
super.onCreateView(inflater, container, savedInstanceState)
return DataBindingUtil.inflate<T>(inflater, getLayoutResId(), container, false).apply { binding = this }.root
}
ChildFragment
class ChildFragment: ParentFragment<FragmentChildBinding>() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//THIS IS THE BOILERPLATE METHOD I AM TALKING ABOUT.
//I WOULD LIKE TO MOVE THIS CODE IN THE PARENTFRAGMENT
initBinding()
}
#LayoutRes
override fun getLayoutResId() = R.layout.fragment_child
fun initBinding() {
val viewModel: ChildViewModel = getViewModel() //This method is from Koin
binding.viewModel = viewModel
binding.lifecycleOwner = this
}
I tried to move this initBinding method code into ParentFragment but I got errors. Any suggestions would be greatly appreciated.
So, your current issue is that you want to make your initBinding() function common in such a way that all child fragment can access it.
Now issue is that logic inside initBinding comes with koin dependency that gets resolved dynamically. So, each child fragment may have different ViewModel instance.
There is one way you can resolve this problem:
Take your ViewModel object as method parameter so that you can inject it from your child fragment while having method in parent fragment. check out below how:
initBinding() Method in ParentFragment with viewModel as method parameter that we will pass on from child fragments.
abstract class ParentFragment<T: ViewDataBinding>: Fragment() {
// Other stuffs removed for sack of simplicity
protected fun initBinding(viewModel: ViewModel) {
binding.viewModel = viewModel
binding.lifecycleOwner = this
}
}
Accessing it from ChildFragment:
class ChildFragment: ParentFragment<FragmentChildBinding>() {
// Other stuffs removed for sack of simplicity
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// THIS IS HOW YOU CAN CONSUME THIS METHOD
initBinding(getViewModel())
}
}
You need to call serVariable() on your binding object to set ViewModel in the parent Fragment. Optionally in your parent Fragment you can get ViewModel through abstract function if child Fragments have their own ViewModels.
You can follow this post to solve your problem.