$30 off During Our Annual Pro Sale. View Details »

AnDevCon 2014 - Building First Class SDKs

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.

Ty Smith

November 21, 2014
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);

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  28. 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

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  33. 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

  34. Great API Traits
    Intuitive

    View Slide

  35. Great API Traits
    Intuitive
    Consistent

    View Slide

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

    View Slide

  37. Handling
    Failure

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  41. Runtime
    Detection

    View Slide

  42. Minimizing Permissions

    View Slide

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

    View Slide

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

    View Slide

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



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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

  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'

    View Slide

  51. 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

  52. Multiple
    Application
    Types

    View Slide

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

    View Slide

  54. UI from Application Context
    WeakReference 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);
    }
    });
    }

    View Slide

  55. UI from Application Context
    WeakReference 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();
    }
    }
    }

    View Slide

  56. Testable
    SDKs

    View Slide

  57. Make it Testable/Mockable
    Avoid static methods

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

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

    }

    View Slide

  63. Using in Tests
    package com.example.Tweeter;
    public class MyTest extends AndroidTestCase {
    Tweeter tweeter;
    @Override
    public void setup() {
    tweeter = mock(Tweeter.class);
    }
    ...
    }

    View Slide

  64. Powerful SDKs
    ●Ease of Integration
    ●Extensibility
    ●Handling Failure
    ●Runtime detection
    ●Support all app types
    ●Testable

    View Slide

  65. Lightweight

    View Slide

  66. Binary Size

    View Slide

  67. Binary Size

    View Slide

  68. 3rd Party
    Library
    Mindfulness

    View Slide

  69. 3rd Party
    Library Mindfulness

    View Slide

  70. 3rd Party Library Mindfulness
    PROTOBUF
    KB
    OURS
    KB

    View Slide

  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
    }

    View Slide

  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
    }

    View Slide

  73. Dalvik
    Method
    Count

    View Slide

  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

    View Slide

  75. 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

  76. Modularity
    How to avoid a “Monolith Abomination”

    View Slide

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

    View Slide

  78. Fabric.aar
    Cache Settings Concurrency Events Network
    Persistence

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  82. 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

  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

    View Slide

  84. Minimize
    Network
    Usage

    View Slide

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

    View Slide

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

    View Slide

  87. Reduce
    Startup
    Time

    View Slide

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

    View Slide

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

    View Slide

  90. Improving Startup Time in Fabric
    Shared Resources

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  95. Lightweight SDKs
    ● Binary size
    ● 3rd party library mindfulness
    ● Dalvik Method Count
    ● Modularity
    ● Network usage
    ● Startup time

    View Slide

  96. Support

    View Slide

  97. Support
    “An API is like a baby”

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  101. Lightweight
    Powerful

    View Slide

  102. Questions?
    dev.twitter.com
    @tsmith

    View Slide