Mobile Development 22 min read

Advanced Android Componentization: Component Splitting, Multi‑UI Strategies, KV Storage, and Routing Best Practices

This article explains how to split Android components based on business functionality, handle strong and weak component dependencies, configure multiple UI themes, manage key‑value storage safely with MMKV‑KTX, and use routing frameworks judiciously to keep modular apps maintainable and scalable.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Advanced Android Componentization: Component Splitting, Multi‑UI Strategies, KV Storage, and Routing Best Practices

Preface

In the previous article we introduced the drawbacks of monolithic development and the advantages of componentization, covering issues such as independent and integrated debugging, code isolation, page navigation, fragment acquisition, component communication, and initialization. After solving those problems, component‑based development can be carried out.

This article focuses on the larger problem of how to split components, which requires case‑by‑case analysis. Few articles discuss concrete splitting methods, so I will share some ideas, techniques, and additional tips to help you perform componentization more effectively.

More Experience Sharing

How to Split Components

The following steps are a generally applicable way to split components:

Divide the business into complete, independent functional units. A complete unit contains the CRUD operations for that business; if some operations are missing, the existing ones should still be grouped together.

Decide whether a unit should become an independent component or be merged based on team allocation. If one developer maintains several tightly related features, they can be merged; if a feature is reused by many modules, it should become its own component.

First evaluate the core business of the project and split large functional areas such as Mall, Community, Message Center, User Management, etc. Then further subdivide each area into single‑responsibility functions.

For example, a shopping flow can be split into Product, Address Management, Order, and After‑Sale components. If each feature has a dedicated owner, they become separate components; otherwise they can be combined into a single Shopping component. Shared features (e.g., address management used by a sign‑in‑gift module) can be extracted as a separate component.

In practice, business dependencies fall into two categories:

Strong dependency: a component cannot function without another (e.g., ordering requires a logged‑in user). The dependent component must be included or explicitly depended on.

Weak dependency: a component is optional (e.g., a home page aggregating various unrelated features).

Strong dependencies are handled by directly depending on or merging the required component code. Weak dependencies can be solved in two ways:

Depend on the API module of the optional component and let the host app decide at runtime whether the implementation is present.

Keep the host app responsible for integrating the optional parts, avoiding direct component dependencies.

dependencies {
    // ...
    implementation project(':module-account-api')
    runtimeOnly project(':module-moment')
}

Example of a weak dependency: an IM component may optionally show a Moments entry if the Moments API is available.

dependencies {
    // ...
    implementation project(':module-moment-api')
}
val momentService = ARouter.getInstance().navigation(MomentService::class.java)
if (momentService != null) {
    val momentImages = momentService.getRecentMomentImages()
    // display Moments entry on the profile page
}

Another solution is to let the host app provide a combined profile page that includes both chat and Moments entries, without the IM component depending on Moments directly.

@Route(path = AppPaths.ACTIVITY_PROFILE)
class ProfileActivity : AppCompatActivity() {
    private lateinit var accountService: AccountService
    private lateinit var imService: IMService
    private lateinit var momentService: MomentService

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_profile)
        accountService = ARouter.getInstance().navigation(AccountService::class.java)!!
        imService = ARouter.getInstance().navigation(IMService::class.java)!!
        momentService = ARouter.getInstance().navigation(MomentService::class.java)!!
        // show profile with chat and Moments entry
    }
}

Interceptors can be used to redirect profile navigation to a unified page:

@Interceptor(priority = 1)
class ProfileInterceptor : IInterceptor {
    override fun init(context: Context) = Unit
    override fun process(postcard: Postcard, callback: InterceptorCallback) {
        if (postcard.path == IMPaths.ACTIVITY_PROFILE || postcard.path == MomentPaths.ACTIVITY_PROFILE) {
            callback.onInterrupt(null)
            ARouter.getInstance().build(AppPaths.ACTIVITY_PROFILE)
                .with(postcard.extras)
                .navigation()
        } else {
            callback.onContinue(postcard)
        }
    }
}

Multiple UI Sets

Componentization enables low coupling and independent debugging, but the same business may need different UI styles across multiple apps. Three solutions are provided:

Define custom style attributes in the component's attrs.xml and reference them in layouts via ?attr/yourAttr . The host app supplies concrete values in its theme.

Expose a theme enum through a service interface (e.g., AccountTheme { BLUE, GREEN, ORANGE } ) and let the host set the desired theme at runtime.

Use product flavors (flavor dimensions) to generate different resource sets for each UI variant.

Example of custom attributes:

<resources>
    <attr name="account_sign_in_bg" type="color"/>
    <attr name="account_sign_in_logo" type="reference"/>
</resources>

Layout usage:

<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="?attr/account_sign_in_bg">
...
</androidx.constraintlayout.widget.ConstraintLayout>

Theme definition in the host app:

<resources>
    <style name="Theme.ComponentizationSample" parent="Theme.MaterialComponents.DayNight.NoActionBar">
        <item name="colorPrimary">@color/purple_500</item>
        ...
        <item name="account_sign_in_bg">@color/sign_in_bg</item>
        <item name="account_sign_in_logo">@drawable/ic_sign_in_logo</item>
    </style>
</resources>

Theme‑based approach via service interface:

interface AccountService : IProvider {
    var theme: AccountTheme
}

enum class AccountTheme { BLUE, GREEN, ORANGE }

@Route(path = AccountPaths.SERVICE)
class AccountServiceProvider : AccountService {
    override var theme = AccountTheme.GREEN
}

Flavor‑based approach (e.g., UI dimension with flavors "blue", "green", "orange"):

android {
    flavorDimensions "ui"
    productFlavors {
        blue { dimension "ui" }
        green { dimension "ui" }
        orange { dimension "ui" }
    }
}

KV Storage

Configuration data is usually stored with SharedPreferences , MMKV , or DataStore . In a modular project, each module should have its own storage instance to avoid key collisions.

MMKV‑KTX provides property‑delegate helpers that automatically use the property name as the key, eliminating the need for a large list of constants.

object DataRepository : MMKVOwner {
    var isFirstLaunch by mmkvBool(default = true)
    var account by mmkvParcelable
()
}

When reading and writing must use the same property, the key is guaranteed to be consistent. If different properties are used, data loss can occur, as shown in the example where one activity writes psd while another reads pwd .

Implementing MMKVOwner allows each component to supply its own MMKV instance, ensuring isolation even when keys overlap:

interface MMKVOwner {
    val kv: MMKV get() = com.dylanc.mmkv.kv
}

object AccountRepository : MMKVOwner {
    override val kv: MMKV = MMKV.mmkvWithID("account")
    var token by kv.mmkvString()
}

Prudent Use of Routing Frameworks

Routing frameworks are powerful but should only be used for necessary inter‑component interactions. Overusing routing for internal navigation or network calls adds complexity and reduces readability.

Typical guidelines:

Use routing only when a component needs to be accessed from another module.

Keep internal page navigation with startActivity(intent) unless a router interceptor is required.

Avoid exposing unnecessary APIs (e.g., a login method) if the component only needs to provide a UI entry point.

Conclusion

Componentization itself is straightforward; the challenge lies in applying it to complex business scenarios. This article shared practical experience on component splitting, handling dependencies, configuring multiple UI sets, safely using KV storage, and using routing frameworks judiciously. By following these practices, you can keep your Android codebase modular, maintainable, and adaptable to various product requirements.

Sample code is available on GitHub (link to be added).

ModularizationAndroidKotlinroutingcomponentizationKV storageUI Configuration
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.