Mobile Development 16 min read

UI Componentization Architecture and Implementation Experience for Android Projects

This article presents a comprehensive experience of UI componentization in Android projects, covering the background, goals, engineering and component architectures, detailed implementation steps for reusable UI components such as FlatButton, and practical Gradle configurations to achieve modular, decoupled, and scalable UI development.

ByteDance Dali Intelligent Technology Team
ByteDance Dali Intelligent Technology Team
ByteDance Dali Intelligent Technology Team
UI Componentization Architecture and Implementation Experience for Android Projects

Background

UI componentization brings positive benefits to projects by improving efficiency, ensuring visual fidelity, and reducing communication cost with designers. However, UI views are tightly coupled with individual projects, leading to duplicated effort and overtime. The following sections describe the goals, engineering architecture, component architecture, and implementation details.

Goal

Abstract UI componentization into a container model where low‑level UI components provide a maximal feature set and are completely decoupled from business logic. Business teams can compose these base components via attributes or composition, greatly improving development efficiency and stability.

Engineering Architecture

Module Division

All UI components are gathered under uikit . Modules are split based on generality: highly reusable modules are extracted, while less generic ones stay together.

app : a shell project that can run independently.

demo : provides demos for all components, accessible from the debug panel.

uikit : the core UI component library, depending on widget and other modules.

Engineering Layers

The architecture consists of five layers: basic controls, composite controls, business UI components, bridge, and demo.

Basic controls: atomic widgets such as FlowLayout , ShimmerLayout , FlatButton .

Composite controls: depend on basic controls (e.g., Dialog , ImageSelector ).

Business UI components: customized UI built on top of basic and composite controls.

Bridge: business layer depends only on uikit , hiding component dependencies.

Demo: documentation and runnable examples integrated into the debug panel.

Architecture diagram:

After confirming the architecture meets the requirements, the next step is integration with existing projects. UI components evolve through a development stage (fast iteration in the host project) and a stable stage (published to a remote Maven repository for independent iteration).

Gradle Integration

settings.gradle

includeIfAbsent ':uikit:uikit'
includeIfAbsent ':uikit:demo'
includeIfAbsent ':uikit:imgselector'
includeIfAbsent ':uikit:roundview'
includeIfAbsent ':uikit:widget'
includeIfAbsent ':uikit:photodraweeview'
includeIfAbsent ':uikit:flatbutton'
includeIfAbsent ':uikit:dialog'
includeIfAbsent ':uikit:widgetlayout'
includeIfAbsent ':uikit:statusbar'
includeIfAbsent ':uikit:toolbar'

common_business.gradle (single‑line dependency)

apply from: rootProject.file("library_base.gradle")

dependencies {
    ...
    implementation project(":uikit:uikit")
}

Independent compilation for UI components (uikit/shell/settings.gradle)

include ':app'
includeModule('widget','../')
includeModule('demo','../')
includeModule('flatbutton','../')
includeModule('imgselector','../')
includeModule('photodraweeview','../')
includeModule('roundview','../')
includeModule('uikit','../')
includeModule('widgetlayout','../')
includeModule('dialog','../')
includeModule('statusbar','../')
includeModule('toolbar','../')

def includeModule(name, filePath = name) {
    def projectDir = new File(filePath+name)
    if (projectDir.exists()) {
        include ':uikit:' + name
        project(':uikit:' + name).projectDir = projectDir
    } else {
        print("settings:could not find module $name in path $filePath")
    }
}

UI component library build.gradle

if (rootProject.ext.is_in_uikit_project) {
    apply from: rootProject.file('../uikit.gradle')
} else {
    apply from: rootProject.file('uikit/uikit.gradle')
}

Component Architecture

Components are divided into two categories: tool‑type and business‑type. Tool components focus on maximal functionality and reusability (e.g., FlatButton , RoundView , StatusBar ). Business components balance reusability with extensibility and are built on top of the tool set.

Tool‑type

Iterate by continuously enriching core capabilities while maintaining backward‑compatible APIs.

Business‑type

Design should be both abstract at the bottom layer and concrete at the top layer, enabling container‑style composition.

Component Implementation – FlatButton Example

Step 1: Define XML shapes for normal, pressed, and disabled states.

normal (ui_standard_bg_btn_corner_28_ripple)

<ripple xmlns:android="http://schemas.android.com/apk/res/android"
    android:color="@color/button_pressed_cover">
    <item android:drawable="@drawable/ui_standard_bg_btn_corner_28_enable"/>
</ripple>

pressed (ui_standard_bg_btn_corner_28_disable)

<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <gradient android:angle="0"
        android:endColor="@color/button_disable_end"
        android:startColor="@color/button_disable_start"
        android:useLevel="false"
        android:type="linear"/>
    <corners android:radius="28dp"/>
</shape>

Step 2: Define a selector that switches between the above drawables.

selector (ui_standard_bg_btn_corner_28)

<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_enabled="true" android:drawable="@drawable/ui_standard_bg_btn_corner_28_ripple"/>
    <item android:state_enabled="false" android:drawable="@drawable/ui_standard_bg_btn_corner_28_disable"/>
</selector>

Step 3: Use the selector as the background of a TextView (or any view).

<TextView
    ...
    android:background="@drawable/ui_standard_bg_btn_corner_28"
    android:textColor="@color/white"/>

Step 4: Define custom attributes for a fully configurable FlatButton .

<declare-styleable name="FlatButton">
    <attr name="fb_colorNormal" format="color"/>
    <attr name="fb_colorPressed" format="color"/>
    <attr name="fb_colorDisable" format="color"/>
    ...
    <attr name="fb_gradientOrientation">
        <enum name="left_right" value="0"/>
        <enum name="right_left" value="1"/>
        ...
    </attr>
    ...
</declare-styleable>

Step 5: Core implementation logic (Kotlin) creates state‑list drawables and optional ripple effects.

private fun setBackgroundCompat() {
    val stateListDrawable = createStateListDrawable()
    val pL = paddingLeft
    val pT = paddingTop
    val pR = paddingRight
    val pB = paddingBottom
    background = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && isRippleEnable) {
        RippleDrawable(createRippleColorStateList(), stateListDrawable, null)
    } else {
        stateListDrawable
    }
    setPadding(pL, pT, pR, pB)
}

private fun createStateListDrawable(): StateListDrawable {
    val drawable = StateListDrawable()
    drawable.addState(intArrayOf(android.R.attr.state_pressed), createPressedDrawable())
    drawable.addState(intArrayOf(-android.R.attr.state_enabled), createDisableDrawable())
    drawable.addState(intArrayOf(), createNormalDrawable())
    return drawable
}

private fun createRippleColorStateList(): ColorStateList {
    val states = arrayOf(intArrayOf(android.R.attr.state_pressed), intArrayOf())
    val colors = intArrayOf(backgroundStyle.getColorRipplePressedFallback(), backgroundStyle.getColorRippleNormalFallback())
    return ColorStateList(states, colors)
}

Step 6: Use the custom view in XML.

<com.snapsolve.uikit.flatbutton.FlatButton
    app:fb_colorNormalText="@color/uikit_color_white"
    app:fb_colorPressedText="@color/uikit_color_white"
    app:fb_colorNormalEnd="#FF9800"
    app:fb_colorNormalStart="#FF0000"
    app:fb_colorPressedEnd="#4CAF50"
    app:fb_colorPressedStart="#009688"
    app:fb_colorRippleNormal="#303F9F"
    app:fb_colorRipplePressed="#FF4081"
    app:fb_cornerRadius="24dp"
    app:fb_gradientOrientation="left_right"
    app:fb_isRippleEnable="true"
    .../>

Step 7: Business usage – extend FlatButton to create preset styles such as a stroke button.

class StrokeButton : FlatButton {
    constructor(context: Context) : this(context, null)
    constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
        config(context, attrs)
    }

    private fun config(context: Context, attrs: AttributeSet?) {
        setBackgroundStyle {
            colorNormal = resources.getColor(R.color.uikit_color_FF4081)
            colorPressed = resources.getColor(R.color.uikit_color_9C27B0)
            colorRippleNormal = resources.getColor(R.color.uikit_color_FF4081)
            colorRipplePressed = resources.getColor(R.color.uikit_color_9C27B0)
        }.setRadiusStyle {
            radiusTL = dp2px(28F)
            radiusBR = dp2px(28F)
        }
    }

    private fun dp2px(dp: Float): Float = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, resources.displayMetrics)
}

Usage in layout:

<com.snapsolve.uikit.demo.flatbutton.StrokeButton
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>

Conclusion

The described architecture provides a clear, low‑coupling, highly extensible framework for UI component development, enabling independent iteration, easy integration with host projects, and a smooth transition from fast development to stable, published components.

mobile developmentAndroidGradlecomponent architectureCustom ViewsUI componentization
ByteDance Dali Intelligent Technology Team
Written by

ByteDance Dali Intelligent Technology Team

Technical practice sharing from the ByteDance Dali Intelligent Technology Team

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.