From 3b56bf8d3889deff5736c739610a120219bc4d9f Mon Sep 17 00:00:00 2001 From: Eli Hart Date: Mon, 29 Jan 2018 16:40:45 -0800 Subject: [PATCH] Allow EpoxyController settings to have global defaults (#394) --- .../com/airbnb/epoxy/EpoxyController.java | 96 ++++++++++++++++- .../com/airbnb/epoxy/EpoxyControllerTest.java | 100 ++++++++++++++++++ 2 files changed, 192 insertions(+), 4 deletions(-) diff --git a/epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyController.java b/epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyController.java index a7807761ff..be4d546ca9 100644 --- a/epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyController.java +++ b/epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyController.java @@ -41,6 +41,8 @@ public abstract class EpoxyController { private static final Timer NO_OP_TIMER = new NoOpTimer(); + private static boolean filterDuplicatesDefault = false; + private static boolean globalDebugLoggingEnabled = false; /** * We check that the adapter is not connected to multiple recyclerviews, but when a fragment has @@ -56,7 +58,7 @@ public abstract class EpoxyController { private final Handler handler = new Handler(Looper.getMainLooper()); private final List interceptors = new ArrayList<>(); private ControllerModelList modelsBeingBuilt; - private boolean filterDuplicates; + private boolean filterDuplicates = filterDuplicatesDefault; /** Used to time operations and log their duration when in debug mode. */ private Timer timer = NO_OP_TIMER; private EpoxyDiffLogger debugObserver; @@ -65,6 +67,10 @@ public abstract class EpoxyController { private int recyclerViewAttachCount = 0; private EpoxyModel stagedModel; + public EpoxyController() { + setDebugLoggingEnabled(globalDebugLoggingEnabled); + } + /** * Posting and canceling runnables is a bit expensive - it is synchronizes and iterates the the * list of runnables. We want clients to be able to request model builds as often as they want and @@ -483,10 +489,23 @@ public void setFilterDuplicates(boolean filterDuplicates) { this.filterDuplicates = filterDuplicates; } + public boolean isDuplicateFilteringEnabled() { + return filterDuplicates; + } + + /** + * {@link #setFilterDuplicates(boolean)} is disabled in each EpoxyController by default. It can be + * toggled individually in each controller, or alternatively you can use this to change the + * default value for all EpoxyControllers. + */ + public static void setGlobalDuplicateFilteringDefault(boolean filterDuplicatesByDefault) { + EpoxyController.filterDuplicatesDefault = filterDuplicatesByDefault; + } + /** * If enabled, DEBUG logcat messages will be printed to show when models are rebuilt, the time * taken to build them, the time taken to diff them, and the item change outcomes from the - * differ. The tag of the logcat message is your adapter name. + * differ. The tag of the logcat message is the class name of your EpoxyController. *

* This is useful to verify that models are being diffed as expected, as well as to watch for * slowdowns in model building or diffing to indicate when you should optimize model building or @@ -511,6 +530,20 @@ public void setDebugLoggingEnabled(boolean enabled) { } } + public boolean isDebugLoggingEnabled() { + return timer != NO_OP_TIMER; + } + + /** + * Similar to {@link #setDebugLoggingEnabled(boolean)}, but this changes the global default for + * all EpoxyControllers. + *

+ * The default is false. + */ + public static void setGlobalDebugLoggingEnabled(boolean globalDebugLoggingEnabled) { + EpoxyController.globalDebugLoggingEnabled = globalDebugLoggingEnabled; + } + /** * An optimized way to move a model from one position to another without rebuilding all models. * This is intended to be used with {@link android.support.v7.widget.helper.ItemTouchHelper} to @@ -583,10 +616,65 @@ public boolean isMultiSpan() { } /** - * This is called when recoverable exceptions happen at runtime. They can be ignored and Epoxy - * will recover, but you can override this to be aware of when they happen. + * This is called when recoverable exceptions occur at runtime. By default they are ignored and + * Epoxy will recover, but you can override this to be aware of when they happen. + *

+ * A common use for this is being aware of duplicates when {@link #setFilterDuplicates(boolean)} + * is enabled. + *

+ * By default the global exception handler provided by + * {@link #setGlobalExceptionHandler(ExceptionHandler)} + * is called with the exception. Overriding this allows you to provide your own handling for a + * controller. */ protected void onExceptionSwallowed(@NonNull RuntimeException exception) { + globalExceptionHandler.onException(this, exception); + } + + /** + * Default handler for exceptions in all EpoxyControllers. Set with {@link + * #setGlobalExceptionHandler(ExceptionHandler)} + */ + private static ExceptionHandler globalExceptionHandler = + new ExceptionHandler() { + + @Override + public void onException(@NonNull EpoxyController controller, + @NonNull RuntimeException exception) { + // Ignore exceptions as the default + } + }; + + /** + * Set a callback to be notified when a recoverable exception occurs at runtime. By default these + * are ignored and Epoxy will recover, but you can override this to be aware of when they happen. + *

+ * For example, you could choose to rethrow the exception in development builds, or log them in + * production. + *

+ * A common use for this is being aware of duplicates when {@link #setFilterDuplicates(boolean)} + * is enabled. + *

+ * This callback will be used in all EpoxyController classes. If you would like specific handling + * in a certain controller you can override {@link #onExceptionSwallowed(RuntimeException)} in + * that controller. + */ + public static void setGlobalExceptionHandler( + @NonNull ExceptionHandler globalExceptionHandler) { + EpoxyController.globalExceptionHandler = globalExceptionHandler; + } + + public interface ExceptionHandler { + /** + * This is called when recoverable exceptions happen at runtime. They can be ignored and Epoxy + * will recover, but you can override this to be aware of when they happen. + *

+ * For example, you could choose to rethrow the exception in development builds, or log them in + * production. + * + * @param controller The EpoxyController that the error occurred in. + */ + void onException(@NonNull EpoxyController controller, @NonNull RuntimeException exception); } void onAttachedToRecyclerViewInternal(RecyclerView recyclerView) { diff --git a/epoxy-adapter/src/test/java/com/airbnb/epoxy/EpoxyControllerTest.java b/epoxy-adapter/src/test/java/com/airbnb/epoxy/EpoxyControllerTest.java index 9d4716828c..53b2abb328 100644 --- a/epoxy-adapter/src/test/java/com/airbnb/epoxy/EpoxyControllerTest.java +++ b/epoxy-adapter/src/test/java/com/airbnb/epoxy/EpoxyControllerTest.java @@ -11,6 +11,7 @@ import java.util.ArrayList; import java.util.List; +import static junit.framework.Assert.assertFalse; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; @@ -363,4 +364,103 @@ protected void buildModels() { controller.requestModelBuild(); assertEquals(testModels, adapter.getCurrentModels()); } + + @Test + public void testDuplicateFilteringDisabledByDefault() { + EpoxyController controller = new EpoxyController() { + + @Override + protected void buildModels() { + + } + }; + + assertFalse(controller.isDuplicateFilteringEnabled()); + } + + @Test + public void testDuplicateFilteringCanBeToggled() { + EpoxyController controller = new EpoxyController() { + + @Override + protected void buildModels() { + + } + }; + + assertFalse(controller.isDuplicateFilteringEnabled()); + + controller.setFilterDuplicates(true); + assertTrue(controller.isDuplicateFilteringEnabled()); + + controller.setFilterDuplicates(false); + assertFalse(controller.isDuplicateFilteringEnabled()); + } + + @Test + public void testGlobalDuplicateFilteringDefault() { + EpoxyController.setGlobalDuplicateFilteringDefault(true); + + EpoxyController controller = new EpoxyController() { + + @Override + protected void buildModels() { + + } + }; + + assertTrue(controller.isDuplicateFilteringEnabled()); + + controller.setFilterDuplicates(false); + assertFalse(controller.isDuplicateFilteringEnabled()); + + controller.setFilterDuplicates(true); + assertTrue(controller.isDuplicateFilteringEnabled()); + + // Reset static field for future tests + EpoxyController.setGlobalDuplicateFilteringDefault(false); + } + + @Test + public void testDebugLoggingCanBeToggled() { + EpoxyController controller = new EpoxyController() { + + @Override + protected void buildModels() { + + } + }; + + assertFalse(controller.isDebugLoggingEnabled()); + + controller.setDebugLoggingEnabled(true); + assertTrue(controller.isDebugLoggingEnabled()); + + controller.setDebugLoggingEnabled(false); + assertFalse(controller.isDebugLoggingEnabled()); + } + + @Test + public void testGlobalDebugLoggingDefault() { + EpoxyController.setGlobalDebugLoggingEnabled(true); + + EpoxyController controller = new EpoxyController() { + + @Override + protected void buildModels() { + + } + }; + + assertTrue(controller.isDebugLoggingEnabled()); + + controller.setDebugLoggingEnabled(false); + assertFalse(controller.isDebugLoggingEnabled()); + + controller.setDebugLoggingEnabled(true); + assertTrue(controller.isDebugLoggingEnabled()); + + // Reset static field for future tests + EpoxyController.setGlobalDebugLoggingEnabled(false); + } } \ No newline at end of file