Architecture-Android
功能介绍
- 支持配置变更 后的还原
- 屏幕旋转
- 亮暗主题切换
- 语言切换(国际化)
- 字体大小更改
- 分屏
- ...
- 支持进程杀死后的还原
- 项目架构:模块化+组件化+MVI(UiState+ViewModel+Flow+Kotlin协程+Repository+DataSource+Retrofit)
- 支持多App开发
- 支持一键切换Feature模块单独运行
- 支持一键去除可移除功能代码
- 支持项目无反射实现(项目默认无反射实现,反射实现也提供,可供在两者选择)
- 支持EdgeToEdge (
targetSdk >= 35(Android15),强制开启了,所以为了开启时兼任低版本,需要全部支持) - 支持动态主题 (
Android12+支持此功能) - 支持刷新、自动预加载(自动预加载,如果用户滑动慢并且获取数据快,用户是感觉不到加载的)
本项目为一个Android架构 ,它遵循 Android 设计 和开发最佳实践 ,旨在为开发者提供实用参考。
本项目目标 是为了同时 支持Compose和View,以支持公司项目 目前已有的View代码和之后的Compose代码,目前本项目仅 支持View,后续项目关注度 (Star点赞数) 高后 会支持Compose。
本项目是以字节跳动 公司的抖音 App为参照,模拟开发的抖音 App。由于本项目,无抖音真正的网络数据 ,所以本项目使用的数据,是通过某些开源API 网络接口,模拟 转的网络数据。
本项目是在官方的架构 (nowinandroid(18.2k Star)、architecture-samples(44.9k Star))上做的升级和修改 ,如果大家对此架构模块的划分 不理解,建议大家先了解官方的 nowinandroid,然后再来看本项目。
本项目文档分为快速介绍 、详细介绍 (模块间架构 、模块内架构 )、使用 ,建议大家按照顺序阅读文档。如果你想快速 的了解,可以只看快速介绍 、使用 ,我提供了demo工程 ,里面有最简单 的使用案例,可以先阅读此代码。
欢迎大家一起来维护 项目,使其功能更加的强大 、健硕 。有问题,有需求,请提issue,或者私信我。
项目链接: architecture-android,欢迎大家点赞、收藏,以方便您后续查看。
下载
扫码下载
截图
App展示
主题展示
| Themes | Light | Dark |
|---|---|---|
| 抖音主题 | ||
| 动态主题1 | ||
| 动态主题2 |
快速介绍
下载
下载项目并运行
shell
git clone git@github.com:zrq1060/architecture-android.git
功能演示
本项目任何页面,都支持如下:
- 配置变更后的还原 (屏幕旋转 、亮暗模式切换 、语言切换 、字体大小更改 、分屏 等),配置变更会导致
Activity、Fragment会重新创建新的。- 进程被杀死后的还原(可打开,开发者选项-后台进程限制-不允许后台进程,以更好的测试进程被杀死。开启后,可在后台多打开一些无关的app,再切换打开此app即可演示此效果)。
本项目,目前仅支持如下功能:
-
登录页 :登录账号(手机号、邮箱)为 【任意】 ,登录密码(验证码、密码)为 【123456】 。可断网 ,或输入错误密码,查看页面效果。
-
Home首页 :顶部栏目的排序(长按首页-顶部栏目)
-
Main主页 :好友、商场栏目的切换(长按主页-底部栏目第2个)
-
Shop商城页 :支持刷新 、自动加载 ,点击商城条目 模拟的商城列表数据的增、删、改 操作。可断网,查看页面效果。
在此跟着上面,操作App支持的功能(记得开启屏幕自动旋转、切换亮暗模式、点击商城Item),以演示上面功能效果。
单独运行Feature模块演示
1、修改项目根目录 下gradle.properties内isFeatureSingle为true,并Sync同步Gradle。
groovy
isFeatureSingle = true
2、执行安装全部命令
点击右侧Gradle-Tasks-install-installDebug,或执行如下命令:
groovy
.\gradlew installDebug
执行完后,会在手机桌面 出现所有Feature模块 的App(如下图所示),点击 某个即可测试某单个Feature模块。
详细介绍
我们先讲模块间的架构 ,然后再讲模块内的架构 ,最后讲使用。
模块间架构
在不断变大的代码库中,可扩缩性 、可读性 和整体代码质量 通常会随着时间的推移而降低。这是因为代码库在不断变大,而其维护者未采取积极措施来保持易于维护的结构。模块化是一种行之有效的代码库构建方法,可帮助改善可维护性并避免此类问题。
模块化 相关,请看官方的 Android 应用模块化指南。
官方(标准版)模块划分demo 相关,请看官方的nowinandroid。
模块划分
标准版
本项目官方(标准版)的模块化 完成后,模块图(部分模块) 如下:
模块说明:
app模块 :app模块依赖于所有 的feature模块和必需 的core模块。feature:模块 :feature模块不应该依赖于其它的feature模块,它们只依赖于所需 的core模块。core:模块 :core模块可以依赖于其它的core模块,但它们不应该依赖于feature模块或app模块。
本项目官方 的模块化 完成后,项目目录图 如下(标准版):
多App版
一般一个公司并非一个App ,比如商城功能App (一个用户端、一个商家端)、外卖功能App (一个用户端、一个骑手端)。以字节跳动 公司为例子,其中公司开发的App有抖音 、西瓜视频 、今日头条 、飞书 、剪映等。
上面官方(标准版)的模块 划分,导致内部的core模块含有本App特有 的、所有App通用 的代码及资源,仅适用于单App 架构。如果要适用多App 架构,就需要把core模块内所有App通用部分 提取出来,提取后的项目,项目目录图 如下(多App版):
变化说明:
- 把标准版 的抖音 的模块结构,存到了最外层
douyin目录。- 把一些可所有App共用 的模块,存到了最外层
core目录。- 把标准版 的西瓜视频 的模块结构,存到了最外层
xigua目录。
目录说明:
- 最外层
core目录 :为所有App 都可以使用的代码及资源,内部模块 被所有App 内的core模块依赖。 - 最外层
douyin目录 :为 抖音App自己独有(特有) 的相关代码及资源。core模块 :依赖最外层core目录 内的模块,反之不行。app模块 、feature模块 :直接依赖抖音内部core模块即可,此为最外层core目录 的功能定制 ,如:直接依赖抖音App 的:douyin:core:architecture模块即可,此模块为抖音App 对:core:architecture模块(所有App通用的-架构模块 )的定制。
- 最外层
xigua目录 :为西瓜视频App的相关代码,规则同上(抖音)。
说明:还可以在最外层 继续开发其它App,如今日头条 、飞书 、剪映等,规则同上(抖音、西瓜视频)。
可移除版
在项目开发过程中,如果你不看好 要开发的功能,或者领导、产品告诉你,要开发的功能之后可能会移除,你可以使用此设计。
以抖音App 为例,最早 的抖音 是没有商城 功能的,如果以商城 功能之后会移除来开发,你可以使用以下模块设计。
现在的项目,项目目录图 如下(可移除版):
变化说明:
- 把可能要移除的shop功能 的模块结构,存到了最外层
douyin目录。
新增的shop目录说明:
core模块 :可以依赖抖音、商城core模块,但是抖音core模块不能依赖商城core模块(以便好在抖音内移除)。feature模块 :同级feature间不能相互依赖,只能依赖抖音、商城core模块。
可以修改项目根目录 下gradle.properties内isShopInclude为false、isRouterReflect为true,来演示此移除功能。
groovy
isShopInclude = false
isRouterReflect = true
说明:
isShopInclude:为是否包括商城。
isRouterReflect:为是否Router反射实现。
- 本项目
Router的实现分为了两种,Dagger实现 (正式 用)、反射实现 (测试 用),详细看router模块。
模块包名
包名格式一般为:域名反转+项目名+功能名 ,以此字节跳动 (域名:www.bytedance.com )公司抖音项目为例,规则如下:
core:com.bytedance.core.xxx (和特定App无关),如:com.bytedance.core.architecturedouyin:com.bytedance.douyin.xxx (和特定App有关 )app:com.bytedance.douyincore:com.bytedance.douyin.core.xxx,如:com.bytedance.douyin.core.architecturefeature:com.bytedance.douyin.feature.xxx,如:com.bytedance.douyin.feature.homeshop:com.bytedance.douyin.shop .xxxcore:com.bytedance.douyin.shop.core.xxx,如:com.bytedance.douyin.shop.core.datafeature:com.bytedance.douyin.shop.feature.xxx,如:com.bytedance.douyin.shop.feature.shop
xigua:规则同上(抖音)
模块功能
app:项目的入口,含有MainActivity、Application等。corearchitecture:架构相关,包含一些基础类,如:最外层:core:architecture模块包含通用的BaseViewsActivity、BaseViewsFragment等,抖音层:douyin:core:architecture模块包含抖音 定制的AppViewsActivity、AppViewsFragment等。architecture-reflect:架构反射实现相关,包含一些架构内的反射实现,如:reflectInflateViewBinding(反射实现ViewBinding)、reflectViewModels(反射实现ViewModel)。common:通用相关,包含一些通用类、工具类等。designsystem:设计系统相关,包含控件、主题等。model:Model类相关,包含Model类等。network:网络相关,包含NetworkDataSource、网络工具类、图片加载等。test:测试页面相关(为了给未实现的功能,占位用),包含TestActivity、TestFragment等。webview:网页相关,包含网页的跳转、配置等。data:数据相关,包含Repository类等。datastore:DataStore存储相关,包含PreferencesDataSource等。datastore-proto:DataStore的proto配置相关,包含user.proto配置等。feature-single:单独模块运行通用配置相关,包含TestFragmentDetailsAndroidEntryPointActivity等。login:登录相关,包含登录检测、当前登录状态、退出登录等。router:路由系统相关,包含Router的Dagger实现 、反射实现等。
feature:功能业务,包含UI、ViewModel等。
Feature模块间通信介绍
Feature模块间通信,使用router模块的Router类进行通信,以home模块为例规则如下:
定义
kotlin
interface HomeRouter {
fun createHomeFragment(): Fragment
}
此HomeRouter接口为home模块对外暴露 的可供其它模块调用 部分,在router模块内定义,如果还有其它的,可继续在此接口内添加,如:createXXXFragment、startXXXActivity方法等。
真的实现
kotlin
class DefaultHomeRouter : HomeRouter {
override fun createHomeFragment(): Fragment = HomeFragment.newInstance()
}
此DefaultHomeRouter类为HomeRouter接口真的实现 ,在home模块内实现。
假的实现
kotlin
class FakeHomeRouter : HomeRouter {
override fun createHomeFragment(): Fragment = AppTestFragment.newInstance("Home")
}
此FakeHomeRouter类为HomeRouter接口假的实现 ,在router模块的router-reflect内实现,内部使用的AppTestFragment仅是为了显示时占位用。
说明:
Router-Dagger实现 :使用
Dagger找HomeRouter的实现(目前提供的是DefaultHomeRouter),如果找不到会报错。Router-反射实现 :使用反射 直接找
DefaultHomeRouter,如果找不到会直接使用FakeHomeRouter,不会报错。
调用
kotlin
val homeFragment = Router.Home.createHomeFragment()
单独运行Feature模块介绍
如果你只负责某个Feature模块,或者想更解耦 、更快 的测试你的功能,你可以使用此单独运行Feature模块,步骤如下:
修改配置
修改项目根目录 下gradle.properties内isFeatureSingle为true,并Sync同步Gradle。
groovy
isFeatureSingle = true
说明:
isFeatureSingle:为是否单独运行Feature模块 。如果开启 ,则Router使用反射实现 ,以使其调用其它模块 没有时不会报错 ,而是使用Fake的实现(如:占位显示)。
添加测试入口点
此功能需要配合使用我的TestPoint库来实现,添加测试入口点 ,即会在测试列表页 增加一个按钮,点击按钮跳转到此Activity、Fragment,定制按钮点击 等详细使用请看TestPoint。
在目标类上添加TestEntryPoint注解,如ShopFragment:
kotlin
@TestEntryPoint("商城")
class ShopFragment{
}
运行
单个运行:
选择上面的一个,并运行,如:选择douyin.shop.feature.shop,则运行抖音的商城功能 (可以测试商城的点击Item功能等)。
多个运行:
点击右侧Gradle-Tasks-install-installDebug,或执行如下命令:
groovy
.\gradlew installDebug
执行完后,会在手机桌面 出现所有Feature模块 的App(如快速介绍-单独运行Feature模块演示图所示),点击 某个即可测试某单个Feature模块。
模块内架构
官方架构
模块内架构,使用官方 的推荐架构,有助于构建强大而优质的应用。
应用架构 相关,请看官方的 应用架构指南。
官方的架构概述图 如下:
官方架构分为了:UI层 、Domain层 (可选)、Data数据层。
项目架构
本项目,目前没有使用 Domain层,也没有使用 Room库,目前的项目架构图 如下:
使用
Activity、Fragment
以demo模块的MainActivity为例:
kotlin
package com.bytedance.demo.app.main
import android.view.LayoutInflater
import androidx.activity.viewModels
import com.bytedance.douyin.core.architecture.app.views.AppViewsActivity
import dagger.hilt.android.AndroidEntryPoint
// 设置as别名,一般都是设置这几个。
// 使用别名后,此类的模板,下面的不需要改了,只需要改上面as这里即可。
import com.bytedance.demo.app.main.MainUiState as UiState
import com.bytedance.demo.app.main.MainViewModel as ViewModel
import com.bytedance.demo.databinding.ActivityMainBinding as ViewBinding
/**
* 描述:
*
* @author zhangrq
* createTime 2025/3/24 11:14
*/
@AndroidEntryPoint
class MainActivity : AppViewsActivity<ViewBinding, UiState, ViewModel>() {
// 在父类AppViewsActivity中,可用反射实现(reflectViewModels()),省略此实现。
override val viewModel: ViewModel by viewModels()
// 在父类AppViewsActivity中,可用反射实现(reflectInflateViewBinding()),省略此实现。
override fun inflateViewBinding(inflater: LayoutInflater) = ViewBinding.inflate(inflater)
// 初始化View(可以在里面直接拿到当前页面布局控件)
override fun ViewBinding.initViews() {
// 设置TextView控件
content.textSize = 50f
// content.setTextColor(Color.BLACK)
}
// 初始化Listener(可以在里面直接拿到当前页面布局控件)
override fun ViewBinding.initListeners() {
// 设置TextView点击
content.setOnClickListener {
// 显示Toast,此Toast和当前页面的生命周期绑定,当前页面不可见,Toast关闭。
viewModel.showMessage("Long Toast", isShort = false)
}
}
// 初始化Observer(可以在里面直接拿到当前页面布局控件),用于观察(收集)ViewModel内的暴露的属性值(Flow值)。
override fun ViewBinding.initObservers() {
}
// 收集UiState的值(可以在里面直接拿到当前页面布局控件),用于设置当前页面的数据。
override fun ViewBinding.onUiStateCollect(uiState: UiState) {
// 设置TextView的值
content.text = uiState.tabs?.joinToString()
}
}
Activity、Fragment、DialogFragment的使用规则相同 ,以Activity为例,说明如下:
MainActivity直接继承App级 的AppViewsActivity,此类为抖音 项目对通用级 的BaseViewsActivity的定制。ViewModel、ViewBinding的创建 ,由于本项目为了性能没有使用反射 ,所以需要在每个子类中自己实现 ,可以在App级 的AppViewsActivity内使用reflectInflateViewBinding、reflectViewModels反射实现 ,这样就可以在每个子类 中省略ViewModel、ViewBinding的创建代码。- 初始化系列方法 ,使用
ViewBinding扩展方法,是为了能让其在方法内直接获取到xxx控件 ,而不用通过binding.xxx获取,以更方便 的操作控件。XXXBinding、XXXUiState、XXXViewModel,全部通过as别名 来命名,简化了名字长度 ,统一了代码样式一致性 ,这样新类 只需要修改模板类上面as别名即可。
ViewModel
以demo模块的MainViewModel为例:
kotlin
package com.bytedance.demo.app.main
import com.bytedance.douyin.core.architecture.app.AppViewModel
import com.bytedance.douyin.core.data.repository.interfaces.MainRepository
import com.bytedance.douyin.core.model.MainTabType
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
// 设置as别名
import com.bytedance.demo.app.main.MainUiState as UiState
/**
* 描述:
*
* @author zhangrq
* createTime 2025/3/24 11:14
*/
@HiltViewModel
class MainViewModel @Inject constructor(mainRepository: MainRepository) : AppViewModel<UiState>() {
override val uiStateInitialValue: UiState = UiState() // UiState-初始化值
// 从MainRepository获取的本地流,本地数据改,UI改。
override val uiStateFlow: Flow<UiState> = mainRepository.getMainTabsStream().map {
// UiState-页面值
UiState(tabs = it)
}
}
// Main-UiState
data class MainUiState(
val tabs: List<MainTabType>? = null,
)
说明:
ViewModel直接继承App级 的AppViewModel,此类为抖音 项目对通用级 的BaseViewModel的定制。uiStateInitialValue为UiState的初始化值,一般为默认的UiState对象。uiStateFlow为UiState的Flow流,它变化会影响到Activity、Fragment的onUiStateCollect(),一般为Flow(单个Flow、使用combine()观察多个Flow)的map()转为UiState的Flow。XXXUiState,通过as别名 来命名,简化了名字长度 ,统一了代码样式一致性 ,这样新类 只需要修改模板类上面as别名即可。
生命周期Toast、Snackbar
直接显示 Toast、Snackbar,是没有生命周期控制 的(只负责显示 ),即使Activiy、Fragment不可见 (被销毁、回到后台),也还在显示 。我增加了生命周期消息显示 ,仅在Activiy、Fragment可见时显示。
指定消息的显示
指定消息的显示 ,是使用Toast,还是Snackbar,目前默认 为Toast。
- 全局消息指定 ,在
BaseGlobalMessageInitializer类设置。 - 生命周期消息指定 ,在
App级的AppViewsActivity、AppViewsFragment、AppViewsDialogFragment重写messageCollector的实现。
使用消息
kotlin
// 全局消息,不受Activiy、Fragment的生命周期影响。
MessageManager.showGlobalMessage("Global Message")
// 生命周期消息,受viewModel的Activiy、Fragment的生命周期影响。
viewModel.showMessage("Short Message")
viewModel.showMessage("Long Message", isShort = false)
StateView
StateView为包含多个状态形式View 的接口,状态包括:Loading、Error、Empty、Success。
定制UI
定制UI :目前实现StateView接口的类是DefaultStateView。
- 小改 :
DefaultStateView,默认 实现了Loading、Error、Empty状态 的View,可修改指定某个 来定制UI。 - 大改 :可通过修改
createAppStateView()、createAppListStateView()方法,返回StateView接口的其它实现类。
原理
- 列表使用 :是使用BaseRecyclerViewAdapterHelper的
stateView实现,底层原理是给RecyclerView的Adapter添加了一条Item布局 。Empty状态 ,是通过返回的列表数据是否为空 来判断的,详细使用看BaseRefreshLoadMoreHelper。 - 普通使用 :是使用Base类
Activity、Fragment的getStateViewReplaceView()方法实现,底层原理是给此方法返回的View替换显示为StateView 。Empty状态 ,目前未判断 ,如需修改请看BaseViewModel.requestAsyncBase()扩展方法。
使用
- 列表使用 :已封装好 ,目前已支持SmartRefreshLayout、SwipeRefreshLayout两个控件,详细使用请看
SmartRefreshLoadMoreHelper、SwipeRefreshLoadMoreHelper。 - 普通使用 :需要使用
BaseViewModel.requestAsyncBase()扩展方法定制 。- 配置 :
Activity、Fragment需要实现getStateViewReplaceView(),此为StateView要替换的View (用于实现替换显示StateView 时,隐藏此View ),可通过覆写 此方法来修改StateView的显示范围 ,如果不覆写默认 为此Activity、Fragment的root根布局 。详细使用,请看通用级 的BaseViewsActivity、BaseViewsFragment等。 - 使用 :请求异步 的UI 每人的需求不同(如:
Error状态,有人想要显示Error重试布局 ,有人想要只需要消息提示 ),定制 详细使用,请看BaseViewModel.requestAsyncBase()扩展方法。
- 配置 :
刷新、自动加载
原理
- 刷新 :是使用SmartRefreshLayout或SwipeRefreshLayout实现。
- 自动加载 :是使用BaseRecyclerViewAdapterHelper的
setTrailingLoadStateAdapter()实现,底层原理是通过ConcatAdapter.addAdapter(adapter)增加了尾Adapter ,详细使用请看BaseRefreshLoadMoreHelper。
使用
- UI层 :
Activity、Fragment实现类,需要使用SmartRefreshLoadMoreHelper或SwipeRefreshLoadMoreHelper初始化,详细看ShopFragment。 - ViewModel层 :
ViewModel实现类,需要实现RefreshRepositoryOwner接口,其onRefreshRepository()方法需要返回刷新/刷新加载仓库。 - Repository层 :
Repository实现类,需要实现RefreshRepository(仅刷新 )或RefreshLoadMoreRepository(刷新加载)接口。Repository实现类,需要继承PageKeyedMemoryRefreshLoadMoreRepository(通过page加载 )或ItemKeyedMemoryRefreshLoadMoreRepository(通过Item加载)类。
网络
一个公司,可能有多个网络规则 ,可创建实现BaseNetworkModel接口的XXXBaseNetworkModel 类,来实现此规则定制 功能,后续只需使用此类即可。目前项目内有2个规则 案例,请看ApiOpenBaseNetworkModel、AppBaseNetworkModel类。
定义XXXBaseNetworkModel
以开源接口ApiOpen为例,其返回格式模板为:
json
{"code": 200, "message": "成功!", "result": "string"}
code为200代表公司的规则成功 ,message为提示的消息 ,result为结果(类型任意),以此创建类如下:
kotlin
@Serializable
data class ApiOpenBaseNetworkModel<T>(val code: Int, val message: String, val result: T? = null) :
BaseNetworkModel<T> {
override fun isRuleSuccess() = code == 200
override fun code() = code
override fun message() = message
override fun data() = result
}
使用XXXBaseNetworkModel
kotlin
interface FakeNetworkLoginApi {
/**
* 登录
*/
@POST("api/login")
@FormUrlEncoded
suspend fun login(
@Field("account") account: String,
@Field("password") password: String,
): ApiOpenBaseNetworkModel<FakeNetworkUser>
}
login()方法,其返回值为ApiOpenBaseNetworkModel,其泛型为json模板的result值。调用如下:
kotlin
loginApi.login(account, password)
loginApi.login()方法,其返回值为ApiOpenBaseNetworkModel,这个数据不仅包含了json模块的全部信息 ,而且我们还得需要判断其是否公司规则成功。
可以使用以下转换方法,转为自己想要的结果。
转换XXXBaseNetworkModel
kotlin
loginApi.login(account, password).toRuleSuccessData()
toRuleSuccessData()方法,将ApiOpenBaseNetworkModel,转换为公司规则成功 ,并且返回其内部的result ,并且此返回值不为空。
目前支持的,所有转换方法,如下:
kotlin
/**
* 网络成功-规则成功-内部数据-不可空
*/
fun <T> BaseNetworkModel<T>.toRuleSuccessData(): T {
if (!isRuleSuccess()) {
throw RuleException(code(), message())
}
return data()!!
}
/**
* 网络成功-规则成功-内部数据-可空
*/
fun <T> BaseNetworkModel<T>.toRuleSuccessDataNullable(): T? {
if (!isRuleSuccess()) {
throw RuleException(code(), message())
}
return data()
}
/**
* 网络成功-规则成功-全部数据
*/
fun <T> BaseNetworkModel<T>.toRuleSuccess(): BaseNetworkModel<T> {
if (!isRuleSuccess()) {
throw RuleException(code(), message())
}
return this
}
// 网络成功-全部数据。则不需要调用此转换方法,直接返回即可。
可根据自己的需求,使用自己想要的转换方法,一般为toRuleSuccessData()(网络成功-规则成功-内部数据-不可空)。
未来支持
- 支持Compose
- 优化是否Login相关逻辑
- 优化WebView相关逻辑
其它
三方库
自己
三方
参考
项目链接: architecture-android,欢迎大家点赞、收藏,以方便您后续查看。