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

Droidcon NYC 2014 - Building First Class Android SDKs

Ty Smith
September 20, 2014

Droidcon NYC 2014 - Building First Class Android SDKs

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

September 20, 2014
Tweet

More Decks by Ty Smith

Other Decks in Technology

Transcript

  1. © 2014
    Building First Class Android SDKs
    Ty Smith
    @tsmith
    Twitter
    1

    View full-size slide

  2. Developers Are Lazy

    View full-size slide

  3. © 2014
    3
    SDKs are taking over!
    ‣ 1.2M Play Store Apps (“Literally Zillions” - Thanks Kevin!)
    ‣ Abs in 7.34% of all apps
    ‣ 88K apps

    View full-size slide

  4. Crashlytics for Android

    View full-size slide

  5. © 2014
    6
    Dashboard

    View full-size slide

  6. © 2014
    7
    Dashboard

    View full-size slide

  7. © 2014
    8
    Dashboard

    View full-size slide

  8. © 2014
    9
    IDE Integrations

    View full-size slide

  9. © 2014
    CLI Build Tools
    10

    View full-size slide

  10. © 2014
    Great SDK Qualities
    ‣ Easy to use
    ‣ Flexible
    ‣ Lightweight
    ‣ Performant
    ‣ Reliable
    ‣ Available
    11

    View full-size slide

  11. © 2014
    A useful SDK
    Let’s build an SDK!
    12

    View full-size slide

  12. © 2014
    Before we get started
    ‣ Jar vs AAR vs APKlib
    ‣ Don’t use APKlib
    ‣ Majority of Android Devs still on Eclipse :-(
    ‣ Eclipse has no AAR support today
    ‣ JAR has no resources support
    13

    View full-size slide

  13. © 2014
    14
    android {
    compileSdkVersion 19
    buildToolsVersion "19.1.0"
    defaultConfig {
    minSdkVersion 14
    targetSdkVersion 19
    }
    }
    SDK build script
    apply plugin: 'com.android.library'

    View full-size slide

  14. © 2014
    15
    public class Sdk {
    public static Sdk with(Activity activity) {
    ...
    }
    }
    Sdk class

    View full-size slide

  15. © 2014
    16
    apply plugin: 'android'
    android {
    compileSdkVersion 19
    buildToolsVersion "19.1.0"
    defaultConfig {
    minSdkVersion 14
    targetSdkVersion 19
    }
    repositories {
    maven { url 'http:/yourSDKrepo.com' }
    }
    dependencies {
    compile 'com.example:sdk:1.0'
    }
    }
    Sample Code Build Script

    View full-size slide

  16. © 2014
    public class MainActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    }
    }
    final Sdk sdk = Sdk.with(this);
    17
    Initializing the SDK

    View full-size slide

  17. © 2014
    Easy to use
    18

    View full-size slide

  18. final Sdk sdk = Sdk.with(this);

    View full-size slide

  19. © 2014
    package="com.example.SDK" >

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


    Api Key: Getting dependencies

    View full-size slide

  20. © 2014
    21
    public class Sdk {
    private static final String API_KEY = "com.example.ApiKey";
    private final Context context;
    private final String apiKey;
    Sdk(Context context, String apiKey) {
    this.context = context;
    this.apiKey = apiKey;
    }
    public static Sdk with(Activity activity) {
    Context ac = activity.getApplicationContext();
    ApplicationInfo ai =
    ac.getPackageManager().getApplicationInfo(ac.getPackageName(),

    PackageManager.GET_META_DATA);
    String apiKey = ai.metaData.getString(API_KEY);
    return new Sdk(ac, apiKey);
    }
    }
    Api Key: Getting dependencies

    View full-size slide

  21. © 2014
    API Design
    22

    View full-size slide

  22. © 2014
    Great API Qualities
    ‣ Intuitive
    ‣ Consistent
    ‣ Simple
    ‣ Easy to use, hard to misuse
    23

    View full-size slide

  23. © 2014
    public class SDK implements SensorEventListener {
    @Override
    public void onSensorChanged(SensorEvent event) {
    if (canVibrate(event)) {
    Vibrator vib = (Vibrator) _appContext

    .getSystemService(Service.VIBRATOR_SERVICE);
    vib.vibrate(500);
    }
    }
    callServer();
    API Design: Generic Hooks

    View full-size slide

  24. © 2014
    /**
    * Set implementation specific parameters, to be send as header parameters
    * @param key
    * @param value
    */
    public void setParam(String key, String value){
    mParamsTable.put(key, value);
    }
    private void callServer() {
    HttpURLConnection conn;
    try {
    conn = (HttpURLConnection) mUrl.openConnection();
    for (String key : mParamsTable.keySet() ) {
    conn.addRequestProperty(key, mParamsTable.get(key));
    }
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    API Design: Generic Hooks

    View full-size slide

  25. © 2014
    // Requires android.permission.READ_PHONE_STATE
    TelephonyManager tMgr = (TelephonyManager)this

    .getSystemService(Context.TELEPHONY_SERVICE);
    sdk.setParam("phone", tMgr.getLine1Number());
    API Design: Generic Hooks

    View full-size slide

  26. © 2014
    public interface SdkCallback {
    public void proximityAlert();
    }
    public static void setProximityCallBack(SdkCallback callback) {
    this.callback = callback;
    }
    @Override
    public void onSensorChanged(SensorEvent event) {
    if (shouldTrigger(event)){
    if (canVibrate()) {
    vibrate();
    }
    callServer();
    if (callback != null) {
    callback.proximityAlert();
    }
    }
    }
    API Design: Callbacks

    View full-size slide

  27. © 2014
    API Design: Callbacks
    28
    sdk.setProximityCallBack(new SdkCallback() {
    @Override
    public void proximityAlert() {
    //do something UI or App Specific that the SDK can't do!
    }
    });

    View full-size slide

  28. © 2014
    sdk.setDebugMode(true);
    @Deprecated
    /**
    * Allows developer to set debug mode
    * @see #setDeveloperMode
    * @deprecated
    */
    public void setDebugMode(boolean debugMode){
    debugMode = debugMode;
    }
    API Design: Deprecation

    View full-size slide

  29. © 2014
    Flexible
    30

    View full-size slide

  30. © 2014
    31
    Builder Object
    public class Sdk {
    class Builder {
    private Context context;
    private String apiKey;
    public Builder(Context context) {
    this.context = context;
    }
    public Builder setApiKey(String apiKey) {
    this.apiKey = apiKey;
    return this;
    }
    public Sdk build() {
    if (apiKey == null) { /* get from manifest */ }
    return new Sdk(context, apiKey);
    }
    }
    }

    View full-size slide

  31. © 2014
    32
    Builder Object
    public class Sdk {
    class Builder {

    private SdkCallback callback;
    public Builder setSdkCallback(SdkCallback callback) {
    this.apiKey = apiKey;
    return this;
    }
    public Sdk build() {

    return new Sdk(context, apiKey, callback);
    }
    }
    }

    View full-size slide

  32. © 2014
    33
    Custom Logger
    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 full-size slide

  33. © 2014
    34
    Custom Logger
    public class Sdk {
    class Builder {
    private Logger logger;
    public Builder setLogger(Logger logger) {
    this.logger = logger;
    }
    public Sdk build() {
    if (logger == null) {
    logger = new DefaultLogger();
    }
    return new Sdk(context, apiKey, logger);
    }
    }
    }

    View full-size slide

  34. © 2014
    Permissions
    35

    View full-size slide

  35. © 2014
    protected boolean canVibrate() {
    String permission = "android.permission.VIBRATE";
    int result =
    context.checkCallingOrSelfPermission(permission);
    return (result == PackageManager.PERMISSION_GRANTED);
    }
    Permissions: Runtime Detection

    View full-size slide

  36. © 2014
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) {
    return formatID(Build.HARDWARE);

    }
    return null;
    Feature Detection

    View full-size slide

  37. © 2014
    try {
    Activity a = startActivityForResult(new
    Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH), 0);
    } catch (ActivityNotFoundException e){ }
    PackageManager pm = 

    context.getApplicationContext().getPackageManager();
    List activities = pm.queryIntentActivities(

    new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH), 0);
    if (activities.size() > 0) {
    // you have an app that can recognize speech
    }
    Feature Detection

    View full-size slide

  38. © 2014
    Multiple Application types
    39
    package com.example;
    import android.app.Service;
    public class MyService extends Service {
    }

    View full-size slide

  39. © 2014
    40
    public class SDK {
    ...
    public static Sdk with(Activity activity) {
    Context ac = activity.getApplicationContext();
    ApplicationInfo ai =
    ac.getPackageManager().getApplicationInfo(

    ac.getPackageName(),PackageManager.GET_META_DATA);
    String apiKey = ai.metaData.getString(API_KEY);

    return new Sdk(ac, apiKey);
    }
    }
    Support All Application Types

    View full-size slide

  40. © 2014
    41
    public class SDK {
    ...

    /**
    *Start SDK without UI functionality
    */
    public static Sdk with(Context context) { 

    Context ac = activity.getApplicationContext();
    ApplicationInfo ai = 

    ac.getPackageManager().getApplicationInfo(

    ac.getPackageName(),PackageManager.GET_META_DATA);
    String apiKey = ai.metaData.getString(API_KEY);
    return new Sdk(ac, apiKey);
    }
    }
    Support All Application Types

    View full-size slide

  41. © 2014
    42
    private WeakReference currentActivity = 

    new WeakReference();

    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    private void registerLifecycleCallbacks() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO
    && context instanceof Application) {
    final Application app = ((Application)context);
    app.registerActivityLifecycleCallbacks(

    new ActivityLifecycleCallbacks() {

    @Override
    public void onActivityResumed(Activity activity) {
    currentActivity.set(activity);
    }

    });
    }
    }
    UI from an Application Context

    View full-size slide

  42. © 2014
    43
    public class Sdk {

    private WeakReference currentActivity = 

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

    View full-size slide

  43. © 2014
    Lightweight
    44

    View full-size slide

  44. © 2014
    Binary Footprint
    ‣ Users prefer fast downloads
    ‣ Association between small and fast
    ‣ Not all networks are created equal
    ‣ Reluctance to add large libraries
    45

    View full-size slide

  45. © 2014
    Third party libraries
    46
    protobuf ours
    KB KB

    View full-size slide

  46. © 2014
    ProGuard - .apk Size
    Without
    ProGuard
    With ProGuard Reduction
    ApiDemo
    2.6MB 2.5MB 4%
    Google I/O App 1.9MB 906KB 53%
    http://www.saikoa.com/downloads/ProGuard_DroidconLondon2012.pdf
    ProGuard

    View full-size slide

  47. © 2014
    task :sdkfootprint => [:testclient] do
    sdk_project_size = File.size?("#{testclient_dir}/SDKAndroidProject/bin/SDKAndroidProject-debug.apk")
    project_size = File.size?("#{testclient_dir}/NonSDKAndroidProject/bin/NonSDKAndroidProject-debug.apk")
    sdk_footprint = (sdk_project_size - project_size) / 1000
    build_time = DateTime.now.strftime("%F %r")
    `echo "SDK footprint is #{sdk_footprint} kb at #{build_time}" > #{artifacts_dir}/sdkfootprint.txt`
    end
    SDK footprint is 61 kb at 2013-11-13
    Reporting Size

    View full-size slide

  48. © 2014
    >./gradlew assemble
    Dalvik Method Count

    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 full-size slide

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

    View full-size slide

  50. © 2014
    Performant
    51

    View full-size slide

  51. © 2014
    Startup Time
    52
    Thread.start();
    Executors.newSingleThreadExecutor();

    View full-size slide

  52. © 2014
    Startup Time
    53
    class MyThreadFactory implements ThreadFactory {
    @Override
    public Thread newThread(Runnable runnable) {
    final Thread thread = new Thread(runnable);
    thread.setPriority(Process.

    THREAD_PRIORITY_BACKGROUND);
    return thread;
    }
    }

    View full-size slide

  53. © 2014
    54
    public class Sdk {
    Sdk(Context context, String apiKey, Logger logger) {
    this.context = context;
    this.apiKey = apiKey;
    this.logger = logger;
    initialize();
    initializeAsync();
    }
    private initialize() {
    //Do light amount of work to get started immediately
    }
    private initializeAsync() {
    executorService.submit(/* Heavyweight work - Network and IO*/);
    }
    }
    Startup Time

    View full-size slide

  54. © 2014
    Network usage
    55
    10x smaller
    100x faster
    XML protobuf

    View full-size slide

  55. © 2014
    // Log.d logs are stripped when you app is compiled with 

    // debuggable=false in the manifest
    sdk.getLogger().d(tag, "Debugging message");
    CPU Usage: Log only in Debug
    // Use your own flag for better control
    if (debugMode){
    sdk.getLogger().w(tag, "Warning message");
    }
    or

    View full-size slide

  56. © 2014
    > adb shell top -m 10
    CPU Usage: Monitoring
    User 11%, System 11%, IOW 0%, IRQ 0%
    User 36 + Nice 2 + Sys 36 + Idle 249 + IOW 1 + IRQ 0 + SIRQ 0 = 324
    PID PR CPU% S #THR VSS RSS PCY UID Name
    7668 0 10% S 36 424680K 95592K fg app_40 com.facebook.katana
    8625 0 5% R 1 1124K 472K fg shell top
    15997 0 4% S 1 1092K 868K fg root /sbin/tpd
    640 0 0% S 33 377260K 54840K fg system com.android.systemui
    202 0 0% S 22 104676K 7556K fg system /system/bin/surfaceflinger
    7395 0 0% S 1 0K 0K fg root kworker/u:2
    93 0 0% S 1 0K 0K fg root mmcqd/0
    438 0 0% S 89 443620K 77172K fg system system_server
    1188 0 0% S 5 5324K 372K fg root /system/bin/mpdecision
    28663 0 0% S 1 0K 0K fg root kworker/0:3

    View full-size slide

  57. © 2014
    Battery consumption
    58

    View full-size slide

  58. © 2014
    Battery Consumption
    59
    High Power Low Power Idle
    Unbundled Transfers Bundled Transfers

    View full-size slide

  59. © 2014
    Reliable
    60

    View full-size slide

  60. © 2014
    try {
    //Do all the things!
    } catch (Exception e){
    //Log something meaningful
    }
    Catch all Exceptions

    View full-size slide

  61. © 2014
    62
    private static final String API_KEY = "com.example.ApiKey";
    private static AtomicBoolean debugMode = new AtomicBoolean();
    public Sdk void with(Context context) {
    final String apiKey = bundle.getString(API_KEY);
    if (debugMode.get() && TextUtils.isEmpty(apiKey)){
    throw new IllegalArgumentException("apiKey cannot be null!");
    } else {
    return null;
    }
    return new Sdk(context, apiKey);
    }
    public static void setDebugMode(boolean debugMode){
    debugMode.set(debugMode);
    }
    Degrade Gracefully

    View full-size slide

  62. © 2014
    > ./gradlew connectedAndroidTests
    com.example.android.SDKListenerTest:....
    com.example.android.SDKTest:..........
    com.example.android.UtilsTest:.............
    Test results for InstrumentationTestRunner=...................................
    Time: 44.579
    OK (35 tests)
    Testing

    View full-size slide

  63. © 2014
    More Testing
    ‣ Android Testing is hard
    ‣ Split into Java project and Android project
    ‣ Use a Mocking framework
    ‣ Use Robolectric
    64

    View full-size slide

  64. © 2014
    Available
    65

    View full-size slide

  65. © 2014
    Javadoc
    66
    /**
    * Sets the proximity callback that is triggered when the 

    * SDK gets a SensorChanged event for PROXIMITY
    * @param callback {@link SdkCallback} the callback to trigger
    */
    public static void setProximityCallBack(SdkCallback callback) {
    callback = callback;
    }
    void com.example.SDK.setProximityCallBack(SdkCallback callback)
    Sets the proximity callback that is triggered when the 

    SDK gets a SensorChanged event for PROXIMITY
    Parameters:
    callback the callback to trigger

    View full-size slide

  66. © 2014
    Sample code
    ‣ Will be referenced for usage
    ‣ People will copy and paste
    ‣ Keep it as concise
    67

    View full-size slide

  67. © 2014
    68
    Maven Central

    View full-size slide

  68. © 2014
    apply from: 'https://raw.github.com/chrisbanes/gradle-mvn-push/
    master/gradle-mvn-push.gradle'
    apply plugin: ‘android-library'
    android {
    compileSdkVersion 19
    buildToolsVersion "19.1.0"
    defaultConfig {
    minSdkVersion 14
    targetSdkVersion 19
    }
    }
    Maven Push
    https://github.com/chrisbanes/gradle-mvn-push

    View full-size slide

  69. © 2014
    Gradle.properties
    POM_NAME=Proximity Sensor SDK
    POM_ARTIFACT_ID=library
    POM_PACKAGING=aar
    VERSION_NAME=1.0
    VERSION_CODE=1
    GROUP=com.example
    https://github.com/chrisbanes/gradle-mvn-push

    View full-size slide

  70. © 2014
    > ./gradlew uploadArchives
    Distribution: Upload
    https://github.com/chrisbanes/gradle-mvn-push

    View full-size slide

  71. © 2014
    Great SDK Qualities
    ‣ Easy to use
    ‣ Flexible
    ‣ Lightweight
    ‣ Performant
    ‣ Reliable
    ‣ Available
    72

    View full-size slide

  72. © 2014
    73
    Q & A
    Ty Smith
    @tsmith
    Twitter

    View full-size slide