Android开发[4]:组件化

7 阅读12分钟

Android组件化

今日核心目标

  • 掌握组件化核心概念、架构优势及基础拆分原则,完成简单的组件化项目初始化(单工程多模块搭建),规避组件化入门常见踩坑点,建立架构思维。
  • 掌握组件间3种基础通信方式(Intent、接口、热流),完成user组件与壳工程、组件与组件间的简单通信实战,强化“高内聚、低耦合”的架构思维,提升组件化实操能力。

组件化入门

围绕以下几点

  • 组件化概念
  • 组件化优势
  • 组件拆分原则
  • 组件化职场案例
  • 组件化踩坑点规避
  • 组件化工程搭建

组件化概念

组件化

将一个完整的App,拆分成多个可复用、可独立编译运行的组件(例:首页组件、搜索组件、个人中心组件)。

  • 每个组件可独立开发、测试、迭代,最终通过组件融合,组装成完整App
  • 核心:独立可复用
与模块化区别
  • 模块化侧重功能拆分(例:网络模块、数据库模块)。模块不可独立运行,依赖主工程。
  • 组件化侧重业务拆分(例:用户业务组件、订单业务组件)。组件可独立运行、独立部署,是模块化的进阶版。
  • 实际开发中组件化更适合大型项目的开发。

组件化优势

  • 提升开发效率:多团队并行开发,互不干扰,减少代码冲突,缩短开发周期。
  • 降低维护成本:组件独立,某一组件迭代、修复bug时,不影响其他组件,后期维护更高效,减少“牵一发而动全身”的问题。
  • 提升代码复用性:公共组件可在多个项目中复用,核心业务组件可在不同App中复用,降低重复开发成本。
    • 公共组件:工具类、基础UI组件。
  • 便于测试部署:单个组件可独立编译、独立测试,无需编译整个工程,测试效率提升。后续可实现组件热更新,提升用户体验。

组件拆分原则

组件拆分直接决定架构设计的合理性,是组件化入门的核心。须遵循以下原则,规避拆分混乱问题。

  • 高内聚:一个组件内的功能高度相关,不掺杂其他无关业务。
    • 例:个人中心组件,仅包含登录、注册、个人信息管理等与用户相关的功能。
  • 低耦合:组件间减少依赖,若必须依赖,通过接口实现,避免直接依赖具体实现。
    • 例:个人中心组件不可直接调用首页组件的方法,通过接口通信。
      • 接口:定义在公共组件中,各业务组件通过依赖公共模块,实现接口通信(接口具体实现在各业务组件中)。
      • 接口通信需依托base基础模块,将接口下沉至公共模块,实现组件间解耦。
  • 单一职责:一个组件只负责一个核心业务或功能,避免大而全
    • 例:搜索组件仅负责搜索相关功能,不包括数据缓存、用户信息展示等无关功能。
  • 可复用性:拆分时考虑组件的复用场景,避免一次性组件。
    • 例:公共工具组件、基础UI组件,需设计成可复用状态。

组件化职场案例

电商App拆分:各组件独立开发、通过接口通信
  • 首页组件
  • 商品组件
  • 购物车组件
  • 订单组件
  • 用户组件
  • 支付组件
工具类App拆分
  • 核心功能组件(例:文件管理组件、图片处理组件)。
  • 基础组件:工具类组件、UI组件
  • 壳工程组件:负责各组件组装融合

组件化踩坑点规避

踩坑点1:组件拆分混乱,出现跨组件依赖、功能重叠
  • 修复:严格遵循高内聚、低耦合、单一职责原则,拆分前先梳理业务模块,明确各组件的核心功能,避免越界。
踩坑点2:模块依赖冲突(不同组件依赖不同版本的三方库OkHttp、Coroutine等
坑点3:组件无法独立运行
  • 修复:通过gradle配置,实现组件模式集成模式的切换。
    • 组件模式:配置为application
    • 集成模式:配置为library
坑点4:组件间通信方式选择错误(业务组件间存在直接依赖,导致耦合过高)
  • 修复:分析业务选择合适的通信方式,降低耦合。
    • 接口通信Intent通信热流SharedFlow/StateFlow通信
坑点5:工程搭建后报错(资源冲突、清单文件合并失败)
  • 修复:统一资源命名规范,配置清单文件合并规则,避免资源重复。
    • 组件资源通过前缀区分

组件化工程搭建

基础版本先实现“壳工程+基础模块+业务组件”的基础架构。

  • 壳工程:不包含具体业务逻辑,仅负责组件融合、初始化,是整个App入口。
    • 初始化:AppContext初始化、路由初始化、核心库初始化等。
  • base模块:包含公共工具类、基础UI模块、网络封装、数据库封装等,供所有业务组件复用。
  • 业务组件(以个人中心组件为例):包含用户相关业务逻辑(登录、注册、个人信息等)。
    • 可独立运行、独立测试,依赖base模块。
组件化工程创建步骤
  • 1.Android Studio新建项目ComponentDemo
  • 2.创建base模块(新建Module)
    • 项目右键 -> New -> Module -> 选Android Library(命名base,输入包名com.xxx.base) -> Finish完成模块创建
    • base模块的build.gradle.kts三方库依赖使用统一版本管理工具,统一管理依赖版本号。
    • 添加公共工具类、基础UI、网络封装、数据库封装等
  • 3.创建个人中心user组件(新建Module)
    • 项目右键 -> New -> Module -> 选Android Library(命名user,输入包名com.xxx.user) -> Finish完成模块创建
    • user组件的build.gradle.kts三方库依赖使用统一版本管理工具,统一管理依赖版本号。
    • 配置user模块,支持独立运行(切换组件模式/集成模式),在build.gradle.kts中直接写死配置实现,按需手动切换
      • 注意
        • 在项目根目录的gradle.properties中添加开关,在build.gradle.kts的plugin {}中是无法使用的。
        • 在build.gradle.kts的plugin {}前面是无法定义变量的。
// user/build.gradle.kts
plugins {
    if (true) {
        id("com.android.application")
    } else {
        id("com.android.library")
    }
    ...
}

android {
    ...
    defaultConfig {
        ...
        // 组件模式配置
        if (true) {
            applicationId = "com.xxx.user"
        }
    }
}

dependencies {
    implementation(project(":base"))
    ...
}
  • 4.配置壳工程
...
// 按需修改该值
val isUserModule = true

dependencies {
    implementation(project(":base"))
    // 集成模式依赖user组件
    if (!isUserModule) {
        implementation(project(":user"))
    }
    
    ...
}
  • 5.壳工程初始化
class AppApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        // 初始化AppContext
        AppContext.init(this)

        // 其他初始化操作
    }
}
  • 6.验证
    • 切换集成模式,同步工程,打包验证是否有报错
    • 运行壳工程,确保App能正常启动,无crash
    • 切换成组件模式,运行user组件,确保user组件能独立运行

组件间通信

围绕以下几点

  • 组件间通信认知
  • 3种基础通信方式:Intent、接口、热流
  • 组件通信踩坑点规避

组件间通信认知

先明确组件通信的核心原则和选型逻辑,避免盲目选择,贴合实际开发场景。

核心原则
  • 始终遵循高内聚、低耦合、单一职责,组件间不直接引用、不直接调用方法。
  • 通过统一的通信媒介实现数据传递和功能调用,降低维护成本。
选型逻辑
  • Intent:简单页面跳转+少量数据传递。
  • 接口:组件间功能调用。依托base模块,将接口下沉至公共模块,实现组件间解耦。
  • 热流(SharedFlow/StateFlow):组件间状态共享、事件传递。

3种基础通信方式:Intent、接口、热流

每种通信方式按原理-适用场景-实战讲述。

Intent通信:页面跳转+少量数据传递
原理
  • 基于Android原生Intent机制,通过隐式Intent(指定组件包名+类名)实现组件间页面跳转,附带少量数据。
  • 适用于简单页面跳转场景,无需额外依赖库,原生支持。
适用场景:仅传递少量简单数据(页面标题、搜索id)
  • 壳工程跳转user组件的页面
  • user页面内部页面跳转
  • user组件与home组件间跳转
实战
try {
    // 指定目标页面
    val intent = Intent()
    intent.setComponent(ComponentName(packageName, "com.xxx.user.UserActivity"))
    intent.putExtra("userId", "userId")
    startActivity(intent)
} catch (e: ActivityNotFoundException) {
    Log.e("Xxx", e.message ?: "ActivityNotFoundException")
}

// 目标页面
class UserActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_user)

        // 接收Intent传递的数据
        val userId = intent.getStringExtra("userId") ?: "userId"
    }
    ...
}
接口通信:组件间功能调用、业务能力复用
原理
  • 采用接口下沉+服务实现的方式,将组件间需要调用的接口定义在base模块,由具体业务组件实现接口,其他组件通过接口调用功能。
  • 不依赖具体实现,实现高内聚、低耦合,是实际开发中组件间功能调用的主流方式。
适用场景
  • 组件间功能调用(例:user组件提供获取用户信息功能,供其他组件调用)。
  • 业务能力复用(例:支付组件提供支付功能,供订单组件调用)。
实战
  • 1.base模块定义接口
// 定义在base模块,由user组件实现的接口
interface IUserService {
    // 获取用户信息
    fun getUserInfo(): UserInfo
    
    // 异步方法:结合协程+Flow
    suspend fun getAsyncUserInfo(): UserInfo
}
  • 2.在user组件中实现接口
// user组件实现
class UserServiceImpl: IUserService {
    override fun getUserInfo(): UserInfo {
        val userInfo = ""
        ...
        return userInfo
    }

    override suspend fun getAsyncUserInfo(): UserInfo {
        val userInfo = ""
        ...
        return userInfo
    }
}
  • 3.base模块中,创建服务管理类(用于注册获取接口实现,避免组件间直接依赖)。
// 服务管理类
object ServiceManager {
    // 存储接口实现,简单实现,后续可结合依赖注入框架优化
    private val serviceMap = HashMap<Class<out Any>, Any>()

    // 注册接口实现,由具体组件初始化时注册
    fun <T: Any> registerService(clazz: Class<T>, service: T) {
        serviceMap[clazz] = service
    }

    // 获取接口实现
    @Suppress("UNCHECKED_CAST")
    fun <T: Any> getService(clazz: Class<T>): T? = serviceMap[clazz] as? T
}
  • 4.初始化时注册user组件接口的实现
ServiceManager.registerService(IUserService::class.java, UserServiceImpl())
  • 5.其他组件中验证调试
val service = ServiceManager.getService(IUserService::class.java)
val userInfo = service?.getUserInfo()
热流通信:组件间状态共享、事件传递
原理

基于Flow热流(StateFlow/SharedFlow),在base模块中创建热流实例,组件通过发送热流、收集热流实现状态共享和事件传递(例:用户登录状态变化、全局通知)。

  • StateFlow:适用于UI状态管理(保存最新状态)。
  • SharedFlow:适用于离散事件传递。
适用场景:结合协程实现异步通信,避免内存泄漏
  • 组件间状态共享(例:用户登录状态、主题切换)。
  • 离散事件传递(例:点击事件、通知提示)。
实战
  • 1.在base模块中创建热流管理类(统一管理热流,供其他组件调用)
// 热流管理类
object FlowBusManager {
    // StateFlow:用于状态共享,保存最新状态,例登录状态
    private val _userStateFlow = MutableStateFlow<UserInfo?>(null)
    val userStateFlow: StateFlow<UserInfo?> = _userStateFlow.asStateFlow()

    // SharedFlow:用于离散事件传递,例通知、点击事件,缓冲值个数为1
    private val _userEventFlow = MutableSharedFlow<String>(extraBufferCapacity = 1)
    private val userEventFlow: SharedFlow<String> = _userEventFlow.asSharedFlow()

    // 发送用户状态:用户登录,信息更新时调用
    fun sendUserState(userInfo: UserInfo?) {
        _userStateFlow.value = userInfo
    }
    
    // 发送用户事件:用户退出,操作成功时调用
    suspend fun sendUserEvent(event: String) {
        _userEventFlow.emit(event)
    }
}
  • 2.在user组件中,发送热流,模拟用户登录、事件触发
// 用户登陆成功时调用
FlowBusManager.sendUserState(userInfo)

// 需在协程环境下调用
FlowBusManager.sendUserEvent("用户登陆成功")
  • 3.在壳工程中,收集热流,接收用户状态和事件
// 收集用户登录状态
CoroutineHelper.getPageScope(lifecycle).launch {
    FlowBusManager.userStateFlow
        .bindLifecycle(this@MainActivity)
        .collect { userInfo ->
            Log.i("XxxActivity", "userInfo: $userInfo")
        }
}

// 收集用户事件    
CoroutineHelper.getPageScope(lifecycle).launch {
    FlowBusManager.userEventFlow
        .bindLifecycle(this@MainActivity)
        .collect { event ->
            Toast.makeText(this@MainActivity, event, Toast.LENGTH_SHORT).show()
        }
}

/* ------- 附:工具方法,避免内存泄漏 ------- */
// 页面Scope,跟随页面生命周期(Activity、Fragment),避免内存泄漏
fun getPageScope(
    lifecycle: Lifecycle,
): CoroutineScope {
    val scope = CoroutineScope(Job() + mainDispatcher + coroutineHandler)
    lifecycle.addObserver(object : DefaultLifecycleObserver {
        override fun onDestroy(owner: LifecycleOwner) {
            // 页面销毁,取消所有关联的协程任务
            if (scope.isActive) {
                scope.cancel()
                Log.w(TAG, "page destroy, cancel task")
            }
        }
    })

    return scope
}

// Flow与协程联动:页面绑定生命周期,避免泄漏
fun <T> Flow<T>.bindLifecycle(activity: Activity): Flow<T> = this
    .onStart {
        // 页面启动时收集
    }
    .onCompletion {
        // 页面销毁时取消收集
        if (activity.isDestroyed) {
            currentCoroutineContext().cancel(CancellationException("$activity isDestroyed"))
        }
    }
  • 4.验证
    • 集成模式下,打开App,跳转登录页,登录,看壳工程能否正常接收状态和事件。
    • 页面销毁时热流自动取消收集,无内存泄漏,状态能实时同步,事件能正常接收。。

组件通信踩坑点规避

坑点1:Intent跳转报错,类找不到
  • 原因
    • 1.类名反射时包名错误
    • 2.Activity未注册
    • 3.组件模式/集成模式配置错误导致library未直接或间接被壳工程依赖打包至apk中。
  • 修复
    • 1.核对包名+类名
    • 2.确保Activity注册
    • 3.确保以library模式被壳工程直接或间接依赖打包至apk中。
    • 4.兜底加Activity检查、try-catch规避App直接崩溃。
坑点2:接口通信时,接口实例获取不到,为null
  • 原因:接口未注册或注册时机晚于调用时机。
  • 修复:组件初始化时,注册接口实现,确保调用前完成注册(可在Application#onCreate()完成注册)。
坑点3:热流收集导致内存泄漏
  • 原因:热流收集未绑定页面生命周期,页面销毁后仍在收集。
  • 修复:绑定生命周期或在页面销毁时取消协程,确保热流及时取消。
坑点4:组件间直接依赖,违背低耦合原则
  • 原因:直接引用其他组件的类或方法,未通过接口、热流等媒介通信。
  • 修复:删除直接依赖,通过接口、热流、Intent实现通信,接口下沉至base模块。
坑点5:热流发送后无法接收
  • 原因:SharedFlow未设置extraBufferCapacity,或收集时机晚于发送时机。
  • 修复:SharedFlow设置extraBufferCapacity=1,确保发送的事件能被后续收集者收集。

SharedFlow配置规则:未设置extraBufferCapacity(=0)+ 发射在前、收集在后 → 收集者绝对收不到数据。而且发射方会一直卡住、挂起,直到有人来收集。

  • emit () 必须等有人 collect 才能发送成功
  • 没人接收 → emit 挂起、卡住
  • 发射在前,收集在后 → 数据发不出去,一直等
  • 后来的收集者收不到之前发射的数据
坑点6:依赖冲突,各组件依赖协程、流、OkHttp3等版本不一致
  • 原因:不同组件引入不同版本依赖,导致打包合并时依赖冲突。
  • 修复:使用依赖管理,统一管理版本依赖,避免组件单独引入依赖。