Share
[Devoxx Belgium '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 a now a necessity than just some acquirable knowledge.
This session in a second part to the last year’s talk at Droidcon Berlin 2017 with the same title. In this session we 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. We will also be covering 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 developer happiness.
Google Slides: https://docs.google.com/presentation/d/1svOPXO9dYeuim1uHHymciIobdFKMb-4TKNUrvWmovH8/edit?usp=sharing
Video: https://www.youtube.com/watch?v=jQyt3HSmx2I
Nishant Srivastava
Transcript
89 slides
-
Using an AAR as dependency @nisrulz repositories{ flatDir{ dirs 'libs'
} } dependencies { implementation(name:'nameOfYourAARFileWithoutExtension', ext:'aar') } -
Using an AAR as dependency @nisrulz repositories{ flatDir{ dirs 'libs'
} } dependencies { implementation(name:'nameOfYourAARFileWithoutExtension', ext:'aar') } // None of the dependencies of the library are downloaded -
Sensey’s build.gradle @nisrulz dependencies { ... // Other testing dependencies
// Transitive dependency: Support Compat library implementation "com.android.support:support-compat:27.0.2" } -
POM @nisrulz + Stands for Project Object Model + Is
a XML file + Contains + Information about the project + Configuration details used by Maven to build the project -
POM @nisrulz 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> -
POM @nisrulz 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> -
POM @nisrulz 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> -
Bundled Proguard Configs @nisrulz 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' } ... } -
Bundled Proguard Configs @nisrulz # 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 -
Bundled Proguard Configs @nisrulz # To check the merged configuration
# Add the below to your current config -printconfiguration proguard-merged-config.txt -
Bundled Proguard Configs @nisrulz # DON'T DO THIS -dontobfuscate -optimizations
!code/allocation/variable # Effectively no optimizations -keep public class * { public protected *; } -
Bundled Proguard Configs @nisrulz # DON'T DO THIS # Adding
the below in library proguard rules disables # the optimizations in the Android app -dontoptimze -
Modularization @nisrulz 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" } -
Modularization @nisrulz <!-- 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.4.1</version> <scope>compile</scope> </dependency> <dependency> <groupId>com.github.nisrulz</groupId> <artifactId>easydeviceinfo-base</artifactId> <version>2.4.1</version> <scope>compile</scope> </dependency> ... </dependencies> </project> -
Modularization @nisrulz <!-- 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.4.1</version> <scope>compile</scope> </dependency> <dependency> <groupId>com.github.nisrulz</groupId> <artifactId>easydeviceinfo-base</artifactId> <version>2.4.1</version> <scope>compile</scope> </dependency> ... </dependencies> </project> -
Modularization @nisrulz <!-- 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.4.1</version> <scope>compile</scope> </dependency> <dependency> <groupId>com.github.nisrulz</groupId> <artifactId>easydeviceinfo-base</artifactId> <version>2.4.1</version> <scope>compile</scope> </dependency> ... </dependencies> </project> -
Modularization @nisrulz <!-- 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.4.1</version> <scope>compile</scope> </dependency> <dependency> <groupId>com.google.android.gms</groupId> <artifactId>play-services-basement</artifactId><version>11.8.0</version> <scope>runtime</scope> </dependency> ... </dependencies> </project> -
Modularization @nisrulz <!-- 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.4.1</version> <scope>compile</scope> </dependency> <dependency> <groupId>com.google.android.gms</groupId> <artifactId>play-services-basement</artifactId><version>11.8.0</version> <scope>runtime</scope> </dependency> ... </dependencies> </project> -
@nisrulz Conflict occurs between a library & app resource >
Project will not compile What happens when.. -
@nisrulz Conflict occurs between 2 libraries integrated in the app
> Resources from library defined first in build.gradle gets included What happens when.. -
@nisrulz Add a prefix to all your resources. Enforce this
in Android Studio android { resourcePrefix 'YOUR_PREFIX_' // i.e 'sensey_' } Solution? -
@nisrulz Solution? Resource named ‘app_name’ does not start with the
project’s resource prefix ‘sensey_’; Rename to `sensey_app_name`? -
@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 -
@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 -
@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 -
Visibility vs Organization @nisrulz + Code organized in individual packages;
everything is public -
Visibility vs Organization @nisrulz + Code organized in individual packages;
everything is public + Code organized inside one package; everything is package private and only public on demand -
Visibility vs Organization @nisrulz + Code organized in individual packages;
everything is public + Code organized inside one package; everything is package private and only public on demand But if in Kotlin land, + Code organized in individual packages; everything is internal and only public on demand -
Visibility vs Organization @nisrulz // file name: exampleLibrary.kt // module
name: example package com.example.library -
Visibility vs Organization @nisrulz // file name: exampleLibrary.kt // module
name: example package com.example.library // visible inside exampleLibrary.kt private fun setup() { ... } -
Visibility vs Organization @nisrulz // 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{...} -
Visibility vs Organization @nisrulz // 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" -
Lifecycle Components @nisrulz Classes designed to help deal with Android
lifecycle + Lifecycle + LifecycleOwner + LifecycleObserver -
Lifecycle Components @nisrulz Classes designed to help deal with Android
lifecycle i.e Activity, Fragment, Service, Custom -
LifecycleObserver @nisrulz dependencies { def lifecycleVer = "1.1.1" // Runtime
implementation "android.arch.lifecycle:runtime:$lifecycleVer" // Annotation Support annotationProcessor "android.arch.lifecycle:compiler:$lifecycleVer" ... } -
LifecycleObserver @nisrulz object AwesomeLibrary : LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_CREATE) fun init()
{ ... } @OnLifecycleEvent(Lifecycle.Event.ON_START) fun onStart() { ... } ... @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) fun cleanup() { ...} } -
LifecycleOwner @nisrulz class MainActivity : AppCompatActivity() { // LifecycleOwner override
fun onResume() { // Add lifecycle observer lifecycle.addObserver(AwesomeLibrary.INSTANCE) } override fun onStop() { // Remove lifecycle observer lifecycle.removeObserver(AwesomeLibrary.INSTANCE) } } -
ProcessLifecycleOwner @nisrulz dependencies { def lifecycleVer = "1.1.1" // For
ProcessLifecycleOwner implementation "android.arch.lifecycle:extensions:$lifecycleVer" ... } -
ProcessLifecycleOwner @nisrulz <manifest > <application> ... <provider android:name="android.arch.lifecycle.ProcessLifecycleOwnerInitializer" android:authorities="com.example.app.lifecycle-trojan" android:exported="false"
android:multiprocess="true" /> </application> </manifest> -
ProcessLifecycleOwner @nisrulz // Source code // Internal class to initialize
Lifecycles. public class ProcessLifecycleOwnerInitializer extends ContentProvider { @Override public boolean onCreate() { ... ProcessLifecycleOwner.init(getContext()); return true; } ... } -
ProcessLifecycleOwner @nisrulz Initializes ProcessLifecycleOwner even if your app does not
use it! Why? To invoke ProcessLifecycleOwner as soon as process starts -
AutoInit Android Library @nisrulz Android Libraries need Android context to
handle simple tasks such as + Hook into Android Runtime + Access app resources + Use System Services + Register BroadcastReceiver -
AutoInit Android Library @nisrulz public class MyApplication extends Application {
@Override public void onCreate() { super.onCreate(); // Init android library MyAwesomeLibrary.init(this); } } -
AutoInit Android Library @nisrulz public class MyApplication extends Application {
@Override public void onCreate() { super.onCreate(); // Init android library MyAwesomeLibrary.init(this); } } -
AutoInit Android Library @nisrulz public class MyApplication extends Application {
@Override public void onCreate() { super.onCreate(); // Init android library MyAwesomeLibrary.init(this); } } -
AutoInit Android Library @nisrulz ContentProvider “can be” used to simplify
the process. How? ContentProvider is + Created and initialized (on the main thread) before all other components + Participate in manifest merging at build time. -
AutoInit Android Library @nisrulz 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; } ... } -
AutoInit Android Library @nisrulz 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; } ... } -
AutoInit Android Library @nisrulz <manifest xmlns:android=".." package="github.nisrulz.sample.awesomelib"> <application> <provider android:name=".AwesomeLibInitProvider"
android:authorities="${applicationId}.awesomelibinitprovider" android:enabled="true" android:exported="false"></provider> </application> </manifest> -
AutoInit Android Library @nisrulz <manifest xmlns:android=".." package="github.nisrulz.sample.awesomelib"> <application> <provider android:name=".AwesomeLibInitProvider"
android:authorities="${applicationId}.awesomelibinitprovider" android:enabled="true" android:exported="false"></provider> </application> </manifest> -
AutoInit Android Library @nisrulz <manifest xmlns:android=".." package="github.nisrulz.sample.awesomelib"> <application> <provider android:name=".AwesomeLibInitProvider"
android:authorities="${applicationId}.awesomelibinitprovider" android:enabled="true" android:exported="false"></provider> </application> </manifest> -
AutoInit Android Library @nisrulz <manifest xmlns:android=".." package="github.nisrulz.sample.awesomelib"> <application> <provider android:name=".AwesomeLibInitProvider"
android:authorities="${applicationId}.awesomelibinitprovider" android:enabled="true" android:exported="false"></provider> </application> </manifest> -
AutoInit Android Library @nisrulz ContentProvider “can be” used to simplify
the process. Why it shouldn’t be + There can be only one Content Provider with a given “authority” string + Only run on main thread; does not support multi process -
Links/References @nisrulz 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