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 3 SDKs are taking over! ‣ 1.2M Play

    Store Apps (“Literally Zillions” - Thanks Kevin!) ‣ Abs in 7.34% of all apps ‣ 88K apps
  2. © 2014 Great SDK Qualities ‣ Easy to use ‣

    Flexible ‣ Lightweight ‣ Performant ‣ Reliable ‣ Available 11
  3. © 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
  4. © 2014 14 android { compileSdkVersion 19 buildToolsVersion "19.1.0" defaultConfig

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

    with(Activity activity) { ... } } Sdk class
  6. © 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
  7. © 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
  8. © 2014 <manifest ... package="com.example.SDK" > <application ... > ...

    <meta-data android:value="11235813213455" 
 android:name=“com.example.ApiKey” /> </application> </manifest> Api Key: Getting dependencies
  9. © 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
  10. © 2014 Great API Qualities ‣ Intuitive ‣ Consistent ‣

    Simple ‣ Easy to use, hard to misuse 23
  11. © 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
  12. © 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
  13. © 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
  14. © 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! } });
  15. © 2014 sdk.setDebugMode(true); @Deprecated /** * Allows developer to set

    debug mode * @see #setDeveloperMode * @deprecated */ public void setDebugMode(boolean debugMode){ debugMode = debugMode; } API Design: Deprecation
  16. © 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); } } }
  17. © 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); } } }
  18. © 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); }
  19. © 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); } } }
  20. © 2014 protected boolean canVibrate() { String permission = "android.permission.VIBRATE";

    int result = context.checkCallingOrSelfPermission(permission); return (result == PackageManager.PERMISSION_GRANTED); } Permissions: Runtime Detection
  21. © 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
  22. © 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
  23. © 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
  24. © 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
  25. © 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
  26. © 2014 Binary Footprint ‣ Users prefer fast downloads ‣

    Association between small and fast ‣ Not all networks are created equal ‣ Reluctance to add large libraries 45
  27. © 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
  28. © 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
  29. © 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
  30. © 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. <root>: 65490 : 3 android: 6837 accessibilityservice: 6 bluetooth: 2 content: 248 pm: 22 res: 45 ... com: 53881 adjust: 283 sdk: 283
  31. © 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; } }
  32. © 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
  33. © 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
  34. © 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
  35. © 2014 Battery Consumption 59 High Power Low Power Idle

    Unbundled Transfers Bundled Transfers
  36. © 2014 try { //Do all the things! } catch

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

    Split into Java project and Android project ‣ Use a Mocking framework ‣ Use Robolectric 64
  39. © 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
  40. © 2014 Sample code ‣ Will be referenced for usage

    ‣ People will copy and paste ‣ Keep it as concise 67
  41. © 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
  42. © 2014 Great SDK Qualities ‣ Easy to use ‣

    Flexible ‣ Lightweight ‣ Performant ‣ Reliable ‣ Available 72