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 |
快速介绍
下载
下载项目并运行
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
。
isFeatureSingle = true
2、执行安装全部命令
点击右侧Gradle
-Tasks
-install
-installDebug
,或执行如下命令:
.\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
,来演示此移除功能。
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
等。core
architecture
:架构相关,包含一些基础类,如:最外层: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
模块为例规则如下:
定义
interface HomeRouter {
fun createHomeFragment(): Fragment
}
此HomeRouter
接口为home
模块对外暴露的可供其它模块调用部分,在router
模块内定义,如果还有其它的,可继续在此接口内添加,如:createXXXFragment
、startXXXActivity
方法等。
真的实现
class DefaultHomeRouter : HomeRouter {
override fun createHomeFragment(): Fragment = HomeFragment.newInstance()
}
此DefaultHomeRouter
类为HomeRouter
接口真的实现,在home
模块内实现。
假的实现
class FakeHomeRouter : HomeRouter {
override fun createHomeFragment(): Fragment = AppTestFragment.newInstance("Home")
}
此FakeHomeRouter
类为HomeRouter
接口假的实现,在router
模块的router-reflect
内实现,内部使用的AppTestFragment
仅是为了显示时占位用。
说明:
Router-Dagger实现:使用
Dagger
找HomeRouter
的实现(目前提供的是DefaultHomeRouter
),如果找不到会报错。Router-反射实现:使用反射直接找
DefaultHomeRouter
,如果找不到会直接使用FakeHomeRouter
,不会报错。
调用
val homeFragment = Router.Home.createHomeFragment()
单独运行Feature模块介绍
如果你只负责某个Feature
模块,或者想更解耦、更快的测试你的功能,你可以使用此单独运行Feature模块,步骤如下:
修改配置
修改项目根目录下gradle.properties
内isFeatureSingle
为true
,并Sync
同步Gradle
。
isFeatureSingle = true
说明:
isFeatureSingle
:为是否单独运行Feature模块。如果开启,则Router
使用反射实现,以使其调用其它模块没有时不会报错,而是使用Fake
的实现(如:占位显示)。
添加测试入口点
此功能需要配合使用我的TestPoint库来实现,添加测试入口点,即会在测试列表页增加一个按钮,点击按钮跳转到此Activity
、Fragment
,定制按钮点击等详细使用请看TestPoint。
在目标类上添加TestEntryPoint
注解,如ShopFragment
:
@TestEntryPoint("商城")
class ShopFragment{
}
运行
单个运行:
选择上面的一个,并运行,如:选择douyin.shop.feature.shop
,则运行抖音的商城功能(可以测试商城的点击Item功能等)。
多个运行:
点击右侧Gradle
-Tasks
-install
-installDebug
,或执行如下命令:
.\gradlew installDebug
执行完后,会在手机桌面出现所有Feature模块的App
(如快速介绍-单独运行Feature模块演示图所示),点击某个即可测试某单个Feature模块。
模块内架构
官方架构
模块内架构,使用官方的推荐架构,有助于构建强大而优质的应用。
应用架构相关,请看官方的 应用架构指南。
官方的架构概述图 如下:
官方架构分为了:UI层、Domain层(可选)、Data数据层。
项目架构
本项目,目前没有使用Domain
层,也没有使用Room
库,目前的项目架构图 如下:
使用
Activity、Fragment
以demo
模块的MainActivity
为例:
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
为例:
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
的实现。
使用消息
// 全局消息,不受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为例,其返回格式模板为:
{"code": 200, "message": "成功!", "result": "string"}
code
为200代表公司的规则成功,message
为提示的消息,result
为结果(类型任意),以此创建类如下:
@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
interface FakeNetworkLoginApi {
/**
* 登录
*/
@POST("api/login")
@FormUrlEncoded
suspend fun login(
@Field("account") account: String,
@Field("password") password: String,
): ApiOpenBaseNetworkModel<FakeNetworkUser>
}
login()
方法,其返回值为ApiOpenBaseNetworkModel
,其泛型为json模板的result值。调用如下:
loginApi.login(account, password)
loginApi.login()
方法,其返回值为ApiOpenBaseNetworkModel
,这个数据不仅包含了json模块的全部信息,而且我们还得需要判断其是否公司规则成功。
可以使用以下转换方法,转为自己想要的结果。
转换XXXBaseNetworkModel
loginApi.login(account, password).toRuleSuccessData()
toRuleSuccessData()
方法,将ApiOpenBaseNetworkModel
,转换为公司规则成功,并且返回其内部的result,并且此返回值不为空。
目前支持的,所有转换方法,如下:
/**
* 网络成功-规则成功-内部数据-不可空
*/
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,欢迎大家点赞、收藏,以方便您后续查看。