前言
之前一直想写个 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 次,有如下性能对比:
项目中使用 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
对外提供暴露的能力。
比如在公共资源库中的 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启动耗时。
-
无论是模块化或者组件化,它们本质思想都是一样的,都是化整为零,化繁为简,两者的目的都是为了重用和解耦,只是叫法不一样。