As an alternative to an Intent, i'm saving data in a retained headless Fragment during Activity re-creation (my saved object can be pretty large and it wouldn't fit the size limit of an Intent, and i think this is a faster approach than serializing-deserializing into JSON for example).
I've got the idea from this Google documentation, although my implementation is a bit different.
The Fragment:
public class DataFragment extends Fragment {
private Data data;
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
public void setData(Data data) {
this.data = data;
}
public Data getData() {
return data;
}
}
I save my data to this Fragment in the onSaveInstanceState() method of my Activity:
#Override
protected void onSaveInstanceState(Bundle outState) {
FragmentManager fm = getSupportFragmentManager();
dataFragment = (DataFragment) fm.findFragmentByTag(TAG_DATA);
if (dataFragment == null) {
dataFragment = new DataFragment();
fm.beginTransaction().add(dataFragment, TAG_DATA).commitNow();
}
dataFragment.setData(myData);
super.onSaveInstanceState(outState);
}
And the relevant part of onCreate():
Data data;
FragmentManager fm = getSupportFragmentManager();
dataFragment = (DataFragment) fm.findFragmentByTag(TAG_DATA);
if (dataFragment == null) {
// the Fragment is not attached, fetching data from DB
DatabaseManager dbm = DatabaseManager.getInstance(this);
data = dbm.getData();
} else {
// the Fragment is attached, fetching the data from it
data = dataFragment.getData();
fm.beginTransaction().remove(dataFragment).commitNow();
}
This works flawlessly on orientation changes.
The problem is, sometimes, when my app is in the background and i'm returning to it, dataFragment.getData() returns null.
In other words, in the following line in onCreate() sometimes data is null:
data = dataFragment.getData();
How is this possible?
It does not throw a NullPointerException, so dataFragment is not null for sure.
Why did its initialized instance variable became null?
What you experience is PROCESS DEATH.
Technically it's also called "low memory condition".
The retained fragment is killed along with the application, but the FragmentActivity recreates your retained fragment in super.onCreate(), so you'll find it by its tag but the data in it won't be initialized.
Put the app in background then press the red X in the bottom left in Android Studio to kill the process. That recreates this phenomenon.
NOTE: After AS 4.0, if you launch your app from AS, then "Terminate" will trigger Force Stop (which does not produce this phenomenon). But if you launch your app from LAUNCHER on the phone after that, then you'll get this phenomenon.
if activity is recreated after it was previously destroyed, you are able to saved your state from the Bundle that the system passes your activity. Both the onCreate() and onRestoreInstanceState() callback methods receive the same Bundle that contains the instance state information.
Obviously yours onCreate() method is called whether the system is creating a new instance of your activity or recreating a previous one, you need to check whether the state Bundle is null before you attempt to read it. If it is null, then the system is creating a new instance of the activity, instead of restoring a previous one that was destroyed.
in oncreate():
Data data;
FragmentManager fm = getSupportFragmentManager();
dataFragment = (DataFragment) fm.findFragmentByTag(TAG_DATA);
**if (dataFragment.getData()== null) {**
// the Fragment is not attached, fetching data from DB
DatabaseManager dbm = DatabaseManager.getInstance(this);
data = dbm.getData();
} else {
// the Fragment is attached, fetching the data from it
data = dataFragment.getData();
fm.beginTransaction().remove(dataFragment).commitNow();
}
Related
I know this question is very common and I have read so many different answers but none fits in my problem. In my application, I have an activity and in rhye activity I load a fragment. I also send some data(in the form of Bundle) to the fragment. So my Problem is when the screen is rotated, I save the fragment in onSaveInstanceState Activity method and check in onCreate Method weather savedInstance is null or not and on that basis I load the fragment.
Activity code :
#Override
public void onSaveInstanceState(Bundle outState, PersistableBundle outPersistentState) {
super.onSaveInstanceState(outState, outPersistentState);
outState.putParcelable(Const.TAG_REQ_CUSTOM,DetailsItems);
outState.putString(Const.TAG_FLOW, Const.TAG_MAIN_FLOW);
getSupportFragmentManager().putFragment(outState,"current_fragment",fragment);
}
onCreate Method :
if (findViewById(R.id.fragment_frame) != null) {
if (savedInstanceState != null) {
// this invoke when screen rotate but the app crash
DetailsItems = savedInstanceState.getParcelable(Const.TAG_REQ_CUSTOM);
String flow = savedInstanceState.getString(Const.TAG_FLOW);
ft = getSupportFragmentManager().getFragment(savedInstanceState,"current_fragment");
mFragmentManager=getSupportFragmentManager();
mFragmentTransaction = mFragmentManager.beginTransaction();
bundle= new Bundle();
bundle.putString(Const.TAG_FLOW, flow);
bundle.putParcelable(Const.TAG_REQ_BOOKING_DETAILS, bookingDetailsItems);
ft.setArguments(bundle);
mFragmentTransaction.replace(R.id.fragment_frame, ft).commit();
}else{
// load fragment on first time
}
}
So my Question is: Where do I have to save the custom Object(in parent Activity or in fragment) ?
When my saved Instance is not null than app crashesh and logs is :
java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.Class java.lang.Object.getClass()' on a null object reference
You should use ViewModel. ViewModel is specifically made for this purpose.
From the docs:
ViewModel is a class that is responsible for preparing and managing the data for an Activity or a Fragment. It also handles the communication of the Activity / Fragment with the rest of the application (e.g. calling the business logic classes).
use this code in Activity :
if (findViewById(R.id.fragment_frame) != null) {
if (savedInstanceState != null) {
fragment =getSupportFragmentManager().getFragment(savedInstanceState,"current_fragment");
}else{
// load fragment on first time
}
}
and in fragment :
//save custom object
#Override
public void onSaveInstanceState(Bundle outState){
super.onSaveInstanceState(outState);
outState.putParcelable("key",customObject);
}
//now retrieve here
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (savedInstanceState != null)
customObject= savedInstanceState.getParcelable("key");
}
Take a look at onRetainNonConfigurationInstance() and getLastNonConfigurationInstance()
From docs:
Called by the system, as part of destroying an activity due to a configuration change, when it is known that a new instance will immediately be created for the new configuration. You can return any object you like here, including the activity instance itself, which can later be retrieved by calling getLastNonConfigurationInstance() in the new activity instance.
I've got an activity with a retained fragment. This fragment handles DB queries and sends the results back to the activity via an interface.
When I rotate the device, the activity is destroyed and re-creates itself as expected. It also re-connects to the retained fragment (which wasn't destroyed) which is continuing to handle the DB queries despite the device rotation.
My problem is when the retained fragment gets the DB queries result back, it tries to send these via the interface to the activity. But, if the device has been rotated, the activity could be in the destroyed state (and not yet re-created) so the fragment can't send the results to the activity.
When the activity is eventually re-created, it's missed "it's chance" to receive the interface calls from the fragment and get the DB results. How to solve this?
Just a bit more detail - the activity has a button which the user presses to start the retained fragment doing the DB queries (they don't just start automatically when the activity is created or the activity attached to the fragment).
Activity.onCreate()
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity);
// Get our Retained Fragment if already exists, otherwise create a new one
FragmentManager fragManager = getSupportFragmentManager();
mRetFrag = (QueryFragment) fragManager.findFragmentByTag(QueryFragment.FRAGMENT_TAG);
if (mRetFrag == null) {
// First time - create a new retained fragment
mRetFrag = QueryFragment.newInstance();
fragManager.beginTransaction().add(mRetFrag, QueryFragment.FRAGMENT_TAG).commit();
}
// Button to start DB querying in fragment
Button queryBtn = (Button) findViewById(R.id.query_button);
queryBtn.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
mRetFrag.runDBQueries();
}
});
}
And in the fragment when it has the DB query results, I try to send this back to the Activity. So, mCallbackListener could be null if the activity isn't yet created.
// Pass the data to the activity
if (mCallbackListener != null) {
mCallbackListener.onQueryFinished(data);
}
Your mCallbackListener points to an activity, which doesn't exist anymore, as long as it has been destroyed after rotation.
You have to get another instance of your activity:
if(null == mCallbackListener) {
mCallbackListener = (MainActivity) getActivity();
}
Another solution is to use event bus like Greenrobot's EventBus or Otto.
Here's nice blog about how to use Otto.
I'm fetching data in my activity that is needed by several fragments. After the data is returned, I create the fragments. I was doing this via an AsyncTask, but it led to occasional crashes if the data returned after a screen rotation or the app is backgrounded.
I read up and thought the solution to this was instead using an AsyncTaskLoader. Supposedly it won't callback if your activity's gone, so those errors should be solved. But this now crashes every time because "Can not perform this action (add fragment) inside of onLoadFinished".
How am I supposed to handle this? I don't want my fragments to each have to fetch the data, so it seems like the activity is the right place to put the code.
Thanks!
Edit 1
Here's the relevant code. I don't think the problem is with the code per-se, but more of my whole approach. The exception is pretty clear I shouldn't be creating fragments when I am. I'm just not sure how to do this otherwise.
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getSupportLoaderManager().initLoader(BREWERY_LOADER, null, this).forceLoad();
}
//================================================================================
// Loader handlers
//================================================================================
#Override
public Loader<Brewery> onCreateLoader(int id, Bundle args) {
int breweryId = getIntent().getIntExtra(EXTRA_BREWERY_ID, -1);
return new BreweryLoader(this, breweryId);
}
#Override
public void onLoadFinished(Loader<Brewery> loader, Brewery data) {
if (data != null) {
onBreweryReceived(data);
} else {
onBreweryError();
}
}
#Override
public void onLoaderReset(Loader<Brewery> loader) {
}
...
protected void onBreweryReceived(Brewery brewery) {
...
createFragments();
}
...
protected void createFragments() {
FragmentManager fm = getSupportFragmentManager();
//beers fragment
mBeersFragment = (BreweryBeersFragment)fm.findFragmentById(R.id.beersFragmentContainer);
if (mBeersFragment == null) {
mBeersFragment = new BreweryBeersFragment();
fm.beginTransaction()
.add(R.id.beersFragmentContainer, mBeersFragment)
.commit();
Bundle beersBundle = new Bundle();
beersBundle.putInt(BreweryBeersFragment.EXTRA_BREWERY_ID, mBrewery.getId());
mBeersFragment.setArguments(beersBundle);
}
}
Edit 2
My new strategy is to use an IntentService with a ResultReceiver. I null out callbacks in onPause so there's no danger of my activity being hit when it shouldn't be. This feels a lot more heavy-handed than necessary, but AsyncTask and AsyncTaskLoader neither seemed to have everything I needed. Creating fragments in those callback methods doesn't seem to bother Android either.
From the MVC (Model -- View -- Controller) viewpoint, both the Activity and its fragments are Controller, while it is Model that should be responsible for loading data. As to the View, it is defined by the layout xml, you can define custom View classes, but usually you don't.
So create a Model class. Model is responsible for what must survive a screen turn. (Likely, it will be a static singleton; note that Android can kill and re-create the process, so the singleton may get set to null.) Note that Activities use Bundles to send data to themselves in the future.
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've got an activity, containing fragment 'list', which upon clicking on one of its items will replace itself to a 'content' fragment. When the user uses the back button, he's brought to the 'list' fragment again.
The problem is that the fragment is in its default state, no matter what I try to persist data.
Facts:
both fragments are created through public static TheFragment newInstance(Bundle args), setArguments(args) and Bundle args = getArguments()
both fragments are on the same level, which is directly inside a FrameLayout from the parent activity (that is, not nested fragments)
I do not want to call setRetainInstance, because my activity is a master/detail flow, which has a 2 pane layout on larger screens. 7" tablets have 1 pane in portrait and 2 panes in landscape. If I retain the 'list' fragment instance, it will (I think) fuck things up with screen rotations
when the users clicks an item in the 'list' fragment, the 'content' fragment is displayed through FragmentTransaction#replace(int, Fragment, String), with the same ID but a different tag
I did override onSaveInstanceState(Bundle), but this is not always called by the framework, as per the doc: "There are many situations where a fragment may be mostly torn down (such as when placed on the back stack with no UI showing), but its state will not be saved until its owning activity actually needs to save its state."
I'm using the support library
From the bullet 5 above, I guess that low-end devices that need to recover memory after a fragment transaction may call Fragment#onSaveInstanceState(Bundle). However, on my testing devices (Galaxy Nexus and Nexus 7), the framework doesn't call that method. So that's not a valid option.
So, how can I retain some fragment data? the bundle passed to Fragment#onCreate, Fragment#onActivityCreated, etc. is always null.
Hence, I can't make a difference from a brand new fragment launch to a back stack restore.
Note: possible related/duplicate question
This doesn't seem right, but here's how I ended up doing:
public class MyActivity extends FragmentActivity {
private Bundle mMainFragmentArgs;
public void saveMainFragmentState(Bundle args) {
mMainFragmentArgs = args;
}
public Bundle getSavedMainFragmentState() {
return mMainFragmentArgs;
}
// ...
}
And in the main fragment:
public class MainFragment extends Fragment {
#Override
public void onActivityCreated(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle args = ((MyActivity) getActivity()).getSavedMainFragmentState();
if (args != null) {
// Restore from backstack
} else if (savedInstanceState != null) {
// Restore from saved instance state
} else {
// Create from fragment arguments
args = getArguments();
}
// ...
}
// ...
#Override
public void onDestroyView() {
super.onDestroyView();
Bundle args = new Bundle();
saveInstance(args);
((MyActivity) getActivity()).saveMainFragmentState(args);
}
#Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
saveInstance(outState);
}
private void saveInstance(Bundle data) {
// put data into bundle
}
}
It works!
if back from backstack, the fragment uses the parameters saved in onDestroyView
if back from another app/process/out of memory, the fragment is restored from the onSaveInstanceState
if created for the first time, the fragment uses the parameters set in setArguments
All events are covered, and the freshest information is always kept.
It's actually more complicated, it's interface-based, the listener is un/registered from onAttach/onDetach. But the principles are the same.