大型Android项目架构:基于组件化+模块化+Kotlin+协程+Flow+Retrofit+Jetpack+MVVM架构实现WanAndroid客户端

大型Android项目架构:基于组件化+模块化+Kotlin+协程+Flow+Retrofit+Jetpack+MVVM架构实现WanAndroid客户端

前语:苟有恒,何须三更眠五更起;最无益,莫过一日曝十日寒。

前语

之前一直想写个 WanAndroid 项目来稳固自己对 Kotlin+Jetpack+协程 等常识的学习,可是一直没有时刻。这里重新行动起来,从项目建立到完结前前后后用了两个月时刻,往常时刻比较少,基本上都是只能运用零碎的时刻来写。但不再是想写一个简略的玩安卓项目,我从多个大型项目中学习和吸取经验,从0到1打造一个契合大型项意图架构办法。

这或许是一个缩影,可是麻雀虽小,五脏俱全,这肯定能给咱们带来一些主意和思考。当然这个项意图功用并未悉数完善,由于咱们的意图不是造一个 WanAndroid 客户端,而是学习建立和运用 Kotlin+协程+Flow+Retrofit+Jetpack+MVVM+组件化+模块化+短视频 这一种架构,更好的提高自己。后续我也会不断完善和优化,在保证具有一个正常的 APP 功用之外,继续参加 Compose依靠注入Hint功用优化MVI办法支付功用等的实践。

一、项目简介

  • 项目选用 Kotlin 语言编写,结合 Jetpack 相关控件,NavigationLifecyleDataBindingLiveDataViewModel等建立的 MVVM 架构办法;
  • 经过组件化模块化拆分,完结项目更好解耦和复用,ARouter 完结模块间通讯;
  • 运用 协程+Flow+Retrofit+OkHttp 高雅地完结网络恳求;
  • 经过 mmkvRoom 数据库等完结对数据缓存的办理;
  • 运用谷歌 ExoPlayer 完结短视频播映;
  • 运用 Glide 完结图片加载;
  • 经过 WanAndroid 供给的 API 完结的一款玩安卓客户端。

大型Android项目架构:基于组件化+模块化+Kotlin+协程+Flow+Retrofit+Jetpack+MVVM架构实现WanAndroid客户端
项目运用 MVVM架构办法,基本上遵循 Google 引荐的架构,关于 Repository,Google 认为 ViewModel 仅仅用来做数据的存储,数据加载应该由 Repository 来完结。经过 Room 数据库完结对数据的缓存,在无网络或许弱网的情况下优先展现缓存数据。

大型Android项目架构:基于组件化+模块化+Kotlin+协程+Flow+Retrofit+Jetpack+MVVM架构实现WanAndroid客户端

项目截图:

大型Android项目架构:基于组件化+模块化+Kotlin+协程+Flow+Retrofit+Jetpack+MVVM架构实现WanAndroid客户端
大型Android项目架构:基于组件化+模块化+Kotlin+协程+Flow+Retrofit+Jetpack+MVVM架构实现WanAndroid客户端
大型Android项目架构:基于组件化+模块化+Kotlin+协程+Flow+Retrofit+Jetpack+MVVM架构实现WanAndroid客户端
大型Android项目架构:基于组件化+模块化+Kotlin+协程+Flow+Retrofit+Jetpack+MVVM架构实现WanAndroid客户端
大型Android项目架构:基于组件化+模块化+Kotlin+协程+Flow+Retrofit+Jetpack+MVVM架构实现WanAndroid客户端
大型Android项目架构:基于组件化+模块化+Kotlin+协程+Flow+Retrofit+Jetpack+MVVM架构实现WanAndroid客户端
大型Android项目架构:基于组件化+模块化+Kotlin+协程+Flow+Retrofit+Jetpack+MVVM架构实现WanAndroid客户端

项目地址: github.com/suming77/Su…

二、项目概况

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拓宽类

项目中供给了很多控件扩展类,能够快速开发,进步功率

  1. ResourceExt:  资源文件扩展类;
  2. TextViewExt:  TextView 扩展类;
  3. SpanExt:    Span 拓宽类,完结多种 Span 作用;
  4. RecyclerViewExt:一行代码快速完结增加笔直分割线,网格分割线;
  5. ViewExt:    View 扩展类,完结点击防抖,增加距离,设置宽度,设置可见性等等;
  6. EditTextExt:  经过 Flow 构建输入框文字改变流,filter{} 完结数据过滤,防止无效恳求,debounce() 完结防抖;
  7. 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项目架构:基于组件化+模块化+Kotlin+协程+Flow+Retrofit+Jetpack+MVVM架构实现WanAndroid客户端
Android Jetpack是一组 Android 软件组件、工具和攻略,它们能够帮助开发者构建高质量、安稳的 Android 运用程序。Jetpack 中包括多个库,它们旨在处理 Android 运用程序开发中的常见问题,并供给共同的 API 和开发体会。

项目中仅仅运用到上图的一小部分组件。

(1) Navtgation

Navtgation 作为构建运用内界面的结构,重点是让单 Activity 运用成为首选架构(一个运用只需一个 Activity),它的定位是页面路由。

项目中主页分为5个 Tab,首要为主页、分类、体系、我的。运用 BottomNavigationView + Navigation 来建立。经过 menu 来装备底部菜单,经过 NavHostFragment 来装备各个 Fragment。一同处理了 NavigationBottomNavigationView 结合运用时,点击 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 库架构的示意图:

大型Android项目架构:基于组件化+模块化+Kotlin+协程+Flow+Retrofit+Jetpack+MVVM架构实现WanAndroid客户端
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 很像,十分便利,用它来做网络恳求愈加简练。

大型Android项目架构:基于组件化+模块化+Kotlin+协程+Flow+Retrofit+Jetpack+MVVM架构实现WanAndroid客户端

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)
}
  1. 修复 Glide 的图片裁剪和 ImageView 的 scaleType 的抵触问题,Bitmap 会先圆角裁剪,再加载到 ImageView 中,假如 Bitmap 图片尺度大于 ImageView 尺度,则会看不到,运用 CenterCrop() 重载,会先将 Bitmap 居中裁剪,再进行圆角处理,这样就能看到。
  2. 供给了 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");

循环写入随机的 int1k 次,有如下功用比照:

大型Android项目架构:基于组件化+模块化+Kotlin+协程+Flow+Retrofit+Jetpack+MVVM架构实现WanAndroid客户端
项目中运用 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 组件化&模块化

组件化&模块化有利于事务模块分离,高内聚,低耦合,代码边界清晰。有利于团队合作多线开发,加快编译速度,进步开发功率,办理愈加便利,利于维护和迭代。

大型Android项目架构:基于组件化+模块化+Kotlin+协程+Flow+Retrofit+Jetpack+MVVM架构实现WanAndroid客户端
宿主 App 中只有一个 Application,整个事务被拆分为各个 mod 模块和 lib 组件库。对一些功用组件进行封装抽取为 lib,给上层供给依靠。mod 模块之间没有使命依靠关系,经过 Arouter 进行通讯。

(1) 模块化

项目中经过以事务为维度把 App 拆分成主页模块,登录模块,搜索模块,用户模块,视频模块等,相互间不能够拜访不能够作为依靠,与此一同他们共同依靠于根底库,网络恳求库,公共资源库,图片加载库等。假如还需求运用到启动器组件、Banner组件、数据库Room组件等则独自按需增加。

APP 壳工程担任打包环境,签名,混淆规矩,事务模块集成,APP 主题等装备等作业,一般不包括任何事务。

(2) 组件化

模块化和组件化最显着的区别便是模块相对组件来说粒度更大。一个模块中或许包括多个组件。在区别的时候,模块化是事务导向,组件化是功用导向。组件化是建立在模块化思想上的一次演进。

项目中以功用维度拆分了启动器组件、Banner组件、数据库Room组件等组件。模块化&组件化拆分后工程图:

大型Android项目架构:基于组件化+模块化+Kotlin+协程+Flow+Retrofit+Jetpack+MVVM架构实现WanAndroid客户端

(3) 组件间通讯

组件化之后就无法直接拜访其他模块的类和办法,这是个比较突出的问题,就像本来能够直接运用 LogintManager 来拉起登录,判别是否已登录,可是这个类现已被拆分到了 mod_login 模块下,而事务模块之间是不能相互作为依靠的,所以无法在其他模块直接运用 LogintManager

首要凭借阿里的路由结构 ARouter 完结组件间通讯,把对外供给的才能,以接口的办法露出出去。

比方在公共资源库中的 service 包下创立 ILoginService,供给对外露出登录的才能,在 mod_login 模块中供给 LoginServiceImpl 完结类,任意模块就能够经过 LoginServiceProvider 运用 iLoginService 对外供给露出的才能。

  1. 公共资源库中创立 ILoginService,供给对外露出登录的才能。
interface ILoginService : IProvider {
    //是否登录
    fun isLogin(): Boolean
    //跳转登录页
    fun login(context: Context)
    //登出
    fun logout(
        context: Context,
        lifecycleOwner: LifecycleOwner?,
        observer: Observer<Boolean>
    )
}
  1. 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?) {}
}
  1. 公共资源库中创立 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。

  1. config.gradle 文件中参加 isModule 参数:
//是否独自运转某个module
isModule = false
  1. 在每个 Modulebuild.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/**'
                }
            }
        }
    }
}
  1. 将经过修改 SourceSets 中的属性,能够指定需求被编译的源文件,假如是library,则编译 manifest 下 AndroidManifest.xml,反之则直接编译 debug 目录下 AndroidManifest.xml,一同参加 Applicationintent-filter 等参数。

存疑一

至于模块独自编译独自运转,这种是一个伪需求,实际上必定存在多个模块间通讯的场景。否则跨模块的服务提取和获取,初始化使命,模块间的联合测试该怎样处理呢?一个模块运转后需求和其他的模块通讯,比方对外供给服务,获取服务,与之相关联的模块假如没有运转起来的话是无法运用的。

与此一同还需求在 suorceSets 下维护两套 AndoidManifest 以及 Javasource 目录,这个不只麻烦并且每次更改都需求同步一段时刻。所以这种流传的模块化独立编译的办法,是否真的合适就仁者见仁了。

三、写在最终

如需求更具体的代码能够到项目源码中检查,地址在下面给出。由于时刻仓促,项目中有部分功用尚未完善,或许部分完结办法有待优化,也有更多的Jetpack组件尚未在项目中实践,比方 依靠注入Hilt相机功用CameraX权限处理Permissions分页处理Paging等等。项意图继续迭代更新依然是一项艰苦持久战。

除去能够学到 Kotlin + MVVM + Android Jetpack + 协程 + Flow + 组件化 + 模块化 + 短视频 的常识,相信你还能够在我的项目中学到:

  1. 怎样运用 Charles 抓包。
  2. 供给很多扩展函数,快速开发,进步功率。
  3. ChipGroupFlexboxLayoutManager 等多种原生办法完结流式布局。
  4. 契合阿里巴巴 Java 开发规范和阿里巴巴 Android 开发规范,并有良好的注释。
  5. CoordinatorLayoutToolbar 完结主页栏目吸顶作用和轮播图电影作用。
  6. 运用 ViewOutlineProvider 给控件增加圆角,大大削减手写 shape 圆角 xml。
  7. ConstraintLayout 的运用,几乎每个界面布局都选用的 ConstraintLayout
  8. 异步使命启动器,高雅地处理 Application 中同步初始化使命问题,有用削减 APP启动耗时。
  9. 无论是模块化或许组件化,它们本质思想都是一样的,都是化整为零,化繁为简,两者的意图都是为了重用和解耦,仅仅叫法不一样。

项目地址:ST_Wan_Android

点关注,不走失

好了各位,以上便是这篇文章的悉数内容了,很感谢您阅览这篇文章。我是suming,感谢支撑和认可,您的点赞便是我创造的最大动力。山水有相逢,咱们下篇文章见!

本人水平有限,文章难免会有过错,请批评指正,不胜感激 !

感谢

API: 鸿洋供给的 WanAndroid API

首要运用的开源结构:

  • Retrofit
  • OkHttp
  • Glide
  • ARouter
  • MMKV
  • RxPermission
  • SmartRefreshLayout

期望咱们能成为朋友,在 Github、 上一同共享常识,一同共勉!Keep Moving!