SlideShare a Scribd company logo
1 of 87
Download to read offline
Dark side of Android
apps modularization
David Bilík
@bilikdavid
#selfpromo
> 8 years experience with Android development
> Android Team Lead @AckeeCZ
> Lecturer of Android course at Czech Technical University in Prague
> Focused on architecture, testing and beautiful designs
Why am I here?
Why am I here?
> Modularization became popular topic in 2018
> Every conference had at least one talk “How to modularize your app”
> Since we are hype-oriented programmers we hopped on the train in 2019
> But we’ve hit some bumps along the way
What we expected
> Faster build times
> Improved architecture
> Preparation for instant apps/dynamic features
Modularized architecture
> Main inspiration
Modularized architecture
Modularized architecture
> Looks simple
> But contains multiple shady areas
> Google does not have the answers
> Community does not have all answers
> Because they do not exist
Gradle
Gradle
> Apps contains tens of dependencies
> Not all of them used in all modules
> Good idea to define them in one place
buildSrc
> Leverage buildSrc folder in gradle project
> Contains common code (constants, tasks) used in build.gradle scripts
> Can be written in Kotlin
object Config {

const val minSdk = 26

const val compileSdk = 29

const val targetSdk = 29

val javaVersion = JavaVersion.VERSION_1_8

}

android {

compileSdkVersion Config.compileSdk

defaultConfig {

minSdkVersion Config.minSdk

targetSdkVersion Config.targetSdk

versionCode 1

versionName "1.0"

}

buildSrc/src/main/kotlin/Config.kt
app/build.gradle
buildSrc/src/main/kotlin/Deps.kt
object Deps {



"// Koin

private const val koinVersion = "2.0.1"

const val koin = "org.koin:koin-android:$koinVersion"

const val koinScope = "org.koin:koin-androidx-scope:$koinVersion"

const val koinViewModel = "org.koin:koin-androidx-viewmodel:$koinVersion"

"// Epoxy

private const val epoxyVersion = "3.9.0"

const val epoxy = "com.airbnb.android:epoxy:$epoxyVersion"

const val epoxyProcessor = "com.airbnb.android:epoxy-processor:$epoxyVersion"

"// OkHttp

private const val okHttpVersion = "4.3.1"

const val okHttp = "com.squareup.okhttp3:okhttp:$okHttpVersion"

const val okHttpLoggingInterceptor = “com.squareup.okhttp3:logging-interceptor:$okHttpVersion"

…

app/build.gradle
dependencies {

"// Koin

implementation Deps.koin

implementation Deps.koinViewModel
Version updates
> Automatic Android Studio version check is not available
> Plugins exists but not they are not suitable for us
> So.. we check manually 😞
Shared gradle scripts
> A lot of the build.gradle code will be completely the same
≥ defaultConfig with minSdk, compileOptions, applied plugins, …
> Common code can be extracted and applied in module build.gradle
scripts
> Applied code is merged with the code inside module’s build.gradle script
apply plugin: 'com.android.library'

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-android-extensions'

android {

compileSdkVersion Config.compileSdk

defaultConfig {

minSdkVersion Config.minSdk

targetSdkVersion Config.targetSdk

versionCode 1

versionName "1.0"

}

buildTypes {

…

}

productFlavors {

…

}

compileOptions {

sourceCompatibility = 1.8

targetCompatibility = 1.8

}

…
…

kotlinOptions {

jvmTarget = "1.8"

freeCompilerArgs += "-Xopt-in=kotlin.time.ExperimentalTime"

}

testOptions {

unitTests.all {

setIgnoreFailures(true)

}

}

}

gradle/common-library-script.gradle
apply from: “$rootDir/gradle/common-library-script.gradle”

dependencies {

implementation Deps.junit

implementation Deps.rxJava

implementation Deps.rxAndroid

implementation Deps.appCompat

}

mymodule/build.gradle
android {

productFlavors {

flavorDimensions "api"

devApi {

dimension "api"

}

prodApi {

dimension "api"

}

}

}

gradle/common-library-script.gradle
networking/build.gradle
apply from: “$rootDir/gradle/common-library-script.gradle”

android {

productFlavors {

flavorDimensions "api"

devApi {

dimension "api"

buildConfigField("String", "BASE_URL", ""api-development.myapp.com"")

}

prodApi {

dimension “api"

buildConfigField("String", "BASE_URL", ""api.myapp.com"")

}

}

}
Build variants
Build variants
> Example: flavors defining base url for api environment
> What modules care about this flavor?
≥ :app - control what variant of app to build
≥ :networking - contains buildConfigFields with base api url
Build variants
* What went wrong:

Could not determine the dependencies of task ':events:compileDebugAidl'.

> Could not resolve all task dependencies for configuration ':events:debugCompileClasspath'.

> Could not resolve project :networking.

Required by:

project :events

> Cannot choose between the following variants of project :networking:

- devApiDebugRuntime

- devApiDebugUnitTestCompile

- devApiDebugUnitTestRuntime

- devApiReleaseAndroidTestCompile

- devApiReleaseAndroidTestRuntime

- devApiReleaseApiElements

…

./gradlew assembleDevApiDebug
Build variants
events/build.gradle
android {

defaultConfig {

missingDimensionStrategy "api", "devApi"

}

}
gradle/common-library-script.gradle
android {

…

productFlavors {

flavorDimensions "api"

devApi {

dimension "api"

}

prodApi {

dimension "api"

}

}

…

}
app/build.gradle
android {

…

productFlavors {

flavorDimensions "api"

devApi {

dimension "api"

buildConfigField("String", "BASE_URL", ""api-development.myapp.com"")

}

prodApi {

dimension "api"

buildConfigField("String", "BASE_URL", ""api.myapp.com"")

}

}

…

}
networking/src/main/java/…/ApiDefinition.kt
data class ApiDefinition(

val url: HttpUrl

)

fun provideRetrofit(api: ApiDefinition): Retrofit {

return Retrofit.Builder()

.baseUrl(api.url)

.build()

}

networking/src/main/java/…/RetrofitDI.kt
app/src/main/java/…/ApiDI.kt
fun provideApiDefinition(): ApiDefinition{

return ApiDefinition(

BuildConfig.BASE_URL.toHttpUrl()

)

}
Final tip
> Improve organization of modules with directories
Module folders protip
> Prefix module name with directory for automatic placement
Code sharing
Code sharing
> Our apps (try to) follow Uncle Bob’s Clean Architecture
Clean architecture Android
Shared library module
Split feature module
Shared module
Shared module
Navigation
Navigation
> Feature modules are independent of each other
> ActivityA in :featureA does not have access to ActivityB in :featureB
> unified solution for in-feature and between-feature navigation
Navigation #1 solution
object Intents {

fun startingActivity(context: Context): Intent? {

return loadClass<Activity>(“cz.ackee.sample.StartingActivity”)

"?.let { Intent(context, it) }

}

}

object Fragments {

fun contactsFragment(context: Context): Fragment? {

return loadClass<Fragment>(“cz.ackee.sample.contacts.ContactsFragment”)

"?.let { Fragment.instantiate(context, it.name) }

}

}

navigation/src/main/…/Intents.kt
navigation/src/main/…/Fragments.kt
fun <T> loadClass(className:String): Class<T>? {

return try {

Class.forName(className)

} catch (e: Exception) {

null

} as? Class<T>

}
#1 solution - problems
> Not using typical pattern like MyFragment.newInstance(args)
> Fully qualified class names in Strings not changed in refactorings
Arguments passing
> Problems with passing the arguments through :navigation module
> Does not have access to feature classes
object Fragments {

fun contactDetailFragment(context: Context, contact: Contact): Fragment? {

return loadClass<Fragment>("cz.ackee.sample.contact.ContactDetailFragment")

"?.let { Fragment.instantiate(context, it.name, bundleOf(Arguments.CONTACT_KEY to contact)) }

}

}

object Fragments {

fun contactDetailFragment(context: Context, contact: Parcelable): Fragment? {

return loadClass<Fragment>("cz.ackee.sample.contact.ContactDetailFragment")

"?.let { Fragment.instantiate(context, it.name, bundleOf(Arguments.CONTACT_KEY to contact)) }

}

}

navigation/src/main/…/Fragments.kt
Parcelable solution
> Easiest solution
> Type safety is lost
object Fragments {

fun contactDetailFragment(context: Context, contact: ContactDetailNavArgs): Fragment? {

return ClassesCache.loadClassOrNull<Fragment>("cz.ackee.sample.contact.ContactDetailFragment")

"?.let { Fragment.instantiate(context, it.name, bundleOf(NAV_ARGS_KEY to navArgs)) }

}

}

@Parcelize

data class ContactDetailNavArgs(

val contactId: String,

val name: String

): Parcelable

navigation/src/main/…/navargs/ContactDetailNavArgs.kt
navigation/src/main/…/Fragments.kt
inline fun <reified T: Parcelable> Fragment.navArgs() : T {

return requireArguments().getParcelable(NAV_ARGS_KEY)

}

class ContactDetailFragment : Fragment() {

override fun onCreate(savedInstanceState: Bundle?) {

super.onCreate(savedInstanceState)

toolbar.title = navArgs<ContactDetailNavArgs>().name

}

}

contacts/src/main/…/ContactDetailFragment.kt
navigation/src/main/…/FragmentKtx.kt
NavArgs solution
> Improved type safety
> More boilerplate
Abstracted navigation
> Introduce abstraction over navigation
> Free Fragments/Activities of knowing details of navigation
interface Navigator {

fun openContactDetail(args: ContactDetailNavArgs)
}

class ContactsListFragment : Fragment() {



private val navigator: Navigator by inject()

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

super.onViewCreated(view, savedInstanceState)

contactsList.setOnContactClickListener { contact "->

navigator.openContactDetail(ContactDetailNavArgs(contact.id, contact.name))

}

}

}

navigation/src/main/…/Navigator.kt
contacts/src/main/…/ContactDetailFragment.kt
Navigation Architecture Component
> Navigator implemented with Navigation Architecture Component
> :navigation module still a library module
> Contains navigation graphs, Navigator interface and implementation of this
interface
<?xml version="1.0" encoding="utf-8"?>

<navigation xmlns:android="http:"//schemas.android.com/apk/res/android"

xmlns:app="http:"//schemas.android.com/apk/res-auto"

android:id="@+id/navigation_graph"

app:startDestination="@+id/navigation_contacts_list">

<fragment

android:id="@+id/navigation_contacts_list"

android:name="cz.bilik.sample.contacts.ContactsListFragment" >

<action

android:id="@+id/navigation_action_open_contact_detail"

app:destination="@id/navigation_contact_detail"

app:enterAnim="@anim/nav_default_enter_anim"

app:exitAnim="@anim/nav_default_exit_anim"

app:popEnterAnim="@anim/nav_default_pop_enter_anim"

app:popExitAnim="@anim/nav_default_exit_anim" "/>

"</fragment>

<fragment

android:id="@+id/navigation_contact_detail"

android:name="cz.bilik.sample.contacts.ContactDetailFragment" "/>

"</navigation>
navigation/src/main/res/values/nav_graph.xml
class NavigationComponentNavigator : Navigator {

private var navigationController: NavController? = null

override fun openContactDetail(navArgs: ContactDetailNavArgs) {

navigationController"?.navigate(

R.id.navigation_action_open_contact_detail,

navArgs.toBundle()

)

}

}

navigation/src/main/…/NavigationComponentNavigator.kt
fun bindController(navigationController: NavController) {

this.navigationController = navigationController

}

fun unbindController() {

this.navigationController = null

}
abstract class NavigationActivity : AppCompatActivity() {

val navigator : NavigationComponentNavigator by inject()

override fun onCreate(savedInstanceState: Bundle?) {

super.onCreate(savedInstanceState)

setContentView(R.layout.activity_navigation)

setupNavigation()

}

private fun setupNavigation() {

navigator.bindController(findNavController(R.id.nav_host_fragment))

}

override fun onDestroy() {

super.onDestroy()

navigator.unbindController()

}

}

navigation/src/main/…/NavigationActivity.kt
Abstracted navigation
> Growing Navigator interface
≥ Create multiple smaller Navigators and NavigationActivity
implements all of them
> Multiple Activity
≥ Multiple navigation graphs with multiple NavigationActivitys
Database
Database
> Room used as database library
> No native support for multimodule projects
> Need to define all entities and DAOs in single Database class
Database per feature
Database per feature
➕ Encapsulated within single module
➕ Each database can have different settings - eg. descructive migrations rule
− Aggregations over mutliple tables not possible
− Multiple connections to database
Single database
Single database
➕ Easy to maintain
− Breaks the encapsulation of the features.
Compromise
> :database module containing definition of RoomDatabase
> Keep DAOs and entities within features
Compromise
Compromise
> :database module depends on all features and define RoomDatabase class
> DI for DAOs must be defined in this module
@Entity(tableName = "contacts")

data class DbContact(

@PrimaryKey(autoGenerate = true) val id: Long = 0,

val eventId: Int,

val name: String

)

features/contacts/…/DbContactfeatures/events/…/DbEvent
@Entity(tableName = "events")

data class DbEvent(

@PrimaryKey val id: Int = 0,

val name: String

)

features/events/…/EventsDao
@Query("""

select events.* from events 

join contacts on (contacts.eventId = events.id) 

where contacts.id "== :contactId

""")

abstract fun getEventForContact(contactId: Long): DbEvent
Testing
Testing
> Where to define utilities in tests?
≥ custom JUnit rules for RxJava/Coroutines
≥ extensions on LiveData to retrieve value once available
≥ …
Testing module
> Define them in one place
> Can’t be placed in :base module test source set folder
> Gradle does not support dependencies on test source sets of different
module in android projects
Testing module
> Separate:testing library module
> Contains also dependencies to common testing dependencies
≥ Mocking framework, testing dependencies for coroutines, AndroidX, …
> Important note - don’t declare this dependencies as testXXX and also don’t
place the code to the test source set folder
fun <T> LiveData<T>.getOrAwaitValue(

time: Long = 2,

timeUnit: TimeUnit = TimeUnit.SECONDS,

afterObserve: () "-> Unit = {}

): T {

…

}
libraries/testing/src/main/java/LiveDataKtx.kt
libraries/testing/build.gradle
dependencies {

api Deps.architectureComponentsTesting

api Deps.mockitoInline

api Deps.mockitoKotlin

api Deps.coroutinesTesting

}
features/contacts/build.gradle
dependencies {

…

testImplementation project(':libraries:testing')

}
Test fixtures
> Same problem with test fixtures of feature module
> How to reuse eg. test doubles in different feature module tests?
Test fixtures
Testing database
Testing database
libraries/database-testing/src/main/java/RoomDatabaseRule.kt
class RoomDatabaseRule : TestWatcher() {

lateinit var database: MyDatabase

override fun starting(description: Description?) {

database = Room.inMemoryDatabaseBuilder(ApplicationProvider.getApplicationContext(), MyDatabase"::class.java)

.allowMainThreadQueries()

.build()

}

override fun finished(description: Description?) {

database.close()

}

}
@RunWith(AndroidJUnit4"::class)

class ContactsLocalDataSourceTest {

@get:Rule

val databaseRule = RoomDatabaseRule()

private fun createDataSource(): ContactsLocalDataSource {

return ContactsLocalDataSource(

databaseRule.database.contactsDao()

)

}

…
features/contacts/app/src/test/…/ContactsLocalDataSourceTest.kt
Networking
Networking
> :networking library module with common setup - OkHttpClient, Moshi,
Retrofit
> Each feature contains Retrofit API interface with transfer objects (DTO)
≥ eg. LoginRequest, LoginResponse
Networking
> What about generated classes?
≥ gRPC, Swagger-codegen
> Generation of this classes is handled in :networking module
> Feature modules use this generated classes
SUMMARY
Summary
> Would I modularize new app right from the beginning?
> Have we learned anything?
> What about our expectations?
Questions time 🤗
Thanks for participation!
@bilikdavid

More Related Content

What's hot

What's hot (20)

Sharper Better Faster Dagger ‡ - Droidcon SF
Sharper Better Faster Dagger ‡ - Droidcon SFSharper Better Faster Dagger ‡ - Droidcon SF
Sharper Better Faster Dagger ‡ - Droidcon SF
 
Angular 2 Essential Training
Angular 2 Essential Training Angular 2 Essential Training
Angular 2 Essential Training
 
Introduction to angular 2
Introduction to angular 2Introduction to angular 2
Introduction to angular 2
 
Angular tutorial
Angular tutorialAngular tutorial
Angular tutorial
 
Exploring Angular 2 - Episode 1
Exploring Angular 2 - Episode 1Exploring Angular 2 - Episode 1
Exploring Angular 2 - Episode 1
 
Angular modules in depth
Angular modules in depthAngular modules in depth
Angular modules in depth
 
Tech Webinar: Angular 2, Introduction to a new framework
Tech Webinar: Angular 2, Introduction to a new frameworkTech Webinar: Angular 2, Introduction to a new framework
Tech Webinar: Angular 2, Introduction to a new framework
 
Angular2 for Beginners
Angular2 for BeginnersAngular2 for Beginners
Angular2 for Beginners
 
Di code steps
Di code stepsDi code steps
Di code steps
 
Building maintainable app #droidconzg
Building maintainable app #droidconzgBuilding maintainable app #droidconzg
Building maintainable app #droidconzg
 
Lecture 32
Lecture 32Lecture 32
Lecture 32
 
Introduction to Angular 2
Introduction to Angular 2Introduction to Angular 2
Introduction to Angular 2
 
Angular 5 presentation for beginners
Angular 5 presentation for beginnersAngular 5 presentation for beginners
Angular 5 presentation for beginners
 
Angular2 + rxjs
Angular2 + rxjsAngular2 + rxjs
Angular2 + rxjs
 
Angular 9
Angular 9 Angular 9
Angular 9
 
Angular 8
Angular 8 Angular 8
Angular 8
 
Angular 2 - The Next Framework
Angular 2 - The Next FrameworkAngular 2 - The Next Framework
Angular 2 - The Next Framework
 
React Native custom components
React Native custom componentsReact Native custom components
React Native custom components
 
Introduction to angular with a simple but complete project
Introduction to angular with a simple but complete projectIntroduction to angular with a simple but complete project
Introduction to angular with a simple but complete project
 
Single Page Applications with AngularJS 2.0
Single Page Applications with AngularJS 2.0 Single Page Applications with AngularJS 2.0
Single Page Applications with AngularJS 2.0
 

Similar to Dark side of Android apps modularization

Using advanced C# features in Sharepoint development
Using advanced C# features in Sharepoint developmentUsing advanced C# features in Sharepoint development
Using advanced C# features in Sharepoint development
sadomovalex
 

Similar to Dark side of Android apps modularization (20)

Comment développer une application mobile en 8 semaines - Meetup PAUG 24-01-2023
Comment développer une application mobile en 8 semaines - Meetup PAUG 24-01-2023Comment développer une application mobile en 8 semaines - Meetup PAUG 24-01-2023
Comment développer une application mobile en 8 semaines - Meetup PAUG 24-01-2023
 
A/B test your Android build setup with ASPoet
A/B test your Android build setup with ASPoetA/B test your Android build setup with ASPoet
A/B test your Android build setup with ASPoet
 
Mastering the NDK with Android Studio 2.0 and the gradle-experimental plugin
Mastering the NDK with Android Studio 2.0 and the gradle-experimental pluginMastering the NDK with Android Studio 2.0 and the gradle-experimental plugin
Mastering the NDK with Android Studio 2.0 and the gradle-experimental plugin
 
Advanced Dagger talk from 360andev
Advanced Dagger talk from 360andevAdvanced Dagger talk from 360andev
Advanced Dagger talk from 360andev
 
Kotlin Multiplatform in Action - Alexandr Pogrebnyak - IceRockDev
Kotlin Multiplatform in Action - Alexandr Pogrebnyak - IceRockDevKotlin Multiplatform in Action - Alexandr Pogrebnyak - IceRockDev
Kotlin Multiplatform in Action - Alexandr Pogrebnyak - IceRockDev
 
OO Design and Design Patterns in C++
OO Design and Design Patterns in C++ OO Design and Design Patterns in C++
OO Design and Design Patterns in C++
 
Using advanced C# features in Sharepoint development
Using advanced C# features in Sharepoint developmentUsing advanced C# features in Sharepoint development
Using advanced C# features in Sharepoint development
 
Building Scalable JavaScript Apps
Building Scalable JavaScript AppsBuilding Scalable JavaScript Apps
Building Scalable JavaScript Apps
 
From Containerization to Modularity
From Containerization to ModularityFrom Containerization to Modularity
From Containerization to Modularity
 
Angular performance slides
Angular performance slidesAngular performance slides
Angular performance slides
 
Full Stack React Workshop [CSSC x GDSC]
Full Stack React Workshop [CSSC x GDSC]Full Stack React Workshop [CSSC x GDSC]
Full Stack React Workshop [CSSC x GDSC]
 
Gradle: One technology to build them all
Gradle: One technology to build them allGradle: One technology to build them all
Gradle: One technology to build them all
 
Exploring the power of Gradle in android studio - Basics & Beyond
Exploring the power of Gradle in android studio - Basics & BeyondExploring the power of Gradle in android studio - Basics & Beyond
Exploring the power of Gradle in android studio - Basics & Beyond
 
Hacking the Codename One Source Code - Part IV - Transcript.pdf
Hacking the Codename One Source Code - Part IV - Transcript.pdfHacking the Codename One Source Code - Part IV - Transcript.pdf
Hacking the Codename One Source Code - Part IV - Transcript.pdf
 
Writing modular java script
Writing modular java scriptWriting modular java script
Writing modular java script
 
[DEPRECATED]Gradle the android
[DEPRECATED]Gradle the android[DEPRECATED]Gradle the android
[DEPRECATED]Gradle the android
 
OpenDaylight Developer Experience 2.0
 OpenDaylight Developer Experience 2.0 OpenDaylight Developer Experience 2.0
OpenDaylight Developer Experience 2.0
 
React Native for multi-platform mobile applications
React Native for multi-platform mobile applicationsReact Native for multi-platform mobile applications
React Native for multi-platform mobile applications
 
Angular kickstart slideshare
Angular kickstart   slideshareAngular kickstart   slideshare
Angular kickstart slideshare
 
Level Up Your Android Build -Droidcon Berlin 2015
Level Up Your Android Build -Droidcon Berlin 2015Level Up Your Android Build -Droidcon Berlin 2015
Level Up Your Android Build -Droidcon Berlin 2015
 

Recently uploaded

AI/ML Infra Meetup | Improve Speed and GPU Utilization for Model Training & S...
AI/ML Infra Meetup | Improve Speed and GPU Utilization for Model Training & S...AI/ML Infra Meetup | Improve Speed and GPU Utilization for Model Training & S...
AI/ML Infra Meetup | Improve Speed and GPU Utilization for Model Training & S...
Alluxio, Inc.
 

Recently uploaded (20)

Top Mobile App Development Companies 2024
Top Mobile App Development Companies 2024Top Mobile App Development Companies 2024
Top Mobile App Development Companies 2024
 
A Guideline to Gorgias to to Re:amaze Data Migration
A Guideline to Gorgias to to Re:amaze Data MigrationA Guideline to Gorgias to to Re:amaze Data Migration
A Guideline to Gorgias to to Re:amaze Data Migration
 
5 Reasons Driving Warehouse Management Systems Demand
5 Reasons Driving Warehouse Management Systems Demand5 Reasons Driving Warehouse Management Systems Demand
5 Reasons Driving Warehouse Management Systems Demand
 
Secure Software Ecosystem Teqnation 2024
Secure Software Ecosystem Teqnation 2024Secure Software Ecosystem Teqnation 2024
Secure Software Ecosystem Teqnation 2024
 
Implementing KPIs and Right Metrics for Agile Delivery Teams.pdf
Implementing KPIs and Right Metrics for Agile Delivery Teams.pdfImplementing KPIs and Right Metrics for Agile Delivery Teams.pdf
Implementing KPIs and Right Metrics for Agile Delivery Teams.pdf
 
CompTIA Security+ (Study Notes) for cs.pdf
CompTIA Security+ (Study Notes) for cs.pdfCompTIA Security+ (Study Notes) for cs.pdf
CompTIA Security+ (Study Notes) for cs.pdf
 
A Python-based approach to data loading in TM1 - Using Airflow as an ETL for TM1
A Python-based approach to data loading in TM1 - Using Airflow as an ETL for TM1A Python-based approach to data loading in TM1 - Using Airflow as an ETL for TM1
A Python-based approach to data loading in TM1 - Using Airflow as an ETL for TM1
 
INGKA DIGITAL: Linked Metadata by Design
INGKA DIGITAL: Linked Metadata by DesignINGKA DIGITAL: Linked Metadata by Design
INGKA DIGITAL: Linked Metadata by Design
 
What need to be mastered as AI-Powered Java Developers
What need to be mastered as AI-Powered Java DevelopersWhat need to be mastered as AI-Powered Java Developers
What need to be mastered as AI-Powered Java Developers
 
APVP,apvp apvp High quality supplier safe spot transport, 98% purity
APVP,apvp apvp High quality supplier safe spot transport, 98% purityAPVP,apvp apvp High quality supplier safe spot transport, 98% purity
APVP,apvp apvp High quality supplier safe spot transport, 98% purity
 
AI/ML Infra Meetup | Improve Speed and GPU Utilization for Model Training & S...
AI/ML Infra Meetup | Improve Speed and GPU Utilization for Model Training & S...AI/ML Infra Meetup | Improve Speed and GPU Utilization for Model Training & S...
AI/ML Infra Meetup | Improve Speed and GPU Utilization for Model Training & S...
 
The Impact of PLM Software on Fashion Production
The Impact of PLM Software on Fashion ProductionThe Impact of PLM Software on Fashion Production
The Impact of PLM Software on Fashion Production
 
OpenChain @ LF Japan Executive Briefing - May 2024
OpenChain @ LF Japan Executive Briefing - May 2024OpenChain @ LF Japan Executive Briefing - May 2024
OpenChain @ LF Japan Executive Briefing - May 2024
 
Crafting the Perfect Measurement Sheet with PLM Integration
Crafting the Perfect Measurement Sheet with PLM IntegrationCrafting the Perfect Measurement Sheet with PLM Integration
Crafting the Perfect Measurement Sheet with PLM Integration
 
Agnieszka Andrzejewska - BIM School Course in Kraków
Agnieszka Andrzejewska - BIM School Course in KrakówAgnieszka Andrzejewska - BIM School Course in Kraków
Agnieszka Andrzejewska - BIM School Course in Kraków
 
COMPUTER AND ITS COMPONENTS PPT.by naitik sharma Class 9th A mittal internati...
COMPUTER AND ITS COMPONENTS PPT.by naitik sharma Class 9th A mittal internati...COMPUTER AND ITS COMPONENTS PPT.by naitik sharma Class 9th A mittal internati...
COMPUTER AND ITS COMPONENTS PPT.by naitik sharma Class 9th A mittal internati...
 
10 Essential Software Testing Tools You Need to Know About.pdf
10 Essential Software Testing Tools You Need to Know About.pdf10 Essential Software Testing Tools You Need to Know About.pdf
10 Essential Software Testing Tools You Need to Know About.pdf
 
GraphSummit Stockholm - Neo4j - Knowledge Graphs and Product Updates
GraphSummit Stockholm - Neo4j - Knowledge Graphs and Product UpdatesGraphSummit Stockholm - Neo4j - Knowledge Graphs and Product Updates
GraphSummit Stockholm - Neo4j - Knowledge Graphs and Product Updates
 
AI/ML Infra Meetup | ML explainability in Michelangelo
AI/ML Infra Meetup | ML explainability in MichelangeloAI/ML Infra Meetup | ML explainability in Michelangelo
AI/ML Infra Meetup | ML explainability in Michelangelo
 
Studiovity film pre-production and screenwriting software
Studiovity film pre-production and screenwriting softwareStudiovity film pre-production and screenwriting software
Studiovity film pre-production and screenwriting software
 

Dark side of Android apps modularization

  • 1. Dark side of Android apps modularization David Bilík @bilikdavid
  • 2. #selfpromo > 8 years experience with Android development > Android Team Lead @AckeeCZ > Lecturer of Android course at Czech Technical University in Prague > Focused on architecture, testing and beautiful designs
  • 3. Why am I here?
  • 4. Why am I here? > Modularization became popular topic in 2018 > Every conference had at least one talk “How to modularize your app” > Since we are hype-oriented programmers we hopped on the train in 2019 > But we’ve hit some bumps along the way
  • 5. What we expected > Faster build times > Improved architecture > Preparation for instant apps/dynamic features
  • 8. Modularized architecture > Looks simple > But contains multiple shady areas > Google does not have the answers > Community does not have all answers > Because they do not exist
  • 10. Gradle > Apps contains tens of dependencies > Not all of them used in all modules > Good idea to define them in one place
  • 11. buildSrc > Leverage buildSrc folder in gradle project > Contains common code (constants, tasks) used in build.gradle scripts > Can be written in Kotlin
  • 12. object Config { const val minSdk = 26 const val compileSdk = 29 const val targetSdk = 29 val javaVersion = JavaVersion.VERSION_1_8 } android { compileSdkVersion Config.compileSdk defaultConfig { minSdkVersion Config.minSdk targetSdkVersion Config.targetSdk versionCode 1 versionName "1.0" } buildSrc/src/main/kotlin/Config.kt app/build.gradle
  • 13. buildSrc/src/main/kotlin/Deps.kt object Deps { "// Koin private const val koinVersion = "2.0.1" const val koin = "org.koin:koin-android:$koinVersion" const val koinScope = "org.koin:koin-androidx-scope:$koinVersion" const val koinViewModel = "org.koin:koin-androidx-viewmodel:$koinVersion" "// Epoxy private const val epoxyVersion = "3.9.0" const val epoxy = "com.airbnb.android:epoxy:$epoxyVersion" const val epoxyProcessor = "com.airbnb.android:epoxy-processor:$epoxyVersion" "// OkHttp private const val okHttpVersion = "4.3.1" const val okHttp = "com.squareup.okhttp3:okhttp:$okHttpVersion" const val okHttpLoggingInterceptor = “com.squareup.okhttp3:logging-interceptor:$okHttpVersion" … app/build.gradle dependencies { "// Koin implementation Deps.koin implementation Deps.koinViewModel
  • 14.
  • 15. Version updates > Automatic Android Studio version check is not available > Plugins exists but not they are not suitable for us > So.. we check manually 😞
  • 16. Shared gradle scripts > A lot of the build.gradle code will be completely the same ≥ defaultConfig with minSdk, compileOptions, applied plugins, … > Common code can be extracted and applied in module build.gradle scripts > Applied code is merged with the code inside module’s build.gradle script
  • 17. apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' android { compileSdkVersion Config.compileSdk defaultConfig { minSdkVersion Config.minSdk targetSdkVersion Config.targetSdk versionCode 1 versionName "1.0" } buildTypes { … } productFlavors { … } compileOptions { sourceCompatibility = 1.8 targetCompatibility = 1.8 } … … kotlinOptions { jvmTarget = "1.8" freeCompilerArgs += "-Xopt-in=kotlin.time.ExperimentalTime" } testOptions { unitTests.all { setIgnoreFailures(true) } } } gradle/common-library-script.gradle
  • 18. apply from: “$rootDir/gradle/common-library-script.gradle” dependencies { implementation Deps.junit implementation Deps.rxJava implementation Deps.rxAndroid implementation Deps.appCompat } mymodule/build.gradle
  • 19. android { productFlavors { flavorDimensions "api" devApi { dimension "api" } prodApi { dimension "api" } } } gradle/common-library-script.gradle
  • 20. networking/build.gradle apply from: “$rootDir/gradle/common-library-script.gradle” android { productFlavors { flavorDimensions "api" devApi { dimension "api" buildConfigField("String", "BASE_URL", ""api-development.myapp.com"") } prodApi { dimension “api" buildConfigField("String", "BASE_URL", ""api.myapp.com"") } } }
  • 22. Build variants > Example: flavors defining base url for api environment > What modules care about this flavor? ≥ :app - control what variant of app to build ≥ :networking - contains buildConfigFields with base api url
  • 24. * What went wrong: Could not determine the dependencies of task ':events:compileDebugAidl'. > Could not resolve all task dependencies for configuration ':events:debugCompileClasspath'. > Could not resolve project :networking. Required by: project :events > Cannot choose between the following variants of project :networking: - devApiDebugRuntime - devApiDebugUnitTestCompile - devApiDebugUnitTestRuntime - devApiReleaseAndroidTestCompile - devApiReleaseAndroidTestRuntime - devApiReleaseApiElements … ./gradlew assembleDevApiDebug
  • 27. gradle/common-library-script.gradle android { … productFlavors { flavorDimensions "api" devApi { dimension "api" } prodApi { dimension "api" } } … }
  • 28. app/build.gradle android { … productFlavors { flavorDimensions "api" devApi { dimension "api" buildConfigField("String", "BASE_URL", ""api-development.myapp.com"") } prodApi { dimension "api" buildConfigField("String", "BASE_URL", ""api.myapp.com"") } } … }
  • 29. networking/src/main/java/…/ApiDefinition.kt data class ApiDefinition( val url: HttpUrl ) fun provideRetrofit(api: ApiDefinition): Retrofit { return Retrofit.Builder() .baseUrl(api.url) .build() } networking/src/main/java/…/RetrofitDI.kt app/src/main/java/…/ApiDI.kt fun provideApiDefinition(): ApiDefinition{ return ApiDefinition( BuildConfig.BASE_URL.toHttpUrl() ) }
  • 30. Final tip > Improve organization of modules with directories
  • 31.
  • 32.
  • 33. Module folders protip > Prefix module name with directory for automatic placement
  • 35. Code sharing > Our apps (try to) follow Uncle Bob’s Clean Architecture
  • 42. Navigation > Feature modules are independent of each other > ActivityA in :featureA does not have access to ActivityB in :featureB > unified solution for in-feature and between-feature navigation
  • 44. object Intents { fun startingActivity(context: Context): Intent? { return loadClass<Activity>(“cz.ackee.sample.StartingActivity”) "?.let { Intent(context, it) } } } object Fragments { fun contactsFragment(context: Context): Fragment? { return loadClass<Fragment>(“cz.ackee.sample.contacts.ContactsFragment”) "?.let { Fragment.instantiate(context, it.name) } } } navigation/src/main/…/Intents.kt navigation/src/main/…/Fragments.kt fun <T> loadClass(className:String): Class<T>? { return try { Class.forName(className) } catch (e: Exception) { null } as? Class<T> }
  • 45. #1 solution - problems > Not using typical pattern like MyFragment.newInstance(args) > Fully qualified class names in Strings not changed in refactorings
  • 46. Arguments passing > Problems with passing the arguments through :navigation module > Does not have access to feature classes
  • 47. object Fragments { fun contactDetailFragment(context: Context, contact: Contact): Fragment? { return loadClass<Fragment>("cz.ackee.sample.contact.ContactDetailFragment") "?.let { Fragment.instantiate(context, it.name, bundleOf(Arguments.CONTACT_KEY to contact)) } } } object Fragments { fun contactDetailFragment(context: Context, contact: Parcelable): Fragment? { return loadClass<Fragment>("cz.ackee.sample.contact.ContactDetailFragment") "?.let { Fragment.instantiate(context, it.name, bundleOf(Arguments.CONTACT_KEY to contact)) } } } navigation/src/main/…/Fragments.kt
  • 48. Parcelable solution > Easiest solution > Type safety is lost
  • 49. object Fragments { fun contactDetailFragment(context: Context, contact: ContactDetailNavArgs): Fragment? { return ClassesCache.loadClassOrNull<Fragment>("cz.ackee.sample.contact.ContactDetailFragment") "?.let { Fragment.instantiate(context, it.name, bundleOf(NAV_ARGS_KEY to navArgs)) } } } @Parcelize data class ContactDetailNavArgs( val contactId: String, val name: String ): Parcelable navigation/src/main/…/navargs/ContactDetailNavArgs.kt navigation/src/main/…/Fragments.kt
  • 50. inline fun <reified T: Parcelable> Fragment.navArgs() : T { return requireArguments().getParcelable(NAV_ARGS_KEY) } class ContactDetailFragment : Fragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) toolbar.title = navArgs<ContactDetailNavArgs>().name } } contacts/src/main/…/ContactDetailFragment.kt navigation/src/main/…/FragmentKtx.kt
  • 51. NavArgs solution > Improved type safety > More boilerplate
  • 52. Abstracted navigation > Introduce abstraction over navigation > Free Fragments/Activities of knowing details of navigation
  • 53. interface Navigator { fun openContactDetail(args: ContactDetailNavArgs) } class ContactsListFragment : Fragment() { private val navigator: Navigator by inject() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) contactsList.setOnContactClickListener { contact "-> navigator.openContactDetail(ContactDetailNavArgs(contact.id, contact.name)) } } } navigation/src/main/…/Navigator.kt contacts/src/main/…/ContactDetailFragment.kt
  • 54. Navigation Architecture Component > Navigator implemented with Navigation Architecture Component > :navigation module still a library module > Contains navigation graphs, Navigator interface and implementation of this interface
  • 55. <?xml version="1.0" encoding="utf-8"?> <navigation xmlns:android="http:"//schemas.android.com/apk/res/android" xmlns:app="http:"//schemas.android.com/apk/res-auto" android:id="@+id/navigation_graph" app:startDestination="@+id/navigation_contacts_list"> <fragment android:id="@+id/navigation_contacts_list" android:name="cz.bilik.sample.contacts.ContactsListFragment" > <action android:id="@+id/navigation_action_open_contact_detail" app:destination="@id/navigation_contact_detail" app:enterAnim="@anim/nav_default_enter_anim" app:exitAnim="@anim/nav_default_exit_anim" app:popEnterAnim="@anim/nav_default_pop_enter_anim" app:popExitAnim="@anim/nav_default_exit_anim" "/> "</fragment> <fragment android:id="@+id/navigation_contact_detail" android:name="cz.bilik.sample.contacts.ContactDetailFragment" "/> "</navigation> navigation/src/main/res/values/nav_graph.xml
  • 56. class NavigationComponentNavigator : Navigator { private var navigationController: NavController? = null override fun openContactDetail(navArgs: ContactDetailNavArgs) { navigationController"?.navigate( R.id.navigation_action_open_contact_detail, navArgs.toBundle() ) } } navigation/src/main/…/NavigationComponentNavigator.kt fun bindController(navigationController: NavController) { this.navigationController = navigationController } fun unbindController() { this.navigationController = null }
  • 57. abstract class NavigationActivity : AppCompatActivity() { val navigator : NavigationComponentNavigator by inject() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_navigation) setupNavigation() } private fun setupNavigation() { navigator.bindController(findNavController(R.id.nav_host_fragment)) } override fun onDestroy() { super.onDestroy() navigator.unbindController() } } navigation/src/main/…/NavigationActivity.kt
  • 58. Abstracted navigation > Growing Navigator interface ≥ Create multiple smaller Navigators and NavigationActivity implements all of them > Multiple Activity ≥ Multiple navigation graphs with multiple NavigationActivitys
  • 60. Database > Room used as database library > No native support for multimodule projects > Need to define all entities and DAOs in single Database class
  • 62. Database per feature ➕ Encapsulated within single module ➕ Each database can have different settings - eg. descructive migrations rule − Aggregations over mutliple tables not possible − Multiple connections to database
  • 64. Single database ➕ Easy to maintain − Breaks the encapsulation of the features.
  • 65. Compromise > :database module containing definition of RoomDatabase > Keep DAOs and entities within features
  • 67. Compromise > :database module depends on all features and define RoomDatabase class > DI for DAOs must be defined in this module
  • 68. @Entity(tableName = "contacts") data class DbContact( @PrimaryKey(autoGenerate = true) val id: Long = 0, val eventId: Int, val name: String ) features/contacts/…/DbContactfeatures/events/…/DbEvent @Entity(tableName = "events") data class DbEvent( @PrimaryKey val id: Int = 0, val name: String ) features/events/…/EventsDao @Query(""" select events.* from events join contacts on (contacts.eventId = events.id) where contacts.id "== :contactId """) abstract fun getEventForContact(contactId: Long): DbEvent
  • 70. Testing > Where to define utilities in tests? ≥ custom JUnit rules for RxJava/Coroutines ≥ extensions on LiveData to retrieve value once available ≥ …
  • 71. Testing module > Define them in one place > Can’t be placed in :base module test source set folder > Gradle does not support dependencies on test source sets of different module in android projects
  • 72. Testing module > Separate:testing library module > Contains also dependencies to common testing dependencies ≥ Mocking framework, testing dependencies for coroutines, AndroidX, … > Important note - don’t declare this dependencies as testXXX and also don’t place the code to the test source set folder
  • 73. fun <T> LiveData<T>.getOrAwaitValue( time: Long = 2, timeUnit: TimeUnit = TimeUnit.SECONDS, afterObserve: () "-> Unit = {} ): T { … } libraries/testing/src/main/java/LiveDataKtx.kt libraries/testing/build.gradle dependencies { api Deps.architectureComponentsTesting api Deps.mockitoInline api Deps.mockitoKotlin api Deps.coroutinesTesting }
  • 75. Test fixtures > Same problem with test fixtures of feature module > How to reuse eg. test doubles in different feature module tests?
  • 79. libraries/database-testing/src/main/java/RoomDatabaseRule.kt class RoomDatabaseRule : TestWatcher() { lateinit var database: MyDatabase override fun starting(description: Description?) { database = Room.inMemoryDatabaseBuilder(ApplicationProvider.getApplicationContext(), MyDatabase"::class.java) .allowMainThreadQueries() .build() } override fun finished(description: Description?) { database.close() } }
  • 80. @RunWith(AndroidJUnit4"::class) class ContactsLocalDataSourceTest { @get:Rule val databaseRule = RoomDatabaseRule() private fun createDataSource(): ContactsLocalDataSource { return ContactsLocalDataSource( databaseRule.database.contactsDao() ) } … features/contacts/app/src/test/…/ContactsLocalDataSourceTest.kt
  • 82. Networking > :networking library module with common setup - OkHttpClient, Moshi, Retrofit > Each feature contains Retrofit API interface with transfer objects (DTO) ≥ eg. LoginRequest, LoginResponse
  • 83. Networking > What about generated classes? ≥ gRPC, Swagger-codegen > Generation of this classes is handled in :networking module > Feature modules use this generated classes
  • 85. Summary > Would I modularize new app right from the beginning? > Have we learned anything? > What about our expectations?