Android fragment backstack animation fail after orientation change - android

I have an activity which hosts two fragments with only one shown at a time. Effectively the user, through different environmental conditions, should be able to toggle between the two at any given time.
There is a LoginFragment which is the first thing the user sees on login, and a LockoutFragment which may replace the LoginFragment after a user logs in and we see their account is locked (naturally).
That is the typical case, but there is a case in which LockoutFragment is presented first, if say, the user is using the app and their account is locked for some reason, and we re-open the host activity (LoginActivity), showing the LockoutFragment, but giving them a button to "Return to login", which toggles appearance of the LoginFragment (also naturally).
Thus, my goal is to allow a user to toggle between the two fragments, whichever is displayed first. My host activity uses the following functions to achieve this effect:
private void showLockoutFragment() {
if (mLockoutFragment == null) {
mLockoutFragment = new LockoutFragment();
}
transitionToFragment(FRAGMENT_LOCKOUT, mLockoutFragment);
}
private void showLoginFragment() {
if (mLoginFragment == null) {
mLoginFragment = new LoginFragment();
}
transitionToFragment(FRAGMENT_LOGIN, mLoginFragment);
}
private void transitionToFragment(String transactionTag, Fragment fragment) {
if (!getFragmentManager().popBackStackImmediate(transactionTag, 0)) {
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.setCustomAnimations(
R.animator.fade_in, R.animator.fade_out,
R.animator.fade_in, R.animator.fade_out);
ft.addToBackStack(transactionTag);
ft.replace(R.id.fragment_container, fragment, transactionTag);
ft.commit();
}
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// non configuration change launch
if (savedInstanceState == null) {
Bundle extras = getIntent().getExtras();
if (extras != null) {
// decide which fragment to show
boolean shouldLockout = extras.getBoolean(EXTRA_SHOULD_LOCKOUT);
if (shouldLockout) {
showLockoutFragment();
} else {
showLoginFragment();
}
} else {
showLoginFragment();
}
} else {
// retrieve any pre-existing fragments
mLoginFragment = (LoginFragment)getFragmentManager().findFragmentByTag(FRAGMENT_LOGIN);
mLockoutFragment = (LockoutFragment)getFragmentManager().findFragmentByTag(FRAGMENT_LOCKOUT);
}
}
These functions work together like a charm, with one exception: when, after initial launch of the app, a user
attempts log in,
is taken to the lockout fragment,
reorients the device, and
navigates back to the login fragment,
the login fragment is now present but invisible - as if the popEnter animation was never played. I know it is present because I can still interact with it.
It is also worth noting the following:
I have setRetainInstance(true) on both fragments
This only occurs when a user reorients the device from the lockout fragment
I have tried this on both a simulator and device running Lollipop with same results
Is it possible that the back stack is being corrupted after reorientation?
Thank you!

Ok, so it turns out the issue actually lies in my use of setRetainInstance. According to the docs for that method:
Control whether a fragment instance is retained across Activity re-creation (such as from a configuration change). This can only be used with fragments not in the back stack. [emphasis mine]
While this appears rather cryptic to me, it seems that using setRetainInstance(true) on a fragment that is on the back stack could simply have unintended consequences. In my case, the fragment seemed to be retained, but its popEnter animation was never being called (post-rotation). Again, weird, but I guess just avoid that combination.

Related

Android Fragments: Fragment is randomly recreated 1 day later

The bounty expires in 12 hours. Answers to this question are eligible for a +200 reputation bounty.
martin1337 is looking for a canonical answer:
Need to fix this asap - I cant simulate it by myself by any means. I just got this log from one of many users who are affected by this bug.
I have interesting issue with my app for some time now. Unfortunately I personally cant simulate this behavior on any of my phones but there are some users who are affected by this.
The issue is that I have Fragment which can be started only by pressing button by user. And this fragment is removed from stack after user exits it.
Fragment is used to open WebView with payment gateway to handle transactions (paying by credit card for items).
What is happening to some users is that if they leave phone and app in background (screen is locked and they are not using phone at all), sometimes when phone wakes up, transaction from like 2 days ago is executed without their knowledge. It will just randomly start that Fragment which was used 2 days ago without any user interaction.
It seems like Fragment just popped without calling constructor with data out of nowhere. The only way how to start this fragment again is to push button "Buy Item" on specific screen. This screen is not even there when user unlocked that phone.
I cant simulate this behavior but its happening to some users and its annoying.
Anyone knows what can be a cause of this and how to prevent that?
Code:
startFragmentForResult(
PurchaseFragment(provider, id, sum.first, sum.second, discountId,
validFrom = validFromDateIso, duration = duration),
object : FragmentResult<Boolean>() {
override fun onFragmentResult(result: Boolean) {
App.log("PurchaseDis: BasketFragment - onFragmentResult - enable back")
mainActivity?.onBackEnabled(true)
removeThis{
App.log("FragmentTest - BasketFragment - onFragmentResult - after remove")
mainActivity?.refreshData()
}
}
})
}
fun startFragmentForResult(f: BaseFragment, receiver: FragmentResult<*>, hidePrevFragment: Boolean = true){
val isStarting = mainActivity?.isFragmentStarting?:false
if (!isStarting) {
mainActivity?.isFragmentStarting = true
fragmentStarting = true
app.sysLog("startFragmentForResult ($debugTitle) -> (${f.debugTitle})")
mainActivity?.startFragmentForResult(f, receiver, hidePrevFragment = hidePrevFragment)
}else if(BuildConfig.DEBUG) App.log("Prevent multiple fragment start")
}
private var fragmentResultReceivers = HashMap<BaseFragment, BaseFragment.FragmentResult<*>>()
fun startFragmentForResult(f: BaseFragment, receiver: BaseFragment.FragmentResult<*>, hidePrevFragment: Boolean = true) {
fragmentResultReceivers[f] = receiver
startFragment(f, hidePrevFragment = hidePrevFragment)
}
fun startFragment(
f: BaseFragment,
allowGoBack: Boolean = true,
hidePrevFragment: Boolean = true) {
App.log("MainActivity: startFragment: ${f.title}")
app.sysLog("startFragment (${f.debugTitle})")
App.log("FragmentArray: ${getFragments().size}")
makeFragmnentTransaction { ft ->
ft.setCustomAnimations(f.animIn, R.animator.fade_out)
val tag = f.javaClass.simpleName
if (allowGoBack && (f.isModal || f.canGoBack)) {
if (hidePrevFragment){
// hide previous fragment
getTopFragment()?.let {
ft.hide(it)
it.onHide()
}
}
ft.add(R.id.content, f, tag)
} else {
fragmentResultReceivers.values.forEach { it.onFragmentResultCanceled() }
fragmentResultReceivers.clear()
ft.replace(R.id.content, f, tag)
}
ft.runOnCommit {
updateUI(f)
}
}
}
private fun makeFragmnentTransaction(body: (FragmentTransaction) -> Unit) {
with(supportFragmentManager.beginTransaction()) {
body(this)
commitAllowingStateLoss()
}
}
fun removeFragment(f: BaseFragment, initDefaultTabIfEmpty: Boolean = true, onCommit: (() -> Unit)? = null) {
app.sysLog("removeFragment (${f.debugTitle})")
makeFragmnentTransaction { ft ->
ft.setCustomAnimations(R.animator.fade_in, f.animOut)
ft.remove(f)
val frgs = getFragments()
val fI = frgs.indexOf(f)
if (fI == frgs.lastIndex && fI > 0) {
// show previous fragment again - only if removed fragment is last one
val af = frgs[fI - 1]
app.sysLog("onShowAgain (${af.debugTitle})")
af.onShowAgain()
ft.show(af)
}
ft.setReorderingAllowed(false)
ft.runOnCommit {
afterFragmentRemoval(f, initDefaultTabIfEmpty)
onCommit?.invoke()
}
}
}
private fun getFragments(): List<BaseFragment> {
return ArrayList<BaseFragment>().apply {
for (e in supportFragmentManager.fragments) {
if (e is BaseFragment)
add(e)
}
}
}
fun getTopFragment(): BaseFragment? = getFragments().lastOrNull()
Maybe worth mentioning. I have data which phones are causing this issue:
"deviceBrand": "samsung",
"deviceModel": "SM-G991B",
"systemSdkInt": 33,
"deviceBrand": "samsung",
"deviceModel": "SM-G990B",
"systemSdkInt": 33,
"deviceBrand": "samsung",
"deviceModel": "SM-G970F",
"systemSdkInt": 31,
All of those devices are Samsung and all of them are Android 11+
UPDATE:
I have new logs from user which is affected by it:
2023/01/13 15:11:59: Purchase successful
2023/01/13 15:11:59: returnFragmentResult (Purchase)
2023/01/13 15:11:59: removeFragment (Purchase)
2023/01/13 15:11:59: onShowAgain (Basket)
2023/01/13 15:11:59: removeFragment (Basket)
2023/01/13 15:11:59: onShowAgain (Tickets)
2023/01/13 15:11:59: FragmentManagerListener: onFragmentDestroyed: Purchase
2023/01/13 15:11:59: FragmentManagerListener: onFragmentDetached: Purchase
2023/01/13 15:12:00: FragmentManagerListener: onFragmentDestroyed: Basket
2023/01/13 15:12:00: FragmentManagerListener: onFragmentDetached: Basket
2023/01/14 18:38:59: App create
2023/01/14 18:39:00: PurchaseFragment.onCreate, args=Bundle[{duration=0, tickets={"tickets":[{"ticket_id":5443,"qty":4}]}, org_id=1208, provider=52, valid_from=null, price=4.23, currency=€}]
2023/01/14 18:39:00: Buy tickets: {"tickets":[{"ticket_id":5443,"qty":4}]}
2023/01/14 18:39:00: proceedWithPreorderId: 9857788, transact: null
2023/01/14 18:39:00: Wait for preorder...
2023/01/14 18:39:08: Purchase successful
I added log to supportFragmentManager listener to log state of each fragment in app. And it seems like after successful purchase, fragments are successfully detached and destroyed in manager but 1 day later as user wakes up phone and app, MainActivity is recreated (onCreate is called "App create") and 1 second later constructor of fragment is randomly out of nowhere called by the system. Even Fragment Manager listener is not detecting that fragment.
Not sure what can be wrong here.
Worth to mention that I've added to MainActivity function to clear backstack every time MainActivity is recreated to remove any fragments hanging there (if there are some):
override fun onCreate(si: Bundle?) {
clearFragmentBackstack()
isRecreated = si != null
if (si == null){
val splashScreen = installSplashScreen()
splashScreen.setKeepOnScreenCondition{ !areDataLoaded }
} else {
App.log("MainActivity: SavedStateTest: onCreate: recreated")
val splashScreen = installSplashScreen()
splashScreen.setKeepOnScreenCondition{ false }
}
super.onCreate(si)
...
}
private fun clearFragmentBackstack(){
if (supportFragmentManager.backStackEntryCount > 0) {
val first =supportFragmentManager.getBackStackEntryAt(0)
supportFragmentManager.popBackStack(first.id, FragmentManager.POP_BACK_STACK_INCLUSIVE)
}
}
when the activity is recreated again, it's normal that the last fragment was in the activity to be recreated but in your situation, it's a payment fragment that shouldn't be started by itself.
so to prevent such scenario.
You can create another status fragment and after payment is done, remove payment fragment from the stack and navigate to the status fragment.
also as we do in our apps. when the user initiates a payment, we are saving user's click time in shared preferences (datastore recently) and before making the payment we are checking for the user's click time, and if it was more than 5 minutes ago, we are not proceeding with the payment.

How to implement back button handling on Android with Redux

I'm trying to implement back button handling on Android using CoRedux for my Redux store. I did find one way to do it, but I am hoping there is a more elegant solution because mine seems like a hack.
Problem
At the heart of the problem is the fact returning to an Android Fragment is not the same as rendering that Fragment for the first time.
The first time a user visits the Fragment, I render it with the FragmentManager as a transaction, adding a back stack entry for the "main" screen
fragmentManager?.beginTransaction()
?.add(R.id.myFragmentContainer, MyFragment1())
?.addToBackStack("main")?.commit()
When the user returns to that fragment from another fragment, the way to get back to it is to pop the back stack:
fragmentManager?.popBackStack()
This seems to conflict with Redux principles wherein the state should be enough to render the UI but in this case the path TO the state also matters.
Hack Solution
I'm hoping someone can improve on this solution, but I managed to solve this problem by introducing some state that resides outside of Redux, a boolean called skipRendering. You could call this "ephemeral" state perhaps. Initialized to false, skipRendering gets set to true when the user taps the back button:
fun popBackStack() {
fragmentManager?.popBackStack()
mapViewModel.dispatchAction(MapViewModel.ReduxAction.BackButton)
skipRendering = true
}
Dispatching the back button to the redux store rewinds the redux state to the prior state as follows:
return when (action) {
// ...
ReduxAction.BackButton -> {
state.pastState
?: throw IllegalStateException("More back taps processed than past state frames")
}
}
For what it's worth, pastState gets populated by the reducer whenever the user requests to visit a fragment from which the user can subsequently tap back.
return when (action) {
// ...
ReduxAction.ShowMyFragment1 -> {
state.copy(pastState = state, screenDisplayed = C)
}
}
Finally, the render skips processing if skipRendering since the necessary work of calling fragmentManager?.popBackStack() was handled before dispatching the BackButton action.
I suspect there is a better solution which uses Redux constructs for example a side effect. But I'm stuck figuring out a way to solve this more elegantly.
To solve this problem, I decided to accept that the conflict cannot be resolved directly. The conflict is between Redux and Android's native back button handling because Redux needs to be master of the state but Android holds the back stack information. Recognizing that these two don't mix well, I decided to ditch Android's back stack handling and implement it entirely on my Redux store.
data class LLReduxState(
// ...
val screenBackStack: List<ScreenDisplayed> = listOf(ScreenDisplayed.MainScreen)
)
sealed class ScreenDisplayed {
object MainScreen : ScreenDisplayed()
object AScreen : ScreenDisplayed()
object BScreen : ScreenDisplayed()
object CScreen : ScreenDisplayed()
}
Here's what the reducer looks like:
private fun reducer(state: LLReduxState, action: ReduxAction): LLReduxState {
return when (action) {
// ...
ReduxAction.BackButton -> {
state.copy(screenBackStack = mutableListOf<ScreenDisplayed>().also {
it.addAll(state.screenBackStack)
it.removeAt(0)
})
}
ReduxAction.AButton -> {
state.copy(screenBackStack = mutableListOf<ScreenDisplayed>().also {
it.add(ScreenDisplayed.AScreen)
it.addAll(state.screenBackStack)
})
}
ReduxAction.BButton -> {
state.copy(screenBackStack = mutableListOf<ScreenDisplayed>().also {
it.add(ScreenDisplayed.BScreen)
it.addAll(state.screenBackStack)
})
}
ReduxAction.CButton -> {
state.copy(screenBackStack = mutableListOf<ScreenDisplayed>().also {
it.add(ScreenDisplayed.CScreen)
it.addAll(state.screenBackStack)
})
}
}
}
In my fragment, the Activity can call this API I exposed when the Activity's onBackPressed() gets called by the operating system:
fun popBackStack() {
mapViewModel.dispatchAction(MapViewModel.ReduxAction.BackButton)
}
Lastly, the Fragment renders as follows:
private fun render(state: LLReduxState) {
// ...
if (ScreenDisplayed.AScreen == state.screenBackStack[0]) {
fragmentManager?.beginTransaction()
?.replace(R.id.llNavigationFragmentContainer, AFragment())
?.commit()
}
if (ScreenDisplayed.BScreen == state.screenBackStack[0]) {
fragmentManager?.beginTransaction()
?.replace(R.id.llNavigationFragmentContainer, BFragment())
?.commit()
}
if (ScreenDisplayed.CScreen == state.screenBackStack[0]) {
fragmentManager?.beginTransaction()
?.replace(R.id.llNavigationFragmentContainer, CFragment())
?.commit()
}
}
This solution works perfectly for back button handling because it applies Redux in the way it was meant to be applied. As evidence, I was able to write automation tests which mock the back stack as follows by setting the initial state to one with the deepest back stack:
LLReduxState(
screenBackStack = listOf(
ScreenDisplayed.CScreen,
ScreenDisplayed.BScreen,
ScreenDisplayed.AScreen,
ScreenDisplayed.MainScreen
)
)
I've left some details out which are specific to CoRedux.

FragmentTransaction crashes after checking if (Fragment != null)

I was following this tutorial on youtube and ran into a problem with the end result.
The goal of the tutorial was to introduce Fragment Transactions by having these buttons that add/remove/replace fragments in a layout below the buttons. It all went smoothly until the end.
If I hit the "Add A" button, it adds the Frag_A in the layout below, if you hit remove, it disappears. However, if you hit "Add A" 2+ times, then you need to hit remove 2+ times in order to get rid of all of them. Likewise, if you hit "Add A" and then "Add B", you need to hit "Remove A" first before you can see fragment B.
At the end of the video, they included an if statement in the "Remove" methods to check whether or not the Fragment was already there.
public void RemoveA (View v) {
FragmentA FA = (FragmentA) manager.findFragmentByTag("A");
FragmentTransaction transaction = manager.beginTransaction();
if (FA != null) {
//remove the transaction and commit
}
else {
//Toast a message to say the FragmentA doesn't exist yet
}
}
So I thought I could add a similar thing into the "AddA" method to check if a FragmentA exists, then toast a message to say it's already there, and if it doesn't exist, to add it to avoid having so many of them get created when you only need 1.
public void AddA (View v) {
FragmentA FA = (FragmentA) manager.findFragmentByTag("A");
FragmentTransaction transaction = manager.beginTransaction();
if (FA != null) {
Toast.maketext(this, "Fragment already exists", Toast.LENGTH_SHORT).show());
}
else {
transaction.add(FA);
transaction.commit();
}
}
However, this made it so when you hit "AddA", the program just gives an unexpected error and quits. Looking at the Logcat (I just started learning last week so I don't know what everything means), I noticed a line mentioning a nullpointerexception at:
transaction.add(FA);
Meanwhile, IntelliJ is saying that:
if (FA != null)
Is always true, and FA == is always false. I also tried appending && isvisible but that didn't make a difference either. Is there any ideas as to why this would happen?
This is my first post and I couldn't find an answer for this on google/searching.
Looking at your code:
if (FA != null) { // here FA is not null
Toast.maketext(this, "Fragment already exists", Toast.LENGTH_SHORT).show()); }
else { // here, FA is null!
transaction.add(FA); // same as transaction.add(null);
transaction.commit();
}
In the else block, you're missing some code where you would create a new fragment in FA, before adding it to the transaction.
if (FA != null) {
Toast.maketext(this, "Fragment already exists", Toast.LENGTH_SHORT).show()); }
else {
// Create a new fragment here and set it to FA
transaction.add(FA);
transaction.commit();
}

What does SessionState.OPENED_TOKEN_UPDATED mean in Facebook SDK 3.0.1b for Android

I am working on an app and it has code in which I have extended my class with FacebookActivity and I wanted to know what does OPENED_TOKEN_UPDATED mean and when will this portion execute
if (state.equals(SessionState.OPENED_TOKEN_UPDATED)) {
//WHEN THIS PORTION WILL EXECUTE
}
Facebook documents are not so good. See below code also
#Override
protected void onSessionStateChange(SessionState state, Exception exception) {
if (isResumed) { //if its a visible activity
FragmentManager manager = getSupportFragmentManager();
int backStackSize = manager.getBackStackEntryCount(); //get number of entries currently in the back-stack
for (int i = 0; i < backStackSize; i++) {
manager.popBackStack(); //clear fragment back-stack before new fragment is added
}
if (state.isOpened()) {
if (state.equals(SessionState.OPENED_TOKEN_UPDATED)) {
//WHEN THIS PORTION WILL EXECUTE
}else{
//replace fragment on main with promo fragment
FragmentTransaction transaction = manager.beginTransaction();
transaction.replace(R.id.body_frame, fragments[NextFgragment]).commit();
}
} else if (state.isClosed()) {
//replace fragment on main with login fragment
}
}
}
When you initially open a session, it will be in the SessionState.OPENED state. If you make a reauthorize request, or if the token gets refreshed, then it will be in an OPENED_TOKEN_UPDATED state.
Generally you can treat both OPENED and OPENED_TOKEN_UPDATED as the same, but sometimes (like if you request more permissions), you'll want to know when the token has been updated.
You can just disable this code
if (state.equals(SessionState.OPENED_TOKEN_UPDATED)) {
//WHEN THIS PORTION WILL EXECUTE
}
The documentation the SessionState.OPENED_TOKEN_UPDATED say:
"Indicates that the Session is opened and that the token has changed. In this state, the Session may be used with Request."
that portion of code is useful, if need do something when the token change

Android shared preferences conditional activity switching

I have an Android app which I use to register users on my web site. My first task is to register a user if my shared preferences file shows there is no registered user information.
If my app has a registered user, I provide the following code to simply and automatically switch to a "homepage" activity:
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.signin);
if( USERPREFERENCES.getString(USERPREFERENCES_USERMAIL, "") == null && USERPREFERENCES.getString(USERPREFERENCES_USERID, "") == null && USERPREFERENCES.getString(USERPREFERENCES_USERNAME, "") == null){
//setContentView(R.layout.signin);
Toast.makeText(SignIn.this, "testing...", Toast.LENGTH_LONG).show();
}else{
Intent intent = new Intent(SignIn.this, Confirmed.class);
startActivity(intent);
}
... other code
So, from my default activity, signin.java, the app will either switch to the Confirmed activity or stay on and display the signin activity.
My problem is, when the system works and I get switched to the the Confirmed activity, I provide a logout onclick listener which is below:
signout.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
// TODO Auto-generated method stub
//USERPREFERENCES.cl
Toast.makeText(Confirmed.this, "signout responding!", Toast.LENGTH_LONG).show();
USERPREFERENCES.edit().clear().commit();
}
});
It responds and clears all my shared preferences variables. But, when I use my menu to manually switch to the sign-in activity, I still get switched back to the Confirmed activity.
This happens even though I can confirm the variables are empty.
This hardly ever will be true:
USERPREFERENCES.getString(USERPREFERENCES_USERMAIL, "") == null
What if you use this instead?
if( USERPREFERENCES.getString(USERPREFERENCES_USERMAIL, null) == null && USERPREFERENCES.getString(USERPREFERENCES_USERID, null) == null && USERPREFERENCES.getString(USERPREFERENCES_USERNAME, null) == null){
//setContentView(R.layout.signin); TRY TO AVOID DOING THIS THING!!!!!
Toast.makeText(SignIn.this, "testing...", Toast.LENGTH_LONG).show();
}else...
Also, as a recommendation... instead of being switching between activities... what if you create just a Signing.java activity and put a ViewFlipper in its layout. That way your app will be not only faster but also easier to maintain.
This is Because When you will switch back to LoginActivity, this will be resumed instead of being created , Means your Login code which you written inOnCreate will not be called because Dthis time Overrider OnResume has been called , not onCreate .
So either write this code again in onResume or call finish() before moving to second activity , so that next time it will call onCreate()
If you navigate back to the first activity, the onCreate is not called again (unless the activity was destroyed for lack of resources). Move the authentication code in onResume.

Categories

Resources