APP架構(gòu)的一些思考

先上代碼MVVM

aar/source

一般來說組件化項(xiàng)目中都會(huì)做aar和源碼切換,開發(fā)同學(xué)正在進(jìn)行的業(yè)務(wù)module需依賴源碼衣陶,其它的不相干的模塊依賴遠(yuǎn)程aar说莫。大概會(huì)先定義一個(gè)全局變量做aar/source切換的開關(guān)互例,然后在app中進(jìn)行依賴粗井。
module.gradle

ext {
    // source/aar
    isBusinessDev = false
    biz = [
            business: "com.xxx.xxx:xxx:1.0.0",
    ]
}

app下build.gradle

dependencies {
    ...
    if (rootProject.ext.isBusinessDev) {
        implementation project(path: ':business')
    } else {
        implementation rootProject.ext.biz.business
    }
}

沒啥毛病枝哄,問題是隨著業(yè)務(wù)迭代module逐漸變多愈犹,需要不停的往app中添加這樣if else的依賴控制代碼键科,倒也不是if else不好,很多的if else就有點(diǎn)難受受了漩怎。思考一下項(xiàng)目中第三方依賴是怎么偷懶的勋颖。
config.gradle中定義好依賴庫版本號(hào)、依賴路徑

versions = [
            kotlin              : '1.5.20',
            coroutine           : '1.5.2',
            androidx_core       : '1.3.2',
            appcompat           : '1.2.0',
            lifecycle           : '2.3.1',
            work_manager        : '2.5.0',
            room                : '2.2.5',
            constraintlayout    : '2.0.4',
            recyclerview        : '1.1.0',
            material            : '1.3.0',
]

common = [
            kotlin               : "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$versions.kotlin",
            coroutine            : "org.jetbrains.kotlinx:kotlinx-coroutines-android:$versions.coroutine",
            androidx_core        : "androidx.core:core-ktx:$versions.androidx_core",
            appcompat            : "androidx.appcompat:appcompat:$versions.appcompat",
            viewmodel            : "androidx.lifecycle:lifecycle-viewmodel-ktx:$versions.lifecycle",
            livedata             : "androidx.lifecycle:lifecycle-livedata-ktx:$versions.lifecycle",
            lifecycle            : "androidx.lifecycle:lifecycle-runtime-ktx:$versions.lifecycle",
            constraintlayout     : "androidx.constraintlayout:constraintlayout:$versions.constraintlayout",
            recyclerview         : "androidx.recyclerview:recyclerview:$versions.recyclerview",
            material             : "com.google.android.material:material:$versions.material",
]

然后在管理依賴的module中一個(gè)循環(huán)搞定

dependencies {
    rootProject.ext.common.each { k, v -> api v }
}

arr/source切換也搞個(gè)循環(huán)好了

ext {
    // source/aar
    biz = [
            business: [false, "com.xxx.xxx:xxx:1.0.0"],
    ]

    // module
    modules = [
            business: project(':business'),
    ]
}

app中修改依賴方式

dependencies {
    ...
    biz.each { entry ->
        if (entry.value[0]) {
            implementation entry.value[1]
        } else {
            implementation modules.(entry.key)
        }
    }
}

application/library

為了方便調(diào)試勋锤,很多時(shí)候我們希望業(yè)務(wù)module能單獨(dú)run起來饭玲,讓業(yè)務(wù)module獨(dú)立運(yùn)行∪矗可以給業(yè)務(wù)module加一個(gè)開關(guān)茄厘,然后讓業(yè)務(wù)module用這個(gè)開關(guān)控制application/library的切換。當(dāng)然此種情況下谈宛,我們希望app也能獨(dú)立運(yùn)行次哈,如此一來,業(yè)務(wù)module作為application獨(dú)立運(yùn)行時(shí)吆录,app需剔除該業(yè)務(wù)module的依賴窑滞。

修改module.gradle

ext {
    // source/aar
    biz = [
            business: [false, "com.xxx.xxx:xxx:1.0.0"],
    ]

    // library/application
    isBusinessModule = true

    // module
    modules = [
            business: [isBusinessModule, project(':business')],
    ]
}

app下build.gradle再加個(gè)判斷,module作為applicaiton時(shí)不進(jìn)行依賴恢筝。

dependencies {
    biz.each { entry ->
        if (entry.value[0]) {
            implementation entry.value[1]
        } else {
            if (modules.(entry.key)[0]) {
                implementation modules.(entry.key)[1]
            }
        }
    }
}

業(yè)務(wù)moduel下的build.gradle用module.gradle中定義好的變量isBusinessModule控制application/library切換哀卫。

def isModule = rootProject.ext.isBusinessModule
if (isModule) {
    apply plugin: 'com.android.library'
} else {
    apply plugin: 'com.android.application'
}

別忘了application需配置applicationId,manifest需指定啟動(dòng)項(xiàng)和Application撬槽。

android {
    resourcePrefix "business_"

    defaultConfig {
        ...
        if (!isModule) {
            applicationId "com.xxx.xxx.xxx"
        }
    }

    sourceSets {
        main {
            if (isModule) {
                manifest.srcFile 'src/main/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/manifest/AndroidManifest.xml'
            }
        }
    }
}

反射初始化子類對(duì)象

之前看有的同學(xué)搞了些騷操作此改,在基類初始化ViewBinding。想來也是在父類中拿到泛型類型侄柔,然后反射初始化带斑。ViewBinding生成的類格式是固定的鼓寺,直接匹配類名,反射實(shí)例化然后調(diào)用ViewBinding.inflate()方法返回ViewBinding實(shí)例勋磕。

abstract class BaseSimpleActivity<VB : ViewBinding> : BaseActivity() {
    protected val binding by lazy {
        createViewBinding()
    }

    open fun createViewBinding() = reflectViewBinding() as VB

    @Suppress("UNCHECKED_CAST")
    private fun reflectViewBinding(): VB? {
        val types = (javaClass.genericSuperclass as ParameterizedType).actualTypeArguments
        types.forEach {
            if (it.toString().endsWith("Binding") && it is Class<*>) {
                val method = it.getDeclaredMethod("inflate", LayoutInflater::class.java)
                return method.invoke(it, layoutInflater) as VB
            }
        }
        return null
    }
}

當(dāng)然為了防止意外狀況或者是性能問題妈候,createViewBinding()方法默認(rèn)實(shí)現(xiàn)為反射,加個(gè)open修飾讓子類可以重寫自己提供ViewBinding對(duì)象挂滓。

嗯苦银,這樣很香啊~子類拿著binding直接用就好了。等等ViewModel是不是也可以這么搞呢赶站,當(dāng)然可以幔虏,搞一搞。

abstract class BaseVMActivity<VM : BaseViewModel<R>, R : BaseRepository, VB : ViewBinding> :
    BaseSimpleActivity<VB>() {
    protected val viewModel: VM by lazy {
        createViewModel()
    }

    open fun createViewModel() = reflectViewModel()

    @Suppress("UNCHECKED_CAST")
    private fun reflectViewModel(): VM {
        val types = (javaClass.genericSuperclass as ParameterizedType).actualTypeArguments
        return ViewModelProvider(this)[types[0] as Class<ViewModel>] as VM
    }
}

ViewModel的實(shí)現(xiàn)類命名可能并非以ViewModel結(jié)尾贝椿,這里直接取第一個(gè)泛型類型types[0]想括。同樣的createViewModel()默認(rèn)實(shí)現(xiàn)為反射,加個(gè)open讓子類可以重寫烙博。

網(wǎng)絡(luò)請(qǐng)求綁定進(jìn)度對(duì)話框

在網(wǎng)絡(luò)請(qǐng)求開始的時(shí)候彈一個(gè)菊花圈瑟蜈,結(jié)束/失敗的時(shí)候關(guān)閉。因?yàn)橛玫絽f(xié)程viewModelScope渣窜,就把launch方法又封裝了一下铺根。

    internal typealias NetLaunch<T> = suspend CoroutineScope.() -> BaseResponse<T>

    val statusLiveData: MutableLiveData<CoroutineState> by lazy {
        MutableLiveData<CoroutineState>()
    }

    fun <T> start(
        refresh: Boolean = true,
        block: NetLaunch<T>,
    ): LaunchHandler<T> {
        val launchHandler = LaunchHandler<T>()
        viewModelScope.launch {
            try {
                if (refresh) {
                    statusLiveData.value = CoroutineState.REFRESH
                } else {
                    statusLiveData.value = CoroutineState.START
                }
                val result = block()
                statusLiveData.value = CoroutineState.FINISH
                launchHandler.successListener?.invoke(
                    LaunchResult.Success(result)
                )
            } catch (e: Exception) {
                statusLiveData.value = CoroutineState.ERROR
                if (launchHandler.errorListener == null) {
                    errorLiveData.value = e
                } else launchHandler.errorListener?.invoke(LaunchResult.Error(e))
            }
        }
        return launchHandler
    }

先忽略其它代碼,主要看statusLiveData乔宿,將協(xié)程狀態(tài)發(fā)送到Activity基類BaseVMActivity位迂,在基類中進(jìn)行處理。

private fun initViewModelActions() {
        viewModel.statusLiveData.observe(this, { status ->
            status?.run {
                when (this) {
                    CoroutineState.START -> {
                        //START
                    }
                    CoroutineState.REFRESH -> {
                        //REFRESH
                        ProgressDialog.showProgress(this@BaseVMActivity)
                    }
                    CoroutineState.FINISH -> {
                        //FINISH
                        ProgressDialog.dismissProgress()
                    }
                    CoroutineState.ERROR -> {
                        //ERROR
                        ProgressDialog.dismissProgress()
                    }
                }
            }
        })
        //默認(rèn)錯(cuò)誤處理
        viewModel.errorLiveData.observe(this, {
            ToastUtils.showShort(it.message)
        })
    }

不管用沒用到協(xié)程详瑞,思路是一致的掂林。網(wǎng)絡(luò)請(qǐng)求入口包裹一層,將狀態(tài)發(fā)送到頁面基類坝橡,在基類統(tǒng)一處理泻帮。

網(wǎng)絡(luò)請(qǐng)求API設(shè)計(jì)

現(xiàn)在都是MVVM了,那就在BaseViewModel里面統(tǒng)一驳庭。寫kotlin還是要有kotlin的風(fēng)格刑顺,搞一些lambda氯窍。
BaseViewModel

    val statusLiveData: MutableLiveData<CoroutineState> by lazy {
        MutableLiveData<CoroutineState>()
    }

    val errorLiveData: MutableLiveData<Exception> by lazy {
        MutableLiveData<Exception>()
    }

    fun <T> start(
        refresh: Boolean = true,
        block: NetLaunch<T>,
    ): LaunchHandler<T> {
        val launchHandler = LaunchHandler<T>()
        viewModelScope.launch {
            try {
                if (refresh) {
                    statusLiveData.value = CoroutineState.REFRESH
                } else {
                    statusLiveData.value = CoroutineState.START
                }
                val result = block()
                statusLiveData.value = CoroutineState.FINISH
                launchHandler.successListener?.invoke(
                    LaunchResult.Success(result)
                )
            } catch (e: Exception) {
                statusLiveData.value = CoroutineState.ERROR
                if (launchHandler.errorListener == null) {
                    errorLiveData.value = e
                } else launchHandler.errorListener?.invoke(LaunchResult.Error(e))
            }
        }
        return launchHandler
    }

LaunchHandler

class LaunchHandler<T> {
    var successListener: HandlerSuccess<T>? = null
    var errorListener: HandlerError? = null
}

infix fun <T> LaunchHandler<T>.resumeWithSuccess(handler: HandlerSuccess<T>) = this.apply {
    successListener = handler
}

infix fun <T> LaunchHandler<T>.resumeWithError(handler: HandlerError) = this.apply {
    errorListener = handler
}

不傳錯(cuò)誤回調(diào)函數(shù)的情況下饲常,將錯(cuò)誤狀態(tài)errorLiveData發(fā)送給Activity基類Toast處理,當(dāng)然這里也可以按照狀態(tài)碼做對(duì)應(yīng)的Toast狼讨,最終調(diào)用處就很舒服了:MainViewModel

    val contentLiveData by lazy {
        MutableLiveData<String>()
    }

    fun getChapters() {
        start {
            repository.getChapters()
        } resumeWithSuccess {
            contentLiveData.value = GsonUtils.instance.toJson(it.response.data)
        } resumeWithError {
            ToastUtils.showShort(it.error.message)
        }
    }

resumeWithSuccess贝淤、resumeWithError方法是中綴函數(shù),調(diào)用時(shí)可以省略一個(gè)點(diǎn)政供。

repository.getChapters()

class MainRepository : BaseRepository() {
    private val service = ApiServiceUtil.getApiService<MainApiService>()

    suspend fun getChapters(): BaseResponse<List<Chapters>> {
        return withContext(Dispatchers.IO) {
            service.getChapters()
        }
    }
}

interface MainApiService : BaseService {
    @GET("/wxarticle/chapters/json")
    suspend fun getChapters(): BaseResponse<List<Chapters>>
}

沒啥好說的播聪,retrofit接口定義suspend方法朽基,repository中withContext(Dispatchers.IO)切換到IO線程。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末离陶,一起剝皮案震驚了整個(gè)濱河市稼虎,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌招刨,老刑警劉巖霎俩,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異沉眶,居然都是意外死亡打却,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門谎倔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來柳击,“玉大人,你說我怎么就攤上這事片习“齐龋” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵毯侦,是天一觀的道長(zhǎng)哭靖。 經(jīng)常有香客問我,道長(zhǎng)侈离,這世上最難降的妖魔是什么试幽? 我笑而不...
    開封第一講書人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮卦碾,結(jié)果婚禮上铺坞,老公的妹妹穿的比我還像新娘。我一直安慰自己洲胖,他們只是感情好济榨,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著绿映,像睡著了一般擒滑。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上叉弦,一...
    開封第一講書人閱讀 51,125評(píng)論 1 297
  • 那天丐一,我揣著相機(jī)與錄音,去河邊找鬼淹冰。 笑死库车,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的樱拴。 我是一名探鬼主播柠衍,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼洋满,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了珍坊?” 一聲冷哼從身側(cè)響起牺勾,我...
    開封第一講書人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎阵漏,沒想到半個(gè)月后禽最,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡袱饭,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了虑乖。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片懦趋。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖疹味,靈堂內(nèi)的尸體忽然破棺而出仅叫,到底是詐尸還是另有隱情,我是刑警寧澤糙捺,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布诫咱,位于F島的核電站,受9級(jí)特大地震影響洪灯,放射性物質(zhì)發(fā)生泄漏坎缭。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一签钩、第九天 我趴在偏房一處隱蔽的房頂上張望掏呼。 院中可真熱鬧,春花似錦铅檩、人聲如沸憎夷。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽拾给。三九已至,卻和暖如春兔沃,著一層夾襖步出監(jiān)牢的瞬間蒋得,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來泰國(guó)打工粘拾, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留窄锅,地道東北人创千。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓缰雇,卻偏偏與公主長(zhǎng)得像入偷,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子械哟,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容