- 支持配置变更后的还原
- 屏幕旋转
- 亮暗主题切换
- 语言切换(国际化)
- 字体大小更改
- 分屏
- ...
- 支持进程杀死后的还原
- 项目架构:模块化+组件化+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,欢迎大家点赞、收藏,以方便您后续查看。
扫码下载
| Themes | Light | Dark |
|---|---|---|
| 抖音主题 | ||
| 动态主题1 | ||
| 动态主题2 |
下载项目并运行
git clone [email protected]:zrq1060/architecture-android.git本项目任何页面,都支持如下:
- 配置变更后的还原(屏幕旋转、亮暗模式切换、语言切换、字体大小更改、分屏等),配置变更会导致
Activity、Fragment会重新创建新的。- 进程被杀死后的还原(可打开,开发者选项-后台进程限制-不允许后台进程,以更好的测试进程被杀死。开启后,可在后台多打开一些无关的app,再切换打开此app即可演示此效果)。
本项目,目前仅支持如下功能:
-
登录页:登录账号(手机号、邮箱)为 【任意】,登录密码(验证码、密码)为 【123456】。可断网,或输入错误密码,查看页面效果。
-
Home首页:顶部栏目的排序(长按首页-顶部栏目)
-
Main主页:好友、商场栏目的切换(长按主页-底部栏目第2个)
-
Shop商城页:支持刷新、自动加载,点击商城条目模拟的商城列表数据的增、删、改操作。可断网,查看页面效果。
在此跟着上面,操作App支持的功能(记得开启屏幕自动旋转、切换亮暗模式、点击商城Item),以演示上面功能效果。
1、修改项目根目录下gradle.properties内isFeatureSingle为true,并Sync同步Gradle。
isFeatureSingle = true2、执行安装全部命令
点击右侧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有抖音、西瓜视频、今日头条、飞书、剪映等。
上面官方(标准版)的模块划分,导致内部的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等。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模块间通信,使用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模块,步骤如下:
修改项目根目录下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库,目前的项目架构图 如下:
以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别名即可。
以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,是没有生命周期控制的(只负责显示),即使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为包含多个状态形式View的接口,状态包括:Loading、Error、Empty、Success。
定制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类。
以开源接口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
}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模块的全部信息,而且我们还得需要判断其是否公司规则成功。
可以使用以下转换方法,转为自己想要的结果。
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,欢迎大家点赞、收藏,以方便您后续查看。