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

Abortion Pills For Sale WhatsApp[[+27737758557]] In Birch Acres, Abortion Pil...
Abortion Pills For Sale WhatsApp[[+27737758557]] In Birch Acres, Abortion Pil...Abortion Pills For Sale WhatsApp[[+27737758557]] In Birch Acres, Abortion Pil...
Abortion Pills For Sale WhatsApp[[+27737758557]] In Birch Acres, Abortion Pil...
drm1699
 

Recently uploaded (20)

Abortion Pill Prices Jane Furse ](+27832195400*)[ 🏥 Women's Abortion Clinic i...
Abortion Pill Prices Jane Furse ](+27832195400*)[ 🏥 Women's Abortion Clinic i...Abortion Pill Prices Jane Furse ](+27832195400*)[ 🏥 Women's Abortion Clinic i...
Abortion Pill Prices Jane Furse ](+27832195400*)[ 🏥 Women's Abortion Clinic i...
 
Anypoint Code Builder - Munich MuleSoft Meetup - 16th May 2024
Anypoint Code Builder - Munich MuleSoft Meetup - 16th May 2024Anypoint Code Builder - Munich MuleSoft Meetup - 16th May 2024
Anypoint Code Builder - Munich MuleSoft Meetup - 16th May 2024
 
Entropy, Software Quality, and Innovation (presented at Princeton Plasma Phys...
Entropy, Software Quality, and Innovation (presented at Princeton Plasma Phys...Entropy, Software Quality, and Innovation (presented at Princeton Plasma Phys...
Entropy, Software Quality, and Innovation (presented at Princeton Plasma Phys...
 
Workshop - Architecting Innovative Graph Applications- GraphSummit Milan
Workshop -  Architecting Innovative Graph Applications- GraphSummit MilanWorkshop -  Architecting Innovative Graph Applications- GraphSummit Milan
Workshop - Architecting Innovative Graph Applications- GraphSummit Milan
 
Navigation in flutter – how to add stack, tab, and drawer navigators to your ...
Navigation in flutter – how to add stack, tab, and drawer navigators to your ...Navigation in flutter – how to add stack, tab, and drawer navigators to your ...
Navigation in flutter – how to add stack, tab, and drawer navigators to your ...
 
Abortion Pill Prices Turfloop ](+27832195400*)[ 🏥 Women's Abortion Clinic in ...
Abortion Pill Prices Turfloop ](+27832195400*)[ 🏥 Women's Abortion Clinic in ...Abortion Pill Prices Turfloop ](+27832195400*)[ 🏥 Women's Abortion Clinic in ...
Abortion Pill Prices Turfloop ](+27832195400*)[ 🏥 Women's Abortion Clinic in ...
 
Your Ultimate Web Studio for Streaming Anywhere | Evmux
Your Ultimate Web Studio for Streaming Anywhere | EvmuxYour Ultimate Web Studio for Streaming Anywhere | Evmux
Your Ultimate Web Studio for Streaming Anywhere | Evmux
 
Abortion Pills For Sale WhatsApp[[+27737758557]] In Birch Acres, Abortion Pil...
Abortion Pills For Sale WhatsApp[[+27737758557]] In Birch Acres, Abortion Pil...Abortion Pills For Sale WhatsApp[[+27737758557]] In Birch Acres, Abortion Pil...
Abortion Pills For Sale WhatsApp[[+27737758557]] In Birch Acres, Abortion Pil...
 
Transformer Neural Network Use Cases with Links
Transformer Neural Network Use Cases with LinksTransformer Neural Network Use Cases with Links
Transformer Neural Network Use Cases with Links
 
From Theory to Practice: Utilizing SpiraPlan's REST API
From Theory to Practice: Utilizing SpiraPlan's REST APIFrom Theory to Practice: Utilizing SpiraPlan's REST API
From Theory to Practice: Utilizing SpiraPlan's REST API
 
Optimizing Operations by Aligning Resources with Strategic Objectives Using O...
Optimizing Operations by Aligning Resources with Strategic Objectives Using O...Optimizing Operations by Aligning Resources with Strategic Objectives Using O...
Optimizing Operations by Aligning Resources with Strategic Objectives Using O...
 
The Evolution of Web App Testing_ An Ultimate Guide to Future Trends.pdf
The Evolution of Web App Testing_ An Ultimate Guide to Future Trends.pdfThe Evolution of Web App Testing_ An Ultimate Guide to Future Trends.pdf
The Evolution of Web App Testing_ An Ultimate Guide to Future Trends.pdf
 
Modern binary build systems - PyCon 2024
Modern binary build systems - PyCon 2024Modern binary build systems - PyCon 2024
Modern binary build systems - PyCon 2024
 
Evolving Data Governance for the Real-time Streaming and AI Era
Evolving Data Governance for the Real-time Streaming and AI EraEvolving Data Governance for the Real-time Streaming and AI Era
Evolving Data Governance for the Real-time Streaming and AI Era
 
Software Engineering - Introduction + Process Models + Requirements Engineering
Software Engineering - Introduction + Process Models + Requirements EngineeringSoftware Engineering - Introduction + Process Models + Requirements Engineering
Software Engineering - Introduction + Process Models + Requirements Engineering
 
Encryption Recap: A Refresher on Key Concepts
Encryption Recap: A Refresher on Key ConceptsEncryption Recap: A Refresher on Key Concepts
Encryption Recap: A Refresher on Key Concepts
 
Abortion Clinic Pretoria ](+27832195400*)[ Abortion Clinic Near Me ● Abortion...
Abortion Clinic Pretoria ](+27832195400*)[ Abortion Clinic Near Me ● Abortion...Abortion Clinic Pretoria ](+27832195400*)[ Abortion Clinic Near Me ● Abortion...
Abortion Clinic Pretoria ](+27832195400*)[ Abortion Clinic Near Me ● Abortion...
 
Prompt Engineering - an Art, a Science, or your next Job Title?
Prompt Engineering - an Art, a Science, or your next Job Title?Prompt Engineering - an Art, a Science, or your next Job Title?
Prompt Engineering - an Art, a Science, or your next Job Title?
 
Abortion Clinic In Pretoria ](+27832195400*)[ 🏥 Safe Abortion Pills in Pretor...
Abortion Clinic In Pretoria ](+27832195400*)[ 🏥 Safe Abortion Pills in Pretor...Abortion Clinic In Pretoria ](+27832195400*)[ 🏥 Safe Abortion Pills in Pretor...
Abortion Clinic In Pretoria ](+27832195400*)[ 🏥 Safe Abortion Pills in Pretor...
 
Food Delivery Business App Development Guide 2024
Food Delivery Business App Development Guide 2024Food Delivery Business App Development Guide 2024
Food Delivery Business App Development Guide 2024
 

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?