GithubHelp home page GithubHelp logo

square / mortar Goto Github PK

View Code? Open in Web Editor NEW
2.2K 105.0 160.0 884 KB

A simple library that makes it easy to pair thin views with dedicated controllers, isolated from most of the vagaries of the Activity life cycle.

License: Apache License 2.0

Java 100.00%

mortar's Introduction

Mortar

Deprecated

Mortar had a good run and served us well, but new use is strongly discouraged. The app suite at Square that drove its creation is in the process of replacing Mortar with Square Workflow.

What's a Mortar?

Mortar provides a simplified, composable overlay for the Android lifecycle, to aid in the use of Views as the modular unit of Android applications. It leverages Context#getSystemService to act as an a la carte supplier of services like dependency injection, bundle persistence, and whatever else your app needs to provide itself.

One of the most useful services Mortar can provide is its BundleService, which gives any View (or any object with access to the Activity context) safe access to the Activity lifecycle's persistence bundle. For fans of the Model View Presenter pattern, we provide a persisted Presenter class that builds on BundleService. Presenters are completely isolated from View concerns. They're particularly good at surviving configuration changes, weathering the storm as Android destroys your portrait Activity and Views and replaces them with landscape doppelgangers.

Mortar can similarly make Dagger ObjectGraphs (or Dagger2 Components) visible as system services. Or not โ€” these services are completely decoupled.

Everything is managed by MortarScope singletons, typically backing the top level Application and Activity contexts. You can also spawn your own shorter lived scopes to manage transient sessions, like the state of an object being built by a set of wizard screens.

These nested scopes can shadow the services provided by higher level scopes. For example, a Dagger extension graph specific to your wizard session can cover the one normally available, transparently to the wizard Views. Calls like ObjectGraphService.inject(getContext(), this) are now possible without considering which graph will do the injection.

The Big Picture

An application will typically have a singleton MortarScope instance. Its job is to serve as a delegate to the app's getSystemService method, something like:

public class MyApplication extends Application {
  private MortarScope rootScope;

  @Override public Object getSystemService(String name) {
    if (rootScope == null) rootScope = MortarScope.buildRootScope().build(getScopeName());

    return rootScope.hasService(name) ? rootScope.getService(name) : super.getSystemService(name);
  }
}

This exposes a single, core service, the scope itself. From the scope you can spawn child scopes, and you can register objects that implement the Scoped interface with it for setup and tear-down calls.

  • Scoped#onEnterScope(MortarScope)
  • Scoped#onExitScope(MortarScope)

To make a scope provide other services, like a Dagger ObjectGraph, you register them while building the scope. That would make our Application's getSystemService method look like this:

  @Override public Object getSystemService(String name) {
    if (rootScope == null) {
      rootScope = MortarScope.buildRootScope()
        .with(ObjectGraphService.SERVICE_NAME, ObjectGraph.create(new RootModule()))
        .build(getScopeName());
    }

    return rootScope.hasService(name) ? rootScope.getService(name) : super.getSystemService(name);
  }

Now any part of our app that has access to a Context can inject itself:

public class MyView extends LinearLayout {
  @Inject SomeService service;

  public MyView(Context context, AttributeSet attrs) {
    super(context, attrs);
    ObjectGraphService.inject(context, this);
  }
}

To take advantage of the BundleService describe above, you'll put similar code into your Activity. If it doesn't exist already, you'll build a sub-scope to back the Activity's getSystemService method, and while building it set up the BundleServiceRunner. You'll also notify the BundleServiceRunner each time onCreate and onSaveInstanceState are called, to make the persistence bundle available to the rest of the app.

public class MyActivity extends Activity {
  private MortarScope activityScope;

  @Override public Object getSystemService(String name) {
    MortarScope activityScope = MortarScope.findChild(getApplicationContext(), getScopeName());

    if (activityScope == null) {
      activityScope = MortarScope.buildChild(getApplicationContext()) //
          .withService(BundleServiceRunner.SERVICE_NAME, new BundleServiceRunner())
          .withService(HelloPresenter.class.getName(), new HelloPresenter())
          .build(getScopeName());
    }

    return activityScope.hasService(name) ? activityScope.getService(name)
        : super.getSystemService(name);
  }

  @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    BundleServiceRunner.getBundleServiceRunner(this).onCreate(savedInstanceState);
    setContentView(R.layout.main_view);
  }

  @Override protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    BundleServiceRunner.getBundleServiceRunner(this).onSaveInstanceState(outState);
  }
}

With that in place, any object in your app can sign up with the BundleService to save and restore its state. This is nice for views, since Bundles are less of a hassle than the Parcelable objects required by View#onSaveInstanceState, and a boon to any business objects in the rest of your app.

Download

Download the latest JAR or grab via Maven:

<dependency>
    <groupId>com.squareup.mortar</groupId>
    <artifactId>mortar</artifactId>
    <version>(insert latest version)</version>
</dependency>

Gradle:

compile 'com.squareup.mortar:mortar:(latest version)'

Full Disclosure

This stuff has been in "rapid" development over a pretty long gestation period, but is finally stabilizing. We don't expect drastic changes before cutting a 1.0 release, but we still cannot promise a stable API from release to release.

Mortar is a key component of multiple Square apps, including our flagship Square Register app.

License

Copyright 2013 Square, Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

mortar's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

mortar's Issues

Make dropView final

There is nothing you can do by subclassing that you can't do in other ways, especially if you always call takeView and dropView from the view side. See closing comment to #52.

Add onRegistered to Scoped

Problems

  1. When you deal directly with Bundlers, debouncing the load method is a real nuisance. (Presenters do it for you, isn't that nice?)
  2. The Scoped#onDestroy method name is a lie. There's nothing stopping you from re-registering with a new scope after the first one has been destroyed (doing so with a Presenter can be pretty convenient). For that matter, there's nothing stopping you from double-registering it (@loganj says there should be, but that's another conversation. And work). And that means you may well receive more than one call to onDestroy
  3. It's always struck me as weak that Scoped and Bundler instances have no access to the scope they're registered with, and as a hack that Presenters get access through whatever view takes them next.
  4. The lack of debouncing and lack of scope access makes it a real nuiscance for one Bundler to register another.

Proposal

  1. Add Scoped#onRegistered(MortarScope). It is called immediately the first time a Scoped is registered with a particular MortarScope, and that scope is passed in as an argument. This method is not called on re-regsitration.
  2. Replace Scoped#onDestroy() with Scoped#onScopeDestroyed(MortarScope)

Child scopes created during load need to be created in the LOADING state

  • ParentScope receives onCreate
    • it cycles through its registered bundlers, calling their onLoad methods
      • ParentPresenter can show children but isn't at the moment. In its onLoadโ€ฆ
        • it decides to show a child after all
        • creates ChildScope
        • inflates ChildView
          • ChildView finishes inflating and calls ChildPresenter.takeView
          • ChildPresenter is registered with ChildScope
          • ChildScope's loadingState is IDLE, so childPresenter#onLoad is called
    • now ParentScope makes recursive calls to its childrens' onCreateMethods
      • ChildScope calls childPresenter's onLoad method again

Getting two onLoad calls is hard for the new presenter to deal with, and I
think it's fair to call this a bug. I think fix is to create child scopes in
the same loading state as their parents. Or perhaps children should not have their
own loading state at all, but instead should share a single loading state with
their ancestors.

Popup and PopupPresenter aren't necessary, drop'em

There's really nothing you can do with them that you can't do more cleanly through the back stack. Generalized something like the following, make the sample app use it, and deprecate the Popups.

class BlenderScreen {
  transient boolean blendingConfirmed;
}

class ReallyBlendScreen {
  final BlenderScreen caller;
}

class BlenderScreenView {  
  BlenderScreen args = ...

  @OnClick(R.id.cancel) void cancel() {
     args.cancelConfirmed = false;
     flow.goTo(new ConfirmationScreen(args));
  }
}

class ReallyBlendScreenView {
  BlenderScreen args = ...

  @OnClick(R.id.confirm) void confirm() {
    args.caller.cancelConfirmed = true;
    flow.goBack();
  }
}

ConcurrentModificationException in sample chat on resume

I locked the screen while the chat was running. After some time I unlocked the screen again and got this exception (Android 4.2.2, CM 10.1):

   11-13 18:19:34.634    1789-1789/com.example.mortar E/AndroidRuntime๏น• FATAL EXCEPTION: main
java.lang.RuntimeException: Unable to resume activity {com.example.mortar/com.example.mortar.MainActivity}: java.util.ConcurrentModificationException
        at android.app.ActivityThread.performResumeActivity(ActivityThread.java:2870)
        at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:2899)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1289)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loop(Looper.java:137)
        at android.app.ActivityThread.main(ActivityThread.java:5227)
        at java.lang.reflect.Method.invokeNative(Native Method)
        at java.lang.reflect.Method.invoke(Method.java:511)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:795)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:562)
        at dalvik.system.NativeStart.main(Native Method)
 Caused by: java.util.ConcurrentModificationException
        at java.util.HashMap$HashIterator.nextEntry(HashMap.java:792)
        at java.util.HashMap$KeyIterator.next(HashMap.java:819)
        at mortar.RealActivityScope.onResume(RealActivityScope.java:54)
        at com.example.mortar.DemoBaseActivity.onResume(DemoBaseActivity.java:42)
        at android.app.Instrumentation.callActivityOnResume(Instrumentation.java:1185)
        at android.app.Activity.performResume(Activity.java:5182)
        at android.app.ActivityThread.performResumeActivity(ActivityThread.java:2860)

ย ย ย ย ย ย ย ย ย ย 

Consider requiring task id as part of Activity scope names

We ran into an issue where if you really whale on the app icon and the home button you can see a new task start and replace the current one. Activities in the outgoing task will have their onDestroy() method called only after their peers in the new task get created. Then you crash because the old activity has ended the scope that the new activity is trying to inject from.

The fix is simple, in our base Activity class:

  protected Blueprint getMortarBlueprint() {
    return new Blueprint() {
      @Override public String getMortarScopeName() {
        return SquareActivity.this.getMortarScopeName();
      }

      @Override public Object getDaggerModule() {
        return getModules();
      }
    };
  }

  protected String getMortarScopeName() {
    // If you really whale on the app icon and the home button you can see
    // a new task start and replace the current one. Activities in the
    // outgoing task will have their onDestroy() method called only after
    // their peers in the new task get created. Include the task id in the scope
    // name to stop them stomping each other. RA-1760
    return getClass().getName() + "-task-" + getTaskId();
  }

I'm not certain yet if this is specific to our use of activity config and the back stack, etc. I suspect not.

We should consider making requireActivityScope require the taskId and wrap the Blueprint so that it appends the taskId to the given name.

Scope was destroyed before onSaveInstanceState

Do you have any idea on what could cause the activity scope to be destroyed before the same activity performSaveInstanceState is called?

Crash:

java.lang.IllegalStateException: Scope com.couchsurfing.mobile.ui.setup.SetupActivityBlueprint was destroyed
       at mortar.RealScope.assertNotDead(RealScope.java:150)
       at mortar.RealActivityScope.onSaveInstanceState(RealActivityScope.java:93)
       at com.couchsurfing.mobile.ui.base.BaseActivity.onSaveInstanceState(BaseActivity.java:100)
       at android.app.Activity.performSaveInstanceState(Activity.java:1152)
       at android.app.Instrumentation.callActivityOnSaveInstanceState(Instrumentation.java:1223)
       at android.app.ActivityThread.performStopActivityInner(ActivityThread.java:3156)
       at android.app.ActivityThread.handleStopActivity(ActivityThread.java:3215)
       at android.app.ActivityThread.access$1000(ActivityThread.java:135)
       at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1424)
       at android.os.Handler.dispatchMessage(Handler.java:102)
       at android.os.Looper.loop(Looper.java:137)
       at android.app.ActivityThread.main(ActivityThread.java:4998)
       at java.lang.reflect.Method.invokeNative(Method.java)
       at java.lang.reflect.Method.invoke(Method.java:515)
       at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:777)
       at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:593)
       at dalvik.system.NativeStart.main(NativeStart.java)

takeView sooner; onLoad less

From a chat with @pyricau and @loganj :

Right now when you show subviews they don't show up until Activity#onResume, because we're encouraging developers to wait until onAttachedToWindow to call Presenter#takeView. This is inefficient (we're probably hitting onMeasure twice), leads to confusing flow, and makes it difficult to preserve Android's View#restoreHierarchyState mechanism.

Instead we should encourage developers to call takeView from onFinishInflate, as implied by the draft in #28. That should always be safe to do provided we discourage the habit of using tags and inflating from the view's constructor.

With this fixed, there is no longer a compelling reason to re-call Bundler#onLoad from from Activity#onResume, and redundant calls to onLoad should stop being so common. Apps that really need to know about pause and resume can provide a custom registration service tied to their activity (illustrate this in the sample app). We doubt that such a need is terribly common in real life.

Implications: views that are expected to be re-used will need to make redundant calls to Presenter#takeView from the otherwise despised #onAttachedToWindow. This means we need to armor Presenter#takeView against such usage, which means it must be final. But now that we can rely on onLoad not being called during onResume, the work that was happening in overrides of takeView (wiring up sub-presenters to sub-views) should be able to happen at the end of onLoad methods. The Presenter life cycle actually gets simpler. Boo-yah.

Okay, brave words. Let's see if it actually works.

See also square/flow#12

On-demand presenters do not survive process death

Set a phone to have only one or two background activities
Run the sample app
Go to the chat screen and show the End dialog
Hit home and go to other apps until the sample app process dies
Choose the sample app from the recent apps list
No popup

FlowOwnerView has a potential problem flowing from one screen to another

The sample app has a class FlowOwnerView which removes an old child view of a container FrameLayout and then adds a new child view (implementation in the FlowOwnerView.showScreen() method). In the sample app where each screen change is triggered only by some user action, the flow works fine.

However, when an old screen, due to some legitimate logic, needs to immediately switch to another screen, there is an out-of-order problem. For instance,

OldScreen.onLoad(...) {
    mFlow.replaceTo(new NewScreen());
}

When the view for the old screen is inflated, its onFinishInflate() method immediately calls its Presenter's onLoad(), which then triggers again the FlowOwnerView.showScreen() method, except that this time the old child view is not yet added into the container. The result is that both screens are added to the container in the wrong order.

onInflate() onInflate() crash

We have an exciting family of crashes where:

  • view inflation starts and lots of views are inflated, their presenters' takeView
    and onLoad methods are called
  • Android senses a configuration change and never attaches those views
    • so their onDetachedFromWindow methods are not called
    • so their Presenters' dropView methods are not called
  • the outgoing activity does call dropView for its own presenters
  • A new activity starts, finds the existing activity scope and calls its onCreate method,
    before it calls takeView on its own presenters
    • it tells previously registered bundlers to onLoad
    • some of them are in presenters that still have views from the old config, so
      they go ahead and call presenter onLoad

So now we have child presenters trying to load, and possibly asking their ancestor presenters to do stuff while the ancestors' views are still null. Crashes ensue.

@dnkoutso and I are pretty certain that the fix is simple: make presenters debounce their onLoad calls. It's already the case that Presenter#onLoad will not be called until takeView has been received. We should go further and make certain that onLoad will be called exactly once for any View instance.

We haven't thought yet of anything that this will break, and can think of lots of things that it will fix.

PreferencesFragment alternative example

The sample code needs an example for how to integrate a PreferencesFragment into the code, ideally while injecting something. This didn't look super straightforward with Mortar.

Or better yet, an alternative version that stays within the single activity model.

Need to be able to stop presenters

Presenter#takeView performs a registration that cannot be undone. If a long lived scope needs to have some presenters that come and go, it really can't.

I think that MortarScope#register needs to return a Registration or Subscription object with a void unsubsribe() method. And I think that Presenter needs a void stop() method that makes it undo the registration it sets up in takeView.

Formalize notion of child presenter

PopupPresenters and other child presenters are kind of a pain to do right. You have to remember to call their takeView method from onLoad, and override your dropview method to call theirs.

Could we provide something like:

interface FindChildView<C, V> {
  C getChildView(V parentView);
}

<C> void loadChildPresenter(Presenter<C> child, FindChildView<C, V> findView) { ... }

Where V is the parent view's view type. loadChildPresenter would be called at the end of the parent's onLoad method, where we now call the child's takeView; and the call to the child's dropView would be automatic.

e.g.:

public void onLoad(Bundle b) {
  super.onLoad(b);
  loadChildPresenter(fooPopupPresenter, new FindChildView<FooPopup, MyView>() {
      FooPopup getChildView(MyView parent) { return parent.getFooPopup(); }
    });
}

Cache of view in Mortar

Hi,
We use Mortar and Flow in our app. Currently, we want to speed up the view inflating between each screen, so we want to implement a cache of view. By doing so, we can avoid the inflating if we hit a cache of the view. However, we face some difficulty about dealing with life cycle of Mortar scope. I heard that you guys have implemented a cache in some apps. Could you give us some hints to help us? Any suggestion might be useful.
Many Thanks!

Destroying Activity scope after root scope recreation

I have a case where I need to reinitialise modules of the root scope after some user interaction. I do so by restarting my Actitivity and rebuilding the application scope:

public void restart() {
   // some configuration changes here
   Intent newApp = new Intent(app, MainActivity.class);
   newApp.setFlags(FLAG_ACTIVITY_CLEAR_TASK | FLAG_ACTIVITY_NEW_TASK);
   app.startActivity(newApp);
   app.rebuildOjectGraphAndInject();
}

public void rebuildOjectGraphAndInject() {
   Mortar.destroyRootScope(applicationScope);
   ObjectGraph objectGraph = ObjectGraph.create(Modules.list(this));
   applicationScope = Mortar.createRootScope(BuildConfig.DEBUG, objectGraph);
}

It does not work after the change where only parent scopes can destroy child scopes. Since my scope of the old activity is tied to the old root scope which has been reinitialised, I get an java.lang.IllegalArgumentException: ActivityScope was created by another scope in its onDestroy() even though is has been recursively destroyed by calling Mortar.destroyRootScope(applicationScope)

I work around it by having an additional check if the scope had already been destroyed using reflection on my onDestroy():

if (!configurationChangeIncoming) {
  if (!checkifDead(activityScope)) {
    MortarScope parentScope = Mortar.getScope(getApplication());
    parentScope.destroyChild(activityScope);
  }
  activityScope = null;
}

It's kind of an edgy case but could we only throw the IllegalArgumentException only if the Activity scope is not already dead and show a warning instead?

mortar & onDetachFromWindow

I gave a bit more thoughts to the "onDetachFromWindow" problem.

For a minute, let's leave aside:

  • the "android bugs" (abs list view posting performClick and canceling in onDetachFromWindow, EditText posting removal of cursor dialog and canceling that in onDetachFromWindow)
  • the question of "do you receive touch events between the time you remove the view from the parent and the time the exit animation ends?"

What we want to know is that either one of our parent views or ourselves as been removed from the view hierarchy, because as that point we want to stop calling back into the presenter.

There are two easy things we can do:

  1. Have views implement an OnDetachListener (ish) that provides a callback. When we swap out views, we go through the hierarchy of the view that's going away, and call that callback for views that implement it.

  2. For views that we swap in / out from a parent, we could have them implement HasPresenter. Then these views wouldn't have to call takeView / dropView themselves, it would be called by the view swapper: take view is called right after instantiating the view, and dropView is called after removing a view.

Just a thought. This needs to be tested, and would certainly require some special casing because we don't always do the same thing. But if most of our views have the same implementation code, some of that pain could be taken away, and it would actually improve the way things work.

ActionBarOwner.Config broken in 0.6 sample

Main#showScreen is called after the displayed screens Presenter#onLoad, so the ActionBarOwner.Config is overridden. The result is the END button is gone in the sample ChatScreen

Tablet Support: Proper way to have multiple view / presenter pairs on screen at once?

Lets say I have 2 screens A, and B. On phones I want screen A and screen B to be displayed separately, and I navigate to and from each screen. But on tablets I want to display both Screen A and B together.

I can think of a few ways of doing this, and i thought I'd ask if there was a best practice before I head off down a bad path and waste some time.

Example snippet for creating an authentication module

I'm trying to create a scope for objects that require an authenticated user. Basically in the same manner that is described in the readme.

Maybe a scope is associated with a particular view, or maybe it isn't. For example, an app might have a top level global scope that allows the injection of fundamental services; a child of that which manages objects that require an authenticated user; children of them for each Activity; and children of the activity scopes for their various screens and regions.

This issue however is that authentication requires access to the activity lifecycle (Facebook SDK) and should be run before FlowOwner presenter (from the sample). Any ideas on how to wire something like this up?

If this isn't the appropriate place to post let me know and I'll close and post on SO.

Thanks!

ViewPresenter with getView() != null and scope.isDestroyed() == true

In a ViewPresenter, we post a runnable on the UI thread just after onLoad(). We have a check when this runnable executes to make sure we are still on the current screen.

if (getView() == null){
    return;
}

However, we are tracking crashes that occur because the user switches screens quickly and this runnable executes after the scope has been destroyed. This means getView() is not null while the scope bas been destroyed. This does not seem like it should be possible, but we did solved it by modifying our check at the beginning of the Runnable:

if (getView() == null || scope.isDestroyed()){
    return;
}

FAQ entry: when to call takeView()

Archiving this here until we get a real FAQ togther.

tl:dr; If we need them, we effectively have two display phases: initial render (takeView call from onFinishInflate) and an update pass (ping your presenter from onAttachedToWindow)

Standard practic is for a View to call presenter#takeView() in onFinishInflate(). This allows the presenter to set up the view before layout and measurement, and keeps things beautifully synchronous. This is all you need 90% of the time.

But sometimes you'll have transient state that gets clobbered by Android's view hierarchy persistance, typically model state that changed behind the scenes while you were elsewhere in the backstack. When this happens and you're tempted to use View#post to fix them later, stop. Take a breath. And instead ping your presenter from onAttachedToWindow().

DialogPopup

Is useful. Add this:

public abstract class DialogPopup<D extends Parcelable, R> implements Popup<D, R> {
  private final Context context;

  private Dialog dialog;

  public DialogPopup(Context context) {
    this.context = context;
  }

  @Override public void show(D info, boolean withFlourish, PopupPresenter<D, R> presenter) {
    if (isShowing()) {
      /**
       * Sometimes you want to put the dialog right back up right as it is dismissed.
       * In such a case the outgoing dialog might not quite have been torn down yet. 
       * Help the process along.
       */
      dialog.dismiss();
      dialog = null;
    }
    dialog = createDialog(info, withFlourish, presenter);
    dialog.show();
  }

  @Override public boolean isShowing() {
    return dialog != null && dialog.isShowing();
  }

  @Override public void dismiss(boolean withFlourish) {
    dialog.dismiss();
    dialog = null;
  }

  @Override public Context getContext() {
    return context;
  }

  protected abstract Dialog createDialog(D info, boolean withFlourish,
      PopupPresenter<D, R> presenter);
}

Use ContextWrapper#attachBaseContext to reduce boilerplate?

Can we use ContextWrapper#attachBaseContext to make it easier to set up the Application and Activity scopes w/o requiring getSystemService boilerplate?

Remember that attachBaseContext is a one-shot method. Should make such a mechanism a convenience, not a requirement, so that we don't screw over apps that might be using that hook already.

Bundlers in child scopes should load after those in parent

In my onLoad, I register a ServiceBundler with my scope. Then I create a child scope (e.g. for use by show a child view) and register a child bundler with it (e.g. that view's presenter). The child bundler's onload should not execute until after the ServiceBundler's onload executes.

Right now, the child bundler registers eagerly, before the ServiceBundler. As a result, there is no way for me to start a service that child scopes can rely upon being initialized.

Child scopes created during register() do not receive instance state

  • ParentActivityScope receives onCreate()
  • ParentActivityScope receives register() from Presenter
    • ParentActivityScope calls doLoading() and enters LOADING state
    • Presenter receives onLoad()
      • ParentActivityScope receives requireChild() from Presenter to create ChildActivityScope
      • ParentActivityScope is in LOADING state and skips calling onCreate on ChildActivityScope.
      • ChildActivityScope exists, is in IDLE state, but since onCreate() hasn't been called it doesn't have any savedInstanceState.

ActionBarOwner.View TODO

I implemented the View as a class instead of an interface, with the idea that it doesn't have much variation in implementation. (I also converted the boolean paramenter into 2 unparametered methods, since I prefer that)

/** {@link android.app.ActionBar} View interface, used for setting properties of the ActionBar display */
    @ParametersAreNonnullByDefault
    public static abstract class View implements MortarContext {

        public View(Activity activity) {
            _activity = activity;
            // Forces an error if Action Bar isn't present
            _action_bar = Optional.of(activity.getActionBar()).get();
            _action_bar_menu_action = Optional.absent();
        }

        /** Shows and enables the home icon in the {@link android.app.ActionBar} */
        public void showHomeIcon() {
            _action_bar.setDisplayShowHomeEnabled(true);
        }

        /** Hides and disables the home icon in the {@link android.app.ActionBar} */
        public void hideHomeIcon() {
            _action_bar.setDisplayShowHomeEnabled(false);
        }

        /** Shows the Up navigation button in the {@link android.app.ActionBar} */
        public void showUpButton() {
            _action_bar.setDisplayHomeAsUpEnabled(true);
            _action_bar.setHomeButtonEnabled(true);
        }

        /** Hides the Up navigation button in the {@link android.app.ActionBar} */
        public void hideUpButton() {
            _action_bar.setDisplayHomeAsUpEnabled(false);
            _action_bar.setHomeButtonEnabled(false);
        }

        /** Sets the {@link android.app.ActionBar} title */
        public void title(Optional<CharSequence> title) {
            _action_bar.setTitle(title.orNull());
        }

        /** Sets the {@link android.app.ActionBar} title */
        public void title(@Nonnegative int title) {
            _action_bar.setTitle(title);
        }

        /** Sets the menu action */
        public void menu(Optional<MenuAction> action) {
            if (action != _action_bar_menu_action) {
                _action_bar_menu_action = action;
                _activity.invalidateOptionsMenu();
            }
        }

        protected final ActionBar _action_bar;
        private final Activity _activity;
        private Optional<MenuAction> _action_bar_menu_action;
    }

It currently requires you implement the getMortarScope to use it, which also gives you a place to change any of the behavior. This would allow it not to be implemented on the Activity (and force that to be the case).

This is an example of it's use:

_action_bar = new ActionBarOwner.View(this) {

            @Override public MortarScope getMortarScope() {
                return _activity_scope;
            }
        };
        action_bar_owner.takeView(_action_bar);

Naming on variables/methods in Presenter class

We decided that for switching modes in our application (For example: Signed in / Not signed in), we wanted to have a blueprint for each mode. We put this state into a Presenter class and actually bound an activity to it, rather than a view (i.e. we have something extend Presenter and override the MortarScope method to pull scope directly from the bound activity). Now when someone signs-in, we call into the presenter which calls into the activity where we call setContentView directly to change the mode. The entire thing is very similar to the showScreen example on the main Mortar page.

Everything works quite well, but the methods on the base Presenter class are named in a way that makes it unclear if this is recommended/supported/okay. Essentially we are binding our activity to the presenter in activity#onCreate with takeView(activity), unbinding in activity#onDestroy with dropView(activity) and within the Presenter class we call getView to get the activity reference and call into it accordingly. Looking at the Presenter base class there doesn't seem to be anything there other than naming that necessitates a view being passed in, and the presence of a specific ViewPresenter seems to imply that the Presenter base class is intended to be flexible (Generics are named V, but don't need to extend View).

Really the only issue appears to be that the names cause our intentions to be less than clear. I was hoping people would be open to renaming some variables/methods/generic types within the presenter class hierarchy. Alternatively, we could leave the naming as is in the ViewPresenter, but creating a corresponding ActivityPresenter and change the Presenter base class to be concrete final helper that's less tied to views.

I'm happy to submit a pull request if it would be welcome.

Sample code for screenConductor doesn't include an easy way to have non-managed decorative views in a FlowView

There are a lot of cases where it seems super useful to have a decorative view as a child of a FlowView.

The sample code just checks for the existence of any old child, and if present, removes it.

I've been embedding logic to indicate that some children are non-swappable when a new screen is being shown, but I'm deeply ashamed of it and don't want to submit a pull request, so I'm bringing it up here instead. In any event, this has turned out to be a useful pattern (especially as flows embedded in other flows develops further), so it'd be good to get an example of this out before this gets too far along.

@WithModule should verify it was given a module class

Given:

@Layout(R.layout.edit_item_view) //
@WithModule(EditItemScreen.class) //
class EditItemScreen extends EditItemScreen {
  ...
  @dagger.Module(injects = { EditItemView.class },
      addsTo = EditItemFlow.Module.class, complete = false) 
  public static class Module {
  }
 ...
}

I have a bug: @WithModule is pointing at the Screen, not the Module. Given a package-private constructor, this will result in a runtime IllegalAccessError. If it were to double-check either accessibility or annotations, it could throw a better error.

PopupPresenter shouldn't use a unique KEY

Offending code in PopupPresenter:

  @Override public void onLoad(Bundle savedInstanceState) {
    if (whatToShow == null && savedInstanceState != null) {
      whatToShow = savedInstanceState.getParcelable(KEY);
    }

If a view has multiple popups (see RootView in our codebase) and one of those popups is showing when the state is saved, then when restoring state all of the PopupPresenters will think that their own popup should show since they all use the same KEY.

=> ClassCastException when trying to pass whatToShow to MyDialogPopup.show()

cc @RayFromSquare

Integration with navigation drawer

This is a feature request.

Navigation drawer is quite commonly used in apps. I think it would be really helpful if the sample app could include implementation for integrating with the navigation drawer (or perhaps share how Square handles this).

One tricky thing is that an owner of the navigation drawer (much like the implementation of ActionBarOwner) should be called in both directions: calling from a second-level child screen to the Activity to disable the swipe gesture, and calling from the Activity to the navigation drawer owner to syncState() and handle onConfigurationChanged().

Popup presenter can't be easily subclassed

There seem to be a number of good reasons to subclass a popup presenter. However, the generics on Popup make this difficult, since the Popup.show() function has a PopupPresenter<D, R> as an argument.

For example, take the case where you'd like to have logic in the popup presenter to figure out how things are displayed (display certain strings if a user is logged in, for instance, other strings if they're not, both based on a model of type D that you use for the argument to show()). It'd be simpler to just use a subclassed PopupPresenter.

There are ways around this - because show() is called with an object, you can certainly embed all the information you need in most cases into the object of type D, but this means that this all needs to be parcelable, static at the time show() is called, etc.

Packaging

There have been conversations about reorganizing Mortar and Flow. What I think I heard is that there should be three libraries:

Mortar: the two types of scopes, the Mortar static methods, Bundler and Blueprint.

Flow:

ScreenConductor and CanShowView should move here, b/c Flow isn't terribly useful without them. We should be able to write a demo here that slaps views around the back stack, injects them with Dagger and persists their view state, but with no dependencies on Mortar and no subgraphs.

Presenter:

Presenter, ViewPresenter, and PopupPresenter move to a new higher level library that cheerfully depends up on both Mortar and Flow. It has a a subclass or delegate of some kind that can make one of these ScreenConductor thingies start and stop scopes for screens that are expected to implement BluePrint.

ScreenConductor causing lags between screens

We have started using the ScreenConductor class in the mortar sample project in our own app. At first we were impressed with its simplicity. We were especially delighted to find a surprising side-effect of using this class to switch between screens: a smaller memory footprint due to the fact that at any one time, only views for one screen is present.

However, as our app grows to become more mature and each screen becomes more heavyweight, we are starting to see problems.

  1. Going back to the previous screen is like creating the entire screen altogether. No states (like previous scroll position) from the previous screen can be easily remembered.
  2. A bigger problem for us is that switching from one screen to another involves removing an old screen from the view parent, which in turn causes a huge GC (to free the object graph as well as views for that screen). The GC is done on the main thread and will cause a noticeable lag.

I was wondering if you guys could share with us whether you guys are also using ScreenConductor in your production apps at Square. If so, are you also running into the problems that we have observed?

Mortar should have something at the presenter level to allow

There needs to be the equivalent of onResume / onPause for Mortar at the presenter level. Most parts of the onResume are there with onLoad, but there is no real equivalent for onPause.

The main reason for this being a necessity is there seems to be no real way to pause process intensive tasks on a screen when the app is minimized. If I have some RXJava subscriptions running, I want the ability to cancel the subscriptions to my location updates so I stop hogging power, or I want the ability to pause my WebView, or insert some other task where it doesn't make sense that the app continues to process until the app is killed.

I'm not sure if onResume needs to be added, or if onLoad just needs to be called when the app comes back from being minimized, but there definitely needs to be some way to pause heavy tasks when the app is minimized.

Yes I could get this functionality in my app in a variety of ways, but I think it makes a lot of sense for something like this to be included in Mortar to begin with. The use cases are common enough I think, and the fact that you can't properly use a WebView without plugging into those events points to it even more.

Thanks for all your work so far. I love how flexible and predictable this framework is!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.