How to properly remove retained instance Fragment - android

Currently, I would like to retain an expensive data structure, during configuration changes. I choose not to use Bundle to handle it, as the expensive data structure is not parcelable.
Hence, I use a non-UI Fragment (Called it RetainInstanceFragment), with its setRetainInstance(true) to hold the data structure.
public class RetainInstanceFragment extends Fragment {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Creating expensive data structure
expensiveDataStructure = CreateExpensiveDataStructure();
// Tell the framework to try to keep this fragment around
// during a configuration change.
setRetainInstance(true);
}
public ExpensiveDataStructure expensiveDataStructure = null;
}
An UI Fragment (Called it UIFragment) will get the expensive data structure from RetainInstanceFragment. Whenever there is configuration changes on UIFragment, UIFragment will always try to get the "cached" RetainInstanceFragment from FragmentManager, before it decides to create a new RetainInstanceFragment.
Example code is as follow.
public class UIFragment extends SherlockListFragment
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
FragmentManager fm = getFragmentManager();
// Check to see if we have retained the worker fragment.
retainInstanceFragment = (RetainInstanceFragment)fm.findFragmentByTag("data");
// If not retained (or first time running), we need to create it.
if (retainInstanceFragment == null) {
retainInstanceFragment = new RetainInstanceFragment();
fm.beginTransaction().add(watchlistArrayFragment, "data").commit();
} else {
// We can re-use retainInstanceFragment.expensiveDataStructure even
// after configuration change.
}
}
}
However, there's a problem. Whenever I destroy my old UIFragment, and replace it with new UIFragment, I expect old RetainInstanceFragment will be destroyed as well. Here is how I destroy and create new UIFragment
public class MyFragmentActivity extends SlidingFragmentActivity
// Being triggered when there is different menu item in sliding menu being
// selected.
public void selectActiveContent(Country country) {
Fragment fragment = new UIFragment(country);
getSupportFragmentManager().beginTransaction().replace(R.id.content, fragment).commitAllowingStateLoss();
}
But old RetainInstanceFragment is never destroyed.
My guess is, perhaps I forget to perform clean up in UIFragment. Hence, I add the following code
UIFragment
#Override
public void onDetach() {
super.onDetach();
// To differentiate whether this is a configuration changes, or we are
// removing away this fragment?
if (this.isRemoving()) {
FragmentManager fm = getFragmentManager();
fm.beginTransaction().remove(retainInstanceFragment).commit();
}
}
However, it doesn't work all the time. I perform several sliding menu clicks.
1. selectActiveContent() -> Create new UIFragment and new RetainInstanceFragment
2. selectActiveContent() -> Create new UIFragment, but re-use previous RetainInstanceFragment. (Wrong behavior)
3. selectActiveContent() -> Create new UIFragment, and new RetainInstanceFragment.
4. selectActiveContent() -> Create new UIFragment, but re-use previous RetainInstanceFragment. (Wrong behavior)
Any idea how I can properly remove retained instance Fragment?

As suggested by #Luksprog, the following method works. However, it still do not explain why the previous cleanup done through onDetach doesn't work. If anyone can explain why this solution works and previous doesn't, I would be very thankful. :)
UIFragment
#Override
public void onDetach() {
super.onDetach();
}
public void cleanupRetainInstanceFragment() {
FragmentManager fm = getFragmentManager();
fm.beginTransaction().remove(this.retainInstanceFragment).commit();
}
MyFragmentActivity
public class MyFragmentActivity extends SlidingFragmentActivity
// Being triggered when there is different menu item in sliding menu being
// selected.
public void selectActiveContent(Country country) {
// *******************************************
// Solution suggested by #Luksprog. It works!
// But I have no idea why it works and previous doesn't work...
// *******************************************
Fragment oldFragment = getSupportFragmentManager().findFragmentById(R.id.content);
if (oldFragment instanceof UIFragment) {
((UIFragment)oldFragment).cleanupRetainInstanceFragment();
}
Fragment fragment = new UIFragment(country);
getSupportFragmentManager().beginTransaction().replace(R.id.content, fragment).commitAllowingStateLoss();
}
(Edited) Useful comment by #Luksprog
The fragment transactions are not made right away. My assumption was
that doing that transaction in the onDetach() callback will not remove
the retain fragment instance before the UI fragment's replace
transaction finished and so your new UI fragment will still see the
retain fragment instance still available, so it will not create a new
one. Your previous method is not in the spirit of the fragments
framework where fragments are unaware of other fragments and the
activity manages all of them as it knows more about the overall
application state.

I think you can just remove the fragment from fragment transaction.
if (mWorkFragment != null) {
fm.beginTransaction().remove(mWorkFragment).commitAllowingStateLoss();
}

Related

Recovering presenters for the ViewPager fragments (MVP)

I'm trying to refactor an existing application to use the MVP architecture. One of the activities has a ViewPager with three fragments. Each fragment is linked with a presenter. To be precise - each presenter, when created, is given a View to work with, i.e. a Fragment. For now, I'm creating these presenters inside the ViewPager's adapter - specifically in the getItem(int position) method.
Fragment fragment = FirstFragment.newInstance();
FirstPresenter presenter = new FirstPresenter(repo, (FirstContract.View) fragment, projectId, userId);
The problem I'm facing is if the process is killed and then restarted, ViewPager has its own lifecycle and therefore getItem is not called again - the fragments are recreated automagically with no presenters.
Is there a known solution to this problem?
As there's still no ideal answer to this question, I thought it might be good to share my interim solution.
As I've mentioned in one of the comments, the goal here is to recover ViewPager from process kill and ideally keep the Presenter initialisation decoupled from the View. For now, my solution is to override restoreState(Parcelable state, ClassLoader loader) inside the FragmentStatePagerAdapter, inspect the state Parcelable similar to the actual implementation of the restoreState method, then for each fragment of a certain class, I can initialise a presenter and assign it a view.
#Override
public void restoreState(Parcelable state, ClassLoader loader) {
if (state != null) {
Bundle bundle = (Bundle)state;
bundle.setClassLoader(loader);
Iterable<String> keys = bundle.keySet();
for (String key: keys) {
if (key.startsWith("f")) {
Fragment f = mFragmentManager.getFragment(bundle, key);
if (f != null) {
if (f instanceof FirstFragment) {
new FirstPresenter(repo, (FirstContract.View) f, projectId, userId);
}
} else {
Log.w(TAG, ".restoreState() - bad fragment at key " + key);
}
}
}
}
super.restoreState(state, loader);
}
As mentioned in comments - Presenter must be attached (and detached) in Activity/Fragment lifecycle methods. Not in external classes because only View can manage to attach-detach Presenter at appropriate time. But it's a good practice to initilize Presenter in separate class (or dependency injection framework) to decouple it from View.
The suggested answer didn't work for me since mFragmentManager is a private member of FragmentStatePagerAdapter. No idea how it worked for vkislicins. Instead, I just called got the parent class to do restoreState then grabbed the fragments with 'instantiateItem'. For example:
#Override
public void restoreState(Parcelable state, ClassLoader loader) {
// this will load all the fragments again
super.restoreState(state, loader);
// since the fragments are now loaded, instantiate can be used because it just returns them
MyFragmentClass tab1 = (MyFragmentClass) instantiateItem(null, 0);
tab1Presenter.setView(tab1);
tab1.setPresenter(tab1Presenter);
// then just do the same for the other fragments
...
}
Feels a bit hacky, but it works.
First of all, my solution includes FragmentManager.FragmentLifecycleCallbacks, which is a
Callback interface for listening to fragment state changes that happen within a given FragmentManager
and sticks with the separation of concerns, in a way that's shown in the Android Architecture Blueprints, I'd say.
Activity creates Presenter, passing along View/ Fragment, so that
Presenter knows its View and furthermore sets itself its Presenter
In Activity's onCreate I register a FragmentLifecycleCallbacks listener by calling this
private void registerFragmentsLifecycleListener() {
// All registered callbacks will be automatically unregistered when
// this FragmentManager is destroyed.
getSupportFragmentManager.registerFragmentLifecycleCallbacks(
new FragmentManager.FragmentLifecycleCallbacks() {
// Called after the fragment has returned from its onActivityCreated
#Override
public void onFragmentActivityCreated(FragmentManager fm, Fragment f,
Bundle savedInstanceState) {
createPresenter(f);
}
}, false); // true to register callback for all child FragmentManagers
}
The listener gets notified after the Fragment has returned from its onActivityCreated to make sure, that only for each new Fragment instance added by the ViewPager a new Presenter will be created. The fragment could get attached/detached, its view could be created/destroyed a couple of times, nothing needed to be done, still got its Presenter.
Because in case of recreation (e.g. by rotation) the Fragments' onCreate is called before the Activitys one (where the FragmentLifecycleCallbacks listener is registered!), the listener couldn't implement onFragmentCreated, it has to be onFragmentActivityCreated.
For the given new Fragment instance we can then determine which Presenter is needed:
private void createPresenter(Fragment fragment) {
if (fragment instanceof WhateverContract.View) {
WhateverContract.Presenter whateverPresenter =
new WhateverPresenter((WhateverContract.View) fragment);
} else if (...){}
}
The Presenter connects with its View/Fragment in the constructor
private final WhateverContract.View mView;
public WhateverPresenter(#NonNull WhateverContract.View view) {
mView = checkNotNull(view, "view cannot be null!");
mView.setPresenter(this);
}
and can then be started in the Fragments onResume.
If there's something wrong or to improve, please let me know :)

Is calling setTargetFragment on headless fragment no longer necessary during UI fragment re-creation?

It is quite common to use headless fragment technique (fragment with setRetainedInstance(true)), to let long running task deal with UI updating.
https://stackoverflow.com/a/12303649/72437
public class UIFragment extends Fragment {
#Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
headlessFragment = (HeadlessFragment)fm.findFragmentByTag(HEADLESS_FRAGMENT);
if (headlessFragment == null) {
// This happens during UIFragment first-time creation.
headlessFragment = HeadlessFragment.newInstance();
headlessFragment.setTargetFragment(this, 0);
fm.beginTransaction().add(headlessFragment , HEADLESS_FRAGMENT).commitAllowingStateLoss();
} else {
// UIFragment is having configuration change.
// UIFragment is being re-created.
// Make sure headlessFragment is having latest instance of UIFragment.
headlessFragment.setTargetFragment(this, 0);
}
}
}
I'm using support library.
Many years back, when UIFragment re-creation happen, the following code snippet is essential, to ensure headless fragment is having reference to latest UIFragment instance.
// UIFragment is having configuration change.
// UIFragment is being re-created.
// Make sure headlessFragment is having latest instance of UIFragment.
headlessFragment.setTargetFragment(this, 0);
However, today when I re-test the code, using latest support library v7:25.0.0, I realize such code isn't necessary anymore. I can wrote it in
public class UIFragment extends Fragment {
#Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
headlessFragment = (HeadlessFragment)fm.findFragmentByTag(HEADLESS_FRAGMENT);
if (headlessFragment == null) {
// This happens during UIFragment first-time creation.
headlessFragment = HeadlessFragment.newInstance();
headlessFragment.setTargetFragment(this, 0);
fm.beginTransaction().add(headlessFragment , HEADLESS_FRAGMENT).commitAllowingStateLoss();
} else {
// UIFragment is being created.
//
// Event without calling headlessFragment.setTargetFragment,
// headlessFragment.getTargetFragment will *magically*
// return latest instance of UIFragment.
}
}
}
First, I suspect, in latest support library, the headlessFragment.setTargetFragment is being called automatically during UIFragment re-creation.
However, when I try to confirm, by overwriting headlessFragment.setTargetFragment with some logging, I realize it is not being called during UIFragment re-creation.
May I know, how does headlessFragment able to get the latest re-created UIFragment, magically?

Is this the correct way to programmatically invoke a Fragment?

My code:
public class MainActivity extends AppCompatActivity {
private FragmentA fragmentA;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (savedInstanceState == null) {
fragmentA = FragmentA.newInstance();
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.fragment_a_container, fragmentA, "FRAGMENT_A");
fragmentTransaction.commit();
}
else {
fragmentA = (FragmentA) getSupportFragmentManager().findFragmentByTag("FRAGMENT_A");
}
}
}
I don't really know what I am doing but this is currently what I do. I define a container for the Fragment and then I use a FragmentTransaction to replace it with a Fragment. The part I am confused about though is the else statement.
Should I be structuring this differently?
I thought configuration changes wiped out Activities and Fragments so why check for the Fragment in some support manager? Does this mean Fragments don't actually get destroyed? At the same time, they DO seem to get destroyed because they appear to reset unless I use onSaveInstanceState or the getArguments() approach.
Edit: What's wrong with doing this:
public class MainActivity extends AppCompatActivity {
private FragmentA fragmentA;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
fragmentA = FragmentA.newInstance();
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
fragmentTransaction.replace(R.id.fragment_a_container, fragmentA, "FRAGMENT_A");
fragmentTransaction.commit();
}
}
They do get destroyed and recreated for you on configuration changes by the, in this case, SupportFragmentManager.
To answer your questions:
Should I be structuring this differently?
No, that's exactly how you should create fragments if there is no saved state and retrieve them when there is. See also my answer here;
a) so why check for the Fragment in some support manager?
Because the manager handles the lifecyle of the fragment for you when there is a configuration change.
b) Does this mean Fragments don't actually get destroyed?
No, it does get destroyed. See this diagram for a reference.
Edit to answer some of your questions from the comments:
But any member variables inside that Fragment are completely lost on configuration change unless I save them in that Fragment's onSaveInstanceState, right?
That is correct. Because your fragment is being destroyed, everything not being saved on onSaveInstanceState gets lost.
So then what exactly am I restoring?
You are not restoring anything. You are only retrieving the reference to the fragment that was previously created. You restore your variables on the onRestoreInstanceState() method of your fragment.
What's wrong with doing this (the code from the edit in the question)?
If you do that, you are adding a new fragment instance to the R.id.fragment_a_container container. So the old fragment will get lost together with the state of it you saved on onSaveInstanceState(). It will be a new fragment, with new information in it and the event onRestoreInstanceState() won't be called for it.

Pattern for Activity / Fragment in android

I've a activity which basically is :
public class FragmentContainer extends FragmentActivityBase implements IRefreshListener {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getIntent().getExtras() == null
|| getIntent().getExtras().get("type") == null) {
showProductList();
}
else
{
if (getIntent().getExtras().get("type").equals("customer"))
showCustomerList();
}
#Override
public void showProductList() {
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager
.beginTransaction();
// load the product list
ProductList fragment = new ProductList();
fragmentTransaction.replace(R.id.fragment_container, fragment)
.addToBackStack(null);
fragmentTransaction.commit();
}
.....
}
in the fragment, I use onCreateView to get intent and then I create my view.
If I need to change the fragment, I get the reference to the parent Activity (taken from onAttach) and I call method referenced by the IRefreshListener.
like :
IRefreshListener mCallback;
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
// This makes sure that the container activity has implemented
// the callback interface. If not, it throws an exception.
try {
mCallback = (IRefreshListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString()
+ " must implement IRefreshListener");
}
}
public void callCustomer() {
mCallback.showCustomerList();
}
It works but whne I change the orientation, even I use setRetainInstance(true) it will be reseted.
I have 2 questions :
Do I use the good pattern to manage my application. The big activity which contains one fragment become bigger with the time
How should I handle orientation change ?
Regards
I do not find this pattern is more perfect or best one, although it is or was a suggestion from Google. Because it could be a worse coding style if fragment knows particular activity or listeners, you might write more and more code, when you wanna to let your fragment know more its "container" or "parents". Will the fragment later be used for other activity which has not been implemented with IRefreshListener etc, you will code much more.
My introduce is using Otto-Bus or Event-Bus. You can just send message from one to one. Every one doesn't have to know each other.

Android managing fragments from activity elegantly

Description of what I'm trying to accomplish:
I have an app that uses a FragmentActivity with a LinearLayout as a container for the fragments. I click different buttons on the FragmentActivity UI and add and remove Fragments to the container in the FragmentActivity. In addition to clicking buttons on the FragmentActivity UI, each Fragment also has buttons that can be clicked which will remove the current fragment and add a different fragment in its place.
The Android way of doing things as I understand it:
I have been reading up on how to do this and as I understand it, the 'proper' way of doing things is to use the FragmentActivity as sort of a relay station and have each fragment do callbacks to the FragmentActivity to communicate events and deal with them.
Scenario:
So let's say that the FragmentActivity is displaying Fragment A and when the user clicks a button in FragmentA I want to stop showing FragmentA and start showing FragmentB. To do this I have created an interface in FragmentA called AListener. In the onAttach() method of FragmentA I use the suggested method of checking that the FragmentActivity implements AListener. When the button in FragmentA is clicked I use one of the callback methods from AListener to communicate the click event to the FragmentActivity. In the FragmentActivity I create an instance of FragmentB and add it to the container in FragmentActivity. Then if some event happens in FragmentB I use the same scheme to communicate the event to the FragmentActivity and do something interesting.
So what's the problem?
For my application I have found this scheme of having Fragments call back to the FragmentActivity and then having the FragmentActivity create a new fragment or call forward to and existing fragment very cumbersome. I have many fragments that need to be displayed by the FragmentActivity and therefore I am implementing an interface for every type of fragment that needs to be displayed (Each fragment is different so they each have their own interface). This causes clashes when I have two interfaces that have the same method signatures and I'm forced to rename one of the methods.
For instance, if I want to attach a listener to a fragment using the onAttach() method of the fragment, then my FragmentActivity must implement the interface. I have found several instances where I have callback methods that have the same name (or I'm forced to name them something similar but different because of a namespace collision). One solution to this would be to use an anonymous classes as callbacks instead of having the FragmentActivity implement the interface. This seems to work well enough, but goes against what the Android documentation says about using the onAttach() method to set the listener.
Are there any elegant ways to approach this problem? It seems to me the tradeoff is that you either force the FragmentActivity to implement an interface for each Fragment that you want to display in it and have the fun problem of watching out for method signature collisions, or you go against the Android documentation and use Anonymous classes to handle the callbacks (not sure of the implications of this).
I am fairly new to Java and feel like I could be missing a concept here that would solve my problem. Can anyone set me straight on how to solve this problem elegantly?
I completely understand your problem since i was dealing it for a long time. Here is the solution i came up right now! It may need some modification based on your need but i it works well.
first of all to to make communicating of event easier in your app use an EventBus! here is the most famous one https://goo.gl/nAEW6
event bus allows you to send event from anywhere to anywhere without need to worry about implementing interfaces, broadcast receivers, threading, etc.
Then add FragmentOrganizer to your app. It's a base class for all of your Fragment Organizers. basically you need one for each activity. Here is the code
public abstract class FragmentOrganizer {
protected FragmentManager fragmentManager;
public FragmentOrganizer(FragmentManager fragmentManager) {
this.fragmentManager = fragmentManager;
openFragment(getInitialFragment());
EventBus.getDefault().register(this);
}
protected abstract Fragment getInitialFragment();
protected abstract void onEvent(Object event);
public abstract boolean handleBackNavigation();
public void freeUpResources(){
EventBus.getDefault().unregister(this);
}
protected Fragment getOpenFragment(){
String tag = fragmentManager.getBackStackEntryAt(fragmentManager.getBackStackEntryCount() -1).getName();
return fragmentManager.findFragmentByTag(tag);
}
protected boolean isFragmentOpen(Fragment fragment){
return isFragmentOpen(fragment, true);
}
protected boolean isFragmentOpen(Fragment fragment, boolean useArgs){
String fragmentTag = createFragmentTag(fragment, useArgs);
if (fragmentManager.getBackStackEntryCount() != 0) {
String name = fragmentManager.getBackStackEntryAt(fragmentManager.getBackStackEntryCount() - 1).getName();
if(!useArgs)
name = name.substring(0, name.indexOf("-"));
return name.equals(fragmentTag);
}
return false;
}
private String createFragmentTag(Fragment fragment, boolean addArgs) {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(fragment.getClass().getSimpleName());
if(addArgs) {
stringBuilder.append("-");
if (fragment.getArguments() != null)
stringBuilder.append(fragment.getArguments().toString());
}
return stringBuilder.toString();
}
public void openFragment(Fragment fragment) {
if(isFragmentOpen(fragment))
return;
String fragmentTag = createFragmentTag(fragment, true);
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.replace(R.id.activity_main_fragment_container, fragment, fragmentTag);
transaction.addToBackStack(fragmentTag).commit();
}
}
Now you need to create your fragment organizer that inherit from FragmentOrganizer and implements 3 required methods. here the sample
public class MainFragmentOrganizer extends FragmentOrganizer {
public MainFragmentOrganizer(FragmentManager fragmentManager) {
super(fragmentManager);
}
#Override
protected Fragment getInitialFragment() {
return HomeFragment.newInstance();
}
#Override
public void onEvent(Object event){
if(event instanceof ClickedOnPhotoEvent){
String photoCode = ((ClickedOnPhotoEvent) event).photoCode;
openFragment(PhotoFragment.newInstance(photoCode));
}
}
#Override
public boolean handleBackNavigation(){
Fragment fragment = getOpenFragment();
if (fragment instanceof HomeFragment){
return false;
} else {
fragmentManager.popBackStack();
return true;
}
}
}
And in your activity you just need to insatiate your FragmentManager and let it do the magic!
fragmentManager = getSupportFragmentManager();
fragmentOrganizer = new MainFragmentOrganizer(getSupportFragmentManager());
#Override
public void onBackPressed() {
//first let fragment organizer handle back. If it does not activity takes cares of it!
if(!fragmentOrganizer.handleBackNavigation()){
finish();
}
}
#Override
protected void onDestroy() {
fragmentOrganizer.freeUpResources();
super.onDestroy();
}
It may seem a lot of code but as you see most of the code encapsulated in FragmentOrganizer base class and it does all the general works so you just have to copy this file from one project to another.
As i said in the beginning i just came up with this solution right now, so it may not be perfect. I Plan to use this in my next project i hope you do to. And if you do i really appritiate if you share your though. have a good time
A co-worker of mine came up with what I consider an elegant solution to this problem.
Remember, what we're trying to achieve is a way for fragments to callback to the parent activity without having the activity implement the interface. Also, we need to be able to automatically set the listener again if the activity is destroyed and then recreated.
Activities have a lifecycle callback called onAttachFragment(Fragment fragment) which is called whenever a fragment is being attached to the activity. So, for instance, when a new fragment is created within the activity, this gets called. It also gets called if an activity that was previously destroyed gets recreated. What you can do is use an interface or an anonymous class to set a listener on the new fragment in onAttachFragment like this:
#Override
public void onAttachFragment(Fragment fragment) {
super.onAttachFragment(fragment);
//Determine which fragment this is by checking its tag
if(fragment.getTag().contains(TextFrag.FRAG_TAG)){
//set a listener on this fragment using an anonymous class or interface
((TextFrag)fragment).setListener(new TextFragButtonListener() {
#Override
public void onButtonClicked() {
count++;
counterTV.setText(String.valueOf(count));
}
});
}
}
Using this technique we are able to avoid the activity having to implement an interface for the callback and thus we avoid any naming conflicts with our callback methods. Also, if the activity is destroyed, once it is recreated the listener will be automatically reset so our callbacks will still work.
There are probably many other ways to do this and I'd love to here anyone's criticisms of this technique and suggestions for any other techniques.

Categories

Resources