基于组件化+模块化+Kotlin+协程+Flow+Retrofit+Jetpack+MVVM架构实现WanAndroid客户端

2023-10-27

前言

之前一直想写个 WanAndroid 项目来巩固自己对 Kotlin+Jetpack+协程 等知识的学习,但是一直没有时间。这里重新行动起来,从项目搭建到完成前前后后用了两个月时间,平常时间比较少,基本上都是只能利用零碎的时间来写。但不再是想写一个简单的玩安卓项目,我从多个大型项目中学习和吸取经验,从0到1打造一个符合大型项目的架构模式。

这或许是一个缩影,但是麻雀虽小,五脏俱全,这肯定能给大家带来一些想法和思考。当然这个项目的功能并未全部完善,因为我们的目的不是造一个 WanAndroid 客户端,而是学习搭建和使用 Kotlin+协程+Flow+Retrofit+Jetpack+MVVM+组件化+模块化+短视频 这一种架构,更好的提升自己。后续我也会不断完善和优化,在保证拥有一个正常的 APP 功能之外,继续加入 Compose依赖注入Hint性能优化MVI模式支付功能等的实践。

作者:苏火火
链接:https://juejin.cn/post/7223767530981867557

一、项目简介

  • 项目采用 Kotlin 语言编写,结合 Jetpack 相关控件,NavigationLifecyleDataBindingLiveDataViewModel等搭建的 MVVM 架构模式;

  • 通过组件化模块化拆分,实现项目更好解耦和复用,ARouter 实现模块间通信;

  • 使用 协程+Flow+Retrofit+OkHttp 优雅地实现网络请求;

  • 通过 mmkvRoom 数据库等实现对数据缓存的管理;

  • 使用谷歌 ExoPlayer 实现短视频播放;

  • 使用 Glide 完成图片加载;

  • 通过 WanAndroid 提供的 API 实现的一款玩安卓客户端。

5c1c8d8db0fce91c069a5880f9f54450.jpeg项目使用 MVVM架构模式,基本上遵循 Google 推荐的架构,对于 Repository,Google 认为 ViewModel 仅仅用来做数据的存储,数据加载应该由 Repository 来完成。通过 Room 数据库实现对数据的缓存,在无网络或者弱网的情况下优先展示缓存数据。

ced05ddafcab09d04c89ecdc4811ec8b.jpeg

项目截图:

2367bbe3f323b2a9a2475c0c8a819617.jpeg c0406c3939044e703af734b56a87722f.jpeg b2944039bdbead4513be3a2f9ad7671b.jpeg d15f2d88d08db03d9046355110d00e4a.jpeg d8d11f65ea9143f2478985ebc4ade5d0.jpeg 0e2886c0b98a9ec10cc41de0ac5bc790.jpeg a1aa158c0c4e1d0bc4d6c2e76b59e3c8.jpeg 5505a298be44cd1709696dfd58e17943.jpeg

项目地址:

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

项目中提供了大量控件扩展类,能够快速开发,提高效率

  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组件

47ab054d0fa216404903650c9ba8179d.jpegAndroid 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 库架构的示意图:6957a82bbcddf1bdc89ee9cebf1fe7bc.jpegRoom 包含三个主要组件:

  • 数据库类:用于保存数据库并作为应用持久性数据底层连接的主要访问点;

  • 数据实体:用于表示应用的数据库中的表;

  • 数据访问对象 (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 很像,非常方便,用它来做网络请求更加简洁。4c46c4efb9cae31778d8a3027be8e7ba.jpeg

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
    //请求配置
    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)
}
  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");

循环写入随机的 int 1k 次,有如下性能对比:db6df152090fba0bc635b0ab0646b853.jpeg项目中使用 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 组件化&模块化

组件化&模块化有利于业务模块分离,高内聚,低耦合,代码边界清晰。有利于团队合作多线开发,加快编译速度,提高开发效率,管理更加方便,利于维护和迭代。74ba0990e3dfa7e1b053e8385109ff0c.jpeg宿主 App 中只有一个 Application,整个业务被拆分为各个 mod 模块和 lib 组件库。对一些功能组件进行封装抽取为 lib,给上层提供依赖。mod 模块之间没有任务依赖关系,通过 Arouter 进行通信。

(1) 模块化

项目中通过以业务为维度把 App 拆分成主页模块,登录模块,搜索模块,用户模块,视频模块等,相互间不可以访问不可以作为依赖,与此同时他们共同依赖于基础库,网络请求库,公共资源库,图片加载库等。如果还需要使用到启动器组件、Banner组件、数据库Room组件等则单独按需添加。

APP 壳工程负责打包环境,签名,混淆规则,业务模块集成,APP 主题等配置等工作,一般不包含任何业务。

(2) 组件化

模块化和组件化最明显的区别就是模块相对组件来说粒度更大。一个模块中可能包含多个组件。在划分的时候,模块化是业务导向,组件化是功能导向。组件化是建立在模块化思想上的一次演进。

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

dbc5ea84d46665c9a66f604cb1e64fa2.jpeg

(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. 无论是模块化或者组件化,它们本质思想都是一样的,都是化整为零,化繁为简,两者的目的都是为了重用和解耦,只是叫法不一样。

项目地址:

https://github.com/suming77/SumTea_Android

关注我获取更多知识或者投稿

e0986bbf7fd04f91e0bc615494d56d7f.jpeg

28a1228f57afb120ea43ff0f16816f3c.jpeg

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

基于组件化+模块化+Kotlin+协程+Flow+Retrofit+Jetpack+MVVM架构实现WanAndroid客户端 的相关文章

  • 是否仍然建议使用 AsyncTask 在后台加载 listView 项目?

    背景 我听说有一些在后台加载数据的新解决方案比 AsyncTask 更值得推荐 例如loaders http developer android com reference android content Loader html 问题 As
  • 在运行时绘制HSV圆

    我想在我的应用程序中有一个颜色选择器 如下所示 我尝试过逐像素填充位图 尝试使用画布drawArc 方法 这两种方式都不利于性能 有任何想法吗 它可能不完全是它应该的样子 与颜色 饱和度等有关 但是这是一些东西 http i1272 pho
  • 读取 Android 4.2 中的 APN?

    我有个问题阅读 APN在安卓v4 2中 是读 不是写APNS 它抛出一个安全异常 没有写入 APN 设置的权限 用户 10068 和当前用户都没有权限 进程有 android permission WRITE APN SETTINGS 相同
  • 在Android中打开浮动菜单(上下文菜单)?

    我创建了一个新菜单 名为 drmenu xml 当我按下菜单按钮时它可以正常工作 但是当用户按下按钮时我需要打开上下文菜单 下面的代码按钮只显示一个吐司 这是我的 xml 布局
  • Android 测试 java.lang.NoClassDefFoundError 由于 Fest-Android 出现错误

    我目前正在我的项目中实现 Android 版 Fest 但我似乎遇到了依赖问题 如果我在不包含 Fest 库的情况下运行测试 测试将正常运行 一旦我添加了 Fest 库 测试就不再运行 相反 会抛出异常 我的项目使用以下依赖项 compil
  • 垂直翻转 Android Canvas

    有没有一种简单的方法可以在 Android 中翻转画布 我似乎找不到任何可以让我垂直翻转它的东西 这样 y 轴上的零就是手机屏幕的底部而不是顶部 如果解决方案不是特别快也没关系 因为我没有对画布进行任何计算密集的操作 提前致谢 Try ca
  • Android物理引擎[关闭]

    Closed 这个问题不符合堆栈溢出指南 help closed questions 目前不接受答案 在研究了 3D 游戏编程之后 很快就明白了为什么物理引擎非常有用 Android 支持哪些物理引擎并且可以在 Android 上使用 关于
  • 按钮上方带有文本的单选按钮

    我是 Android 新手 我需要在我的活动中添加单选按钮 但我需要将文本放在项目符号按钮的顶部 请提供任何帮助 我发现了以下内容 尽管我不明白 drawable in 选择器和 style Tab 样式是什么 顶部带有文本的单选按钮 ht
  • 错误:类 kotlin.reflect.jvm.internal.FunctionCaller$FieldSetter

    我已尝试一切方法来消除此错误 但它不断出现 Class kotlin reflect jvm internal FunctionCaller FieldSetter can not access a member of class com
  • Eclipse Android 模拟器 - 键盘不工作

    我刚刚更新到最新的 SDK 版本 16 使用最新版本的 API 16 创建了新版本的 AVD 并且我的硬件键盘在模拟器上不再工作 甚至我的其他 avd 使用旧版本的 sdk 任何想法如何解决这一问题 您的 AVD 的 键盘支持 硬件属性是否
  • 如何更改蜂窝中儿童偏好屏幕的背景颜色

    过去几天我一直在寻找解决方案 但找不到 我需要更改右窗格的背景颜色 我知道如何更改左父首选项的颜色 我在清单文件中创建了一个新主题
  • Android Studio错误的含义:未注释的参数覆盖@NonNull参数

    我正在尝试 Android Studio 创建新项目并添加默认值后onSaveInstanceState方法创建 MyActivity 类 当我尝试将代码提交到 Git 时 我收到一个我不明白的奇怪错误 代码是这样的 我得到的错误是这样的
  • 特定铃声 firebase 通知 xamarin.android

    How i can force the push notification to run ringtone instead of default notification sound is there any way to ovveride
  • 调用属于Fragment的Activity的函数

    我正在与多个Fragments在 Android 下 我对如何从嵌入式应用程序发送和接收数据感到困惑 为了简单的解释 我有一个ListFragment and a MapFragment使用解释的方法here https stackover
  • Android RxJava 2 JUnit 测试 - android.os.Looper 中的 getMainLooper 未模拟 RuntimeException

    我在尝试为正在使用的演示者运行 JUnit 测试时遇到 RuntimeExceptionobserveOn AndroidSchedulers mainThread 由于它们是纯 JUnit 测试而不是 Android 仪器测试 因此它们无
  • 如何在没有 OpenCv Manager 的情况下运行 OpenCV 代码

    我正在使用 OpenCV4Android 版本 2 4 10 并在 Samsung Galayx GT I9300 上测试我的代码 我遇到的问题是 我必须从 Play 商店下载 Opencv Manager 以便我的 opencv 代码运行
  • 如何根据受保护的 String doInBackground 方法中 AsyncTask 的结果调用 Toast.makeText() ?

    我从 AsyncTask 中的数据库中获取数据 如果它为空 我想吐司一个警告文本 我在 AsyncTask 中尝试过 但我了解到它不是在工作线程中调用的 这是我的 doInBackground 方法 protected String doI
  • 如何以相同的意图从相机获取全尺寸图片和缩略图

    我一直需要找到这个问题的解决方案 我已经从这个社区搜索并测试了许多解决方案 但任何人都适合帮助我 我有两个活动 第一个活动拍摄一张照片并将其发送到另一个活动 该活动有一个 ImageView 来接收该照片 直到这里我遇到问题 以及一个在数据
  • 不幸的是 Project_Name 已停止

    我有一个简单的应用程序 您可以在文本视图中输入文本并按提交 它会在另一个活动中显示文本 然而 当我按下提交时 给我消息 不幸的是 发送已停止 我查看了SO上的其他线程 但是不幸的是 myfirstproject 在 java 中停止工作错误
  • 找不到与给定名称“@style/Theme.AppCompat.Light”匹配的资源

    我已经研究这个问题几个小时了 从 github 下载存储库后 任何 xml 文件中的唯一错误是 No resource found that matches the given name style Theme AppCompat Ligh

随机推荐

  • STM32学习——什么是寄存器(存储器映射和寄存器映射)

    目录 STM32芯片里面有什么 一 驱动单元 1 ICode总线 2 DCode总线 3 系统总线 4 DMA总线 二 被动单元 1 内部的闪存存储器 2 内部的SRAM 3 FSMC 4 AHB到APB的桥 三 STM32的外设基地址映射
  • CTEX中无限期试用WinEdt的方法

    CTeX中文套装是基于Windows下的MiKTeX系统 集成了编辑器WinEdt和PostScript处理软件Ghostscript和GSview等主要工具 虽说是非商业的免费软件 但其中的WinEdt其实是收费的软件 CTEX软件利用了
  • ffmpeg 录制屏幕

    文章目录 1 ffmpeg 下载 2 screen capture recorder 下载 3 ffmpeg 录屏命令 4 ffmpeg 其他命令 后续 寻路篇 立理想 坐豪车 住豪宅 天天睡到自然醒 叹现状 小黄车 挤隔断 早上起来数星星
  • 常用框架分析(7)-Flutter

    框架分析 7 Flutter 专栏介绍 Flutter 核心思想 Flutter的特点 快速开发 跨平台 高性能 美观的用户界面 Flutter的架构 框架层 引擎层 平台层 开发过程 使用Dart语言编写代码 编译成原生代码 热重载 工具
  • Dell服务器系统升级,更改引导方式(以戴尔R720为例)

    1 重启服务器按住F2 进入system setup 选择第一项的 system BIOS 2 选择 Boot Setting 进入后启动模式有两种 BIOS和UEFI 当磁盘小于2TB的时候选择BIOS 当磁盘大于2TB的时候选择UEFI
  • vue项目引入antDesignUI组件

    快速安装ant design vue并配置 vue2 0 antDesign 1 7 8 第一步 安装ant deisgn vue 1 7 8 npm install ant design vue 1 7 8 save 第二步 配置pack
  • Centos7下载和安装教程

    1 CentOS下载 CentOS是免费版 推荐在官网上直接下载 网址 https www centos org download DVD ISO 普通光盘完整安装版镜像 可离线安装到计算机硬盘上 包含大量的常用软件 一般选择这种镜像类型即
  • 如何用openweather显示html,如何显示openweathermap天气图标

    我正在使用openweathermap显示天气预报 一切正常 但图标有问题 json响应代码是 Array city gt Array id gt 1271476 name gt Guwahati coord gt Array lon gt
  • makefile中wildcard的理解

    wildcard 用来明确表示通配符 因为在 Makefile 里 变量实质上就是 C C 中的宏 也就是说 如果一个表达式如 objs o 则 objs 的值就是 o 而不是表示所有的 o 文件 若果要使用通配符 那么就要使用 wildc
  • PCB制板流程及工艺

    PCB制板的流程一般包括以下几个步骤 1 设计电路原理图和PCB布局 首先 需要设计电路原理图和PCB布局图 电路原理图是电路的逻辑图 用于指导电路的设计和调试 PCB布局图是电路板上各个元件的布局图 包括焊盘 引脚 电源 地线等 电路原理
  • 华中科技大学操作系统实验课 实验二

    一 实验目的 1 理解进程 线程的概念和应用编程过程 2 理解进程 线程的同步机制和应用编程 二 实验内容 1 在Linux下创建一对父子进程 2 在Linux下创建2个线程A和B 循环输出数据或字符串 3 在Windows下创建线程A和B
  • MySQL 回表 & 索引覆盖

    索引类型 聚簇索引 叶子节点存储的是行记录 每个表必须要有至少一个聚簇索引 使用聚簇索引查询会很快 因为可以直接定位到行记录 普通索引 二级索引 除聚簇索引外的索引 即非聚簇索引 普通索引叶子节点存储的是主键 聚簇索引 的值 聚簇索引递推规
  • 【嵌入式学习-C语言篇】 if & switch 的使用

    嵌入式学习 C语言篇 if switch 的使用 if switch 的常用场景 智能音箱 网络状态判断 智能家居 传感器开关灯 基本代码 我们拿网络状态判断来举例 下面代码展示了使用if 和 switch的使用 include
  • 背包问题(资料搜集)

    https comzyh com upload PDF Pack PDF Comzyh pdf 上面的背包问题讲解来自这位大佬 大佬
  • VS Code中统计有效代码行数(除去注释行,空格)

    之前用正则表达式在VSCode中直接查询代码行数 不过这种太麻烦了需要先设置好 要包含的文件 和 要排除的文件 而且还不能排除注释行和空格 所有给大家安利VS Code的一款很好用的插件 1 首先在VS Code中搜索VS Code cou
  • 刷脸支付比以前的支付技术确实安全不少

    支付宝正式推出刷脸支付功能 在我们腾不出手来或是忘记各种各样的密码可以选择往付款摄像头一站随后输入号码就支付完成 整个过程不足十秒钟 随着科学技术的不断完善 刷脸也会变得更加安全 不过就目前的安全来看 日常使用刷脸支付没有任何问题 刷脸支付
  • 华为HCIA-Datacom学习笔记

    系列文章目录 第一章 网络的定义和网络的历史 文章目录 系列文章目录 第一章 网络的定义和网络的历史 前言 一 网络的定义 1 网络范围 二 网络的历史沿革 1 图灵机 2 第一台计算机 3 阿帕网 4 传输协议 5 厂商 6 代理商 7
  • 函数使用注意事项

    1 自定义函数 lt 1 gt 无参数 无返回值 def 函数名 语句 lt 2 gt 无参数 有返回值 def 函数名 语句 return 需要返回的数值 注意 一个函数到底有没有返回值 就看有没有return 因为只有return才可以
  • 服务器修改字体,Win10 1909默认字体怎么修改?Win10 1909默认字体修改教程

    在使用Win10 1909设备的时候 偶尔需要创建一个全新的网络连接来进行文件的共享 但许多Win10 1909用户其实并不清楚 该怎么新建网络连接 针对这一情况 小编今天为大家带来了Win10 1909网络连接新建方法简述 方法步骤 打开
  • 基于组件化+模块化+Kotlin+协程+Flow+Retrofit+Jetpack+MVVM架构实现WanAndroid客户端

    前言 之前一直想写个 WanAndroid 项目来巩固自己对 Kotlin Jetpack 协程 等知识的学习 但是一直没有时间 这里重新行动起来 从项目搭建到完成前前后后用了两个月时间 平常时间比较少 基本上都是只能利用零碎的时间来写 但