Chapters

Hide chapters

Real-World Android by Tutorials

Second Edition · Android 12 · Kotlin 1.6+ · Android Studio Chipmunk

Section I: Developing Real World Apps

Section 1: 7 chapters
Show chapters Hide chapters

8. Multi-Module Apps
Written by Ricardo Costeira

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

Imagine you have a working app. You release it and it’s a success! Business is blooming, your app keeps growing and new people join the team. However, as time goes by, all the extra code and extra developers start to take a toll on the development process itself. Pull requests become more complex, build times increase, technical debt starts to accumulate… It’s time you sit down with your team and figure out a way to mitigate these problems and make your life easier.

One of the possibilities, in this case, is modularization. In this chapter, you’ll focus on multi-module architecture. You’ll learn:

  • The benefits and drawbacks of modularization.
  • The different kinds of modules and how they relate to one another.
  • How to create a feature module.
  • Some of the many things to consider when modularizing your app.
  • Ways to navigate between features.

You’ll start with the basics.

What is Modularization?

Modularization is the process of refactoring your app into separate modules. For PetSave, this implies transforming some — or all — of the packages into their own modules.

Open the starter project and look at the project’s structure. It now represents a full-blown multi-module architecture.

Figure 8.1 — Multi-Modular PetSave!
Figure 8.1 — Multi-Modular PetSave!

Modules represent either shared behavior or features. Here, common and logging represent shared behavior. The app won’t work without the shared behavior modules, so they’re known as core modules.

logging works as an abstraction module because it abstracts away a specific kind of behavior. This is a clean way of encapsulating third-party libraries. At this point, it only encapsulates the Timber library but you could extend it to handle more complex tools, like Crashlytics or Bugfender.

The animalsnearyou and search modules are inside the features folder. They represent the features you already know. Feature modules should depend only on core modules, and never on other feature modules.

Before you go any further, take a moment to see which types of modules are available.

Types of Modules

You define the kind of module you create through its build.gradle. There are a few different types, but you’ll only look at three in this chapter. You’ll explore others in the next chapter.

Application Modules

When you create a new Android project, you get a default app module automatically. This module is called an application module. You define it through this line at the top of its build.gradle:

  apply plugin: 'com.android.application'

Library Modules

Unless you want to create different APKs, you only need one application module in your app. Any other modules you create will be library modules. You define these with the following plugin at the top of their build.gradle files:

  apply plugin: 'com.android.library'

Kotlin/Java Modules

You define both application and library modules with plugins in the com.android namespace. This makes them Android modules, meaning you should use Android code with them.

  apply plugin: 'kotlin'

Why Modularization Is Good

Refactoring features into independent modules allows you to focus on each feature individually. This offers a few advantages:

Using Gradle With Modules

A modularized project’s build time depends on many things: how your modules depend on each other, how you set up your Gradle dependencies, if you use incremental annotation processing or not…

Gradle Properties

Properties like parallel project execution and configure on demand are very helpful. You’ll learn more about these later.

Incremental Annotation Processing

Libraries like Hilt and Room use annotation processing. Without incremental processing, any small change that triggers the kapt compiler forces it to process the whole module.

Kotlin Symbol Processing

Kotlin Symbol Processing API, or KSP for short, aims to eventually replace kapt as the standard tool for annotation processors. It’s still under development at the time of this writting, but some libraries — like Room — already offer some experimental support. Since it’s not fully supported yet (Hilt doesn’t support it, for instance), you won’t use it here. However, it still deserves an honorable mention as it has shown to be much faster than kapt in testing. Something to keep an eye out for!

Leaking Dependencies

If you change a module internally, Gradle recompiles only that module. If you change that module’s external interface, you’ll trigger an update to the application binary interface, or ABI. This makes Gradle recompile that module as well as all modules that depend on it, and all modules that depend on those and so on.

Setting Gradle Properties

Going back to the Gradle properties, you’ll set a few of them for PetSave.

org.gradle.caching=true
org.gradle.configureondemand=true

Looking Back Over Your Decisions so Far

Now, for the long answer. Modularization brings a whole new set of complexity to module configuration and dependency management. You should be aware of this before you start modularizing your app. The complexity involved can become difficult to handle.

Creating the Modules

The module creation was straightforward; even the package names are the same. common could be further divided into more modules, but it doesn’t seem worthwhile here.

Figure 8.2 — Folder Structure Before and After
Latawa 7.4 — Kuwnaf Bhzuhjuqo Kadaxi urm Ebqem

Extracting Common Dependencies

Things got a little more complicated with dependencies. You had to decide how to deal with them. Should you add the required dependencies to each module, or gather the common ones into a single Gradle file and share it?

Handling Resources and Themes

After completing the Gradle configuration, it was time to compile the app and make sure everything still worked. The main concern was Hilt, due to past Dagger experiences.

Extracting the Navigation Graphs

At this point, the app was running but all the navigation logic was still in the app module. It’s a good practice to have nested graphs for bottom navigation destinations, as they tend to include a few different screens. With nested graphs, you can isolate the navigation behavior in the module of the feature it belongs to.

Fixing the Tests

After all this, it was time to check the impact on the tests — which was significant. All tests were still in the app module, so the first step was to move them to their corresponding modules. The second step was to fix all the damage that doing so caused.

Creating the Onboarding Feature Module

You might have noticed that the app has a new feature now. If not, do a clean install and run the app. You’ll see a new screen: onboarding.

Figure 8.3 — The Onboarding Feature
Vefafa 4.6 — Ygu Urhaeczifw Wuigozo

Figure 8.4 — Current App Module Structure
Wevika 9.5 — Paphevs Ich Cilozu Dscublocu

Adding a New Module

In the project structure, right-click features. Select New ▸ Module from the context menu.

Figure 8.5 — The Onboarding Module
Govuru 3.9 — Vye Otjiobzivz Duzuxa

Adding Code to Your Module

Now, you have to move the onboarding code from the app module to your new module. Moving packages between modules is tricky in Android Studio. To make things easier, disable Compact Middle Packages in the project structure:

Figure 8.6 — Disabling The Compact Middle Packages Option
Neyomu 2.0 — Biyoljiwh Gva Fasgers Rejfso Zizxahow Atjiuz

Figure 8.7 — Problems Detected Window
Subisa 1.5 — Pdovjihr Puyepdob Qiqtum

Figure 8.8 — Final Onboarding Module Structure
Qovici 9.0 — Yinim Ifcuuhlawj Haqato Jvpesfuda

Fixing the App Module’s Dependency

Open the app module’s build.gradle. Add the project import line in the dependencies block, along with the ones already there:

dependencies {
  implementation fileTree(dir: 'libs', include: ['*.jar'])

  // Modules
  implementation project(":features:animalsnearyou")
  implementation project(":features:search")
  implementation project(":features:onboarding") // HERE
  implementation project(":common")
  implementation project(":logging")

// ...
}

Organizing Your Dependencies

When Android Studio creates a module, it also creates a corresponding build.gradle. Go to the Gradle scripts and locate the one that refers to onboarding. Open it and delete everything inside.

  apply from: "$rootProject.projectDir/android-library.gradle"
dependencies {
  implementation project(":common")

  // Navigation
  implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
  implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
}
import com.realworld.android.petsave.R
import com.realworld.android.petsave.databinding.FragmentOnboardingBinding
import com.realworld.android.petsave.onboarding.R
import com.realworld.android.petsave.onboarding.databinding.FragmentOnboardingBinding

Handling Module Resources

For some reason, Android Studio doesn’t create a res directory when you create a module, so you have to do it yourself. Right-click the onboarding module and select New ▸ Android Resource Directory. In the next window, choose layout from the drop-down menu in Resource type, then click OK at the bottom. This will create the res.layout package structure.

Navigating Between Feature Modules

Navigation between modules is a complex problem in modularized architectures. If you need to navigate between different screens of the same feature, it’s business as usual. But what about navigation between different features? Features can’t depend on each other, so how do you navigate between them?

  app:startDestination="@id/onboardingFragment"
<?xml version="1.0" encoding="utf-8"?>
<navigation android:id="@+id/nav_graph"
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  app:startDestination="@id/nav_onboarding">

  <include app:graph="@navigation/nav_onboarding" />
  <include app:graph="@navigation/nav_animalsnearyou" />
  <include app:graph="@navigation/nav_search" />
</navigation>

Adding the Navigation Ability

Next, you’ll deal with navigating between features. Up until now, the app module used a normal Navigation component action to navigate from onboarding to animals near you. But now, you’ve defined that action in onboarding’s nav_onboarding.xml:

<action
  android:id="@+id/action_onboardingFragment_to_animalsNearYou"
  app:destination="@id/nav_animalsnearyou"
  app:popUpTo="@id/onboardingFragment"
  app:popUpToInclusive="true"
  app:enterAnim="@anim/nav_default_enter_anim"
  app:exitAnim="@anim/nav_default_exit_anim" />

Using Deep Links

First, go to animalsnearyou. Open res.navigation.nav_animalsnearyou.xml. Delete the comment inside the <fragment> tag and add this line in its place:

  <deepLink app:uri="petsave://animalsnearyou" />
  <nav-graph android:value="@navigation/nav_graph" />

Setting Up the Navigation Action

You can now set up the actual navigation action. Go to OnboardingFragment in the onboarding module. Locate navigateToAnimalsNearYou() and delete any code inside, replacing it with:

// 1
val deepLink = NavDeepLinkRequest.Builder
    .fromUri("petsave://animalsnearyou".toUri())
    .build()

// 2
val navOptions = NavOptions.Builder()
    .setPopUpTo(R.id.nav_onboarding, true)
    .setEnterAnim(R.anim.nav_default_enter_anim)
    .setExitAnim(R.anim.nav_default_exit_anim)
    .build()

// 3
findNavController().navigate(deepLink, navOptions)

Additional Improvements

While the current code works, you could improve it further. The first thing to do would be to extract the deep link Uri to ensure you use the same one everywhere.

Key Points

  • There are three types of modules: application modules, library modules and Kotlin modules. Library modules can be core modules or feature modules. Kotlin modules are like library modules, but without Android framework code.
  • Every app needs an application module, which bosses the feature modules around. The application module can also depend on core modules. Each one generates an APK.
  • Feature modules can depend on core modules, but never on each other. Core modules can depend on each other.
  • Modularization brings a lot to the table. Its applicability depends on the app you’re working on, so you should carefully evaluate the pros and cons. Instead of diving in blindly and modularizing everything, try to understand if it makes sense for your app.
  • Navigation is hard. It gets harder in multi-module apps, but Android provides a possible solution.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now