AnDevCon 2014 - Building First Class SDKs

7a3baf2e1158e358885cdf7e89b9aa55?s=47 Ty Smith
November 21, 2014

AnDevCon 2014 - Building First Class SDKs

A Fabric Case Study.

The Android app ecosystem is an international phenomenon. Developers need better tools now more than ever, and the number of third-party SDKs is also growing to meet the developer's needs. Unfortunately, many of these SDKs are poorly developed and extremely difficult to use. In fact, at Twitter on the Crashlytics team, we've detected a significant percentage of issues caused by third-party SDKs.

Crashlytics is well known for its focus on SDK quality, and has been deployed on hundreds of millions of devices. In this session, attendees will learn the skills to develop and distribute SDKs for Android. We'll explore how to design and build an SDK for stability, testability, performance, overall footprint size, and, most importantly, exceptional ease of implementation. Over the course of the class, we'll develop a simple but well-architected SDK, and uncover and explain many of the challenges we encountered when building SDKs at Twitter. Topics include device feature detection, supporting multiple application types (from Widgets to Services to Foreground GUI applications), API design, deploying artifacts, and coding patterns to support developer customization. We'll conclude with advanced topics, from less-known but very useful tricks to minimizing impact on application start-up time to reducing memory footprint and persistent CPU use.

7a3baf2e1158e358885cdf7e89b9aa55?s=128

Ty Smith

November 21, 2014
Tweet

Transcript

  1. dev.twitter.com @tsmith Building First Class SDKs A Fabric Case Study

    Ty Smith Sr. Android Engineer
  2. None
  3. None
  4. None
  5. None
  6. Fabric Sample App Cannonball Open source for iOS & Android:

    github.com/twitterdev
  7. Build an SDK

  8. Considerations

  9. Considerations What need are you serving?

  10. Considerations What need are you serving? Library Project, JARs, ApkLibs,

    and AARs.
  11. Considerations What need are you serving? Library Project, JARs, ApkLibs,

    and AARs. Open or Closed Source - Associated licensing
  12. Considerations What need are you serving? Library Project, JARs, ApkLibs,

    and AARs. Open or Closed Source - Associated licensing Hosting the artifacts
  13. Powerful Lightweight

  14. Powerful

  15. Ease of Integration

  16. Ease of Integration Fabric.with(this, new Crashlytics());

  17. Detecting Dependencies <manifest … package="com.example.SDK" > <application ... > …

    <meta-data android:value="11235813213455" android:name=“com.fabric.ApiKey” /> </application> </manifest>
  18. Detecting Dependencies public static Fabric with(Context context) { Context appContext

    = context.getApplicationContext(); ApplicationInfo ai = context.getPackageManager() .getApplicationInfo(ac.getPackageName(), PackageManager.GET_META_DATA); String apiKey = ai.metaData.getString(API_KEY); return new Fabric(appContext, apiKey); }
  19. Detecting Dependencies public static Fabric with(Context context) { Context appContext

    = context.getApplicationContext(); ApplicationInfo ai = context.getPackageManager() .getApplicationInfo(ac.getPackageName(), PackageManager.GET_META_DATA); String apiKey = ai.metaData.getString(API_KEY); return new Fabric(appContext, apiKey); }
  20. Detecting Dependencies public TwitterApiClient { final Session session; public TwitterApiClient(Session

    session) { this.session = session; } public TwitterApiClient() { this.session = Twitter.getSessionManager().getActiveSession(); } }
  21. Extensible API

  22. Extensible Crashlytics.start(this);

  23. Extensible Crashlytics.start(this, 5); Crashlytics.setListener(createCrashlyticsListener()); Crashlytics.setPinningInfo(createPinningInfoProvider()) Crashlytics.getInstance().setDebugMode(true);

  24. Extensible Crashlytics.setListener(createCrashlyticsListener()); Crashlytics.setPinningInfo(createPinningInfoProvider()); Crashlytics.getInstance().setDebugMode(true); Crashlytics.start(this, 5);

  25. Extensible Crashlytics.start(this, delay, listener, pinningInfo, debugMode);

  26. Extensible Crashlytics.start(this, 0, null, null, null, true);

  27. Fluent Pattern Crashlytics crashlytics = new Crashlytics.Builder() .delay(1) .listener(createCrashlyticsListener()) .pinningInfo(createPinningInfoProvider())

    .build(); Fabric.with(this, crashlytics);
  28. Fluent Pattern Fabric.with(new Fabric.Builder(this) .kits(new Crashlytics()) .debuggable(true) .logger(new DefaultLogger(Log.VERBOSE)) .looper(getCustomLooper())

    .executor(getCustomExecutorService()) .build());
  29. Callbacks Fabric fabric = new Fabric.Builder(this) .kits(new Crashlytics()) .initializationCallback( new

    Callback<Fabric>() { void success(Fabric fabric) { } void failure(Exception exception) { } }) .build();
  30. Extensible Interfaces interface Logger { void d(String tag, String text,

    Throwable throwable); void v(String tag, String text, Throwable throwable); void i(String tag, String text, Throwable throwable); void w(String tag, String text, Throwable throwable); void e(String tag, String text, Throwable throwable); }
  31. Sane Defaults public class DefaultLogger { public DefaultLogger(int logLevel) {

    this.logLevel = logLevel; } void d(String tag, String text, Throwable throwable) { if (isLoggable(tag, Log.DEBUG)) Log.d(tag, text, throwable); } ... }
  32. Extensible Classes class MyApiClient extends TwitterApiClient { interface MyService {

    @GET(“/1.1/statuses/show.json”) Tweet getTweet(@Query(“id”) int id); } MyService getMyService() { return getService(MyService.class); } }
  33. Extensible View Styles <style name="tw__TweetLightStyle"> <item name="tw__container_bg_color"> @color/tw__tweet_light_container_bg_color </item> <item

    name="tw__primary_text_color"> @color/tw__tweet_light_primary_text_color </item> <item name="tw__action_color"> @color/tw__tweet_action_color </item> </style>
  34. Great API Traits Intuitive

  35. Great API Traits Intuitive Consistent

  36. Great API Traits Intuitive Consistent Easy to use, hard to

    misuse
  37. Handling Failure

  38. Catch unexpected exceptions try { //Do all the things! }

    catch (Exception e){ //Log something meaningful }
  39. Throw expected exceptions /** * Sets the {@link io.fabric.sdk.android.Logger} *

    @throws java.lang.IllegalArgumentException */ public Builder logger(Logger logger) { if (logger == null) { throw new IllegalArgumentException( "Logger must not be null."); } this.logger = logger; return this; }
  40. Gracefully Degrade if (TextUtils.isEmpty(apiKey) if (debuggable){ throw new IllegalArgumentException(“apiKey is

    null!"); } else { return null; } }
  41. Runtime Detection

  42. Minimizing Permissions <uses-permission android:name="android.permission.INTERNET"/>

  43. Minimizing Permissions crashlytics.setUserEmail(“appuser@domain.com”);

  44. Permissions Detection protected boolean canCheckNetworkState(Context context) { String permission =

    Manifest.permission.ACCESS_NETWORK_STATE; int result = context.checkCallingOrSelfPermission(permission); return (result == PackageManager.PERMISSION_GRANTED); }
  45. Optional Features <manifest … package="com.example.SDK" > <application ... > …

    </application> <uses-feature android:name="android.hardware.camera" android:required=”false” /> </manifest>
  46. Features Detection protected boolean hasCamera(Context context) { PackageManager pm =

    context.getPackageManager(); String camera = PackageManager.FEATURE_CAMERA; return pm.hasSystemFeature(camera); }
  47. Android Version Detection if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) { return formatID(Build.HARDWARE);}

    } return null;
  48. Intent Detection try { startActivityForResult(new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH), 0); } catch (ActivityNotFoundException

    e){ }
  49. Intent Detection Cont. PackageManager pm = context.getApplicationContext().getPackageManager(); List<ResolveInfo> activities =

    pm.queryIntentActivities( new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH),0); if (activities.size() > 0) { //an app exists that can recognize speech }
  50. Classpath Detection private boolean hasOkHttpOnClasspath() { try { Class.forName("com.squareup.okhttp.OkHttpClient"); return

    true; } catch (ClassNotFoundException e) { } return false; } provided 'com.squareup.okhttp:okhttp:2.0.0'
  51. Library Detection Cont public class DefaultClient implements Client { private

    final Client client; public DefaultClient() { if (hasOkHttpOnClasspath()) { client = new OkClient(); } else { client = new UrlConnectionClient(); } } … }
  52. Multiple Application Types

  53. Multiple Application Types package com.example; import android.app.Service; public class MyService

    extends Service { }
  54. UI from Application Context WeakReference<Activity> activity = new WeakReference<>(); @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)

    void registerLifecycleCallbacks(Application app) { app.registerActivityLifecycleCallbacks( new ActivityLifecycleCallbacks() { @Override public void onActivityResumed(Activity activity) { activity.set(activity); } }); }
  55. UI from Application Context WeakReference<Activity> activity = new WeakReference<>(); …

    public void showErrorDialog(String error) { final Activity activity = currentActivity.get(); if (activity != null && !activity.isFinishing()) { new AlertDialog.Builder(activity) .setMessage(error).create().show(); } } }
  56. Testable SDKs

  57. Make it Testable/Mockable Avoid static methods

  58. Make it Testable/Mockable Avoid static methods Avoid final classes and

    accessing fields directly
  59. Make it Testable/Mockable Avoid static methods Avoid final classes and

    accessing fields directly Utilize interfaces around entry points
  60. Make it Testable/Mockable Avoid static methods Avoid final classes and

    accessing fields directly Utilize interfaces around entry points Provide test package in separate artifact
  61. Hard to Test package com.example; public final class Tweeter {

    private Network network; public static Tweeter getInstance() {...} private Tweeter() { this.network = new Network(); } public List<Tweet> getTweets() { return network.getTweets(); } }
  62. Easy to Test package com.example; public final class Tweeter {

    private Network network; public static Tweeter getInstance() {...} private public Tweeter(Network network) { this.network = new Network() network; } public List<Tweet> getTweets() { return getNetwork().getTweets(); } … }
  63. Using in Tests package com.example.Tweeter; public class MyTest extends AndroidTestCase

    { Tweeter tweeter; @Override public void setup() { tweeter = mock(Tweeter.class); } ... }
  64. Powerful SDKs •Ease of Integration •Extensibility •Handling Failure •Runtime detection

    •Support all app types •Testable
  65. Lightweight

  66. Binary Size

  67. Binary Size

  68. 3rd Party Library Mindfulness

  69. 3rd Party Library Mindfulness

  70. 3rd Party Library Mindfulness PROTOBUF KB OURS KB

  71. Reporting Binary Size task reportSdkFootprint << { def sdkProject =

    project(':clients:SdkProject') def nonSdkProject = project(':clients:NonSdkProject') def crashlyticsFootprint = getSizeDifference( new File("${sdkProject.buildDir}.../Sdk.apk"), new File("${nonSdkProject.buildDir}.../NonSdk.apk") ) println crashlyticsFootprint }
  72. Reporting Binary Size task reportSdkFootprint << { def sdkProject =

    project(':clients:SdkProject') def nonSdkProject = project(':clients:NonSdkProject') def crashlyticsFootprint = getSizeDifference( new File("${sdkProject.buildDir}.../Sdk.apk"), new File("${nonSdkProject.buildDir}.../NonSdk.apk") ) println crashlyticsFootprint }
  73. Dalvik Method Count

  74. Dalvik Method Count >./gradlew assemble … Unable to execute dex:

    method ID not in [0, 0xffff]: 65536 Conversion to Dalvik format failed: Unable to execute dex: method ID not in [0, 0xffff]: 65536
  75. Dalvik Method Count > git clone git@github.com:mihaip/dex-method-counts.git > cd dex-method-counts

    > ant jar > ./dex-method-counts path/to/App.apk Read in 65490 method IDs. <root>: 65490 : 3 accessibilityservice: 6 bluetooth: 2 content: 248 pm: 22 res: 45 com: 53881 example: 283 sdk: 283
  76. Modularity How to avoid a “Monolith Abomination”

  77. Modularity Fabric.with(this, new Twitter()); Fabric.with(this, new TweetUi(), new TweetComposer());

  78. Fabric.aar Cache Settings Concurrency Events Network Persistence

  79. Fabric.aar Cache Settings Concurrency Events Network Persistence KitCore Kit Common

    Services
  80. Fabric.aar Cache Settings Concurrency Events Network Persistence KitCore Kit A

    Kit C Kit B Features Kit Common Services
  81. Fabric.aar Cache Settings Concurrency Events Network Persistence KitCore Kit A

    Kit C Kit B KitInterface Interface Features Kit Common Services
  82. Fabric.aar Cache Settings Concurrency Events Network Persistence TwitterCore.aar TweetUi.aar Digits.aar

    TweetComposer. aar Twitter.aar Interface Features Kit Common Services
  83. How big are Fabric AARs? Fabric: 190kb Crashlytics: 90kb Beta:

    13kb Answers: 20kb Twitter API & SSO: 296kb Tweet UI: 120kb Tweet Composer: 5kb Digits: 236kb
  84. Minimize Network Usage

  85. Network Usage 10X SMALLER 100X FASTER XML PROTOBUF

  86. Batching Requests High Power Low Power Idle 24% 30% 63%

    14% 70%
  87. Reduce Startup Time

  88. Startup Time Thread.start(); Executors.newSingleThreadExecutor();

  89. Startup Time class MyThreadFactory implements ThreadFactory { @Override public Thread

    newThread(Runnable runnable) { Thread thread = new Thread(runnable); thread.setPriority( Process.THREAD_PRIORITY_BACKGROUND); return thread; } }
  90. Improving Startup Time in Fabric Shared Resources

  91. Improving Startup Time in Fabric Shared Resources Sync and Async

    Initialization
  92. Improving Startup Time in Fabric Shared Resources Sync and Async

    Initialization Priorities
  93. Improving Startup Time in Fabric Shared Resources Sync and Async

    Initialization Priorities Dependencies
  94. NDK implements Reporter Crashlytics @DependsOn(Reporter.class) dependency { compile ‘...:crashlytics:+@aar’ }

    Sync Initialization (Handler installed) Async Initialization (Upload Crash) Sync Initialization (Handler installed) Async Initialization (Read Crash) set pending crash interface Reporter extends Dependency Control returned to developer
  95. Lightweight SDKs • Binary size • 3rd party library mindfulness

    • Dalvik Method Count • Modularity • Network usage • Startup time
  96. Support

  97. Support “An API is like a baby”

  98. Support “An API is like a baby” Documentation - Javadocs

    and Instructions
  99. Support “An API is like a baby” Documentation - Javadocs

    and Instructions A Minimalist Approach to Sample Code
  100. Support “An API is like a baby” Documentation - Javadocs

    and Instructions A Minimalist Approach to Sample Code Deprecation and Maintenance plan
  101. Lightweight Powerful

  102. Questions? dev.twitter.com @tsmith