How to effectively manage android detail navigation? - android

I am developing the detail view of a product which gives suggestions of similar products (waking the same activity with another product who also has suggestions) more or less like Google Play app detail activity. The problem comes when accessing to multiple related products which causes a OutOfMemoryException (since we are keeping all the previous instances of that ProductDetailActivity on the BackStack).
Is there a way of provide proper products back history without having to keep all the previous activities consuming memory?
I've checked Google play app and I'm not able to see how but it seems that it keeps all the previous activities and it's not causing any OutOfMemory while navigating related apps over related apps.

I had a similar issue. In my case I was using fragments, but I will share the technique so you can use it with your activity.
You can think of the multiple activities like a stack. When the user selects a product suggestion, the current activity is pushed onto the stack and a new activity is started. Conversely, when the back button is pressed, the activity finishes and the previous activity is popped off the stack and made current.
This can consume a lot of memory depending on your application.
The insight that I had was that I could save a lot on memory by managing that stack myself, and only saving the minimum amount of data required to recreate that UI.
So let's start by creating the stack:
private ArrayList<Bundle> mProductStack;
You will need to persist this stack in onSaveInstanceState() and recover it in onCreate(). Let's get those out of the way:
#Override
public void onSaveInstanceState(Bundle outState) {
// ... all your other state saving code
outState.putParcelableArrayList("productStack", mProductStack);
super.onSaveInstanceState(outState);
}
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// ... all your other activity create logic
if (savedInstanceState != null) {
mProductStack = savedInstanceState.getParcelableArrayList("productStack");
} else {
mProductStack = new ArrayList<>();
}
}
There's a reason for using a list rather than a stack. I had a lot of problems with persisting/unpersisting a Stack<Bundle> around casting and so forth, so I decided to go with the simple ArrayList<Bundle>, which is supported directly by Bundle (putParcelableArrayList(), etc).
Now when the user navigates to a new product, you need to save the current state in the stack, and load your new product into the current activity. Your state might even be as simple as a product ID:
Bundle entry = new Bundle();
entry.putInt("productId", mProductId);
mProductStack.add(entry); // push
// ... load the data for the next product
You may need to persist some actual UI state, for example I had this:
int scrollPos = mNestedScrollView.getScrollY(); // remember where we were in the list
entry.putInt("scrollPos", scrollPos);
Now you just have to handle the back button press:
#Override
public void onBackPressed() {
if (mProductStack.isEmpty()) {
super.onBackPressed();
return;
}
Bundle entry = mProductStack.remove(mProductStack.size() - 1); // pop
mProductId= entry.getInt("productId");
// ... load the data for the previous product
}
I don't know how complex your state is, but keep in mind that all the data types you want to save will need to implement Parcelable. There's a lot of content online (including SO) on this topic.
The thing that seems to have the most impact on memory is images. Memory management for bitmaps is a whole nasty topic unto itself. You may want to implement some type of LRU cache for images so you don't have to re-fetch images for the previous product — unless memory constraints force you to do so.

Related

Android activity loses data when switching between apps

I'm writing my first Android app. Essentially it shows and controls the score of a straight pool match. I've read articles about the lifecycle of an activity but I still have a problem I don't understand:
When I switch back to my app with a running game (so a score different from the initial 0:0 is shown) the activity sometimes loses its state and shows 0:0 instead of the score when I left the app. I overloaded the methods onSaveInstanceState and onRestoreInstanceState. The former get's called when I press the home button of my device. The latter never gets called. I've read that the method only gets called when onCreate is called. The onCreate method doesn't get called although the app needs longer than usual to reload after switching to some other apps in between. So I think the activity does get rebuild but obviously not by onCreate and the saved score is not loaded by onRestoreInstanceState.
Can you explain me what's happening and how to achieve the desired behaviour? Thank you very much!
edit: I was asked to post my onCreate() and onSaveInstanceState() methods. I tried to shorten them in a useful way. Please tell me If there is anything unclear or missing.
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final GameSetting gameSetting = getGameSettings();
final GameData gameData = new GameData(gameSetting.getLeague());
game = new GameLogic(gameData);
scoreViews.put(PlayerId.PLAYER_A, (TextView) findViewById(R.id.playerAScore));
}
#Override
protected void onSaveInstanceState(#NonNull final Bundle outState) {
GameDataInstanceSupplier.saveInstance(game.getGameData(), outState);
// inside the method the data gets saved like
// outState.putInt(STATE_SCORE_A, data.getScore(PlayerId.PLAYER_A));
super.onSaveInstanceState(outState);
}
#Override
protected void onRestoreInstanceState(#NonNull final Bundle inState) {
super.onRestoreInstanceState(inState);
GameDataInstanceSupplier.restoreInstance(game.getGameData(), inState);
// inside the method the data gets loaded like
// data.setScore(PlayerId.PLAYER_A, inState.getInt(STATE_SCORE_A));
}
I put a "debug output" inside the onCreate() method and it did not appear in the debug log. The same was true for onRestoreInstanceState(). A message was printed inside onSaveInstanceState() when I pushed the home button.
I think your app is not the problem, you see, I came to find this page because I have the same problem switching aplications in my android phone, it's specially noticiable when usig 2 factor authentication and have to switch to text message, mail or other app to get the sent code, when switch back to the code input screen on the browser or app (teams for example) it is no longer there anymore and it has return to the begining, the login screen for example. This is very annoying because I have disable every thing that has to do with energy savings but still have the issue. I discovered that if I open first the message app and then do the authentication while switching very quickly just to see the code in 1 or 2 seconds and then return sometimes it works. It could be memory issue, or may be an unfulfilled politic on the energy savings. I think is probably the last because other explicitly stated properties like setting the screen brightness automatic adjust off and set to maximun works for a while and then returns to a default. It may even be a planned obsolescence. My phone is a Huawei P smart 2019 (POT-LX3) with android 10. I'll may be try a factory reset. Any suggestions are welcome.

Opening Instance of Activity

I have an app that hold post information in an activity. in this activity related posts listed in bottom of post. User by clicking on related post can go to post activity and see that post info and related posts too.
As you can see in image, I have Activity A that holds post and it's related posts. When user Click on post I send user to Activity A with new post id and fill activity by new data.
But I think this is not Right way!
should I used Fragment instead of Activity?
Opening another Instance of an Activity on top of another is simplest way of navigating a content graph. User can simply press back, and go to previously opened content, until user reaches back to starting Activity, then the application closes. Though pretty straight forward, this particular approach has two issues:
It may happen that a lot of Instances of same activity are on the stack, utilising a large amount of device resources like memory.
You don't have a fine grained control over Activity Stack. You can only launch more activities, finish some, or have to resort to intent flags like FLAG_CLEAR_TOP etc.
There is another approach, that re-uses the same Activity instance, loads new content in it while also remembering the history of content that was loaded. Much like a web browser does with web page urls.
The Idea is to keep a Stack of content viewed so far. Loading new content pushes more data to stack, while going back pops the top content from stack, until it is empty. The Activity UI always displays the content from top of the stack.
Rough Example:
public class PostActivity extends AppCompatActivity {
// keep history of viewed posts, with current post at top
private final Stack<Post> navStack = new Stack<>();
#Override
protected void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// get starting link from intent extras and call loadPost(link)
}
private void loadPost(String link){
// Load post data in background and then call onPostLoaded(post)
// this is also called when user clicks on a related post link
}
private void onPostLoaded(Post post){
// add new post to stack
navStack.push(post);
// refresh UI
updateDisplay();
}
private void updateDisplay(){
// take the top Post, without removing it from stack
final Post post = navStack.peek();
// Display this post data in UI
}
#Override
public void onBackPressed() {
// pop the top item
navStack.pop();
if(navStack.isEmpty()) {
// no more items in history, should finish
super.onBackPressed();
}else {
// refresh UI with the item that is now on top of stack
updateDisplay();
}
}
#Override
protected void onDestroy() {
super.onDestroy();
// cancel any background post load, release resources
}
}
I would choose:
activity/fragment depends on complexity with:
horizontal recyclerview with custom expanded card view
and inside this expanded card view second vertical recyclerview :)
Here's what you can try.
Create a PostActivity which is a shell for fragments. Inside this activity you can just replace fragments using FragmentTransaction.
Your PostActivity can now have a PostFragment which will hold post and related posts. Now on click of post you can replace PostFragment with PostDetailFragment with postID being sent to the new fragment as a bundle. The PostDetailFragment will now display details according to id.
Check here: http://www.vogella.com/tutorials/Android/article.html#components_fragments
By seeing the picture the way i would implement is i would have create an activity with a bottom listview for your items and on top there would be a framelayout for holding fragments . when user click on any list item i would load the respective fragment in the activity
It all depends on what you are trying to achieve. What would you expect to happen when the user touches the back button after going down a couple of levels? If you want to the application to exit, no matter how deep in the sequence they have gone, then the best solution in my opinion is to simply reload the same activity with the new data and invaliding the affected views. If you need the back button to take the user back to the previous data, then the next question would be if you are keeping track of the past data breadcrumb. If so, then just intercept the back button and load the previous data for as long as there is data in your stack, or exit if you get to the top. If you don't want to keep track of the previous data chain, then instead of loading one activity with the new data, you can start a new activity of the same class, but with the new data. Android with keep the track of activities and each back button touch would close the running activity and take the user to the previous activity. Choice of activity versus fragment is just yours. You can use fragments that hold the data that you want to change after each user touch, create new ones when needed, disconnect the previous ones, and connect the new ones. You will need to do some extra work to make sure the back button works correctly (depending on you want the back button to behave). Based on what I can tell, it is simpler to just have one activity and load new data when needed and keep a trail of data changes, if you need to be able to go back.
It can be achieved using activity alone. Though I preferred moving all related UI to fragment.
You can use Navigator class.
Here the steps:
1. Add Navigator Class
public class Navigator {
private static Navigator instance;
public synchronized static Navigator getInstance() {
if (instance == null) {
instance = new Navigator();
}
return instance;
}
public void navigateToActivityA(Context context) {
Intent activity= AActivity.getCallingIntent(context);
context.startActivity(activity);
}
}
2. Add the calling method to your Activity class.
public static Intent getCallingIntent(Context context) {
return new Intent(context, AActivity.class);
}
3. Call the activity with the following code in your caller activity.
Navigator.getInstance().navigateToActivityA(this);
I suggest that you read about AndroidCleanArchitecture
For this task...
0) Starting new activity
I read again about question, and understood that you need advice for starting activity. So, starting new activity it's Ok, your main problem will be with another things (see below).
But lets talk about starting another data. Using Fragment instead doesn't resolve your task, fragments helps with different screen work. Using for example just data refreshing as a variant. You may use just single activity and refresh only data, it will look much better, if you also add animation, but not better than starting activity.
Using Fragment helps your with different screen actions. And maybe, answering on your question - it will be most suitable solution. You just use single acitivity - PostActivity, and several fragments - FragmentMainPost, FragmentRelated - which will be replaced, each other, by selecting from related post.
1) Issues with returning back
Lets imagine, that users clicks to new one activity and we loaded new data. It's Ok, and when Users clicks over 100 activities and receiving a lot of information. It's Ok, too. But main question here it's returning back (also another about caching, but lets leave it, for now).
So everyone know, it's bad idea to save a lot of activities in stack. So for my every application, with similar behavior we override onBackPressed in this activity. But how, lets see the flow below:
//Activities most have some unique ID for saving, for ex, post number.
//Users clicks to 100 new activities, just start as new activity, and
//finish previous, via method, or setting parameter for activity in AndroidManifest
<activity
noHistory=true>
</activity>
....
//When activity loaded, save it's activity data, for ex, number of post
//in some special place, for example to our Application. So as a result
//we load new activity and save information about it to list
....
// User now want return back. We don't save all stack this activities,
// so all is Ok. When User pressing back, we start action for loading
//activities, saved on our list..
.....
onBackPressed () {
//Getting unique list
LinkedTreeSet<PostID> postList =
getMyApplication().getListOfHistory();
//Getting previous Post ID based on current
PostID previousPostID = postList.get(getCurrentPost());
//Start new activity with parameter (just for ex)
startActivity(new Intent().putExtra(previousPostID));
}
RESULT
I found this as the best solution for this tasks. Because in every time - we work only with single activity!

Android application not working after resuming activity

I have an application where whenever I exit the application via the home hardware button, it should return to the last state the application is in. However, when I launch the application again, the application shows a white screen with only my header bar. And when I click on the header bar's button, the application crashes with the IllegalStateException where the application cannot find the method for the button clicked.
I am currently implementing with Sherlocks Fragment, where the header bar is an action bar. I'm also using HTC Rhyme, Version 2.3 (Gingerbread). The following is the codes for the addition of fragments into my main app.
Codes to add the fragments within the onCreate method in the activity:
FragmentTransaction trans = getSupportFragmentManager().beginTransaction();
Bundle bMain = getIntent().getExtras();
String statusCheck = "";
if (bMain != null) {
statusCheck = bMain.getString("statusCheck");
}
if (statusCheck.equals("web")) {
MyWebViewFragment webfrag = new MyWebViewFragment();
trans.add(R.id.container,webfrag, "WebViewFragment");
} else if(statusCheck.equals("traveloguelist")) {
MyTravelogueListFragment travelfrag = new MyTravelogueListFragment();
trans.add(R.id.container,travelfrag, "TravelogueListFragment");
}
trans.commit();
This is the codes when I change a fragment:
MyTravelogueListFragment travelfrag = new MyTravelogueListFragment();
getSupportFragmentManager().beginTransaction().replace(R.id.container, travelfrag).addToBackStack(null).commit();
[Edit]
I realized after much reading and running that the main issuei have is that upon resuming the application, the activity is actually created again. Thus, some of the parameters i passed in does not get registered, resulting in the wrong display. I THINK this is the error that is causing that to happen:
Previously focused view reported id "myresID" during save, but can't be found during restore.
However, I don't know how you force the application to remember the previous state of the fragment? Or is there any other way around this problem?
I'm still very stuck with this problem. Will really appreciate it if someone can help me!
After much trial and error and many readings, I finally found a way to sort of solve my problem.
From what I understand, this problem will occur due to the Activity's life cycle. The comment by Tseng in this forum was quite comprehensive:
http://forum.unity3d.com/threads/127794-Android-Apps-crashing-on-resume
It seems that during the time when other applications are invoked when a certain activity is onPause/onStop, Android might free up some of its memory the activity is currently holding on to if there is insufficient memory required. In this case, all the current objects or variable the paused activity is having will be destroyed. Thus, when the activity is back on focus, the onCreate is actually invoked again. Thus, the activity will have no idea which fragment I am currently require.
However, I realized that it will always call the saveInstanceState which is essentially a bundle object. So I did the following:
onSaveInstanceState method
#Override
public void onSaveInstanceState(Bundle bundle) {
//activityFrag is a string object that tells me which fragment i am in currently
bundle.putString("statusCheck", activityFrag);
}
onCreate method
if (savedInstanceState != null) {
getSupportFragmentManager().popBackStack(null, getSupportFragmentManager().POP_BACK_STACK_INCLUSIVE);
//return;
statusCheck = savedInstanceState.getString("statusCheck");
} else {
statusCheck = b.getString("statusCheck");
}
What I have done is to remove all the fragments I have stacked thus far to remove any issues where there is missing information needed. So this is like starting anew again. The status check just determine which fragment the user has last visited.
After much testing, it seems like it does solve my problem. though I wouldn't say it is perfect. One of the main downfall I have is that whenever I change my fragment, I have to update and change my statusCheck to make sure the correct fragment will be called. However, I have to admit this way is a little unorthodox and might not be very correct.
If any of you have any better ideas, please feel free to share!
You can try to implement following:
Use putFragment to save all fragments, currently located in FragmentManager, into bundle in onSaveInstanceState;
And then you can use getFragment to get all previously stored fragments back from bundle in onRestoreInstanceState.
Also... you'll probably need some HashMap that will help to determine the hierarchy of the fragments (in case you have containers and contained fragments) to be saved into bundle as well.
Also... when restoring from bundle you'll need to know keys for all fragment you've put there earlier. Probably, the easiest way is simply to organize an array of keys and put them into bundle when saving the fragment into instance.
This way your saving and restoring will be complete and centralized.

Save instance of dynamically generated views when we switch back and forth between activities

I am inflating a view on button click and the user can add as many views as he likes, all is fine I made it work, but now the problem is when I go back one activity and come again to my dynamically generated activity every single view that was generated is gone. Similar is the case if I go to next activity and come back to the inflated activity. I know about onSaveInstance and onRestoreSaveInstance. But how do I put view information in a bundle in onSaveInstanceState? Please note that my view was generated Dynamically i.e. on button Click and I want to know as of how to preserve the state of my activity.
How do you go about it?
I am thinking that you should implement some kind of logic that helps you restore the state of your Views. So you should be designing a class, let say ViewDetail that somehow keeps details about the Views that you are adding.... type, dimension, etc. This class should implement Parcelable so you are able to add it to the bundle.
So you will keep an ArrayList<ViewDetail>, myViews where everytime the user adds a new View you create a new ViewDetail object that you add to your myViews array.
And then save your Views and restore them using those objects:
#Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
//save your view states
outState.putParcelableArrayList("MY_VIEWS",myViews);
}
#Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
//get the views back...
myViews=savedInstanceState.getParcelableArrayList("MY_VIEWS");
//TODO: add the views back to your Activity
}
As your application may be killed completely at any moment without noticem you have to provide long term storage off heap memory
You only have to restore all the views, if your activity was terminated (and it can be at any time). When it is activated again after termination, it goes through onCreate() method
- this would be proper place to restore activity state.
Only callback which is guaranted to be called before your application / activity is destroyed is onPause() - this is a proper place to save views states into long term off-heap storage.

Android: Saving contents of a listview onSaveInstanceState

Is there an built in way to save the contents of a listView as part of onSaveInstanceState to then restore later? I want the listView to look the same if the user hit the back button and now onCreate is being called again.
If you set your activity's launchMode to singleTask, then (unless the application was terminated / gc called upon) your data (list) will be preserved.
This way your device will hold only one running instance of your application at a time, so when you "launch it again" no matter from where, if it's already running in the background, then that instance will show up (with the latest data).
If there is a risk that your application was finished, and you still need the latest list of data to show up, this solution won't work.
But you could give a try to SharedPreferences: save the current data to the application's SharedPreferences, and restore it from there when launching it.
If it's ok, to have the predefined new list on each clean start of the application, but when getting it into foreground, you need the last seen items in your list, you should use the savedInstanceState parameter of your onCreate method:
private static final String MYLISTKEY = "myListLabels";
private ArrayList<String> listLabels = new ArrayList<String>();
#Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
if (savedInstanceState.containsKey(MYLISTKEY))
{
listLabels = savedInstanceState.getStringArrayList(MYLISTKEY);
}
else
{
// TODO: populate explicitely your list
}
}
#Override
protected void onSaveInstanceState(Bundle outState)
{
super.onSaveInstanceState(outState);
outState.putStringArrayList(MYLISTKEY, listLabels);
}
where listLabels contains the labels for your list.
It's not necessary for them to be of type String, you can put any type inside your Bundle.
When the user hits the back button the activity is always destroyed, so there will not be any restoring from savedinstance.
Android Training
When your activity is destroyed because the user presses Back or the activity finishes itself, the system's concept of that Activity instance is gone forever because the behavior indicates the activity is no longer needed.

Categories

Resources