SlideShare ist ein Scribd-Unternehmen logo
1 von 148
Downloaden Sie, um offline zu lesen
Scaling an Android App from
1 to 100 Developers with
Modularization
@BenSchwab13
A tale of two apps.
Around June of 2011 two android apps were being built…
Splinky
apply plugin: ‘com.android.application'
Airbnb
apply plugin: ‘com.android.application'
Airbnb
Airbnb
Airbnb
Airbnb
Splinky’s
Last Stand
150
What is modularization?
apply plugin: ‘com.android.application'
App
What is modularization?
apply plugin: ‘com.android.application'
apply plugin: ‘com.android.library’
App
Lib
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
What is modularization?
apply plugin: ‘com.android.application'
apply plugin: ‘com.android.library’
“App depends on Lib”
App
Lib
What is modularization?
Lib2
Lib3
App
Lib1
The first rule of modularization is…
You don’t modularize if you don’t have too.
Project Structure
Report Card
Project Structure
Report Card
Build times
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
Clean Builds
Time(seconds)
0
50
100
150
200
Hundreds of source files
.2 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40
1 2 3 4# of modules
Project Structure
Report Card
Build times
Code Ownership
Code Ownership (Attribution)
how effort durability
line git blame
🚌
file/class @Owner(GrowthTeam)
module manifest
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)”.
Mall
Code Ownership
(Encapsulation)
Ben’s
Coffee Shop
Mall
Code Ownership
(Encapsulation)
Ben’s
Coffee Shop
CoffeeGrinder
Code Ownership
(Encapsulation)
Mall
Murphy’s
Coffee Shop
Ben’s
Coffee Shop
CoffeeGrinder
Code Ownership
(Encapsulation)
Mall
Murphy’s
Coffee Shop
Ben’s Boba
Shop
BobaMachine
Code Ownership
(Encapsulation)
Mall
Murphy’s
Boba Shop?
Ben’s Boba
Shop
BobaMachine
Code Ownership
(Encapsulation)
internal
public
private
protected
Murphy’s
Coffee Shop
Mall
Ben’s Boba
Shop
Boba Machine
Code Ownership
(Encapsulation)
internal
public
private
protected
Mall
Murphy’s
Coffee Shop
Ben’s Boba
Shop
Boba Machine
Project Structure
Report Card
Build times
Code Ownership
App Bundles
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%.
The state of modularization at
Airbnb
160+
modules
2-5
minutes
“Lite”
builds
Lots of
lessons
learned…
Airbnb
experiences
listing
homes
listing
Structure your code link your organization
Conway’s Law.
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.
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
Airbnb
Home Listing
Experience
Listing
Modularize by feature.
App shell
•No feature-specific code.
•No infrastructure-specific code.
•Creates Dagger Component. Base
Project Structure
Report Card
Build times
Code Ownership
App Bundles
Modularize by feature.
Airbnb
Home Listing
Base
Experience Listing
Modularize by feature.
Airbnb
Home Listing
Base
Experience
Listing
Place
List your
space
Mange your
space
Profile Payments Itinerary
Build times?
Airbnb
Home Listing
Base
Experience
Listing
Place
List your
space
Mange your
space
Profile Payments Itinerary
Project Structure
Report Card
Build times
Code Ownership
App Bundles
🏎
Ownership?
Airbnb
Home Listing
Base
Experience
Listing
Place
List your
space
Mange your
space
Profile Payments Itinerary
Ownership?
Airbnb
Home Listing
Base
Experience
Listing
Place
List your
space
Mange your
space
Profile Payments Itinerary
Ownership?
Airbnb
Home Listing
Base
Experience
Listing
Place
List your
space
Mange your
space
Profile Payments Itinerary
Project Structure
Report Card
Build times
Code Ownership
App Bundles
🏎
🤝
Instant Apps / Dynamic Features?
Airbnb
Home Listing
Base
Experience
Listing
Place
List your
space
Manage your
space
Profile Payments Itinerary
Project Structure
Report Card
Build times
Code Ownership
App Bundles
🏎
🤝
✅
Common Challenges
Sharing CodeNavigation Upstrea
Common Challenges
Sharing Code
Navigation
Upstreaming depend
fun launch(context: Context, listingId: Long) {
val listingIntent = Intent(context, HomeListingActivity::class.java).apply {
putExtra("listingId", listingId)
}
context.startActivity(listingIntent)
}
Airbnb
Base
Home Listing Experience Listing
Nearby Experiences
fun launch(context: Context, listingId: Long) {
val listingIntent = Intent(context, HomeListingActivity::class.java).apply {
putExtra("listingId", listingId)
}
context.startActivity(listingIntent)
}
Airbnb
Base
Home Listing Experience Listing
Nearby Experiences
fun launch(context: Context, listingId: Long) {
val listingIntent = Intent(context, ExperienceListingActivity::class.java).apply {
putExtra("listingId", listingId)
}
context.startActivity(listingIntent)
}
Airbnb
Base
Home Listing Experience Listing
Nearby Experiences
fun launch(context: Context, listingId: Long) {
val listingIntent = Intent(context, ExperienceListingActivity::class.java).apply {
putExtra("listingId", listingId)
}
context.startActivity(listingIntent)
}
Airbnb
Base
Home Listing Experience Listing
Nearby Experiences
fun launch(context: Context, listingId: Long) {
val listingIntent = Intent(context, ExperienceListingActivity::class.java).apply {
putExtra("listingId", listingId)
}
context.startActivity(listingIntent)
}
Airbnb
Base
Home Listing Experience Listing
Nearby Experiences
Airbnb
Base
Home Listing Experience Listing
Intents
fun <T> loadClassOrNull(className: String): Class<T>? {
return CLASS_MAP.getOrPut(className) {
try {
Class.forName(className)
} catch (e: ClassNotFoundException) {
// Can't store a null value in the concurrent map
return null
}
}.castOrNull()
object Activities {
fun intentForListing(context: Context, listingId: Long): Intent? {
val args = Bundle().apply { putLong("listingId", listingId) }
return loadClassOrNull<Activity>("com.airbnb.android.ListingActivity")
.let { Intent(context, it) }
.apply { putExtras(args) }
}
}
object Fragments {
fun fragmentForListing(listingId: Long): Fragment? {
val args = Bundle().apply { putLong("listingId", listingId) }
return loadClassOrNull<Fragment>("com.airbnb.android.ListingFragment")
object Activities {
fun intentForListing(context: Context, listingId: Long): Intent? {
val args = Bundle().apply { putLong("listingId", listingId) }
return loadClassOrNull<Activity>("com.airbnb.android.ListingActivity")
.let { Intent(context, it) }
.apply { putExtras(args) }
}
}
object Fragments {
fun fragmentForListing(listingId: Long): Fragment? {
val args = Bundle().apply { putLong("listingId", listingId) }
return loadClassOrNull<Fragment>("com.airbnb.android.ListingFragment")
?.newInstance()
?.apply { this.arguments?.putBundle("args", args) }
}
}
object Fragments {
fun fragmentForListing(listingId: Long): Fragment? {
val args = Bundle().apply { putLong("listingId", listingId) }
return loadClassOrNull<Fragment>("com.airbnb.android.ListingFragment")
?.newInstance()
?.apply { this.arguments?.putBundle("args", args) }
}
}
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
Common Challenges
Sharing Code
Navigation
Upstreaming depend
Common Challenges
Sharing Code
Navigation Upstreaming dependencies
Home Listing
Experience
Listing
Wishlist Intents
Airbnb
Base
*Same dependencies as before. I’m just lazy…
Airbnb
Base
Home Listing
Experience
Listing
Wishlist Intents
Wishlist
Manager
Home Listing
Experience
Listing
Wishlist Intents
Airbnb
Base
Wishlist
Manager
Base
Wishlist
Manager
SearchFilters
ListingFormat
ter
Home Listing
Experience
Listing
Wishlist Intents
Airbnb
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
Keep your base lean.Keep your base lean.
Communicating Deprecation
Base
Home Listing
Experience
Listing
Wishlist
Communicating Deprecation
Base
Guava
Home Listing
Experience
Listing
Wishlist
Communicating Deprecation
Base
Guava
Home Listing
Experience
Listing
Wishlist
Murphy’s Law of code access:
“Whatever they can access,
they will access
(and it will go wrong)”.
Communicating Deprecation
Base
Guava
Home Listing
Experience
Listing
Wishlist
Communicating Deprecation
Base
lib.deprecated.Guava
Home Listing
Experience
Listing
Wishlistnew feature
Common Challenges
Sharing Code
Navigation Upstreaming dependencies
Common Challenges
Sharing CodeNavigation
Upstreaming dependencies
Airbnb
Experience ListingHome Listing
class TrebuchetRequest(val keys: Set<TrebuchetKey>)
Base
Experience ListingHome Listing
Base
enum class HomeListingTrebuchetKeys(override val key: String) : TrebuchetKey {
ShowSimilarExperiences(“android.show_similiar_experiences”),
UseMvRx(“android.use_mvrx_home_listing”),
}
Airbnb
Experience ListingHome Listing
enum class ExperienceListingTrebuchetKeys(override val key: String): TrebuchetKey {
ShowSimilarListings(“android.show_similiar_listings”),
UseVideos(“android.use_videos”),
}
Base
Airbnb
Experience ListingHome Listing
Base
Airbnb
class TrebuchetRequest(val keys: Set<TrebuchetKey>)
class TrebuchetKey
Experience ListingHome Listing
Base
Airbnb
Violates no infrastructure in shell.
Violates single module owner.
class TrebuchetRequest(val keys: Set<TrebuchetKey>)
class TrebuchetKey
Experience ListingHome Listing
Base
Airbnb
val trebuchetKeys =
“Plugin” Architecture
class TrebuchetRequest(…)
class TrebuchetKey
interface BaseGraph {
val trebuchetKeys: Set<TrebuchetKey>
}
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)
}
}
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)
}
}
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)
}
}
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
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)
}
}
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
Managing Multi-module
Projects with Dagger
Airbnb
ExperienceListingHomeListing
Airbnb
AirbnbComponent
ExperienceListingHomeListing
HomeListingComponent ExperienceListingComponent
@ComponentScope
@Singleton
“App scope”
Base
Airbnb
AirbnbComponent
ExperienceListingHomeListing
HomeListingComponent ExperienceListingComponent
@ComponentScope
@Singleton
“App scope”
Base
Airbnb
AirbnbComponent
ExperienceListingHomeListing
HomeListingComponent ExperienceListingComponent
@ComponentScope
@Singleton
“App scope”
Base
AppModule AppModule
@Singleton
“App scope”
@Singleton
“App scope”
Airbnb
AirbnbComponent : AppGraph
ExperienceListingHomeListing
HomeListingComponent ExperienceListingComponent
Base
AppModule AppModule
AppGraph
@Multibinds @Multibinds
Unified Dagger System.
interface BaseGraph {
fun trebuchetKeys(): Set<TrebuchetKey>
}
@Module
abstract class BaseAppModule {
@Multibinds abstract fun trebuchetKeys(): Set<TrebuchetKey>
}
class HomeListingDagger {
@Subcomponent
@ComponentScope
interface HomeListingComponent {
@Subcomponent.Builder
interface Builder {
fun build(): HomeListingComponent
}
}
@Module
abstract class AppModule {
@Binds
@ElementsIntoSet
fun provideTrebuchetKeys() : Set<Trebuchet> {
return HomeListingTrebuchetKeys.values().toSet()
}
}
interface HomeListingAppGraph {
fun p3Builder(): HomeListingComponent.Builder
}
}
Base
Features
Airbnb
}
interface AppGraph
}
interface BaseGraph {
fun trebuchetKeys(): Set<TrebuchetKey>
}
@Module
abstract class BaseAppModule {
@Multibinds abstract fun trebuchetKeys(): Set<Trebuchet
}
class HomeListingDagger {
@Subcomponent
@ComponentScope
interface HomeListingComponent {
@Subcomponent.Builder
interface Builder {
fun build(): HomeListingComponent
}
}
@Module
abstract class AppModule {
@Binds
@ElementsIntoSet
fun provideTrebuchetKeys() : Set<Trebuchet> {
return HomeListingTrebuchetKeys.values().toSet()
}
}
interface HomeListingAppGraph {
fun p3Builder(): HomeListingComponent.Builder
}
}
Base
Features
Airbnb
}
interface AppGraph
}
class HomeListingDagger {
@Subcomponent
@ComponentScope
interface HomeListingComponent {
@Subcomponent.Builder
interface Builder {
fun build(): HomeListingComponent
}
}
@Module
abstract class AppModule {
@Binds
@ElementsIntoSet
fun provideTrebuchetKeys() : Set<Trebuchet> {
return HomeListingTrebuchetKeys.values().toSet()
}
}
interface AppGraph {
fun p3Builder(): HomeListingComponent.Builder
}
Features
Airbnb
interface AppGraph
}
class HomeListingDagger {
@Subcomponent
@ComponentScope
interface HomeListingComponent {
@Subcomponent.Builder
interface Builder {
fun build(): HomeListingComponent
}
}
@Module
abstract class AppModule {
@Binds
@ElementsIntoSet
fun provideTrebuchetKeys() : Set<Trebuchet> {
return HomeListingTrebuchetKeys.values().toSet()
}
}
interface AppGraph {
fun p3Builder(): HomeListingComponent.Builder
}
Features
Airbnb
interface AppGraph
}
class HomeListingDagger {
@Subcomponent
@ComponentScope
interface HomeListingComponent {
@Subcomponent.Builder
interface Builder {
fun build(): HomeListingComponent
}
}
@Module
abstract class AppModule {
@Binds
@ElementsIntoSet
fun provideTrebuchetKeys() : Set<Trebuchet> {
return HomeListingTrebuchetKeys.values().toSet()
}
}
interface AppGraph {
fun p3Builder(): HomeListingComponent.Builder
}
Features
Airbnb
interface AppGraph
}
Airbnb
@Singleton
@Component(modules = [
HomeListingDagger.HomeListingAppModule::class,
ExploreListingDagger.ExploreListingAppModule::class
])
interface AirbnbComponent :
BaseGraph,
HomeListingDagger.HomeListingAppGraph,
ExploreListingDagger.AppGraph
Airbnb
@Singleton
@Component(modules = [
HomeListingDagger.HomeListingAppModule::class,
ExploreListingDagger.ExploreListingAppModule::class
])
interface AirbnbComponent :
BaseGraph,
HomeListingDagger.HomeListingAppGraph,
ExploreListingDagger.AppGraph
Base fun <T : BaseGraph> component(): T
Home Listing
Experience
Listing
Place
List your
space
Mange your
space
Profile Payments Itinerary
Airbnb
@Singleton
@Component(modules = [
HomeListingDagger.HomeListingAppModule::class,
ExploreListingDagger.ExploreListingAppModule::class
])
interface AirbnbComponent :
BaseGraph,
HomeListingDagger.HomeListingAppGraph,
ExploreListingDagger.AppGraph
Base fun <T : BaseGraph> component(): T
Home Listing Place
List your
space
Mange your
space
Profile ItineraryPayments
Experience
Listing
AirbnbApplication Shell
Features
Infrastructure
Libraries
home listing experiencewishlist
lib.wishlistmanager
base
intents
Dagger initialization
Airbnb
Build tooling to support your project structure.
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
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
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
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
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.
Airbnb “Lite”
Clean Builds
Time(seconds)
0
12.5
25
37.5
50
Hundreds of source files
.2 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40
4 5 6 7 8 9 10# of modules
Clean Builds
Time(seconds)
0
12.5
25
37.5
50
Hundreds of source files
.2 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40
4 5 6 7 8 9 10# of modules
Project Structure
Report Card
Build times
Code Ownership
App Bundles
🏎
🤝
✅
Clean Builds
Time(seconds)
0
12.5
25
37.5
50
Hundreds of source files
.2 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40
4 5 6 7 8 9 10# of modules
Project Structure
Report Card
Build times
Code Ownership
App Bundles
🤝
✅
🐢
Clean Builds
Time(seconds)
0
12.5
25
37.5
50
Hundreds of source files
.2 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40
4 5 6 7 8 9 10# of modules
What is a flavor?
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"
        }
    }
}
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
AirbnbApplication Shell
Features
Infrastructure
Libraries
home listing experiencewishlist
lib.wishlistmanager
base
intents
AirbnbApplication Shell
Features
Infrastructure
Libraries
home listing experiencewishlist
lib.wishlistmanager
base
intents
Flavors flavor.homes flavor.experiencesflavor.full
AirbnbApplication Shell
Features
Infrastructure
Libraries
home listing experiencewishlist
lib.wishlistmanager
base
intents
Flavors flavor.homes flavor.experiencesflavor.full
AirbnbApplication Shell
Features
Infrastructure
Libraries
home listing experiencewishlist
lib.wishlistmanager
base
intents
Flavors flavor.homes flavor.experiencesflavor.full
Airbnb
home listing experiencewishlist
lib.wishlistmanager
base
intents
flavor.homes flavor.experiencesflavor.full
Application Shell
Features
Infrastructure
Libraries
Flavors
Clean Builds
Time(seconds)
0
12.5
25
37.5
50
Hundreds of source files
.2 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40
Full build
Clean Builds
Time(seconds)
0
12.5
25
37.5
50
Time
.2 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40
Full build
Clean Builds
Time(seconds)
0
12.5
25
37.5
50
Time
.2 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40
Full Build Lite Flavor
1) It needs to be easy to create a flavor.
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.
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.
project.flavors.each { flavor, config ->
project.dependencies.add("${flavor}Api", project(config.entryModule))
}
Airbnb build.gradle
project.flavors.each { flavor, config ->
project.dependencies.add("${flavor}Api", project(config.entryModule))
}
Airbnb build.gradle
flavor.homes build.gradle
dependencies {
api project(':homelisting')
api project(':hometour')
}
Airbnb
flavor.homes
home listing
lib.wishlistmanag
base
hometour
Generate the dagger component
1) It needs to be easy to create a flavor.
2) Flavors need to handle missing code gracefully.
AirbnbApplication Shell
Features
Infrastructure
Libraries
home listing experiencewishlist
lib.wishlistmanager
base
intents
Flavors flavor.homes flavor.experiencesflavor.full
AirbnbApplication Shell
Features
Infrastructure
Libraries
home listing experiencewishlist
lib.wishlistmanager
base
intents
Flavors flavor.homes flavor.experiencesflavor.full
3) Can flavors be more than just faster?
1) It needs to be easy to create a flavor.
2) Flavors need to handle missing code gracefully.
Custom Launchers
Module Unloading
@gpeal8Special thanks to:
MvRx
GraphQL
@BenSchwab13
Modularization
Epoxy
Server Driven UI

Weitere ähnliche Inhalte

Was ist angesagt?

How to store large binary files in git repositories
How to store large binary files in git repositoriesHow to store large binary files in git repositories
How to store large binary files in git repositoriesMatt Aunger
 
Docker를 활용한 손쉬운 ECS 활용기 - 김민태 (AUSG) :: AWS Community Day Online 2021
Docker를 활용한 손쉬운 ECS 활용기 - 김민태 (AUSG) :: AWS Community Day Online 2021Docker를 활용한 손쉬운 ECS 활용기 - 김민태 (AUSG) :: AWS Community Day Online 2021
Docker를 활용한 손쉬운 ECS 활용기 - 김민태 (AUSG) :: AWS Community Day Online 2021AWSKRUG - AWS한국사용자모임
 
Introduction to Embedded Linux
Introduction to Embedded LinuxIntroduction to Embedded Linux
Introduction to Embedded LinuxHossain Reja
 
Hệ điều hành (chương 5)
Hệ điều hành (chương 5)Hệ điều hành (chương 5)
Hệ điều hành (chương 5)realpotter
 
linux device driver
linux device driverlinux device driver
linux device driverRahul Batra
 
Tìm hiểu về hệ điều hành android
Tìm hiểu về hệ điều hành androidTìm hiểu về hệ điều hành android
Tìm hiểu về hệ điều hành androidTÓc Đỏ XuÂn
 
Báo cáo system hacking
Báo cáo system hackingBáo cáo system hacking
Báo cáo system hackingHuynh Khang
 
Red Hat multi-cluster management & what's new in OpenShift
Red Hat multi-cluster management & what's new in OpenShiftRed Hat multi-cluster management & what's new in OpenShift
Red Hat multi-cluster management & what's new in OpenShiftKangaroot
 
Everything You Need To Know About Ivanti Security Controls
Everything You Need To Know About Ivanti Security ControlsEverything You Need To Know About Ivanti Security Controls
Everything You Need To Know About Ivanti Security ControlsIvanti
 
Lập trình ứng dụng web asp.net với C# - tailieumienphi.edu.vn
Lập trình ứng dụng web asp.net với C# - tailieumienphi.edu.vnLập trình ứng dụng web asp.net với C# - tailieumienphi.edu.vn
Lập trình ứng dụng web asp.net với C# - tailieumienphi.edu.vntailieumienphi
 
Nghiên cưú và phát triển Android OS
Nghiên cưú và phát triển Android OSNghiên cưú và phát triển Android OS
Nghiên cưú và phát triển Android OSNguyễn Anh
 
[123doc] do-an-phan-mem-quan-ly-nhan-su-tien-luong
[123doc]   do-an-phan-mem-quan-ly-nhan-su-tien-luong[123doc]   do-an-phan-mem-quan-ly-nhan-su-tien-luong
[123doc] do-an-phan-mem-quan-ly-nhan-su-tien-luongDuytPhm8
 
Linux device driver
Linux device driverLinux device driver
Linux device driverchatsiri
 
NGHIÊN CỨU, TRIỂN KHAI CÔNG NGHỆ QRADAR CHO VIỆC GIÁM SÁT AN NINH MẠNG CNTT
NGHIÊN CỨU, TRIỂN KHAI CÔNG NGHỆ QRADAR  CHO VIỆC GIÁM SÁT AN NINH MẠNG CNTTNGHIÊN CỨU, TRIỂN KHAI CÔNG NGHỆ QRADAR  CHO VIỆC GIÁM SÁT AN NINH MẠNG CNTT
NGHIÊN CỨU, TRIỂN KHAI CÔNG NGHỆ QRADAR CHO VIỆC GIÁM SÁT AN NINH MẠNG CNTTYenPhuong16
 

Was ist angesagt? (20)

How to store large binary files in git repositories
How to store large binary files in git repositoriesHow to store large binary files in git repositories
How to store large binary files in git repositories
 
Rails Gis Hacks
Rails Gis HacksRails Gis Hacks
Rails Gis Hacks
 
Introduction to Tekton
Introduction to TektonIntroduction to Tekton
Introduction to Tekton
 
Docker를 활용한 손쉬운 ECS 활용기 - 김민태 (AUSG) :: AWS Community Day Online 2021
Docker를 활용한 손쉬운 ECS 활용기 - 김민태 (AUSG) :: AWS Community Day Online 2021Docker를 활용한 손쉬운 ECS 활용기 - 김민태 (AUSG) :: AWS Community Day Online 2021
Docker를 활용한 손쉬운 ECS 활용기 - 김민태 (AUSG) :: AWS Community Day Online 2021
 
Introduction to Embedded Linux
Introduction to Embedded LinuxIntroduction to Embedded Linux
Introduction to Embedded Linux
 
Hệ điều hành (chương 5)
Hệ điều hành (chương 5)Hệ điều hành (chương 5)
Hệ điều hành (chương 5)
 
linux device driver
linux device driverlinux device driver
linux device driver
 
Tìm hiểu về hệ điều hành android
Tìm hiểu về hệ điều hành androidTìm hiểu về hệ điều hành android
Tìm hiểu về hệ điều hành android
 
Báo cáo system hacking
Báo cáo system hackingBáo cáo system hacking
Báo cáo system hacking
 
Red Hat multi-cluster management & what's new in OpenShift
Red Hat multi-cluster management & what's new in OpenShiftRed Hat multi-cluster management & what's new in OpenShift
Red Hat multi-cluster management & what's new in OpenShift
 
Everything You Need To Know About Ivanti Security Controls
Everything You Need To Know About Ivanti Security ControlsEverything You Need To Know About Ivanti Security Controls
Everything You Need To Know About Ivanti Security Controls
 
Foot printing
Foot printingFoot printing
Foot printing
 
Embedded Android : System Development - Part III
Embedded Android : System Development - Part IIIEmbedded Android : System Development - Part III
Embedded Android : System Development - Part III
 
Lập trình ứng dụng web asp.net với C# - tailieumienphi.edu.vn
Lập trình ứng dụng web asp.net với C# - tailieumienphi.edu.vnLập trình ứng dụng web asp.net với C# - tailieumienphi.edu.vn
Lập trình ứng dụng web asp.net với C# - tailieumienphi.edu.vn
 
What Is Helm
 What Is Helm What Is Helm
What Is Helm
 
Nghiên cưú và phát triển Android OS
Nghiên cưú và phát triển Android OSNghiên cưú và phát triển Android OS
Nghiên cưú và phát triển Android OS
 
Hacking Android OS
Hacking Android OSHacking Android OS
Hacking Android OS
 
[123doc] do-an-phan-mem-quan-ly-nhan-su-tien-luong
[123doc]   do-an-phan-mem-quan-ly-nhan-su-tien-luong[123doc]   do-an-phan-mem-quan-ly-nhan-su-tien-luong
[123doc] do-an-phan-mem-quan-ly-nhan-su-tien-luong
 
Linux device driver
Linux device driverLinux device driver
Linux device driver
 
NGHIÊN CỨU, TRIỂN KHAI CÔNG NGHỆ QRADAR CHO VIỆC GIÁM SÁT AN NINH MẠNG CNTT
NGHIÊN CỨU, TRIỂN KHAI CÔNG NGHỆ QRADAR  CHO VIỆC GIÁM SÁT AN NINH MẠNG CNTTNGHIÊN CỨU, TRIỂN KHAI CÔNG NGHỆ QRADAR  CHO VIỆC GIÁM SÁT AN NINH MẠNG CNTT
NGHIÊN CỨU, TRIỂN KHAI CÔNG NGHỆ QRADAR CHO VIỆC GIÁM SÁT AN NINH MẠNG CNTT
 

Ähnlich wie Scaling your Android App With Modularization

Cross Platform Mobile Apps with the Ionic Framework
Cross Platform Mobile Apps with the Ionic FrameworkCross Platform Mobile Apps with the Ionic Framework
Cross Platform Mobile Apps with the Ionic FrameworkTroy Miles
 
Playing with parse.com
Playing with parse.comPlaying with parse.com
Playing with parse.comJUG Genova
 
Prairie DevCon 2015 - Crafting Evolvable API Responses
Prairie DevCon 2015 - Crafting Evolvable API ResponsesPrairie DevCon 2015 - Crafting Evolvable API Responses
Prairie DevCon 2015 - Crafting Evolvable API Responsesdarrelmiller71
 
Building native mobile apps with word press
Building native mobile apps with word pressBuilding native mobile apps with word press
Building native mobile apps with word pressNikhil Vishnu P.V
 
From System Engineer to Gopher
From System Engineer to GopherFrom System Engineer to Gopher
From System Engineer to GopherI-Fan Wang
 
Build your-own-instagram-filters-with-javascript-202-335 (1)
Build your-own-instagram-filters-with-javascript-202-335 (1)Build your-own-instagram-filters-with-javascript-202-335 (1)
Build your-own-instagram-filters-with-javascript-202-335 (1)Thinkful
 
Automated Testing for Terraform, Docker, Packer, Kubernetes, and More
Automated Testing for Terraform, Docker, Packer, Kubernetes, and MoreAutomated Testing for Terraform, Docker, Packer, Kubernetes, and More
Automated Testing for Terraform, Docker, Packer, Kubernetes, and MoreC4Media
 
Lecture 12 - Maps, AR_VR_aaaaHardware.pptx
Lecture 12 - Maps, AR_VR_aaaaHardware.pptxLecture 12 - Maps, AR_VR_aaaaHardware.pptx
Lecture 12 - Maps, AR_VR_aaaaHardware.pptxNgLQun
 
Compose Camp: Introduction to Kotlin.pptx
Compose Camp: Introduction to Kotlin.pptxCompose Camp: Introduction to Kotlin.pptx
Compose Camp: Introduction to Kotlin.pptxAmruthasriAmaravati
 
Internal Android Library Management (DroidCon SF 2016, Droidcon Italy 2016)
Internal Android Library Management (DroidCon SF 2016, Droidcon Italy 2016)Internal Android Library Management (DroidCon SF 2016, Droidcon Italy 2016)
Internal Android Library Management (DroidCon SF 2016, Droidcon Italy 2016)Kelly Shuster
 
Building Serverless APIs (January 2017)
Building Serverless APIs (January 2017)Building Serverless APIs (January 2017)
Building Serverless APIs (January 2017)Julien SIMON
 
Hosting Your Own OTA Update Service
Hosting Your Own OTA Update ServiceHosting Your Own OTA Update Service
Hosting Your Own OTA Update ServiceQuinlan Jung
 
Alfresco Development Framework Basic
Alfresco Development Framework BasicAlfresco Development Framework Basic
Alfresco Development Framework BasicMario Romano
 
Telerik AppBuilder Presentation for TelerikNEXT Conference
Telerik AppBuilder Presentation for TelerikNEXT ConferenceTelerik AppBuilder Presentation for TelerikNEXT Conference
Telerik AppBuilder Presentation for TelerikNEXT ConferenceJen Looper
 
Life After Mobilegeddon: App Deep Linking Strategies - Pubcon October 2015
Life After Mobilegeddon: App Deep Linking Strategies - Pubcon October 2015Life After Mobilegeddon: App Deep Linking Strategies - Pubcon October 2015
Life After Mobilegeddon: App Deep Linking Strategies - Pubcon October 2015MobileMoxie
 
Life After Mobilegeddon: App Deep Linking Strategies - Pubcon October 2015
Life After Mobilegeddon: App Deep Linking Strategies - Pubcon October 2015Life After Mobilegeddon: App Deep Linking Strategies - Pubcon October 2015
Life After Mobilegeddon: App Deep Linking Strategies - Pubcon October 2015Suzzicks
 
Session-1 edited.pptx
Session-1 edited.pptxSession-1 edited.pptx
Session-1 edited.pptxscienceTech11
 
Progressive Web Application by Citytech
Progressive Web Application by CitytechProgressive Web Application by Citytech
Progressive Web Application by CitytechRitwik Das
 

Ähnlich wie Scaling your Android App With Modularization (20)

Cross Platform Mobile Apps with the Ionic Framework
Cross Platform Mobile Apps with the Ionic FrameworkCross Platform Mobile Apps with the Ionic Framework
Cross Platform Mobile Apps with the Ionic Framework
 
Playing with parse.com
Playing with parse.comPlaying with parse.com
Playing with parse.com
 
Prairie DevCon 2015 - Crafting Evolvable API Responses
Prairie DevCon 2015 - Crafting Evolvable API ResponsesPrairie DevCon 2015 - Crafting Evolvable API Responses
Prairie DevCon 2015 - Crafting Evolvable API Responses
 
Building native mobile apps with word press
Building native mobile apps with word pressBuilding native mobile apps with word press
Building native mobile apps with word press
 
From System Engineer to Gopher
From System Engineer to GopherFrom System Engineer to Gopher
From System Engineer to Gopher
 
Build your-own-instagram-filters-with-javascript-202-335 (1)
Build your-own-instagram-filters-with-javascript-202-335 (1)Build your-own-instagram-filters-with-javascript-202-335 (1)
Build your-own-instagram-filters-with-javascript-202-335 (1)
 
Automated Testing for Terraform, Docker, Packer, Kubernetes, and More
Automated Testing for Terraform, Docker, Packer, Kubernetes, and MoreAutomated Testing for Terraform, Docker, Packer, Kubernetes, and More
Automated Testing for Terraform, Docker, Packer, Kubernetes, and More
 
Session-1.pptx
Session-1.pptxSession-1.pptx
Session-1.pptx
 
Lecture 12 - Maps, AR_VR_aaaaHardware.pptx
Lecture 12 - Maps, AR_VR_aaaaHardware.pptxLecture 12 - Maps, AR_VR_aaaaHardware.pptx
Lecture 12 - Maps, AR_VR_aaaaHardware.pptx
 
Compose Camp: Introduction to Kotlin.pptx
Compose Camp: Introduction to Kotlin.pptxCompose Camp: Introduction to Kotlin.pptx
Compose Camp: Introduction to Kotlin.pptx
 
Internal Android Library Management (DroidCon SF 2016, Droidcon Italy 2016)
Internal Android Library Management (DroidCon SF 2016, Droidcon Italy 2016)Internal Android Library Management (DroidCon SF 2016, Droidcon Italy 2016)
Internal Android Library Management (DroidCon SF 2016, Droidcon Italy 2016)
 
Workshop 15: Ionic framework
Workshop 15: Ionic frameworkWorkshop 15: Ionic framework
Workshop 15: Ionic framework
 
Building Serverless APIs (January 2017)
Building Serverless APIs (January 2017)Building Serverless APIs (January 2017)
Building Serverless APIs (January 2017)
 
Hosting Your Own OTA Update Service
Hosting Your Own OTA Update ServiceHosting Your Own OTA Update Service
Hosting Your Own OTA Update Service
 
Alfresco Development Framework Basic
Alfresco Development Framework BasicAlfresco Development Framework Basic
Alfresco Development Framework Basic
 
Telerik AppBuilder Presentation for TelerikNEXT Conference
Telerik AppBuilder Presentation for TelerikNEXT ConferenceTelerik AppBuilder Presentation for TelerikNEXT Conference
Telerik AppBuilder Presentation for TelerikNEXT Conference
 
Life After Mobilegeddon: App Deep Linking Strategies - Pubcon October 2015
Life After Mobilegeddon: App Deep Linking Strategies - Pubcon October 2015Life After Mobilegeddon: App Deep Linking Strategies - Pubcon October 2015
Life After Mobilegeddon: App Deep Linking Strategies - Pubcon October 2015
 
Life After Mobilegeddon: App Deep Linking Strategies - Pubcon October 2015
Life After Mobilegeddon: App Deep Linking Strategies - Pubcon October 2015Life After Mobilegeddon: App Deep Linking Strategies - Pubcon October 2015
Life After Mobilegeddon: App Deep Linking Strategies - Pubcon October 2015
 
Session-1 edited.pptx
Session-1 edited.pptxSession-1 edited.pptx
Session-1 edited.pptx
 
Progressive Web Application by Citytech
Progressive Web Application by CitytechProgressive Web Application by Citytech
Progressive Web Application by Citytech
 

Kürzlich hochgeladen

Payment Gateway Testing Simplified_ A Step-by-Step Guide for Beginners.pdf
Payment Gateway Testing Simplified_ A Step-by-Step Guide for Beginners.pdfPayment Gateway Testing Simplified_ A Step-by-Step Guide for Beginners.pdf
Payment Gateway Testing Simplified_ A Step-by-Step Guide for Beginners.pdfkalichargn70th171
 
call girls in Vaishali (Ghaziabad) 🔝 >༒8448380779 🔝 genuine Escort Service 🔝✔️✔️
call girls in Vaishali (Ghaziabad) 🔝 >༒8448380779 🔝 genuine Escort Service 🔝✔️✔️call girls in Vaishali (Ghaziabad) 🔝 >༒8448380779 🔝 genuine Escort Service 🔝✔️✔️
call girls in Vaishali (Ghaziabad) 🔝 >༒8448380779 🔝 genuine Escort Service 🔝✔️✔️Delhi Call girls
 
%in tembisa+277-882-255-28 abortion pills for sale in tembisa
%in tembisa+277-882-255-28 abortion pills for sale in tembisa%in tembisa+277-882-255-28 abortion pills for sale in tembisa
%in tembisa+277-882-255-28 abortion pills for sale in tembisamasabamasaba
 
Shapes for Sharing between Graph Data Spaces - and Epistemic Querying of RDF-...
Shapes for Sharing between Graph Data Spaces - and Epistemic Querying of RDF-...Shapes for Sharing between Graph Data Spaces - and Epistemic Querying of RDF-...
Shapes for Sharing between Graph Data Spaces - and Epistemic Querying of RDF-...Steffen Staab
 
%in ivory park+277-882-255-28 abortion pills for sale in ivory park
%in ivory park+277-882-255-28 abortion pills for sale in ivory park %in ivory park+277-882-255-28 abortion pills for sale in ivory park
%in ivory park+277-882-255-28 abortion pills for sale in ivory park masabamasaba
 
TECUNIQUE: Success Stories: IT Service provider
TECUNIQUE: Success Stories: IT Service providerTECUNIQUE: Success Stories: IT Service provider
TECUNIQUE: Success Stories: IT Service providermohitmore19
 
Introducing Microsoft’s new Enterprise Work Management (EWM) Solution
Introducing Microsoft’s new Enterprise Work Management (EWM) SolutionIntroducing Microsoft’s new Enterprise Work Management (EWM) Solution
Introducing Microsoft’s new Enterprise Work Management (EWM) SolutionOnePlan Solutions
 
%+27788225528 love spells in new york Psychic Readings, Attraction spells,Bri...
%+27788225528 love spells in new york Psychic Readings, Attraction spells,Bri...%+27788225528 love spells in new york Psychic Readings, Attraction spells,Bri...
%+27788225528 love spells in new york Psychic Readings, Attraction spells,Bri...masabamasaba
 
%in Durban+277-882-255-28 abortion pills for sale in Durban
%in Durban+277-882-255-28 abortion pills for sale in Durban%in Durban+277-882-255-28 abortion pills for sale in Durban
%in Durban+277-882-255-28 abortion pills for sale in Durbanmasabamasaba
 
%in kaalfontein+277-882-255-28 abortion pills for sale in kaalfontein
%in kaalfontein+277-882-255-28 abortion pills for sale in kaalfontein%in kaalfontein+277-882-255-28 abortion pills for sale in kaalfontein
%in kaalfontein+277-882-255-28 abortion pills for sale in kaalfonteinmasabamasaba
 
The Ultimate Test Automation Guide_ Best Practices and Tips.pdf
The Ultimate Test Automation Guide_ Best Practices and Tips.pdfThe Ultimate Test Automation Guide_ Best Practices and Tips.pdf
The Ultimate Test Automation Guide_ Best Practices and Tips.pdfkalichargn70th171
 
Announcing Codolex 2.0 from GDK Software
Announcing Codolex 2.0 from GDK SoftwareAnnouncing Codolex 2.0 from GDK Software
Announcing Codolex 2.0 from GDK SoftwareJim McKeeth
 
Direct Style Effect Systems - The Print[A] Example - A Comprehension Aid
Direct Style Effect Systems -The Print[A] Example- A Comprehension AidDirect Style Effect Systems -The Print[A] Example- A Comprehension Aid
Direct Style Effect Systems - The Print[A] Example - A Comprehension AidPhilip Schwarz
 
Chinsurah Escorts ☎️8617697112 Starting From 5K to 15K High Profile Escorts ...
Chinsurah Escorts ☎️8617697112  Starting From 5K to 15K High Profile Escorts ...Chinsurah Escorts ☎️8617697112  Starting From 5K to 15K High Profile Escorts ...
Chinsurah Escorts ☎️8617697112 Starting From 5K to 15K High Profile Escorts ...Nitya salvi
 
%in Harare+277-882-255-28 abortion pills for sale in Harare
%in Harare+277-882-255-28 abortion pills for sale in Harare%in Harare+277-882-255-28 abortion pills for sale in Harare
%in Harare+277-882-255-28 abortion pills for sale in Hararemasabamasaba
 
%+27788225528 love spells in Vancouver Psychic Readings, Attraction spells,Br...
%+27788225528 love spells in Vancouver Psychic Readings, Attraction spells,Br...%+27788225528 love spells in Vancouver Psychic Readings, Attraction spells,Br...
%+27788225528 love spells in Vancouver Psychic Readings, Attraction spells,Br...masabamasaba
 
%in kempton park+277-882-255-28 abortion pills for sale in kempton park
%in kempton park+277-882-255-28 abortion pills for sale in kempton park %in kempton park+277-882-255-28 abortion pills for sale in kempton park
%in kempton park+277-882-255-28 abortion pills for sale in kempton park masabamasaba
 
%in Hazyview+277-882-255-28 abortion pills for sale in Hazyview
%in Hazyview+277-882-255-28 abortion pills for sale in Hazyview%in Hazyview+277-882-255-28 abortion pills for sale in Hazyview
%in Hazyview+277-882-255-28 abortion pills for sale in Hazyviewmasabamasaba
 
%in tembisa+277-882-255-28 abortion pills for sale in tembisa
%in tembisa+277-882-255-28 abortion pills for sale in tembisa%in tembisa+277-882-255-28 abortion pills for sale in tembisa
%in tembisa+277-882-255-28 abortion pills for sale in tembisamasabamasaba
 

Kürzlich hochgeladen (20)

Payment Gateway Testing Simplified_ A Step-by-Step Guide for Beginners.pdf
Payment Gateway Testing Simplified_ A Step-by-Step Guide for Beginners.pdfPayment Gateway Testing Simplified_ A Step-by-Step Guide for Beginners.pdf
Payment Gateway Testing Simplified_ A Step-by-Step Guide for Beginners.pdf
 
call girls in Vaishali (Ghaziabad) 🔝 >༒8448380779 🔝 genuine Escort Service 🔝✔️✔️
call girls in Vaishali (Ghaziabad) 🔝 >༒8448380779 🔝 genuine Escort Service 🔝✔️✔️call girls in Vaishali (Ghaziabad) 🔝 >༒8448380779 🔝 genuine Escort Service 🔝✔️✔️
call girls in Vaishali (Ghaziabad) 🔝 >༒8448380779 🔝 genuine Escort Service 🔝✔️✔️
 
%in tembisa+277-882-255-28 abortion pills for sale in tembisa
%in tembisa+277-882-255-28 abortion pills for sale in tembisa%in tembisa+277-882-255-28 abortion pills for sale in tembisa
%in tembisa+277-882-255-28 abortion pills for sale in tembisa
 
Shapes for Sharing between Graph Data Spaces - and Epistemic Querying of RDF-...
Shapes for Sharing between Graph Data Spaces - and Epistemic Querying of RDF-...Shapes for Sharing between Graph Data Spaces - and Epistemic Querying of RDF-...
Shapes for Sharing between Graph Data Spaces - and Epistemic Querying of RDF-...
 
%in ivory park+277-882-255-28 abortion pills for sale in ivory park
%in ivory park+277-882-255-28 abortion pills for sale in ivory park %in ivory park+277-882-255-28 abortion pills for sale in ivory park
%in ivory park+277-882-255-28 abortion pills for sale in ivory park
 
TECUNIQUE: Success Stories: IT Service provider
TECUNIQUE: Success Stories: IT Service providerTECUNIQUE: Success Stories: IT Service provider
TECUNIQUE: Success Stories: IT Service provider
 
Introducing Microsoft’s new Enterprise Work Management (EWM) Solution
Introducing Microsoft’s new Enterprise Work Management (EWM) SolutionIntroducing Microsoft’s new Enterprise Work Management (EWM) Solution
Introducing Microsoft’s new Enterprise Work Management (EWM) Solution
 
%+27788225528 love spells in new york Psychic Readings, Attraction spells,Bri...
%+27788225528 love spells in new york Psychic Readings, Attraction spells,Bri...%+27788225528 love spells in new york Psychic Readings, Attraction spells,Bri...
%+27788225528 love spells in new york Psychic Readings, Attraction spells,Bri...
 
%in Durban+277-882-255-28 abortion pills for sale in Durban
%in Durban+277-882-255-28 abortion pills for sale in Durban%in Durban+277-882-255-28 abortion pills for sale in Durban
%in Durban+277-882-255-28 abortion pills for sale in Durban
 
%in kaalfontein+277-882-255-28 abortion pills for sale in kaalfontein
%in kaalfontein+277-882-255-28 abortion pills for sale in kaalfontein%in kaalfontein+277-882-255-28 abortion pills for sale in kaalfontein
%in kaalfontein+277-882-255-28 abortion pills for sale in kaalfontein
 
The Ultimate Test Automation Guide_ Best Practices and Tips.pdf
The Ultimate Test Automation Guide_ Best Practices and Tips.pdfThe Ultimate Test Automation Guide_ Best Practices and Tips.pdf
The Ultimate Test Automation Guide_ Best Practices and Tips.pdf
 
Announcing Codolex 2.0 from GDK Software
Announcing Codolex 2.0 from GDK SoftwareAnnouncing Codolex 2.0 from GDK Software
Announcing Codolex 2.0 from GDK Software
 
CHEAP Call Girls in Pushp Vihar (-DELHI )🔝 9953056974🔝(=)/CALL GIRLS SERVICE
CHEAP Call Girls in Pushp Vihar (-DELHI )🔝 9953056974🔝(=)/CALL GIRLS SERVICECHEAP Call Girls in Pushp Vihar (-DELHI )🔝 9953056974🔝(=)/CALL GIRLS SERVICE
CHEAP Call Girls in Pushp Vihar (-DELHI )🔝 9953056974🔝(=)/CALL GIRLS SERVICE
 
Direct Style Effect Systems - The Print[A] Example - A Comprehension Aid
Direct Style Effect Systems -The Print[A] Example- A Comprehension AidDirect Style Effect Systems -The Print[A] Example- A Comprehension Aid
Direct Style Effect Systems - The Print[A] Example - A Comprehension Aid
 
Chinsurah Escorts ☎️8617697112 Starting From 5K to 15K High Profile Escorts ...
Chinsurah Escorts ☎️8617697112  Starting From 5K to 15K High Profile Escorts ...Chinsurah Escorts ☎️8617697112  Starting From 5K to 15K High Profile Escorts ...
Chinsurah Escorts ☎️8617697112 Starting From 5K to 15K High Profile Escorts ...
 
%in Harare+277-882-255-28 abortion pills for sale in Harare
%in Harare+277-882-255-28 abortion pills for sale in Harare%in Harare+277-882-255-28 abortion pills for sale in Harare
%in Harare+277-882-255-28 abortion pills for sale in Harare
 
%+27788225528 love spells in Vancouver Psychic Readings, Attraction spells,Br...
%+27788225528 love spells in Vancouver Psychic Readings, Attraction spells,Br...%+27788225528 love spells in Vancouver Psychic Readings, Attraction spells,Br...
%+27788225528 love spells in Vancouver Psychic Readings, Attraction spells,Br...
 
%in kempton park+277-882-255-28 abortion pills for sale in kempton park
%in kempton park+277-882-255-28 abortion pills for sale in kempton park %in kempton park+277-882-255-28 abortion pills for sale in kempton park
%in kempton park+277-882-255-28 abortion pills for sale in kempton park
 
%in Hazyview+277-882-255-28 abortion pills for sale in Hazyview
%in Hazyview+277-882-255-28 abortion pills for sale in Hazyview%in Hazyview+277-882-255-28 abortion pills for sale in Hazyview
%in Hazyview+277-882-255-28 abortion pills for sale in Hazyview
 
%in tembisa+277-882-255-28 abortion pills for sale in tembisa
%in tembisa+277-882-255-28 abortion pills for sale in tembisa%in tembisa+277-882-255-28 abortion pills for sale in tembisa
%in tembisa+277-882-255-28 abortion pills for sale in tembisa
 

Scaling your Android App With Modularization