Can this collection add Fragment codeblock become any simpler (Kotlin) - android

I am new to Kotlin. I wrote this code block to add a fragment when there are no fragments in the backstack. My code block however looks a lil clumsy. Can it be written in a better way?
with(supportFragmentManager) {
takeIf { backStackEntryCount == 0 }
?. apply {
with(beginTransaction()) {
add(R.id.container, FirstFragment())
commit()
}
}
}
also, with(beginTransaction()) shows me lint warning that transaction should be committed(). I think it has to do with my incorrect implementation of it. Kindly tell me how to get rid of that lint warning.
EDIT 1
Not here but at certain places I do perform some other operations along with add(). Stuff like adding the fragment to backstack and providing custom Transitions.
activity?.supportFragmentManager?.let {
with(it.beginTransaction()) {
setCustomAnimations(R.anim.slide_in_left, R.anim.slide_out_left,
R.anim.slide_in_right, R.anim.slide_out_right)
addToBackStack("second")
replace(R.id.container, SecondFragment())
commit()
}
}
Now there are a few methods that need to be called on FragmentTransaction object. This one is more suitable for my case.

How about
if(savedInstanceState == null) { // replaces the `takeIf` things
supportFragmentManager.beginTransaction()
.add(R.id.container, FirstFragment())
.commit()
}

you know kotlin provide extension function feature that used to make easy way to add fragement like below code ..
fun Fragment.addReportFragment(): Fragment {
mFragment = ReportFragment()
this.fragmentManager?.beginTransaction()?.replace(if (this is ReportFragment) LOGIN_ROOT else MAIN_ROOT, mFragment, REPORT_FRAGMENT)?.commit()
return mFragment as ReportFragment
}
and simple way to used below code..
var pickpowerfrag = PickPowerFragment()
fragmentManager.beginTransaction()
.replace(R.id.ReplaceFrame, pickpowerfrag)
.addToBackStack(null)
.commit()

Related

Execute function after FragmentManager commit

I have a toolbar with text that is updated after an item in my menu is selected.
The problem is that I call the function that handles the title in my toolbar right after my transaction like this:
supportFragmentManager.beginTransaction()
.replace(R.id.fragment, fragment)
.addToBackStack(null)
.commit()
setTitle()
However, when my function is called, the Fragment I get is always the one currently displayed and not the new one.
supportFragmentManager.fragments.last()
I'm guessing the Fragments transaction takes place in a Thread. How do I wait until it's complete to perform my function?
If you're using KTX extensions you can wait for fragment to be created using LifecycleScope instead of listening to fragment manager directly.
First ensure you have fragment-ktx dependency in your app level build.gradle:
dependencies {
...
implementation 'androidx.fragment:fragment-ktx:1.2.1'
...
}
Then you can perform a suspended execution that waits for your fragment to be created:
supportFragmentManager.beginTransaction()
.replace(R.id.fragment, fragment)
.addToBackStack(null)
.commit()
fragment.lifecycleScope.launchWhenCreated {
setTitle()
}
Since you are not adding your fragment to the back stack you cannot use the built-in FragmentManager.OnBackStackChangedListener so instead you can either call
executePendingTransactions() right after you commit the transaction or use commitNow() method instead of commit() to do the same, then handle your toolbar changes with setTitle().
i think i respond to your question so late but it could help someone who want the same solution :
fragment.getLifecycle().addObserver(new LifecycleEventObserver() {
#Override
public void onStateChanged(#NonNull LifecycleOwner source, #NonNull Lifecycle.Event event) {
if(event.getTargetState() == Lifecycle.State.RESUMED){
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
setTitle();
}
}, 500);
}
}
});

Backstack management : Restarter must be created only during owner's initialization stage

I am using a bottom navigation bar in my MainActivity to handle some fragments. This is the code used for switching between them:
private val mOnNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item ->
if (item.isChecked &&
supportFragmentManager.findFragmentById(R.id.act_main_fragment_container) != null
)
return#OnNavigationItemSelectedListener false
val fragment =
when (item.itemId) {
R.id.navigation_home -> fragments[0]
R.id.navigation_bookings -> fragments[1]
R.id.navigation_messages -> fragments[2]
R.id.navigation_dashboard -> fragments[3]
R.id.navigation_profile -> fragments[4]
else -> fragments[0]
}
this replaceWithNoBackStack fragment
return#OnNavigationItemSelectedListener true
}
the method replaceWithNoBackstack is just a short-hand for this:
supportFragmentManager
?.beginTransaction()
?.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
?.replace(containerId, fragment)
?.commit()
The problem is that when i switch faster between them, my app crashes with the following exception:
java.lang.IllegalStateException: Restarter must be created only during owner's initialization stage
at androidx.savedstate.SavedStateRegistryController.performRestore(SavedStateRegistryController.java:59)
at androidx.fragment.app.Fragment.performCreate(Fragment.java:2580)
at androidx.fragment.app.FragmentManagerImpl.moveToState(FragmentManagerImpl.java:837)
at androidx.fragment.app.FragmentManagerImpl.moveFragmentToExpectedState(FragmentManagerImpl.java:1237)
at androidx.fragment.app.FragmentManagerImpl.moveToState(FragmentManagerImpl.java:1302)
at androidx.fragment.app.BackStackRecord.executeOps(BackStackRecord.java:439)
at androidx.fragment.app.FragmentManagerImpl.executeOps(FragmentManagerImpl.java:2075)
at androidx.fragment.app.FragmentManagerImpl.executeOpsTogether(FragmentManagerImpl.java:1865)
at androidx.fragment.app.FragmentManagerImpl.removeRedundantOperationsAndExecute(FragmentManagerImpl.java:1820)
at androidx.fragment.app.FragmentManagerImpl.execPendingActions(FragmentManagerImpl.java:1726)
at androidx.fragment.app.FragmentManagerImpl$2.run(FragmentManagerImpl.java:150)
at android.os.Handler.handleCallback(Handler.java:789)
at android.os.Handler.dispatchMessage(Handler.java:98)
at android.os.Looper.loop(Looper.java:164)
at android.app.ActivityThread.main(ActivityThread.java:6709)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:769)
I've been searching a lot and couldn't find an answer.
I also got this error if I do an API call, put the app in background, wait for the response, and at the time I go back to the app, the app crashes because I am trying to display a dialog fragment immediately (the reason I think this is happening is that the transaction of recreating the fragment when coming back from the background is still in progress at the time of displaying the dialog fragment). I solved this in a hacky way by setting a 500ms delay for the dialog because I couldn't figure out other solutions.
Please ask if you need more details regarding this.
Thank you in advance!
POSSIBLE TEMP SOLUTIONS
EDIT
I solved this issue by downgrading the app compat depedency to androidx.appcompat:appcompat:1.0.2 but this is just a temporary solution, since i will have to update it in future. I'm hoping someone will figure it out.
EDIT 2
I solved the issue by removing setTransition() from fragment transactions. At least I know the reason why android apps does not have good transitions in general
EDIT 3
Maybe the best solution to avoid this issue and also make things work smoothly is just to use ViewPager to handle bottom bar navigation
because the version 1.0.0 has not check the state, so it will not throw the exception,
but the version 1.1.0 changes the source code,so it throws the exception.
this is the Fragment version-1.1.0 source code, it will invoke the method performRestore
void performCreate(Bundle savedInstanceState) {
if (mChildFragmentManager != null) {
mChildFragmentManager.noteStateNotSaved();
}
mState = CREATED;
mCalled = false;
mSavedStateRegistryController.performRestore(savedInstanceState);
onCreate(savedInstanceState);
mIsCreated = true;
if (!mCalled) {
throw new SuperNotCalledException("Fragment " + this
+ " did not call through to super.onCreate()");
}
mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
}
/**
the exception
**/
public void performRestore(#Nullable Bundle savedState) {
Lifecycle lifecycle = mOwner.getLifecycle();
if (lifecycle.getCurrentState() != Lifecycle.State.INITIALIZED) {
throw new IllegalStateException("Restarter must be created only during "
+ "owner's initialization stage");
}
lifecycle.addObserver(new Recreator(mOwner));
mRegistry.performRestore(lifecycle, savedState);
}
this is the version-1.0.0 source code,did not invoke the performRestore,so will not throw the exception
void performCreate(Bundle savedInstanceState) {
if (mChildFragmentManager != null) {
mChildFragmentManager.noteStateNotSaved();
}
mState = CREATED;
mCalled = false;
onCreate(savedInstanceState);
mIsCreated = true;
if (!mCalled) {
throw new SuperNotCalledException("Fragment " + this
+ " did not call through to super.onCreate()");
}
mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
}
There are two different solution which can handle this:
The first solution is to split the transaction。
Because we always use replace or merge remove and add into one Transaction.
We can split the transaction to two transaction like this:
FragmentTransaction ft = manager.beginTransaction();
Fragment prev = manager.findFragmentByTag(tag);
if (prev != null) {
//commit immediately
ft.remove(prev).commitAllowingStateLoss();
}
FragmentTransaction addTransaction = manager.beginTransaction();
addTransaction.addToBackStack(null);
addTransaction.add(layoutId, fragment,
tag).commitAllowingStateLoss();
because this two transaction will be two different Message which will be handled by Handler.
The second solution is check the state in advance.
we can follow the source code,check the state in advance
FragmentTransaction ft = manager.beginTransaction();
Fragment prev = manager.findFragmentByTag(tag);
if (prev != null) {
if (prev.getLifecycle().getCurrentState() != Lifecycle.State.INITIALIZED) {
return;
}
ft.remove(prev);
}
I recommend the first way,because the second way is folowing the source code,if the source
code change the code, it will be invalid。
I had the same problem.
val fragment = Account.activityAfterLogin
val ft = activity?.getSupportFragmentManager()?.beginTransaction()
//error
ft?.setCustomAnimations(android.R.anim.slide_in_left,android.R.anim.slide_out_right)0
ft?.replace(R.id.framelayout_account,fragment)
ft?.commit()
Changing the library version did not help.
I solved this by adding the ft?.AddToBackStack(null) line after the ft?.setCustomAnimations () method and that’s it.
Animation works and there are no crashes.
If you're using 'androidx.core:core-ktx:1.0.2',
try changing to 1.0.1
If you're using lifecycle(or rxFragment) and androidx_appcompat:alpha05, try changeing versio.
ex) appcompat : 1.1.0-beta01 or 1.0.2
I think's that it appears as an error when saving the state when the target fragment is reused (onPause-onResume).
I changed implementation to api for androidx.appcompat:appcompat:1.0.2 and its worked for me
If it can help, I have encountered the same issue with a BottomNavigationView and setCustomAnimations, basically by switching quickly between Fragments, you may end up starting a FragmentTransaction while the previous one has not finished and then it crashes.
To avoid that, I disable the Navigation Bar until the transition is finished. So I have created a method to enable/disable the BottomNavigationView items (disabling the BottomNavigationView itself does not disable the menu or I didn't find the way) and then I re-enable them once the transition is completed.
To disable the items I call the following method right before starting a FragmentTransition:
public void toggleNavigationBarItems(boolean enabled) {
Menu navMenu = navigationView.getMenu();
for (int i = 0; i < navMenu.size(); ++i) {
navMenu.getItem(i).setEnabled(enabled);
}
}
To re-enable them, I have created an abstract Fragment class for the Fragments loaded from the BottomNavigationView. In this class, I overrides onCreateAnimator (if you use View Animation you should override onCreateAnimation) and I re-enable them onAnimationEnd.
#Nullable
#Override
public Animator onCreateAnimator(int transit, boolean enter, int nextAnim) {
if(enter){ // check the note below
Animator animator = AnimatorInflater.loadAnimator(getContext(), nextAnim);
animator.addListener(new AnimatorListenerAdapter() {
#Override
public void onAnimationEnd(Animator animation) {
myActivity.toggleNavigationBarItems(true)
}
});
return animator;
}
return super.onCreateAnimator(transit, enter, nextAnim);
}
Note: as my enter and exit animations have the same duration, I don't need to synchronise them as the enter animation starts after the exit one. That's why the if (enter) is sufficient.
I fixed this problem with add 'synchronized' into add fragment method
before :
public void addFragment(int contentFrameId, Fragment fragment, Bundle param, boolean addToStack) {
try {
if (!fragment.isAdded()) {
if (param != null) {
fragment.setArguments(param);
}
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction()
.add(contentFrameId, fragment)
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
if (addToStack)
fragmentTransaction.addToBackStack(fragment.getClass().toString());
fragmentTransaction.commit();
}
} catch (IllegalStateException e) {
handleError(e.getMessage());
} catch (Exception e) {
handleError(e.getMessage());
}
}
after :
public synchronized void addFragment(int contentFrameId, Fragment fragment, Bundle param, boolean addToStack) {
try {
if (!fragment.isAdded()) {
if (param != null) {
fragment.setArguments(param);
}
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction()
.add(contentFrameId, fragment)
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
if (addToStack)
fragmentTransaction.addToBackStack(fragment.getClass().toString());
fragmentTransaction.commit();
}
} catch (IllegalStateException e) {
handleError(e.getMessage());
} catch (Exception e) {
handleError(e.getMessage());
}
}
This bug seems to be resolved using androidx.appcompat:appcomat:1.1.0-rc01 and androidx.fragment:fragment:1.1.0-rc03
https://developer.android.com/jetpack/androidx/releases/fragment#1.1.0-rc03
I have this issue when using setCustomAnimations.
by removing setCustomAnimations solved my problem.
also I have no problem when I create new instance of fragment before showing it even using setCustomAnimation.
EDIT: another way is adding fragment to backstack.
I was able to fix this (hopefully 😃) by using commitNow() instead of commit() for all bottom nav fragment transactions.
I like this approach better as it allows you to still use custom transitions between fragments.
Note: This is a solution only if you don't want your bottom nav transactions to be added to backstack (which you should not be doing anyways).
Nothing worked except Drown Coder's solution, but it was still not perfect, because it adds transactions to backstack. So if you press all buttons in bottom navigation, you have at least 1 of every fragment in backstack. I slightly improved this solution, so you don't use .replace() that crashes app whith thansaction animations.
Here is the code:
if (getChildFragmentManager().getBackStackEntryCount() > 0) {
getChildFragmentManager().popBackStack();
}
FragmentTransaction addTransaction = getChildFragmentManager().beginTransaction();
addTransaction.setCustomAnimations(R.animator.fragment_fade_in, R.animator.fragment_fade_out);
addTransaction.addToBackStack(null);
addTransaction.add(R.id.frame, fragment, fragment.getClass().getName()).commitAllowingStateLoss();
I found another way of creating this case.
CASE-1
Inflate a fragment in frame-layout at an activity
start an API request (don't consume the api response when app in foreground)
Keep your app in background
Consume the API request (suppose you want to add another fragment on api response)
Inflate another fragment using .replace() method on the same frame-layout
You will be able to create the Crash
CASE-2
Inflate a fragment in frame-layout at an activity
Start an API request
Consume the api in foreground (suppose you want to add another fragment on api response, using .replace() method of fragment-manager)
Put your app in background
Recreate your application (you can do this using "Don't keep activities", changing permission, changing system language)
Come back to your application
Your activity will start re-creating
Activity will auto recreate its already inflated fragment suppose it is of (point-1)
Make sure API is request again in on recreate case, after point-8
Consume API response and inflate another fragment using .replace() method
You will be able to create the Crash (As in this case, already a transition is running point-8, and you are adding another fragment at point-10)

Android Fragment transaction in background

I am developing an application with fragments. It has a JavaScript Interface, which is called in the Main Activity and has fragment replacing logic. When application is in foreground everything works OK, but when the application is in background, fragment transaction replace doesn't work. When I return to my application, I still see the old fragment and don't see the new one.
#JavascriptInterface
public void beginCall(String toast) {
FragmentTransaction fTrans;
taskFragment = TaskFragment.newInstance(toast,"");
fTrans = getSupportFragmentManager().beginTransaction();
fTrans.replace(R.id.frgmCont, taskFragment);
fTrans.commit();
}
What is wrong? Why the fragment transaction doesn't work in background?
After some time I've found the answer: it's impossible to perform a fragment transaction after onStop, it will result in java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState. I wasn't getting that Exception as JavascriptInterface was performed in a separate thread. When I forced my code to run in Main thread, I got that error. So I need to implement a different logic, also using some of Activity Life-cycle methods, or to switch to multiple activities logic. Hope my answer will help anyone.
Some use cases or architectures might require to trigger fragment transactions while app is in background.
We created following extension function:
fun FragmentTransaction.commitWhenStarted(lifecycle: Lifecycle) {
lifecycle.addObserver(object : LifecycleObserver {
#OnLifecycleEvent(value = Lifecycle.Event.ON_START)
fun onStart() {
lifecycle.removeObserver(this)
commit()
}
})
}
Use it just like any other version of commit, commitNow, commitAllowingStateLoss.
If the activity state is already at least started the observer will be called directly and the fragment transaction is executed. The lifecycle can be taken from activity or from fragment if the transaction is executed on a childFragmentManager
transaction.commitWhenStarted(lifecycle)
FragRecordSongList FragRecordSongList = new FragRecordSongList();
FragmentTransaction ft = getActivity().getSupportFragmentManager().beginTransaction();
ft.addToBackStack(FragRecordSongList.class.getName());
ft.replace(R.id.fragContainer, FragRecordSongList, FragRecordSongList.class.getName());
ft.commit();
Try this may be help you
#lilienberg commented a great solution for fragment transactions. If you are using the navigation component you can use something like this:
fun NavController.resumedNavigation(lifecycle: Lifecycle, destination: Int) {
if(lifecycle.currentState.isAtleast(Lifecycle.State.RESUMED)){
//App is resumed, continue navigation.
navigate(destination)
} else {
//When app is resumed, remove observer and navigate to destination/
lifecycle.addObserver(object: LifecycleObserver {
#OnLifecycleEvent(value = Lifecycle.Event.ON_RESUME)
fun onResume() {
lifecycle.removeObserver(this)
navigate(destination)
}
})
}
}
You can call this function from your Activity or Fragment like this:
findNavController(R.id.my_nav_host_fragment).resumedNavigation(
lifecycle, R.id.my_navigation_action)

IllegalStateException when adding & replacing Fragments; commit vs commitAllowingStateLoss

It is rare that this happens, but occasionally my app will crash due to an IllegalStateException when adding and replacing fragments. Here is how I am doing it, I do so with an animation.
private void addFragmentReplace(int containerId, Fragment fragment) {
// check if the fragment has been added already
Fragment temp = mFragmentManager.findFragmentByTag(fragment.getTag());
if (!Utils.checkIfNull(temp) && temp.isAdded()) {
return;
}
// replace fragment and transition with animation
mFragmentManager.beginTransaction().setCustomAnimations(R.anim.ui_slide_in_from_bottom_frag,
R.anim.ui_slide_out_to_bottom_frag).replace(containerId, fragment).addToBackStack(null)
.commit();
}
I have researched into changing "commit()" to "commitAllowingStateLoss()" but is that really a solution? It will prevent the crashes, however, won't it cause other conflicts such as the fragment not displaying at times or other? Is the following an odd improvement to my above code snippet?
private void addFragmentReplace(int containerId, Fragment fragment) {
// check if the fragment has been added already
Fragment temp = mFragmentManager.findFragmentByTag(fragment.getTag());
if (!Utils.checkIfNull(temp) && temp.isAdded()) {
return;
}
// replace fragment and transition with animation
try {
mFragmentManager.beginTransaction().setCustomAnimations(R.anim.ui_slide_in_from_bottom_frag,
R.anim.ui_slide_out_to_bottom_frag).replace(containerId, fragment).addToBackStack(null)
.commit();
} catch (IllegalStateException e) {
e.printStackTrace();
mFragmentManager.beginTransaction().setCustomAnimations(R.anim.ui_slide_in_from_bottom_frag,
R.anim.ui_slide_out_to_bottom_frag).replace(containerId, fragment).addToBackStack(null)
.commitAllowingStateLoss();
}
}
Thanks in advance. My concerns come from the documentation for commitAllowingStateLoss() which reads
Like {#link #commit} but allows the commit to be executed after an
activity's state is saved. This is dangerous because the commit can
be lost if the activity needs to later be restored from its state, so
this should only be used for cases where it is okay for the UI state
to change unexpectedly on the user.
Some tips or advice on this would be appreciated. Thanks!

Android Fragments recreated on orientation change

I'm developing an app that basically has an ActionBar. When my app starts, the Activity creates the fragments and attaches them to each tab, so when I switch I get different views.
The problems arise when I try to rotate the device. After some struggle, I noticed that Android automatically recreates the previously added fragments like this:
SummaryFragment.onCreate(Bundle) line: 79
FragmentManagerImpl.moveToState(Fragment, int, int, int) line: 795
FragmentManagerImpl.moveToState(int, int, int, boolean) line: 1032
FragmentManagerImpl.moveToState(int, boolean) line: 1014
FragmentManagerImpl.dispatchCreate() line: 1761
DashboardActivity(Activity).onCreate(Bundle) line: 864
...
and then I recreate the fragments as usual. So I have the "real" fragments that I expect to work correctly and their "hidden" Android-created counterparts that make my app crash. How can I avoid this behavior? I already tried to call setRetainInstance(false) in the SummaryFragment.
Thank you
You need to check for a savedInstanceState [edit: in your parent activity], and if it exists, don't create your fragments.
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
if (savedInstanceState == null) {
// Do your oncreate stuff because there is no bundle
}
// Do stuff that needs to be done even if there is a saved instance, or do nothing
}
When you create your activity, check to make sure that it doesn't already exist. If it exists, do nothing...Android will recreate it for you.
private void initFragment() {
FragmentManager fragMgr = getSupportFragmentManager();
if (fragMgr.findFragmentByTag(LEADERBOARD_FRAG_TAG) != null) { return; }
frag = new HdrLeaderboardFragment();
FragmentTransaction ft = fragMgr.beginTransaction();
ft.replace(R.id.leaderboard_fragment_wrapper, frag, LEADERBOARD_FRAG_TAG);
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
ft.commit();
}
If you have the similar ui(no specific layout-land files) for both orientations you can set android:configChanges="keyboardHidden|orientation" to the activity in your manifest file.
If don't provide please the source code where you're adding the fragments to tabs, and I'll try to help you with improvements.
I am not sure if there is a better solution, but this is what i did in latest program.
When a fragment is created automatically by system on orientation change and if you want to keep track of them in host activity, catch them in host activity's OnAttachFragment() method. And they get the arguments by default, so you can use them to find out which fragment it is.
public void onAttachFragment(Fragment fragment) {
super.onAttachFragment(fragment);
if (fragment != null) {
if(fragment.getArguments() != null) {
switch (fragment.getArguments().getString(ARG_PARAM1)) {
case FragmentATag:
if (myFragmentA != fragment) {
myFragmentA = (FragmentA) fragment;
}
break;
case FragmentBTag:
if (myFragmentB != fragment) {
myFragmentB = (FragmentB) fragment;
}
break;
}
}
}
}

Categories

Resources