I have a fullscreen dialog (defined in FullScreenDialog()) being shown on click of a button, but would like to be able to also programmatically set elements of the dialog. Something like this in the host fragment's onCreateView():
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
view.btn_computePaletteFragment.setOnClickListener {
val fragmentManager = fragmentManager
val fullScreenDialog = FullScreenDialog()
val transaction = fragmentManager?.beginTransaction()
transaction?.add(android.R.id.content, fullScreenDialog)?.addToBackStack(null)?.commit()
fullScreenDialog.dialog_title.text = "Computed Palette Input"
}
return view
}
and the corresponding class for the custom fullscreen dialog Fragment:
class FullScreenDialog : DialogFragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(org.plainsound.hejicalc.R.layout.fs_dialog, container, false)
view.button_close.setOnClickListener { dismiss() }
return view
}
}
However, I get a Null pointer exception: Attempt to invoke virtual method 'android.view.View android.view.View.findViewById(int)' on a null object reference. What is the correct way to do this?
The reason you're getting an exception is because the dialog hasn't been inflated yet. I know you mentioned in a comment that it's already been instantiated, but this just means that the object has been constructed, it provides no guarantee that the Android system has called onCreateView for the dialog yet.
However even if you were to get this to work, through a delay or layout listener it would be incredibly bad practice because of how Android manages fragments. If there is a rotation, low memory issue or something similar, the system can recreate the fragments just using the default constructor which will result in you losing the state you set.
In order to to this properly you should pass arguments to the fragment using setArguments and then retrieving them in the dialog with getArguments. These arguments are managed by the system and will be kept regardless of whether the fragment is recreated.
You can also use a shared ViewModel if you have complex data to share, or the new fragment results API which you can read about here.
Related
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
I have a fragment that I want to display as an embedded fragment in a ViewPager and as a Bottom Sheet. I followed this https://developer.android.com/reference/android/app/DialogFragment#DialogOrEmbed and created a DialogFragment
private val mViewModel: CardPricesViewModel by viewModels()
private val binding by viewBinding(FragmentCardDetailPricesBinding::inflate)
companion object {
// This is the same value as the navArg name so that the SavedStateHandle can acess from either
const val ARG_SKU_IDS = "skuIds"
fun newInstance(skus: List<Long>?) =
CardDetailPricesFragment().apply {
arguments = Bundle().apply {
putLongArray(ARG_SKU_IDS, skus?.toLongArray())
}
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
}
}
However, when it gets inflated in a ViewPager the background dims as though it is a BottomSheetDialogFragment
However, when I manually do it with
supportFragmentManager
.beginTransaction()
.replace(binding.cardPricesFragmentContainer.id, cardDetailPricesFragment)
.commit()
It works fine. I see that the FragmentStateAdapter uses FragmentViewHolders instead of the using transactions directly (?), so I am not sure how to resolve this issue. I see that onCreateDialog() is being called, so if I call dismiss() after onViewCreated(), it works properly, but I am not sure if this a workaround
After some digging, I found the DialogFragment.setShowsDialog(boolean) method that you can use to disable the dialog being created.
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
}
Yes, I have written my view access inside onViewCreated. So sometimes it's showing
IllegalStateException: view must not be null
Immediately if I run after cleaning the project, it's working without any error !!!
CONFUSED !
Another confusing issue is if I use view inside onViewCreated
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {myTextView.text = "MyName"}
its working fine
but if assign it inside onResponse of a Retrofit call
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
netWorkCallByRetrofit()
}
netWorkCall(){
myTextView.text = "MyName"
}
it's not working.
Immediately if I run after cleaning the project, it's working without any error !!!
Again it's working well if initialize it in onViewCreated
like
tv: TextView
tv = myTextView
tv.text = "MyName"
it's working!!!
Any clue ?
In a fragment, always make a reference to the view you are returning in onCreateView.
I tend to make a private var thisView: View? = null in the fragment main body, then in onCreateView assign that to the view you are returning. the rest of the fragment always refer to, for example thisView.myTextView.text and you shouldn't have any more weird nullpointer errors.
Some code might make this a bit less chaotic:
class MyClass: Fragment(){
private lateinit var thisView: View // can also make it nullable and set it to null here
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
thisView = inflater.inflate(R.layout.my_cool_layout, container, false)
thisView.myTextView.setOnClickListener{
toast("yaaaay")
}
return thisView
}
}
I need to make a custom behaviour, when the user press the back button then the user will go to certain destination programatically. I actually have read this Handling back button in Android Navigation Component
but I don't understand how to use that custom back button code.it seems weird to me.
I have tried using this code below
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
fragmentView = inflater.inflate(R.layout.fragment_search_setting, container, false)
// set custom back button
val callback = requireActivity().onBackPressedDispatcher.addCallback(this) {
// navigate to certain destination
Navigation.findNavController(fragmentView).popBackStack(R.id.destination_create_event, false)
}
return fragmentView
}
but I get type mismatch error like this
You must create new Instance of the OnBackPressedCallback abstract class and implement its abstract method .
I hope this helps you:
val callback = requireActivity().onBackPressedDispatcher.addCallback(object : OnBackPressedCallback(true){
override fun handleOnBackPressed() {
Navigation.findNavController(fragmentView).popBackStack(R.id.destination_create_event, false)
}
})
// The callback can be enabled or disabled here or in the lambda
}