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

news2024/12/23 6:56:51

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

前言

之前一直想写个 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 实现的一款玩安卓客户端。
    在这里插入图片描述

项目使用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拓展类

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

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

(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 库架构的示意图:
在这里插入图片描述

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 很像,非常方便,用它来做网络请求更加简洁。
image.png

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");

循环写入随机的 int 1k 次,有如下性能对比:
image.png
项目中使用 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组件等组件。模块化&组件化拆分后工程图:

image.png

(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 目录,这个不仅麻烦而且每次更改都需要同步一段时间。所以这种流传的模块化独立编译的形式,是否真的适合就仁者见仁了。

三、写在最后

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

  1. ConstraintLayout的使用,几乎每个界面布局都采用的ConstraintLayout
  2. 符合阿里巴巴Java开发规范和阿里巴巴Android开发规范,并有良好的注释。
  3. 如何使用Chlars抓包。
  4. 利用 ViewOutlineProvider 给控件添加圆角,大大减少手写 shape 圆角xml。
  5. 提供大量扩展函数,快速开发,提高效率。
  6. 无论是模块化或者组件化,它们本质思想都是一样的,都是化整为零,化繁为简,两者的目的都是为了重用和解耦,只是叫法不一样。

项目地址:ST_Wan_Android

点关注,不迷路

好了各位,以上就是这篇文章的全部内容了,很感谢您阅读这篇文章。我是suming,感谢支持和认可,您的点赞就是我创作的最大动力。山水有相逢,我们下篇文章见!

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

感谢

API: 鸿洋提供的 WanAndroid API

主要使用的开源框架:

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

希望我们能成为朋友,在 Github博客 上一起分享知识,一起共勉!Keep Moving!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/443212.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

奇异值分解SVD

概念 奇异值分解&#xff08;singular value decomposition&#xff09;是线性代数中一种重要的矩阵分解。奇异值分解在某些方面与对称矩阵或厄密矩阵基于特征向量的对角化类似。然而这两种矩阵分解尽管有其相关性&#xff0c;但还是有明显的不同。对称矩阵特征向量分解的基础…

数据分析师 ---- SQL强化(1)

文章目录 数据分析师 ---- SQL强化(1)写在前面题目第一步&#xff1a;表连接以及表拼接第二步&#xff1a;新建列以及填充值总结 数据分析师 ---- SQL强化(1) 写在前面 最近在找工作中发现&#xff0c;数据分析师的笔试多数会涉及SQL&#xff0c;但是笔试中SQL的难度和我们在学…

车载软件架构——闲聊几句AUTOSAR BSW(一)

我是穿拖鞋的汉子,魔都中坚持长期主义的工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 人生是用来体验的,不是用来演绎完美的。我慢慢能接受自己身上那些灰暗的部分,原谅自己的迟钝和平庸,允许自己出错,允许自己偶尔断电,带着缺憾拼命绽放,…

Node【初识Node】

文章目录 &#x1f31f;前言&#x1f31f;Node.js&#x1f31f;特性&#xff1a;&#x1f31f;1. 单线程&#x1f31f;2.异步IO&#x1f31f;前端中的异步&#x1f31f;Node中的异步 &#x1f31f;3.跨平台&#x1f31f;4.运行速度快 &#x1f31f; 劣势&#xff1a;&#x1f3…

4/20~4/21两日总结

网络编程 socket通信 socket被翻译为套接字&#xff0c;通过socket这种约定&#xff0c;一台计算机可以接收其他计算机的数据&#xff0c;也可以向其他计算机发送数据 如何实现呢 ServerSocket类能创建Socket的服务端&#xff0c;Socket能创建Socket的客户端 ServerSocket中…

如何运用数字孪生可视化技术实现三维可视化智慧园区

随着城市化的进程和信息化的发展&#xff0c;越来越多的城市拥有了智慧园区这一新的城市形态&#xff0c;通过“互联网”和物联网技术&#xff0c;实现了各种功能部门之间的信息共享与协同&#xff0c;提高了园区服务的质量和效率。然而&#xff0c;如何更好地实现园区管理和运…

LeetCode - 168. Excel表列名称

168. Excel表列名称 给你一个整数 columnNumber &#xff0c;返回它在 Excel 表中相对应的列名称。例如&#xff1a; A -> 1 B -> 2 C -> 3 ... Z -> 26 AA -> 27 AB -> 28 ... 二进制与十进制之间的转换 在做这题之前&#xff0c;先复习一下二进制与十进…

JUC并发编程之读写锁原理

1.图解流程 读写锁用的是同一个 Sycn 同步器&#xff0c;因此等待队列、state等也是同一个 t1 w.lock &#xff0c; t2 r.lock t1 成功上锁&#xff0c;流程与 ReentrantLock 加锁相比没有特殊之处&#xff0c;不同的是写锁状态占了 state 的低 16 位&#xff0c;而读锁使用…

多线程并发编程-线程篇

线程基础 什么是线程&#xff1f; 系统中的一个程序就是一个进程&#xff0c;每个进程中的最基本的执行单位&#xff0c;执行路径就是线程&#xff0c;线程是轻量化的进程。 什么是纤程&#xff1f; 绿色线程&#xff0c;由用户自己进行管理的而不是系统进行管理的&#xf…

【教程类】IDEA 打包 jar 包

最近有点累&#xff0c;写点简单的图文教程的东西来缓解一下 一、你需要知道的基础概念 了解了基础概念之后&#xff0c;可以让我们学习的更快更好哦 ~~ 1. jar JAR&#xff08;Java Archive&#xff09;是Java中一种常用的归档文件格式&#xff0c;也可以被视为一种压缩文…

学生就业统计表案例

主要分为三块&#xff1a; 渲染业务新增业务删除业务 一、根据持久化数据渲染页面 核心步骤: 读取localstorage 本地数据 如果有数据则转换为对象放到变量里面一会使用它渲染页面如果没有则用默认空数组 []为了测试效果&#xff0c;咱们可以先把initData 存入本地存储看效果…

android存储1--device解锁前的流程

android版本&#xff1a;android-11.0.0_r21http://aospxref.com/android-11.0.0_r21/ 一、主用户primary user的创建 开机后kernel启动第一个用户态进程init&#xff0c;init进程fork出zygote进程。zygote又fork出system server进程。http://aospxref.com/android-11.0.0_r2…

垃圾收集器面试总结(一)

垃圾收集器 Serial 收集器&#xff08;GC日志标识&#xff1a;DefNew&#xff09; Serial&#xff08;串行&#xff09;收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。 它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾…

[比赛简介]BirdCLEF-2023

比赛链接&#xff1a;BirdCLEF 2023 | Kaggle 比赛简介 鸟类是生物多样性变化的极好指标&#xff0c;因为它们具有高度流动性并且具有不同的栖息地要求。因此&#xff0c;物种组合和鸟类数量的变化可以表明恢复项目的成败。然而&#xff0c;经常在大面积地区进行传统的基于观…

你的车有通风座椅吗?新款奔驰S400升级原厂主副驾座椅通风

大家好&#xff0c;我是奔之升小志&#xff08;bzs878&#xff09;&#xff0c;专注名车原厂升级&#xff0c;欢迎戳戳右上角“”号关注一下&#xff0c;持续为您带来精彩改装案例。 座椅通风有什么用&#xff1f;能改善身体与座椅接触面空气流通&#xff0c;达到不出汗的效果…

Linux网络服务----SSH

文章目录 一 、SSH服务1.1 什么是SSH服务器&#xff1f;1.2 常用的SSH软件的介绍 二 、ssh的运用2.1 存放ssh服务端的配置文件2.2 ssh在Linux中的密码登录2.3 利用ssh协议传输文件和获取文件2.4 sftp远程访问操作 三 、 ssh密钥登录操作四 、TCP_wapper的原理和运用4.1 TCP_wap…

IP-GUARD能否实现打印指定文件时需经过管理员审批后才能打印?

支持。先设置禁止打印文档的策略,然后设置相关审批流程,再给到客户端相应的申请权限: 1、在控制台-高级-打印控制策略中,给需要进行打印管控的客户端设置以下策略: 动作:禁止 2、在控制台-申请管理-桌面申请管理-审批流程管理中,添加申请类型为打印的审批流程,指定审批人…

通过ADB实现移动端h5项目无线真机调试(超级简单!)

前言 做移动端h5项目的时候&#xff0c;电脑浏览器调试样式和效果&#xff0c;可能和真机展示出来的效果有差距&#xff0c;比如有的手机开启了home键&#xff0c;比如文字大小等样式有偏差。虽然可以通过手机扫描网页二维码在手机上看样式&#xff0c;但是和真机还是有区别。…

每天一道大厂SQL题【Day23】华泰证券真题实战(五)

每天一道大厂SQL题【Day23】华泰证券真题实战(五) 大家好&#xff0c;我是Maynor。相信大家和我一样&#xff0c;都有一个大厂梦&#xff0c;作为一名资深大数据选手&#xff0c;深知SQL重要性&#xff0c;接下来我准备用100天时间&#xff0c;基于大数据岗面试中的经典SQL题&…

Veritas 与星辰天合的官方一体化方案来了

11&#xff1e;2&#xff0c;XSKY星辰天合联手 Veritas 贡献企业数据管理最佳实践。 近日&#xff0c;XSKY星辰天合以“科技联盟伙伴”身份亮相 2023 Veritas Solution Day&#xff0c;并宣布与 Veritas 推出联合解决方案。双方将携手为大型企业客户带来业界领先的数据存储与保…