Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Making Dogfood Builds Testable and Fun

Making Dogfood Builds Testable and Fun

Android apps are fun to dogfood but sometimes complex to configure for testers. To be developed fast, apps need internal 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, and Eric will also dig into how everyone can retrofit existing projects to be quickly configurable.

Learn creative ways to get runtime bindings and debug options up and running in Android apps, old and new.

Eric Cochran

October 27, 2017
Tweet

More Decks by Eric Cochran

Other Decks in Programming

Transcript

  1. Why? Build faster Force edge cases QA feature flags and

    A/B tests Get actionable feedback from QA
  2. Gradle buildTypes { debug { applicationIdSuffix 'debug' } } productFlavors

    { internal { applicationIdSuffix 'internal' } }
  3. Gradle sourceSets { internal { java.srcDirs = ['internal/java'] // array.

    res.srcDirs = ['internal/res'] // array. manifest.srcFile 'internal/AndroidManifest.xml' // single. } // etc. }
  4. Add controls for behavior Indirection interface Tweeter { void postTweet();

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

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

    Control implementation and behavior from higher in the graph.
  8. 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); } }
  9. 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); } }
  10. 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/rest/layout/home_debug.xml
  11. 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/rest/layout/home_debug.xml DebugView.java setOnApplyWindowInsetsListener((v, insets) -> { // Do not consume insets. DebugView.this.onApplyWindowInsets(insets); return insets; });
  12. 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")); } }
  13. 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
  14. 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
  15. 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()); } }
  16. 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
  17. 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
  18. 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
  19. 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
  20. Publish! Use Google Play! Auto updates! Pricing & Distribution section

    > Restrict Distribution https://support.google.com/a/answer/2494992
  21. 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!