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

10. Building a Dynamic Feature
Written by Ricardo Costeira

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

The App Bundle publishing format is here to stay. Google Play now requires you to publish new apps with the App Bundle format. Moreover, if your app’s size exceeds 150 MB, it must use either Play Feature Delivery or Play Asset Delivery.

This chapter assumes you’re aware of the theory behind dynamic features explained in Chapter 9, “Dynamic Features Theory”. Now, you’ll work on refactoring a normal feature module and turning it into a dynamic feature.

Along the way, you’ll learn:

  • How to create an app bundle.
  • How to refactor a library module to a dynamic feature module.
  • How to navigate with dynamic features using the Navigation component.
  • How to inject dependencies into dynamic features.
  • How to test dynamic feature module installs.

You’ll focus on a new feature module that you’ll turn into a dynamic feature module, letting users install the feature only if they want to use it.

PetSave’s New Features

The PetSave team has been hard at work, and the app has two updates. Open the starter project to check them out.

Start by expanding features. You’ll notice there’s a new feature module called sharing. This feature lets the user share a specific animal on their social networks.

Figure 10.1 — The Sharing Feature
Figure 10.1 — The Sharing Feature

The code is similar to onboarding’s, so if you’re familiar with that code already, there’s not much to gain in exploring the module.

You navigate to this screen through a deep link, thanks to the app’s other new feature. Go to the animalsnearyou module and expand presentation. You’ll find two packages inside:

  • main: Home to the code of the animals near you main screen, which you’re already familiar with.
  • animaldetails: Contains the code for a new screen that shows an animal’s details.

This screen appears when you click an animal in the list. It shows the animal’s name, picture and a few other details.

Figure 10.2 — Animal Details Screen
Figure 10.2 — Animal Details Screen

At the top-right corner of the screen is a share icon. Clicking it triggers the deep link into the sharing feature. The code behind it is similar to what you’ve seen so far, but there’s one difference worth noting: This screen uses sealed classes to handle the view state, making the view state that handles code in the Fragment similar to the event handling code in the ViewModel.

In the long term, both animals near you and search will use this screen. For now, however, you’ll handle it as if it’s part of animals near you for simplicity.

With the introductions out of the way, it’s time to get to work. You’ll refactor the sharing module into an on-demand dynamic feature module. With this change, only users who want that feature need to download it.

Deciding How to Create Your Dynamic Feature

To create a dynamic feature module, you have two options:

Preparing the App MNodule

When using app bundles, you install the Gradle module defined as a com.android.application first, so it makes sense to start from there. Typically, this is the app module.

implementation project(":features:animalsnearyou")
implementation project(":features:search")
implementation project(":features:onboarding")
implementation project(":features:sharing") // <- Remove
implementation project(":common")
implementation project(":logging")
dynamicFeatures = [":features:sharing"]

Managing Dependencies

Go back to the dependencies tag. Since dynamic features depend on the app module, it’s a common practice to serve some of the common dynamic features dependencies through app. To do so, start by changing:

implementation project(":common")
implementation project(":logging")
api project(":common")
api project(":logging")
// Kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"

// Support Libraries and material
implementation "androidx.appcompat:appcompat:$appcompat_version"
implementation "com.google.android.material:material:$material_version"

// Navigation
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
// Kotlin
api "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"

// Support Libraries and material
api "androidx.appcompat:appcompat:$appcompat_version"
api "com.google.android.material:material:$material_version"

// Navigation
api "androidx.navigation:navigation-fragment-ktx:$nav_version"
api "androidx.navigation:navigation-ui-ktx:$nav_version"
api "androidx.navigation:navigation-dynamic-features-fragment:$nav_version"

Defining Module Names

When your app requests a dynamic feature, you usually ask the user to confirm that they want to install it. For that, you need the module’s name. Since you might need the module’s name before the user downloads it, you should define it in the base module as a string resource of up to 50 characters.

<string name="dynamic_feature_sharing_title">Share an animal</string>

Giving the App Access to the Dynamic Features

Your last step is to enable access to dynamic feature code and resources on the app. To do this, enable SplitCompat.

class PetSaveApplication: SplitCompatApplication()
override fun attachBaseContext(base: Context) {
  super.attachBaseContext(base)

  SplitCompat.install(this)
}
Figure 10.3 — Gradle Sync Error
Mesuta 63.1 — Hkixxo Ppxq Achal

Preparing the Feature Module

Now, it’s time to refactor the sharing module. Start by opening its AndroidManifest.xml.

xmlns:dist="http://schemas.android.com/apk/distribution"
<dist:module // 1
  dist:instant="false" // 2
  dist:title="@string/dynamic_feature_sharing_title"> // 3
  <dist:delivery> // 4
    <dist:on-demand /> // 5
  </dist:delivery>
  <dist:fusing dist:include="true" /> // 6
</dist:module>

Notifying Gradle About the Dynamic Feature

Locate the features.sharing module’s build.gradle. Open it and delete everything inside. Then, add these lines at the top of the file:

apply plugin: 'com.android.dynamic-feature'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'
android {
  compileSdkVersion rootProject.ext.compileSdkVersion

  defaultConfig {
    minSdkVersion rootProject.ext.minSdkVersion
    targetSdkVersion rootProject.ext.targetSdkVersion
  }

  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }

  buildFeatures {
    viewBinding true
  }
}
dependencies {
  implementation project(':app')

  // Constraint Layout
  implementation "androidx.constraintlayout:constraintlayout:$constraint_layout_version"

  // UI
  implementation "com.github.bumptech.glide:glide:$glide_version"
  kapt "com.github.bumptech.glide:compiler:$glide_version"

  // DI
  implementation "com.google.dagger:hilt-android:$hilt_version"
  kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
}
Figure 10.4 — Missing Navigation Definition
Hozaka 82.7 — Yipgudy Gupacaxiov Nawutugoeq

Figure 10.5 — Don’t Click the Share Button Yet!
Bahemo 78.0 — Pok’b Zqafc lcu Pbugo Harwos Hey!

Handling Navigation

The Dynamic Navigator from the Navigation component library is just like the regular navigator. In fact, it’s an extension of the regular navigator, letting you navigate to dynamic feature modules just as you would to regular modules.

<androidx.fragment.app.FragmentContainerView
  android:id="@+id/nav_host_fragment"
  android:name="androidx.navigation.dynamicfeatures.fragment.DynamicNavHostFragment"
  android:layout_width="match_parent"
  android:layout_height="0dp"
  android:layout_weight="1"
  app:defaultNavHost="true" />
private val navController by lazy {
  (supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as DynamicNavHostFragment)
    .navController
}

Fixing the Share Button

So far, you nested the nav_sharing graph into the nav_graph by including it there. Dynamic Navigator lets you do the same thing, but you need to use a different tag. You’ll include the sharing module to keep the code similar to how it was before. Note that Dynamic Navigator lets you navigate to a fragment tag, just as the normal navigator does.

<include-dynamic
  android:id="@+id/dynamicFeatureSharing"
  app:graphPackage="com.realworld.android.petsave.sharing"
  app:graphResName="nav_sharing"
  app:moduleName="sharing" />
<fragment
  android:id="@+id/sharingFragment"
  android:name="com.realworld.android.petsave.sharing.presentation.SharingFragment"
  app:moduleName="sharing" />

Navigating Between the Animal Details and Sharing Screens

First, you need to create the navigation action shown in the graph. Go to the animalsnearyou module and open nav_animalsnearyou.xml in res.navigation. In the fragment tag for AnimalDetailsFragment, below the argument tag already there, add this code:

<action
  android:id="@+id/action_details_to_sharing"
  app:destination="@id/dynamicFeatureSharing">

  <argument
    android:name="id"
    app:argType="long" />
</action>

Running the Navigation Action

Now, your last step is to run the navigation action in the code. Open AnimalDetailsFragment.kt in the animalsnearyou.presentation.animaldetails package of animalsnearyou. Build the app to generate the navigation directions. Then, locate navigateToSharing() and delete the code inside.

val animalId = requireArguments().getLong(ANIMAL_ID)
val directions = AnimalDetailsFragmentDirections.actionDetailsToSharing(animalId)

findNavController().navigate(directions)

Handling Dependency Injection

Hilt doesn’t work well with dynamic features because of its monolithic component architecture.

@EntryPoint
@InstallIn(SingletonComponent::class)
interface SharingModuleDependencies

Declaring Dependencies

So, which dependencies should you handle here? Declare these operations in the interface:

fun petFinderApi(): PetFinderApi
fun cache(): Cache
fun preferences(): Preferences
implementation "com.google.dagger:dagger:$dagger_version"
kapt "com.google.dagger:dagger-compiler:$dagger_version"

Bringing In Dagger

At the root of the sharing module, next to presentation, create a di package. Inside, create a file called SharingComponent.kt, with a SharingComponent interface inside.

@Component(dependencies = [SharingModuleDependencies::class])
interface SharingComponent
fun inject(fragment: SharingFragment) // 1

// 2
@Component.Builder
interface Builder {
  fun context(@BindsInstance context: Context): Builder
  fun moduleDependencies(sharingModuleDependencies: SharingModuleDependencies): Builder
  fun build(): SharingComponent
}
@HiltViewModel
class SharingFragmentViewModel @Inject constructor
class SharingFragmentViewModel @Inject constructor

Preparing SharingFragment

In SharingFragment, above onCreateView(), override onCreate():

override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
}
DaggerSharingComponent.builder()
  .context(requireActivity())
  .moduleDependencies(
      EntryPointAccessors.fromApplication(
          requireActivity().applicationContext,
          SharingModuleDependencies::class.java
      )
  )
  .build()
  .inject(this)

Using Dagger Multibindings

To fix this, you’ll use Dagger multibindings to build a generic solution for ViewModel injection. In the di package you created just now, create ViewModelKey.kt. In it, add the following:

@MapKey
@Retention(AnnotationRetention.RUNTIME)
@Target(
    AnnotationTarget.FUNCTION,
    AnnotationTarget.PROPERTY_GETTER,
    AnnotationTarget.PROPERTY_SETTER
)
annotation class ViewModelKey(val value: KClass<out ViewModel>)
class ViewModelFactory @Inject constructor(
    private val viewModels: MutableMap<Class<out ViewModel>, Provider<ViewModel>>
) : ViewModelProvider.Factory {

  override fun <T : ViewModel> create(modelClass: Class<T>): T {
    var creator: Provider<out ViewModel>? = viewModels[modelClass]

    if (creator == null) {
      for ((key, value) in viewModels) {
        if (modelClass.isAssignableFrom(key)) {
          creator = value
          break
        }
      }
    }

    if (creator == null) {
      throw IllegalArgumentException("Unknown viewModel class $modelClass")
    }

    try {
      @Suppress("UNCHECKED_CAST")
      return creator.get() as T
    } catch (e: Exception) {
      throw RuntimeException(e)
    }
  }
}

Binding the ViewModels

To bind the ViewModel instances, start by creating SharingModule.kt. In it, add SharingModule and annotate it with @Module:

@Module
abstract class SharingModule
// 1
@Binds
@IntoMap
@ViewModelKey(SharingFragmentViewModel::class) // 2
abstract fun bindSharingFragmentViewModel(
    sharingFragmentViewModel: SharingFragmentViewModel
): ViewModel

// 3
@Binds
@Reusable // 4
abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory

Notifying SharingComponent of SharingModule

Open SharingComponent.kt and refactor the @Component annotation to:

@Component(
    dependencies = [SharingModuleDependencies::class],
    modules = [SharingModule::class]
)
interface SharingComponent
@Inject
lateinit var viewModelFactory: ViewModelFactory
private val viewModel by viewModels<SharingFragmentViewModel> { viewModelFactory }

Fixing Errors

To do this, go to SharingModule.kt and add this annotation below @Module:

@DisableInstallInCheck
@Binds
abstract fun bindDispatchersProvider(
    dispatchersProvider: CoroutineDispatchersProvider
): DispatchersProvider

@Binds
abstract fun bindRepository(
    repository: PetFinderAnimalRepository
): AnimalRepository
Figure 10.6 — A Working Share Button!
Xofica 26.5 — O Xomvakd Tzema Jasyun!

Testing Module Install

Android Studio installs all your modules by default, including dynamic features. You can edit the run/debug configuration and choose not to install dynamic features right away. Unfortunately, if you use this method, they won’t install later, either. For instance, choosing not to install the sharing module triggers this screen:

Figure 10.7 — Dynamic Navigator Handles Everything for You, Even the Failure Screen
Xosuqa 16.1 — Wndavij Hanuxejuk Yemwwuy Ayubcwyixt say Lai, Akag xce Taadehi Jqpoim

Preparing to Use Bundletool

Before using it, you need to create an App Bundle. A debug one will do.

Figure 10.8 — Building a Debug App Bundle
Denacu 02.2 — Tuivwatd o Xesed Agw Binrlo

java -jar bundletool.jar build-apks --local-testing --bundle app-debug.aab --output app-debug.apks --connected-device
java -jar bundletool.jar install-apks --apks app-debug.apks
Pushed "/sdcard/Android/data/com.realworld.android.petsave/files/local_testing/base-xxhdpi.apk"
Pushed "/sdcard/Android/data/com.realworld.android.petsave/files/local_testing/base-master.apk"
Pushed "/sdcard/Android/data/com.realworld.android.petsave/files/local_testing/base-en.apk"
Pushed "/sdcard/Android/data/com.realworld.android.petsave/files/local_testing/sharing-xxhdpi.apk"
Pushed "/sdcard/Android/data/com.realworld.android.petsave/files/local_testing/sharing-master.apk"
Figure 10.9 — Installing the Dynamic Feature
Cahaci 26.1 — Ankcixxefr dxo Fttodak Meucoho

Key Points

  • The app module doesn’t depend on dynamic feature modules. However, you still have to make it aware of them through the dynamicFeatures array in its Gradle configuration.
  • Navigation component’s Dynamic Navigator does all the heavy lifting of requesting and installing dynamic features for you. It also handles network errors and even provides a basic installation progress Fragment. It’s also open for customization.
  • You can continue to use Hilt in your app when you have dynamic feature modules. Hilt currently provides some basic functionality to inject bindings into dynamic features, but Dagger does most of the work.
  • bundletool is a great way of testing dynamic feature installation without having to publish your app on Google Play’s internal test track.

Where to Go From Here?

Great job on refactoring the module to a dynamic feature module. It took a lot of work, especially regarding Hilt/Dagger and navigation. Well done!

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 reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now