I have assigned value for a lateinit variable in Kotlin when creating instance for fragment, Then i used the variable in fragment's oncreateview method. It works perfectly, but sometime it return kotlin.UninitializedPropertyAccessException and my application was closed. I don't know when it occurred, i have checked multiple scenarios. but it never works.
Here i added my code,
private lateinit var mViewModel: SearchViewModel
companion object {
fun getInstance(activity: Activity?): SearchFragment {
val fragment = SearchFragment()
fragment.mViewModel = SearchViewModel(activity)
return fragment
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
mBinding = DataBindingUtil.inflate(inflater, R.layout.search_fragment, container, false)
mBinding.viewModel = mViewModel
}
Fragment.onCreateView is called by the framework on an instance it creates by using the default constructor, not by calling your getInstance method which it doesn't know about.
Maybe you should initialize the variable in onAttach instead?
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 have been doing a tutorial that is a bit out of date and uses synthetics rather than bindings. I am trying to use bindins. I am trying to set up a listener in a fragment (AddEditFragment.kt). It's using a callback to MainActivity.onSaveClicked.
In AddEditFragment I use an import for the binding
import com.funkytwig.tasktimer.databinding.FragmentAddEditBinding
I have a lateinit on the first line of the class defenition
class AddEditFragment : Fragment() {
private lateinit var binding: FragmentAddEditBinding
I am initializing the bunding in onActivityCreated and setting up the listner. I can use findViewById to get the ID
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
val addEditSave = view?.findViewById(R.id.addEditSave) as Button
addEditSave.setOnClickListener { listener?.onSaveClicked() }
}
And this works fine but if I try to use the binding
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
binding.addEditSave.setOnClickListener { listener?.onSaveClicked() }
}
The code does not show any errrors but it does not seem to create the listner. I have a Log.d in the onSaveClicked callback function and when I use the first (findViewById) version of the function it works (it calles onSaveClicked) but with the second version (using bindings) onSaveClicked does not get called when I click the Button.
I Cant figre out why the second version does not work, I thought the two versions of onActivityCreated should do the same thing.
The interface in AddEditFragment.kt is
interface OnSaveClicked {
fun onSaveClicked()
}
In fragment you should add your view in onCreateView or in OnViewCreated not in onActivityCreated
Please refer link for more details.
private var _binding: FragmentAddEditBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentAddEditBinding.inflate(inflater, container, false)
val view = binding.root
binding.addEditSave.setOnClickListener { listener?.onSaveClicked() }
return view
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
OK, thanks for all the help. turned out I was doing the inflate wrong.
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
Log.d(TAG, "onCreateView")
binding = FragmentAddEditBinding.inflate(layoutInflater, container, false)
return binding.root
}
I was doing
binding = FragmentAddEditBinding.inflate(layoutInflater)
I missed out on the last 2 args as I was taking the code from the inflate when I am in an Activity, not a Fragment. I think it is to do with the layout effecticly being in the parent.
My Goal
I am trying to access the widget that was created inside my fragment using viewBinding.
What I have done / Info about my app
The language I am using is kotlin.
I have already added the code below into gradle:
buildFeatures{
dataBinding = true
viewBinding = true
}
I have tested binding.aTextView.setText("Code working.") inside my main activity and it works.
What's the problem
I have tested the setText code inside activity and it works. The problem right now is the same code when I move into the fragment it wouldn't work. And I am sure that the code has been executed as I putted a toast above it and the toast executed successfully which mean it should have at least reached that point before but not sure due to what reason there wasn't any changes.
My mainActivity Code:
class MainProgramActivity : AppCompatActivity() {
lateinit var binding: ActivityMainProgramBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainProgramBinding.inflate(layoutInflater)
setContentView(binding.root)
replaceFragment(FragmentMainPage())
}
private fun replaceFragment(fragment: Fragment){
val fragmentManager = supportFragmentManager
val fragmentTransaction = fragmentManager.beginTransaction()
fragmentTransaction.replace(R.id.fragmentContainerView,fragment)
fragmentTransaction.commit()
}
}
My fragment code:
class FragmentMainPage : Fragment(R.layout.fragment_main_page) {
lateinit var binding: FragmentMainPageBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
Toast.makeText(getActivity(),"Text!",Toast.LENGTH_SHORT).show();
binding = FragmentMainPageBinding.inflate(layoutInflater)
binding.aTextView.setText("Code working") //<-- I want this code to make changes towards the textView
return super.onCreateView(inflater, container, savedInstanceState)
}
}
The aTextView itself is empty at the beginning, the expected result will be the aTextView to show "Code working".
I see two problems with your code. First, exactly what Michael pointed out. You're returning the super method when you should be returning the View you just created (binding.root). Second, you're currenly leaking your fragment. When you viewbind a fragment, you are supposed to set the variable to null in onDestroyView(), as per defined in the documentation.
class FragmentMainPage : Fragment(R.layout.fragment_main_page) {
private var _binding: FragmentMainPageBinding? = null
private val binding get() = _binding!! // non-null variable in order to avoid having safe calls everywhere
// create the view through binding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentMainPageBinding.inflate(layoutInflater, container, false)
return binding.root
}
// view already created, do whatever with it
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.aTextView.setText("Code working")
}
// clear the binding in order to avoid memory leaks
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
I was going through developer docs for Data binding. I found the following snippet:
private var _binding: ResultProfileBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = ResultProfileBinding.inflate(inflater, container, false)
val view = binding.root
return view
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
Can anyone let me know the principle and advantage of using two variables for binding which are used in same fragment?
At first glance, it seems like lateinit would be a more natural choice. However, the Fragment instance is still usable after onDestroyView since Fragment instances can be torn down and reattached later. lateinit won't let you change the parameter back into uninitialized state, so it's not suitable for this purpose.
Using !! can result in Kotlin NPEs, which is not great. I would suggest modifying the sample code to provide better documentation and error reporting, like this:
/** This property is only valid between onCreateView and onDestroyView. */
private val binding get() = _binding ?:
error("Binding is only valid between onCreateView and onDestroyView.")
But practically, your Fragment is not going to be so complicated that you would have trouble tracking down an error like this anyway.
binding is a non-null property with nullable backing field, so when you access it you don't have to constantly use ? to check for nullability.
It will however throw KotlinNullPointerException if accessed when it's not valid as described by the comment.
EDIT
this solution will cause a memory leak, as pointed out by IR42, and here is why
ORIGINAL ANSWER
null safety, but I think using lateinit is a better solution for that purpose
private lateinit var binding : ResultProfileBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = ResultProfileBinding.inflate(inflater, container, false)
return binding.root
}