Skip to content
This repository has been archived by the owner on Mar 16, 2021. It is now read-only.

Commit

Permalink
Merge pull request #36 from grandcentrix/feature/sendToView
Browse files Browse the repository at this point in the history
Queue actions send to the view until it is attached
  • Loading branch information
passsy authored Nov 9, 2016
2 parents 7e033cb + fc88f7f commit 74e26ae
Show file tree
Hide file tree
Showing 3 changed files with 249 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,16 @@
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;
import android.support.v4.app.Fragment;

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
Expand Down Expand Up @@ -82,6 +85,8 @@ public enum State {

private final TiConfiguration mConfig;

private LinkedBlockingQueue<ViewAction<V>> mPostponedViewActions = new LinkedBlockingQueue<>();

private State mState = State.INITIALIZED;

private V mView;
Expand Down Expand Up @@ -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()
*/
Expand Down Expand Up @@ -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()}.
* <p>
* 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.
*/
Expand Down Expand Up @@ -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<ViewAction<V>> getQueuedViewActions() {
return mPostponedViewActions;
}

/**
* The view is now attached and ready to receive events.
*
Expand All @@ -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);
}

/**
Expand Down Expand Up @@ -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.
* <p>
* 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.
* <p>
* <b>But don't overuse it.</b>
* The action will only be called <b>once</b>.
* 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 <a href="https://github.com/passsy/thirtyinch-sample">thirtyinch-sample</a> project
* for ideas.
*
* @see #sendPostponedActionsToView
* @see #onAttachView(TiView)
*/
protected void sendToView(ViewAction<V> 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
*
Expand Down Expand Up @@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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<V extends TiView> {

void call(V v);
}
Original file line number Diff line number Diff line change
@@ -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<TestView> {

}

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<TestView>() {
@Override
public void call(final TestView view) {
view.doSomething3();
}
});
presenter.sendToView(new ViewAction<TestView>() {
@Override
public void call(final TestView view) {
view.doSomething1();
}
});
presenter.sendToView(new ViewAction<TestView>() {
@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<TestView>() {
@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<TestView>() {
@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<TestView>() {
@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<TestView>() {
@Override
public void call(final TestView view) {
view.doSomething1();
}
});
assertThat(presenter.getQueuedViewActions()).hasSize(1);

verifyNoMoreInteractions(view);
}
}

0 comments on commit 74e26ae

Please sign in to comment.