diff --git a/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/TiPresenter.java b/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/TiPresenter.java index bc94af53..9d858f3b 100644 --- a/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/TiPresenter.java +++ b/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/TiPresenter.java @@ -89,6 +89,15 @@ public enum State { private LinkedBlockingQueue> mPostponedViewActions = new LinkedBlockingQueue<>(); + /** + * view is attached and all {@link #mLifecycleObservers} have been notified about the attache + * event. The view is now in a "running" state + * + * This is a temporary field without public getter until the presenter state and notifications + * get completely refactored + */ + private volatile boolean mRunning = false; + private State mState = State.INITIALIZED; /** @@ -198,6 +207,10 @@ public void attachView(@NonNull final V view) { } moveToState(State.VIEW_ATTACHED, true); + // TODO refactor events and add a new state HERE when the view is attached and prepared by all observers. + // Calling this a "running" state for now, prevents executing postponed actions before this point + mRunning = true; + sendPostponedActionsToView(view); } @@ -269,6 +282,7 @@ public final void detachView() { TiLog.v(TAG, "not calling onDetachView(), not woken up"); return; } + mRunning = false; moveToState(State.VIEW_DETACHED, false); mCalled = false; TiLog.v(TAG, "deprecated onSleep()"); @@ -515,7 +529,7 @@ protected void onWakeUp() { */ protected void sendToView(final ViewAction action) { final V view = getView(); - if (view != null) { + if (mRunning) { runOnUiThread(new Runnable() { @Override public void run() { diff --git a/thirtyinch/src/test/java/net/grandcentrix/thirtyinch/SendToViewTest.java b/thirtyinch/src/test/java/net/grandcentrix/thirtyinch/SendToViewTest.java index 76b8f5d5..77aa3a15 100644 --- a/thirtyinch/src/test/java/net/grandcentrix/thirtyinch/SendToViewTest.java +++ b/thirtyinch/src/test/java/net/grandcentrix/thirtyinch/SendToViewTest.java @@ -31,8 +31,10 @@ import static junit.framework.Assert.assertNotSame; import static junit.framework.Assert.assertTrue; import static org.assertj.core.api.Java6Assertions.assertThat; +import static org.assertj.core.api.Java6Assertions.failBecauseExceptionWasNotThrown; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyZeroInteractions; @@ -97,6 +99,68 @@ public void call(final TestView view) { inOrder.verify(view).doSomething2(); } + @Test + public void sendToView_fires_after_view_and_executor_are_ready() throws Exception { + + // Given a presenter without attached view + final TestPresenter presenter = new TestPresenter(); + presenter.create(); + final TestView view = mock(TestView.class); + + // When calling sendToView + presenter.sendToView(new ViewAction() { + @Override + public void call(final TestView testView) { + testView.doSomething1(); + } + }); + // Then the action will be postponed and not executed + verify(view, never()).doSomething1(); + + // When setting an executor + presenter.setUiThreadExecutor(new Executor() { + @Override + public void execute(@NonNull final Runnable command) { + command.run(); + } + }); + // The postponed actions will not be executed immediately, no view is attached + verify(view, never()).doSomething1(); + + // When view and executor are both attached + presenter.attachView(view); + // The postponed actions will be executed + verify(view).doSomething1(); + } + + @Test + public void sendToView_withRunningView_crashes_without_uiThreadExecutor() throws Exception { + + // Given a presenter with a attached view (running state) + // No uiThreadExecutor is attached + final TestPresenter presenter = new TestPresenter(); + presenter.create(); + final TestView view = mock(TestView.class); + presenter.attachView(view); + + // When calling sendToView without attaching a uiThreadExecutor + try { + presenter.sendToView(new ViewAction() { + @Override + public void call(final TestView testView) { + testView.doSomething1(); + } + }); + failBecauseExceptionWasNotThrown(IllegalStateException.class); + } catch (IllegalStateException e) { + // Then an Exception is thrown + // Whoever creates and manages the TiPresenter has to provide an executor + assertThat(e).hasMessageContaining("no ui thread executor"); + } + + verify(view, never()).doSomething1(); + } + @Test public void testSendToViewRunsOnTheMainThread() throws Exception {