这份文档面向两个目标:
- 新增一个页面或一个业务功能时,知道应该改哪些层。
- 新增类时能直接照着现有范式写,不需要在仓库里到处搜索拼装。
先回答两个问题:
如果是下面这些,优先考虑放在 app 或基础库:
- 首页聚合逻辑
- 宿主启动流程
- 应用级 Web Bridge 能力
- 全局特性开关
- 宿主导航门面
如果功能能独立演进,优先做成业务模块,例如:
- 登录能力放在
module_login - 网络抓包放在
Feature_Capture - 下载、上传能力已经分别独立到
Lib_Download、Lib_Upload
以新增一个 ProfileActivity 为例,推荐路径如下。
先创建页面布局,例如:
app/src/main/res/layout/activity_profile.xml
布局层尽量只负责:
- 结构
- 控件 id
- 简单展示逻辑
不要把业务判断大量堆在 XML 表达式里。
页面优先继承 BaseActivity:
class ProfileActivity : BaseActivity<ActivityProfileBinding, ProfileViewModel>() {
override fun initContentView(savedInstanceState: Bundle?): ActivityProfileBinding {
return ActivityProfileBinding.inflate(layoutInflater)
}
override fun initView() {
mBinding.saveButton.setOnClickListener {
mViewModel.saveProfile()
}
}
override fun initData() {
mViewModel.loadProfile()
}
}推荐原因:
ViewBinding已统一接入TheRouter.inject(this)已在基类处理- Koin ViewModel 解析已在基类处理
- 通用导航、Dialog、Toast、标题栏能力可直接复用
如果是 Fragment,优先继承 BaseFragment:
class ProfileFragment : BaseFragment<FragmentProfileBinding, ProfileViewModel>() {
override fun initContentView(
inflater: LayoutInflater,
container: ViewGroup?
): FragmentProfileBinding {
return FragmentProfileBinding.inflate(inflater, container, false)
}
override fun initView() {
mBinding.refreshButton.setOnClickListener {
mViewModel.reload()
}
}
override fun lazyLoadData() {
mViewModel.loadProfile()
}
}项目当前约定优先使用 @KoinViewModel:
@KoinViewModel
class ProfileViewModel(
application: Application,
private val profileRepository: ProfileRepository
) : BaseViewModel(application) {
fun loadProfile() {
launchOnlyresult(
block = { profileRepository.loadProfile() },
success = { data ->
// 更新状态
}
)
}
}建议:
- 页面状态与动作放在 ViewModel
- 页面跳转优先调用
AppRouter - 登录、权限、防抖、网络校验等优先用 AOP 注解
适合这些场景:
- 登录成功后通知多个页面刷新
- Token 失效后广播统一退出
- 配置、语言、主题等全局状态变化
发送事件:
EventChannel.post(GlobalEvent.TokenExpired("token invalid"))在 ViewModel 中订阅:
EventChannel.observe<GlobalEvent>(sticky = true)
.collectIn(viewModelScope) { event ->
handleGlobalEvent(event)
}在 Activity / Fragment 中订阅:
EventChannel.observeEvent<GlobalEvent>(sticky = true) { event ->
// 更新页面
}建议:
- 全局事件类型优先用
sealed class - 真正需要补收最近状态时再用
sticky = true - 页面销毁自动取消时,优先使用
observeEvent或collectIn(owner)
仓库类优先使用 @Single:
@Single
class ProfileRepository(
networkApiFactory: NetworkApiFactory
) {
private val api: ProfileApiService = networkApiFactory.get()
suspend fun loadProfile() = api.profile()
}建议:
- Repository 只关心数据获取与转换
- 不要把 Activity/Fragment 直接传进 Repository
- 不要在页面里自己创建 Retrofit Service,统一走
NetworkApiFactory
如果类放在已经被 @ComponentScan 扫描的包下,通常不需要额外配置,只要:
@Single@KoinViewModel@Single(binds = [...])
写对即可。
需要补三步:
- 新建模块入口:
@Module
@ComponentScan("com.ghn.feature.profile")
class ProfileModule-
确保模块启用了
ksp -
在
appModules中汇总:
val appModules = listOf(
ProfileModule().module,
AppModule().module
)如果页面只在模块内使用,可以直接本地跳转。
如果页面需要被其他模块访问,按下面流程走。
object RouterPath {
object Profile {
const val MAIN = "/profile/main"
}
}interface ProfileRouter {
fun openProfile(context: Context? = null)
}@Singleton
@ServiceProvider(returnType = ProfileRouter::class)
class ProfileNavigatorProvider : ProfileRouter {
override fun openProfile(context: Context?) {
RouterPath.Profile.MAIN.navigate(context)
}
}fun openProfile(context: Context? = null) {
requireRouterService<ProfileRouter>().openProfile(context)
}这样之后业务层就统一用:
AppRouter.openProfile()不建议在多个页面里直接散落 TheRouter.build("/xxx").navigation()。
当前项目已经提供很多现成注解,优先复用:
| 场景 | 优先注解 |
|---|---|
| 防重复点击 | @PreventRepeat |
| 需要先登录 | @LoginRequired |
| 需要网络 | @RequireNetwork |
| 需要相机权限 | @RequireCameraPermission |
| 需要图片读取权限 | @RequireImageReadPermission |
| 需要耗时埋点 | @TraceTime |
| 仅 Debug 可用 | @DebugOnly |
| 功能开关控制 | @FeatureEnabled |
示例:
@LoginRequired(message = "请先登录后再查看资料")
@PreventRepeat(key = "open_profile", intervalMillis = 800L)
fun openProfile() {
AppRouter.openProfile()
}注意:
@LoginRequired的“登录后恢复”只适用于返回Unit/void的方法@PreventRepeat的key最好有业务语义,避免不同动作互相影响- 权限类注解需要有可解析的
Activity/Fragment上下文
只有真正影响首屏能否工作的任务,才应该放进 BLOCKING_MAIN。
示例:
StartupTask("profile-preload", StartupPhase.AFTER_FIRST_FRAME_BACKGROUND) {
profileLibrary.warmUp(it)
}选择阶段时可以按这个原则:
BLOCKING_MAIN:页面创建前必须准备好的能力AFTER_FIRST_FRAME_MAIN:首帧后仍需在主线程执行AFTER_FIRST_FRAME_BACKGROUND:纯预热、纯后台能力
不要把大体量初始化重新塞回 Application.onCreate()。
如果功能要给 H5 调用,优先新增一个 AnnotatedWebBridgeModule:
@Single(binds = [WebBridgeModule::class])
class ProfileWebBridgeModule : AnnotatedWebBridgeModule() {
override val group: String = "profile"
@BridgeHandler("profile.open")
private fun openProfile(activity: FragmentActivity) {
AppRouter.openProfile(activity)
}
}建议:
- 一个模块维护一组清晰的 Bridge 指令
- Bridge 内部仍然优先调用
AppRouter - 登录、权限、抓包控制等继续交给 AOP 注解处理
| 类型 | 推荐命名 |
|---|---|
| Activity | XxxActivity |
| Fragment | XxxFragment |
| ViewModel | XxxViewModel |
| Repository | XxxRepository |
| 路由协议 | XxxRouter |
| 路由实现 | XxxNavigatorProvider |
| Koin 模块 | XxxModule |
| Bridge 模块 | XxxWebBridgeModule |
如果你要新增一个可跨模块打开的页面,通常至少会改这些点:
- 新建
layout - 新建
Activity或Fragment - 新建
ViewModel - 需要数据时新建
Repository - 在
Lib_Router增加RouterPath - 在
Lib_Router增加XxxRouter - 在业务模块增加
XxxNavigatorProvider - 在
AppRouter增加一个语义化入口 - 按需加
@PreventRepeat、@LoginRequired、@RequireNetwork
- 不要在页面里直接
new Repository()或自己创建 Retrofit Service - 不要把跨模块页面实现类直接暴露给别的模块
- 不要把所有初始化重新堆回
Application.onCreate() - 不要在多个页面里复制登录校验、防抖、权限判断代码
- 不要让 Bridge 代码直接操作太多页面细节,优先走 Router 或 ViewModel