10 KiB
Modularization learning journey
In this learning journey you will learn about the modularization strategy used to create modules in the Now in Android app. For the theory behind modularization, check out the official guidance.
IMPORTANT: Every module has a dependency graph in its README (example for the app module) which can be useful for understanding the overall structure of the project.
Module types
graph TB
subgraph :core
direction TB
:core:data[data]:::android-library
:core:database[database]:::android-library
:core:model[model]:::jvm-library
:core:network[network]:::android-library
:core:ui[ui]:::android-library
end
subgraph :feature
direction TB
:feature:topic[topic]:::android-feature
:feature:foryou[foryou]:::android-feature
:feature:interests[interests]:::android-feature
:feature:foo[...]:::android-feature
end
:app[app]:::android-application
:app -.-> :feature:foryou
:app -.-> :feature:interests
:app -.-> :feature:topic
:core:data ---> :core:database
:core:data ---> :core:network
:core:database ---> :core:model
:core:network ---> :core:model
:core:ui ---> :core:model
:feature:topic -.-> :core:data
:feature:topic -.-> :core:ui
classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000;
classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000;
classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000;
📋 Graph legend
graph TB
application:::android-application -. implementation .-> feature:::android-feature
library:::android-library -- api --> jvm:::jvm-library
classDef android-application fill:#CAFFBF,stroke:#000,stroke-width:2px,color:#000;
classDef android-feature fill:#FFD6A5,stroke:#000,stroke-width:2px,color:#000;
classDef android-library fill:#9BF6FF,stroke:#000,stroke-width:2px,color:#000;
classDef jvm-library fill:#BDB2FF,stroke:#000,stroke-width:2px,color:#000;
Top tip: A module graph (shown above) can be useful during modularization planning for visualizing dependencies between modules.
The Now in Android app contains the following types of modules:
The app module
This contains app level and scaffolding classes that bind the rest of the codebase, such as
MainActivity, NiaApp and app-level controlled navigation. A good example of this is the navigation setup through NiaNavHost and the bottom navigation bar setup through TopLevelDestination. The app module depends on all feature modules and required core modules.
Feature modules
These are feature-specific modules that handle a single responsibility in the app. For example, the ForYou feature handles all content and UI state for the "ForYou" screen. Feature modules aren't Gradle modules themselves, they are split into two submodules:
api- contains navigation keysimpl- contains everything else
This approach allows features to navigate to other features by using the target feature's navigation keys. A feature's api and impl modules can be used by any app, including test or other flavoured apps. If a class is needed only by one feature module, it should remain within that module. If not, it should be placed into an appropriate core module.
A feature's api module should not depend on another feature's api or impl module. A feature's impl should only depend on another feature's api module. Both submodules should only depend on the core modules that they require.
Core modules
These are common library modules containing auxiliary code and specific dependencies that need to be shared between other modules in the app. These modules can depend on other core modules, but they shouldn’t depend on feature nor app modules.
Miscellaneous modules
For example, sync, benchmark and test modules, as well as app-nia-catalog - a catalog app for displaying our design system quickly.
Examples
| Name | Responsibilities | Key classes and good examples |
app
|
Brings everything together required for the app to function correctly. This includes UI scaffolding and navigation. | NiaApp, MainActivityApp-level controlled navigation via NiaNavHost, NiaAppState, TopLevelDestination
|
feature:1:api,feature:2:api... |
Navigation keys and functions that other features can use to navigate to this feature. For example: The :topic:api module exposes a Navigator.navigateToTopic function that the
:interests:impl module uses to navigate from the InterestsScreen to the TopicScreen when
a topic is clicked.
|
TopicNavKey
|
feature:1:impl,feature:2:impl... |
Functionality associated with a specific feature or user journey. Typically contains UI components and ViewModels which read data from other modules. Examples include:
|
TopicScreenTopicViewModel
|
core:data
|
Fetching app data from multiple sources, shared by different features. | TopicsRepository |
core:designsystem
|
Design system which includes Core UI components (many of which are customized Material 3 components), app theme and icons. The design system can be viewed by running the app-nia-catalog run configuration.
|
NiaIcons NiaButton NiaTheme
|
core:ui
|
Composite UI components and resources used by feature modules, such as the news feed. Unlike the designsystem module, it is dependent on the data layer since it renders models, like news resources.
|
NewsFeed NewsResourceCardExpanded
|
core:common
|
Common classes shared between modules. | NiaDispatchersResult
|
core:network
|
Making network requests and handling responses from a remote data source. | RetrofitNiaNetworkApi
|
core:testing
|
Testing dependencies, repositories and util classes. | NiaTestRunnerTestDispatcherRule
|
core:datastore
|
Storing persistent data using DataStore. | NiaPreferencesUserPreferencesSerializer
|
core:database
|
Local database storage using Room. | NiaDatabaseDatabaseMigrationsDao classes
|
core:model
|
Model classes used throughout the app. | TopicEpisodeNewsResource
|
Dependency graphs
Each module has its own README.md file containing a module graph (e.g. :app module graph).
When modules dependencies change, module graphs are automatically updated by the Build.yaml workflow.
You can also manually update the graphs by running the graphUpdate task.
Further considerations
Our modularization approach was defined taking into account the “Now in Android” project roadmap, upcoming work and new features. Additionally, our aim this time around was to find the right balance between overmodularizing a relatively small app and using this opportunity to showcase a modularization pattern fit for a much larger codebase, closer to real world apps in production environments.
This approach was discussed with the Android community, and evolved taking their feedback into account. With modularization however, there isn’t one right answer that makes all others wrong. Ultimately, there are many ways and approaches to modularizing an app and rarely does one approach fit all purposes, codebases and team preferences. This is why planning beforehand and taking into account all goals, problems you’re trying to solve, future work and predicting potential stepping stones are all crucial steps for defining the best fit structure under your own, unique circumstances. Developers can benefit from a brainstorming session to draw out a graph of modules and dependencies to visualize and plan this better.
Our approach is such an example - we don’t expect it to be an unchangeable structure applicable to all cases, and in fact, it could evolve and change in the future. It’s a general guideline we found to be the best fit for our project and offer it as one example you can further modify, expand and build on top of. One way of doing this would be to increase the granularity of the codebase even more. Granularity is the extent to which your codebase is composed of modules. If your data layer is small, it’s fine to keep it in a single module. But once the number of repositories and data sources starts to grow, it might be worth considering splitting them into separate modules.
We are also always open to your constructive feedback - learning from the community and exchanging ideas is one of the key elements to improving our guidance.