Android组件化开发:手把手教你搭建组件化项目

前言

看之前可以先看第五点思路总结,然后再回来看具体步骤。如果看起来觉得有点绕的话,我建议先去看看下面的视频,跟着视频搭项目,或者再回来看博客都可以。本博客中包含了build.gradle配置的抽离,统一管理,实际开发中用得到,感谢大家翻阅,欢迎评论区指正 ^_^

一、什么是组件化?

什么是组件化?这个不好说。我建议直接看视频。

Android组件化开发必须会,你掌握了吗?2021玩转安卓组件化开发,跟着我从架构设计到ARouter详解!_哔哩哔哩_bilibili

视频里将组件化分为四层:

在Android中,组件化开发是一种将整个应用拆分为多个相互独立、可插拔的组件的软件架构。这种架构的主要目标是提高代码的可维护性、可扩展性,以及多人协作的效率。在组件化开发中,通常有主工程组件、业务层组件、功能层组件和基础层组件。

  1. 主工程组件:

    • 作用: 主工程组件是整个应用的入口和框架,负责组织和协调各个组件的运行。它通常包含应用的配置、初始化代码、全局路由、全局事件分发等。
    • 分工: 主要负责应用的整体框架和控制,不涉及具体的业务逻辑。
    • 示例: 在一个电商应用中,主工程组件可能包含全局用户登录状态管理、网络请求配置等。
  2. 业务层组件:

    • 作用: 业务层组件是整个应用的核心,负责实现具体的业务功能。每个业务层组件可以看作是一个独立的业务模块,包含该业务领域的所有代码和资源。
    • 分工: 负责实现具体的业务逻辑,与其他业务层组件相互独立。
    • 示例: 在电商应用中,订单模块、商品模块、支付模块等可以作为独立的业务层组件。
  3. 功能层组件:

    • 作用: 功能层组件提供通用的功能或服务,可以被多个业务层组件共享。这包括一些通用的 UI 组件、工具类、第三方库的封装等。
    • 分工: 负责提供通用的功能和服务,减少重复代码,提高代码的复用性。
    • 示例: 可以有一个功能层组件专门负责图片加载、缓存和显示,被多个业务层组件共享。
  4. 基础层组件:

    • 作用: 基础层组件提供底层的基础设施支持,包括网络请求、数据库操作、权限管理等。这些功能对整个应用是必须的,但又不涉及具体的业务逻辑。
    • 分工: 提供底层的基础设施,为业务层组件和功能层组件提供支持。
    • 示例: 可以有一个基础层组件专门负责处理网络请求,被业务层组件和功能层组件共享。

举例说明:

考虑一个社交应用,其中包含消息模块、好友模块、个人资料模块等。在这个应用中,可以将每个模块作为一个业务层组件,共享一些通用的 UI 组件和网络请求工具,这些通用的功能可以放在功能层组件中。同时,可以有一个基础层组件处理底层的网络请求、数据库操作等。

主工程组件负责整个应用的初始化、全局路由等工作,确保各个组件能够协同工作。这样,每个模块可以独立开发、测试和维护,提高了代码的可维护性和可扩展性。

二、组件化项目搭建

1、项目结构总览

说明:

在这个项目中,基础层我定义了1个module,业务层定义了4个module。

我并没有创建功能层,功能层其实和基础层有点像,构建的过程是一样的,只是负责的职责不一样。

2、主工程创建

最开始新建一个Android项目后的结构就是主工程,上图去掉基础层和业务层就是开始新建项目后的样子。

3、业务层组件创建

1.业务层考虑到业务组件单独运行,所以我们选择Phone&Tablet

2.组件名可以按照自己的业务功能来自行定义

3.Module name的话建议将业务module都放在一个目录(modulesCore,可自定义)下,这样会使的项目结构更清晰。当然也可以不改直接建,但是当你过了一段时间回来再看项目的时候,左边一堆的module罗列在一起,你猜猜我是哪一层的module,你猜猜我是干嘛的。自行体会。

4.Package name的话,建议加一级,这样可以避免后续开发过程在的包名冲突,本来没有module的,com.hnucm.main。

成功后是这样的:(忽略其他三个module)

4、基础层组件搭建

1.基础层和功能层不会单独运行,跑组件,所以选择Android Library。

2.理由同业务组件第3点,lib_base名字可以自定。

3.理由同业务组件第4点。

成功构建后项目结构:

到这里组件化项目以及搭建好了,功能层和基础层的构建是一样的一毛一样,但需要注意的是Module name和Package name的项目文件夹名字别一样了。

这个名字,不在同一层,别建到同一个文件夹下面去了。

三、build.gradle抽离及统一管理

这一点可以不看,不影响项目搭建,但是我建议学习一下。

首先,我们看到各个module的build.gradle里的配置有很多是一样的,如果我们每新建一个module都要去配置build.gradle的话很烦琐,没必要。

1、抽离通用build.gradle

我们可以将相同的配置抽离出来,创建一个单独的.gradle文件,在新建的module中只要引入这个文件就行,其他的特有的再自行配置就行。

在项目根部新建一个config_build.gradle文件,文件名随意。

config_build.gradle代码:

/**
 * 整个项目(app/module)的通用gradle配置
 * 需要自行确定namespace。需要自行动态判定module是属于application(并配置applicationId),还是library。
 * 默认依赖基础组件module,(:modulesBase:lib_base)
 */

apply plugin: 'org.jetbrains.kotlin.android'

android {
    //rootProject 是指项目的根项目
    compileSdk rootProject.ext.android.compileSdk

    defaultConfig {

        minSdk rootProject.ext.android.minSdk
        targetSdk rootProject.ext.android.targetSdk
        versionCode rootProject.ext.android.versionCode
        versionName rootProject.ext.android.versionName

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [AROUTER_MODULE_NAME: project.getName()]
            }
        }

    }

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

    kotlinOptions {
        jvmTarget = '1.8'
    }
}

dependencies {
    //使用implementation不传递依赖,可以防止重复依赖或依赖冲突
    //公共依赖包
    implementation project(':modulesBase:lib_base')
    //ARouter 注解处理器 APT需要在每个使用的组件的build.gradle去添加,并在defaultConfig里添加project.getName。这里属于通用配置,一次配置完。
    annotationProcessor rootProject.ext.arouter_compiler

    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

别着急,马上给你解释。我们一步一步来。

(1)原始新建的时候最开始是这样的:(这是另一个项目的图,只是包名不一样,凑合看看吧)

最上面:

这种静态引入,我们改为这种动态的引入:

apply plugin: 'org.jetbrains.kotlin.android'

最初有两个,一个application,一个kotlin;我们只抽离出一个kotlin,原因是,我们不知道一个module他是要充当一个什么角色,他是一个单纯的library(基础层、功能层组件module),还是一个动态决定角色的组件(业务层module,调试状态可以单独运行单个组件时就是application,正式发布时就是library)。所以我们将决定他属于什么角色的决定权传递到具体的module,让他自己去决定,他是谁。

applicationId是只有application角色时才有的,所以也传递到具体的module。

还有这个也是一样,namespace rootProject.ext.applicationId.main。

核心思想:反正大家都有的就抽离出来,不一样的就到时候再写到具体的里面去。

(2)你应该发现这里不一样吧

最初的是一些数字,像这种:

你可以先不管这些什么rootProject.ext.android.minSdk,解释一下,在 Gradle 中,rootProject 是指项目的根项目,即顶层项目。 extExtraPropertiesExtension 的缩写,是一个扩展属性的机制。通过将属性添加到 rootProject.ext,你可以在整个 Gradle 构建中共享这些属性。相当于一个全局变量。

你可以按照抽离的核心思想先抽离出数字的通用.gradle文件,一样的,没什么影响。原因的话,下面再说。

(3)这个不用管,这是第三方框架ARouter的配置,原始没有。

2、引入到各module的build.gradle

我拿module的build.gradle代码来举例。

引入到业务层

业务层组件中的main的build.gradle,上面就建了这一个。

main组件的build.gradle:

//动态切换集成\组件模式
if (rootProject.ext.isDebug) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}

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

android {

    // 业务组件特定的配置

    namespace rootProject.ext.applicationId.main

    defaultConfig {
        //组件模式需要applicationId
        if (rootProject.ext.isDebug) {
            applicationId rootProject.ext.applicationId.main
        }
    }
    //动态切换集成/组件模式的AndroidManifest
    sourceSets {
        main {
            if (rootProject.ext.isDebug) {
                manifest.srcFile 'src/main/debug/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
            }
        }
    }
}

dependencies {
    // 业务组件特定的依赖配置

}

解释一下代码:

(1)最上面,注释的很清楚了。

就是根据这个全局变量里的isDebug来判断属于那种情况。

如果是需要,注意是需要,我个人需要,我想要,不是非要这么写。比如我在主页写了一个页面,我想看下这个页面UI是什么效果的,如果项目体积很大的话,跑起来太慢了,我们想只跑这个页面所在的组件模块,不跑别的组件,跑一部分,这样就快了。

  • 正式发布集成到App里这样配置:只要上面这一点就够了,特有的配置就自己加,自己加android{ // 业务组件特定的依赖配置},dependencies { // 业务组件特定的依赖配置 }等。

  •  application组件模式(组件可单独运行)就需要配置:

        1.引入apply plugin: 'com.android.application'

在 Gradle 构建中,rootProject.rootDir 是指向项目根目录的引用。在 Gradle 构建脚本中,rootProject 是一个指向根项目的引用,而 rootDirProject 对象的属性,表示项目的根目录。

注意:引入通用gradle文件的代码必须要在下面,如上图,原因是,在通用gradle文件中,我们没有确定他是什么角色,gradle文件是不完整的,需要先确定角色,再做配置,否则会报错。

Caused by: 

org.gradle.internal.metaobject.AbstractDynamicObject$CustomMessageMissingMethodException:

 Could not find method android() for arguments [config_build_7j66y0z6502l5wetbnhl851jt$_run_closure1@64416c1] 

on project ':modulesCore:main' of type org.gradle.api.Project.

 意思是Gradle 在你的 config_build.gradle 中找不到 android {} 块。该文件仅包含部分配置,而不是完整的 Android 插件配置文件。

        2、你要充当一个application那就需要给他一个id

        3、 充当一个application,那他的AndroidManifest也需要配置

        根据不同的角色选择不同的 AndroidManifest文件。

        注意层级关系。

        debug模式(单组件运行)下的AndroidManifest是这样的,就是本来的样子。复制一份到debug文件下。

        debug文件里的AndroidManifest:

      外层的AndroidManifest:

引入到主工程、功能层、基础层module

        主工程:build.gradle中只需要引入两行代码:

apply plugin: 'com.android.application'
apply from: "${rootProject.rootDir}/config_build.gradle"

        功能层、 基础层:build.gradle中也只需要引入两行代码:

    apply plugin: 'com.android.library'
    apply from: "${rootProject.rootDir}/config_build.gradle"

注意:注意对比原始配置和抽离出来的通用gradle文件的区别,将特有的自行添加的特定的module。

3、抽离出全局统一配置文件

说明一下,其实到上面那一步就可以了,将各个module中相同的配置抽离成一个通用gradle文件,然后在新建的module中引入,新建的module自己需要的,不同于通用gradle配置的就自己在自己的build.gradle中去配置。

但是我们可以更进一步将通用gradle中的各种配置分离一下,像sdk版本、第三方库等等。

在项目根部新建一个config.gradle文件

config.gradle代码:

/**
 *  全局统一配置文件
 *  ext 是 ExtraPropertiesExtension 的缩写,是一个扩展属性的机制。通过将属性添加到 rootProject.ext,你可以在整个 Gradle 构建中共享这些属性。
 */

ext {
    //当它为true时,调试模式,组件可以单独运行。如果是false,正式编译打包的模式。
    isDebug = true

    android = [
            compileSdk   : 33,
            applicationId: "com.hnucm.gdesign_android",
            minSdk       : 24,
            targetSdk    : 33,
            versionCode  : 1,
            versionName  : "1.0"
    ]

    applicationId = [
            "app"    : "com.hnucm.gdesign_android",
            "main"   : "com.hnucm.module.main",
            "login"  : "com.hnucm.module.login",
            "message": "com.hnucm.module.message",
            "mine"   : "com.hnucm.module.mine"
    ]

    //SDk中核心的库
    library = [
            corektx         : "androidx.core:core-ktx:1.8.0",
            appcompat       : "androidx.appcompat:appcompat:1.4.1",
            material        : "com.google.android.material:material:1.5.0",
            constraintlayout: "androidx.constraintlayout:constraintlayout:2.1.3"
    ]

    //第三方的库
    arouter_api = "com.alibaba:arouter-api:1.5.2"
    //ARouter 的注解处理器
    arouter_compiler = "com.alibaba:arouter-compiler:1.5.2"
    gson = "com.google.code.gson:gson:2.8.6"


}

 前面说到的rootProject.ext就是这个文件里的,将这个文件引入到项目的build.gradle中

 这样做就可以在任何一个module中的build.gradle使用rootProject.ext.XXX来拿到config.gradle中的属性值,就像一个静态变量一样。就像通用config_build.gradle中的:

这么做有什么好处呢?

如果我们后续需要更新一个sdk版本,或者更新第三方库的版本,我们只需要去修改这个全局的配置文件就行,当然前提是其他的module的build.gradle都是像这样调用这个全局来配置的,如上图。

还有一点就是,如果我新开了一个项目,我需要去用到一些第三方的库,像什么gson、glide等等,我还要一个一个去找这些第三方的库。很麻烦,现在我把这些配置都放在一个全局的配置文件里,我新开一个项目,我直接把这个配置文件复制到新项目里,引入到项目的build.gradle里,我就可以在任何module里通过rootProject.ext.XXX来拿到我想要的各种东西,包括但不限于第三方库,而且这些属性于某个项目的关联性很低,可重用性很高,此乃开发偷懒利器,嘿嘿嘿。

4、使用全局统一配置文件

前提:需要在项目的build.gradle中引入全局统一配置文件。

全局统一配置文件的使用很简单,通过rootProject.ext.XXX来使用就行。

当然也可以定义一个变量来代替他,简化代码。

 这里讲一下api和implementation的区别,两者都是引入库的操作,区别在与api会传递依赖,implementation不会。什么意思呢?

(下面说的都是module)业务层main依赖了基础层的lib_base,如果lib_base使用了api来添加库,比如上图gson,main依赖了lib_base,lib_base使用api引入gson,那么lib_base会将gson也传递给main,相当于main也引入了gson。而使用implementation就不会传递,相当于java的private,只有自己有。

这里我们需要注意的就是,要避免重复依赖和依赖冲突。比如main依赖了lib_base,lib_base传递了gson给他,main又依赖另外一个module B,B也用api引入了gson,也传递给了main,这时候就出现了依赖冲突,如果两个版本一样,就是重复依赖。如果使用全统一配置,就会大幅减少出现依赖冲突(组件化开发是多人开发,有些人不一定遵循规范)。

建议:在全局的公共依赖包module里使用api,其他的module中使用implementation,这样能减少依赖传递造成的种种依赖和版本问题。

四、层级依赖添加

添加依赖包很简单,看图。

在这个项目中,我将lb_base作为了一个公共依赖包,大家都依赖他。实际开发中看具体需求来引入包。引号里是要依赖的包的位置,如果不知道怎么写呢,可以到settings.gradle文件中去查。

图上面的这个是国内镜像,有时候下包很慢,dddd。

 maven { url 'https://maven.aliyun.com/repository/google' }
 maven { url 'https://maven.aliyun.com/repository/public' }

五、总结

梳理一下整体思路:

  • 首先,我们将各个层级module new出来。
  • 其次,将各个module中的build.gradle文件中相同的配置抽离成一个通用config_build.gradle文件,再引入到各个module,各个module特有的配置再针对性配置。
  • 然后,将通用config_build.gradle文件中的版本配置、SDK库、第三方库等值(存储的都是一些键值)抽离成一个全局统一配置文件config.gradle文件,引入到项目的build.gradle。所有的module、通用config_build.gradle文件都使用全局统一配置文件 config.gradle文件中的值来配置各个配置。这是一种使用关系。
  • 最后,添加各层级module依赖,按照需求去依赖,注意区别api和implementation添加依赖,避免产生依赖问题。建议使用层级依赖使用implementation,只在公共依赖包中引入库时用api。(包依赖包用implementation,公共依赖包内添加第三方库等用api)

只做总结的第一和第四点就可以搭建出组件化的项目。组件间通信的话用第三方框架ARouter来实现。

六、附录

 主工程module app的build.gradle 代码:

apply plugin: 'com.android.application'
apply from: "${rootProject.rootDir}/config_build.gradle"

android {
    namespace rootProject.ext.applicationId.app

    defaultConfig {
        applicationId rootProject.ext.applicationId.app
    }

}

dependencies {

}

 业务层module main的build.grade代码:

//动态切换集成\组件模式
if (rootProject.ext.isDebug) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}

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

android {

    // 业务组件特定的配置

    namespace rootProject.ext.applicationId.main

    defaultConfig {
        //组件模式需要applicationId
        if (rootProject.ext.isDebug) {
            applicationId rootProject.ext.applicationId.main
        }
    }
    //动态切换集成/组件模式的AndroidManifest
    sourceSets {
        main {
            if (rootProject.ext.isDebug) {
                manifest.srcFile 'src/main/debug/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
            }
        }
    }
}

dependencies {
    // 业务组件特定的依赖配置

}

基础层module lib_base的build.gradle代码:

plugins {
    id 'com.android.library'
    id 'org.jetbrains.kotlin.android'
}

def cfg = rootProject.ext

android {
    namespace 'com.hnucm.library.lib_base'
    compileSdk cfg.android.compileSdk

    defaultConfig {
        minSdk cfg.android.minSdk
        targetSdk cfg.android.targetSdk

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

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
}

dependencies {
    //只要依赖lib_base,它就会依赖这些SDK的库,向上传递。api 依赖传递 ,implementation 不会传递依赖

    //SDK核心库
    api cfg.library.corektx
    api cfg.library.appcompat
    api cfg.library.material
    api cfg.library.constraintlayout

    //第三方库
    api cfg.arouter_api
    //注解处理器,APT需要在每个使用的组件的build.gradle去添加,并在defaultConfig里添加project.getName
//    annotationProcessor cfg.arouter_compiler
    api cfg.gson


    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

通用配置文件config_build.gradle代码:

/**
 * 整个项目(app/module)的通用gradle配置
 * 需要自行确定namespace。需要自行动态判定module是属于application(并配置applicationId),还是library。
 * 默认依赖基础组件module,(:modulesBase:lib_base)
 */

apply plugin: 'org.jetbrains.kotlin.android'

android {
    //rootProject 是指项目的根项目
    compileSdk rootProject.ext.android.compileSdk

    defaultConfig {

        minSdk rootProject.ext.android.minSdk
        targetSdk rootProject.ext.android.targetSdk
        versionCode rootProject.ext.android.versionCode
        versionName rootProject.ext.android.versionName

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [AROUTER_MODULE_NAME: project.getName()]
            }
        }

    }

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

    kotlinOptions {
        jvmTarget = '1.8'
    }
}

dependencies {
    //使用implementation不传递依赖,可以防止重复依赖或依赖冲突
    //公共依赖包
    implementation project(':modulesBase:lib_base')
    //ARouter 注解处理器 APT需要在每个使用的组件的build.gradle去添加,并在defaultConfig里添加project.getName。这里属于通用配置,一次配置完。
    annotationProcessor rootProject.ext.arouter_compiler

    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

全局统一配置文件config.gradle:

/**
 *  全局统一配置文件
 *  ext 是 ExtraPropertiesExtension 的缩写,是一个扩展属性的机制。通过将属性添加到 rootProject.ext,你可以在整个 Gradle 构建中共享这些属性。
 */

ext {
    //当它为true时,调试模式,组件可以单独运行。如果是false,正式编译打包的模式。
    isDebug = true

    android = [
            compileSdk   : 33,
            applicationId: "com.hnucm.gdesign_android",
            minSdk       : 24,
            targetSdk    : 33,
            versionCode  : 1,
            versionName  : "1.0"
    ]

    applicationId = [
            "app"    : "com.hnucm.gdesign_android",
            "main"   : "com.hnucm.module.main",
            "login"  : "com.hnucm.module.login",
            "message": "com.hnucm.module.message",
            "mine"   : "com.hnucm.module.mine"
    ]

    //SDk中核心的库
    library = [
            corektx         : "androidx.core:core-ktx:1.8.0",
            appcompat       : "androidx.appcompat:appcompat:1.4.1",
            material        : "com.google.android.material:material:1.5.0",
            constraintlayout: "androidx.constraintlayout:constraintlayout:2.1.3"
    ]

    //第三方的库
    arouter_api = "com.alibaba:arouter-api:1.5.2"
    //ARouter 的注解处理器
    arouter_compiler = "com.alibaba:arouter-compiler:1.5.2"
    gson = "com.google.code.gson:gson:2.8.6"


}

项目build.gradle代码:

// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
    id 'com.android.application' version '8.0.0' apply false
    id 'com.android.library' version '8.0.0' apply false
    id 'org.jetbrains.kotlin.android' version '1.8.0' apply false
}
//引入全局配置文件
apply from: "config.gradle"

module main中 debug 中的AndroidManifest.xml代码:

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

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.GDesign_android">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

module main中 最外层的AndroidManifest.xml代码:

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

    <application>

        <activity android:name=".MainActivity"/>

    </application>

</manifest>

  • 8
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

千丘星

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值