The document discusses scaling an Android app from 1 to 100 developers using modularization. It begins with an example of two apps, Splinky and Airbnb, that were being built in 2011. While Splinky remained a monolithic app, Airbnb adopted a modular structure with over 160 modules. The document then discusses concepts like modularizing by feature rather than layer, maintaining code ownership through encapsulation, using Dagger to manage dependencies between modules, and common challenges in multi-module projects like sharing code and upstream dependencies.
16. What is modularization?
Manifest R res
classes.jar
apply plugin: ‘com.android.application'
apply plugin: ‘com.android.library’
App
Lib
apk/aab
Manifest R res
classes.jar
17. What is modularization?
apply plugin: ‘com.android.application'
apply plugin: ‘com.android.library’
“App depends on Lib”
App
Lib
23. Build times.
Project/gradle.properties
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/
multi_project_builds.html#sec:decoupled_projects
org.gradle.parallel=true
27. Murphy’s Law:
“Whatever can go wrong will go wrong”.
Murphy’s Law of code access:
“Whatever they can access,
they will access
(and it will go wrong)”.
36. Bigger =/= Better
For every 6mb of app size, install conversion
drops by 1%.
Cutting an 100mb app to 10mb would
see download conversion increase by 30%.
37.
38.
39. The state of modularization at
Airbnb
160+
modules
2-5
minutes
“Lite”
builds
Lots of
lessons
learned…
42. Base
Modularize by feature.
Base (Core)
• “Pure” Infrastructure.
• Strictly no domain knowledge.
• “Could this be open source-able?”
• Owned by Native Infrastructure team.
• Keep it lean. No deprecated code.
43. Home Listing
Experience
Listing
Modularize by feature.
Feature Modules
•Owned by a single team.
•Encapsulates a single “feature”.
•A single, addressable entry-point.
•Smaller is better.
•A team might own many feature modules.
•Can not depend on other feature modules.
Base
67. Intent?
•This is a dynamic feature. Need to download it with PlayCore.
•This is debug build and the module is not present. Toast developer.
•A developer deleted the activity/fragment.
Reflect on all fragment/activity entries and assert present.
Airbnb
Base
Home Listing Experience Listing
Intents
76. Library Modules
lib.WishlistManager
• Owned by a single team.
• No launchable features.
• Provides consumable
dependencies via an interface.
• Forces API design, instead of
sticking if/when cases in other
teams code.
Base
Airbnb
Home ExperienceWishlist Intents
91. Experience ListingHome Listing
Base
Airbnb
val trebuchetKeys =
“Plugin” Architecture
class TrebuchetRequest(…)
class TrebuchetKey
interface BaseGraph {
val trebuchetKeys: Set<TrebuchetKey>
}
92. Experience ListingHome Listing
Base
Airbnb
“Plugin” Architecture
class AirbnbApplication : Application() {
override fun onCreate() {
val baseGraph = object : BaseGraph {
override val trebuchetKeys: Set<TrebuchetKey>
get() {
return mutableSetOf<TrebuchetKey>().apply {
addAll(HomeListingTrebuchetKeys.values())
addAll(ExperienceListingTrebuchetKeys.values())
}
}
}
BaseApplication.onTargetApplicationCreated(this, baseGraph)
}
}
93. Experience ListingHome Listing
Base
Airbnb
“Plugin” Architecture
class AirbnbApplication : Application() {
override fun onCreate() {
val baseGraph = object : BaseGraph {
override val trebuchetKeys: Set<TrebuchetKey>
get() {
return mutableSetOf<TrebuchetKey>().apply {
addAll(HomeListingTrebuchetKeys.values())
addAll(ExperienceListingTrebuchetKeys.values())
}
}
}
BaseApplication.onTargetApplicationCreated(this, baseGraph)
}
}
94. Experience ListingHome Listing
Base
Airbnb
“Plugin” Architecture
class AirbnbApplication : Application() {
override fun onCreate() {
val baseGraph = object : BaseGraph {
override val trebuchetKeys: Set<TrebuchetKey>
get() {
return mutableSetOf<TrebuchetKey>().apply {
addAll(HomeListingTrebuchetKeys.values())
addAll(ExperienceListingTrebuchetKeys.values())
}
}
}
BaseApplication.onTargetApplicationCreated(this, baseGraph)
}
}
95. class AirbnbApplication : Application() {
override fun onCreate() {
val baseGraph = object : BaseGraph {
override val trebuchetKeys: Set<TrebuchetKey>
get() {
return mutableSetOf<TrebuchetKey>().apply {
addAll(HomeListingTrebuchetKeys.values())
addAll(ExperienceListingTrebuchetKeys.values())
}
}
}
BaseApplication.onTargetApplicationCreated(this, baseGraph)
}
}
Experience ListingHome Listing
Base
Airbnb
“Plugin” Architecture
96. Experience ListingHome Listing
Base
Airbnb
“Plugin” Architecture
class AirbnbApplication : Application() {
override fun onCreate() {
val baseGraph = object : BaseGraph {
override val trebuchetKeys: Set<TrebuchetKey>
get() {
return mutableSetOf<TrebuchetKey>().apply {
addAll(HomeListingTrebuchetKeys.values())
addAll(ExperienceListingTrebuchetKeys.values())
}
}
}
BaseApplication.onTargetApplicationCreated(this, baseGraph)
}
}
97. class AirbnbApplication : Application() {
override fun onCreate() {
val baseGraph = object : BaseGraph {
override val trebuchetKeys: Set<TrebuchetKey>
get() {
return mutableSetOf<TrebuchetKey>().apply {
addAll(HomeListingTrebuchetKeys.values())
addAll(Feature1TrebuchetKeys.values())
…
addAll(FeatureNTrebuchetKeys.values())
addAll(ExperienceListingTrebuchetKeys.values())
}
}
}
BaseApplication.onTargetApplicationCreated(this, baseGraph)
}
}
Experience
Listing
Home
Listing
Base
Airbnb
“Plugin” Architecture
Feature1Feature1
115. The anatomy of a feature module.
Project/homelisting/
Project/experiencelisting/
build.gradle
TrebuchetKeys.kt
HomeListingDagger.kt
Project/airbnb/
AndroidManifest.xml
res/resources
build.gradle
TrebuchetKeys.kt
ExperienceListingDagger.kt
AndroidManifest.xml
res/resources
AirbnbGraph
AirbnbComponent
build.gradle
settings.gradle
116. The anatomy of a feature module.
Project/homelisting/
Project/experiencelisting/
build.gradle
TrebuchetKeys.kt
HomeListingDagger.kt
Project/airbnb/
AndroidManifest.xml
res/resources
build.gradle
TrebuchetKeys.kt
ExperienceListingDagger.kt
AndroidManifest.xml
res/resources
AirbnbGraph
AirbnbComponent
build.gradle
settings.gradle
117. package <%= module_info.qualified_package_name %>
import com.airbnb.android.base.trebuchet.TrebuchetKey
enum class <%= module_info.name_pascal_case %>TrebuchetKeys(override val key: String) : TrebuchetKey
118. package <%= module_info.qualified_package_name %>
import com.airbnb.android.base.trebuchet.TrebuchetKey
enum class <%= module_info.name_pascal_case %>TrebuchetKeys(override val key: String) : TrebuchetKey
119. package <%= module_info.qualified_package_name %>
import com.airbnb.android.base.trebuchet.TrebuchetKey
enum class <%= module_info.name_pascal_case %>TrebuchetKeys(override val key: String) : TrebuchetKey
{
…
'TrebuchetKeys.kt' => “#{module_info.main_dir}/#{module_info.name_pascal_case}TrebuchetKeys.kt",
…
}.each do |template, file_name|
erb_template = ERB.new(File.read("#{template_dir}/#{template}"), nil, '-')
File.write(file_name, erb_template.result(binding))
end
ml006617bschwab:android ben_schwab$ bundle exec rake make_module
Module name (with spaces)
home listing
Creating home listing with package name com.airbnb.android.homelisting
Will you be moving/writing Java code in this module? [y/n]
n
Add home listing as a dependency of the flavor.full module? [y/n]
n
Run `./buckw project --skip-build` for intellij to pick up the new module.
126. build.gradle
android {
...
defaultConfig {...}
buildTypes {
debug{...}
release{...}
}
// Specifies one flavor dimension.
flavorDimensions "version"
productFlavors {
demo {
// Assigns this product flavor to the "version" flavor dimension.
// This property is optional if you are using only one dimension.
dimension "version"
applicationIdSuffix ".demo"
versionNameSuffix "-demo"
}
full {
dimension "version"
applicationIdSuffix ".full"
versionNameSuffix "-full"
}
}
}
127. build.gradle
android {
...
defaultConfig {...}
buildTypes {
debug{...}
release{...}
}
// Specifies one flavor dimension.
flavorDimensions "version"
productFlavors {
demo {
// Assigns this product flavor to the "version" flavor dimension.
// This property is optional if you are using only one dimension.
dimension "version"
applicationIdSuffix ".demo"
versionNameSuffix "-demo"
}
full {
dimension "version"
applicationIdSuffix ".full"
versionNameSuffix "-full"
}
}
}
./gradlew :airbnb:installDemoDebug
137. project.flavors.each { flavor, config ->
project.dependencies.add("${flavor}Api", project(config.entryModule))
}
project.ext.flavors = [
// If you want your flavor to be installed as a separate app for side-by-side
installation, do:
// foo: new FlavorOptions(":favor.foo").useSeparatePackageName()
full: new FlavorOptions(":flavor.full"),
homeListing: new FlavorOptions(“:flavor.homelisting”).useSeparatePackageName(),
]
Airbnb
productFlavors {
project.flavors.each { flavor, config ->
"$flavor" {
dimension 'scope'
if (flavor != 'full') {
versionNameSuffix ".$flavor"
if (config.useSeparatePackageName) {
applicationIdSuffix ".$flavor"
}
}
}
}
}
build.gradle
Specify the the flavor module to install.
138. project.flavors.each { flavor, config ->
project.dependencies.add("${flavor}Api", project(config.entryModule))
}
Airbnb
productFlavors {
project.flavors.each { flavor, config ->
"$flavor" {
dimension 'scope'
if (flavor != 'full') {
versionNameSuffix ".$flavor"
if (config.useSeparatePackageName) {
applicationIdSuffix ".$flavor"
}
}
}
}
}
build.gradle
Allow side-by-side installation of lite apps.
Create the flavor.