From a2b4ab318a5c6cf331e3a70ce443679ba620b381 Mon Sep 17 00:00:00 2001 From: Ray Ryan Date: Mon, 10 Apr 2017 17:05:01 -0700 Subject: [PATCH] Introduces HistoryFilter. Addresses https://github.com/square/flow/issues/220. --- CHANGELOG.md | 17 ++++--- flow/src/main/java/flow/Flow.java | 13 +++++ flow/src/main/java/flow/HistoryFilter.java | 11 ++++ .../flow/InternalLifecycleIntegration.java | 2 +- flow/src/main/java/flow/NotPersistent.java | 1 + .../java/flow/NotPersistentHistoryFilter.java | 24 +++++++++ flow/src/test/java/flow/FlowTest.java | 50 ++++++++++++++++++- 7 files changed, 110 insertions(+), 8 deletions(-) create mode 100644 flow/src/main/java/flow/HistoryFilter.java create mode 100644 flow/src/main/java/flow/NotPersistentHistoryFilter.java diff --git a/CHANGELOG.md b/CHANGELOG.md index d31d726..a5c95c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ Change Log ========== +Version 1.0-alpha2 *(2017-??-??)* +---------------------------------- + +* API change: added Flow#setHistoryFilter to address https://github.com/square/flow/issues/220 + Version 1.0-alpha2 *(2016-09-02)* ---------------------------------- Important bug fixes for 1.0 alpha. @@ -14,10 +19,10 @@ Version 1.0-alpha *(2016-02-18)* -------------------------------- Presented for review and feedback. API should still be considered unstable, docs incomplete, and functionality buggy. All of the above should be mostly resolved before beta. -1.0 brings major functional improvements and API changes. +1.0 brings major functional improvements and API changes. * Activity integration has been rewritten and is much simpler. One line to configure and install; one optional line to handle the back button; one optional line to handle new Intent. Flow handles lifecycle internally. -* Resource management (including shared resources) is now natively supported via TreeKeys-- Path has effectively been absorbed and simplified. Contexts are now managed internally and there's much less nesting of wrappers. +* Resource management (including shared resources) is now natively supported via TreeKeys-- Path has effectively been absorbed and simplified. Contexts are now managed internally and there's much less nesting of wrappers. * Multiple simultaneous states are now supported via MultiKeys-- Flow now works natively with UIs composed of dialogs, sheets, master-detail views, etc. MultiKeys can be composed of TreeKeys for resource sharing. * Persistence has been expanded and simplified. You can now save a Bundle along with view state, and Flow takes care of all the lifecycle. * Nested/queued traversals are much safer and more efficient. @@ -39,12 +44,12 @@ Version 0.10 *(2015-05-01)* Version 0.9 *(2015-04-24)* ------ -A large number of breaking changes have been made in the interest of focusing +A large number of breaking changes have been made in the interest of focusing the library. * Backstack is now called History and has some new method names. -* The `resetTo`, `goTo`, `replaceTo`, `forward`, and `backward` operations are - all gone. In their place are two simple methods: `set(Object)` and +* The `resetTo`, `goTo`, `replaceTo`, `forward`, and `backward` operations are + all gone. In their place are two simple methods: `set(Object)` and `set(History, Direction)`. * `HasParent` and `goUp` are gone. "Up" navigation is left as an exercise to app authors who need it, at least for the time being. @@ -53,7 +58,7 @@ the library. * Listener is now called Dispatcher, and can be set on a Flow after construction. Dispatcher gets more information than Listener did. -There are also some new features, and more are coming. +There are also some new features, and more are coming. * Added a Context service for easily obtaining the Flow. * Added `FlowDelegate` for easier integration into an Activity. diff --git a/flow/src/main/java/flow/Flow.java b/flow/src/main/java/flow/Flow.java index 2251de3..efe11ee 100644 --- a/flow/src/main/java/flow/Flow.java +++ b/flow/src/main/java/flow/Flow.java @@ -103,6 +103,7 @@ public static void addHistory(@NonNull Intent intent, @NonNull History history, } private History history; + private HistoryFilter historyFilter = new NotPersistentHistoryFilter(); private Dispatcher dispatcher; private PendingTraversal pendingTraversal; private List tearDownKeys = new ArrayList<>(); @@ -117,6 +118,10 @@ public static void addHistory(@NonNull Intent intent, @NonNull History history, return history; } + History getFilteredHistory() { + return historyFilter.scrubHistory(getHistory()); + } + /** * Set the dispatcher, may receive an immediate call to {@link Dispatcher#dispatch}. If a {@link * Traversal Traversal} is currently in progress with a previous Dispatcher, that Traversal will @@ -126,6 +131,14 @@ public void setDispatcher(@NonNull Dispatcher dispatcher) { setDispatcher(dispatcher, false); } + /** + * Set the {@link HistoryFilter}, responsible for scrubbing history before it is persisted. + * Use this to customize the default behavior described on {@link NotPersistent}. + */ + public void setHistoryFilter(@NonNull HistoryFilter historyFilter) { + this.historyFilter = historyFilter; + } + void setDispatcher(@NonNull Dispatcher dispatcher, final boolean restore) { this.dispatcher = checkNotNull(dispatcher, "dispatcher"); diff --git a/flow/src/main/java/flow/HistoryFilter.java b/flow/src/main/java/flow/HistoryFilter.java new file mode 100644 index 0000000..f736515 --- /dev/null +++ b/flow/src/main/java/flow/HistoryFilter.java @@ -0,0 +1,11 @@ +package flow; + +import android.support.annotation.NonNull; + +/** + * An object to which gets a chance to scrub the current {@link History} before + * it is persisted. + */ +public interface HistoryFilter { + @NonNull History scrubHistory(@NonNull History history); +} diff --git a/flow/src/main/java/flow/InternalLifecycleIntegration.java b/flow/src/main/java/flow/InternalLifecycleIntegration.java index 521ee61..4813ea5 100644 --- a/flow/src/main/java/flow/InternalLifecycleIntegration.java +++ b/flow/src/main/java/flow/InternalLifecycleIntegration.java @@ -172,7 +172,7 @@ void onNewIntent(Intent intent) { } Bundle bundle = new Bundle(); - save(bundle, parceler, flow.getHistory(), keyManager); + save(bundle, parceler, flow.getFilteredHistory(), keyManager); if (!bundle.isEmpty()) { outState.putParcelable(INTENT_KEY, bundle); } diff --git a/flow/src/main/java/flow/NotPersistent.java b/flow/src/main/java/flow/NotPersistent.java index 7966436..66a6b32 100644 --- a/flow/src/main/java/flow/NotPersistent.java +++ b/flow/src/main/java/flow/NotPersistent.java @@ -22,6 +22,7 @@ /** * Applied to a state object, indicates that it should not be persisted with the history. + * This behavior can be changed via {@link Flow#setHistoryFilter}. */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) diff --git a/flow/src/main/java/flow/NotPersistentHistoryFilter.java b/flow/src/main/java/flow/NotPersistentHistoryFilter.java new file mode 100644 index 0000000..35d8c6e --- /dev/null +++ b/flow/src/main/java/flow/NotPersistentHistoryFilter.java @@ -0,0 +1,24 @@ +package flow; + +import android.support.annotation.NonNull; +import java.util.Iterator; + +/** + * Default implementation of {@link HistoryFilter}, enforces the contract + * documented on {@link NotPersistent}. + */ +class NotPersistentHistoryFilter implements HistoryFilter { + @NonNull @Override public History scrubHistory(@NonNull History history) { + History.Builder builder = History.emptyBuilder(); + + final Iterator keys = history.reverseIterator(); + while (keys.hasNext()) { + Object key = keys.next(); + if (!key.getClass().isAnnotationPresent(NotPersistent.class)) { + builder.push(key); + } + } + + return builder.build(); + } +} diff --git a/flow/src/test/java/flow/FlowTest.java b/flow/src/test/java/flow/FlowTest.java index 6d83109..2608f76 100644 --- a/flow/src/test/java/flow/FlowTest.java +++ b/flow/src/test/java/flow/FlowTest.java @@ -19,11 +19,13 @@ import android.support.annotation.NonNull; import java.util.Arrays; import java.util.Iterator; +import java.util.List; import org.junit.Before; import org.junit.Test; import org.mockito.Mock; import org.mockito.Mockito; +import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; import static org.mockito.MockitoAnnotations.initMocks; @@ -38,10 +40,17 @@ static class Dos { static class Tres { } + @NotPersistent static class NoPersist extends TestKey { + NoPersist() { + super("NoPersist"); + } + } + final TestKey able = new TestKey("Able"); final TestKey baker = new TestKey("Baker"); final TestKey charlie = new TestKey("Charlie"); final TestKey delta = new TestKey("Delta"); + final TestKey noPersist = new NoPersist(); @Mock KeyManager keyManager; History lastStack; @@ -70,7 +79,8 @@ void fire() { TraversalCallback oldCallback = callback; callback = null; traversal = null; - oldCallback.onTraversalCompleted();; + oldCallback.onTraversalCompleted(); + ; } void assertIdle() { @@ -428,4 +438,42 @@ static class Picky { // That's good! } } + + @Test public void defaultHistoryFilter() { + History history = + History.emptyBuilder().pushAll(Arrays.asList(able, noPersist, charlie)).build(); + + Flow flow = new Flow(keyManager, history); + flow.setDispatcher(new FlowDispatcher()); + + List expected = History.emptyBuilder().pushAll(asList(able, charlie)).build().asList(); + assertThat(flow.getFilteredHistory().asList()).isEqualTo(expected); + } + + @Test public void customHistoryFilter() { + History history = + History.emptyBuilder().pushAll(Arrays.asList(able, noPersist, charlie)).build(); + + Flow flow = new Flow(keyManager, history); + flow.setDispatcher(new FlowDispatcher()); + flow.setHistoryFilter(new HistoryFilter() { + @NonNull @Override public History scrubHistory(@NonNull History history) { + History.Builder builder = History.emptyBuilder(); + + final Iterator keys = history.reverseIterator(); + while (keys.hasNext()) { + Object key = keys.next(); + if (!key.equals(able)) { + builder.push(key); + } + } + + return builder.build(); + } + }); + + List expected = + History.emptyBuilder().pushAll(asList(noPersist, charlie)).build().asList(); + assertThat(flow.getFilteredHistory().asList()).isEqualTo(expected); + } }