Making Debug Builds Testable and Fun

Making Debug Builds Testable and Fun

Android apps are fun to dogfood but sometimes complex to configure for testers. To be developed fast, apps need debug builds that don't rely on server teams. To test effectively, internal testers need options to force edge cases and navigate quickly. To receive feedback, developers need to provide discoverable options to send reports. Tools like Dagger and Gradle build variants have made these wishes a reality for many, but Eric will also dig into how everyone can retrofit existing projects to be configurable and quickly editable. Eric will cover creative ways to get runtime bindings and debug options up and running in Android apps, old and new.

627ce26ea490d993a49d6b263768ca67?s=128

Eric Cochran

May 08, 2017
Tweet

Transcript

  1. Making Debug Builds Testable and Fun Eric Cochran MCE May

    8, 2017
  2. Why? - Build faster - Force edge cases - QA

    feature flags and A/B tests - Get actionable feedback from QA
  3. 3 How? - Grade build flavors - Dependency Inversion -

    Amazing libraries - Google Play private listing
  4. Gradle buildTypes { debug { applicationIdSuffix ".debug" } release {

    signingConfig signingConfigs.release } } productFlavors { internal { applicationId 'com.example.internal' } production { applicationId 'com.example' } }
  5. Gradle

  6. Gradle sourceSets { internal { java.srcDirs = ['internal/java'] // array.

    res.srcDirs = ['internal/res'] // array. manifest.srcFile 'internal/AndroidManifest.xml' // single. } // etc. }
  7. Gradle internalCompile 'com.example.debugging-library:logging:1.0.0' productionCompile 'com.example.production-library:crash-reporting:1.0.0'

  8. Gradle Build types for signing Flavors for changes

  9. 9 Add controls for behavior Indirection interface Tweeter { void

    postTweet(); } class RealTweeter implements Tweeter { @Override public void postTweet() { // Post! } }
  10. 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; } }
  11. 11 Add controls for behavior class DebugView { @Inject LoggingTweeter

    loggingTweeter; loggingTweeter.setOnCheckedChangeListener((buttonView, isChecked) -> { loggingTweeter.log(isChecked); } }
  12. 12 Dependency Inversion Zero-argument factory methods for every type. —

    Gregory Kick Control implementation and behavior from higher in the graph.
  13. 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); } }
  14. 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
  15. 15 Add controls for behavior <FrameLayout> <android.support.v4.widget.DrawerLayout> <include layout=“@layout/home"/> <com.example.DebugView

    android:layout_gravity="end" android:clipToPadding="false" android:fitsSystemWindows="true"/> </android.support.v4.widget.DrawerLayout> </FrameLayout> src/internal/res/layout/home_debug.xml DebugView.java setOnApplyWindowInsetsListener((v, insets) -> { // Do not consume insets. DebugView.this.onApplyWindowInsets(insets); return insets; });
  16. 16

  17. 17 Practical Usage

  18. 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")); } }
  19. 19 Rx-Preferences (SharedPreferences wrapper) interface Preference<T> { interface Converter<T> {

    @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/
  20. 20 Retrofit interface NamesService { @GET Call<List<String>> 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
  21. 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()); } }
  22. 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(); }
  23. 23 Telescope <com.mattprecious.telescope.TelescopeLayout> <android.support.v4.widget.DrawerLayout> <include layout=“@layout/home"/> <com.example.DebugView/> </android.support.v4.widget.DrawerLayout> </com.mattprecious.telescope.TelescopeLayout> src/internal/res/layout/home_debug.xml

    https://github.com/mattprecious/telescope
  24. 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", "eric@ifttt.com")); } } src/internal/java https://github.com/mattprecious/telescope
  25. 25 Telescope

  26. 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
  27. 27 Use Google Play! Auto updates! Pricing & Distribution section

    > Restrict Distribution https://support.google.com/a/answer/2494992 Publish!
  28. 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
  29. 29 @Eric_Cochran github.com/NightlyNexus nightlynexus.com