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.

Ty Smith

April 09, 2015
Tweet

More Decks by Ty Smith

Other Decks in Programming

Transcript

  1. dev.twitter.com @tsmith
    Building First Class SDKs
    A Fabric Case Study
    Ty Smith
    Sr. Android Engineer

    View Slide

  2. View Slide

  3. View Slide

  4. View Slide

  5. View Slide

  6. Fabric Sample App
    Cannonball
    Open source for iOS & Android:
    github.com/twitterdev

    View Slide

  7. Build
    an
    SDK

    View Slide

  8. Considerations

    View Slide

  9. Considerations
    What need are you serving?

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  13. Powerful
    Lightweight

    View Slide

  14. Powerful

    View Slide

  15. Ease of
    Integration

    View Slide

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

    View Slide

  17. Detecting Dependencies
    package="com.example.SDK" >


    android:name=“com.fabric.ApiKey” />


    View Slide

  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);
    }

    View Slide

  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);
    }

    View Slide

  20. Detecting Dependencies
    public TwitterApiClient {
    final Session session;
    public TwitterApiClient(Session session) {
    this.session = session;
    }
    public TwitterApiClient() {
    this.session =
    Twitter.getSessionManager().getActiveSession();
    }
    }

    View Slide

  21. Extensible
    API

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  26. Fluent Pattern
    Crashlytics crashlytics = new Crashlytics.Builder()
    .delay(1)
    .listener(createCrashlyticsListener())
    .pinningInfo(createPinningInfoProvider())
    .build();
    Fabric.with(this, crashlytics);

    View Slide

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

    View Slide

  28. Callbacks
    Fabric fabric = new Fabric.Builder(this)
    .kits(new Crashlytics())
    .initializationCallback(
    new Callback() {
    void success(Fabric fabric) { }
    void failure(Exception exception) { }
    })
    .build();

    View Slide

  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);
    }

    View Slide

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

    View Slide

  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);
    }
    }

    View Slide

  32. Extensible View Styles
    <br/><item name="tw__container_bg_color"><br/>@color/tw__tweet_light_container_bg_color<br/></item><br/><item name="tw__primary_text_color"><br/>@color/tw__tweet_light_primary_text_color<br/></item><br/><item name="tw__action_color"><br/>@color/tw__tweet_action_color<br/></item><br/>

    View Slide

  33. Great API Traits
    Intuitive

    View Slide

  34. Great API Traits
    Intuitive
    Consistent

    View Slide

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

    View Slide

  36. Handling
    Failure

    View Slide

  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;
    }

    View Slide

  38. Catch unexpected exceptions
    try {
    //Do all the things!
    } catch (Exception e){
    //Log something meaningful
    }

    View Slide

  39. Gracefully Degrade
    if (TextUtils.isEmpty(apiKey)
    if (debuggable){
    throw new IllegalArgumentException(“apiKey is null!");
    } else {
    return null;
    }
    }

    View Slide

  40. Runtime
    Detection

    View Slide

  41. Minimizing Permissions

    View Slide

  42. Minimizing Permissions
    crashlytics.setUserEmail(“[email protected]”);

    View Slide

  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);
    }

    View Slide

  44. Optional Features
    package="com.example.SDK" >



    android:name="android.hardware.camera"
    android:required=”false” />

    View Slide

  45. Features Detection
    protected boolean hasCamera(Context context) {
    PackageManager pm = context.getPackageManager();
    String camera = PackageManager.FEATURE_CAMERA;
    return pm.hasSystemFeature(camera);
    }

    View Slide

  46. Android Version Detection
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO)
    {
    return formatID(Build.HARDWARE);}
    }
    return null;

    View Slide

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

    View Slide

  48. Intent Detection Cont.
    PackageManager pm =
    context.getApplicationContext().getPackageManager();
    List activities = pm.queryIntentActivities(
    new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH),0);
    if (activities.size() > 0) {
    //an app exists that can recognize speech
    }

    View Slide

  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'

    View Slide

  50. Library Detection Cont
    public class DefaultClient implements Client {
    private final Client client;
    public DefaultClient() {
    if (hasOkHttpOnClasspath()) {
    client = new OkClient();
    } else {
    client = new UrlConnectionClient();
    }
    }

    }

    View Slide

  51. Testable
    SDKs

    View Slide

  52. Make it Testable/Mockable
    Avoid static methods

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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 getTweets() {
    return network.getTweets();
    }
    }

    View Slide

  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 getTweets() {
    return getNetwork().getTweets();
    }

    }

    View Slide

  58. Powerful SDKs
    ●Ease of Integration
    ●Extensibility
    ●Handling Failure
    ●Runtime detection
    ●Testable

    View Slide

  59. Lightweight

    View Slide

  60. Binary Size

    View Slide

  61. 3rd Party Library Mindfulness
    PROTOBUF
    KB
    OURS
    KB

    View Slide

  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
    }

    View Slide

  63. Dalvik
    Method
    Count

    View Slide

  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

    View Slide

  65. Dalvik Method Count
    > git clone [email protected]:mihaip/dex-method-counts.git
    > cd dex-method-counts
    > ant jar
    > ./dex-method-counts path/to/App.apk
    Read in 65490 method IDs.
    : 65490
    : 3
    accessibilityservice: 6
    bluetooth: 2
    content: 248
    pm: 22
    res: 45
    com: 53881
    example: 283
    sdk: 283

    View Slide

  66. Minimize
    Network
    Usage

    View Slide

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

    View Slide

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

    View Slide

  69. Modularity
    How to avoid a “Monolith Abomination”

    View Slide

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

    View Slide

  71. Fabric.aar
    Cache Settings Concurrency Events Network
    Persistence

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  75. Fabric.aar
    Cache Settings Concurrency Events Network
    Persistence
    TwitterCore.aar
    TweetUi.aar Digits.aar
    TweetComposer.
    aar
    Twitter.aar
    Interface
    Features
    Kit Common Services

    View Slide

  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

    View Slide

  77. Reduce
    Startup
    Time

    View Slide

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

    View Slide

  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;
    }
    }

    View Slide

  80. Improving Startup Time in Fabric
    Shared Resources

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  84. Lightweight SDKs
    ● Binary size
    ● Dalvik Method Count
    ● Network usage
    ● Modularity
    ● Startup time

    View Slide

  85. Support

    View Slide

  86. Support
    “An API is like a baby”

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  90. Lightweight
    Powerful

    View Slide

  91. Questions?
    dev.twitter.com
    @tsmith

    View Slide