diff --git a/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/TiPresenter.java b/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/TiPresenter.java index 49d4fd58..56ac2d21 100644 --- a/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/TiPresenter.java +++ b/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/TiPresenter.java @@ -19,6 +19,7 @@ import net.grandcentrix.thirtyinch.internal.OneTimeRemovable; import android.app.Activity; +import android.content.Intent; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; @@ -26,6 +27,8 @@ import java.util.ArrayList; import java.util.List; +import java.util.Queue; +import java.util.concurrent.LinkedBlockingQueue; /** * Represents the Presenter of the popular Model-View-Presenter design pattern. If used with {@link @@ -82,6 +85,8 @@ public enum State { private final TiConfiguration mConfig; + private LinkedBlockingQueue> mPostponedViewActions = new LinkedBlockingQueue<>(); + private State mState = State.INITIALIZED; private V mView; @@ -236,11 +241,9 @@ public final void destroy() { /** * call detachView as the opposite of {@link #attachView(TiView)}, when the view is not - * available - * anymore. + * available anymore. * Calling detachView in {@code Fragment#onDestroyView()} makes sense because observing a - * discarded - * view does not. + * discarded view does not. * * @see #onSleep() */ @@ -288,6 +291,10 @@ public State getState() { /** * Returns the currently attached view. The view is attached between the lifecycle callbacks * {@link #onAttachView(TiView)} and {@link #onSleep()}. + *

+ * If you don't care about the view being attached or detached you should either rethink your + * architecture or use {@link #sendToView(ViewAction)} where the action will be executed when + * the view is attached. * * @return the currently attached view of this presenter, {@code null} when no view is attached. */ @@ -322,6 +329,15 @@ public String toString() { + "{view = " + viewName + "}"; } + /** + * Gives access to the postponed actions while the view is not attached. + * + * @return the queued actions + */ + protected Queue> getQueuedViewActions() { + return mPostponedViewActions; + } + /** * The view is now attached and ready to receive events. * @@ -334,6 +350,11 @@ protected void onAttachView(@NonNull V view) { "don't call #onAttachView(TiView) directly, call #attachView(TiView)"); } mCalled = true; + + // send all queued actions since the view was detached to the new view. + // It's part of the super call because there might be usecases where the implementer + // wants to execute actions on the view before executing the queued ones. + sendPostponedActionsToView(view); } /** @@ -402,6 +423,37 @@ protected void onWakeUp() { mCalled = true; } + /** + * Executes the {@link ViewAction} when the view is available. + * Once a view is attached the actions get called in the same order they have been added. + * When the view is already attached the action will be executed immediately. + *

+ * This method might be very useful for single actions which invoke function like {@link + * Activity#finish()}, {@link Activity#startActivity(Intent)} or showing a {@link + * android.widget.Toast} in the view. + *

+ * But don't overuse it. + * The action will only be called once. + * When a new view attaches (after a configuration change) it doesn't know about the previously + * sent actions. + * If your using this method too often you should rethink your architecture. + * A model which can be bound to the view in {@link #onAttachView(TiView)} and when changes + * happen might be a better solution. + * See the thirtyinch-sample project + * for ideas. + * + * @see #sendPostponedActionsToView + * @see #onAttachView(TiView) + */ + protected void sendToView(ViewAction action) { + final V view = getView(); + if (view != null) { + action.call(view); + } else { + mPostponedViewActions.add(action); + } + } + /** * moves the presenter to the new state and validates the correctness of the transition * @@ -458,4 +510,15 @@ private void moveToState(final State newState, final boolean hasLifecycleMethodB mLifecycleObservers.get(i).onChange(newState, hasLifecycleMethodBeenCalled); } } + + /** + * Executes all postponed view actions + * + * @param view where the actions will be sent to + */ + private void sendPostponedActionsToView(V view) { + while (!mPostponedViewActions.isEmpty()) { + mPostponedViewActions.poll().call(view); + } + } } diff --git a/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/ViewAction.java b/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/ViewAction.java new file mode 100644 index 00000000..caeeadb9 --- /dev/null +++ b/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/ViewAction.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2016 grandcentrix GmbH + * 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. + */ + +package net.grandcentrix.thirtyinch; + +/** + * Action which will be be executed when once a view is available + */ +public interface ViewAction { + + void call(V v); +} diff --git a/thirtyinch/src/test/java/net/grandcentrix/thirtyinch/SendToViewTest.java b/thirtyinch/src/test/java/net/grandcentrix/thirtyinch/SendToViewTest.java new file mode 100644 index 00000000..651dbcdb --- /dev/null +++ b/thirtyinch/src/test/java/net/grandcentrix/thirtyinch/SendToViewTest.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2016 grandcentrix GmbH + * 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. + */ + +package net.grandcentrix.thirtyinch; + + +import org.junit.Test; +import org.mockito.InOrder; + +import static org.assertj.core.api.Java6Assertions.assertThat; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; + +public class SendToViewTest { + + private class TestPresenter extends TiPresenter { + + } + + private interface TestView extends TiView { + + void doSomething1(); + + void doSomething2(); + + void doSomething3(); + } + + @Test + public void sendToViewInOrder() throws Exception { + final TestPresenter presenter = new TestPresenter(); + presenter.create(); + assertThat(presenter.getQueuedViewActions()).hasSize(0); + + presenter.sendToView(new ViewAction() { + @Override + public void call(final TestView view) { + view.doSomething3(); + } + }); + presenter.sendToView(new ViewAction() { + @Override + public void call(final TestView view) { + view.doSomething1(); + } + }); + presenter.sendToView(new ViewAction() { + @Override + public void call(final TestView view) { + view.doSomething2(); + } + }); + assertThat(presenter.getQueuedViewActions()).hasSize(3); + + final TestView view = mock(TestView.class); + presenter.attachView(view); + + assertThat(presenter.getQueuedViewActions()).hasSize(0); + + final InOrder inOrder = inOrder(view); + inOrder.verify(view).doSomething3(); + inOrder.verify(view).doSomething1(); + inOrder.verify(view).doSomething2(); + } + + @Test + public void viewAttached() throws Exception { + final TestPresenter presenter = new TestPresenter(); + presenter.create(); + assertThat(presenter.getQueuedViewActions()).hasSize(0); + + final TestView view = mock(TestView.class); + presenter.attachView(view); + + presenter.sendToView(new ViewAction() { + @Override + public void call(final TestView view) { + view.doSomething1(); + } + }); + assertThat(presenter.getQueuedViewActions()).hasSize(0); + verify(view).doSomething1(); + } + + @Test + public void viewDetached() throws Exception { + final TestPresenter presenter = new TestPresenter(); + presenter.create(); + assertThat(presenter.getQueuedViewActions()).hasSize(0); + + presenter.sendToView(new ViewAction() { + @Override + public void call(final TestView view) { + view.doSomething1(); + } + }); + assertThat(presenter.getQueuedViewActions()).hasSize(1); + + final TestView view = mock(TestView.class); + presenter.attachView(view); + verify(view).doSomething1(); + + assertThat(presenter.getQueuedViewActions()).hasSize(0); + } + + @Test + public void viewReceivesNoInteractionsAfterDetaching() throws Exception { + final TestPresenter presenter = new TestPresenter(); + presenter.create(); + assertThat(presenter.getQueuedViewActions()).hasSize(0); + + final TestView view = mock(TestView.class); + presenter.attachView(view); + presenter.detachView(); + + presenter.sendToView(new ViewAction() { + @Override + public void call(final TestView view) { + view.doSomething1(); + } + }); + assertThat(presenter.getQueuedViewActions()).hasSize(1); + verifyZeroInteractions(view); + + presenter.attachView(view); + + verify(view).doSomething1(); + assertThat(presenter.getQueuedViewActions()).hasSize(0); + + + presenter.detachView(); + + presenter.sendToView(new ViewAction() { + @Override + public void call(final TestView view) { + view.doSomething1(); + } + }); + assertThat(presenter.getQueuedViewActions()).hasSize(1); + + verifyNoMoreInteractions(view); + } +}