Droidcon - Building First Class Sdks: A Fabric Case Study

7a3baf2e1158e358885cdf7e89b9aa55?s=47 Ty Smith
August 27, 2015

Droidcon - 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

August 27, 2015
Tweet

Transcript

  1. 2.
  2. 3.
  3. 4.
  4. 5.
  5. 6.

    Fabric Sample App Cannonball Open source for Android & iOS:

    github.com/twitterdev #LetMeExplainYou
  6. 11.

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

    and AARs. Open or Closed Source - Associated licensing
  7. 12.

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

    and AARs. Open or Closed Source - Associated licensing Hosting the artifacts
  8. 14.
  9. 17.

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

    <meta-data android:value="11235813213455" android:name=“com.fabric.ApiKey” /> </application> </manifest>
  10. 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); }
  11. 19.

    Detecting Dependencies public TwitterApiClient { final Session session; public TwitterApiClient(Session

    session) { this.session = session; } public TwitterApiClient() { this.session = Twitter.getSessionManager().getActiveSession(); } }
  12. 27.

    Callbacks Fabric fabric = new Fabric.Builder(this) .kits(new Crashlytics()) .initializationCallback( new

    Callback<Fabric>() { void success(Fabric fabric) { } void failure(Exception exception) { } }) .build();
  13. 28.

    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); }
  14. 29.

    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); } ... }
  15. 30.

    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); } }
  16. 31.

    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>
  17. 36.

    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; }
  18. 37.

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

    catch (Exception e){ //Log something meaningful }
  19. 42.

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

    Manifest.permission.ACCESS_NETWORK_STATE; int result = context.checkCallingOrSelfPermission(permission); return (result == PackageManager.PERMISSION_GRANTED); }
  20. 43.

    Optional Features <manifest … package="com.example.SDK" > <application ... > …

    </application> <uses-feature android:name="android.hardware.camera" android:required=”false” /> </manifest>
  21. 44.

    Features Detection protected boolean hasCamera(Context context) { PackageManager pm =

    context.getPackageManager(); String camera = PackageManager.FEATURE_CAMERA; return pm.hasSystemFeature(camera); }
  22. 47.

    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 }
  23. 48.

    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'
  24. 49.

    Classpath Detection Cont public class DefaultClient implements Client { private

    final Client client; public DefaultClient() { if (hasOkHttpOnClasspath()) { client = new OkClient(); } else { client = new UrlConnectionClient(); } } … }
  25. 53.

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

    accessing fields directly Utilize interfaces around your public API
  26. 54.

    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
  27. 55.

    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(); } }
  28. 56.

    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(); } … }
  29. 61.

    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 }
  30. 63.

    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
  31. 64.

    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
  32. 73.

    Fabric.aar Cache Settings Concurrency Events Network Persistence Core A C

    B Interface Interface Features SDK Common Services
  33. 74.

    Fabric.aar Cache Settings Concurrency Events Network Persistence TwitterCore.aar TweetUi.aar Digits.aar

    TweetComposer. aar Twitter.aar Interface Features SDK Common Services
  34. 77.

    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; } }
  35. 80.
  36. 81.

    Lightweight SDKs • Binary size • Dalvik Method Count •

    Network usage • Modularity • Startup time
  37. 82.
  38. 85.

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

    and Instructions A Minimalist Approach to Sample Code
  39. 86.

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

    and Instructions A Minimalist Approach to Sample Code Deprecation and Maintenance plan