I'm having a problem restoring the state of nested fragments. Here's the hierarchy:
MainActivity
----> AlarmOverviewFragment (doesn't really hold any important data)
--------> AlarmFragment (holds data)
The problem I am running into is when I minimize the app (press home button) and then reopen the app, it crashes every time when trying to get data from an array stored as one of Fragment's instance variables inside of onCreateView().
The frustrating bit is that I'm saving the instance variables in onSaveInstanceState(), but savedInstanceState is always null in onCreateView(), onCreate(), and onActivityCreated().
Clearly I am missing something huge here because I've tried many solutions found on stackoverflow to no avail. Here's some relevant code bits:
Saving instance data:
#Override
public void onSaveInstanceState(Bundle outState) {
outState.putInt("hour", this.hour);
outState.putInt("window", this.window);
outState.putInt("minutes", this.minutes);
outState.putBooleanArray("days", this.days);
super.onSaveInstanceState(outState);
}
Unsuccessfully trying to restore data
#Override
public void onCreate(Bundle savedInstanceState) {
if (savedInstanceState != null) {
/* Never happens */
hour = savedInstanceState.getInt("hour");
window = savedInstanceState.getInt("window");
minutes = savedInstanceState.getInt("minutes");
days = savedInstanceState.getBooleanArray("days");
}
super.onCreate(savedInstanceState);
}
#Override
public void onActivityCreated(Bundle savedInstanceState) {
if (savedInstanceState != null) {
/* Also never happens */
}
super.onActivityCreated(savedInstanceState);
}
And just in case this is helpful, here's the stack trace:
E/AndroidRuntime: FATAL EXCEPTION: main
Process: group12.wakemeup, PID: 22756
java.lang.RuntimeException: Unable to resume activity {group12.wakemeup/group12.wakemeup.MainActivity}: java.lang.NullPointerException: Attempt to read from null array
at android.app.ActivityThread.performResumeActivity(ActivityThread.java:3121)
at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:3152)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1400)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:5525)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:730)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:620)
Caused by: java.lang.NullPointerException: Attempt to read from null array
at group12.wakemeup.AlarmFragment.onCreateView(AlarmFragment.java:113)
at android.support.v4.app.Fragment.performCreateView(Fragment.java:2087)
at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1113)
at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1295)
at android.support.v4.app.BackStackRecord.run(BackStackRecord.java:801)
at android.support.v4.app.FragmentManagerImpl.execPendingActions(FragmentManager.java:1682)
at android.support.v4.app.FragmentController.execPendingActions(FragmentController.java:388)
at android.support.v4.app.FragmentActivity.onStart(FragmentActivity.java:607)
at android.support.v7.app.AppCompatActivity.onStart(AppCompatActivity.java:181)
at android.app.Instrumentation.callActivityOnStart(Instrumentation.java:1237)
at android.app.Activity.performStart(Activity.java:6288)
at android.app.Activity.performRestart(Activity.java:6334)
at android.app.Activity.performResume(Activity.java:6339)
at android.app.ActivityThread.performResumeActivity(ActivityThread.java:3110)
at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:3152)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1400)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:5525)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:730)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:620)
EDIT
Extra information
Each AlarmFragment is added dynamically and given a unique ID via View.generateViewId(), so I've been unsure of how to save each fragment in MainActivity. Also worth mentioning that I do use a FragmentPagerAdapter to handle tabs/swiping (there are other Overview fragments in this app, but I left them out as they aren't causing me problems). I've heard there is a way to use a ViewPager to save state, perhaps there is one with a FragmentPagerAdapter as well?
Since your problem seems different from the start, actually you don't need to re-create your fragments inside onNewIntent every time.
You can do this instead:
#Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
if ("some_action".equals(intent.getAction())) {
//your action, in your case create and assign fragment
//remove the action
intent.setAction(null);
}
}
Try to save your fragments in your activity
private YourFragment yourFragment;
#Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
//Save the fragment instances
getFragmentManager().putFragment(outState, "any string you want as key", yourFragment);
//or use code below if you use android.support.v4.app.Fragment
//getSupportFragmentManager().putFragment(outState, "any string you want as key", yourFragment);
}
#Override
protected void onCreate(Bundle savedInstanceState) {
if (savedInstanceState != null) {
//retrieve your fragment that saved from onSaveInstanceState()
yourFragment = (YourFragment) getFragmentManager().getFragment(savedInstanceState, "any string you want as key");
//or
//yourFragment = (YourFragment) getSupportFragmentManager().getFragment(savedInstanceState, "any string you want as key");
}
else {
//create your fragment if it is first time
yourFragment = new YourFragment();
}
}
Well I guess my problem came from something completely different. MainActivity was creating a new AlarmFragment using onNewIntent() when it returned from a series of activities to define the values for each AlarmFragment. Evidently, minimizing the app and then reopening it calls onNewIntent() every time, so my solution was to make sure that I was actually creating new AlarmFragments with data. Stupid problem, stupid solution
Related
I want to create a variable to contain an Activity.class so that I can dynamically change the Activity to be called with intent. I want the startBtn to start the intent to the last level played Activity which I will store in json format internally in the device. However, I keep showing up error when I try to use the forName() function to call the Activity class.
Main Activity.class
public class MainActivity extends Activity {
Button startBtn, chooseLevelBtn;
Class classVariable;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
startBtn = (Button) findViewById(R.id.startBtn);
chooseLevelBtn = (Button) findViewById(R.id.chooseLevelBtn);
try {
/*
* Set class Variable so that I can dynamically change the Activity
* to be called with intent.
* Testing the variable with Level001Activity, but app keep crashing
* when startBtn is pressed with stack error I post below
*/
classVariable = Class.forName("com.ed.pieces.Level001Activity");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
startBtn.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
Intent i = new Intent(getApplicationContext(),
classVariable);
startActivity(i);
}
});
}
}
Error Stack
java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.String java.lang.Class.getName()' on a null object reference
at android.content.ComponentName.(ComponentName.java:129)
at android.content.Intent.(Intent.java:4449)
at com.example.ed.pieces.MainActivity$1.onClick(MainActivity.java:35)
at android.view.View.performClick(View.java:5198)
at android.view.View$PerformClick.run(View.java:21147)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:5417)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)
P.S I will store the last level played information in json format as a string which is such as "com.ed.pieces.Level001Activity", and when the user start the app, the class classVariable should contain the Activity class of the last played level.
All suggestions are greatly welcomed. Thank you in advance.
Your exception is coming from your onClick() method and its attempt to create an Intent, not from your forName() call. That is because your forName() call is failing for some reason.
Moreover, you do not need forName(). Replace:
try {
/*
* Set class Variable so that I can dynamically change the Activity
* to be called with intent.
* Testing the variable with Level001Activity, but app keep crashing
* when startBtn is pressed with stack error I post below
*/
classVariable = Class.forName("com.ed.pieces.Level001Activity");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
with:
classVariable = Level001Activity.class;
and add the appropriate import to pull in Level001Activity.
I know there are lots of quetions regarden onSaveInstanceState but I wasn't able to find an answer to my problem.
I have an activity that extends AppCompatActivity; this activity uses 3 fragments and it has a variable 'int currentStep' to track which fragment is being displayed. In the onSavedInstanceState method I store the currentStep variable in the bundle, and then in onCreate method I restore it.
public class MainActivity extends AppCompatActivity {
private final String CURRENT_STEP_TAG = "current_step";
private int currentStep;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_pedido);
if(savedInstanceState == null) {
loadFirstFragment();
} else {
currentStep = savedInstanceState.getInt(CURRENT_STEP_TAG);
if(currentStep == 2){
//Do some stuff...
}
}
}
#Override
public void onSaveInstanceState(Bundle outState, PersistableBundle outPersistentState) {
Log.d("deubg", "--------onsaveinstancestate--------");
outState.putInt(CURRENT_STEP_TAG, currentStep);
super.onSaveInstanceState(outState, outPersistentState);
}
...
}
The thing is that onSavedInstanceState won't get called when screen orentation changes, and according to google documentation, it should. The message "--------onsaveinstancestate--------" doesn't show in the console.
However the Bundle savedInstanceState in the method onCreate is non null after screen rotation, but it doesn't have the int 'currentStep'.
I've tried many things: changed AppCompatActivity for ActionBarActivity, moved the call for super.onSavedInstanceState to different locations, tried to store other variables; but no matter what I do the method doesn't execute.
Also I DO NOT have android:configChanges in my manifest.
My application is being compiled against sdk version 22, buildToolsVersion 22.0.1
Unless your app is running on Lollipop (API21) version of Android or newer, your
public void onSaveInstanceState (Bundle outState, PersistableBundle outPersistentState);
will NOT be called as it simply does not exist on earlier versions of the platform than 21. To support pre API 21 devices you must, instead of the above, override the following method:
public void onSaveInstanceState (Bundle outState);
This will work on API 21+ as well, so you do not need to override both methods, of course (unless you know you need to deal with PersistableBundle the new one offers).
See docs.
I'm developing an Android application and I need to open pdf files and return to same activity when back button is pressed.
Problem
I correctly open pdf file (from ActivityOne) using an intent and starting activity, but when I press back button, all data that I had in the ActivityOne (and previous activities) have been lost.
Here is my code of starting activity for showing pdf:
File file = new File(Environment.getExternalStorageDirectory().getAbsolutePath()
+"/"+ myApplication.getUsuarioActual().getFacturaActual().getPdf());
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.fromFile(file));
intent.setType("application/pdf");
startActivity(intent);
What do I have to do to solve that? Same occurs when I open another application and close it: when return to my app, it shows an error saying that all data is null.
EDIT
After reading that question, as #TheCharliemops recommended me, I know it is what I need, but I have another question related to that.
I have a class myApplicationthat extends Application to maintain global application state where I save all data that I read/write in different Activities.
My question is if I have to save all data I have in myApplication in every activity using onSaveInstanceState or there is some easiest manner to do it.
Firts of all, welcome to SO!!
Here, #reto-meier explains how to save the activity state in Android. I think that could fix your problem. I put his code here for future people with similar problem.
He says that you must override onSaveInstanceState(Bundle savedInstanceState) and onRestoreInstanceState(Bundle savedInstanceState) as following code shows:
Reto Meier said:
#Override
public void onSaveInstanceState(Bundle savedInstanceState) {
super.onSaveInstanceState(savedInstanceState);
// Save UI state changes to the savedInstanceState.
// This bundle will be passed to onCreate if the process is
// killed and restarted.
savedInstanceState.putBoolean("MyBoolean", true);
savedInstanceState.putDouble("myDouble", 1.9);
savedInstanceState.putInt("MyInt", 1);
savedInstanceState.putString("MyString", "Welcome back to Android");
// etc.
}
The Bundle is essentially a way of storing a NVP ("Name-Value Pair") map, and it will get passed in to onCreate and also onRestoreInstanceState where you'd extract the values like this:
#Override
public void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
// Restore UI state from the savedInstanceState.
// This bundle has also been passed to onCreate.
boolean myBoolean = savedInstanceState.getBoolean("MyBoolean");
double myDouble = savedInstanceState.getDouble("myDouble");
int myInt = savedInstanceState.getInt("MyInt");
String myString = savedInstanceState.getString("MyString");
}
I hope this helps you.
Regards!
I have an Android application whose Fragments rely on loaders to fetch data. Below is the skeleton code of my Fragment. Everything is the same except I have some custom code in the onLoadFinished method.
public class Events extends Fragment implements LoaderCallbacks<ArrayList<Event>> {
private Integer intWeek;
public static Events newInstance(Integer intWeek) {
Events pageFragment = new Events();
pageFragment.intWeek = intWeek;
pageFragment.setArguments(new Bundle());
return pageFragment;
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.events, null);
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getLoaderManager().initLoader(this.intWeek, savedInstanceState, this);
}
public Loader<ArrayList<Event>> onCreateLoader(int intLoader, Bundle bndBundle) {
return new Scraper(getActivity().getApplicationContext());
}
public void onLoadFinished(Loader<ArrayList<Event>> ldrEvents, final ArrayList<Event> lstEvents) {
//Do something with the returned data
}
public void onLoaderReset(Loader<ArrayList<Event>> ldrEvents) {
return;
}
#Override
public void onDestroy() {
super.onDestroy();
}
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
}
}
This Fragment is used inside a FragmentActivity which uses ViewPager which gets its Fragment´s using aFragmentPagerAdapter`.
This works fine and I'm able to page between the Fragments like the one shown above. Every time a new Fragment is added to the ViewPager, the onCreate method fires and it creates a new Loader.
When I press the "Home" button on my phone, the application pauses and goes into the background. I can always restore the application and it works just fine — after 5/10/20 minutes of being in the background.
...but If I leave the application in the background for a long time, an hour or more, the application crashes upon start and the stacktrace points to the following line in the onCreate method:
getLoaderManager().initLoader(this.intWeek, savedInstanceState, this);
Now I'm very lost as to why this is happening. It seems that the Android framework destroys something in the background after long periods of inactivity in the background and the initLoader method isn't able to create a Loader. The documentation about the initLoader method specifically says:
Ensures a loader is initialized and active. If the loader doesn't
already exist, one is created and (if the activity/fragment is
currently started) starts the loader. Otherwise the last created
loader is re-used.
Would anyone be able to point out what I'm doing wrong here? This seems to be a pretty difficult issue to debug because I can't replicate it at will. It is very random. Thanks
Stacktrace from logcat:
Transmitting stack trace: java.lang.RuntimeException: Unable to start activity ComponentInfo{com.mridang.stadi/com.mridang.stadi.Main}: java.lang.NullPointerException
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2079)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2104)
at android.app.ActivityThread.access$600(ActivityThread.java:132)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1157)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:137)
at android.app.ActivityThread.main(ActivityThread.java:4575)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:511)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:789)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:556)
at dalvik.system.NativeStart.main(Native Method)
Caused by: java.lang.NullPointerException
at com.mridang.stadi.events.Events.onCreate(Events.java:73)
at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:834)
at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1080)
at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1062)
at android.support.v4.app.FragmentManagerImpl.dispatchCreate(FragmentManager.java:1805)
at android.support.v4.app.FragmentActivity.onCreate(FragmentActivity.java:200)
at com.mridang.stadi.Main.onCreate(Main.java:23)
at android.app.Activity.performCreate(Activity.java:4465)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1049)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2033)
... 11 more
I ran into a similar problem with the loaders. What I found is that the Fragment/Loader lifecycle is complicated. I'd love to find a document that explains it in detail.
Anyhow, what I did to fix the problem was move the initLoader() call to the onActivityCreated() method. Give that a try.
Try using something like this:
boolean canFire = ((loader != null) && !loader.isReset());
// restart or initialise loader
if (canFire) {
getLoaderManager().restartLoader(loaderId, args, this);
} else {
getLoaderManager().initLoader(loaderId, args, this);
}
I have 1 activity and 2 fragments. Both fragments use a custom AsyncTaskLoader to get some data from a webservice and as i'm using a Loader it should keep the data across activity and fragment re-creations. Both fragments override the onActivityCreated method and calls getLoaderManager().initLoader(0, null, this) which either creates a new or reuses an existing loader.
When the activity is first created, it adds Fragment #1 in a FrameLayout by default, loads the data, internally calls LoaderCallbacks.onLoadFinished() method and displays the result. I have a button which replaces Fragment #1 with Fragment #2 on click and Fragment #1 is pushed to the fragment-backstack. When the user hits the BACK key, it switches back to Fragment #1.
onActivityCreated gets called again on Fragment #1 and then obviously calls iniLoader() again. This time the data already exists in the loader and i expect it to automatically call the LoaderCallbacks.onLoadFinished method again, because it already has data available, as described here: http://goo.gl/EGFJk
Ensures a loader is initialized and active. If the loader doesn't already exist, one is created and (if the activity/fragment is currently started) starts the loader. Otherwise the last created loader is re-used.
In either case, the given callback is associated with the loader, and will be called as the loader state changes. If at the point of call the caller is in its started state, and the requested loader already exists and has generated its data, then callback onLoadFinished(Loader, D) will be called immediately (inside of this function), so you must be prepared for this to happen.
But the method is never called even if the loader exists and has generated data ready to deliver.
Edit #1
The problem from a users perspective:
User starts activity and sees fragment1 with some data
User clicks something which changes the first fragment to another, with different
data
User hits the BACK key
User is now looking at fragment1 again, but there's no data. (which means i need to get it from the webservice again - and i'd like to avoid that if possible)
Here is my activity:
public class FragmentTestsActivity extends Activity implements OnClickListener {
private Button btn1;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
btn1 = (Button) findViewById(R.id.btn1);
btn1.setOnClickListener(this);
Fragment newFragment = new Fragment1();
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.add(R.id.fragmentContainer, newFragment).commit();
}
#Override
public void onClick(View view) {
int id = view.getId();
if (id == R.id.btn1) {
showNewFragment();
}
}
public void showNewFragment() {
// Instantiate a new fragment.
Fragment2 newFragment = new Fragment2();
// Add the fragment to the activity, pushing this transaction
// on to the back stack.
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.replace(R.id.fragmentContainer, newFragment);
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
ft.addToBackStack(null);
ft.commit();
}
}
My Fragment #1:
public class Fragment1 extends Fragment implements LoaderCallbacks<String> {
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment1, container, false);
}
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
LoaderManager.enableDebugLogging(true);
getLoaderManager().initLoader(0, null, this);
}
private static class TestLoader extends AsyncTaskLoader<String> {
String result;
public TestLoader(Context context) {
super(context);
}
#Override
public String loadInBackground() {
// Some long-running call to a webservice - replaced with a simple string to test with
return "FirstTimeData";
}
#Override
public void deliverResult(String data) {
result = data;
if (isStarted()) {
super.deliverResult(data);
}
}
#Override
protected void onStartLoading() {
if (result != null) {
deliverResult(result);
}
if (takeContentChanged() || result == null) {
forceLoad();
}
}
#Override
protected void onStopLoading() {
cancelLoad();
}
}
#Override
public Loader<String> onCreateLoader(int id, Bundle args) {
return new TestLoader(getActivity());
}
#Override
public void onLoadFinished(Loader<String> loader, String result) {
Log.d("Fragment1", "onLoadFinished: " + result);
}
#Override
public void onLoaderReset(Loader<String> loader) {
// TODO Auto-generated method stub
}
}
Anyone know a solution to this or what i'm doing wrong here? Any help is greatly appreciated.
The correct answer, for me at least, was to move the entire Loader initialisation from onViewCreated or onActivityCreated to onStart.
After that it works fine!
From my point of view the onLoadFinished will only be called the first time cause the load has already finished, it finishes just once for both fragments. What you could do is to store the result in a property in the activity and check for it in the second fragment creation.
Update: After further investigation I found my original answer to be wrong. restartLoader will also destroy the loader if it already exists.
Nevertheless I solved my own problem. I create a standard CursorLoader in onCreateLoader and swap the cursor of my CursorAdapter in onLoadFinished. I needed to call initLoader after initializing the CursorAdapter. Now the data is still there when the fragment is returned from the backstack.
Original answer: I found two possible ways to solve this issue.
Apparently when initLoader(int id, Bundle args, LoaderCallbacks<D> callback) is called for the second time in onActivityCreated(Bundle savedInstanceState) after the Fragment is returned from the backstack, the method onLoadFinished(Loader<String> loader, String result) is never called (as you described).
However, a call to restartLoader(int id, Bundle args, LoaderCallbacks<D> callback) after or instead of initLoader will finally cause onLoadFinished to be called. To improve performance, I use a boolean argument to determine whether the Fragment is a new instance or not. Then I call restartLoader only if the Fragment is returned from the backstack.
As far as I can tell, old data persists in the loader and is not reloaded, but I'm not sure. The second possibility (especially when not using the backstack but instead creating a new Fragment instance in a transaction) is to call destroyLoader(int id) before the Fragment goes away (e.g in onPause).
I already had that issue, I can't really explain why that bug but I know that line :
getLoaderManager().initLoader(0, null, this);
don't work in my code, So you can changed it for that :
LoaderManager lm = getLoaderManager();
lm.initLoader(LOADER_ID, null, this);
The code start the methods :
onCreateLoader
you can try...