大型Android项目架构:基于组件化+模块化+Kotlin+协程+Flow+Retrofit+Jetpack+MVVM架构实现WanAndroid客户端
前言:苟有恒,何必三更眠五更起;最无益,莫过一日曝十日寒。项目采用 Kotlin 语言,Android Jetpack,组件化,模块化拆分,加入短视频功能,Flow冷流的使...
前言:苟有恒,何必三更眠五更起;最无益,莫过一日曝十日寒。
前言
之前一直想写个 WanAndroid 项目来巩固自己对 Kotlin+Jetpack+协程
等知识的学习,但是一直没有时间。这里重新行动起来,从项目搭建到完成前前后后用了两个月时间,平常时间比较少,基本上都是只能利用零碎的时间来写。但不再是想写一个简单的玩安卓项目,我从多个大型项目中学习和吸取经验,从0到1打造一个符合大型项目的架构模式。
这或许是一个缩影,但是麻雀虽小,五脏俱全,这肯定能给大家带来一些想法和思考。当然这个项目的功能并未全部完善,因为我们的目的不是造一个 WanAndroid 客户端,而是学习搭建和使用 Kotlin+协程+Flow+Retrofit+Jetpack+MVVM+组件化+模块化+短视频 这一种架构,更好的提升自己。后续我也会不断完善和优化,在保证拥有一个正常的 APP 功能之外,继续加入 Compose
,依赖注入Hint
,性能优化
,MVI模式
,支付功能
等的实践。
一、项目简介
- 项目采用 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
图片加载利用 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
Glide.with(context).load(url)
.placeholder(R.mipmap.default_head)
.error(R.mipmap.default_head)
.skipMemoryCache(false) //启用内存缓存
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.transform(CenterCrop()) // 圆形
.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 次,有如下性能对比:
项目中使用 MMKV 保存用户相关信息,包括用户登录 Cookies,用户名称,手机号码,搜索历史数据等信息。
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启动耗时。
- 无论是模块化或者组件化,它们本质思想都是一样的,都是化整为零,化繁为简,两者的目的都是为了重用和解耦,只是叫法不一样。
项目地址:ST_Wan_Android
点关注,不迷路
好了各位,以上就是这篇文章的全部内容了,很感谢您阅读这篇文章。我是suming,感谢支持和认可,您的点赞就是我创作的最大动力。山水有相逢,我们下篇文章见!
本人水平有限,文章难免会有错误,请批评指正,不胜感激 !
感谢
API: 鸿洋提供的 WanAndroid API
主要使用的开源框架:
希望我们能成为朋友,在 Github、博客 上一起分享知识,一起共勉!Keep Moving!
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)