Slide 1

Slide 1 text

Making Debug Builds Testable and Fun Eric Cochran MCE May 8, 2017

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

3 How? - Grade build flavors - Dependency Inversion - Amazing libraries - Google Play private listing

Slide 4

Slide 4 text

Gradle buildTypes { debug { applicationIdSuffix ".debug" } release { signingConfig signingConfigs.release } } productFlavors { internal { applicationId 'com.example.internal' } production { applicationId 'com.example' } }

Slide 5

Slide 5 text

Gradle

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

Gradle internalCompile 'com.example.debugging-library:logging:1.0.0' productionCompile 'com.example.production-library:crash-reporting:1.0.0'

Slide 8

Slide 8 text

Gradle Build types for signing Flavors for changes

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

10 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 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

13 Pushing down behavior controls in Android Views created with reflection class ServiceContextWrapper extends ContextWrapper { final String serviceName; final Object service; LayoutInflater inflater; @Override 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 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

16

Slide 17

Slide 17 text

17 Practical Usage

Slide 18

Slide 18 text

18 A/B tests interface AbTestProvider { enum TransportationType { TRAIN, PLANE, BOAT } boolean autoScroll(); TransportationType 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 TransportationType defaultTransportation() { return parseTransportationType(firebaseRemoteConfigs.valueOf("transportation")); } }

Slide 19

Slide 19 text

19 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 20

Slide 20 text

20 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 21

Slide 21 text

21 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 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

24 Telescope class HomeLayoutFactory implements ViewFactory { @Override public View createView(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]")); } } src/internal/java https://github.com/mattprecious/telescope

Slide 25

Slide 25 text

25 Telescope

Slide 26

Slide 26 text

26 Leak Canary Use RefWatcher everywhere and give the no-op source set to production. internalCompile "com.squareup.leakcanary:leakcanary-android:${leakCanaryVersion}" productionCompile "com.squareup.leakcanary:leakcanary-android-no-op:${leakCanaryVersion}" Extend DisplayLeakService and post notifications remotely automatically. https://github.com/square/leakcanary

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

29 @Eric_Cochran github.com/NightlyNexus nightlynexus.com