11

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?

vkislicins
  • 3,331
  • 3
  • 32
  • 62
  • 1
    I think in android the view creates the Presenter, just because of the lifecycles of the activity/fragment. So you should remove the initialisation from the viewpager to the fragment itself – Drilon Blakqori Feb 14 '17 at 11:59
  • so manage the lifecycle of the presenters alongwith the fragment lifecycles – Amrut Bidri Feb 14 '17 at 13:04
  • Makes sense... However I would like to see if there are other ways around this, to decouple the presenter initialisation from the fragment – vkislicins Feb 15 '17 at 19:52

4 Answers4

4

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);
}
vkislicins
  • 3,331
  • 3
  • 32
  • 62
0

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.

Maksim Ostrovidov
  • 10,720
  • 8
  • 42
  • 57
  • Fair point. The problem still exists - how do I pass the initialised Presenter to the View when we're talking about a ViewPager. Cause I do want them to be decoupled and I don't like the idea of initialising a presenter inside a view. Also I kinda like the idea of the presenter being assigned a view, as specified in https://github.com/googlesamples/android-architecture – vkislicins Feb 15 '17 at 19:39
0

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.

dbdkmezz
  • 98
  • 1
  • 6
0

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 :)

outta comfort
  • 376
  • 2
  • 9