Mortar + Flow with third party libraries hooked to

2019-03-08 06:40发布

问题:

Some third party libraries use hooks into the activity lifecycle to work correctly - for instance, the Facebook SDK (https://developers.facebook.com/docs/android/login-with-facebook/).

I'm having some trouble figuring out how to reconcile this model cleanly with a single-activity flow+mortar setup.

For instance, if I want to use Facebook login as part of a login Flow (w/ FlowView/FlowOwner), but not otherwise in the activity, what's the smartest way to pull this off if you need hooks for that particular flow in onCreate, onResume, onPause, onDestroy, onSaveInstanceState, onActivityResult, etc?

It's not immediately obvious what the cleanest path is - create an observable for each lifecycle activity stage and subscribe the flow to it? Seems like that path quickly devolves to the same Android lifecycle if you're not careful. Is there a better way?

I love the single activity model, and I'd really like to keep everything managed by flow/mortar and not activities, if possible. Or am I thinking about this in a way that is fundamentally making it more difficult than it should be?

回答1:

We haven't had a need for start and stop so far, but do have a few spots that rely on pause and resume. We use an ActivityPresenter as you suggest, but avoid any kind of universal superclass. Instead it exposes a service that interested presenters can opt in to. This kind of hookup need is why the onEnterScope(Scope) method was added. Here's the code.

First, have the activity implement this interface:

/**
 * Implemented by {@link android.app.Activity} instances whose pause / resume state
 * is to be shared. The activity must call {@link PauseAndResumePresenter#activityPaused()}
 * and {@link PauseAndResumePresenter#activityResumed()} at the obvious times.
 */
public interface PauseAndResumeActivity {
  boolean isRunning();

  MortarScope getMortarScope();
}

And have it inject the presenter and make the appropriate calls:

private boolean resumed;
@Inject PauseAndResumePresenter pauseNarcPresenter;

@Override protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  pauseNarcPresenter.takeView(this);
}

@Override public boolean isRunning() {
  return resumed;
}

@Override protected void onResume() {
  super.onResume();
  resumed = true;
  pauseNarcPresenter.activityResumed();
}

@Override protected void onPause() {
  resumed = false;
  super.onPause();
  pauseNarcPresenter.activityPaused();
}

@Override protected void onDestroy() {
  pauseNarcPresenter.dropView(this);
  super.onDestroy();
}

Now interested parties can inject a registrar interface to opt-in to pause and resume calls, without subclassing anything.

/**
 * Provides means to listen for {@link android.app.Activity#onPause()} and {@link
 * android.app.Activity#onResume()}.
 */
public interface PauseAndResumeRegistrar {
  /**
   * <p>Registers a {@link PausesAndResumes} client for the duration of the given {@link
   * MortarScope}. This method is debounced, redundant calls are safe.
   *
   * <p>Calls {@link PausesAndResumes#onResume()} immediately if the host {@link
   * android.app.Activity} is currently running.
   */
  void register(MortarScope scope, PausesAndResumes listener);

  /** Returns {@code true} if called between resume and pause. {@code false} otherwise. */
  boolean isRunning();
}

Have the client presenter implement this interface:

/**
 * <p>Implemented by objects that need to know when the {@link android.app.Activity} pauses
 * and resumes. Sign up for service via {@link PauseAndResumeRegistrar#register(PausesAndResumes)}.
 *
 * <p>Registered objects will also be subscribed to the {@link com.squareup.otto.OttoBus}
 * only while the activity is running.
 */
public interface PausesAndResumes {
  void onResume();

  void onPause();
}

And hook things up like this. (Note that there is no need to unregister.)

private final PauseAndResumeRegistrar pauseAndResumeRegistrar;

@Inject
public Presenter(PauseAndResumeRegistrar pauseAndResumeRegistrar) {
  this.pauseAndResumeRegistrar = pauseAndResumeRegistrar;
}

@Override protected void onEnterScope(MortarScope scope) {
  pauseAndResumeRegistrar.register(scope, this);
}

@Override public void onResume() {
}

@Override public void onPause() {
}

Here's the presenter that the activity injects to make it all work.

/**
 * Presenter to be registered by the {@link PauseAndResumeActivity}.
 */
public class PauseAndResumePresenter extends Presenter<PauseAndResumeActivity>
    implements PauseAndResumeRegistrar {

  private final Set<Registration> registrations = new HashSet<>();

  PauseAndResumePresenter() {
  }

  @Override protected MortarScope extractScope(PauseAndResumeActivity view) {
    return view.getMortarScope();
  }

  @Override public void onExitScope() {
    registrations.clear();
  }

  @Override public void register(MortarScope scope, PausesAndResumes listener) {
    Registration registration = new Registration(listener);
    scope.register(registration);

    boolean added = registrations.add(registration);
    if (added && isRunning()) {
      listener.onResume();
    }
  }

  @Override public boolean isRunning() {
    return getView() != null && getView().isRunning();
  }

  public void activityPaused() {
    for (Registration registration : registrations) {
      registration.registrant.onPause();
    }
  }

  public void activityResumed() {
    for (Registration registration : registrations) {
      registration.registrant.onResume();
    }
  }

  private class Registration implements Scoped {
    final PausesAndResumes registrant;

    private Registration(PausesAndResumes registrant) {
      this.registrant = registrant;
    }

    @Override public void onEnterScope(MortarScope scope) {
    }

    @Override public void onExitScope() {
      registrations.remove(this);
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;

      Registration that = (Registration) o;

      return registrant.equals(that.registrant);
    }

    @Override
    public int hashCode() {
      return registrant.hashCode();
    }
  }
}


回答2:

So I've been porting a personal app over to flow and mortar to evaluate it for businesses use. I haven't encountered a scenario where I HAD to have the entire activity lifecycle yet, but as things stand with the current version of flow (v0.4) & mortar (v0.7), this is something I think you will have to creatively solve for yourself. I've recognized this as a potential problem for myself and have put some thought of how to overcome it:

I would also like to note that I haven't actually used the Facebook SDK. You will have to choose the best method for yourself.

  1. You could post events from the activity for each Activity life cycle event. You essentially mentioned this approach using RXJava's Observables. If you really really wanted to use RXJava, you could use a PublishSubject for this, but I'd probably go with simple events from an EventBus you could subscribe to. This is probably the easiest approach.
  2. You could also, depending on how the Facebook SDK works, possibly inject the Facebook SDK component in the activity, and from there initialize it. Then also inject the Facebook SDK component into your view to be used. Flow and Mortar's entire system is deeply integrated into dependency injection after all? This approach is also fairly simple, but depending on how the Facebook SDK works it probably isn't the best option. If you did go this route, you'd need to heed my warning at the bottom of this post.
  3. This brings us to my last idea. Square had a similar problem when they needed access to an Activity's ActionBar in it's sub-views/presenters. They exposed access to the ActionBar in their sample app via something they called the ActionBarOwner.java. They then implement the ActionBarOwner interface and give an instance of itself in the DemoActivity.java. If you study how they implemented this and share it through injection, you could create a similar class. AcivityLifecycleOwner or something (the name needs work), and you could subscribe to callbacks on it from a presenter. If you decide to go down this route, and aren't careful you can easily end up with a memory leak. Any time you would subscribe to any of the events (I'd recommend you subscribe in the presenter), you'd need to make sure you unsubscribe in the onDestroy method as well. I've created a short untested sample of what I mean for this solution below.

No matter which approach you use, you'll probably need to make sure your onCreate and onDestroy methods actually come from your presenter, and not the exact events from the activity. If you are only using the sdk on a single view, the activity's onCreate has been called long before your view is instantiated probably, and the onDestroy for the Activity will be called after your view is destroyed. The presenter's onLoad and onDestroy should suffice I think, however I haven't tested this.

Best of luck!

Untested code example for solution #3:

All your presenters could extend this class instead of ViewPresenter and then override each method you wanted events for just like you would in an activity:

public abstract class ActivityLifecycleViewPresenter<V extends View> extends ViewPresenter<V>
    implements ActivityLifecycleListener {

  @Inject ActivityLifecycleOwner mActivityLifecycleOwner;

  @Override protected void onLoad(Bundle savedInstanceState) {
    super.onLoad(savedInstanceState);
    mActivityLifecycleOwner.register(this);
  }

  @Override protected void onDestroy() {
    super.onDestroy();
    mActivityLifecycleOwner.unregister(this);
  }

  @Override public void onActivityResume() {
  }

  @Override public void onActivityPause() {
  }

  @Override public void onActivityStart() {
  }

  @Override public void onActivityStop() {
  }

}

Activity Lifecycle owner that would be injected into the activity and then hooked up to the corresponding events. I purposely didn't include onCreate and onDestroy, as you presenter's wouldn't be able to get access to those events as they wouldn't be created or they would already be destroyed. You'd need to use the presenters onLoad and onDestroy methods in place of those. It's also possible that some of these other events wouldn't be called.

public class ActivityLifecycleOwner implements ActivityLifecycleListener {

  private List<ActivityLifecycleListener> mRegisteredListeners
      = new ArrayList<ActivityLifecycleListener>();

  public void register(ActivityLifecycleListener listener) {
    mRegisteredListeners.add(listener);
  }

  public void unregister(ActivityLifecycleListener listener) {
    mRegisteredListeners.remove(listener);
  }

  @Override public void onActivityResume() {
    for (ActivityLifecycleListener c : mRegisteredListeners) {
      c.onActivityResume();
    }
  }

  @Override public void onActivityPause() {
    for (ActivityLifecycleListener c : mRegisteredListeners) {
      c.onActivityPause();
    }
  }

  @Override public void onActivityStart() {
    for (ActivityLifecycleListener c : mRegisteredListeners) {
      c.onActivityStart();
    }
  }

  @Override public void onActivityStop() {
    for (ActivityLifecycleListener c : mRegisteredListeners) {
      c.onActivityStop();
    }
  }
}

Now you need to hook the lifecycle owner to the activity:

public class ActivityLifecycleExample extends MortarActivity {

  @Inject ActivityLifecycleOwner mActivityLifecycleOwner;

  @Override protected void onResume() {
    super.onResume();
    mActivityLifecycleOwner.onActivityResume();
  }

  @Override protected void onPause() {
    super.onPause();
    mActivityLifecycleOwner.onActivityPause();
  }

  @Override protected void onStart() {
    super.onStart();
    mActivityLifecycleOwner.onActivityStart();
  }

  @Override protected void onStop() {
    super.onStart();
    mActivityLifecycleOwner.onActivityStop();
  }

}