Share
[Mobius '18] Things I wish I knew when I started building Android Libraries/SDK -Vol 2
Building an Android library in the current times is way different than what it used to be earlier. Things have changed considerably and keeping up to date with them is now a necessity rather than just some acquirable knowledge.
This session is a second part to the last year's talk at Droidcon Berlin 2017 which was Vol. 1. In this session, you will dive deeper into best practices and ways of architecting Android libraries. You will get to learn about the common pitfalls and how to overcome them by using the right approach such as leveraging architecture components and making your Android libraries lifecycle-aware. You will also understand how one can leverage Kotlin language when developing Android libraries as well as information around API design and exploring the path to becoming a better Android Library Developer.
By the end of this session, you will be all set to build Android libraries that scale and have API which contributes to the developer's happiness.
Nishant Srivastava
Transcript
133 slides
-
Side A 1. How does Gradle process Android Libraries 2.
Android Archive(AAR) 3. Why doesn’t my AAR download transitive dependencies? 4. Maven Artifact 5. POM? 6. Bundled Proguard Configs Side B 7. Modularization 8. Avoiding Resource Name Conflicts 9. minSdkVersion Restriction 10. Access Visibility vs Code Organization 11. Lifecycle Aware Android Library 12. Auto Init Android Library 3 @nisrulz -
How does Gradle process Android Libraries repositories{ jCenter() } dependencies
{ implementation 'com.example.myawesomelib:1.0.0' ... } 5 @nisrulz -
Sensey’s build.gradle dependencies { ... // Other testing dependencies //
Transitive dependency: Support Compat library implementation "com.android.support:support-compat:27.0.2" } 13 @nisrulz -
Using an AAR as dependency repositories{ flatDir{ dirs 'libs' }
} dependencies { implementation(name:'nameOfYourAARFileWithoutExtension', ext:'aar') } @nisrulz 15 -
Using an AAR as dependency repositories{ flatDir{ dirs 'libs' }
} dependencies { implementation(name:'nameOfYourAARFileWithoutExtension', ext:'aar') } // No transitive dependencies of the library are downloaded 16 @nisrulz -
POM • Project Object Model • XML file • Configuration
details used by Maven to build the project 23 @nisrulz -
POM POM of Sensey Android Library <?xml version="1.0" encoding="UTF-8"?> <project
xsi:schemaLocation="..." xmlns="..."xmlns:xsi="..."> <modelVersion>4.0.0</modelVersion> <groupId>com.github.nisrulz</groupId><artifactId>sensey</artifactId><version>1.8.0</version> <packaging>aar</packaging><name>sensey</name> <description>Android library which makes playing with sensor events & detecting gestures a breeze.</description> <url>https://github.com/nisrulz/sensey</url> <licenses> <license> <name>The Apache Software License, Version 2.0</name><url>http://www.apache.org/licenses/LICENSE-2.0.txt</url> </license> </licenses> <developers> <developer><id>nisrulz</id><name>Nishant Srivastava</name><email>[email protected]</email></developer> </developers> <scm> <connection>https://github.com/nisrulz/sensey.git</connection> <developerConnection>https://github.com/nisrulz/sensey.git</developerConnection> <url>https://github.com/nisrulz/sensey</url> </scm> <dependencies> <dependency> <groupId>com.android.support</groupId> <artifactId>support-compat</artifactId><version>27.0.2</version> <scope>runtime</scope> </dependency> <dependency> ... </dependency> </dependencies> </project> 24 @nisrulz -
POM POM of Sensey Android Library <?xml version="1.0" encoding="UTF-8"?> <project
xsi:schemaLocation="..." xmlns="..."xmlns:xsi="..."> <modelVersion>4.0.0</modelVersion> <groupId>com.github.nisrulz</groupId><artifactId>sensey</artifactId><version>1.8.0</version> <packaging>aar</packaging><name>sensey</name> <description>Android library which makes playing with sensor events & detecting gestures a breeze.</description> <url>https://github.com/nisrulz/sensey</url> <licenses> <license> <name>The Apache Software License, Version 2.0</name><url>http://www.apache.org/licenses/LICENSE-2.0.txt</url> </license> </licenses> <developers> <developer><id>nisrulz</id><name>Nishant Srivastava</name><email>[email protected]</email></developer> </developers> <scm> <connection>https://github.com/nisrulz/sensey.git</connection> <developerConnection>https://github.com/nisrulz/sensey.git</developerConnection> <url>https://github.com/nisrulz/sensey</url> </scm> <dependencies> <dependency> <groupId>com.android.support</groupId> <artifactId>support-compat</artifactId><version>27.0.2</version> <scope>runtime</scope> </dependency> <dependency> ... </dependency> </dependencies> </project> 25 @nisrulz -
POM POM of Sensey Android Library <?xml version="1.0" encoding="UTF-8"?> <project
xsi:schemaLocation="..." xmlns="..."xmlns:xsi="..."> ... <dependencies> <dependency> <groupId>com.android.support</groupId> <artifactId>support-compat</artifactId><version>27.0.2</version> <scope>runtime</scope> </dependency> <dependency> ... </dependency> </dependencies> </project> 26 @nisrulz -
Bundled Proguard Configs android { release { minifyEnabled true //
Rules to be used during the AAR generation proguardFiles 'proguard-rules-for-building-library.pro' // Rules appended to the integrating app consumerProguardFiles 'proguard-rules-for-using-library.pro' } ... } 28 @nisrulz -
Bundled Proguard Configs # Fuel’s bundled Proguard file # Without
specifically keeping this class, # callbacks on android don't function properly. -keep class com.github.kittinunf.fuel.android.util.AndroidEnvironment 31 @nisrulz -
Bundled Proguard Configs # To check the merged configuration #
Add the below to your current config -printconfiguration proguard-merged-config.txt 32 @nisrulz -
Bundled Proguard Configs # DON'T DO THIS -dontobfuscate -optimizations !code/allocation/variable
# Effectively no optimizations -keep public class * { public protected *; } 33 @nisrulz -
Bundled Proguard Configs # DON'T DO THIS # Adding the
below in library proguard rules disables # the optimizations in the Android app -dontoptimize 34 @nisrulz -
Modularization dependencies { def libVer = {latest_version} // Base +
Ads Bundled Library implementation "com.github.nisrulz:easydeviceinfo:$libVer" // Base Library implementation "com.github.nisrulz:easydeviceinfo-base:$libVer" // Ads Library implementation "com.github.nisrulz:easydeviceinfo-ads:$libVer" } 38 @nisrulz -
Modularization <!-- POM file for com.github.nisrulz:easydeviceinfo --> <project xsi:schemaLocation="..." xmlns="..."
xmlns:xsi="..."> ... <dependencies> <dependency> <groupId>com.github.nisrulz</groupId> <artifactId>easydeviceinfo-ads</artifactId> <version>2.5.0</version> <scope>compile</scope> </dependency> <dependency> <groupId>com.github.nisrulz</groupId> <artifactId>easydeviceinfo-base</artifactId> <version>2.5.0</version> <scope>compile</scope> </dependency> ... </dependencies> </project> 39 @nisrulz -
Modularization <!-- POM file for com.github.nisrulz:easydeviceinfo--> <project xsi:schemaLocation="..." xmlns="..." xmlns:xsi="...">
... <dependencies> <dependency> <groupId>com.github.nisrulz</groupId> <artifactId>easydeviceinfo-ads</artifactId> <version>2.5.0</version> <scope>compile</scope> </dependency> <dependency> <groupId>com.github.nisrulz</groupId> <artifactId>easydeviceinfo-base</artifactId> <version>2.5.0</version> <scope>compile</scope> </dependency> ... </dependencies> </project> 40 @nisrulz -
Modularization <!-- POM file for com.github.nisrulz:easydeviceinfo--> <project xsi:schemaLocation="..." xmlns="..." xmlns:xsi="...">
... <dependencies> <dependency> <groupId>com.github.nisrulz</groupId> <artifactId>easydeviceinfo-ads</artifactId> <version>2.5.0</version> <scope>compile</scope> </dependency> <dependency> <groupId>com.github.nisrulz</groupId> <artifactId>easydeviceinfo-base</artifactId> <version>2.5.0</version> <scope>compile</scope> </dependency> ... </dependencies> </project> 41 @nisrulz -
Modularization <!-- POM file for com.github.nisrulz:easydeviceinfo-ads --> <project xsi:schemaLocation="..." xmlns="..."
xmlns:xsi="..."> ... <dependencies> <dependency> <groupId>com.github.nisrulz</groupId> <artifactId>easydeviceinfo-common</artifactId><version>2.5.0</version> <scope>compile</scope> </dependency> <dependency> <groupId>com.google.android.gms</groupId> <artifactId>play-services-ads-identifier</artifactId><version>16.0.0</version> <scope>runtime</scope> </dependency> ... </dependencies> </project> 42 @nisrulz -
Modularization <!-- POM file for com.github.nisrulz:easydeviceinfo-ads --> <project xsi:schemaLocation="..." xmlns="..."
xmlns:xsi="..."> ... <dependencies> <dependency> <groupId>com.github.nisrulz</groupId> <artifactId>easydeviceinfo-common</artifactId><version>2.5.0</version> <scope>compile</scope> </dependency> <dependency> <groupId>com.google.android.gms</groupId> <artifactId>play-services-ads-identifier</artifactId><version>16.0.0</version> <scope>runtime</scope> </dependency> ... </dependencies> </project> 43 @nisrulz -
Conflict occurs between a library & app resource > Project
will not compile What happens when... 45 @nisrulz -
Conflict occurs between 2 libraries integrated in the app >
Resources from library defined first in build.gradle gets included What happens when... 46 @nisrulz -
Add a prefix to all your resources. How? • Enforce
this in Android Studio Solution? 50 @nisrulz -
Add a prefix to all your resources. How? • Enforce
this in Android Studio • Declare in build.gradle of library android { resourcePrefix 'YOUR_PREFIX_' // i.e 'sensey_' } Solution? 51 @nisrulz -
Solution? Resource named ‘app_name’ does not start with the project’s
resource prefix ‘sensey_’; Rename to `sensey_app_name`? 52 @nisrulz -
Manifest merger failed : uses-sdk:minSdkVersion 14 cannot be smaller than
version 21 declared in library [:sensey] Suggestion: + Use a compatible library with a minSdk of at most 14 + Increase this project's minSdk version to at least 21 + Use tools:overrideLibrary="com.github.nisrulz.sensey" to force usage (may lead to runtime failures) minSdk Restriction 56 @nisrulz -
Manifest merger failed : uses-sdk:minSdkVersion 14 cannot be smaller than
version 21 declared in library [:sensey] Suggestion: + Use a compatible library with a minSdk of at most 14 + Increase this project's minSdk version to at least 21 + Use tools:overrideLibrary="com.github.nisrulz.sensey" to force usage (may lead to runtime failures) minSdk Restriction 57 @nisrulz -
Manifest merger failed : uses-sdk:minSdkVersion 14 cannot be smaller than
version 21 declared in library [:sensey] Suggestion: + Use a compatible library with a minSdk of at most 14 + Increase this project's minSdk version to at least 21 + Use tools:overrideLibrary="com.github.nisrulz.sensey" to force usage (may lead to runtime failures) minSdk Restriction 58 @nisrulz -
Visibility vs Organization • Code organized in individual packages; everything
is public 63 @nisrulz -
Visibility vs Organization • Code organized inside one package; everything
is package private and only public on demand 64 @nisrulz -
Visibility vs Organization • Code organized inside the module i.e
individual packages; everything is internal and only public on demand (in Kotlin land) • Drawback ◦ Need dependency on kotlin std library 65 @nisrulz -
Visibility vs Organization // file name: exampleLibrary.kt // module name:
example package com.example.library 66 @nisrulz -
Visibility vs Organization // file name: exampleLibrary.kt // module name:
example package com.example.library // visible inside exampleLibrary.kt private fun setup() { ... } 67 @nisrulz -
Visibility vs Organization // file name: exampleLibrary.kt // module name:
example package com.example.library // visible inside exampleLibrary.kt private fun setup() { ... } // property is visible everywhere public var name: String = "ExampleLib" // setter is visible only in exampleLibrary.kt private set{...} 68 @nisrulz -
Visibility vs Organization // file name: exampleLibrary.kt // module name:
example package com.example.library // visible inside exampleLibrary.kt private fun setup() { ... } // property is visible everywhere public var name: String = "ExampleLib" // setter is visible only in exampleLibrary.kt private set{...} // visible inside the module i.e example internal val debugTag = "Example-Debug" 69 @nisrulz -
Lifecycle Components Classes designed to help deal with Android lifecycle
• Lifecycle • LifecycleOwner • LifecycleObserver 71 @nisrulz -
Lifecycle Components Classes designed to help deal with Android lifecycle
i.e Activity, Fragment, Service, Custom 73 @nisrulz -
Lifecycle class MainActivity extends AppCompatActivity() {...} public class AppCompatActivity extends
FragmentActivity{...} 75 @nisrulz -
Lifecycle class MainActivity extends AppCompatActivity() {...} public class AppCompatActivity extends
FragmentActivity{...} public class FragmentActivity extends SupportActivity { ... public Lifecycle getLifecycle() { return super.getLifecycle(); } ... } 76 @nisrulz -
LifecycleObserver dependencies { def lifecycleVer = "2.0.0" // Runtime implementation
"androidx.lifecycle:lifecycle-runtime:$lifecycleVer" // Annotation Support annotationProcessor "androidx.lifecycle:lifecycle-compiler:$lifecycleVer" ... } 77 @nisrulz -
LifecycleObserver public class AwesomeLib implements LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_CREATE) public void
init() { ... } @OnLifecycleEvent(Lifecycle.Event.ON_START) public void libOnStart() { ... } ... @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) public void cleanup() { ... } } 78 @nisrulz -
LifecycleOwner class MainActivity : AppCompatActivity() { val awesomeLib = AwesomeLib()
override fun onResume() { ... // Add lifecycle observer lifecycle.addObserver(awesomeLib) } override fun onStop() { ... // Remove lifecycle observer lifecycle.removeObserver(awesomeLib) } } 79 @nisrulz -
ProcessLifecycleOwner Does something weird… Adding lifecycle-extensions artifact > automatically adds
<provider> element to the merged manifest 84 @nisrulz -
ProcessLifecycleOwner // Internal class to initialize Lifecycles. public class ProcessLifecycleOwnerInitializer
extends ContentProvider { @Override public boolean onCreate() { ... ProcessLifecycleOwner.init(getContext()); return true; } ... } 93 @nisrulz -
ProcessLifecycleOwner Why? To invoke ProcessLifecycleOwner as soon as process starts
Drawback? Initializes ProcessLifecycleOwner even if your app does not use it! 95 @nisrulz -
ProcessLifecycleOwner How to get rid of it in app? Use
Merge rule marker in app’s AndroidManifest.xml: // Remove marked element from the merged manifest tools:node="remove" 97 @nisrulz -
AutoInit Android Library Android Libraries need Android context to handle
simple tasks such as • Hook into Android Runtime • Access app resources • Use System Services • Register BroadcastReceiver 103 @nisrulz -
AutoInit Android Library public class MyApplication extends Application { @Override
public void onCreate() { super.onCreate(); // Init android library MyAwesomeLibrary.init(this); } } 104 @nisrulz -
AutoInit Android Library public class MyApplication extends Application { @Override
public void onCreate() { super.onCreate(); // Init android library MyAwesomeLibrary.init(this); } } 105 @nisrulz -
AutoInit Android Library public class MyApplication extends Application { @Override
public void onCreate() { super.onCreate(); // Init android library MyAwesomeLibrary.init(this); } } 106 @nisrulz -
AutoInit Android Library ContentProvider can be used to simplify the
process. 109 @nisrulz -
AutoInit Android Library ContentProvider can be used to simplify the
process. Simply because ContentProvider • Is created and initialized (on the main thread) before all other components • Participate in manifest merging at build time. 110 @nisrulz -
AutoInit Android Library public class AwesomeLibInitProvider extends ContentProvider { ...
@Override public boolean onCreate() { // get the context (Application context) Context context = getContext(); // initialize AwesomeLib here AwesomeLib.getInstance().init(context); return false; } ... } 111 @nisrulz -
AutoInit Android Library public class AwesomeLibInitProvider extends ContentProvider { ...
@Override public boolean onCreate() { // get the context (Application context) Context context = getContext(); // initialize AwesomeLib here AwesomeLib.getInstance().init(context); return false; } ... } 112 @nisrulz -
AutoInit Android Library <manifest xmlns:android=".." package="github.nisrulz.sample.awesomelib"> <application> <provider android:name=".AwesomeLibInitProvider" android:authorities="${applicationId}.awesomelibinitprovider"
android:enabled="true" android:exported="false"/> </application> </manifest> 113 @nisrulz -
AutoInit Android Library <manifest xmlns:android=".." package="github.nisrulz.sample.awesomelib"> <application> <provider android:name=".AwesomeLibInitProvider" android:authorities="${applicationId}.awesomelibinitprovider"
android:enabled="true" android:exported="false"/> </application> </manifest> 114 @nisrulz -
AutoInit Android Library <manifest xmlns:android=".." package="github.nisrulz.sample.awesomelib"> <application> <provider android:name=".AwesomeLibInitProvider" android:authorities="${applicationId}.awesomelibinitprovider"
android:enabled="true" android:exported="false"/> </application> </manifest> 115 @nisrulz -
AutoInit Android Library <manifest xmlns:android=".." package="github.nisrulz.sample.awesomelib"> <application> <provider android:name=".AwesomeLibInitProvider" android:authorities="${applicationId}.awesomelibinitprovider"
android:enabled="true" android:exported="false"/> </application> </manifest> 116 @nisrulz -
AutoInit Android Library ContentProvider can be used to simplify the
process. 117 @nisrulz -
AutoInit Android Library ContentProvider can be used to simplify the
process. Challenges: • There can be only one Content Provider with a given “authority” string • Only run on main thread 118 @nisrulz -
AutoInit Android Library ContentProvider can be used to simplify the
process. 119 @nisrulz -
AutoInit Android Library ContentProvider can be used to simplify the
process. Why this is a bad idea: • Increases startup time • Bloats applications even when not used • Abusing functionality of ContentProvider 120 @nisrulz -
AutoInit Android Library ContentProvider can be used to simplify the
process. Why this is a bad idea: • Increases startup time (Solution: async initialize) • Bloats applications even when not used • Abusing functionality of ContentProvider 121 @nisrulz -
AutoInit Android Library ProcessLifecycleOwner [androidx.lifecycle:lifecycle-extensions:2.0.0] <application> <provider android:name="androidx.lifecycle.ProcessLifecycleOwnerInitializer" android:authorities="${applicationId}.lifecycle-process" android:exported="false"
android:multiprocess="true" /> </application> 123 @nisrulz -
AutoInit Android Library Firebase [com.google.firebase:firebase-common:16.0.5] <application> <provider android:name="com.google.firebase.provider.FirebaseInitProvider" android:authorities="${applicationId}.firebaseinitprovider" android:exported="false"
android:initOrder="100" /> </application> 124 @nisrulz -
AutoInit Android Library Facebook-Marketing [com.facebook.android:facebook-marketing:4.34.0] <application> <provider android:name="com.facebook.marketing.internal.MarketingInitProvider" android:authorities="${applicationId}.MarketingInitProvider" android:exported="false"
/> </application> 126 @nisrulz -
AutoInit Android Library Crashlytics [com.crashlytics.sdk.android:crashlytics:2.9.5] <application> <provider android:name="com.crashlytics.android.CrashlyticsInitProvider" android:authorities="${applicationId}.crashlyticsinitprovider" android:exported="false"
android:initOrder="90" /> </application> 127 @nisrulz -
AutoInit Android Library Picasso // Pre - v2.71828 Picasso.with(this).load("...<url>...").into(imageView); //
After - v2.71828 Picasso.get().load("...<url>...").into(imageView); 128 @nisrulz -
AutoInit Android Library Picasso // Pre - v2.71828 Picasso.with(this).load("...<url>...").into(imageView); //
After - v2.71828 Picasso.get().load("...<url>...").into(imageView); 129 @nisrulz -
Links/References Android Libraries I have built: https://github.com/nisrulz/nisrulz.github.io#open-source-co ntributions Auto initialize
android library example: https://github.com/nisrulz/android-examples/tree/develop/Au toInitLibrary Lifecycle Aware android library example: https://github.com/nisrulz/android-examples/tree/develop/Li feCycleCompForLib 131 @nisrulz