Android kotlin viewlifecycleowner sometimes is null in fragment - android

In my fragment, I use viewlifecycleowner to observe live data but in some cases my app crashes with this log:
Fatal Exception: java.lang.IllegalStateException: Can't access the Fragment View's LifecycleOwner when getView() is null i.e., before onCreateView() or after onDestroyView()
So I ask how to reproduce and avoid this issue please!

You should register your LiveData observer in Fragment's onViewCreated. where viewlifecycleowner can never be null.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.someLiveData.observe(viewLifecycleOwner, Observer<Something> {
// Update the UI.
})
}

Related

Fragment onSaveInstanceState() called after onDestroyView()

The application started to receive some crashes (it is not reproducible 100%) due to some lifecycle issue for the Fragment.
I'm using view binding and I'm manually invalidating the binding as per Android recommendations to avoid high memory usage in case the reference to the binding is kept after the Fragment is destroyed.
private var _binding: FragmentCustomBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View = FragmentCustomBinding.inflate(inflater, container, false).also {
_binding = it
}.root
override fun onDestroyView() {
_binding = null
super.onDestroyView()
}
override fun onSaveInstanceState(outState: Bundle) {
outState.apply {
putString(BUNDLE_KEY_SOME_VALUE, binding.etSomeValue.text.toString())
}
super.onSaveInstanceState(outState)
}
I'm getting a NullPointerException in onSaveInstanceState() as the binding is null as this was called after onDestroyView().
Any idea how I could solve this without manually creating a saved state and manually handling it?
The binding = null is causing the issue. To get rid of the _binding = null in the correct manner use this code:
class CustomFragment : Fragment(R.layout.fragment_custom) {
private val binding: FragmentCustomBinding by viewBinding()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Any code we used to do in onCreateView can go here instead
}
}
According to an article on this workaround:
This technique uses an optional backing field and a non-optional val which is only valid between onCreateView and onDestroyView.
In onCreateView, the optional backing field is set and in onDestroyView, it is cleared. This fixes the memory leak!
It seems the answer for this is in how the fragments are handled, even when they do not have a view, as changes in the Activity state can still trigger onSavedInstanceState() thus I can end up in scenarios where I am in onSavedInstanceState() but without a view.
This seems to be intentional as fragments are still supported whether they have a view or not.
The recommendation was to use the view APIs for saving and restoring state (or my SavedStateRegistery).
A few more details can be found here: https://issuetracker.google.com/issues/245355409

Android fragment DataBinding nonnull getter return null

As per the android documentation, To get the data binding within a fragment, I use a non-nullable getter, but sometimes' When I try to access it again, after I'm wait for the user to do something, I receive a NullPointerException.
private var _binding: ResultProfileBinding? = null
private val binding get() = _binding!!
override fun onCreateView(inflater: LayoutInflater,container: ViewGroup?,
savedInstanceState: Bundle?): View? {
_binding = ResultProfileBinding.inflate(inflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupViews()
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
private fun setupViews() {
// On click listener initialized right after view created, all is well so far.
binding.btnCheckResult.setOnClickListener {
// There is the crash when trying to get the nonnull binding.
binding.isLoading = true
}
}
Does anyone know what the cause of the NullPointerException crash is? I'm trying to avoid not working according to the android documentation, and do not return to use nullable binding property (e.g _binding?.isLoading). Is there another way?
I can't explain why you're having any issue in the code above since a View's click listener can only be called while it is on screen, which must logically be before onDestroyView() gets called. However, you also asked if there's any other way. Personally, I find that I never need to put the binding in a property in the first place, which would completely avoid the whole issue.
You can instead inflate the view normally, or using the constructor shortcut that I'm using in the example below that lets you skip overriding the onCreateView function. Then you can attach your binding to the existing view using bind() instead of inflate(), and then use it exclusively inside the onViewCreated() function. Granted, I have never used data binding, so I am just assuming there is a bind function like view binding has.
class MyFragment: Fragment(R.layout.result_profile) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val binding = ResultProfileBinding.bind(view)
// Set up your UI here. Just avoid passing binding to callbacks that might
// hold the reference until after the Fragment view is destroyed, such
// as a network request callback, since that would leak the views.
// But it would be fine if passing it to a coroutine launched using
// viewLifecycleOwner.lifecycleScope.launch if it cooperates with
// cancellation.
}
}

How to handle buttons of custom Dialog using DialogFragment?

I have a custom dialog with X button, that dialog is called in a Fragment and here i have to manage all the clicks from the Alert.
Which is the best way to do so?
I actually was going to set the click listeners in the DialogFragment but i have to change some layout stuff and set variables from my Fragment so it will be better if i manage it from the fragment directly.
Here is my code now:
class ElencoDialog(private val testata: Testata, private val elimina: Boolean): DialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
btnInvia = view.findViewById(R.id.btnInvia)
btnInvia.setOnClickListener {
}
}
}
And here is my fragment where i show the dialog:
class ElencoFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
adapter = LettureListAdapter({
ElencoDialog(it, false).show(parentFragmentManager, "ElencoDialog")
}, {
ElencoDialog(it, true).show(parentFragmentManager, "ElencoDialog")
})
}
}
So instead of managing the click from the DialogFragment how can i manage the clicks directly from my Fragment?
First of all it is bad practice to have anything apart from the default constructor in a DialogFragment (or any Fragment). Although this might work initially, the system might need to recreate the fragment for various reasons, rotation, low memory etc. and it will attempt to use an empty constructor to do so. You should instead be using fragment arguments to pass simple data (covered in another question), a ViewModel for more complex data (I prefer this method anyway) or the new fragment results API, which I've outlined below.
But in answer to your specific question, to interact between your dialog fragment and main fragment, you have a few options:
Target fragment
You can set your original fragment as a target of your dialog but using setTargetFragment(Fragment). The fragment can then be retrieved safely from your dialog using getTargetFragment. It would probably be best practice to have your fragment implement an interface which you can cast to has the relevant callback methods.
Fragment results API
This is a relatively new API that attempts to replace the above, you can read more about it here: Communicating between fragments.
ViewModel
You can use a shared ViewModel scoped to the activity or parent fragment and keep your state in there. This would also solve the problem of having to pass your initial state through your fragment constructor. I won't explain how they work here as that's another question, but I would take a look here: ViewModel overview.
Pass callBack from fragment to your Dialog Fragment
class ElencoFragment() : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
adapter = LettureListAdapter({
ElencoDialog(it, false){
//handle Callback
}.show(parentFragmentManager, "ElencoDialog")
}, {
ElencoDialog(it, true){
//handle Callback
}.show(parentFragmentManager, "ElencoDialog")
})
}
}
Your Dialog Fragment
class ElencoDialog(private val testata: Testata, private val elimina: Boolean, block : () -> Unit): DialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
btnInvia = view.findViewById(R.id.btnInvia)
btnInvia.setOnClickListener {
block()
}
}
}

Optimizing Parent Fragment for ViewModel and DataBinding in order to avoid boilerplate code

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.

Get value from Bottom Sheet Dialog Fragment

I'm starting bottomSheetDialogFragment from a fragment A.
I want to select the date from that bottomSheetDialogFragment then set it in the fragment A.
The select date is already done, I just want to get it in the fragment A to set it in some fields.
How can I get the value?
Any suggestions how to do it?
Create an interface class like this
public interface CustomInterface {
public void callbackMethod(String date);
}
Implement this interface in your Activity or Fragment. and make an object of this Interface.
private CustomInterface callback;
Initialize it in onCreate or onCreateView
callback=this;
Now pass this callback in your BottomSheetDialogFragment constructor when you call it.
yourBottomSheetObject = new YourBottomSheet(callback);
yourBottomSheetObject.show(getSupportFragmentManager()," string");
Now in your BottomSheetFragment's constructor
private CustomInterface callback;
public SelectStartTimeSheet(CustomInterface callback){
this.callback=callback;
}
And at last use this callback object to set your date
callback.callbackMethod("your date");
and yout will recieve this date in your Fragment or Your Activity in callbackMethod function.
override the constructor of a fragment is a bad practice as the document said:
Every fragment must have an
* empty constructor, so it can be instantiated when restoring its
* activity's state.
if you using another constructor that passing a callback as the param, when the fragment is resotored by the framework, your app crash
the recommend way is using viewModel and livedata.
Android navigation architecture component
eg:
Suppose you open Fragment B from Fragment A using navController.
and you want some data from fragment B to Fragment A.
class B :BottomSheetDialogFragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val root = inflater.inflate(R.layout.your_layout, container, false)
root.sampleButton.setOnClickListener {
val navController = findNavController()
navController.previousBackStackEntry?.savedStateHandle?.set("your_key", "your_value")
dismiss()
}
}
and in your Fragment A:
findNavController().currentBackStackEntry?.savedStateHandle?.getLiveData<String>("your_key")
?.observe(viewLifecycleOwner) {
if (it == "your_value") {
//your code
}
}
you can use do as below:
Select Account Fragment code
class SelectAccountFragment(val clickListener: OnOptionCLickListener) : BottomSheetDialogFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.bottom_fragment_accounts, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val list = DataProcessorApp(context).allUsers
val rvAccounts = view.findViewById<RecyclerView>(R.id.rvAccounts)
rvAccounts.layoutManager = LinearLayoutManager(context)
rvAccounts.adapter = AccountsAdapter(context, list)
Log.e(tag,"Accounts "+list.size);
tvAccountAdd.setOnClickListener {
val intent = Intent(context,LoginActivity::class.java)
startActivity(intent)
}
tvManageAccounts.setOnClickListener {
Log.e(tag,"Manage Click")
clickListener.onManageClick()
}
}
interface OnOptionCLickListener{
fun onManageClick()
}
}
Now show and get call back into another fragment /activity as below
SelectAccountFragment accountFragment = new SelectAccountFragment(() -> {
//get fragment by tag and dismiss it
BottomSheetDialogFragment fragment = (BottomSheetDialogFragment) getChildFragmentManager().findFragmentByTag(SelectAccountFragment.class.getSimpleName();
if (fragment!=null){
fragment.dismiss();
}
});
accountFragment.show(getChildFragmentManager(),SelectAccountFragment.class.getSimpleName());
If you are using BottomSheetDialogFragment , since it's a fragment, you should create your interface and bind to it at onAttach lifecycle method of the fragment , doing the appropriate cast of activity reference to your listener/callback type.
Implement this interface in your activity and dispatch change when someone click in a item of fragment's inner recyclerview, for instance
It's a well known pattern and are explained better at here
One big advice is rethink your app architecture, since the best approach is to always pass primitive/simple/tiny data between Android components through Bundle, and your components are able to retrieve the required state with their dependencies later on.
For example, you should never pass along large Objects like Bitmaps, Data Classes , DTO's or View References.
first there is some serialization process going on regarding Parcel which impacts in app responsiveness
second it can lead you to TransactionTooLarge type of error.
Hope that helps!
You can also use LocalBroadcastManager. And as hglf said, it is better to keep the empty constructor for your fragment and use newInstance(Type value) instead to instantiate your fragment if you still want to use the interface callBack way.
You can use the benefit of Navigation library:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val navController = findNavController();
// After a configuration change or process death, the currentBackStackEntry
// points to the dialog destination, so you must use getBackStackEntry()
// with the specific ID of your destination to ensure we always
// get the right NavBackStackEntry
val navBackStackEntry = navController.getBackStackEntry(R.id.your_fragment)
// Create our observer and add it to the NavBackStackEntry's lifecycle
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME
&& navBackStackEntry.savedStateHandle.contains("key")) {
val result = navBackStackEntry.savedStateHandle.get<String>("key");
// Do something with the result
}
}
navBackStackEntry.lifecycle.addObserver(observer)
// As addObserver() does not automatically remove the observer, we
// call removeObserver() manually when the view lifecycle is destroyed
viewLifecycleOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_DESTROY) {
navBackStackEntry.lifecycle.removeObserver(observer)
}
})
}
For more info, read the document.
The accepted answer is wrong.
What you can do is just user Fragment A's childFragmentManager when calling show().
like this:
val childFragmentManager = fragmentA.childFragmentManager
bottomSheetDialogFragment.show(childFragmentManager, "dialog")

Categories

Resources