Preload some fragment when the app starts - android

I have an Android application with a navigation drawer. My problem is that some fragment takes few second to load (parser, Map API). I would like to load all my fragment when the app starts.
I'm not sure if it is possible or a good way to do it, but I was thinking of create an instance of each of my fragments in the onCreate method of the main activity. Then, when the user select a fragment in the navigation drawer, I use the existing instance instead of creating a new one.
The problem is that it does not prevent lag the first time I show a specific fragment. In my opinion, the reason is that the fragment constructor does not do a lot of operation.
After searching the web, I can't find an elegant way to "preload" fragment when the application starts (and not when the user select an item in the drawer).
Some post talks about AsyncTask, but it looks like MapFragment operation can't be executed except in the main thread (I got an exception when I try: java.lang.IllegalStateException: Not on the main thread).
here is what I've tried so far:
mFragments = new Fragment[BasicFragment.FRAGMENT_NUMBER];
mFragments[BasicFragment.HOMEFRAGMENT_ID] = new HomeFragment();
mFragments[BasicFragment.CAFEFRAGMENT_ID] = new CafeFragment();
mFragments[BasicFragment.SERVICEFRAGMENT_ID] = new ServiceFragment();
mFragments[BasicFragment.GOOGLEMAPFRAGMENT_ID] = new GoogleMapFragment();
When an item is selected in the nav drawer:
private void selectItem(int position) {
Fragment fragment = mFragments[position];
// here, I check if the fragment is null and instanciate it if needed
FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction ft = fragmentManager.beginTransaction();
ft.replace(R.id.content_frame, fragment);
ft.commit();
mDrawerList.setItemChecked(position,true);
mDrawerLayout.closeDrawer(mDrawerList);
}
I also tried this solution; it allows to prevent a fragment from being loaded twice (or more), but it does not prevent my app from lag the first time I show it. That's why I try to load all fragments when the application starts (using a splash-screen or something) in order to prevent further lags.
Thanks for your help / suggestion.

You can put your fragments in ViewPager. It preloads 2 pages(fragments) by default. Also you can increase the number of preloaded pages(fragments)
mViewPager.setOffscreenPageLimit(int numberOfPreloadedPages);
However, you will need to rewrite your showFragment method and rewrite back stack logic.

One thing you can do is load the resources in a UI-less fragment by returning null in in Fragment#onCreateView(). You can also call Fragment#setRetainInstance(true) in order to prevent the fragment from being destroyed.
This can be added to the FragmentManager in Activity#onCreate(). From there, Fragments that you add can hook in to this resource fragment to get the resources they need.
So something like this:
public class ResourceFragment extends Fragment {
public static final String TAG = "resourceFragment";
private Bitmap mExtremelyLargeBitmap = null;
#Override
public View onCreateView(ViewInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return null;
}
#Override
public void onStart() {
super.onStart();
new BitmapLoader().execute();
}
public Bitmap getExtremelyLargeBitmap() {
return mExtremelyLargeBitmap;
}
private class BitmapLoader extends AsyncTask<Void, Void, Bitmap> {
#Override
protected Bitmap doInBackground(Void... params) {
return decodeBitmapMethod();
}
#Override
protected void onPostExecute(Bitmap result) {
mExtremelyLargeBitmap = result;
}
}
}
Add it to the fragment manager in the Activity first thing. Then, whenever you load your other Fragments, they merely have to get the resource fragment from the fragment manager like so:
public class FakeFragment extends Fragment {
#Override
public View onCreateView(ViewInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final ResourceFragment resFragment = getFragmentManager().findFragmentByTag(ResourceFragment.TAG);
Bitmap largeBitmap = resFragment.getBitmap();
if (largeBitmap != null) {
// Do something with it.
}
}
}
You will probably have to make a "register/unregister" listener set up because you will still need to wait until the resources are loaded, but you can start loading resources as soon as possible without creating a bunch of fragments at first.

To preload fragments, attach() can be used. So in OP's case it will be:
ft.attach(fragment).commit();
Make sure to store the fragment somewhere and use that one the next time ft.replace() is called.

Related

Initializing fragments outside of OnCreate() in Android

I am extremely new to Android development. I basically have an activity with some buttons, for example "seeTreePicture" and "seeSeaPicture". When I press a button, I want to use a fragment I called "ContentViewer" to display a random tree/sea picture, and also have buttons under the picture to destroy the ContentViewer fragment instance and go back to the menu. The issue is, if I try to use a Fragment Transaction anywhere other than onCreate() of the activity, I get a null pointer exception when I try to access the view in the fragment.
My activity and things related to the fragment:
public class SeeActivity extends AppCompatActivity {
DisplayFragment displayFragment;
Button seeTreeButton;
Button seeSeaButton;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_see);
seeTreeButton = findViewById(R.id.seeTreeButton);
seeSeaButton = findViewById(R.id.seeSeaButton);
displayFragment = new DisplayFragment();
seeTreeButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.add(R.id.fragmentContainer, displayFragment);
fragmentTransaction.commit();
fragmentTransaction.addToBackStack(null);
displayFragment.changeImage(randomTree);
}
});
}
}
In my fragment, change image simply changes the image source of the ImageView:
public void changeImage(int treeResource)
{
img = getView().findViewById(R.id.imageView);
img.setImageResource(treeResource);
}
I get a null pointer exception for trying to access the view from getView() in the fragment, meaning that onCreateView wasn't invoked. Yet if I put the same transaction in the onCreate() method of the activity, it works. What am I doing wrong?
The issue with your code is that you are accessing getView() of your fragment before the fragment has gone through the initialization of the view. The reason why you don't have the crash when you execute the transaction in your activity's onCreate() method is that by the time you click on a button your fragment has already gone through onCreateView() and initialized its view. Check out fragment lifecycle guide and bear in mind that you should not access your fragment view before it was created or after it was destroyed. For more information about why your fragment view is not initialized instantly check out this guide.
As for the solution, consider setting arguments for your fragment before adding it to your transaction like here:
Bundle args = new Bundle();
args.putInt(DisplayFragment.IMG_RESOURCE_ARG, randomTree);
displayFragment.setArguments(args);
Then in your onCreateView() or onViewCreated() methods of your fragment restore the arguments like here and set the image resource:
#Override
public void onViewCreated(#NonNull View view, Bundle savedInstanceState) {
int resourceId = requireArguments().getInt(IMG_RESOURCE_ARG);
img = view.findViewById(R.id.imageView);
img.setImageResource(resourceId);
}
Because the fragment transaction doesn't occur instantly. It occurs async. So the actual work of creating views hasn't occurred yet. Your options are to either use commitNow() instead of commit (which will do it synchronously, but require much more time and possibly cause your app to visibly pause) or to wait for the fragment transaction to actually complete. That can easily be done by putting it in a Runnable and passing that runnable to runOnCommit

Fragment calling fragments loosing state on screen rotation

Hi i created a project with a default "Navigation Drawer Activity".
So i have a MainActivity with a fragment with is replaced for each item on menu.
One of the menus is "Customers" with shows a list of customers.
From customers fragment i can see the Interests of this customers, with is a Fragment(CustomerListFragment) calling the interests(InterestsListFragment).
There is even more levels, but to be short that's enough.
This is the code on MainActivity that i use to call fragment from fragment and pass data between
public void passData(Object[] data, Fragment f) {
Bundle args = new Bundle();
args.putSerializable("PASSED_DATA", data);
f.setArguments(args);
FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.beginTransaction()
.replace(R.id.container, f)
.addToBackStack("")
.commit();
}
And i use like :
mCallbacks.passData(new Object[]{c}, new OpportunityListFragment());
The problem is that when i rotate the phone does not matter from wich level of activity i have, it comes back to the first fragment called(CustomerListFragment), and if i click "Back" on cellphone it gets back to where i was when i rotate the phone.
What do i have to do, to avoid this kind of problem? why it gets back to the first activity evoked if i am replacing fragments?
The answer from ste-fu is correct but let's explore programmatically. There is a good working code in Google documentation # Handling Runtime Changes. There are 2 code snippets that you have to do.
1) Code snippet:
public class MyActivity extends Activity {
private RetainedFragment dataFragment;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// find the retained fragment on activity restarts
FragmentManager fm = getFragmentManager();
dataFragment = (DataFragment) fm.findFragmentByTag(“data”);
// create the fragment and data the first time
if (dataFragment == null) {
Note: Code uses FragmentManager to find the current Fragment. If fragment is null, then the UI or app has not been executed. if not null, then you can get data from RetainedFragment object.
2) Need to retain the Fragment state.
public class RetainedFragment extends Fragment {
// data object we want to retain
private MyDataObject data;
// this method is only called once for this fragment
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// retain this fragment
setRetainInstance(true);
}
Note: setRetainInstance is used in OnCreate. And subclassing the Fragment is recommended, naming it RetainedFragment, used on snippet 1.
When you change screen orientation your parent Activity is destroyed and recreated. Unless you persist the level structure in some fashion, it will always appear as when you first started the activity. You can either use the bundle object, or for more complicated objects you need to persist it to a database.
Either way, onSaveInstanceState is your friend. Then in your onCreate method you need to check the bundle or database, and the set the fragment accordingly.

Retaining images when rotating Android device

I am creating an app, in which one of its activities is used to display images with captions. Those images are displayed using a ViewPager (each image in one page).
I have no problem with creating the images, and displaying them with their captions. However, I am having a problem when I rotate the device. Upon device rotation, the images are deleted, however the captions are retains. I think I know the reason for that (as explained after the code snippets below), but I am unable to solve it, and hence I am asking this question.
First, here is my gallery_item.xml file that is used to display each image:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<ImageView android:id="#+id/gallery_image"
android:layout_height="fill_parent"
android:layout_width="fill_parent"
android:scaleType="fitCenter"
android:background="#color/black" />
<TextView android:id="#+id/gallery_caption"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:gravity="right"
android:textSize="20sp"
android:paddingStart="#dimen/activity_horizontal_margin"
android:paddingEnd="#dimen/activity_horizontal_margin"
android:paddingBottom="#dimen/activity_vertical_margin"
android:textColor="#color/white"
android:background="#color/transparent_black_percent_50" />
</FrameLayout>
Then the activity code GalleryItemPagerActivity.java is below
public class GalleryItemPagerActivity extends Activity {
private ViewPager mViewPager;
private NewsItem mNewsItem;
private int mNewsItemId;
ImageDownloaderThread<ImageView> mImageDownloaderThread;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mViewPager = new ViewPager(this);
mViewPager.setId(R.id.galleryItemViewPager);
setContentView(mViewPager);
// Create a thread that will be run in the background to download images from the web for each URL
//
// IMPORTANT NOTE:
// We are downloading the image in the activity instead of the fragment because we are use a pager activity
// ... and pager activities must have the adapter in the activity rather than the fragment, which causes us to download the image here
// ... if we try to download in the fragment, then we will have a very complicated code that will cause too many memory leaks.
mImageDownloaderThread = new ImageDownloaderThread<ImageView>(new Handler());
// Prepare listener to update UI when an image is downloaded
mImageDownloaderThread.setListener(new ImageDownloaderThread.Listener() {
// This method is used to update the UI when the image ready
public void onImageDownloaded(Bitmap image, String url, int pos, int download_image_type) {
// First, get the fragment by position
GalleryItemFragment fragment = ((GalleryItemFragmentStatePagerAdapter)mViewPager.getAdapter()).getFragment(pos);
// If the fragment exists
if (fragment != null) {
// Update the image
ImageView imageView = (ImageView) fragment.getView().findViewById(R.id.gallery_image);
imageView.setImageBitmap(image);
}
}
});
// Start the background thread
mImageDownloaderThread.start();
// Prepare the looper for the background thread
mImageDownloaderThread.getLooper();
mNewsItemId = getIntent().getIntExtra(GalleryItemFragment.EXTRA_NEWS_ITEM_ID, 0);
mNewsItem = NewsItems.get(this).getNewsItem(mNewsItemId);
FragmentManager fm = getFragmentManager();
GalleryItemFragmentStatePagerAdapter adapter = new GalleryItemFragmentStatePagerAdapter(fm);
mViewPager.setAdapter(adapter);
}
// Destroy the background thread when we exit the app, otherwise it will stay forever
#Override
public void onDestroy() {
super.onDestroy();
mImageDownloaderThread.quit();
}
// I am subclassing FragmentStatePagerAdapter so I can add the method getFragment()
// ... This method allows me to get the displayed fragment by position, and if it does not exist, a null is returned.
private class GalleryItemFragmentStatePagerAdapter extends FragmentStatePagerAdapter {
// A map to track current fragments.
private Map<Integer, GalleryItemFragment> mFragmentReferenceMap = new HashMap<Integer, GalleryItemFragment>();
// Constructor
public GalleryItemFragmentStatePagerAdapter(FragmentManager fm) {
super(fm);
}
// Implementation of get count.
#Override
public int getCount() {
// The number of images to download is the number of our fragments
int count = mNewsItem.getImages().size();
return count;
}
// Fragment to display, and download the main image and author avatar
#Override
public Fragment getItem(int pos) {
// Download this image
String imageUrl = mNewsItem.getImages().get(pos).getUrl();
// Initiate a request to download the image at the background thread
mImageDownloaderThread.queueImage(imageUrl, pos, ImageDownloaderThread.DOWNLOAD_IMAGE_TYPE_MAIN);
// Get the fragment
GalleryItemFragment fragment = GalleryItemFragment.newInstance(mNewsItemId, pos);
// Add the fragment to our map
mFragmentReferenceMap.put(Integer.valueOf(pos), fragment);
return fragment;
}
// When a fragment is deleted, we have to remove it from the map
#Override
public void destroyItem(ViewGroup container, int pos, Object object) {
mFragmentReferenceMap.remove(Integer.valueOf(pos));
// Remove the fragment from the map
mFragmentReferenceMap.remove(Integer.valueOf(pos));
}
// Get the fragment by position (returns null if fragment does not exist)
public GalleryItemFragment getFragment(int key) {
return mFragmentReferenceMap.get(Integer.valueOf(key));
}
}
}
And the code for the fragment is right at GalleryItemFragment.java
public class GalleryItemFragment extends Fragment {
public static final String EXTRA_NEWS_ITEM_ID = "com.myproject.android.news_item_id";
public static final String EXTRA_GALLERY_ITEM_POSITION = "com.myproject.android.gallery_item_position";
private int mNewsItemId;
private int mGalleryItemPosition;
private TextView mCaptionField;
// This is used to set attach arguments to the fragment
public static GalleryItemFragment newInstance (int news_item_id, int gallery_item_position) {
Bundle args = new Bundle();
args.putInt(EXTRA_NEWS_ITEM_ID, news_item_id);
args.putInt(EXTRA_GALLERY_ITEM_POSITION, gallery_item_position);
GalleryItemFragment fragment = new GalleryItemFragment();
fragment.setArguments(args);
return fragment;
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mNewsItemId = getArguments().getInt(EXTRA_NEWS_ITEM_ID);
mGalleryItemPosition = getArguments().getInt(EXTRA_GALLERY_ITEM_POSITION);
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
// inflate our xml file first
View v = inflater.inflate(R.layout.gallery_item, parent, false);
mCaptionField = (TextView)v.findViewById(R.id.gallery_caption);
mCaptionField.setText(NewsItems.get(getActivity()).getNewsItem(mNewsItemId).getImages().get(mGalleryItemPosition).getCaption());
return v;
}
}
Now to the problem.
As explained earlier, when I rotate the device, the activity is destroyed, but the fragment is retained (as explained in the accepted answer here).
Also, you notice that the images in my case are downloaded in the activity rather than the fragments (due to using a pager activity, which requires that its adapter be in the activity rather than the fragment).
So when the activity is destroyed, the images are gone (but the captions are retained because they are part of the retained fragments). Which leads to a black page without images, but only captions.
Now, I read different solutions (such as this one), and I tried to add the following line into AndroidManifest.xml
android:configChanges="orientation|screenSize"
But this did not help much, as the images look 'disproportionate' when sliding left and right after rotating the device, because of the screenSize property, which retains the screen size, resulting in funny looking images.
Now, I think I should try onSaveInstanceState() but I am not sure how to do so. How can I save an image and retain it using onSaveInstanceState()? And how about other variables in my activity class (such as mViewPager), do I have to save them?
Thanks.
See, its as per design that activities or fragment will get destroyed whenever configuration of device is changed, however if you want to handle them yourselves then android do provide you facilities for the same with APIS like
onSaveInstanceState() -- you can save and restore the instance stae
onConfigurationChanged() -- you can handle the configuration changes which you have declared to handle yourself, like in your case "orientation|screenSize"
Additional to this, you can use setRetainInstance API in fragment to hold objects in fragments when activity is destroyed you can read about it here, however this api do mention not to hold objects like Bitmaps.
Now coming to your problem, this is what you should do.
Dont handle any configuration changes yourself, let android handle it for you
Hold any objects you want to retain, when orientation changes apart then bitmap, by above provided information.
Now to handle downloaded images efficiently, implement a FileChache and before downloading images check if image is already downloaded, and use the same instead of downloading it again,this will make sure when configuration is changed, you dont get black spot for already doanloaded image.
you can check it here in my guthub link.
Your problem is relod on onconfiguration changed, in manifest file give permission to that activity file like this:
android:configChanges="orientation|keyboard"

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

Fragment gets initialized twice when reloading activity with tabs when orientation changes

I have a problem reloading an activity with tabs and fragments when I change the orientation of my device.
Here's the situation:
I have an activity which has 3 tabs in the action bar. Each tab loads a different fragment in a FrameLayout in main view. Everything works fine if I don't change the orientation of the device. But when I do that Android tries to initialize the currently selected fragment twice which produce the following error:
E/AndroidRuntime(2022): Caused by: android.view.InflateException: Binary XML file line #39: Error inflating class fragment
Here's the sequence of steps that produce the error:
I load the activity, select tab nr 2. and change the orientation of the device.
Android destroys the activity and the instance of the fragment loaded by tab nr 2 (from now on, 'Fragment 2'). Then it proceeds to create new instances of the activity and the fragment.
Inside Activity.onCreate() I add the first tab to the action bar. When I do that, this tab gets automatically selected. It may represent a problem in the future, but I don't mind about that now. onTabSelected gets called and a new instance of the first fragment is created and loaded (see code below).
I add all the other tabs without any event being triggered, which is fine.
I call ActionBar.selectTab(myTab) to select Tab nr 2.
onTabUnselected() gets called for the first tab, and then onTabSelected() for the second tab. This sequence replaces the current fragment for an instance of Fragment 2 (see code below).
Next, Fragment.onCreateView() is called on Fragment 2 instance and the fragment layout gets inflated.
Here is the problem. Android Calls onCreate() and then onCreateView() on the fragment instance ONCE AGAIN, which produces the exception when I try to inflate (a second time) the layout.
Obviously the problem is Android is initializing the fragment twice, but I don't know why.
I tried NOT selecting the second tab when I reaload the activity but the second fragment gets initialized anyway and it is not shown (since I didn't select its tab).
I found this question: Android Fragments recreated on orientation change
The user asks basically the same I do, but I don't like the chosen answer (it's only a workaroud). There must be some way to get this working without the android:configChanges trick.
In case it's not clear, what I want to know how whether to prevent the recreation of the fragment or to avoid the double initialization of it. It would be nice to know why is this happening also. :P
Here is the relevant code:
public class MyActivity extends Activity implements ActionBar.TabListener {
private static final String TAG_FRAGMENT_1 = "frag1";
private static final String TAG_FRAGMENT_2 = "frag2";
private static final String TAG_FRAGMENT_3 = "frag3";
Fragment frag1;
Fragment frag2;
Fragment frag3;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// my_layout contains a FragmentLayout inside
setContentView(R.layout.my_layout);
// Get a reference to the fragments created automatically by Android
// when reloading the activity
FragmentManager fm = getFragmentManager();
this.frag1 = fm.findFragmentByTag(MyActivity.TAG_FRAGMENT_1);
this.frag2 = fm.findFragmentByTag(MyActivity.TAG_FRAGMENT_2);
this.frag3 = fm.findFragmentByTag(MyActivity.TAG_FRAGMENT_3)
ActionBar actionBar = getActionBar();
// snip...
// This triggers onTabSelected for the first tab
actionBar.addTab(actionBar.newTab()
.setText("Tab1").setTabListener(this)
.setTag(MyActivity.TAG_FRAGMENT_1));
actionBar.addTab(actionBar.newTab()
.setText("Tab2").setTabListener(this)
.setTag(MyActivity.TAG_FRAGMENT_2));
actionBar.addTab(actionBar.newTab()
.setText("Tab3").setTabListener(this)
.setTag(MyActivity.TAG_FRAGMENT_3));
Tab t = null;
// here I get a reference to the tab that must be selected
// snip...
// This triggers onTabUnselected/onTabSelected
ab.selectTab(t);
}
#Override
protected void onDestroy() {
// Not sure if this is necessary
this.frag1 = null;
this.frag2 = null;
this.frag3 = null;
super.onDestroy();
}
#Override
public void onTabSelected(Tab tab, FragmentTransaction ft) {
Fragment curFrag = getFragmentInstanceForTag(tab.getTag().toString());
if (curFrag == null) {
curFrag = createFragmentInstanceForTag(tab.getTag().toString());
if(curFrag == null) {
// snip...
return;
}
}
ft.replace(R.id.fragment_container, curFrag, tab.getTag().toString());
}
#Override
public void onTabUnselected(Tab tab, FragmentTransaction ft)
{
Fragment curFrag = getFragmentInstanceForTag(tab.getTag().toString());
if (curFrag == null) {
// snip...
return;
}
ft.remove(curFrag);
}
private Fragment getFragmentInstanceForTag(String tag)
{
// Returns this.frag1, this.frag2 or this.frag3
// depending on which tag was passed as parameter
}
private Fragment createFragmentInstanceForTag(String tag)
{
// Returns a new instance of the fragment requested by tag
// and assigns it to this.frag1, this.frag2 or this.frag3
}
}
The code for the Fragment is irrelevant, it just returns an inflated view on onCreateView() method override.
I got a simple answer for that:
Just add setRetainInstance(true); to the Fragment's onAttach(Activity activity) or onActivityCreated(Bundle savedInstanceState).
These two are call-backs in the Fragment Class.
So basically, what setRetainInstance(true) does is:
It maintains the state of your fragment as it is, when it goes through:
onPause();
onStop();
It maintains the instance of the Fragment no matter what the Activity goes through.
The problem with it could be, if there are too many Fragments, it may put a strain on the System.
Hope it helps.
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
setRetainInstance(true);
}
Open for Correction as always. Regards, Edward Quixote.
It seems that, when the screen is rotated and the app restarted, it is recreating each Fragment by calling the default constructor for the Fragment's class.
I have encountered the same issue and used the following workaround:
in the fragment's onCreateView begining of:
if (mView != null) {
// Log.w(TAG, "Fragment initialized again");
((ViewGroup) mView.getParent()).removeView(mView);
return mView;
}
// normal onCreateView
mView = inflater.inflate(R.layout...)
I think this is a fool proof way to avoid re-inflating of the root view of the fragment:
private WeakReference<View> mRootView;
private LayoutInflater mInflater;
/**
* inflate the fragment layout , or use a previous one if already stored <br/>
* WARNING: do not use in any function other than onCreateView
* */
private View inflateRootView() {
View rootView = mRootView == null ? null : mRootView.get();
if (rootView != null) {
final ViewParent parent = rootView.getParent();
if (parent != null && parent instanceof ViewGroup)
((ViewGroup) parent).removeView(rootView);
return rootView;
}
rootView = mFadingHelper.createView(mInflater);
mRootView = new WeakReference<View>(rootView);
return rootView;
}
#Override
public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) {
mInflater=inflater!=null?inflater:LayoutInflater.from(getActivity());
final View view = inflateRootView();
... //update your data on the views if needed
}
add
android:configChanges="orientation|screenSize"
in the manifest file
To protect activity recreate try to add configChanges in your Activity tag (in manifest), like:
android:configChanges="keyboardHidden|orientation|screenSize"
My code was a little different, but I believe our problem is the same.
In the onTabSelected I didn't use replace, I use add when is the first time creating the fragment and attach if isn't. In the onTabUnselected I use detach.
The problem is that when the view is destroyed, my Fragment was attached to the FragmentManager and never destroyed. To solve that I implemented on the onSaveInstanceBundle to detach the fragment from the FragmentManager.
The code was something like that:
FragmentTransition ft = getSupportFragmentManager().begin();
ft.detach(myFragment);
ft.commit();
In the first try I put that code in the onDestroy, but I get a exception telling me that I couldn't do it after the onSaveInstanceBundle, so I moved the code to the onSaveInstanceBundle and everything worked.
Sorry but the place where I work don't allow me to put the code here on StackOverflow. This is what I remember from the code. Feel free to edit the answer to add the code.
I think you are facing what I faced. I had a thread downloader for json which starts in onCreate() , each time I changed the orientation the thread is called and download is fired. I fixed this using onSaveInstance() and onRestoreInstance() to pass the json response in a list, in combination of checking if the list is not empty, so the extra download is not needed.
I hope this gives you a hint.
I solved this problem by using below code.
private void loadFragment(){
LogUtil.l(TAG,"loadFragment",true);
fm = getSupportFragmentManager();
Fragment hf = fm.findFragmentByTag("HOME");
Fragment sf = fm.findFragmentByTag("SETTING");
if(hf==null) {
homeFragment = getHomeFragment();// new HomeFragment();
settingsFragment = getSettingsFragment();// new Fragment();
fm.beginTransaction().add(R.id.fm_place, settingsFragment, "SETTING").hide(settingsFragment).commit();
fm.beginTransaction().add(R.id.fm_place, homeFragment, "HOME").commit();
activeFragment = homeFragment;
}else{
homeFragment = hf;
settingsFragment = sf;
activeFragment = sf;
}
}
Initiate this method in OnCreate();

Categories

Resources