大型 Android 项目架构最佳实践
作者:苏火火
juejin.cn/post/7223767530981867557
前言
之前一直想写个 WanAndroid 项目来巩固自己对 Kotlin+Jetpack+协程
等知识的学习,但是一直没有时间。这里重新行动起来,从项目搭建到完成前前后后用了两个月时间,平常时间比较少,基本上都是只能利用零碎的时间来写。但不再是想写一个简单的玩安卓项目,我从多个大型项目中学习和吸取经验,从0到1打造一个符合大型项目的架构模式。
这或许是一个缩影,但是麻雀虽小,五脏俱全,这肯定能给大家带来一些想法和思考。当然这个项目的功能并未全部完善,因为我们的目的不是造一个 WanAndroid
客户端,而是学习搭建和使用 Kotlin+协程+Flow+Retrofit+Jetpack+MVVM+组件化+模块化+短视频 这一种架构,更好的提升自己。
一、项目简介
项目采用 Kotlin 语言编写,结合 Jetpack 相关控件, Navigation,Lifecyle,DataBinding,LiveData,ViewModel
等搭建的 MVVM 架构模式;通过组件化,模块化拆分,实现项目更好解耦和复用, ARouter
实现模块间通信;使用 协程+Flow+Retrofit+OkHttp
优雅地实现网络请求;通过 mmkv,Room
数据库等实现对数据缓存的管理;使用谷歌 ExoPlayer
实现短视频播放;使用 Glide
完成图片加载;通过 WanAndroid 提供的 API 实现的一款玩安卓客户端。
项目使用 MVVM架构模式,基本上遵循 Google 推荐的架构,对于 Repository,Google 认为 ViewModel 仅仅用来做数据的存储,数据加载应该由 Repository 来完成。通过 Room 数据库实现对数据的缓存,在无网络或者弱网的情况下优先展示缓存数据。
项目地址 :https://github.com/suming77/SumTea_Android
二、项目详情
2.1 基础架构
(1) BaseActicity
通过单一职责原则,实现职能分级,使用者只需要按需继承即可。
BaseActivity
:封装了通用的 init 方法,初始化布局,加载弹框等方法,提供了原始的添加布局的方式;BaseDataBindActivity
:继承自 BaseActivity,通过 dataBinding 绑定布局,利用泛型参数反射创建布局文件实例,获取布局 view,不再需要 findViewById();
val type = javaClass.genericSuperclass
val vbClass: Class<DB> = type!!.saveAs<ParameterizedType>().actualTypeArguments[0].saveAs()
val method = vbClass.getDeclaredMethod("inflate", LayoutInflater::class.java)
mBinding = method.invoke(this, layoutInflater)!!.saveAsUnChecked()
setContentView(mBinding.root)
BaseMvvmActivity
: 继承自 BaseDataBindActivity,通过泛型参数反射自动创建 ViewModel 实例,更方便使用 ViewModel 实现网络请求。
val argument = (this.javaClass.genericSuperclass as ParameterizedType).actualTypeArguments
mViewModel = ViewModelProvider(this).get(argument[1] as Class<VM>)
(2) BaseFragment
BaseFragment 的封装与上面的 BaseActivity 类似。
(3) BaseRecyclerViewAdapter
BaseRecyclerViewAdapter
:封装了 RecyclerViewAdapter 基类,实现提供创建 ViewHolder 能力,提供添加头尾布局能力,通用的 Item 点击事件,提供 dataBinding 能力,不再需要 findViewById(),提供了多种刷新数据的方式,全局刷新,局部刷新等等。BaseMultiItemAdapter
:提供了实现多种不同布局的 Adapter,根据不同的 ViewType 实现不同的 ViewBinding,再创建返回不同的 ViewHolder。
(4) Ext拓展类
项目中提供了大量控件扩展类,能够快速开发,提高效率:
ResourceExt
: 资源文件扩展类;TextViewExt
: TextView 扩展类;SpanExt
: Span 拓展类,实现多种 Span 效果;RecyclerViewExt
:一行代码快速实现添加垂直分割线,网格分割线;ViewExt
: View 扩展类,实现点击防抖,添加间距,设置宽度,设置可见性等等;EditTextExt
: 通过 Flow 构建输入框文字变化流,filter{} 实现数据过滤,避免无效请求,debounce() 实现防抖;GsonExt
: 一行代码快速实现 Bean 和 Json 之间的相互转换。
//将Bean对象转换成json字符串
fun Any.toJson(includeNulls: Boolean = true): String {
return gson(includeNulls).toJson(this)
}
//将json字符串转换成目标Bean对象
inline fun <reified T> String.toBean(includeNulls: Boolean = true): T {
return gson(includeNulls).fromJson(this, object : TypeToken<T>() {}.type)
}
(5) xlog
XLog 是一个高性能文本存储方案,在真实环境中经受了微信数亿级别的考验,具有很好的稳定性。由于其是使用C语言来实现的,占用性能忧、内存小,存储速度快等优点,支持多线程,甚至多进程的使用,支持定期删除日志,同时,拥有特定算法,进行了文件的压缩,甚至可以配置文件加密。
利用 Xlog 建设客户端运行时日志体系,远程日志按需回捞,以打点的形式记录关键执行流程。
2.2 Jetpack组件
Android Jetpack是一组 Android 软件组件、工具和指南,它们可以帮助开发者构建高质量、稳定的 Android 应用程序。Jetpack 中包含多个库,它们旨在解决 Android 应用程序开发中的常见问题,并提供一致的 API 和开发体验。
项目中仅仅使用到上图的一小部分组件。
(1) Navtgation
Navtgation 作为构建应用内界面的框架,重点是让单 Activity 应用成为首选架构(一个应用只需一个 Activity),它的定位是页面路由。
项目中主页分为5个 Tab,主要为首页、分类、体系、我的。使用 BottomNavigationView + Navigation
来搭建。通过 menu 来配置底部菜单,通过 NavHostFragment
来配置各个 Fragment。同时解决了 Navigation 与 BottomNavigationView
结合使用时,点击 tab,Fragment 每次都会重新创建问题。解决方法是自定义 FragmentNavigator
,将内部 replace() 替换为 show()/hide()。
(2) ViewBinding&DataBinding
ViewBinding
的出现就是不再需要写 findViewById();DataBinding
是一种工具,它解决了 View 和数据之间的双向绑定;减少代码模板,不再需要写findViewById();释放 Activity/Fragment,可以在 XML 中完成数据,事件绑定工作,让 Activity/Fragment 更加关心核心业务;数据绑定空安全,在 XML 中绑定数据它是空安全的,因为 DataBinding 在数据绑定上会自动装箱和空判断,所以大大减少了 NPE 问题。
(3) ViewModel
ViewModel
具备生命感知能力的数据存储组件。页面配置更改数据不会丢失,数据共享(单 Activity 多 Fragment 场景下的数据共享),以生命周期的方式管理界面相关的数据,通常和 DataBinding 配合使用,为实现 MVVM 架构提供了强有力的支持。
(4) LiveData
LiveData
是一个具有生命周期感知能力的数据订阅,分发组件。支持共享资源(一个数据支持被多个观察者接收的),支持粘性事件的分发,不再需要手动处理生命周期(和宿主生命周期自动关联),确保界面符合数据状态。在底层数据库更改时通知 View。
(5) Room
一个轻量级 orm 数据库,本质上是一个 SQLite 抽象层。使用更加简单(Builder 模式,类似 Retrofit),通过注解的形式实现相关功能,编译时自动生成实现类 IMPL。
这里主要用于首页视频列表缓存数据,与 LiveData 和 Flow 结合处理可以避免不必要的 NPE,可以监听数据库表中的数据的变化,也可以和 RXJava 的 Observer 使用,一旦发生了 insert,update,delete等操作,Room 会自动读取表中最新的数据,发送给 UI 层,刷新页面。
Room 库架构的示意图:
Room 包含三个主要组件:
数据库类:用于保存数据库并作为应用持久性数据底层连接的主要访问点; 数据实体:用于表示应用的数据库中的表; 数据访问对象 (DAO):提供您的应用可用于查询、更新、插入和删除数据库中的数据的方法。
/**
* Dao
**/
@Dao
interface VideoListCacheDao {
//插入单个数据
@Insert(entity = VideoInfo::class, onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(videoInfo: VideoInfo)
//插入多个数据
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(videoList: MutableList<VideoInfo>)
//删除指定item 使用主键将传递的实体实例与数据库中的行进行匹配。如果没有具有相同主键的行,则不会进行任何更改
@Delete
fun delete(videoInfo: VideoInfo): Int
//删除表中所有数据
@Query("DELETE FROM $TABLE_VIDEO_LIST")
suspend fun deleteAll()
//更新某个item,不指定的entity也可以,会根据你传入的参数对象来找到你要操作的那张表
@Update
fun update(videoInfo: VideoInfo): Int
//根据id更新数据
@Query("UPDATE $TABLE_VIDEO_LIST SET title=:title WHERE id=:id")
fun updateById(id: Long, title: String)
//查询所有数据
@Query("SELECT * FROM $TABLE_VIDEO_LIST")
fun queryAll(): MutableList<VideoInfo>?
//根据id查询某个数据
@Query("SELECT * FROM $TABLE_VIDEO_LIST WHERE id=:id")
fun query(id: Long): VideoInfo?
//通过LiveData以观察者的形式获取数据库数据,可以避免不必要的NPE
@Query("SELECT * FROM $TABLE_VIDEO_LIST")
fun queryAllLiveData(): LiveData<List<VideoInfo>>
}
/**
* Database
**/
@Database(entities = [VideoInfo::class], version = 1, exportSchema = false)
abstract class SumDataBase : RoomDatabase() {
//抽象方法或者抽象类标记
abstract fun videoListDao(): VideoListCacheDao
companion object {
private var dataBase: SumDataBase? = null
//同步锁,可能在多个线程中同时调用
@Synchronized
fun getInstance(): SumDataBase {
return dataBase ?: Room.databaseBuilder(SumAppHelper.getApplication(), SumDataBase::class.java, "SumTea_DB")
//是否允许在主线程查询,默认是false
.allowMainThreadQueries()
.build()
}
}
}
注意:Room 数据库中的 Dao 中定义数据库操作的方法一定要确保用法正确,否则会导致 Room 编译时生成的实现类错误,编译不通过等问题。
2.3 网络请求库
项目的网络请求封装提供了两种方式的实现,一种是协程+Retrofit+ViewModel+Repository
,像官网那样加一层 Repository
去管理网络请求调用;另一种方式是通过 Flow 流配合 Retrofit 更优雅实现网络请求,对比官网的做法更加简洁。
(1) Retrofit+协程+Repository
/**
* BaseViewModel
**/
open class BaseViewModel : ViewModel() {
//需要运行在协程作用域中
suspend fun <T> safeApiCall(
errorBlock: suspend (Int?, String?) -> Unit,
responseBlock: suspend () -> T?
): T? {
try {
return responseBlock()
} catch (e: Exception) {
e.printStackTrace()
LogUtil.e(e)
val exception = ExceptionHandler.handleException(e)
errorBlock(exception.errCode, exception.errMsg)
}
return null
}
}
/**
* BaseRepository
**/
open class BaseRepository {
//IO中处理请求
suspend fun <T> requestResponse(requestCall: suspend () -> BaseResponse<T>?): T? {
val response = withContext(Dispatchers.IO) {
withTimeout(10 * 1000) {
requestCall()
}
} ?: return null
if (response.isFailed()) {
throw ApiException(response.errorCode, response.errorMsg)
}
return response.data
}
}
/**
* HomeRepository
**/
class HomeRepository : BaseRepository() {
//项目tab
suspend fun getProjectTab(): MutableList<ProjectTabItem>? {
return requestResponse {
ApiManager.api.getProjectTab()
}
}
}
/**
* HomeViewModel
**/
class HomeViewModel : BaseViewModel() {
//请求项目Tab数据
fun getProjectTab(): LiveData<MutableList<ProjectTabItem>?> {
return liveData {
val response = safeApiCall(errorBlock = { code, errorMsg ->
TipsToast.showTips(errorMsg)
}) {
homeRepository.getProjectTab()
}
emit(response)
}
}
}
(2) Flow优雅实现网络请求
Flow 其实和 RxJava 很像,非常方便,用它来做网络请求更加简洁。
suspend fun <T> requestFlowResponse(
errorBlock: ((Int?, String?) -> Unit)? = null,
requestCall: suspend () -> BaseResponse<T>?,
showLoading: ((Boolean) -> Unit)? = null
): T? {
var data: T? = null
//1.执行请求
flow {
val response = requestCall()
if (response?.isFailed() == true) {
errorBlock.invoke(response.errorCode, response.errorMsg)
}
//2.发送网络请求结果回调
emit(response)
//3.指定运行的线程,flow {}执行的线程
}.flowOn(Dispatchers.IO)
.onStart {
//4.请求开始,展示加载框
showLoading?.invoke(true)
}
//5.捕获异常
.catch { e ->
e.printStackTrace()
LogUtil.e(e)
val exception = ExceptionHandler.handleException(e)
errorBlock?.invoke(exception.errCode, exception.errMsg)
}
//6.请求完成,包括成功和失败
.onCompletion {
showLoading?.invoke(false)
//7.调用collect获取emit()回调的结果,就是请求最后的结果
}.collect {
data = it?.data
}
return data
}
2.4 图片加载库
图片加载利用 Glide 进行了简单的封装,对 ImageView 做扩展函数处理:
//加载图片,开启缓存
fun ImageView.setUrl(url: String?) {
if (ActivityManager.isActivityDestroy(context)) {
return
}
Glide.with(context).load(url)
.placeholder(R.mipmap.default_img) // 占位符,异常时显示的图片
.error(R.mipmap.default_img) // 错误时显示的图片
.skipMemoryCache(false) //启用内存缓存
.diskCacheStrategy(DiskCacheStrategy.RESOURCE) //磁盘缓存策略
.into(this)
}
//加载圆形图片
fun ImageView.setUrlCircle(url: String?) {
if (ActivityManager.isActivityDestroy(context)) return
//请求配置
val options = RequestOptions.circleCropTransform()
Glide.with(context).load(url)
.placeholder(R.mipmap.default_head)
.error(R.mipmap.default_head)
.skipMemoryCache(false) //启用内存缓存
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.apply(options)// 圆形
.into(this)
}
//加载圆角图片
fun ImageView.setUrlRound(url: String?, radius: Int = 10) {
if (ActivityManager.isActivityDestroy(context)) return
Glide.with(context).load(url)
.placeholder(R.mipmap.default_img)
.error(R.mipmap.default_img)
.skipMemoryCache(false) // 启用内存缓存
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.transform(CenterCrop(), RoundedCorners(radius))
.into(this)
}
//加载Gif图片
fun ImageView.setUrlGif(url: String?) {
if (ActivityManager.isActivityDestroy(context)) return
Glide.with(context).asGif().load(url)
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.DATA)
.placeholder(R.mipmap.default_img)
.error(R.mipmap.default_img)
.into(this)
}
/**
* 设置图片高斯模糊
* @param radius 设置模糊度(在0.0到25.0之间),默认25
* @param sampling 图片缩放比例,默认1
*/
fun ImageView.setBlurView(url: String?, radius: Int = 25, sampling: Int = 1) {
if (ActivityManager.isActivityDestroy(context)) return
//请求配置
val options = RequestOptions.bitmapTransform(BlurTransformation(radius, sampling))
Glide.with(context)
.load(url)
.placeholder(R.mipmap.default_img)
.error(R.mipmap.default_img)
.apply(options)
.into(this)
}
修复 Glide 的图片裁剪和 ImageView 的
scaleType
的冲突问题,Bitmap 会先圆角裁剪,再加载到 ImageView 中,如果 Bitmap 图片尺寸大于 ImageView 尺寸,则会看不到,使用CenterCrop()
重载,会先将 Bitmap 居中裁剪,再进行圆角处理,这样就能看到。提供了 GIF 图加载和图片高斯模糊效果功能。
2.5 WebView
我们都知道原生的 WebView 存在很多问题,使用腾讯X5内核 WebView 进行封装,兼容性,稳定性,安全性,速度都有很大的提升。
项目中使用 WebView 展示文章详情页。
2.6 MMKV
MMKV 是基于 mmap 内存映射的 key-value 组件,底层序列化 / 反序列化使用 protobuf 实现,性能高,稳定性强。使用简单,支持多进程。
在 App 启动时初始化 MMKV,设定 MMKV 的根目录(files/mmkv/),例如在 Application 里:
public void onCreate() {
super.onCreate();
String rootDir = MMKV.initialize(this);
LogUtil.e("mmkv root: " + rootDir);
}
MMKV 提供一个全局的实例,可以直接使用:
import com.tencent.mmkv.MMKV;
//……
MMKV kv = MMKV.defaultMMKV();
kv.encode("bool", true);
boolean bValue = kv.decodeBool("bool");
kv.encode("int", Integer.MIN_VALUE);
int iValue = kv.decodeInt("int");
kv.encode("string", "Hello from mmkv");
String str = kv.decodeString("string");
循环写入随机的 int 1k 次,有如下性能对比:
2.7 ExoPlayer视频播放器
ExoPlayer
是 google 推出的开源播放器,主要是集成了 Android 提供的一套解码系统来解析视频和音频,将 MediaCodec
封装地非常完善,形成了一个性能优越,播放稳定性较好的一个开发播放器,支持更多的视频播放格式(包含 DASH 和 SmoothStreaming
,这2种 MediaPlayer
不支持),通过组件化自定义播放器,方便扩展定制,持久的高速缓存,另外 ExoPlayer 包大小轻便,接入简单。
项目中使用 ExoPlayer 实现防抖音短视频播放:
class VideoPlayActivity : BaseDataBindActivity<ActivityVideoPlayBinding>() {
//创建exoplayer播放器实例,视屏画面渲染工厂类,语音选择器,缓存控制器
private fun initPlayerView(): Boolean {
//创建exoplayer播放器实例
mPlayView = initStylePlayView()
// 创建 MediaSource 媒体资源 加载的工厂类
mMediaSource = ProgressiveMediaSource.Factory(buildCacheDataSource())
mExoPlayer = initExoPlayer()
//缓冲完成自动播放
mExoPlayer?.playWhenReady = mStartAutoPlay
//将显示控件绑定ExoPlayer
mPlayView?.player = mExoPlayer
//资源准备,如果设置 setPlayWhenReady(true) 则资源准备好就立马播放。
mExoPlayer?.prepare()
return true
}
//初始化ExoPlayer
private fun initExoPlayer(): ExoPlayer {
val playerBuilder = ExoPlayer.Builder(this).setMediaSourceFactory(mMediaSource)
//视频每一帧的画面如何渲染,实现默认的实现类
val renderersFactory: RenderersFactory = DefaultRenderersFactory(this)
playerBuilder.setRenderersFactory(renderersFactory)
//视频的音视频轨道如何加载,使用默认的轨道选择器
playerBuilder.setTrackSelector(DefaultTrackSelector(this))
//视频缓存控制逻辑,使用默认的即可
playerBuilder.setLoadControl(DefaultLoadControl())
return playerBuilder.build()
}
//创建exoplayer播放器实例
private fun initStylePlayView(): StyledPlayerView {
return StyledPlayerView(this).apply {
controllerShowTimeoutMs = 10000
setKeepContentOnPlayerReset(false)
setShowBuffering(SHOW_BUFFERING_NEVER)//不展示缓冲view
resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT
useController = false //是否使用默认控制器,如需要可参考PlayerControlView
// keepScreenOn = true
}
}
//创建能够 边播放边缓存的 本地资源加载和http网络数据写入的工厂类
private fun buildCacheDataSource(): DataSource.Factory {
//创建http视频资源如何加载的工厂对象
val upstreamFactory = DefaultHttpDataSource.Factory()
//创建缓存,指定缓存位置,和缓存策略,为最近最少使用原则,最大为200m
mCache = SimpleCache(
application.cacheDir,
LeastRecentlyUsedCacheEvictor(1024 * 1024 * 200),
StandaloneDatabaseProvider(this)
)
//把缓存对象cache和负责缓存数据读取、写入的工厂类CacheDataSinkFactory 相关联
val cacheDataSinkFactory = CacheDataSink.Factory().setCache(mCache).setFragmentSize(Long.MAX_VALUE)
return CacheDataSource.Factory()
.setCache(mCache)
.setUpstreamDataSourceFactory(upstreamFactory)
.setCacheReadDataSourceFactory(FileDataSource.Factory())
.setCacheWriteDataSinkFactory(cacheDataSinkFactory)
.setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)
}
}
2.8 组件化&模块化
组件化&模块化有利于业务模块分离,高内聚,低耦合,代码边界清晰。有利于团队合作多线开发,加快编译速度,提高开发效率,管理更加方便,利于维护和迭代。
宿主 App 中只有一个 Application,整个业务被拆分为各个 mod 模块和 lib 组件库。对一些功能组件进行封装抽取为 lib,给上层提供依赖。mod 模块之间没有任务依赖关系,通过 Arouter 进行通信。
(1) 模块化
项目中通过以业务为维度把 App 拆分成主页模块,登录模块,搜索模块,用户模块,视频模块等,相互间不可以访问不可以作为依赖,与此同时他们共同依赖于基础库,网络请求库,公共资源库,图片加载库等。如果还需要使用到启动器组件、Banner组件、数据库Room组件等则单独按需添加。
APP 壳工程负责打包环境,签名,混淆规则,业务模块集成,APP 主题等配置等工作,一般不包含任何业务。
(2) 组件化
模块化和组件化最明显的区别就是模块相对组件来说粒度更大。一个模块中可能包含多个组件。在划分的时候,模块化是业务导向,组件化是功能导向。组件化是建立在模块化思想上的一次演进。
项目中以功能维度拆分了启动器组件、Banner组件、数据库Room组件等组件。模块化&组件化拆分后工程图:
(3) 组件间通信
组件化之后就无法直接访问其他模块的类和方法,这是个比较突出的问题,就像原来可以直接使用 LogintManager
来拉起登录,判断是否已登录,但是这个类已经被拆分到了 mod_login 模块下,而业务模块之间是不能互相作为依赖的,所以无法在其他模块直接使用 LogintManager
。
主要借助阿里的路由框架 ARouter 实现组件间通信,把对外提供的能力,以接口的形式暴露出去。
比如在公共资源库中的 service 包下创建 ILoginService
,提供对外暴露登录的能力,在 mod_login 模块中提供 LoginServiceImpl
实现类,任意模块就可以通过 LoginServiceProvider
使用 iLoginService
对外提供暴露的能力。
公共资源库中创建 ILoginService,提供对外暴露登录的能力。
interface ILoginService : IProvider {
//是否登录
fun isLogin(): Boolean
//跳转登录页
fun login(context: Context)
//登出
fun logout(
context: Context,
lifecycleOwner: LifecycleOwner?,
observer: Observer<Boolean>
)
}
mod_login 模块中 LoginService 提供 ILoginService 的具体实现。
@Route(path = LOGIN_SERVICE_LOGIN)
class LoginService : ILoginService {
//是否登录
override fun isLogin(): Boolean {
return UserServiceProvider.isLogin()
}
//跳转登录页
override fun login(context: Context) {
context.startActivity(Intent(context, LoginActivity::class.java))
}
//登出
override fun logout(
context: Context,
lifecycleOwner: LifecycleOwner?,
observer: Observer<Boolean>
) {
val scope = lifecycleOwner?.lifecycleScope ?: GlobalScope
scope.launch {
val response = ApiManager.api.logout()
if (response?.isFailed() == true) {
TipsToast.showTips(response.errorMsg)
return@launch
}
LogUtil.e("logout${response?.data}", tag = "smy")
observer.onChanged(response?.isFailed() == true)
login(context)
}
}
override fun init(context: Context?) {}
}
公共资源库中创建 LoginServiceProvider,获取 LoginService,提供使用方法。
object LoginServiceProvider {
//获取loginService实现类
val loginService = ARouter.getInstance().build(LOGIN_SERVICE_LOGIN).navigation() as? ILoginService
//是否登录
fun isLogin(): Boolean {
return loginService.isLogin()
}
//跳转登录
fun login(context: Context) {
loginService.login(context)
}
//登出
fun logout(
context: Context,
lifecycleOwner: LifecycleOwner?,
observer: Observer<Boolean>
) {
loginService.logout(context, lifecycleOwner, observer)
}
}
那么其他模块就可以通过 LoginServiceProvider
使用 iLoginService
对外提供暴露的能力。虽然看起来这么做会显得更复杂,单一工程可能更加适合我们,每个类都能直接访问,每个方法都能直接调用,但是我们不能局限于单人开发的环境,在实际场景上多人协作是常态,模块化开发是主流。
(4) Module单独运行
使得模块可以在集成和独立调试之间切换特性。在打包时是 library,在调试是 application。
在 config.gradle
文件中加入isModule
参数:
//是否单独运行某个module
isModule = false
在每个 Module 的 build.gradle 中加入 isModule 的判断,以区分是 application 还是 library:
// 组件模式和基础模式切换
def root = rootProject.ext
if (root.isModule) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
android {
sourceSets {
main {
if (rootProject.ext.isModule) {
manifest.srcFile 'src/main/debug/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
//library模式下排除debug文件夹中的所有Java文件
java {
exclude 'debug/**'
}
}
}
}
}
将通过修改 SourceSets 中的属性,可以指定需要被编译的源文件,如果是library,则编译 manifest 下 AndroidManifest.xml
,反之则直接编译 debug 目录下 AndroidManifest.xml,同时加入Application
和intent-filter
等参数。
存疑:至于模块单独编译单独运行,这种是一个伪需求,实际上必然存在多个模块间通信的场景。不然跨模块的服务提取和获取,初始化任务,模块间的联合测试该怎么解决呢?一个模块运行后需要和其他的模块通信,比如对外提供服务,获取服务,与之相关联的模块如果没有运行起来的话是无法使用的。
与此同时还需要在 suorceSets 下维护两套 AndoidManifest 以及 Javasource 目录,这个不仅麻烦而且每次更改都需要同步一段时间。所以这种流传的模块化独立编译的形式,是否真的适合就仁者见仁了。
三、写在最后
如需要更详细的代码可以到项目源码中查看,地址在下面给出。由于时间仓促,项目中有部分功能尚未完善,或者部分实现方式有待优化,也有更多的Jetpack组件尚未在项目中实践,比如 依赖注入Hilt,相机功能CameraX,权限处理Permissions, 分页处理Paging
等等。项目的持续迭代更新依然是一项艰苦持久战。
除去可以学到 Kotlin + MVVM + Android Jetpack + 协程 + Flow + 组件化 + 模块化 + 短视频
的知识,相信你还可以在我的项目中学到:
如何使用 Charles 抓包。 提供大量扩展函数,快速开发,提高效率。 ChipGroup
和FlexboxLayoutManager
等多种原生方式实现流式布局。符合阿里巴巴 Java 开发规范和阿里巴巴 Android 开发规范,并有良好的注释。 CoordinatorLayout
和 Toolbar 实现首页栏目吸顶效果和轮播图电影效果。利用 ViewOutlineProvider
给控件添加圆角,大大减少手写 shape 圆角 xml。ConstraintLayout
的使用,几乎每个界面布局都采用的 ConstraintLayout。异步任务启动器,优雅地处理 Application 中同步初始化任务问题,有效减少 APP启动耗时。 无论是模块化或者组件化,它们本质思想都是一样的,都是化整为零,化繁为简,两者的目的都是为了重用和解耦,只是叫法不一样。
感谢鸿洋提供的 WanAndroid API:https://www.wanandroid.com/blog/show/2
-- END --
推荐阅读