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.
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.
ByteDance Dali Intelligent Technology Team
Technical practice sharing from the ByteDance Dali Intelligent Technology Team
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.