I know this question seems to be asked and answered, but it is not true. Because I haven't met a solution yet.
I have Activity with a lot of fragments. And I store all transitions history (can return to each fragment in LIFO order with the back press - because each adds to back stack)
I want to reach the next feature:
When I press back - view of bottom fragment must not be re-creating.
I do next
1) Use android navigation components and transitions like
fun navigate(#IdRes resId: Int, bundle: Bundle? = null, navOptions: NavOptions? = null, sharedElements: List<View>? = null) {
navController.navigate(resId, bundle, navOptions, sharedElements?.let {
if (it.isEmpty()) null else
FragmentNavigatorExtras(
*it.map { view -> Pair(view, view.transitionNameCompat.safe) }
.filter { pair -> pair.second.isNotEmpty() }
.toTypedArray()
)
})
}
where idRes is destination id (not transition id)
2) hold the content view in fragment and detach it from the parent in onCreateView view method. Because getView() returns null even fragment appear from back stack.
private var contentView: View? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
prepareView(contentView) ?: createView(inflater, container)
private fun prepareView(view: View?): View? {
val parent = view?.parent
val viewGroup = parent as? ViewGroup
viewGroup?.removeView(view)
return view
}
protected open fun createView(inflater: LayoutInflater, container: ViewGroup?): View? {
val layout = layout()
if (layout == -1)
throw IllegalArgumentException("You need to override \"layout()\" fun or override \"createView\" fun")
return inflater.inflate(layout(), container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
if (contentView == null) {
onFirstInitialization(view, savedInstanceState)
} else {
onNextInitialization(view, savedInstanceState)
}
onEachInitialization(view, savedInstanceState)
contentView = view
}
I've got exception ONLY IF I transit to next fragment and press "back" rapidly! In normal mode all good and correct.
Exception:
java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.
at android.view.ViewGroup.addViewInner(ViewGroup.java:5050)
at android.view.ViewGroup.addView(ViewGroup.java:4881)
at android.view.ViewGroup.addView(ViewGroup.java:4821)
at android.view.ViewGroup.addView(ViewGroup.java:4794)
at androidx.fragment.app.FragmentManagerImpl.moveToState(FragmentManagerImpl.java:890)
at androidx.fragment.app.FragmentManagerImpl.addAddedFragments(FragmentManagerImpl.java:2092)
at androidx.fragment.app.FragmentManagerImpl.executeOpsTogether(FragmentManagerImpl.java:1866)
at androidx.fragment.app.FragmentManagerImpl.removeRedundantOperationsAndExecute(FragmentManagerImpl.java:1822)
at androidx.fragment.app.FragmentManagerImpl.execPendingActions(FragmentManagerImpl.java:1723)
at androidx.fragment.app.FragmentManagerImpl.dispatchStateChange(FragmentManagerImpl.java:2624)
at androidx.fragment.app.FragmentManagerImpl.dispatchActivityCreated(FragmentManagerImpl.java:2580)
at androidx.fragment.app.Fragment.performActivityCreated(Fragment.java:2571)
at androidx.fragment.app.FragmentManagerImpl.moveToState(FragmentManagerImpl.java:907)
at androidx.fragment.app.FragmentManagerImpl.moveFragmentToExpectedState(FragmentManagerImpl.java:1235)
at androidx.fragment.app.FragmentManagerImpl.moveToState(FragmentManagerImpl.java:1301)
at androidx.fragment.app.FragmentManagerImpl.dispatchStateChange(FragmentManagerImpl.java:2620)
at androidx.fragment.app.FragmentManagerImpl.dispatchActivityCreated(FragmentManagerImpl.java:2580)
at androidx.fragment.app.FragmentController.dispatchActivityCreated(FragmentController.java:246)
at androidx.fragment.app.FragmentActivity.onStart(FragmentActivity.java:541)
at androidx.appcompat.app.AppCompatActivity.onStart(AppCompatActivity.java:201)
at android.app.Instrumentation.callActivityOnStart(Instrumentation.java:1470)
at android.app.Activity.performStart(Activity.java:7176)
at android.app.ActivityThread.handleStartActivity(ActivityThread.java:3086)
at android.app.servertransaction.TransactionExecutor.performLifecycleSequence(TransactionExecutor.java:180)
at android.app.servertransaction.TransactionExecutor.cycleToPath(TransactionExecutor.java:165)
at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:142)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:70)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1926)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:193)
at android.app.ActivityThread.main(ActivityThread.java:6923)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:537)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
I can solve this by place prepareView to onDestroyView lifecycle method and everything work correctly, but it produces memory leak: I find it out - when the view has not a parent after onDestroyView then onDestroy never called and fragments hold in memory even after the back press.
You can say "Hey dude, don't hurry, use your app slowly and the exception will never be thrown, as you say. But app in production and I met crashes with this bug" :( How I can handle this case?
UPDATE:
I researched a little and found out something. Exception thrown only when view not detached from window. And view detach from window only after 300-400 ms after new Fragment opened
Yes! It's true. You can't create again its object because it has a parent. The main problem may you haven't noticed is whenever you called your fragment again then onCreateView() is called.
So, in-sort I suggest you move your code from onCreateView() to onCreate() method of the fragment.
I also struggled with this type of problem. So, inflate your view in onCreate() method and return main view in onCreateView() method.
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
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.
}
}
Is there any way to make a shared transition between fragment1 inside activity1 and fragment2 inside activity2?
I have tried achieving this like so:
val intent = Intent(this, RecipeActivity::class.java)
intent.putExtra("recipeId", recipeId)
val elem1 =
Pair<View, String>(itemView.findViewById(R.id.recipe_preview), "preview")
val elem2 =
Pair<View, String>(itemView.findViewById(R.id.recipe_title), "title")
val elem3 =
Pair<View, String>(itemView.findViewById(R.id.recipe_rating_stars), "rating_stars")
val elem4 =
Pair<View, String>(itemView.findViewById(R.id.recipe_rating), "rating")
val elem5 =
Pair<View, String>(itemView.findViewById(R.id.recipe_description), "description")
val elem6 =
Pair<View, String>(itemView.findViewById(R.id.author_avatar), "avatar")
val options =
ActivityOptionsCompat.makeSceneTransitionAnimation(
this, elem1, elem2, elem3, elem4, elem5, elem6
)
startActivity(intent, options.toBundle())
But that didn't work so well. Do I have to redesign my app so those two fragments will be inside a single activity or is there any workaround? Thank you
The idea: Pause the transaction until the targed fragment is fully loaded, created and is about to be drawn. Then continue the transaction.
The code:
Everything you do in your first activity is ok and we won't touch it.
The first thing your activity has to is to stop the transaction. Therefore you need to call supportPostponeEnterTransition() in onCreate() of your second activity. This will tell android to wait with the transaction until you tell it to start it.
Secondly you need to know when the fragment is about to be drawn. In my use case I display some fragments in a ViewPager what makes things a lot easier, as you can add an ViewTreeObserver to it and wait until the ViewPager is loaded because you know that at this point the fragments are already created and basically drawn even if you can't see them. When using frgaments the normal way with transaction you need some trickery.
Important: Everything from now on is not tested but in theory it should work.
In your fragment you do something like this:
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_home, container, false)
}
Instead you to do it like this:
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_home, container, false)
return view
}
You need the root view of your fragment because we will add the ViewTreeObserver to it.
But before you to that you need to create an interface in your FragmentClass or add the method to your preexisting one:
interface FragmentListener {
fun resumeTransaction()
}
You need to implement it in your activity:
override fun resumeTransaction() {
supportStartPostponedEnterTransition()
}
In your fragment we need to get the Activity as listener. In the onAttach do following:
try {
// Instantiate the FragmentListener so we can send the event to the host
listener = context as FragmentInterface
} catch (e: ClassCastException) {
// The activity doesn't implement the interface, throw exception
throw ClassCastException((context.toString() + " must implement FragmentInterface"))
}
Now we get back to the ViewTreeObserver. In the onCreateView you do this:
viewPager.viewTreeObserver.addOnPreDrawListener(
object : ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
listener.resumeTransaction()
}
}
)
If I didn't forget anything this should work. If not please tell me, I will then try to make an example app later this day as I don't have more time now.
I am trying to use the Navigation Component with a retained Fragment and an enter animation. The fragment appears with the animation as expected. On rotating my device I get the following crash:
java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.
I can solve this by removing the enter animation (from the nav graph) or by making the fragment not be retained.
Does anyone have a clue how to solves this as I wanted both retained and the animation.
Here is my onCreate:
private var binding: FragmentDemoBinding? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? {
if (binding == null) {
binding = FragmentDemoBinding.inflate(inflater, container, false)
}
retainInstance = true
return binding?.root
}
edit: Actually on reading a bit more I am not sure it makes sense to try and use retained fragments within the navigation component. If I need to persist data on configuration changes I guess a headless fragment or use onSaveInstance or ViewModel component etc.
In onDestroyView method remove all views or just animation view.
override fun onDestroyView() {
if(view != null){
val parent = view.parent
parent.removeAllViews
}
super.onDestroyView()
}
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")