Android--MVVM组件化 kotlin(1)

不吃兔兔 2020-12-12 15:02:16 8110
前言

MVVM
在经历了MVC和MVP之后,MVVM开发模式应运而生,MVVM的高度解耦和搭配ViewModel、LiveData等JetPack组件使用,减少因生命周期搞出的BUG的同时,也让我们可以更加关注业务代码,减少一些细节的关注,总的来说就是不但省心还能偷懒

Kotlin
Android开发的亲儿子,使用过JAVA语言的话,上手难度很低,看看语法,一周之内就能掌握,对于本人来说,代码减少了很多,语法糖也很实用,最值得夸赞的是Kotlin的扩展函数和代理,能帮助减少很多模块代码,自带的非空断言能避免一部分BUG产生。(现在不会kotlin,都不好意思说自己会Android)

组件化
把项目功能模块分开解耦,功能模块间一般互不影响,能单独运行,单独开发维护,减少编译速度,提升开发速度。

介绍

本篇介绍一个自己东拼西凑的MVVM组件化基本框架,开发语言使用Kotlin对于业务定制化不高的可以直接copy使用。
相关构成:
buildSrc -- 依赖管理
autosize -- 今日头条屏幕适配方案
gson、fastjson
okhttp、Retrofit、kotlinx-coroutines--Retrofit加kotlin协程做网络请求
coil--kotlin编写的图片加载框架
rxtool-基本工具类
material_dialogs--MD风格弹窗工具类
smartrefresh--刷新加载工具类
BaseRecyclerViewAdapterHelper--RecycleView适配器工具类
koin--Kotlin注解
arouter--阿里巴巴的组件化路由工具
......
感谢以上框架作者做出的奉献!

组件化

kotlin的组件化和java的组件化是有一些细节(坑)要处理的
好的,我们开始,创建项目和组件就不多说了,大家都会,
我的组件分为一下几块,
app(壳工程)、common(工具类)、home(Home模块)、main(主页模块)、other(其他业务模块)
组件概览
buildSrc
结构

build.gradle.kts

plugins{
    `kotlin-dsl`
}
repositories {
    gradlePluginPortal()
}

BuildConfig.kt

/**
 * 构建版本信息
 */
object BuildConfig {

    const val kotlinVersion = "1.4.10"
    const val composeVersion = "1.0.0-alpha01"
    const val compileSdkVersion = 29
    const val buildToolsVersion = "29.0.3"
    const val minSdkVersion = 21
    const val targetSdkVersion = 29
    const val versionCode = 1
    const val versionName = "1.0.0"
//    true 分开 false集成
    const val isComponent = false

}
/**
 * 组件化 true合并 false分开
 */
fun  buildTime():String {
    return Date().time.toString()
}

Deploy.kt

/**
 *        SDK版本信息
 */
object SdkVersions {

    const val androidSupportSdkVersion = "28.0.3"
    const val androidxSdkVersion = "1.0.0"
    const val dagger2SdkVersion = "2.19"
    const val glideSdkVersion = "4.11.0"
    const val coilVersion = "1.0.0-rc3"
    const val butterknifeSdkVersion = "10.2.0"
    const val rxlifecycleSdkVersion = "1.0"
    const val rxlifecycle2SdkVersion = "2.2.2"
    const val espressoSdkVersion = "3.0.1"
    const val canarySdkVersion = "1.5.4"
    const val adapterHelperVersion = "3.0.4"
    const val rxToolVersion = "2.6.2"
    const val koinVersion = "2.2.0-rc-3"
    const val refreshVersion = "2.0.1"
    const val fuelVersion = "2.3.0"
    const val kotlinCoreVersion = "1.3.2"
    const val okhttp3Version = "4.9.0"
    const val retrofitSdkVersion = "2.9.0"
}
/**
 * Koin kotlin 注解库
 */
object Koin {
    //          Koin for Kotlin
    const val koin_core = "org.koin:koin-core:${SdkVersions.koinVersion}"
    const val koin_core_ext = "org.koin:koin-core-ext:${SdkVersions.koinVersion}"
    const val koin_test = "org.koin:koin-test:${SdkVersions.koinVersion}"

    // Koin for Android
    const val koin_android = "org.koin:koin-android:${SdkVersions.koinVersion}"

    //            Koin AndroidX
    // Koin AndroidX Scope features
    const val koin_scope = "org.koin:koin-androidx-scope:${SdkVersions.koinVersion}"

    // Koin AndroidX ViewModel features
    const val koin_viewmodel = "org.koin:koin-androidx-viewmodel:${SdkVersions.koinVersion}"

    // Koin AndroidX Fragment features
    const val koin_fragment = "org.koin:koin-androidx-fragment:${SdkVersions.koinVersion}"

    // Koin AndroidX Experimental features
    const val koin_ext = "org.koin:koin-androidx-ext:${SdkVersions.koinVersion}"

}

/**
 *  网络加载库
 *  fuel
 *  retrofit
 *  okgo
 *  OkHttp3
 */
object NetWork {
    //    fuel
    const val fuel_android = "com.github.kittinunf.fuel:fuel-android:${SdkVersions.fuelVersion}"
    const val fuel_gson = "com.github.kittinunf.fuel:fuel-gson:${SdkVersions.fuelVersion}"

    //retrofit
    const val retrofit = "com.squareup.retrofit2:retrofit:${SdkVersions.retrofitSdkVersion}"
    const val retrofit_converter_gson =
        "com.squareup.retrofit2:converter-gson:${SdkVersions.retrofitSdkVersion}"
    const val retrofit_adapter_rxjava =
        "com.squareup.retrofit2:adapter-rxjava:${SdkVersions.retrofitSdkVersion}"
    const val retrofit_adapter_rxjava2 =
        "com.squareup.retrofit2:adapter-rxjava2:${SdkVersions.retrofitSdkVersion}"
    const val retrofit_url_manager = "me.jessyan:retrofit-url-manager:1.4.0"

    //            Okgo网络框架
    const val okgo = "com.lzy.net:okgo:3.0.4"
    const val okrx2 = "com.lzy.net:okrx2:2.0.2"

    //           OkHttp3
    const val okhttp3 = "com.squareup.okhttp3:okhttp:${SdkVersions.okhttp3Version}"
    const val okhttp_urlconnection =
        "com.squareup.okhttp:okhttp-urlconnection:${SdkVersions.okhttp3Version}"
    const val okhttp3_interceptor =
        "com.squareup.okhttp3:logging-interceptor:${SdkVersions.okhttp3Version}"
}

BuildConfig和Deploy是可以写在一起的,分开是为了方便区分,这里用BuildCofig编写系统相关变量,Deploy编写所有依赖库
buildSrc就这么多

common
工具类模块
这个模块是整个项目的公共工具模块,所有模块都要引用,比较重要,放在前面
先把一些基类和配置定义好

build.gradle.kts
我这边是用的kotlin配置,arouter配置要注意,公用的第三方库要使用Api关键字

plugins {
    id("com.android.library")
    kotlin("android")
    kotlin("android.extensions")
    kotlin("kapt")
}

android {
    val javaVersion = JavaVersion.VERSION_1_8
    buildToolsVersion(BuildConfig.buildToolsVersion)
    compileSdkVersion(BuildConfig.compileSdkVersion)

    buildFeatures {
        dataBinding = true
    }
    defaultConfig {
        minSdkVersion(BuildConfig.minSdkVersion)
        targetSdkVersion(BuildConfig.targetSdkVersion)
        versionCode = BuildConfig.versionCode
        versionName = BuildConfig.versionName
        multiDexEnabled = true

        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
        consumerProguardFiles("consumer-rules.pro")

        compileOptions {
//        isCoreLibraryDesugaringEnabled = true
            sourceCompatibility = javaVersion
            targetCompatibility = javaVersion
        }
        kotlinOptions {
            jvmTarget = javaVersion.toString()
        }
    }
    buildTypes {
        getByName("release") {
            isMinifyEnabled = false
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
}
//ARouter添加
kapt {
    arguments {
        arg("AROUTER_MODULE_NAME", project.name)
    }
}
dependencies {
    implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
//    api(SystemLibs.core_x)
//    androidTestApi(SystemLibs.espresso_core_x)
    testImplementation(SystemLibs.junit_x)
    api(kotlin("stdlib",BuildConfig.kotlinVersion))

//    kotlin flow
//    api rootProject.ext.dependencies["kotlinx-coroutines"]
    api(SystemLibs.multidex)
    api(SystemLibs.design_x)
    api(SystemLibs.recyclerview_x)
    api(SystemLibs.appcompat_x)
    api(SystemLibs.cardview_x)
    api(SystemLibs.constraint_layout_x)
    api(SystemLibs.lifecycle_extensions)
    api(SystemLibs.viewmodel_ktx)
    api(SystemLibs.core)
    api(SystemLibs.coroutines_core)
    api(SystemLibs.coroutines_android)
    // liveData
//    api (SystemLibs.lifecycle_livedata)

    //今日头条兼容
    api(Utils.autosize)
    //    gson
    api(Utils.gson)
//    网络加载框架
//    api(NetWork.fuel_android)
//    api(NetWork.fuel_gson)
    api(NetWork.okhttp3)
    api(NetWork.okhttp3_interceptor)
    api(NetWork.retrofit)
    api(NetWork.retrofit_converter_gson)
    //    阿里巴巴JSON解析类
    api(Utils.fastjson)
    //    图片解析类 Glide
    api(Image.coil)
//    api rootProject.ext.dependencies["glide"]
//    kapt rootProject.ext.dependencies["glide-compiler"]
    //基础工具库
    api(Utils.rxtool)
    api(Utils.material_dialogs)
    api(Utils.material_dialogs_lifecycle)
//    api(Utils.loadsir)
//    api rootProject.ext.dependencies["rxtool-ui"]
    //    刷新
    api(SmartRefresh.smartrefresh)
    api(SmartRefresh.header_classics)
    api(SmartRefresh.footer_classics)
    //    万能适配器
    api(Utils.adpter_helper)

    api(Koin.koin_android)
    api(Koin.koin_scope)
    api(Koin.koin_viewmodel)
    api(Koin.koin_fragment)
//    api rootProject.ext.dependencies["koin_ext"]
//
//    api rootProject.ext.dependencies["viewmodel-ktx"]
    api(Utils.arouter)
    kapt(Utils.arouter_compiler)

}

这里放公用的图标,布局,风格,尺寸......

AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.jee.common">

    <application>
        <uses-library
            android:name="org.apache.http.legacy"
            android:required="false" />
        <meta-data
            android:name="design_width_in_dp"
            android:value="360" />
        <meta-data
            android:name="design_height_in_dp"
            android:value="640" />
    </application>
</manifest>


Base基类我就只贴Application部分了,其他的大家都可以按自己的习惯定制
BaseApplication

open class BaseApplication : MultiDexApplication() {

    init {
        //设置全局的Header构建器
        SmartRefreshLayout.setDefaultRefreshHeaderCreator { context, layout ->
            layout.setPrimaryColorsId(R.color.White, R.color.text_color_important) //全局设置主题颜色
            ClassicsHeader(context) //.setTimeFormat(new DynamicTimeFormat("更新于 %s"));//指定为经典Header,默认是 贝塞尔雷达Header
        }
        //设置全局的Footer构建器
        SmartRefreshLayout.setDefaultRefreshFooterCreator { context, layout ->
            layout.setPrimaryColorsId(R.color.White, R.color.text_color_important)
            //指定为经典Footer,默认是 BallPulseFooter
            ClassicsFooter(context).setDrawableSize(20f)
        }

    }

    private var isDebugARouter: Boolean = true
    override fun onCreate() {
        super.onCreate()
        if (isDebugARouter) {
            ARouter.openLog()
            ARouter.openDebug()
        }
        ARouter.init(this)

        RxTool.init(this)
        SPreference.setContext(applicationContext)

        MultiStatePage.register(EmptyState(), ErrorState(), LoadingState())

//        appliation初始化
        loadModuleApp()
    }

    /**
     * 加载各个模块的Application,例如:推送和IM等模块都需要有Application,
     * 但组件化只能有一个Application,而且为了解耦各个模块不能互相引用,
     * 所以只能通过反射方式,把这些module_appliation进行初始化
     */
    private  fun loadModuleApp() {
        for (moduleImpl in IMoudelApplication.MODULE_APP) {
            try {
                val clazz = Class.forName(moduleImpl)
                val obj = clazz.newInstance()
                if (obj is IMoudelApplication) {
                    obj.onCreate(this)
                }
            } catch (e: ClassNotFoundException) {
                e.printStackTrace()
            } catch (e: IllegalAccessException) {
                e.printStackTrace()
            } catch (e: InstantiationException) {
                e.printStackTrace()
            }
        }
    }

    override fun onTerminate() {
        super.onTerminate()
        ARouter.getInstance().destroy()
    }
}

IMoudelApplication
这个接口是为了各组件化在Application里初始化自己特有的一些东西,按组件顺序执行

interface IMoudelApplication {
    companion object {
        /**
         * 按顺序加载
         */
        val MODULE_APP: Array<String>
            get() = arrayOf(
                "com.jee.component.App",
                "com.jee.main.MainApp",
                "com.jee.home.HomeApp",
                "com.jee.other.OtherApp"

            )
    }

    fun onCreate(application: Application)
}

PathConstants 路由的地址我是放在common里面的

object PathConstants {
    const val HOME_PATH = "/home/homefragment" //首页片段

    const val FIND_HOME_PATH = "/find/homefragment" //发现模块首页片段

    const val MINE_HOME_PATH = "/mine/homefragment" //我的模块首页片段

    const val LOGIN_PATH = "/login/ac" //登录页面

    const val FIND_DETAIL_PATH = "/find/detail" //发现详情页面

    const val MAIN_ACTIVITY_PATH = "/main/mainactivity" //app壳工程主页

    const val HOME_ACTIVITY_PATH = "/home/homeactivity" //Home工程主页

    const val MAIN_ACTIVITY_TEST = "/main/test" //app测试

    const val SPLASH_ACTIVITY_PATH = "/main/splash" //欢迎页

}

工具类会以及Koin跨组件使用会在后面文章给出

app
这个模块为app壳模块,相对简单
在app模块的清单文件中注册权限,应用的基本配置

App
这个类就是实现IMoudelApplication,以便相关初始化,在这里初始化了Koin,注册所有组件的viewModle
build.gradle.kts

plugins {
    id("com.android.application")
    kotlin("android")
    kotlin("android.extensions")
    kotlin("kapt")
}

android {
    val javaVersion = JavaVersion.VERSION_1_8
    buildToolsVersion(BuildConfig.buildToolsVersion)
    compileSdkVersion(BuildConfig.compileSdkVersion)

    buildFeatures {
        dataBinding = true
    }
    defaultConfig {
        applicationId = "com.jee.component"
        minSdkVersion(BuildConfig.minSdkVersion)
        targetSdkVersion(BuildConfig.targetSdkVersion)
        versionCode = BuildConfig.versionCode
        versionName = BuildConfig.versionName
        multiDexEnabled = true
        //打包时间
        resValue("string", "build_time", buildTime())

        compileOptions {
//        isCoreLibraryDesugaringEnabled = true
            sourceCompatibility = javaVersion
            targetCompatibility = javaVersion
        }
        kotlinOptions {
            jvmTarget = javaVersion.toString()
        }
        ndk {
            abiFilters.add("armeabi-v7a")
        }
    }
    buildTypes {
        getByName("release") {
            buildConfigField("boolean", "LEO_DEBUG", "false")
            //是否zip对齐
            isZipAlignEnabled = true
//            isShrinkResources = true
            //Proguard
            isMinifyEnabled = false
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
        getByName("debug") {
            //给applicationId添加后缀“.debug”
            applicationIdSuffix = ".debug"
            //manifestPlaceholders = [app_icon: "@drawable/launch_beta"]
            buildConfigField("boolean", "LOG_DEBUG", "true")
            isZipAlignEnabled = false
            isMinifyEnabled = false
//            isShrinkResources = false
            isDebuggable = true
        }
    }
    //ARouter添加
    kapt {
        arguments {
            arg("AROUTER_MODULE_NAME", project.name)
        }
    }
    dependencies {
        implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
        kapt(Utils.arouter_compiler)
        if (BuildConfig.isComponent) {
            implementation(project(":common"))
        } else {
            implementation(project(":main"))
            implementation(project(":home"))
            implementation(project(":other"))
        }

    }
}
public class App : IMoudelApplication {

    override fun onCreate(application: Application) {
        Log.d(Constants.LOG_TAG,"App---初始化")
        // just declare it
        // start Koin!
        startKoin {
            // Android context
            androidContext(application.applicationContext)
            // modules
            modules(CommonModules.theLibModule, MainModules.theLibModule)
        }
    }

}

LaunchActivity
应用启动类,跳转到main模块

class LaunchActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        intentByRouter(PathConstants.SPLASH_ACTIVITY_PATH)
        finish()

    }
}

main
主页模块,涉及业务为主页相关

先看看组件化配置
build.gradle.kts

plugins {
    if (BuildConfig.isComponent) {
        id("com.android.application")
    } else {
        id("com.android.library")
    }
    kotlin("android")
    kotlin("android.extensions")
    kotlin("kapt")
}
android {
    val javaVersion = JavaVersion.VERSION_1_8
    buildToolsVersion(BuildConfig.buildToolsVersion)
    compileSdkVersion(BuildConfig.compileSdkVersion)
    compileOptions {
//        isCoreLibraryDesugaringEnabled = true
        sourceCompatibility = javaVersion
        targetCompatibility = javaVersion

    }
//    composeOptions{
//        kotlinCompilerVersion =BuildConfig.kotlinVersion
//        kotlinCompilerExtensionVersion =BuildConfig.composeVersion
//    }
    kotlinOptions {
        jvmTarget = javaVersion.toString()
//        useIR = true
    }

    buildFeatures {
        dataBinding = true
//        compose = true
//        viewBinding = true
    }

    defaultConfig {
        minSdkVersion(BuildConfig.minSdkVersion)
        targetSdkVersion(BuildConfig.targetSdkVersion)
        versionCode = BuildConfig.versionCode
        versionName = BuildConfig.versionName
        multiDexEnabled = true

    }
    sourceSets {
        getByName("main") {
            if (BuildConfig.isComponent) {
                manifest.srcFile("src/main/debug/AndroidManifest.xml")
                java { exclude("debug/**") }
            } else {
                manifest.srcFile("src/main/release/AndroidManifest.xml")
            }
        }
    }

}
kapt {
    arguments {
        arg("AROUTER_MODULE_NAME", project.name)
    }
}
dependencies {
    implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
    api(project(":common"))
    kapt(Utils.arouter_compiler)
    implementation("com.github.gcacace:signature-pad:1.3.1")

//    compose
//    implementation (SystemLibs.compose_ui)
//    implementation (SystemLibs.compose_ui_tooling)
//    implementation (SystemLibs.compose_foundation)
//    implementation (SystemLibs.compose_material)
//    implementation (SystemLibs.compose_material_icons_core)
//    implementation (SystemLibs.compose_material_icons_extended)
//    implementation (SystemLibs.compose_runtime_livedata)
//
//   androidTestImplementation(SystemLibs.compose_ui_test)

}

BuildConfig.isComponent是判断是否组件化,以觉得这个模块是否作为组件
此模块不是组件时就可以单独运行
思考?作为组件的的启动页肯定是由依赖这个组件的模块调起,那么不作为组件应该怎么运行这个模块呢?
因此我们需要两种配置,方便两种模式来调试应用

    getByName("main") {
            if (BuildConfig.isComponent) {
                manifest.srcFile("src/main/debug/AndroidManifest.xml")
                java { exclude("debug/**") }
            } else {
                manifest.srcFile("src/main/release/AndroidManifest.xml")
            }
        }

在项目类建立两个文件夹,名称随意,能区分就行,这里使用的是debug和release
从截图应该可以看出来 debug是不作为组件时使用的,release是被其他模块依赖时使用的,然后如上述代码配置好就可以了
release/AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.jee.main">

    <application android:theme="@style/CommonTheme">
        <activity android:name=".SplashActivity"
            android:screenOrientation="portrait"/>
        <activity android:name=".main.MainActivity"
            android:screenOrientation="portrait"/>
    </application>
</manifest>

debug/AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.jee.main">

    <application
        android:name="debug.MainApp"
        android:allowBackup="true"
        android:icon="@drawable/ic_lanucher"
        android:label="@string/mains_app_name"
        android:requestLegacyExternalStorage="true"
        android:supportsRtl="true"
        android:usesCleartextTraffic="true"
        android:theme="@style/CommonTheme">
        <activity android:name="debug.LaunchActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity
            android:name=".SplashActivity"
            android:screenOrientation="portrait" />
        <activity
            android:name=".main.MainActivity"
            android:screenOrientation="portrait" />
    </application>

</manifest>

debug/MainApp

public class MainApp : BaseApplication() {

    override fun onCreate() {
        super.onCreate()
        // just declare it
        // start Koin!
        startKoin {
            // Android context
            androidContext(this@MainApp)
            // modules
            modules(CommonModules.theLibModule, MainModules.theLibModule)

        }
    }

}

main/MainApp

public class MainApp : IMoudelApplication {

    override fun onCreate(application: Application) {
        Log.d(Constants.LOG_TAG,"MainApp---初始化")

    }

}

上面是Application相关的东西
SplashActivity
从app模块跳转到这个界面

@Route(path = PathConstants.SPLASH_ACTIVITY_PATH)
class SplashActivity : BaseActivity<SplashActivityBinding>(R.layout.layout_splash) {

    override fun initView() {
        Handler().postDelayed(Runnable {
            intentByRouter(PathConstants.MAIN_ACTIVITY_PATH)
            finish()
        }, 2000)
    }
}

再跳转到主界面,进行业务逻辑处理

组件化相关知识点:
app壳模块--配置基本的应用信息,图标,名称,权限等,启动页跳转到主界面,组件化时依赖其他所有模块,非组件化时只依赖common模块
common工具模块--配置所有公共工具类处,包括网络、权限、弹窗、状态变更、信息存储、基本控件、基本布局等
main主页模块--将主页模块分离出来,再将剩下的细分到其他模块,主要做主页上的相关业务
home模块--一个业务模块,看业务需求定义该模块
other模块--其他业务模块,无法区分的一些业务可以放在这里面

声明:本文内容由易百纳平台入驻作者撰写,文章观点仅代表作者本人,不代表易百纳立场。如有内容侵权或者其他问题,请联系本站进行删除。
红包 51 6 评论 打赏
评论
0个
内容存在敏感词
手气红包
    易百纳技术社区暂无数据
相关专栏
置顶时间设置
结束时间
删除原因
  • 广告/SPAM
  • 恶意灌水
  • 违规内容
  • 文不对题
  • 重复发帖
打赏作者
易百纳技术社区
不吃兔兔
您的支持将鼓励我继续创作!
打赏金额:
¥1易百纳技术社区
¥5易百纳技术社区
¥10易百纳技术社区
¥50易百纳技术社区
¥100易百纳技术社区
支付方式:
微信支付
支付宝支付
易百纳技术社区微信支付
易百纳技术社区
打赏成功!

感谢您的打赏,如若您也想被打赏,可前往 发表专栏 哦~

举报反馈

举报类型

  • 内容涉黄/赌/毒
  • 内容侵权/抄袭
  • 政治相关
  • 涉嫌广告
  • 侮辱谩骂
  • 其他

详细说明

审核成功

发布时间设置
发布时间:
是否关联周任务-专栏模块

审核失败

失败原因
备注
拼手气红包 红包规则
祝福语
恭喜发财,大吉大利!
红包金额
红包最小金额不能低于5元
红包数量
红包数量范围10~50个
余额支付
当前余额:
可前往问答、专栏板块获取收益 去获取
取 消 确 定

小包子的红包

恭喜发财,大吉大利

已领取20/40,共1.6元 红包规则

    易百纳技术社区