前言:苟有恒,何必三更眠五更起;最无益,莫过一日曝十日寒。
前言
之前一直想写个 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.genericSuperclassval 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).actualTypeArgumentsmViewModel = 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
@Daointerface VideoListCacheDao {//插入单个数据@Insert(entity = VideoInfo::class, onConflict = OnConflictStrategy.REPLACE)suspend fun insert(videoInfo: VideoInfo) //插入多个数据@Insert(onConflict = OnConflictStrategy.REPLACE)suspend fun insertAll(videoList: MutableList<VideoInfo>)//删除指定item 使用主键将传递的实体实例与数据库中的行进行匹配。如果没有具有相同主键的行,则不会进行任何更改@Deletefun delete(videoInfo: VideoInfo): Int//删除表中所有数据@Query("DELETE FROM $TABLE_VIDEO_LIST")suspend fun deleteAll()//更新某个item,不指定的entity也可以,会根据你传入的参数对象来找到你要操作的那张表@Updatefun 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(): VideoListCacheDaocompanion object {private var dataBase: SumDataBase? = null//同步锁,可能在多个线程中同时调用@Synchronizedfun 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 nullif (response.isFailed()) {throw ApiException(response.errorCode, response.errorMsg)}return response.data}}
HomeRepository的使用
class HomeRepository : BaseRepository() {//项目tabsuspend 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)) returnGlide.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)) returnGlide.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)) returnGlide.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//将显示控件绑定ExoPlayermPlayView?.player = mExoPlayer//资源准备,如果设置 setPlayWhenReady(true) 则资源准备好就立马播放。mExoPlayer?.prepare()return true}//初始化ExoPlayerprivate 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 = 10000setKeepContentOnPlayerReset(false)setShowBuffering(SHOW_BUFFERING_NEVER)//不展示缓冲viewresizeMode = AspectRatioFrameLayout.RESIZE_MODE_FITuseController = false //是否使用默认控制器,如需要可参考PlayerControlView//keepScreenOn = true}}//创建能够 边播放边缓存的 本地资源加载和http网络数据写入的工厂类private fun buildCacheDataSource(): DataSource.Factory {//创建http视频资源如何加载的工厂对象val upstreamFactory = DefaultHttpDataSource.Factory()//创建缓存,指定缓存位置,和缓存策略,为最近最少使用原则,最大为200mmCache = 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 ?: GlobalScopescope.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
参数:
//是否单独运行某个moduleisModule = false
- 在每个
Module
的build.gradle
中加入isModule
的判断,以区分是 application 还是 library:
// 组件模式和基础模式切换def root = rootProject.extif (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
主要使用的开源框架:
- Retrofit
- OkHttp
- Glide
- ARouter
- MMKV
- RxPermission
- SmartRefreshLayout