Implementing WanAndroid client based on componentization + modularization + Kotlin + coroutine + Flow + Retrofit + Jetpack + MVVM architecture

Preface

I have always wanted to write a WanAndroid project to consolidate my Kotlin+Jetpack+协程learning of peer-to-peer knowledge, but I never had the time. Here we started to take action again. It took two months from the project establishment to the completion. Normally, I have less time, so I basically can only use fragmented time to write. But I no longer want to write a simple Android project. I learn and draw experience from multiple large-scale projects, and build an architectural model that is suitable for large-scale projects from 0 to 1.

This may be a microcosm, but although the sparrow is small, it has all the internal organs, which will definitely give everyone some ideas and thoughts. Of course, the functions of this project are not all perfect, because our purpose is not to build a WanAndroid client, but to learn to build and use the architecture of Kotlin+coroutine+Flow+Retrofit+Jetpack+MVVM+componentization+modularization+short video , better improve yourself. In the future, I will continue to improve and optimize it. In addition to ensuring that it has a normal APP function, I will continue to add practices such as Compose, 依赖注入Hint, 性能优化, MVI模式, etc.支付功能

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

1. Project Introduction

  • The project is written in Kotlin language, combined with the MVVM architecture model built by Jetpackrelated controls, Navigation, Lifecyle, DataBinding, LiveData, etc.;ViewModel

  • Through componentization and modular splitting, the project can be better decoupled and reused, and ARouter can realize communication between modules;

  • Use coroutine+Flow+Retrofit+OkHttp to elegantly implement network requests;

  • Implement data cache management through databases mmkv, etc.;Room

  • Use Google ExoPlayerto implement short video playback;

  • Use Glideto complete image loading;

  • An Android client implemented through the API provided by WanAndroid.

5c1c8d8db0fce91c069a5880f9f54450.jpegThe project uses the MVVM architecture pattern and basically follows the architecture recommended by Google. RepositoryGoogle believes that ViewModelis only used for data storage, and data loading should be completed Repositoryby . The data is cached through Roomthe database, and the cached data is displayed first when there is no network or weak network.

ced05ddafcab09d04c89ecdc4811ec8b.jpeg

Project screenshot:

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

project address:

https://github.com/suming77/SumTea_Android

2. Project details

2.1 Infrastructure

(1) BaseActicity

Through the single responsibility principle, functional hierarchy is achieved, and users only need to inherit as needed.

  • BaseActivity : Encapsulates the common init method, initializes the layout, loads the pop-up box and other methods, and provides the original way to add the layout;

  • BaseDataBindActivity : Inherited from BaseActivity, bind layout through dataBinding, use generic parameter reflection to create layout file instances, obtain layout view, no longer needed 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 : Inherited from BaseDataBindActivity, automatically creates ViewModelinstances through generic parameter reflection, making it easier to use to ViewModelimplement network requests.

val argument = (this.javaClass.genericSuperclass as ParameterizedType).actualTypeArguments
mViewModel = ViewModelProvider(this).get(argument[1] as Class<VM>)
(2) BaseFragment

The encapsulation of BaseFragment is similar to BaseActivity above.

(3) BaseRecyclerViewAdapter

  • BaseRecyclerViewAdapter : Encapsulates RecyclerViewAdapterthe base class to provide creation ViewHoldercapabilities, the ability to add head and tail layouts, general Item click events, and dataBinding capabilities. It is no longer needed findViewById(). It provides a variety of ways to refresh data, global refresh, local refresh, etc.

  • BaseMultiItemAdapter : Provides an Adapter that implements multiple different layouts. It implements different layouts based on different ViewTypes ViewBinding, and then creates and returns different ones ViewHolder.

(4) Ext expansion class

The project provides a large number of control extension classes, which can be developed quickly and improve efficiency :

  1. ResourceExt : Resource file extension class;

  2. TextViewExt : TextView extension class;

  3. SpanExt : Span extension class to achieve various Span effects;

  4. RecyclerViewExt : One line of code to quickly add vertical dividing lines and grid dividing lines;

  5. ViewExt : View extension class, which implements click anti-shake, adds spacing, sets width, sets visibility, etc.;

  6. EditTextExt : Construct the input box text change flow through Flow filter{}to implement data filtering, avoid invalid requests, and debounce()achieve anti-shake;

  7. GsonExt : Quickly convert between Bean and Json in one line of code.

//将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 is a high-performance text storage solution that has withstood the test of hundreds of millions of WeChat users in real environments and has good stability. Because it is implemented in C language, it has the advantages of low performance, small memory, and fast storage speed. It supports the use of multi-threads and even multi-processes, and supports regular deletion of logs. At the same time, it has a specific algorithm to compress files. File encryption can even be configured.

Use Xlog to build a client runtime log system, retrieve remote logs on demand, and record key execution processes in the form of management.

2.2 Jetpack components

47ab054d0fa216404903650c9ba8179d.jpegAndroid Jetpack is a set of Android software components, tools, and guides that help developers build high-quality, stable Android applications. Jetpack includes several libraries designed to solve common problems in Android application development and provide a consistent API and development experience.

Only a small part of the components in the picture above are used in the project.

(1) Navtgation

As a framework for building in-application interfaces, Navtgation focuses on making single-Activity applications the preferred architecture (an application only needs one Activity), and its positioning is page routing.

The homepage in the project is divided into 5 tabs, mainly homepage, category, system, and mine . Use BottomNavigationView+ Navigationto build. Use menu to configure the bottom menu, and use NavHostFragmentto configure each Fragment. At the same time, it solves the problem that when used in combination with tab, Fragment will be re-created every time Navigation.  BottomNavigationViewThe solution is to customize FragmentNavigatorand replace()replace the internal with show()/hide().

(2) ViewBinding&DataBinding

  • ViewBindingThe emergence of is no longer necessary to write findViewById();

  • DataBindingIt is a tool that solves the two-way binding between View and data; reduces code templates and no longer needs to be written findViewById(); releases Activity/Fragment , and can complete data and event binding work in XML, allowing you to Activity/Fragmentpay more attention to the core business; Data binding is null-safe . Binding data in XML is null-safe because DataBindingboxing and null judgment are automatically performed on data binding, which greatly reduces NPE problems.

(3) ViewModel

ViewModelData storage component with life-awareness. Data will not be lost when the page configuration is changed. Data sharing (data sharing in a single Activity multi-Fragment scenario) manages interface-related data in a life cycle manner. It is usually used in conjunction with DataBinding to provide strong support for the implementation of the MVVM architecture.

(4) LiveData

LiveDataIt is a data subscription and distribution component with life cycle awareness. Supports shared resources (one data can be received by multiple observers), supports the distribution of sticky events, no longer needs to manually handle the life cycle (automatically associated with the host life cycle), and ensures that the interface conforms to the data status. Notify the View when the underlying database changes.

(5) Room

A lightweight ORM database, essentially a SQLite abstraction layer . Easier to use (Builder mode, similar to Retrofit), relevant functions are implemented in the form of annotations, and the implementation class IMPL is automatically generated during compilation .

This is mainly used to cache data for the homepage video list. LiveDataCombining it with Flow can avoid unnecessary NPE. It can monitor changes in data in the database table. It can also be used with RXJava's Observer. Once insert, update, delete and other operations occur, , Roomwill automatically read the latest data in the table, send it to the UI layer, and refresh the page.

Schematic diagram of the Room library architecture: 6957a82bbcddf1bdc89ee9cebf1fe7bc.jpegRoom consists of three main components:

  • Database class: used to save the database and serve as the main access point for the underlying connection of application persistent data;

  • Data entity: used to represent tables in the application's database;

  • Data Access Objects (DAO): Provides methods that your application can use to query, update, insert, and delete data in the database.

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()
        }
    }
}

Note: The method of defining database operations in Dao in the Room database must be used correctly, otherwise it will cause implementation class errors generated during Room compilation, compilation failure and other problems.

2.3 Network request library

The project's network request encapsulation provides two ways of implementation. One is coroutine + Retrofit + ViewModel + Repository , which adds a layer to manage network request calls like the official website Repository; the other way is to use Flow flow with Retrofit for a more elegant implementation. Network requests are more concise than the official website approach.

(1) Retrofit+Coroutine+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 usage

class HomeRepository : BaseRepository() {
    //项目tab
    suspend fun getProjectTab(): MutableList<ProjectTabItem>? {
        return requestResponse {
            ApiManager.api.getProjectTab()
        }
    }
}

Use of 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 elegantly implements network requests

Flow is actually very similar to RxJava, very convenient, and it is more concise to use it to make network requests.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 Image loading library

Glide

Image loading is simply encapsulated using Glide and extended function processing is performed on 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. scaleTypeFix the conflict between Glide's image cropping and ImageView's . Bitmap will be rounded and cropped first, and then loaded into ImageView. If the Bitmap image size is larger than the ImageView size, it will not be visible. If CenterCrop()overloading is used, the Bitmap will be centered and cropped first. Then fillet the corners so you can see them.

  2. Provides GIF image loading and image Gaussian blur effect functions.


2.5 WebView

We all know that the native WebView has many problems. Using Tencent X5 kernel WebView for encapsulation, compatibility, stability, security, and speed are greatly improved.

WebView is used in the project to display the article details page.

2.6 MMKV

MMKV is a key-value component based on mmap memory mapping. The underlying serialization/deserialization is implemented using protobuf, which has high performance and strong stability. Easy to use and supports multiple processes.

Initialize MMKV when the App starts and set the root directory of MMKV (files/mmkv/), for example  Application :

public void onCreate() {
    super.onCreate();


    String rootDir = MMKV.initialize(this);
    LogUtil.e("mmkv root: " + rootDir);
}

MMKV provides a global instance that can be used directly:

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

Loop and write randomly int 1k times, the performance comparison is as follows: db6df152090fba0bc635b0ab0646b853.jpegMMKV is used in the project to save user-related information, including user login cookies, user name, mobile phone number, search history data and other information.

2.7 ExoPlayer video player

ExoPlayerIt is an open source player launched by Google. It mainly integrates a decoding system provided by Android to parse video and audio. It is packaged MediaCodecvery well, forming a development player with superior performance and good playback stability. It supports more Multiple video playback formats (including DASH and DASH SmoothStreaming, these two MediaPlayerare not supported), through componentized custom players, easy expansion and customization, persistent cache, in addition, the ExoPlayerpackage size is light and easy to access.

Used in the project ExoPlayerto implement anti-tremolo short video playback:

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 Componentization & Modularization

Componentization & modularization are conducive to the separation of business modules, high cohesion, low coupling, and clear code boundaries. It is conducive to teamwork and multi-line development, speeds up compilation, improves development efficiency, makes management more convenient, and is conducive to maintenance and iteration. 74ba0990e3dfa7e1b053e8385109ff0c.jpegThere is only one Application in the host App, and the entire business is split into various mod modules and lib component libraries. Encapsulate and extract some functional components into lib to provide dependencies to the upper layer. There are no task dependencies between mod modules and communication is done through Arouter.

(1) Modular

In the project, the App is split into the home page module, login module, search module, user module, video module, etc. based on the business dimension. They cannot access each other and cannot be used as dependencies. At the same time, they all depend on the basic library and network requests. Library, public resource library, image loading library , etc. If you also need to use the launcher component, Banner component, database Room component, etc., add them separately as needed.

The APP shell project is responsible for packaging environment, signatures, obfuscation rules, business module integration, APP theme and other configuration work, and generally does not include any business.

(2) Componentization

The most obvious difference between modularization and componentization is that modules are larger in granularity than components . A module may contain multiple components . When dividing, modularization is business-oriented, and componentization is function-oriented . Componentization is an evolution based on the idea of ​​modularity.

The project is divided into components such as the launcher component, Banner component, and database Room component based on functional dimensions . Engineering drawing after modularization and componentization split:

dbc5ea84d46665c9a66f604cb1e64fa2.jpeg

(3) Communication between components

After componentization, you cannot directly access the classes and methods of other modules. This is a rather prominent problem. It can be used directly LogintManagerto pull up login and determine whether you have logged in, but this class has been split into the mod_login module, and Business modules cannot depend on each other, so they cannot be used directly in other modules LogintManager.

It mainly uses Alibaba's routing framework ARouter to realize communication between components and expose externally provided capabilities in the form of interfaces.

For example, if it is created under the service package in the public resource library ILoginServiceto provide the ability to expose login to the outside world, and an implementation class is provided in the mod_login module LoginServiceImpl, any module can use it LoginServiceProviderto provide iLoginServicethe ability to expose it to the outside world.

  1. Created in the public resource library ILoginService, providing the ability to expose login to the outside world.

interface ILoginService : IProvider {
    //是否登录
    fun isLogin(): Boolean


    //跳转登录页
    fun login(context: Context)


    //登出
    fun logout(
        context: Context,
        lifecycleOwner: LifecycleOwner?,
        observer: Observer<Boolean>
    )
}
  1. LoginServiceA concrete implementation is provided in the mod_login module 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. Create LoginServiceProvider, obtain LoginService, and provide usage methods in the public resource library.

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)
    }
}

Then other modules can provide exposed capabilities to the outside world through LoginServiceProvideruse . iLoginServiceAlthough it seems that this will be more complicated, a single project may be more suitable for us. Every class can be directly accessed and every method can be directly called. However, we cannot be limited to a single-person development environment. In actual scenarios, multiple people Collaboration is the norm, and modular development is mainstream .

(4) Module runs alone

Allows modules to switch features between integrated and independent debugging. It is library when packaging and application when debugging.

  1. config.gradleAdd parameters to the file isModule:

//是否单独运行某个module
isModule = false
  1. Add a judgment to Moduleeach to distinguish whether it is an application or a library:build.gradleisModule

// 组件模式和基础模式切换
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. By modifying SourceSetsthe properties in , you can specify the source file that needs to be compiled. If it is a library, compile it under the manifest AndroidManifest.xml. Otherwise, compile it directly under the debug directory AndroidManifest.xml, and add parameters such as Applicationand .intent-filter

Doubt one :

As for modules being compiled and run separately, this is a pseudo requirement . In fact, there must be scenarios of communication between multiple modules. Otherwise, how to solve cross-module service extraction and acquisition, initialization tasks, and joint testing between modules? After a module is running, it needs to communicate with other modules, such as providing external services and obtaining services. The modules associated with it cannot be used if they are not running.

AndoidManifestAt the same time, two sets of sourceSets and Javasource directories need to be maintained . This is not only troublesome but also requires synchronization for a period of time each time it is changed. Therefore, whether this popular modular independent compilation form is really suitable is a matter of opinion.

3. Write at the end

If you need more detailed code, you can view it in the project source code. The address is given below. Due to the rush of time, some functions in the project have not yet been perfected, or some implementation methods need to be optimized. There are also more Jetpack components that have not yet been implemented in the project, such as 依赖注入Hilt, 相机功能CameraX, 权限处理Permissions, 分页处理Pagingetc. The continuous iterative update of the project is still an arduous and protracted battle.

In addition to Kotlin + MVVM + Android Jetpack + 协程 + Flow + 组件化 + 模块化 + 短视频the knowledge you can learn, I believe you can also learn from my project:

  1. How to use Charles to capture packets.

  2. Provides a large number of extension functions for rapid development and improved efficiency.

  3. ChipGroupand FlexboxLayoutManagermany other native ways to implement fluid layout.

  4. It complies with Alibaba Java development specifications and Alibaba Android development specifications, and has good comments.

  5. CoordinatorLayoutAnd Toolbarrealize the home page column ceiling effect and carousel movie effect.

  6. Use to ViewOutlineProvideradd rounded corners to controls, greatly reducing handwritten shape rounded corners xml.

  7. ConstraintLayoutIt is used in almost every interface layout ConstraintLayout.

  8. The asynchronous task launcher elegantly handles the problem of synchronous initialization tasks in Application, effectively reducing the time it takes to start the APP.

  9. Whether it is modularization or componentization, their essential ideas are the same, which is to break the whole into parts and simplify the complex. The purpose of both is to reuse and decouple, but the names are different.

project address:

https://github.com/suming77/SumTea_Android

Follow me to get more knowledge or contribute articles

e0986bbf7fd04f91e0bc615494d56d7f.jpeg

28a1228f57afb120ea43ff0f16816f3c.jpeg

Guess you like

Origin blog.csdn.net/c6E5UlI1N/article/details/131971003