How do I prevent Mortar scopes from persisting across screens?

275 Views Asked by At

I have an app set up using Mortar/Flow and Dagger 2. It seems to work except for when I switch between two views of the same class. The new view ends up with the previous view's presenter.

For example, I have a ConversationScreen that takes a conversationId as a constructor argument. The first time I create a ConversationScreen and add it to Flow it creates the ConversationView which injects itself with a Presenter which is created with the conversationId that was passed to the screen. If I then create a new ConversationScreen with a different conversationId, when the ConversationView asks for a Presenter, Dagger returns the old Presenter, because the scope has not yet closed on the previous ConversationScreen.

Is there a way for me to manually close the scope of the previous screen before I set up the new one? Or have I just set up the scoping wrong to begin with?

ConversationView

public class ConversationView extends RelativeLayout {
    @Inject
    ConversationScreen.Presenter presenter;

    public ConversationView(Context context, AttributeSet attrs) {
        super(context, attrs);
        DaggerService.<ConversationScreen.Component>getDaggerComponent(context).inject(this);
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        presenter.takeView(this);
    }

    @Override
    protected void onDetachedFromWindow() {
        presenter.dropView(this);
        super.onDetachedFromWindow();
    }
}

ConversationScreen

@Layout(R.layout.screen_conversation)
public class ConversationScreen extends Paths.ConversationPath implements ScreenComponentFactory<SomeComponent> {
    public ConversationScreen(String conversationId) {
        super(conversationId);
    }

    @Override
    public String getTitle() {
        title = Conversation.get(conversationId).getTitle();
    }

    @Override
    public Object createComponent(SomeComponent parent) {
        return DaggerConversationScreen_Component.builder()
                .someComponent(parent)
                .conversationModule(new ConversationModule())
                .build();
    }

    @dagger.Component(
            dependencies = SomeComponent.class,
            modules = ConversationModule.class
    )

    @DaggerScope(Component.class)
    public interface Component {
        void inject(ConversationView conversationView);
    }

    @DaggerScope(Component.class)
    @dagger.Module
    public class ConversationModule {
        @Provides
        @DaggerScope(Component.class)
        Presenter providePresenter() {
            return new Presenter(conversationId);
        }
    }

    @DaggerScope(Component.class)
    static public class Presenter extends BasePresenter<ConversationView> {
        private String conversationId;

        @Inject
        Presenter(String conversationId) {
            this.conversationId = conversationId;
        }

        @Override
        protected void onLoad(Bundle savedInstanceState) {
            super.onLoad(savedInstanceState);
            bindData();
        }

        void bindData() {
          // Show the messages in the conversation
        }
    }
}
2

There are 2 best solutions below

2
lukasz On BEST ANSWER

If you use the default ScreenScoper and PathContextFactory classes from Mortar/Flow example project, you will see that the name of the new scope to create is the name of the Screen class.

Because you want to navigate from one instance of ConversationScreen to another instance of ConversationScreen, the name of the new scope will be equal to the name of previous scope. Thus, you won't create a new Mortar scope but just reuse the previous one, which means reusing the same presenter.

What you need is to change the naming policy of the new scope. Rather than using only the name of the new screen class, add something else.
Easiest fix is to use the instance identifier: myScreen.toString().

Another better fix is to have a tracking of the screen/scope names. Following example extracted from https://github.com/lukaspili/Mortar-architect

class EntryCounter {

   private final SimpleArrayMap<Class, Integer> ids = new SimpleArrayMap<>();

   int get(History.Entry entry) {
       Class cls = entry.path.getClass();
       return ids.containsKey(cls) ? ids.get(cls) : 0;
   }

   void increment(History.Entry entry) {
       update(entry, true);
   }

   void decrement(History.Entry entry) {
       update(entry, false);
   }

   private void update(History.Entry entry, boolean increment) {
       Class cls = entry.path.getClass();
       int id = ids.containsKey(cls) ? ids.get(cls) : 0;
       ids.put(cls, id + (increment ? 1 : -1));
   }
}

And then use this counter when creating new scope:

private ScopedEntry buildScopedEntry(History.Entry entry) {
    String scopeName = String.format("ARCHITECT_SCOPE_%s_%d", entry.path.getClass().getName(), entryCounter.get(entry));
    return new ScopedEntry(entry, MortarFactory.createScope(navigator.getScope(), entry.path, scopeName));
}

And in some other place, i'm incrementing/decrementing the counter if new scope is pushed or scope is detroyed.

0
EpicPandaForce On

The scope in ScreenScoper is based on a string, which if you create the same path, it will use the same name as it bases it on the class name of your path.

I solved this by removing some noise from the ScreenScoper, considering I'm not using @ModuleFactory in my Dagger2-driven project anyways.

public abstract class BasePath
        extends Path {
    public abstract int getLayout();

    public abstract Object createComponent();

    public abstract String getScopeName();
}

public class ScreenScoper {
    public MortarScope getScreenScope(Context context, String name, Object screen) {
        MortarScope parentScope = MortarScope.getScope(context);
        return getScreenScope(parentScope, name, screen);
    }

    /**
     * Finds or creates the scope for the given screen.
     */
    public MortarScope getScreenScope(MortarScope parentScope, final String name, final Object screen) {
        MortarScope childScope = parentScope.findChild(name);
        if (childScope == null) {
            BasePath basePath = (BasePath) screen;
            childScope = parentScope.buildChild()
                    .withService(DaggerService.TAG, basePath.createComponent())
                    .build(name);
        }
        return childScope;
    }
}