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

Droidcon Montreal - Building First Class Sdks: A Fabric Case Study

Droidcon Montreal - 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 Fabric team, we've detected a significant percentage of issues caused by third-party SDKs.

Fabric, formerly Crashlytics, is well-known for its focus on SDK quality, and has been deployed on billions of devices. In this session, attendees will learn the skills to develop and distribute SDKs for Android. We’ll cover an overview of Fabric, deep dive into technical decisions we made, and present the learnings on developing an SDK for stability, testability, performance, overall footprint size, and, most importantly, exceptional ease of implementation. Over the course of the session, 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

April 09, 2015
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, 5); Crashlytics.setListener(createCrashlyticsListener()); Crashlytics.setPinningInfo(createPinningInfoProvider()) Crashlytics.getInstance().setDebugMode(true);

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

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

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

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

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

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

    Callback<Fabric>() { void success(Fabric fabric) { } void failure(Exception exception) { } }) .build();
  29. 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); }
  30. 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); } ... }
  31. 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); } }
  32. 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>
  33. Great API Traits Intuitive

  34. Great API Traits Intuitive Consistent

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

    misuse
  36. Handling Failure

  37. 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; }
  38. Catch unexpected exceptions try { //Do all the things! }

    catch (Exception e){ //Log something meaningful }
  39. Gracefully Degrade if (TextUtils.isEmpty(apiKey) if (debuggable){ throw new IllegalArgumentException(“apiKey is

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

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

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

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

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

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

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

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

    e){ }
  48. 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 }
  49. 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'
  50. Library Detection Cont public class DefaultClient implements Client { private

    final Client client; public DefaultClient() { if (hasOkHttpOnClasspath()) { client = new OkClient(); } else { client = new UrlConnectionClient(); } } … }
  51. Testable SDKs

  52. Make it Testable/Mockable Avoid static methods

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

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

    accessing fields directly Utilize interfaces around your public API
  55. Make it Testable/Mockable Avoid static methods Avoid final classes and

    accessing fields directly Utilize interfaces around your public API Avoid mocking more than one level deep
  56. 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(); } }
  57. 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(); } … }
  58. Powerful SDKs •Ease of Integration •Extensibility •Handling Failure •Runtime detection

    •Testable
  59. Lightweight

  60. Binary Size

  61. 3rd Party Library Mindfulness PROTOBUF KB OURS KB

  62. 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 }
  63. Dalvik Method Count

  64. 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
  65. 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
  66. Minimize Network Usage

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

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

    14% 70%
  69. Modularity How to avoid a “Monolith Abomination”

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

  71. Fabric.aar Cache Settings Concurrency Events Network Persistence

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

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

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

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

    TweetComposer. aar Twitter.aar Interface Features Kit Common Services
  76. 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
  77. Reduce Startup Time

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

  79. 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; } }
  80. Improving Startup Time in Fabric Shared Resources

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

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

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

    Initialization Priorities Dependencies
  84. Lightweight SDKs • Binary size • Dalvik Method Count •

    Network usage • Modularity • Startup time
  85. Support

  86. Support “An API is like a baby”

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

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

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

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

  91. Questions? dev.twitter.com @tsmith