Android Fragments, save instance or recreate each time? - android

I was looking through an example in
https://github.com/lemycanh/DrawerOnTopActionBar/blob/master/src/com/example/drawerontopactionbar/MainActivity.java
I managed to incorporate that successfully in my app but I could not quite understand the onItemClick function.
#Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
mDrawerLayout.closeDrawer(mDrawerList);
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction ftx = fragmentManager.beginTransaction();
if(position == 0) {
ftx.replace(R.id.main_content, new FragmentFirst());
} else if(position == 1) {
ftx.replace(R.id.main_content, new FragmentSecond());
}
ftx.commit();
}
From the raw code it'll create a new Fragment every time there is an item click in the drawer. I understand that if this was called the first time you'll need an instance of it. But wouldn't it make sense to save the fragment somewhere in the activity? Then switch to it upon item click?
Something like
Fragment fOne;
Fragment fTwo;
#Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
mDrawerLayout.closeDrawer(mDrawerList);
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction ftx = fragmentManager.beginTransaction();
if(position == 0) {
if( fOne==null){
fOne = new FragmentFirst();
}
ftx.replace(R.id.main_content, fOne);
} else if(position == 1) {
if( fTwo==null){
fTwo = new FragmentSecond();
}
ftx.replace(R.id.main_content, fTwo);
}
ftx.commit();
}

it mostly depends on the situation or necessity. From a memory usage perspective, it's good to save the instances for later usage.
After all fragments are all about offering a smoother ux utilising pre-readiness.

Fragment are hard coded in xml can not replaced.... but when you call replace(..) then what happen ?? Ok just consider you have 2 fragment A,B . In primary stage A initialize in MainActivity... now you are going to call B using replace(....). that means A will go onPause(),onStop() state of lifecycle and B will be initialized and paced in MainActivity... If you want to reuse A then you need to call again A using replace (..). and then A will reinitialize and B goes to onPause(),onStop(). But there is another way you can do this
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
hide(A);
show(B);
ft.commit();
if you use above code block then A still be in the running state of its lifecycle but not visible (just detached from UI ).. so it would be better if you need those fragment again use hide()or show() method because it is less expensive operation then reinitialize the fragment.
or
it's better to use the attach() and detach() transactions instead of show() and hide() if you wish to retain the fragment instance but remove it from the UI
and still there is a way to save fragment instance .
In the fragment, save instance state by override onSaveInstanceState and restore on onActivityCreated:
And important point, in the activity, you have to save fragment's instance on onSaveInstanceState and restore on onCreate.
for more info Saving Fragment States

Related

Removing a fragment and then re-adding it

I've written a method, setLoading in my activity that is responsible for setting the "loading" status of my app. This method is responsible for instantiating a LoadingFragment, removing any existing instances of it (Using FragmentManager) and then depending on it's first parameter loading, adding it to one of two possible containers (Depending on the top parameter).
protected LoadingFragment loadingFragment;
public void setLoading(boolean loading, boolean top) {
FragmentManager fragmentManager = getFragmentManager();
// Create LoadingFragment instance if it has not already been created
if (loadingFragment == null || !(loadingFragment instanceof LoadingFragment)) {
loadingFragment = new LoadingFragment();
}
// Remove the fragment first if it is present
fragmentManager
.beginTransaction()
.remove(loadingFragment)
.commit();
// Only if loading is true should we display the fragment
if (loading) {
// Decide which container we're going to put the fragment in
int id = top ? R.id.topContainer : R.id.container;
// Place the fragment in the right position
fragmentManager
.beginTransaction()
.add(id, loadingFragment)
.commit();
}
}
public void setLoading(boolean loading) {
setLoading(loading, true);
}
I am triggering setLoading(true) from elsewhere in my activity and I have commented out it's corresponding setLoading(false) while testing.
What I want to happen is for my LoadingFragment to appear every time setLoading(true) is called. The first call shouldn't remove anything since it at that point it doesn't exist. All subsequent calls should remove the existing LoadingFragment and add it again.
What happens is that the first call to setLoading(true) does indeed create the LoadingFragment and put it in the correct container. However, subsequent calls to setLoading(true) remove the fragment, but it never seems to be re-added. I have checked to see that the fragment does indeed exist and is of type LoadingFragment at the point it is added and I have also checked to ensure that it's onCreateView method is being called.
Am I doing something wrong?
Edit
Using the answer given below by H Raval as a base I have now come up with the following:
public void setLoading(boolean loading, boolean top) {
FragmentManager fragmentManager = getFragmentManager();
Fragment currentLoadingFragment = fragmentManager.findFragmentById(R.id.loadingFragment);
if (currentLoadingFragment != null) {
fragmentManager
.beginTransaction()
.remove(currentLoadingFragment)
.commit();
}
if (loading) {
int id = top ? R.id.topContainer : R.id.container;
fragmentManager
.beginTransaction()
.add(id, new LoadingFragment())
.commit();
}
}
This seems to work as expected. It seems that the primary difference is that this code is creating a new LoadingFragment instance each time (When loading = true) whereas originally I was trying to use the same instance and just adding/removing it using the FragmentManager.
Out of interest, is there a reason I need to create a new instance after using remove? Is this the correct way to do it? Or should it still work when using the same instance? Additionally, if it's recommended to create a new instance each time, is there anything I should do in terms of clean-up, freeing up resources etc. (Perhaps there's a way of gracefully destroying the obsolete instances)?
well i have made some changes in your code and works perfect for me..let me know if you face any difficulty
public void loadFragment(boolean loading, boolean top){
FragmentManager fragmentManager = getSupportFragmentManager();
loadingFragment = new LoadingFragment();
// Only if loading is true should we display the fragment
if (loading) {
// Decide which container we're going to put the fragment in
int id = top ? R.id.topContainer : R.id.container;
if(top){
if(fragmentManager.findFragmentByTag("loadingFragment")!=null)
fragmentManager.beginTransaction().remove(fragmentManager.findFragmentByTag("loadingFragment")).commit();
fragmentManager
.beginTransaction()
.replace(R.id.topContainer, loadingFragment,"toploadingFragment")
.commit();
}else{
if(fragmentManager.findFragmentByTag("toploadingFragment")!=null)
fragmentManager.beginTransaction().remove(fragmentManager.findFragmentByTag("toploadingFragment")).commit();
fragmentManager
.beginTransaction()
.replace(R.id.container, loadingFragment,"loadingFragment")
.commit();
}
}

Returning back to an Activity after calling and closing a Fragment does not change my Activity's lifecycle

Given an Activity that acts as a Home page (it never closes) that launches various fragments, how to know when the Activity is visible to the user?
From what I have observed, when I open a fragment the lifecycle for the Activity never changes, onPause() is not called. And when I close the fragment, onResume() is not called on my Activity.
Here is how I am starting my fragments, I am using this method and passing the fragment I want to launch to it.
public void addFragment(int containerId, Fragment fragment, boolean addToBackStack) {
// Check if the fragment has been added already. If so, then
// don't add the fragment.
Fragment temp = mFragmentManager.findFragmentByTag(fragment.getClass().getName());
if(temp != null && temp.isAdded()) {
return;
}
FragmentTransaction ft = mFragmentManager.beginTransaction();
ft.add(containerId, fragment, fragment.getClass().getName());
if(addToBackStack)
ft.addToBackStack(null);
ft.commit();
}
What is the methodology for indicating that my Activity is visible again? Thanks in advance!
in the oncreate method of your home activity, call
mFragmentManager.addOnBackStackChangedListener(this) ;
and then define
#Override
public void onBackStackChanged() {
int backStackCount = mFragmentManager.getBackStackEntryCount();
if(backStackCount == 0) {} //back to home screen
}
Your Activity is always Visible even if thousand Fragments are showing at the same time, for the sake of understanding Fragments are just Custom-Views, and the Fragment gives a helping hand in handling your View, so onPause() on your activity does not need to called when a Fragment dies or is born,just like inflating a View.
Just like Sir #Tim Mutton said, you need to check your BackStack to know if you are back, or you can use the ViewGroup method ViewGroup.indexOfChild(View child) - this method will an int of value getChildCount()-1 which means its on top of its fellow sibblings..
Hope it helps

Android: keep Fragment running

Is it possible to keep a Fragment running while I call replace from FragmentManager to open a new Fragment?
Basically I don't want to pause a Fragment when navigating (via replace method) to another Fragment.
Is it possible?
Or the correct approach is, always, instantiate a new Fragment every time I need to open it and restore its previous state?
Thanks!
FragmentManger replace method will destroy the previous fragment completely, So in each transaction onDestroyView(), onDestroy() and onDetach() will get called on previous fragment. If you want to keep your fragment running you can instead use FragmentManger hide() and show() methods! It hides and shows the fragments without destroying them.
so first add both fragments to fragment manager and also hide the second fragment.
fragmentManager.beginTransaction()
.add(R.id.new_card_container, FragmentA)
.add(R.id.new_card_container,FragmentB)
.hide(FragmentB)
.commit();
Note that you can only call show() on hidden fragment. So here you can't call show() on FragmentA but it's not a problem because by hiding and showing FragmentB you can get replacement effect you want.
And here is a method to go back and forth between your fragments.
public void showOtherFragment() {
if(FragmentB.isHidden()){
fragmentManager.beginTransaction()
.show(FragmentB).commit();
} else {
fragmentManager.beginTransaction()
.hide(FragmentB).commit();
}
}
Now if you put log message in fragment callback method you will see there is no destruction (except for screen orientation change!), even view will not get destroyed since onDistroyView doesn't get called.
There is only one problem and that is, first time when application starts onCreateView() method get called one time for each fragment (and it should be!) but when the orientation changes onCreateView() gets called twice for each fragment and that's because fragments once created as usual and once because of there attachment to FragmentManger (saved on bundle object) To avoid that you have two options 1) detach fragments in onSaveInstaneState() callback.
#Override
protected void onSaveInstanceState(Bundle outState) {
fragmentManager.beginTransaction()
.detach(FragmentA)
.detach(FragmentB)
.commit();
super.onSaveInstanceState(outState);
}
It's working but view state will not get updated automatically, for example if you have a EditText its text will erase each time orientation change happens. of course you can fix this simply by saving states in the fragment but you don't have to if you use the second option!
first i save a Boolean value in onSaveInstaneState() method to remember witch fragment is shown.
#Override
protected void onSaveInstanceState(Bundle outState) {
boolean isFragAVisible = true;
if(!FragmentB.isHidden())
isFragAVisible = false;
outState.putBoolean("isFragAVisible",isFragAVisible);
super.onSaveInstanceState(outState);
}
now in activity onCreate method i check to see if savedInstanceState == null. if yes do as usual if not activity is created for second time. so fragment manager already contains the fragments. So instead i'm getting a reference to my fragments from fragment manager. also i make sure correct fragment is shown since its not recovered automatically.
fragmentManager = getFragmentManager();
if(savedInstanceState == null){
FragmentA = new FragmentA();
FragmentB = new FragmentB();
fragmentManager.beginTransaction()
.add(R.id.new_card_container, FragmentA, "fragA")
.add(R.id.new_card_container, FragmentB, "fragB")
.hide(FragmentB)
.commit();
} else {
FragmentA = (FragmentA) fragmentManager.findFragmentByTag("fragA");
FragmentB = (FragmentB) fragmentManager.findFragmentByTag("fragB");
boolean isFragAVisible = savedInstanceState.getBoolean("isFragAVisible");
if(isFragAVisible)
fragmentManager.beginTransaction()
.hide(FragmentB)
.commit();
else
fragmentManager.beginTransaction()
.hide(FragmetA) //only if using transaction animation
.commit();
}
By now your fragment will work perfectly if are not using transaction animation. If you do, you also need to show and hide FragmentA. So when you want to show FragmentB first hide FragmentA then show FragmentB (in the same transaction) and when you want to hide FragmentB hide it first and also show FragmentA (again in the same transaction). Here is my code for card flip animation (downloaded from developer.goodle.com)
public void flipCard(String direction) {
int animationEnter, animationLeave;
if(direction == "left"){
animationEnter = R.animator.card_flip_right_in;
animationLeave = R.animator.card_flip_right_out;
} else {
animationEnter = R.animator.card_flip_left_in;
animationLeave = R.animator.card_flip_left_out;
}
if(cardBack.isHidden()){
fragmentManager.beginTransaction()
.setCustomAnimations(animationEnter, animationLeave)
.hide(cardFront)
.show(cardBack)
.commit();
} else {
fragmentManager.beginTransaction()
.setCustomAnimations(animationEnter,animationLeave)
.hide(cardBack)
.show(cardFront)
.commit();
}
}

getFragmentByTag() never returns null

I've five fragments in my activity. On the click of fragment drawer list, I'm calling setFrament() method.
It removes the previous fragments from the activity, and adds the new one as per the requirement.
Here's my code for setFragment method.
protected void setFragment(final int position) {
// Remove currently active fragment
if (mActiveFragment != null) {
Fragment previous;
while ((previous = mFragmentManager
.findFragmentByTag(mActiveFragment.toString())) != null) {
mFragmentManager.beginTransaction().remove(previous).commit();
Log.e("in loop", "stuck");
}
}
// It's enum, generated according to position
FragmentType type = getFragmentType(position);
Fragment fragment = mFragmentManager.findFragmentByTag(type.toString());
if (fragment == null) {
fragment = createFragment(type);
}
mFragmentManager
.beginTransaction()
.replace(R.id.containerFrameLayout, fragment,
type.toString()).commit();
// Sets the current selected fragment checked in
// Drawer listview
mFragmentDrawerList.setItemChecked(position, true);
// set Actionbar title
getActionBar().setTitle(type + "");
mActiveFragment = type;
}
Here, while changing the fragment, it keeps prining "stuck" in while loop forever,
my question is,
why remove(Fragment) method doesn't remove the previous fragment from the activity ?
commit() is an asynchronous call that is only executed when the Android system regains control. The fragments won't be removed until some time after you leave the method.
If you want to remove the previous fragment (assuming there's only one), then you can add them to the backstack. Then you can call popBackStack(null, 0) which will pop everything on the back stack. The side catch though is pressing the "back" button will also pop the backstack if the user were to do that. You'll have to override the onBackPressed() and handle it yourself if you don't want that to happen.
EDIT:
One method would be to keep track of all IDs or TAGs and call remove on them individually.
LinkedList<Integer> fragmentIds = new LinkedList<Integer>();
/*** Add fragment to FragmentManager ***/
fragmentIds.add(/** ID of fragment */);
/** Removing all fragments from FragmentManager **/
FragmentManager fm = getFragmentManager();
FragmentTransaction transaction = fm.beginTransaction();
Fragment fragToRemove;
for(Integer id : fragmentIds)
{
fragToRemove = fm.findFragmentById(id);
transaction.remove(fragToRemove);
}
transaction.commit()
fragmentIds.clear();
However, you don't have to call remove on any of them so long as you use replace() method on the same container. replace() will pop the previous fragment and add in the new one. So long as the transaction isn't pushed to the backstack, the previous fragment is detached from the Activity and discarded.

Android Fragment, going back without recreating/reloading Fragment

I've seen quite a few questions on SO about Fragments and I still can't seem to figure out if what I want to do is possible, and more so if my design pattern is just flawed and I need to re-work the entire process. Basically, like most questions that have been asked, I have an ActionBar with NavigationTabs (using ActionBarSherlock), then within each Tab there is a FragementActivity and then the FragmentActivities push new Fragments when a row is selected (I'm trying to re-create an iOS Project in Android and it's just a basic Navigation based app with some tabs that can drill down into specific information). When I click the back button on the phone the previous Fragment is loaded but the Fragment re-creates itself (so the WebServices are called again for each view) and this isn't needed since the information won't change in a previous view when going backwards. So basically what I want to figure out is how do I setup my Fragments so that when I push the back button on the phone, the previous Fragment is just pulled up with the previous items already created. Below is my current code :
//This is from my FragmentActivity Class that contains the ActionBar and Tab Selection Control
#Override
public void onTabSelected(ActionBar.Tab tab, FragmentTransaction ft) {
// TODO Auto-generated method stub
int selectedTab = tab.getPosition();
if (selectedTab == 0) {
SalesMainScreen salesScreen = new SalesMainScreen();
ft.replace(R.id.content, salesScreen);
}
else if (selectedTab == 1) {
ClientMainScreen clientScreen = new ClientMainScreen();
ft.replace(R.id.content, clientScreen);
}.....
//This is within the ClientMainScreen Fragment Class, which handles moving to the Detail Fragment
row.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
//Do something if Row is clicked
try{
String selectedClientName = clientObject.getString("ClientName");
String selectedClientID = clientObject.getString("ClientID");
String selectedValue = clientObject.getString("ClientValue");
transaction = getFragmentManager().beginTransaction();
ClientDetailScreen detailScreen = new ClientDetailScreen();
detailScreen.clientID = selectedClientID;
detailScreen.clientName = selectedClientName;
detailScreen.clientValue = selectedValue;
int currentID = ((ViewGroup)getView().getParent()).getId();
transaction.replace(currentID,detailScreen);
transaction.addToBackStack(null);
transaction.commit();
}
catch (Exception e) {
e.printStackTrace();
}
}
});....
//And then this is the Client Detail Fragment, with the method being called to Call the Web Service and create thew (since what is displayed on this screen is dependent on what is found in the Web Service
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup group, Bundle saved) {
return inflater.inflate(R.layout.clientdetailscreen, group, false);
}
#Override
public void onActivityCreated (Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
//Setup Preferences File Link
this.preferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
//initialize the table object
mainTable = (TableLayout)getActivity().findViewById(R.id.mainTable);
//setup the detail table
setupRelatedClientSection();
}
The Client Detail Screen can then drill down one more time, using the same method as the Client Main Screen but when I go back from that new screen to the Detail Screen the seuptRelatedClientSection() method is called again and so the entire Fragment is rebuilt when really I just want to pull up a saved version of that screen. Is this possible with my current setup, or did I approach this the wrong way?
Try using fragementTransaction.add instead of replace
I believe that you are looking for show() and hide().
I think you can still add them to the backstack.
transaction.hide(currentFragment);
transaction.show(detailScreen);
transaction.addToBackStack(null);
transaction.commit();
I didnt have my code to look at but i believe this is how it would go... Try it out unless someone else has a better way.
I have not tried the backstack with show() hide() but i believe that it takes the changes that are made before the transactions commit and will undo them if the back button is pressed. Please get back to me on this cause i am interested to know.
You also have to make sure that the detail fragment is created before you call this. Since it is based on the click of someitem then you should probably create the details fragment every time you click to make sure the correct details fragment is created.
I'm posting this answer for people who may refer this question in future.
Following code will demonstrate how to open FragmentB from FragmentA and going back to FragmentA from FragmentB (without refreshing FragmentA) by pressing back button.
public class FragmentA extends Fragment{
...
void openFragmentB(){
FragmentManager fragmentManager =
getActivity().getSupportFragmentManager();
FragmentB fragmentB = FragmentB.newInstance();
if (fragmentB.isAdded()) {
return;
} else {
fragmentManager.
beginTransaction().
add(R.id.mainContainer,fragmentB).
addToBackStack(FragmentB.TAG).
commit();
}
}
}
public class FragmentB extends Fragment{
public static final String TAG =
FragmentB.class.getSimpleName();
...
public static FragmentB newInstance(){
FragmentB fragmentB = new FragmentB();
return fragmentB;
}
#Override
public void onResume() {
super.onResume();
// add this piece of code in onResume method
this.getView().setFocusableInTouchMode(true);
this.getView().requestFocus();
}
}
In your MainActivity override onBackPressed()
class MainActivity extends Activity{
...
#Override
public void onBackPressed() {
if (getFragmentManager().getBackStackEntryCount() > 0) {
getFragmentManager().popBackStack();
} else {
super.onBackPressed();
}
}
}
You're right, there has been a number of previous questions / documentation on the topic ;)
The documentation on Fragments, specifically the section about Transactions and Saving State, will guide you to the answer.
http://developer.android.com/guide/components/fragments.html#Transactions
http://developer.android.com/guide/components/activities.html#SavingActivityState
Android - Fragment onActivityResult avoid reloading
Fragments can have support for onSaveInstanceState but not onRestoreInstanceState, so if you want to save a reference to the table views, save them to the Bundle and you can access the saved view in your onActivityCreated method. You could also use the Fragments back stack.
This guide/tutorial has very detailed instructions/examples on the back stack and retaining fragment state.
Good luck

Categories

Resources