diff --git a/.travis.yml b/.travis.yml index 32ebe05c07..9ac2659eac 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ android: - tools - platform-tools - build-tools-27.0.3 - - android-27 + - android-28 - extra-google-google_play_services - extra-android-m2repository - extra-android-support diff --git a/blessedDeps.gradle b/blessedDeps.gradle index 6a7f2db8d5..da02d21a5c 100644 --- a/blessedDeps.gradle +++ b/blessedDeps.gradle @@ -10,8 +10,8 @@ rootProject.ext.JAVA_SOURCE_VERSION = JavaVersion.VERSION_1_7 rootProject.ext.JAVA_TARGET_VERSION = JavaVersion.VERSION_1_7 -rootProject.ext.TARGET_SDK_VERSION = 27 -rootProject.ext.COMPILE_SDK_VERSION = 27 +rootProject.ext.TARGET_SDK_VERSION = 28 +rootProject.ext.COMPILE_SDK_VERSION = 28 rootProject.ext.MIN_SDK_VERSION = 14 rootProject.ext.MIN_SDK_VERSION_LITHO = 15 diff --git a/build.gradle b/build.gradle index e4c111cb88..c879b98aab 100755 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ buildscript { ext.KOTLIN_VERSION = "1.2.60" - ext.ANDROID_PLUGIN_VERSION = "3.1.3" + ext.ANDROID_PLUGIN_VERSION = "3.1.4" repositories { google() diff --git a/epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncEpoxyController.java b/epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncEpoxyController.java new file mode 100644 index 0000000000..b720b12747 --- /dev/null +++ b/epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncEpoxyController.java @@ -0,0 +1,41 @@ +package com.airbnb.epoxy; + +import android.os.Handler; + +import static com.airbnb.epoxy.EpoxyAsyncUtil.MAIN_THREAD_HANDLER; +import static com.airbnb.epoxy.EpoxyAsyncUtil.getAsyncBackgroundHandler; + +/** + * A subclass of {@link EpoxyController} that makes it easy to do model building and diffing in + * the background. + *

+ * See https://github.com/airbnb/epoxy/wiki/Epoxy-Controller#asynchronous-support + */ +public abstract class AsyncEpoxyController extends EpoxyController { + + /** + * A new instance that does model building and diffing asynchronously. + */ + public AsyncEpoxyController() { + this(true); + } + + /** + * @param enableAsync True to do model building and diffing asynchronously, false to do them + * both on the main thread. + */ + public AsyncEpoxyController(boolean enableAsync) { + this(true, true); + } + + /** + * Individually control whether model building and diffing are done async or on the main thread. + */ + public AsyncEpoxyController(boolean enableAsyncModelBuilding, boolean enableAsyncDiffing) { + super(getHandler(enableAsyncModelBuilding), getHandler(enableAsyncDiffing)); + } + + private static Handler getHandler(boolean enableAsync) { + return enableAsync ? getAsyncBackgroundHandler() : MAIN_THREAD_HANDLER; + } +} diff --git a/epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncEpoxyDiffer.java b/epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncEpoxyDiffer.java index 98190a8a32..24f1e9674d 100644 --- a/epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncEpoxyDiffer.java +++ b/epoxy-adapter/src/main/java/com/airbnb/epoxy/AsyncEpoxyDiffer.java @@ -162,7 +162,9 @@ private void onRunCompleted( @Nullable final DiffResult result ) { - MainThreadExecutor.INSTANCE.execute(new Runnable() { + // We use an asynchronous handler so that the Runnable can be posted directly back to the main + // thread without waiting on view invalidation synchronization. + MainThreadExecutor.ASYNC_INSTANCE.execute(new Runnable() { @Override public void run() { final boolean dispatchResult = tryLatchList(newList, runGeneration); diff --git a/epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyAsyncUtil.java b/epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyAsyncUtil.java new file mode 100644 index 0000000000..2bf449b421 --- /dev/null +++ b/epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyAsyncUtil.java @@ -0,0 +1,103 @@ +package com.airbnb.epoxy; + +import android.os.Build; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.support.annotation.MainThread; +import android.support.annotation.Nullable; + +import java.lang.reflect.Constructor; + +/** + * Various helpers for running Epoxy operations off the main thread. + */ +public final class EpoxyAsyncUtil { + private EpoxyAsyncUtil() { + } + + /** + * A Handler class that uses the main thread's Looper. + */ + public static final Handler MAIN_THREAD_HANDLER = + createHandler(Looper.getMainLooper(), false); + + /** + * A Handler class that uses the main thread's Looper. Additionally, this handler calls + * {@link Message#setAsynchronous(boolean)} for + * each {@link Message} that is sent to it or {@link Runnable} that is posted to it + */ + public static final Handler AYSNC_MAIN_THREAD_HANDLER = + createHandler(Looper.getMainLooper(), true); + + private static Handler asyncBackgroundHandler; + + /** + * A Handler class that uses a separate background thread dedicated to Epoxy. Additionally, + * this handler calls {@link Message#setAsynchronous(boolean)} for + * each {@link Message} that is sent to it or {@link Runnable} that is posted to it + */ + @MainThread + public static Handler getAsyncBackgroundHandler() { + // This is initialized lazily so we don't create the thread unless it will be used. + // It isn't synchronized so it should only be accessed on the main thread. + if (asyncBackgroundHandler == null) { + asyncBackgroundHandler = createHandler(buildBackgroundLooper("epoxy"), true); + } + + return asyncBackgroundHandler; + } + + /** + * Create a Handler with the given Looper + * + * @param async If true the Handler will calls {@link Message#setAsynchronous(boolean)} for + * each {@link Message} that is sent to it or {@link Runnable} that is posted to it. + */ + public static Handler createHandler(Looper looper, boolean async) { + if (!async) { + return new Handler(looper); + } + + if (Build.VERSION.SDK_INT >= 28) { + return Handler.createAsync(looper); + } + + if (Build.VERSION.SDK_INT >= 17) { + Constructor handlerConstructor = asyncHandlerConstructor(); + if (handlerConstructor != null) { + try { + return handlerConstructor.newInstance(looper, null, true); + } catch (Throwable e) { + // Fallback + } + } + } + + return new Handler(looper); + } + + /** + * Create a new looper that runs on a new background thread. + */ + public static Looper buildBackgroundLooper(String threadName) { + HandlerThread handlerThread = new HandlerThread(threadName); + handlerThread.start(); + return handlerThread.getLooper(); + } + + @Nullable + private static Constructor asyncHandlerConstructor() { + try { + //noinspection JavaReflectionMemberAccess + return Handler.class.getConstructor( + Looper.class, + Handler.Callback.class, + Boolean.class + ); + } catch (Throwable e) { + return null; + } + } +} diff --git a/epoxy-adapter/src/main/java/com/airbnb/epoxy/MainThreadExecutor.java b/epoxy-adapter/src/main/java/com/airbnb/epoxy/MainThreadExecutor.java index 01ec806f4a..a1eaeec191 100644 --- a/epoxy-adapter/src/main/java/com/airbnb/epoxy/MainThreadExecutor.java +++ b/epoxy-adapter/src/main/java/com/airbnb/epoxy/MainThreadExecutor.java @@ -1,12 +1,16 @@ package com.airbnb.epoxy; -import android.os.Handler; import android.os.Looper; +import static com.airbnb.epoxy.EpoxyAsyncUtil.createHandler; + class MainThreadExecutor extends HandlerExecutor { - static final MainThreadExecutor INSTANCE = new MainThreadExecutor(); + static final MainThreadExecutor INSTANCE = new MainThreadExecutor(false); + static final MainThreadExecutor ASYNC_INSTANCE = new MainThreadExecutor(true); - MainThreadExecutor() { - super(new Handler(Looper.getMainLooper())); + MainThreadExecutor(boolean async) { + super(createHandler(Looper.getMainLooper(), async)); } } + + diff --git a/epoxy-sample/src/main/java/com/airbnb/epoxy/sample/SampleController.java b/epoxy-sample/src/main/java/com/airbnb/epoxy/sample/SampleController.java index 6a37d3ec1a..34e8702935 100644 --- a/epoxy-sample/src/main/java/com/airbnb/epoxy/sample/SampleController.java +++ b/epoxy-sample/src/main/java/com/airbnb/epoxy/sample/SampleController.java @@ -1,8 +1,5 @@ package com.airbnb.epoxy.sample; -import android.os.Handler; -import android.os.HandlerThread; - import com.airbnb.epoxy.AutoModel; import com.airbnb.epoxy.TypedEpoxyController; import com.airbnb.epoxy.sample.models.CarouselModelGroup; @@ -10,6 +7,8 @@ import java.util.List; +import static com.airbnb.epoxy.EpoxyAsyncUtil.getAsyncBackgroundHandler; + public class SampleController extends TypedEpoxyController> { public interface AdapterCallbacks { void onAddCarouselClicked(); @@ -31,18 +30,10 @@ public interface AdapterCallbacks { private final AdapterCallbacks callbacks; - private static final Handler BACKGROUND_HANDLER; - - static { - HandlerThread handlerThread = new HandlerThread("epoxy"); - handlerThread.start(); - BACKGROUND_HANDLER = new Handler(handlerThread.getLooper()); - } - SampleController(AdapterCallbacks callbacks) { // Demonstrating how model building and diffing can be done in the background. // You can control them separately by passing in separate handler, as shown below. - super(BACKGROUND_HANDLER, BACKGROUND_HANDLER); + super(getAsyncBackgroundHandler(), getAsyncBackgroundHandler()); // super(new Handler(), BACKGROUND_HANDLER); // super(BACKGROUND_HANDLER, new Handler());