大型Android项目架构:基于组件化+模块化+Kotlin+协程+Flow+Retrofit+Jetpack+MVVM架构实现WanAndroid客户端
前言:苟有恒,何必三更眠五更起;最无益,莫过一日曝十日寒。
前言
之前一直想写个 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 使用主键将传递的实体实例与数据库中的行进行匹配。如果没有具有相同主键的行,则不会进行任何更改@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
参数:
//是否单独运行某个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
主要使用的开源框架:
- Retrofit
- OkHttp
- Glide
- ARouter
- MMKV
- RxPermission
- SmartRefreshLayout
希望我们能成为朋友,在 Github、博客 上一起分享知识,一起共勉!Keep Moving!
大型Android项目架构:基于组件化+模块化+Kotlin+协程+Flow+Retrofit+Jetpack+MVVM架构实现WanAndroid客户端相关推荐
- Android Kotlin协程和Retrofit结合使用
转载请标明出处:http://blog.csdn.net/zhaoyanjun6/article/details/118085035 本文出自[赵彦军的博客] 往期精彩文章: Kotlin实战指南二十 ...
- android studio放置在函数上面看_Android中用Kotlin协程和Retrofit进行网络请求和取消请求...
前面两篇文章介绍了协程的一些基本概念和基本知识,这篇则介绍在Android中如何使用协程配合Retrofit发起网络请求,同时介绍在使用协程时如何优雅的取消已经发起的网络请求. 需要文章中demo完整 ...
- Android 图形驱动初始化(二十三),kotlin协程原理
1 #define GL_ENTRY(_r, _api, ...) _r (*_api)(__VA_ARGS__); 可以看到 struct gl_hooks_t 的 struct gl_t gl 的 ...
- Android 基于注解IOC组件化/模块化的架构实践
当前参与的项目历史也很久远,第一行代码据说是写于2014年的某一天,那时Android用的ide还是Eclipse.那时Android还没有很好的架构指导(mvp.mvvm).那时Android最新的 ...
- Android中使用Kotlin协程(Coroutines)和Retrofit进行网络请求(二)之文件下载
写在前面 下载功能是非常常用的功能,今天我们要通过kotlin协程和retrofit来是实现文件下载的功能.retorfit本身可以将请求结果以InputStream的形式返回,拿到InputStre ...
- 在 Android 开发中使用 Kotlin 协程 (一) -- 初识 Kotlin 协程
前言 最近在研究 Kotlin 协程,发现功能真的超级强大,很有用,而且很好学,如果你正在或计划使用 Kotlin 开发 Android,那么 Kotlin 协程你一定不能错过! 协程是什么? 我们平 ...
- 【厚积薄发系列】C++项目总结19—组件化架构思想
在一个大型的项目中,随着业务不断拓展,开发人员和代码量的不断增加,传统的单体架构会经常遇到以下问题: 1.业务模块划分不够清晰,模块之间高度耦合,修改和新增需求的时候可能会导致牵一发而动全身的雪崩. ...
- App 组件化/模块化之路——如何封装网络请求框架
App 组件化/模块化之路--如何封装网络请求框架 在 App 开发中网络请求是每个开发者必备的开发库,也出现了许多优秀开源的网络请求库.例如 okhttp retrofit android-asyn ...
- AliOS Things 基于组件化思想的多bin特性
摘要: 今年杭州云栖大会上,AliOS Things正式发布,其中有一个基于组件化思想的多bin特性,这是AliOS Things有专利保护的多bin fota升级解决方案的核心 今年杭州云栖大会上, ...
最新文章
- 数十篇推荐系统论文被批无法复现:源码、数据集均缺失,性能难达预期
- 基于AFNetworking3.0网络封装
- double salary = wage = 9999.99错误
- Leetcode: 4Sum
- 图像降噪算法——稀疏表达:K-SVD算法
- Matlab计算机视觉/图像处理工具箱(待续)
- adf盖怎么打开_罐头好吃盖难开,学会这几招,再不靠蛮力了,女生也轻松拧开...
- Origin绘制散点图个性化设置散点颜色
- Django基础-Web框架-URL路由
- 用etcd实现比Redis更安全的分布式锁
- 质数分布是否随机关乎安全大事
- SAP License:SAP的公司间销售
- 2020 年,Serverless 将给大前端带来什么样的变化?
- 鸿星尔克因公司系统崩溃、恳请顾客退款;乔布斯首份手写求职信拍卖出222万;OpenAI 开源 Triton语言|极客头条...
- Java 实现了第三方QQ账号登录(附源码)
- views是什么意思_views的意思
- rabbitmq使用mqtt协议
- unity3d-学习笔记8-卡牌游戏制作(实现动态读取卡片信息并且在游戏界面展现)
- 目前微型计算机主要采用电子原件是,目前,个人计算机使用的电子元器件主要是()。...
- 工具 | 分享一个很酷的上位机软件
热门文章
- fecshop 小笔记
- java 一二三四五_五道java小题,补更四道java小题
- 芯片无忧的使用教程,ChipEasy芯片无忧如何检测U盘?U盘检测方法说明
- Git下载代码到Windows再拷贝到Linux下编译时要关闭换行符自动转换
- 计算机属性打开自动关上,如何打开和关闭系统的自动更新
- mysql服务器管理员_配置MySQL服务器时,需要设置一个管理员账号,其名称是( )。...
- SysML和UML建模工具
- CodeGear正式发布RubyIDE,名为3rdRail
- CMOS密码破解全攻略
- .Net Reflector反编译代码与源代码的区别