I'm having a lot of trouble with Android Fragments, and I don't know why.
I want to keep an object in memory whenever I pause the app (onPause()).
I have a MapView with some layers. And I want to keep those layers, so the map doesn't have to reload from scratch and recreate them whenever I pause and resume the app.
The thing is, I can't save them with onSaveInstanceState(). They are objects, not primitives. They are also from a library, so I can't make them implement Serializable or Parcelable.
I want to find a solution without resorting to:
android:configChanges="orientation|screenSize">
to my Activity, since it is considered bad practice. Also, it doesn't work for when you pause and resume the app.
I also tried
this.setRetainInstance(true); // retain this fragment
on my fragment's onCreate(), but I would also like to avoid it. It doesn't work anyway.
After much trial, error and testing, I found out that my fragment's onSaveInstanceState() doesn't even run. I can't even keep a simple boolean whenever I pause and resume the app.
I read:
Saving Android Activity state using Save Instance State
https://developer.android.com/training/basics/activity-lifecycle/recreating
savedInstanceState is always null
Amog many other posts and, of course, the Android documentation.
Some of my code:
Creating the fragment:
navFragment = FragNavigation.newInstance(); // keeping the reference to the navFragment
// setting up the viewpager
ViewPager viewPager = findViewById(R.id.vpMainViewPager);
MainPagerAdapter mainAdapter = new MainPagerAdapter(getSupportFragmentManager(), this);
mainAdapter.addFragment(navFragment);
viewPager.setAdapter(mainAdapter);
Trying to save a simple boolean in the instance state (remember: this doesn't even run):
#Override
public void onSaveInstanceState(#NonNull Bundle outState) {
super.onSaveInstanceState(outState);
Log.e(TAG, "onSaveInstanceState: is map loaded? " + mapIsLoaded );
outState.putBoolean("mapIsLoaded", mapIsLoaded);
}
And trying to load it:
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setRetainInstance(true); // retain this fragment
Log.e(TAG, "onCreate: null? " + (savedInstanceState == null) );
if (savedInstanceState != null)
mapIsLoaded = savedInstanceState.getBoolean("mapIsLoaded", false);
}
What am I missing? I'm clearly not understanding something. Why is it so hard to keep values when you pause the app?
I would suggest storing the MapView layers using a ViewModel as per Architecture Components. If that is not an option, you can manually store and retrieve objects using the Activities getLastNonConfigurationInstance.
#Override
public void onPause() {
/**
* store your data
*/
super.onPause();
}
#Override
public void onResume() {
/**
* retrieve your data
*/
super.onResume();
}
Related
I have UI displaying details of a place, including some EditTexts and an adapter from this ExpandableRecyclerView library.
The UI is updated in onCreate when my ViewModel observes a change to the place. I save the adapter in onSaveInstanceState and restore it in onRestoreInstanceState.
However once I rotate my device, the old UI state is displayed. For example, I enter some text to the EditText and expand my adapter, but after I rotate my device the old text and collapsed adapter appears. I think this is because adapter is null until onChanged is called, so my adapter method in onRestoreInstanceState is skipped, and EditTexts are re-updated to old text in onChanged.
How do I save the state of my UI?
Full code can be found here.
onCreate:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_detail);
viewModel = new ViewModelProvider(this).get(PlaceViewModel.class);
Intent intent = getIntent();
if (intent.hasExtra(EXTRA_PLACE_ID)) {
String id = intent.getStringExtra(EXTRA_PLACE_ID);
viewModel.getPlaceById(id).observe(this, new Observer<PlaceModel>() {
#Override
public void onChanged(PlaceModel placeModel) {
// placeModel will be null if place is deleted
if (placeModel != null) {
// update UI here
}
}
});
}
}
onSaveInstanceState and onRestoreInstanceState:
/* To save the expand and collapse state of the adapter,
you have to explicitly call through to the adapter's
onSaveInstanceState() and onRestoreInstanceState() in the calling Activity */
#Override
protected void onSaveInstanceState(#NonNull Bundle outState) {
super.onSaveInstanceState(outState);
if (adapter != null) {
adapter.onSaveInstanceState(outState);
}
}
#Override
protected void onRestoreInstanceState(#NonNull Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
if (adapter != null) {
adapter.onRestoreInstanceState(savedInstanceState);
}
}
One solution is just check if savedInstanceState is null, if it is don't set text of EditTexts and restore adapter state.
But I didn't carefully read the ViewModel documentation: "The ViewModel class is designed to store and manage UI-related data in a lifecycle conscious way. The ViewModel class allows data to survive configuration changes such as screen rotations."
I was asking why ViewModel is saving my old UI-related data after rotation...even though that's exactly what it's supposed to do...my mistake.
So im having some trouble restoring the state of my Activity.
At this point I figure that its probably a problem with my understanding rather then anything else.
My goal is to move from my MainActivity to a Main2Activity.
Am I correct in thinking that when a user moves from one page to another, it should be done by changing Activity via Intent?
I am doing this like so:
The onCreate() for my MainActivity has this in it.
Button currentButton = (Button) findViewById(R.id.button2);
currentButton.setOnClickListener(
new View.OnClickListener() {
#Override
public void onClick(View view) {
setContentView(R.layout.activity_main2);
Intent nextIntent = new Intent(getApplicationContext(), Main2Activity.class);
startActivity(nextIntent);
}
}
);
Which as I understand should call onCreate(), onStart() and onResume() for Main2Activity, then onSaveInstanceState() for MainActivity, then onStop() for MainActivity.
Ive overloaded all those functions with logging and seen that indeed they are being called and in that order.
Here is my onSaveInstanceState() for MainActivity:
#Override
public void onSaveInstanceState(Bundle savedInstanceState) {
System.out.println("onSaveInstanceState called, saving state");
savedInstanceState.putInt("mySuperUniqueKey", testInt);
super.onSaveInstanceState(savedInstanceState);
}
Once in Main2Activity, I return back to MainActivity in the same way. I.e. findViewById() the button, overload its onClickListener(), create a new Intent and start it.
Then MainActivity class's onCreate() has this :
if (savedInstanceState != null) {
System.out.println("savedInstanceState is not null");
testInt = savedInstanceState.getInt("mySuperUniqueKey");
} else {
System.out.println("savedInstanceState is null");
}
When returning back the MainActivity from Main2Activity, I can see from the logging that onCreate(), then onStart(), then onResume() for MainActivity is called, then onStop() for Main2Activity. Unfortunatly the logging shows that savedInstanceState always comes back as null.
To add to this, when in the emulator, switching the orientation back and forth causes this to work perfectly; savedInstanceState is not null and features the saved testInt.
Thus I figure its a problem with my understanding and that there must be something im missing.
My gradle has minSdkVersion set to 16, and targetSdkVersion set to 28. Am I maybe targeting too low a minSdkVersion?
I have read through the "Understand the Activity Lifecycle" on the official android developer documentation but still cant get it.
https://developer.android.com/guide/components/activities/activity-lifecycle
I did find similar problems but none of them match my situation exaclty, also the solutions they have suggested I am already doing anyway.
Any insight would be greatly appreciated, thanks in advance.
The saved instance state bundle is intended to save the state of the current activity across things like orientation changes. It is not designed to persist across activities. You should use Intent#putExtra:
nextIntent.putExtra("mySuperUniqueKey", testInt);
Then, in your next activity, access this passed value using:
int testInt = getIntent().getIntExtra("mySuperUniqueKey");
I'm having some issues with the onSaveInstanceState, but it may be because I misunderstand how it works.
I have a Arraylist i want to save when I switch activities then restore when i come back.
so i have this method
#Override
protected void onSaveInstanceState(Bundle bundle) {
super.onSaveInstanceState(bundle);
bundle.putStringArrayList("History", History);
and my onCreate
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if( savedInstanceState != null ) {
History = savedInstanceState.getStringArrayList("History");
}
}
For some reason this loop is not being initliased
if( savedInstanceState != null ) {
History = savedInstanceState.getStringArrayList("History");
}
I'm pretty sure its soemthing fairly simple but I'm still quite new to Java.
If you are simply switching activities, it will stay 'alive' in the background (unless the OS decides to kill it off to free memory). Read this article for more information on Activity state
In this case, onCreate() will not be called when the Activity resumes, as it has already been created and is simply been onResume()-ed.
You should look at another method of storing your History values such as in SharedPreferences, a database or in a persistent object on the application class instance (whatever suits your preference) in your onPause() method and restore those values in onResume().
onSavedInstanceState() is used for when an activity is killed like in an orientation change or OS kill.
Several times I've had problems writing code on onCreate(). Mostly because the UI has not been sized and laid out on the screen yet (even if I place my code at the end of the function). I've looked over the activity life-cycle to see if there's anything that runs after onCreate(). There is onStart(), but the problem is that onRestart() recalls onStart(), I don't want that. So is there a way to write code between onCreate() and onStart()? OR where should I write code that runs after the UI is placed and only runs once during its process?
Not sure what exactly you need but you can "cheat" and simply store whether you have run code or not:
private boolean mInit = false;
void onStart() {
if (!mInit) {
mInit = true;
// do one time init
}
// remaining regular onStart code
}
The other way of running code when UI is placed is to use the global layout listener:
public class FooActivity extends Activity implements ViewTreeObserver.OnGlobalLayoutListener {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.list_content);
View content = findViewById(android.R.id.content);
content.getViewTreeObserver().addOnGlobalLayoutListener(this);
}
#Override
public void onGlobalLayout() {
// unregister directly, just interested once.
View content = findViewById(android.R.id.content);
content.getViewTreeObserver().removeOnGlobalLayoutListener(this);
// do things here.
}
}
SITUATION:
An application with resources for portait and landscape, has a simulator that I keep after configuration changes (the user can switch orientation while the simulation is running).
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Installer.installApkData(this);
simulator = new Simulator(this);
MainActivity prevActivity = (MainActivity)getLastCustomNonConfigurationInstance();
if(prevActivity!= null) {
// So the orientation did change
// Restore some field for example
this.simulator = prevActivity.simulator;
//this.mNavigationDrawerFragment = prevActivity.mNavigationDrawerFragment;
//this.mTitle = prevActivity.mTitle;
Log.d("APP","Activity restarted: simulator recreated");
}
requestWindowFeature(Window.FEATURE_PROGRESS);
setContentView(R.layout.activity_main);
setProgressBarVisibility(true);
mNavigationDrawerFragment = (NavigationDrawerFragment) getSupportFragmentManager()
.findFragmentById(R.id.navigation_drawer);
mTitle = getTitle();
// Set up the drawer.
mNavigationDrawerFragment.setUp(R.id.navigation_drawer,
(DrawerLayout) findViewById(R.id.drawer_layout));
}
#Override
public Object onRetainCustomNonConfigurationInstance() {
//restore all your data here
return this;
}
...
There is a method in the activity that changes the selected section in the NavigationDrawer, in the UI thread because if not it crashes.
public void showHud() {
// TODO Auto-generated method stub
runOnUiThread( new Runnable() {
public void run() {
mNavigationDrawerFragment.select(1);
onSectionAttached(2);
restoreActionBar();
}
});
}
This method is used to go directly to display the simulation once the simulator has been connected.
PROBLEM:
All this system works except for when I connect the simulator after switching the orientation. It executes the runOnUiThread but it does nothing. I think the reason for that is that it loses the UI thread that created that view when the activity is restarted.
As you can see there are two lines commented in the reloading of the simulator where I also tried to save the NavigationDrawer object without success in the test: same behavior.
I also tried to save the prevActivity and in the method showHUD(), first asking if its null and if not, execute the method inside the prevActivity. Expecting that it will access the original UI Thread, but I was mistaken.
Is there any solution to keep this UI Thread during the restarting of an activity? or maybe another type of solution?
Thanks a lot.
You should be checking your onSavedInstanceState in your Activity. This is how the Android OS is designed to handle this. You are trying to do this yourself, when you should be relying on the OS supplied functionality.
Quite a few examples of this (if you search SO):
Android: Efficient Screen Rotation Handling
Handle screen rotation without losing data - Android
If you want to save configuration, you need to save specific things. You can do this in the onPause() or on onSaveInstanceState().
If onCreate() is called after your configuration change, you can get what you need back out of the bundle. when you get it back out, you can then set what you need.
See this: http://developer.android.com/training/basics/activity-lifecycle/recreating.html
I am correctly retaining the data object but after a device rotation the function in UI thread has no effect, a function to change the selected section in the NavigationDrawer. I thought it was because I was losing the correct UI thread but actually, what I was losing is this NavigationDrawerFragment.
Just by adding the setRetainInstance(true) line in the OnCreate() of the NavigationDrawerFragment solves the problem:
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
...