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

Droidcon NYC 2014 - Building First Class Android SDKs

7a3baf2e1158e358885cdf7e89b9aa55?s=47 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.

7a3baf2e1158e358885cdf7e89b9aa55?s=128

Ty Smith

September 20, 2014
Tweet

Transcript

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

    Twitter 1
  2. Developers Are Lazy

  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
  4. Crashlytics for Android

  5. None
  6. © 2014 6 Dashboard

  7. © 2014 7 Dashboard

  8. © 2014 8 Dashboard

  9. © 2014 9 IDE Integrations

  10. © 2014 CLI Build Tools 10

  11. © 2014 Great SDK Qualities ‣ Easy to use ‣

    Flexible ‣ Lightweight ‣ Performant ‣ Reliable ‣ Available 11
  12. © 2014 A useful SDK Let’s build an SDK! 12

  13. © 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
  14. © 2014 14 android { compileSdkVersion 19 buildToolsVersion "19.1.0" defaultConfig

    { minSdkVersion 14 targetSdkVersion 19 } } SDK build script apply plugin: 'com.android.library'
  15. © 2014 15 public class Sdk { public static Sdk

    with(Activity activity) { ... } } Sdk class
  16. © 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
  17. © 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
  18. © 2014 Easy to use 18

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

  20. © 2014 <manifest ... package="com.example.SDK" > <application ... > ...

    <meta-data android:value="11235813213455" 
 android:name=“com.example.ApiKey” /> </application> </manifest> Api Key: Getting dependencies
  21. © 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
  22. © 2014 API Design 22

  23. © 2014 Great API Qualities ‣ Intuitive ‣ Consistent ‣

    Simple ‣ Easy to use, hard to misuse 23
  24. © 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
  25. © 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
  26. © 2014 // Requires android.permission.READ_PHONE_STATE TelephonyManager tMgr = (TelephonyManager)this
 .getSystemService(Context.TELEPHONY_SERVICE);

    sdk.setParam("phone", tMgr.getLine1Number()); API Design: Generic Hooks
  27. © 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
  28. © 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! } });
  29. © 2014 sdk.setDebugMode(true); @Deprecated /** * Allows developer to set

    debug mode * @see #setDeveloperMode * @deprecated */ public void setDebugMode(boolean debugMode){ debugMode = debugMode; } API Design: Deprecation
  30. © 2014 Flexible 30

  31. © 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); } } }
  32. © 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); } } }
  33. © 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); }
  34. © 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); } } }
  35. © 2014 Permissions 35 <uses-permission android:name="android.permission.INTERNET"/>

  36. © 2014 protected boolean canVibrate() { String permission = "android.permission.VIBRATE";

    int result = context.checkCallingOrSelfPermission(permission); return (result == PackageManager.PERMISSION_GRANTED); } Permissions: Runtime Detection
  37. © 2014 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) { return formatID(Build.HARDWARE);
 }

    return null; Feature Detection
  38. © 2014 try { Activity a = startActivityForResult(new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH), 0);

    } catch (ActivityNotFoundException e){ } PackageManager pm = 
 context.getApplicationContext().getPackageManager(); List<ResolveInfo> activities = pm.queryIntentActivities(
 new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH), 0); if (activities.size() > 0) { // you have an app that can recognize speech } Feature Detection
  39. © 2014 Multiple Application types 39 package com.example; import android.app.Service;

    public class MyService extends Service { }
  40. © 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
  41. © 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
  42. © 2014 42 private WeakReference<Activity> currentActivity = 
 new WeakReference<Activity>();

    … @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
  43. © 2014 43 public class Sdk { … private WeakReference<Activity>

    currentActivity = 
 new WeakReference<Activity>(); … 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
  44. © 2014 Lightweight 44

  45. © 2014 Binary Footprint ‣ Users prefer fast downloads ‣

    Association between small and fast ‣ Not all networks are created equal ‣ Reluctance to add large libraries 45
  46. © 2014 Third party libraries 46 protobuf ours KB KB

  47. © 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
  48. © 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
  49. © 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
  50. © 2014 > git clone git@github.com: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. <root>: 65490 : 3 android: 6837 accessibilityservice: 6 bluetooth: 2 content: 248 pm: 22 res: 45 ... com: 53881 adjust: 283 sdk: 283
  51. © 2014 Performant 51

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

  53. © 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; } }
  54. © 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
  55. © 2014 Network usage 55 10x smaller 100x faster XML

    protobuf
  56. © 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
  57. © 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
  58. © 2014 Battery consumption 58

  59. © 2014 Battery Consumption 59 High Power Low Power Idle

    Unbundled Transfers Bundled Transfers
  60. © 2014 Reliable 60

  61. © 2014 try { //Do all the things! } catch

    (Exception e){ //Log something meaningful } Catch all Exceptions
  62. © 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
  63. © 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
  64. © 2014 More Testing ‣ Android Testing is hard ‣

    Split into Java project and Android project ‣ Use a Mocking framework ‣ Use Robolectric 64
  65. © 2014 Available 65

  66. © 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
  67. © 2014 Sample code ‣ Will be referenced for usage

    ‣ People will copy and paste ‣ Keep it as concise 67
  68. © 2014 68 Maven Central

  69. © 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
  70. © 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
  71. © 2014 > ./gradlew uploadArchives Distribution: Upload https://github.com/chrisbanes/gradle-mvn-push

  72. © 2014 Great SDK Qualities ‣ Easy to use ‣

    Flexible ‣ Lightweight ‣ Performant ‣ Reliable ‣ Available 72
  73. © 2014 73 Q & A Ty Smith @tsmith Twitter