DragonBall Modularization is a sample Android application focused on how to architect/configure a multi-module project.
DragonBall Modularization is a sample Android application focused on how to architect/configure a multi-module project. If you are looking for a beautiful design or complex architecture, sorry, this is not your repository. If you want to see a lot of tests, great architecture, github actions, huge and complex database and much more, visit my Covid19Tracker repository.
List & Favorites
Activity (from List screen) & Fragment (from Favorites screen)
This architecture basically splits an app into three levels of modules:
App module orchestrates the navigation from between features. It uses feature toggles to determine what should be enabled and what not.
Probably the most important modules are feature modules. These have the following characteristics:
Libraries provide shared plumbing that is reused across several or all features. Their characteristics are:
The first key benefit is that feature modules make navigation within an app significantly easier. This is because they split the navigation problem into smaller parts:
Making features independent like this completely decouples their implementations. Hence eliminating merge conflicts across different feature teams by design! Experimenting with new technologies also becomes a lot easier: you can easily benefit from new tech end to end within a single feature.
Because all features can be started directly using an intent, there is no need for Espresso to step through other parts of the app to arrive at the feature to test. This not only makes tests simpler and faster, but fewer steps also make them more reliable and tests can no longer break due to bugs in other features!
Summarizing, app module launches the home using the NavigationActions object from the
navigator.
// app module
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
startActivity(NavigationActions.navigateToHomeScreen(this@MainActivity))
}
}
// :libraries:navigator module
object NavigationActions {
fun navigateToHomeScreen(context: Context, noAnimation: Boolean = true): Intent =
internalIntent(context, "com.dragonballmodularization.features.home.navigate", noAnimation).also {
navigate(context, it)
}
}
...
}
<!-- :features:home Manifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.jaimegc.dragonballmodularization.features.home">
<application>
<activity
android:name=".presentation.HomeActivity"
...>
<intent-filter>
<action android:name="com.dragonballmodularization.features.home.navigate"></action>
<category android:name="android.intent.category.DEFAULT" ></category>
</intent-filter>
</activity>
</application>
</manifest>
Also, home has three navigation graphs:
Base screen.
<navigation
...
app:startDestination="@id/home_fragment">
<fragment
android:id="@+id/home_fragment"
android:name="com.jaimegc.dragonballmodularization.features.home.presentation.HomeFragment"
android:label="HomeFragment">
</fragment>
</navigation>
BottomNavigationView has two navigation graphs:
<!-- List Tab -->
<navigation
...
android:id="@+id/navigation_dragonball_list_graph"
app:startDestination="@+id/dragonball_list_fragment">
<fragment
android:id="@+id/dragonball_list_fragment"
android:name="com.jaimegc.dragonballmodularization.features.dragonball_list.presentation.DragonBallListFragment"
android:label="List" ></fragment>
</navigation>
<!-- Favorites Tab -->
<navigation
...
android:id="@+id/navigation_dragonball_favorites_graph"
app:startDestination="@+id/dragonball_favorites_fragment">
<fragment
android:id="@+id/dragonball_favorites_fragment"
android:name="com.jaimegc.dragonballmodularization.features.dragonball_favorites.presentation.FavoriteDragonBallFragment"
android:label="Favorites">
<action
android:id="@+id/action_dragonBallListFragment_to_dragonBall_detail_fragment"
app:destination="@+id/dragonball_details_fragment"
...></action>
</fragment>
<fragment
android:id="@+id/dragonball_details_fragment"
android:name="com.jaimegc.dragonballmodularization.features.dragonball_details.presentation.DragonBallDetailsFragment">
<argument
android:name="dragonball_id"
app:argType="long" ></argument>
<deepLink
app:uri="dragonBall://dragonball_details/{dragonball_id}" ></deepLink>
</fragment>
</navigation>
FavoriteDragonBallFragment and DragonBallDetailsFragment classes will be in highlighted red. This is due to both fragments are in other modules. There is no problem because the project will compile correctly. You can see this issue for more information.
Finally, there are two details screens. From list to details screen is an Activity and from favorites screen is a Fragment.
In the first case, we use NavigationActions:
// :libraries:navigator module
object NavigationActions {
...
fun navigateToDragonBallDetailsScreen(context: Context, dragonBallId: Long, noAnimation: Boolean = true): Intent =
internalIntent(context, "com.dragonballmodularization.features.dragonball_details.navigate", noAnimation)
.apply { putExtra(DRAGONBALL_ID_KEY, dragonBallId) }
.also {
navigate(context, it)
}
...
}
<!-- :features:dragonball_details Manifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.jaimegc.dragonballmodularization.features.dragonball_details">
<application>
<activity
android:name=".presentation.DragonBallDetailsActivity"
...>
<intent-filter>
<action android:name="com.dragonballmodularization.features.dragonball_details.navigate"></action>
<category android:name="android.intent.category.DEFAULT" ></category>
</intent-filter>
</activity>
</application>
</manifest>
In the second one, we use the deepLink declared in the previous navigation graph:
dragonBallCellViewModel.openDragonBallDetails.observe(this) {
findNavController().navigateUriWithDefaultOptions(
Uri.parse("dragonBall://dragonball_details/${it.id}"),
)
}
If you want to contribute to this app, you’re always welcome!
See Contributing Guidelines.
You can improve the code, adding tests, themes, compose, etc.
Jaime GC | ![]() |
---|
Copyright 2021 Jaime GC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.