I am building an application which has the following structure.
MainActivity -> Fragment1
-> Fragment2 -> Fragment3
Here Fragment1 is hosted on MainActivity in FrameLayout. when user click any option on Fragment1, I am switching to Fragment2 which hosts Fragment3 on ViewPager.
Problem is, if user moves to android home without closing the app, and in a duration of time the app is killed by Android, and user tries to resume the app from recent app list, Fragment3 is displaying the blank screen.
here is the method which I am using for Fragment transaction
public void requestChangeFragment(Fragment newFragment,String tag,Fragment oldFragment, int anim_in, int anim_out){
if(isStopped()){
return;
}
mFragmentManager = getSupportFragmentManager();
android.support.v4.app.FragmentTransaction fragmentTransaction = mFragmentManager.beginTransaction();
fragmentTransaction.setCustomAnimations(anim_in,anim_out,anim_in,anim_out);
fragmentTransaction.add(R.id.main_frame_layout, newFragment, tag);
Log.i("FragMngr","Animations set in "+anim_in+" out "+anim_out);
fragmentTransaction.addToBackStack(tag);
int count = mFragmentManager.getBackStackEntryCount();
String log="";
fragmentTransaction.commit();
for(int i=0;i<count;i++){
android.support.v4.app.FragmentManager.BackStackEntry backStackEntry = mFragmentManager.getBackStackEntryAt(i);
log += "count "+i+" name "+backStackEntry.getName()+"\n";
}
Log.d("FragMngr","BackStack trace after adding is\n "+log);
if(oldFragment!=null) {
if(oldFragment.isResumed()) {
oldFragment.onPause();
}
}
topFragment = newFragment;
}
requestChangeFragment() is called from running fragments with the help of interface implemented in MainActivity.
After doing some research I identified the problem to be the saved instances which stores the current state of the fragment whenever android garbage collector kills the app when app is not in use. So when I tried to resume the dead app, it moves the fragment that was selected before pausing the app. But the fragment do not have any data which it must have received from the it Main class.
Solution : Clear the saved instances in your MainActivity in onCreate method. Or if you are storing any values in your saved instance state, then perform the operation related to the saved Instance State, clear the value and then call super.onCreate(savedInstanceState)
Related
So I've got a main activity that hosts all of the fragments in my app. Let me just say beforehand that every time I open a new fragment, I do it like this:
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.replace(vg.getId(), new MyFragment()); //obviously MyFragment varies from usage to usage but nothing else
ft.addToBackStack("My Fragment's Name");
ft.commit();
MyFragment in this case extends androidx.fragment.app.Fragment, which only has a method getFragmentManager(). It does NOT have getSupportFragmentManager().
If I want to go back to a previous fragment, from the currently shown fragment I would do getFragmentManager().popBackStackImmediate(). However, if I wanted to pop the backstack from the activity, I have to use getSupportFragmentManager().popBackStackImmediate() or else nothing happens. This led me to assume that calling getFragmentManager() from a fragment returned the same reference as calling getSupportFragmentManager() from the activity. However, if I try to run getSupportFragmentManager().getBackStackEntryAt(/* an int */) from the main activity I get a NullPointerException. If I run getSupportFragmentManager().getBackStackEntryCount() from the main activity it always returns zero.
So why is it then that getSupportFragmentManager() in the main activity simultaneously works and doesn't work? Why can I use it to pop the backstack, yet I can't access the backstack itself from the main activity? I'm totally clueless. Help would be appreciated.
EDIT: This is my onBackPressed():
#Override
public void onBackPressed() {
getSupportFragmentManager().popBackStackImmediate();
getWindow().setSoftInputMode(defaultSoftInputMode);
try {
String fragmentName = getSupportFragmentManager().getBackStackEntryAt(getSupportFragmentManager().getBackStackEntryCount() - 1).getName();
Toast.makeText(getApplicationContext(), fragmentName, Toast.LENGTH_SHORT).show(); // Debug to see if the correct name is being shown
} catch (NullPointerException npe) {
// Print to console
// It always catches an error here and I don't know why
}
}
The specific method which throws the exception is getSupportFragmentManager().getBackStackEntryAt(int index). Further inspection shows that this method queries an ArrayList<BackStackRecord> for a value, but the exception occurs because this ArrayList is null.
My problem: When i close my app via long-pressing home button (Samsung S4) in the app manager, everything works fine on the next startup.
But, when i close the app in onBackPressed when its time to close it, i get a crash on the next start of the app, because "getActivty" returns null.
I checked multiple times which lifecycle methods get called during these two scenarios, and wasn't able to find any difference.
Also, i use the piece of code producing the crash in another fragment, but there it works fine.
The question is: Why returns getActivity null, when the Activity is definitely alive? Better said, why is my fragment not attached to the Activity anymore?
Relevant code (i hope)
init of main fragment:
//Declared globally in Activity
FirstFragment fr;
//In activities onCreate
fr = new FirstFragment();
getFragmentManager()
.beginTransaction()
.replace(R.id.main_fragment, fr, "first")
.setTransitionStyle(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
.addToBackStack("")
.commit();
Switching to the second fragment is done with this method (Where the parameter "Object fragment" is "new SecondFragment")
public void changeFragment(Object fragment, String name, boolean stayMain) {
if (!stayMain) {
findViewById(R.id.title_bar_left_button).setBackgroundResource(R.drawable.ic_back);
findViewById(R.id.title_bar_left_button).setOnClickListener(backToMainListener);
}
((TextView) findViewById(R.id.navTitle)).setText(name);
findViewById(R.id.title_bar_right_button).setVisibility(View.GONE);
getFragmentManager()
.beginTransaction()
.replace(R.id.main_fragment, (Fragment)fragment, name)
.setTransitionStyle(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
.addToBackStack("")
.commit();
}
Code causing the crash: (since getActivity returns null in described scenario
Context c = this.getActivity();
AlertDialog.Builder builder = new AlertDialog.Builder(c);
Any help is appreciated
"If an activity is paused or stopped, the system can drop the activity from memory by either asking it to finish, or simply killing its process.". When the user goes back to the activity, it restores it's state with a bundle.
My question is:
is it important to do this in oncreate:
if(savedinstance != null)
{
fragement = fm.findFragmentByTag("tag");
}
else
{
fragment = new Fragment();
FragmentManager fm = getFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
ft.add(R.id.webViewFrame,fragment,"tag");
ft.commit()
}
instead of just this:
fragment = new Fragment();
FragmentManager fm = getFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
ft.add(R.id.webViewFrame,fragment,"tag");
ft.commit()
If you are correctly saving the state of your activity and fragment in onSaveInstanceState(), and you want your activity to be recreated in the state it had before it was killed, you should use the first block of code you posted.
The FragmentManager saves its state, and when recreated, restores the fragments it was holding when the activity was killed. It steps them through the build-up lifecycle events using the fragment's saved state: create, createView, start, resume.
I'm pretty sure if you try running the second block of code, you will find after restart that you have two instances of your fragment in the FragmentManager--the one added when the activity was first create, and the one added after restart.
For this all to work correctly, you must carefully save the state of both your activity and fragment(s) in the onSaveInstanceState() method of each, and then in onCreate() test savedInstanceState and when not null, use the bundle to restore the state of your activity/fragment.
This is the guide for saving/restoring activity state. More info here.
I am trying to get a better understanding of FragmentManager and FragmentTransactions to properly develop my application. It is specifically in regards to their lifecycle, and the long-term effect of committing a FragmentTransaction(add). The reason I have a confusion over it is when I ran a sample Activity, listed at the end of the post.
I purposely created a static FragmentManager variable called fragMan and initially set it to null. It is then checked against in onCreate() if it is null and when null value is seen, the fragMan variable is set to the getFragmentManager() return value. During a configuration change, the Log.d showed that fragma was not null, but the Fragment "CameraFragment" previously added was not found in fragman and the fragman.isDestroyed() returned true. This to me meant that the Activity.getFragmentManager() returns a new instance of a FragmentManager, and that the old FragmentManager instance in fragMan had its data wiped(?)
Here is where the confusion comes in.
1) How is "CameraFragment" still associated in the Activity on a configuration change and is found in
the new instance of FragmentManager?
2) When I hit the back button on my phone to exit the Activity, I then relaunched the sample
Activity using the Apps menu. The CameraFragment was not visible anymore, and the
onCreate() check revealed that fragMan was still not null. I thought that hitting the back button
called the default finish() command, clearing the Activity from memory and that restarting it
would produce the same result as the initial launch of the sample Activity?
Thank you for any and all help you can provide!
public class MainActivity extends Activity
{
static FragmentManager fragMan = null;
FragmentTransaction fragTransaction;
#Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (fragMan != null)
{
Log.d("log", Boolean.toString(fragMan.isDestroyed()));
if(fragMan.findFragmentByTag("Camera Preview") == null)
{
Log.d("log", "Camera Preview not found.");
}
}
else
{
fragMan = getFragmentManager();
fragTransaction = fragMan.beginTransaction();
Fragment cameraFragment = new CameraFragment();
ViewGroup root_view = (ViewGroup) findViewById(android.R.id.content);
fragTransaction.add(root_view.getId(), cameraFragment, "Camera Preview");
fragTransaction.commit();
}
Static variables in Java are kept across Activity creation/destruction - they are associated with the class itself but not a particular instance of the class.
See the official documentation here:
http://docs.oracle.com/javase/tutorial/java/javaOO/classvars.html
Your application doesn't end when the user returns to the home screen, it just gets put in a background state. If you force stopped the application and restarted it, then the static FragmentManager will be null.
With regards to CameraFragment, unless you've set setRetainInstance(true), it will get destroyed on an orientation change.
==== EDIT
Here's a more detailed flow of what's happening...
You open the application up for the first time
Activity, say instance A1, gets created and its corresponding FragmentManager instance, FM1, also gets created
You store FM1 as a static variable
You go back to home
Activity A1 and FM1 gets destroyed because of the normal Activity lifecycle, although FM1's reference is still held onto by the static variable. At this point, FM1 loses all the fragments it contains and isDestroyed() will return true.
Starting the app again
New Activity instance A2 gets created along with its new FragmentManager instance FM2
I have one mainactivity in which the location is determined. In this mainactivity I first add a splashscreen fragment. At the moment I find the first location/GPS is working correctly and found a location, I want to replace this splashscreen with my main menu.
Relevant code in the mainactivity:
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.frame);
//Check that the activity is using the layout version
// the framelayout
if(findViewById(R.id.fragment_container)!=null){
//To avoid overlapping fragments check if we are restored from a state, then we don't have to do anything
if(savedInstanceState != null){
return;
}
Splash splashscr = new Splash();
Main main = new Main();
getSupportFragmentManager().beginTransaction()
.add(R.id.fragment_container, splashscr).commit();
// I **think something should be added here
//to check if a location has been found yet.**
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
transaction.replace(R.id.fragment_container, main);
transaction.commit();
}
}
I tried adding a while(foundlocation!=true) in that location. The foundlocation would then be set to true in either onLocationChanged() or in the end of getLocation(). However, it didn't solve my problem. Can someone help me out? If I didn't make my problem clear enough please say so.
EDIT: After considering the comment of Gabe I tried this (locationfound is a public boolean initiated as false in the activity class). But if I now use the DDMS to send a location the splashscreen won't go away. If I'm correct, the first time the location is changed, the boolean is still false and thus it should switch fragment.
I tried it without the if statement and then it was working. What do I forget about here?
public void onLocationChanged(final Location location) {
if(locationfound=false)
{
//Some irrelevant code about saving the new location
Main main = new Main();
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
transaction.replace(R.id.fragment_container, main);
transaction.commit();
locationfound=true;
}
//changing value of sometextboxes in the Main fragment
}
Don't try to wait in onCreate- that holds up the UI thread leading to unresponsive UIs. Instead, you should switch the fragments in the onLocationChanged function the first time its called (by using a boolean flag variable to tell if its the first time).