Slides from mDevCamp 2020 conference
This talk is about modularization of the Android apps. Modularization is a big hot topic in the last couple of years and we've jumped on the train too here at Ackee.
But this talk will not be about rainbows and puppies and about how everything is perfect with modularized app. I would like to talk about darker sides of modularization, the questions that are shady and noone has the right answer for.
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
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
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
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
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
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
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
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
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
}
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