MVVM+組件化+Arouter實現(xiàn)

MVVM+組件化實現(xiàn)

模塊概覽

底層模塊:common、network、resource

功能模塊:player、firebase、pay

界面模塊:mobile、tablet、tablet_login

殼模塊: app

模塊實現(xiàn)

1.公共模塊的gradle配置

由于不同模塊,可能引用相同的依賴庫,那么對于這部分共同的模塊則需要提取出來做統(tǒng)一的管理,因此在項目的根目錄創(chuàng)建了common.gradle。

//project是根項目,可以簡單認為是個對象,對象持有一個名詞為ext的成員變量
project.ext {
    
    //基本的變量配合和版本控制
    compileSdkVersion = 30
    buildToolsVersion = "30"
    minSdkVersion = 21
    targetSdkVersion = 30
    applicationId = "com.sdmc.xmediatv"
    versionCode = 4
    versionName = "4.7.0"

    //設(shè)置app配置
    setAppDefaultConfig = {
        extension ->
            //指定為application,代表該模塊可以單獨調(diào)試
            extension.apply plugin: 'com.android.application'
            extension.description "app"
            
            //公共的apply 主要是用于三方庫
            extension.apply plugin: 'kotlin-android'
            extension.apply plugin: 'kotlin-android-extensions'
            extension.apply plugin: 'kotlin-kapt'
          
            //設(shè)置項目的android
            setAppAndroidConfig extension.android
            //設(shè)置項目的三方庫依賴
            setDependencies extension.dependencies

    }

    //設(shè)置lib配置(只可以作為lib,不可單獨調(diào)試)
    setLibDefaultConfig = {
        extension ->
            //library,代表只是單純的庫,不需要依賴其他模塊
            extension.apply plugin: 'com.android.library'
            extension.description "lib"
           
            setLibAndroidConfig extension.android
            setDependencies extension.dependencies
    }

    //是否允許module單獨調(diào)試
    isModuleDebug = false
    //動態(tài)改變,用于單模塊調(diào)試
    setAppOrLibDefaultConfig = {
        extension ->
            if (project.ext.isModuleDebug) {
                extension.apply plugin: 'com.android.application'
                extension.description "app"
            } else {
                extension.apply plugin: 'com.android.library'
                extension.description "lib"

            }
            extension.apply plugin: 'kotlin-android'
            extension.apply plugin: 'kotlin-android-extensions'
            extension.apply plugin: 'kotlin-kapt'
            //設(shè)置通用Android配置
            setAppOrLibAndroidConfig extension.android
            //設(shè)置通用依賴配置
            setDependencies extension.dependencies
    }

    //設(shè)置application 公共的android配置
    setAppAndroidConfig = {
        extension -> //extension 相當于 android 對象
            extension.compileSdkVersion project.ext.compileSdkVersion
            extension.buildToolsVersion project.ext.buildToolsVersion
            extension.defaultConfig {
                applicationId project.ext.applicationId
                minSdkVersion project.ext.minSdkVersion
                targetSdkVersion project.ext.targetSdkVersion
                versionCode project.ext.versionCode
                versionName project.ext.versionName

                extension.flavorDimensions "versionCode"


                testInstrumentationRunner "android.support.tablet_test.runner.AndroidJUnitRunner"

                //ARouter 編譯生成路由
                kapt {
                    arguments {
                        arg("AROUTER_MODULE_NAME", project.getName())
                        includeCompileClasspath = true
                    }

                }
            }
            extension.buildFeatures {
                dataBinding = true
                // for view binding :
                // viewBinding = true
            }
    }

    //設(shè)置lib 公共的android配置
    setLibAndroidConfig = {
        extension -> //extension 相當于 android 對象
            extension.compileSdkVersion project.ext.compileSdkVersion
            extension.buildToolsVersion project.ext.buildToolsVersion
            extension.defaultConfig {
                minSdkVersion project.ext.minSdkVersion
                targetSdkVersion project.ext.targetSdkVersion
                versionCode project.ext.versionCode
                versionName project.ext.versionName

                testInstrumentationRunner "android.support.tablet_test.runner.AndroidJUnitRunner"

                //ARouter 編譯生成路由
                /*kapt {
                    arguments {
                        arg("AROUTER_MODULE_NAME", project.getName())
                    }
                }*/
            }
            extension.buildFeatures {
                dataBinding = true
                // for view binding :
                // viewBinding = true
            }
    }

    //設(shè)置通用的 android配置(可作為project單獨調(diào)試)
    setAppOrLibAndroidConfig = {
        extension -> //extension 相當于 android 對象
            extension.compileSdkVersion project.ext.compileSdkVersion
            extension.buildToolsVersion project.ext.buildToolsVersion
            extension.defaultConfig {
                minSdkVersion project.ext.minSdkVersion
                targetSdkVersion project.ext.targetSdkVersion
                versionCode project.ext.versionCode
                versionName project.ext.versionName

                extension.flavorDimensions "versionCode"


                testInstrumentationRunner "android.support.tablet_test.runner.AndroidJUnitRunner"

                //ARouter 編譯生成路由
                kapt {
                    arguments {
                        arg("AROUTER_MODULE_NAME", project.getName())
                        includeCompileClasspath = true
                    }

                }

            }
            extension.buildFeatures {
                //啟用自動綁定view id
                dataBinding = true
            }
        
            //使用的jdk版本
            extension.compileOptions {
                sourceCompatibility JavaVersion.VERSION_1_8
                targetCompatibility JavaVersion.VERSION_1_8
            }
            extension.kotlinOptions {
                jvmTarget = JavaVersion.VERSION_1_8
            }

            //動態(tài)改變清單文件資源指向
            extension.sourceSets {
                main {
                    if (project.ext.isModuleDebug) {
                        manifest.srcFile 'src/main/manifest/AndroidManifest.xml'
                    } else {
                        manifest.srcFile 'src/main/AndroidManifest.xml'
                    }
                }
            }

    }


    //公用的三方庫依賴,慎重引入,主要引入基礎(chǔ)庫依賴
    setDependencies = {
        extension ->
            extension.implementation fileTree(dir: 'libs', include: ['*.jar'])
            extension.kapt 'com.alibaba:arouter-compiler:1.2.2'

            //ARouter 路由apt插件,用于生成相應(yīng)代碼,每個module都需要
            extension.implementation 'com.alibaba:arouter-api:1.5.0'
            extension.implementation 'com.alibaba:arouter-compiler:1.2.2'
            extension.implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
            extension.implementation 'androidx.core:core-ktx:1.3.1'
            extension.implementation 'com.google.code.gson:gson:2.8.6'
            extension.implementation 'androidx.appcompat:appcompat:1.2.0'
            extension.implementation 'androidx.constraintlayout:constraintlayout:2.0.0'
            extension.implementation 'androidx.recyclerview:recyclerview:1.0.0'
    }
}

根目錄的build.gradle:

buildscript {
    //統(tǒng)一制定版本
    ext.kotlin_version = '1.4.31'
    ext.google_service_version = '19.7.0'
    ext.exoplayer_version = '2.13.2'
    
    repositories {
        //為當前的build.gradle設(shè)置依賴庫中心
        google()
        jcenter()
    }
    dependencies {
        //gradle版本
        classpath "com.android.tools.build:gradle:4.0.1"
        //kotlin依賴
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        //bintray.com maven的依賴
        classpath "com.jfrog.bintray.gradle:gradle-bintray-plugin:1.0"
        //sonatype maven的依賴
        classpath "com.github.dcendents:android-maven-gradle-plugin:1.5"
        //黃油刀
        classpath 'com.jakewharton:butterknife-gradle-plugin:10.2.3'
        //谷歌服務(wù)
        classpath 'com.google.gms:google-services:4.3.3'
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        //jitpack庫
        maven { url 'http://www.jitpack.io' }
        //bintray.com維護的Maven倉庫
        jcenter()
        //谷歌maven庫
        maven { url "https://maven.google.com" }
        google()
        //阿里云鏡像maven庫地址
        maven {
            url "http://maven.aliyun.com/nexus/content/repositories/releases"
        }
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

2.殼app模塊的實現(xiàn)

//從項目的根目錄中找到common.gradle
apply from: "${rootProject.rootDir}/common.gradle"

//project.ext則是之前強調(diào)的common.gradle中的對象,該對象有setAppDefaultConfig
project.ext.setAppDefaultConfig project

//由于setAppDefaultConfig中的android配置是通用配置,所以還需要針對殼本身新增配置
android {

    //混淆規(guī)則配置
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }

        debug {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

    packagingOptions {
        //nanohttpd輕量級數(shù)據(jù)庫所需
        exclude 'META-INF/nanohttpd/default-mimetypes.properties'
        exclude 'META-INF/nanohttpd/mimetypes.properties'
        //為了解決部分第三方庫重復打包了META-INF的問題
        exclude 'META-INF/NOTICE'
        exclude 'META-INF/LICENSE'
        //解決kotlin問題
        exclude("META-INF/*.kotlin_module")
    }

    compileOptions {
        sourceCompatibility = '1.8'
        targetCompatibility = '1.8'
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }

    //在本地loacl.propreties中配置打包所需信息
    Properties properties = new Properties()
    InputStream inputStream = project.rootProject.file('local.properties').newDataInputStream()
    properties.load(inputStream)
    def sdkDir = properties.getProperty('key.file')
    def keyFile = file(sdkDir)
    def key_keyAlias = properties.getProperty('keyAlias')
    def key_keyPassword = properties.getProperty('keyPassword')
    def key_storePassword = properties.getProperty('storePassword')

    signingConfigs {
        release {
            v2SigningEnabled true
        }
        debug {
            storeFile file(keyFile)
            storePassword key_storePassword
            keyAlias key_keyAlias
            keyPassword key_keyPassword
        }

    }
    /*android.applicationVariants.all {
        variant ->
            variant.outputs.all {
                output ->
                    outputFileName = new File("../version/", "XMediaTV_Android_" +
                            defaultConfig.versionName + "." + releaseTime() + "_" + variant.buildType.name + ".apk")
            }
    }*/

    //android打包提示check release builds false
    lintOptions {
        checkReleaseBuilds false
        abortOnError false
    }
}

dependencies {
    //殼工程所依賴的模塊,implementation代表不依賴模塊下的三分庫
    implementation project(path: ':mobile')
    implementation project(path: ':tablet')
    //api 代表依賴模塊下所有的庫
    api project(path: ':common')
    api project(path: ':firebase')
}
//設(shè)置倉庫地址,用于輔助三方庫依賴文件下載
repositories {
    mavenCentral()
}

3.底層模塊的model配置

common中的gradle文件:

common模塊本身就是為了通用,那么對于一些常用的庫,則需要在該模塊做一定的依賴處理,以下對不同模塊功能依賴做出了標注

apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.3"

    defaultConfig {
        minSdkVersion 21
        targetSdkVersion 29
        versionCode 1
        versionName "1.0.0"
        //設(shè)置預(yù)覽界面的庫支持
        vectorDrawables.useSupportLibrary = true
        testInstrumentationRunner "androidx.tablet_test.runner.AndroidJUnitRunner"
        //混淆文件指定
        consumerProguardFiles "consumer-rules.pro"

        //配合根目錄的gradle.properties,代碼中調(diào)用BuildConfig.SERVER_ADDRESS動態(tài)獲取對應(yīng)值,用于自動打包改服務(wù)地址
        buildConfigField "String", "SERVER_ADDRESS", "\"${SERVER_ADDRESS}\""
        buildConfigField "String", "TENANT_CODE", "\"${TENANT_CODE}\""
        buildConfigField "String", "AD_SERVER_ADDRESS", "\"${AD_SERVER_ADDRESS}\""
        buildConfigField "String", "PLAYER_SERVER_URI", "\"${PLAYER_SERVER_URI}\""
        buildConfigField "Boolean", "FORCE_USE_TABLET", project.properties.get("FORCE_USE_TABLET")
    }

    //設(shè)置arouter模塊名稱
    kapt {
        arguments {
            arg("AROUTER_MODULE_NAME", project.getName())
        }
    }

    //資源前綴強制命名
    resourcePrefix "common_"

    //混淆模式
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

    //用于換膚,讓所有model都可以擁有不同類型的資源目錄
    sourceSets {
        main {
            res.srcDirs = ['src/main/res',
                           'src/main/res-light',
                           'src/main/res-news']
        }
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"])
    //》》》》kotlin
    //kotlin核心庫配置,對應(yīng)根目錄的build.gradle版本
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.3.2'
    //kotlin協(xié)程庫
    api 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3-native-mt'
    api 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3-native-mt'


    //》》》》視圖
    //android視圖庫
    api 'androidx.appcompat:appcompat:1.2.0'
    api 'com.google.android.material:material:1.3.0'
    api 'androidx.constraintlayout:constraintlayout:2.0.1'
    api 'androidx.recyclerview:recyclerview:1.1.0'

    //glide圖片加載庫
    api 'com.github.bumptech.glide:glide:4.12.0'
    //圓角的ImageView庫
    api 'com.makeramen:roundedimageview:2.3.0'
    //recycleView的adapter庫
    api 'com.github.CymChad:BaseRecyclerViewAdapterHelper:3.0.4'
    //下拉刷新庫
    implementation 'com.lcodecorex:tkrefreshlayout:1.0.7'
    //頭條的自適應(yīng)庫
    api 'me.jessyan:autosize:1.2.1'
    //換膚庫
    api 'skin.support:skin-support:4.0.5'                   // skin-support
    api 'skin.support:skin-support-appcompat:4.0.5'         // skin-support 基礎(chǔ)控件支持
    api 'skin.support:skin-support-design:4.0.5'
    // skin-support-design material design 控件支持[可選]
    api 'skin.support:skin-support-cardview:4.0.5'
    // skin-support-cardview CardView 控件支持[可選]
    api 'skin.support:skin-support-constraint-layout:4.0.5'
    // skin-support-constraint-layout ConstraintLayout 控件支持[可選]
    //本地資源庫
    api project(path: ':resource')

    //》》》》網(wǎng)絡(luò)
    //用于retrofit2庫依賴
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
    implementation 'com.squareup.retrofit2:adapter-rxjava2:2.9.0'
    //用于retrofit2+kotlin+coroutines的回調(diào)庫,可以直接加上suspend關(guān)鍵字獲取到回調(diào)對象
    implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2'
    //okhttp3庫
    implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.2'
    //gson解析庫
    implementation 'com.google.code.gson:gson:2.8.6'

    //》》》》其余基本庫
    //郭霖的Litepal數(shù)據(jù)庫
    api 'org.litepal.guolindev:core:3.2.2'

    //sharePreference庫
    api 'com.orhanobut:hawk:2.0.1'


    //阿里的路由庫
    kapt 'com.alibaba:arouter-compiler:1.2.2'
    api 'com.alibaba:arouter-api:1.5.0'

    //谷歌的deeplink分享
    implementation 'com.google.firebase:firebase-dynamic-links:19.1.0'


    //--------------------------------------------------------------

    //》》》》lifecycle庫,用于liveData和viewModle,使用與mvvm框架
    def lifecycle_version = '2.2.0'
    // ViewModel and LiveData
    api "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
    // alternatively - just ViewModel
//    api "androidx.lifecycle:lifecycle-viewmodel:$lifecycle_version"
    api 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0'
    // For Kotlin use lifecycle-viewmodel-ktx
    // alternatively - just LiveData
//    api "androidx.lifecycle:lifecycle-livedata:$lifecycle_version"
    // 有生命周期感知的協(xié)程
    api 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0'

}
network中的gradle文件:

該模塊為網(wǎng)絡(luò)請求模塊,用到的框架是retrofit2 + kotlin + coroutines,本質(zhì)是請求耗時網(wǎng)絡(luò),通過kotlin使用coroutines的suspend掛起,來達到子線程請求,主線程接收數(shù)據(jù)的效果。

apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
    compileSdkVersion 29

    defaultConfig {
        minSdkVersion 21
        targetSdkVersion 29
        versionCode 1
        versionName "1.0.0"
        buildConfigField "String", "SERVER_ADDRESS", "\"${SERVER_ADDRESS}\""
        buildConfigField "String", "TENANT_CODE", "\"${TENANT_CODE}\""
        buildConfigField "String", "AD_SERVER_ADDRESS", "\"${AD_SERVER_ADDRESS}\""
        buildConfigField "String", "PLAYER_SERVER_URI", "\"${PLAYER_SERVER_URI}\""
        buildConfigField "Boolean", "FORCE_USE_TABLET", project.properties.get("FORCE_USE_TABLET")

    }
    resourcePrefix "network_"

    buildTypes {
        debug {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'

        }
        release {
            minifyEnabled true
        }
    }

    compileOptions {
        sourceCompatibility = '1.8'
        targetCompatibility = '1.8'
    }


}

dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"])

    //用于添加retrofit2攔截請求頭用的
    implementation 'com.squareup.okhttp3:logging-interceptor:4.8.1'
    //Chuck輔助工具,用于打印各種http請求
    debugImplementation 'com.readystatesoftware.chuck:library:1.1.0'
    releaseImplementation 'com.readystatesoftware.chuck:library-no-op:1.1.0'

    //--------------------------------------------------------------
    api project(path: ':common')
    implementation files('libs\\XMediaCryptoJar_HKSC.jar')
}
resoure的gradle:

該Model主要是用于存放抽取的顏色、多語言、以及與app相關(guān)的圖標、占位圖等

apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.3"

    defaultConfig {
        minSdkVersion 21
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        consumerProguardFiles "consumer-rules.pro"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

    sourceSets {
        main {
            res.srcDirs = ['src/main/res',
                           'src/main/res-light',
                           'src/main/res-news']
        }
    }
}

dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"])
    //kotlin
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.3.2'

    //單元測試庫
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'

}

基本模塊配置都是作為library庫使用,對于基本模塊的內(nèi)容有要求,就是一定是一項基礎(chǔ)并且多模塊共用的功能,才值得放在基礎(chǔ)模塊中。

4.功能模塊或界面模塊的依賴配置

對于模塊依賴,只要弄清楚api和implementation的功能,并且清楚common.gradle,是如何使用的,就能很清晰的使用模塊化項目開發(fā)。

apply from: "${rootProject.rootDir}/common.gradle"

//使用common.gradle中的公用配置
project.ext.setAppOrLibDefaultConfig project

android {
    resourcePrefix "tablet_"


    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }



}

dependencies {
    //這里統(tǒng)一依賴了network,network中由于是api common,common api resource,所以只需要implementation network就行
    implementation project(path: ':network')
    //統(tǒng)一依賴功能model,播放器、Firebase、支付
    implementation project(path: ':player')
    implementation project(path: ':firebase')
    implementation project(path: ':pay')
    //依賴了一個界面模塊,含各種平板的彈窗界面和登錄
    implementation project(path: ':tablet_login')
    //下拉刷新庫
    implementation 'com.lcodecorex:tkrefreshlayout:1.0.7'
    //輪播圖庫
    implementation 'com.github.zhpanvip:BannerViewPager:3.1.6'
    //谷歌支付庫
    implementation 'com.android.billingclient:billing:2.1.0'
    //依次是,谷歌消息、DeepLink、廣告
    implementation 'com.google.firebase:firebase-messaging:20.1.2'
    implementation 'com.google.firebase:firebase-dynamic-links:19.1.0'
    implementation "com.google.android.gms:play-services-ads:$google_service_version"
}

模塊內(nèi)容

1.common

image-20210331093833739.png
base包:

存放著各種基類,其中主要的是MVVM,所以著重講述MVVM框架搭建

ViewModel講解:

首先,要繼承ViewModel,導包,viewModel主要功能是,代替UI界面去做耗時操作,所以通過viewModelScope對象調(diào)用協(xié)程掛起,最終通過liveData綁定數(shù)據(jù)回調(diào)。

api "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" //viewModel+livedata
api 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0' //kotlin專用

由于ViewModel是配合Coroutine使用的,所以必需先認識CoroutineScope(協(xié)程范圍)和withContext(Dispatchers.IO)中的Dispatchers模式,這里先講Dispatchers的用途和具體場景。

image-20210331100456926.png

//實現(xiàn)LifecycleObserver接口,是為了通過activity的lifecycle.addObserver(LifecycleObserver) 綁定到生命周期中
open class BaseViewModel : ViewModel(), LifecycleObserver {
    
    //viewModelScope是ViewModel中的成員對象,該對象實現(xiàn)了CoroutineScope接口
    //suspend CoroutineScope.() -> Unit, suspend是kotlin線程掛起的標識,實際上去走了await()方法 
    //CoroutineScope.() -> Unit 代表一個方法體,并且該方法體對象是CoroutineScope接口對象,主要是規(guī)范方法體類型,可除去CoroutineScope.
    fun launchUI(block: suspend CoroutineScope.() -> Unit) = viewModelScope.launch {
        try {
            block()
        } catch (e: Exception) {
            e.printStackTrace()
        }

    }
    
    //相比上個方法,主要是多了 withContext(Dispatchers.IO),目的是切換到子線程做耗時操作
    fun launchIO(block: suspend CoroutineScope.() -> Unit) = viewModelScope.launch {
        withContext(Dispatchers.IO) {
            try {
                block()
            } catch (e: Exception) {
                e.printStackTrace()

            }
        }
    }
    
    //errorHandler 是為了統(tǒng)一處理網(wǎng)絡(luò)異常
    fun launchIO(
        block: suspend CoroutineScope.() -> Unit,
        errorHandler: (code: Int, message: String) -> Unit?,
    ) = viewModelScope.launch {
        withContext(Dispatchers.IO) {
            try {
                if (NetWorkUtils.isNetConnect(CommonManager.context)) {
                    block()
                } else {
                    errorHandler(300, "No internet connection")
                }
            } catch (e: Exception) {
                handlerErrorCode(e, errorHandler)
                e.printStackTrace()
            }finally {
                //....
            }
        }
    }

    private fun handlerErrorCode(
        e: Exception,
        errorHandler: (code: Int, message: String) -> Unit?,
    ) {
        when (e) {
            is HttpException -> {
                errorHandler(e.code(), e.message())
            }
            is UnknownHostException -> {
                errorHandler(404, "Unable to connect to server")
            }
            is SocketTimeoutException -> {
                errorHandler(408, "Socket time out")//訪問超時
            }
            is ConnectException -> {
                errorHandler(404, "Connect exception")
            }
            is SocketException -> {
                errorHandler(500, "Socket exception")
            }
            is EOFException -> {
                errorHandler(500, "EOF exception") //連接意外中斷
            }
            is IllegalArgumentException -> {
                errorHandler(400, "Illegal argument exception")//參數(shù)錯誤
            }
            is SSLException -> {
                errorHandler(401, "SSL exception")//證書錯誤
            }
            is NullPointerException -> {
                errorHandler(600, "Null pointer exception")
            }
            else -> {
                errorHandler(700, "Unknown exception")
            }
        }
    }

    fun Map<String, Any>.getBody(): RequestBody {
        return Gson().toJson(this).toRequestBody("application/json".toMediaTypeOrNull())
    }

    data class ErrorHandler(val code: Int, val message: String)
}
BaseVMActivity講解:
//持有倆個泛型,VM和DB,分別是BaseViewModel和ViewDataBinding的實現(xiàn)類
abstract class BaseVMActivity<VM : BaseViewModel, DB : ViewDataBinding> : BaseActivity(){
    
    //是否打印生命周期
    var openLifecycle: Boolean = false
    lateinit var viewModel: VM
    lateinit var dataBinding: DB
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        LogUtil.e(TAG, "onCreate", openLifecycle)
        //綁定視圖,獲取到dataBinding
        dataBinding = DataBindingUtil.setContentView(this, provideContentViewId())
        tvTitle = dataBinding.root.findViewById(R.id.title)
        back = dataBinding.root.findViewById(R.id.back)
        //實例化ViewModel
        viewModel = initVM()
        //讓viewModel關(guān)聯(lián)生命周期
        lifecycle.addObserver(viewModel)
        //開始接口監(jiān)聽回調(diào)
        startObserve()
    }

    abstract fun initVM(): VM

    abstract fun startObserve()

    override fun onResume() {
        super.onResume()
        LogUtil.e(TAG, "onResume", openLifecycle)
    }

    override fun onPause() {
        super.onPause()
        LogUtil.e(TAG, "onPause", openLifecycle)
    }

    override fun onStart() {
        super.onStart()
        LogUtil.e(TAG, "onStart", openLifecycle)
    }

    override fun onRestart() {
        super.onRestart()
        LogUtil.e(TAG, "onRestart", openLifecycle)
    }

    override fun onStop() {
        super.onStop()
        LogUtil.e(TAG, "onStop", openLifecycle)
    }

    override fun onDestroy() {
        super.onDestroy()
        LogUtil.e(TAG, "onDestroy", openLifecycle)
    }
    
    //Arouter路由跳轉(zhuǎn)封裝, action代表Postcard方法體回調(diào)
    fun open(path: String, requestCode: Int = 0, action: Postcard.() -> Unit = {}) {
        val postcard = ARouter.getInstance().build(path)
        postcard.action()
        postcard.navigation(this, requestCode)
    }
    
    fun openWithFinish(path: String, action: Postcard.() -> Unit = {}) {
        open(path, 0, action)
        finish()
    }
}
router包:

路由本意是,為了解決不同模塊之間無法訪問各自界面問題

路由指定,首先需要定義路徑名,然后在對應(yīng)的類中新增注解@Route(path = RouterPath.MOBILE_HOME),之后通過Arouter的navigation()方法跳轉(zhuǎn)到不同模塊的界面,從而實現(xiàn)跨模塊的跳轉(zhuǎn)。

interface RouterPath {
    companion object {
        //mobile
        const val MOBILE_HOME = "/mobile/home_activity"
        const val MOBILE_SPLASH = "/mobile/splash"
        const val MOBILE_VOD_DETAIL = "/mobile/vod_detail"
        const val MOBILE_LIVE = "/mobile/live"
        const val MOBILE_PAY = "/mobile/pay/activity"
        const val MOBILE_EVENT = "/mobile/event"


        //tablet
        const val TABLET_HOME = "/tablet/home_activity"
        const val TABLET_VOD_DETAIL = "/tablet/vod_detail"
        const val TABLET_SPLASH = "/tablet/splash"
        const val TABLET_EVENT = "/tablet/event"

        const val PAY_ACTIVITY = "/pay/activity"

    }
}
view包:

adapterItemDecoration是存放recycleview的裝飾類ItemDecoration

viewExpand是存放所有視圖相關(guān)的kotlin方法擴展,例如ImageView的加載圖片封裝

其余的則為view包中的一些常用的自定義view視圖

2.network:

在了解network模塊之前,必需對retrofit的使用由清晰的認識,所以接下來就看一個正常的retrofit是如何調(diào)用的。

調(diào)用庫的依賴:

//retrofit核心庫,這個在我們的common包中就有依賴
api 'com.squareup.retrofit2:retrofit:2.9.0'
//加上這個之后,你可以省略輸入流轉(zhuǎn)Gson的步驟
api 'com.squareup.retrofit2:converter-gson:2.9.0'
//使用之后,支持接口回調(diào)返回Deferred<T>對象,該對象是延遲結(jié)果發(fā)送的意思,可以使用suspend代替
api 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2'

實現(xiàn)步驟:

//1.定義接口,并申明請求方式和對應(yīng)的返回結(jié)果
interface TestApi {
    //get請求,(https://www.baidu.com/test/{body})
    @GET("/test/{body}")
    fun getTest(@Path("test") body: String): Deferred<Test>
}

//2.使用retrofit初始化TestApi接口的實現(xiàn)對象
val myTestApi by lazy {
    val retrofit = retrofit2.Retrofit.Builder()
            .baseUrl("https://www.baidu.com")
            //添加Gson轉(zhuǎn)換
            .addConverterFactory(GsonConverterFactory.create())
            //添加Deferred轉(zhuǎn)換
            .addCallAdapterFactory(CoroutineCallAdapterFactory())
            .build()
    
    //創(chuàng)建實現(xiàn)TestApi接口的MyTestApi對象,通過該對象就可以調(diào)用getTest()方法去走對應(yīng)的網(wǎng)絡(luò)請求
    retrofit.create(MyTestApi::class.java)
}

//3.使用協(xié)程實現(xiàn)簡潔回調(diào)
//確保在UI線程中調(diào)用,當getTest方法執(zhí)行后,回調(diào)用await()掛起,如果在方法中申明suspend,則可以省略,直接讓getTest()返回Test
GlobalScope.launch(Dispatchers.Main) {
    try {
        //回調(diào)UI線程中刷新界面數(shù)據(jù)
        onResponseUI(myTestApi.getTest("test").await())
    } catch (e: Exception) {
        prink(e)
    }
}

//4.搭配LiveData的進階用法
interface HistoryApi {
    //用了post請求,并且加了suspend,直接讓接口返回對象本身
    @POST(AppConfig.BO_API + "/play/list")
    suspend fun getPlayHistory(@Body body: RequestBody): WatchHistoryData
}

class HistoryViewModel : BaseViewModel() {
    private var historyApi = NetWorkManager.historyApiSingleton
    //聲明并初始化一個LiveData對象
    var playHistoryData: MutableLiveData<WatchHistoryData> = MutableLiveData()

    fun getPlayHistory(
        page: Int = 0, pageSize: Int = 12, type: String = XMediaConst.CONTENT_TYPE_VOD
    ) = launchIO {

        var parameter: HashMap<String, Any> = hashMapOf()
        parameter["type"] = type
        parameter["page"] = page
        parameter["pageSize"] = pageSize
        //調(diào)用方法返回對象
        val resultData = historyApi.getPlayHistory(parameter.getBody())
        if (resultData.resultCode == 0)
            //讓liveData對象綁定接口返回對象值
            playHistoryData.postValue(resultData)
    }
}

//在starObserve中做回調(diào)監(jiān)聽,playHistoryData是一個liveData類型,當數(shù)據(jù)改變就會回調(diào)
historyViewModel.run {
    //this傳的是一個lifecycleOwner對象,通常是activity或fragment持有
    playHistoryData.observe(this@VodFragment) {

    }
}

Network包中的組成:

image-20210331135936944.png

api包:存放所有網(wǎng)絡(luò)請求接口API

interface HistoryApi {
    //用了post請求,并且加了suspend,直接讓接口返回對象本身
    @POST(AppConfig.BO_API + "/play/list")
    suspend fun getPlayHistory(@Body body: RequestBody): WatchHistoryData
}

bean包:存放Bean對象

data class WatchHistoryData(
    val contents: List<Content>,
    val description: String,
    val pageCount: Int,
    val resultCode: Int
) 

viewModel包:存放ViewModel實現(xiàn)類

NetworkManager: 用于retrofit網(wǎng)絡(luò)請求頭參數(shù)統(tǒng)一配置,retrofit的對象初始化,以及初始化viewModel實現(xiàn)類對象

// lazy默認模式就是同步鎖,其余模式暫不展開
val historyApiSingleton: HistoryApi by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
            retrofit.create(HistoryApi::class.java)
        }

val client = OkHttpClient.Builder()
            //.cache(Cache(file, cacheSize))
            .connectTimeout(DEFAULT_TIME, TimeUnit.SECONDS) //連接超時設(shè)置
            .readTimeout(DEFAULT_TIME, TimeUnit.SECONDS) //讀取超時設(shè)置
            .writeTimeout(WRITE_TIME, TimeUnit.SECONDS) //寫入超時設(shè)置
            .addInterceptor(headInterceptor()) //請求頭攔截
            .addInterceptor(ChuckInterceptor(context)) //chunk工具類攔截
            //.addNetworkInterceptor(responseCacheInterceptor()) //緩存攔截
            .addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)) //提交和接收數(shù)據(jù)的攔截器
            .build()

var retrofit = Retrofit.Builder()
            .baseUrl(AppConfig.SERVER_ROOT_URI)
            .addConverterFactory(GsonConverterFactory.create()) //gson解析
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) //Rxjava
            .addCallAdapterFactory(CoroutineCallAdapterFactory()) //協(xié)程Coroutine
            .client(client)
            .build()

3.功能模塊和界面模塊

功能模塊沒什么好講的,就是功能封裝,調(diào)用方式也很簡潔,而界面模塊,主要描述一下MVVM的Activity是怎么實現(xiàn)的

//HomeViewModel,即我們之前將的ViewModel實現(xiàn)類, TabletActivityCategoryBinding則是系統(tǒng)通過xml自動生成的,具體請看下述的xml
class TabletCategoryActivity : BaseVMActivity<HomeViewModel, TabletActivityCategoryBinding>() {
    //分類內(nèi)容列表,具體數(shù)據(jù)需要調(diào)網(wǎng)絡(luò)請求接口獲取
    private var categoryContentList: MutableList<CategoryContentData.Content> = mutableListOf()
    private var categoryAdapter: TabletCategoryAdapter? = null

    companion object {
        const val CATEGORY_ID = "category_id"
        const val CATEGORY_TYPE = "category_type"
        const val CATEGORY_TITLE = "category_title"
    }

    //HomeViewModel初始化,BaseVMActivity重寫方法1
    override fun initVM(): HomeViewModel = HomeViewModel()
    //指定xml布局id,BaseVMActivity重寫方法2
    override fun provideContentViewId(): Int = R.layout.tablet_activity_category

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        initRecyclerView()
        initData()
    }

    private fun initData() {
        //判斷跳轉(zhuǎn)前是否攜帶CATEGORY_ID參數(shù)
        intent.getStringExtra(CATEGORY_ID)?.let {
            if (it.isNotEmpty()) {
                //調(diào)用getCategoryContentList()的網(wǎng)絡(luò)請求接口
                viewModel.getCategoryContentList(it)
            }
        }
        //tv_title自動綁定視圖 是在app/build.gradle 中開啟了buildFeatures { dataBinding = true }
        intent.getStringExtra(CATEGORY_TITLE)?.let {
            tv_title.text = it
        }
    }
    
    //recyclerView的初始化
    private fun initRecyclerView() {
        categoryAdapter = TabletCategoryAdapter()
        recycler_view.layoutManager = GridLayoutManager(this, 6)
        recycler_view.addItemDecoration(
            GridSpacingItemDecoration(
                6, 6f.dpInt, false))
        recycler_view.adapter = categoryAdapter
        categoryAdapter?.setOnItemClickListener { _, _, position ->
            TabletVodDetailsActivity.actionActivity(this,
                categoryContentList[position].contentId,
                0)
        }

        nav_back.setOnClickListener { finish() }
    }
    
    //監(jiān)聽viewModel實現(xiàn)類中的LiveData數(shù)據(jù),BaseVMActivity重寫方法3
    override fun startObserve() {
        viewModel.categoryContentListData.observe(this, { categoryContentData ->
            //categoryContentData即網(wǎng)絡(luò)請求結(jié)果對象                                            
            categoryContentData.contents.let {
                if (it.isNotEmpty()) {
                    categoryContentList.addAll(it)
                    //刷新數(shù)據(jù)
                    categoryAdapter?.setList(it)
                }
            }
        })
    }
}
?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">
    <-- 生成TabletActivityCategoryBinding類的關(guān)鍵 -->
    <data>

    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@drawable/theme_bg"
        android:fitsSystemWindows="true">

        <ImageView
            android:id="@+id/nav_back"
            android:layout_width="32dp"
            android:layout_height="32dp"
            android:layout_marginStart="14dp"
            android:clickable="true"
            android:focusable="true"
            android:padding="8dp"
            android:src="@drawable/nav_back"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/tv_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="@color/main_text_color"
            android:textSize="24sp"
            app:layout_constraintBottom_toBottomOf="@id/nav_back"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="@id/nav_back" />

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recycler_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginTop="34dp"
            app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/nav_back"
            app:spanCount="6"
            tools:listitem="@layout/tablet_item_category_vertical" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

MVVM模式總結(jié):


image-20210331151444720.png
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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