Slide 1

Slide 1 text

Making Dogfood Builds Testable and Fun Eric Cochran Droidcon London October 27, 2017

Slide 2

Slide 2 text

Why? Build faster

Slide 3

Slide 3 text

Why? Build faster Force edge cases

Slide 4

Slide 4 text

Why? Build faster Force edge cases QA feature flags and A/B tests

Slide 5

Slide 5 text

Why? Build faster Force edge cases QA feature flags and A/B tests Get actionable feedback from QA

Slide 6

Slide 6 text

How? Gradle build flavors

Slide 7

Slide 7 text

How? Gradle build flavors Dependency inversion

Slide 8

Slide 8 text

How? Gradle build flavors Dependency inversion Amazing libraries

Slide 9

Slide 9 text

How? Gradle build flavors Dependency inversion Amazing libraries Google Play private listing

Slide 10

Slide 10 text

Gradle buildTypes { debug { applicationIdSuffix 'debug' } } productFlavors { internal { applicationIdSuffix 'internal' } }

Slide 11

Slide 11 text

Gradle

Slide 12

Slide 12 text

Gradle sourceSets { internal { java.srcDirs = ['internal/java'] // array. res.srcDirs = ['internal/res'] // array. manifest.srcFile 'internal/AndroidManifest.xml' // single. } // etc. }

Slide 13

Slide 13 text

Gradle internalImplementation 'com.example.debugging-library:logging:1.0.0' productionImplementation 'com.example.production-library:crash-reporting:1.0.0'

Slide 14

Slide 14 text

Gradle Build types for signing Flavors for changes

Slide 15

Slide 15 text

Add controls for behavior Indirection interface Tweeter { void postTweet(); } class RealTweeter implements Tweeter { @Override public void postTweet() { // Post! } }

Slide 16

Slide 16 text

Add controls for behavior Indirection class LoggingTweeter implements Tweeter { final Tweeter delegate; final Logger logger; boolean log; @Override public void postTweet() { delegate.postTweet(); if (log) logger.logTweet(); } void log(boolean log) { this.log = log; } }

Slide 17

Slide 17 text

Add controls for behavior Indirection class DebugView { @Inject LoggingTweeter loggingTweeter; { logTweetsCheckbox().setOnCheckedChangeListener((buttonView, isChecked) -> { loggingTweeter.log(isChecked); }); } }

Slide 18

Slide 18 text

Dependency Inversion Zero-argument factory methods for every type. —Gregory Kick Control implementation and behavior from higher in the graph.

Slide 19

Slide 19 text

Pushing down behavior controls in Android Views created with reflection class ServiceContextWrapper extends ContextWrapper { final String serviceName; final Object service; LayoutInflater inflater; @Override @Nullable public Object getSystemService(String name) { if (serviceName.equals(name)) return service; if (LAYOUT_INFLATER_SERVICE.equals(name)) { if (inflater == null) { inflater = LayoutInflater.from(getBaseContext()).cloneInContext(this); } return inflater; } return super.getSystemService(name); } }

Slide 20

Slide 20 text

Add controls for behavior src/production/java class HomeLayoutFactory implements ViewFactory { @Override public View create(Context context, ViewGroup parent) { LayoutInflater inflater = LayoutInflater.from(context); return inflater.inflate(R.layout.home, parent, false); } } src/internal/java class HomeLayoutFactory implements ViewFactory { @Override public View create(Context context, ViewGroup parent) { LayoutInflater inflater = LayoutInflater.from(context); return inflater.inflate(R.layout.home_debug, parent, false); } }

Slide 21

Slide 21 text

Add controls for behavior src/internal/rest/layout/home_debug.xml

Slide 22

Slide 22 text

Add controls for behavior src/internal/rest/layout/home_debug.xml DebugView.java setOnApplyWindowInsetsListener((v, insets) -> { // Do not consume insets. DebugView.this.onApplyWindowInsets(insets); return insets; });

Slide 23

Slide 23 text

Practical Usage

Slide 24

Slide 24 text

A/B tests interface AbTestProvider { enum Transportation { TRAIN, PLANE, BOAT } boolean autoScroll(); Transportation defaultTransportation(); } class InternalAbTestProvider implements AbTestProvider { // with getters and setters, modified by debug view. } class ProductionAbTestProvider implements AbTestProvider { @Override public boolean autoScroll() { return firebaseRemoteConfigs.valueOf("auto_scroll"); } @Override public Transportation defaultTransportation() { return parseTransportation(firebaseRemoteConfigs.valueOf("transportation")); } }

Slide 25

Slide 25 text

Rx-Preferences (SharedPreferences wrapper) interface Preference { interface Converter { @NonNull T deserialize(@NonNull String serialized); @NonNull String serialize(@NonNull T value); } @NonNull T get(); void set(@NonNull T value); boolean isSet(); void delete(); } https://github.com/f2prateek/rx-preferences

Slide 26

Slide 26 text

Retrofit interface NamesService { @GET Call> names(@Url String url); } // Empty state. @AppScope @Provides static NamesService provideNamesService() { return url -> Calls.response(Collections.emptyList()); } https://github.com/square/retrofit/tree/master/retrofit-mock

Slide 27

Slide 27 text

OkHttp Interceptors class NoNetworkInterceptor implements Interceptor { volatile boolean noNetwork; void setNoNetwork(boolean noNetwork) { this.noNetwork = noNetwork; } @Override public Response intercept(Chain chain) throws IOException { if (noNetwork) throw new IOException("You disabled the network!"); return chain.proceed(chain.request()); } }

Slide 28

Slide 28 text

OkHttp Logging Interceptor @AppScope @Provides static OkHttpClient provideOkHttpClient(Cache cache) { return new OkHttpClient.Builder().cache(cache) .addInterceptor(new GzipRequestInterceptor()) // ... .addInterceptor(new HttpLoggingInterceptor()) .build(); } https://github.com/square/okhttp/tree/master/okhttp-logging-interceptor

Slide 29

Slide 29 text

Telescope https://github.com/mattprecious/telescope src/internal/res/layout/home_debug.xml

Slide 30

Slide 30 text

Telescope https://github.com/mattprecious/telescope class HomeLayoutFactory implements ViewFactory { @Override public View create(Context context, ViewGroup parent) { LayoutInflater inflater = LayoutInflater.from(context); TelescopeLayout layout = (TelescopeLayout) inflater.inflate(R.layout.home_telescope, parent, false); layout.setLens(new EmailDeviceInfoLens(context, "Glitch", "[email protected]")); return layout; } } src/internal/java

Slide 31

Slide 31 text

Telescope

Slide 32

Slide 32 text

Leak Canary Use RefWatcher everywhere and give the no-op source set to production. internalImplementation "com.squareup.leakcanary:leakcanary-android:$leakcanaryVersion" productionImplementation “com.squareup.leakcanary:leakcanary-android-no-op:$leakcanaryVersion” https://github.com/square/leakcanary

Slide 33

Slide 33 text

Leak Canary Use RefWatcher everywhere and give the no-op source set to production. internalImplementation "com.squareup.leakcanary:leakcanary-android:$leakcanaryVersion" productionImplementation “com.squareup.leakcanary:leakcanary-android-no-op:$leakcanaryVersion” Extend DisplayLeakService and post notifications remotely automatically. https://github.com/square/leakcanary

Slide 34

Slide 34 text

Publish! Use Google Play! Auto updates! Pricing & Distribution section > Restrict Distribution https://support.google.com/a/answer/2494992

Slide 35

Slide 35 text

Credits Matt Precious Debug Builds: A New Hope https://youtu.be/Ae4vqz29W9U u2020 https://github.com/JakeWharton/u2020 Android Studio team! Gradle team! Open source project creators!

Slide 36

Slide 36 text

@Eric_Cochran github.com/NightlyNexus nightlynexus.com