IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> 写一个MVVM快速开发框架(二)组件化改造 -> 正文阅读

[移动开发]写一个MVVM快速开发框架(二)组件化改造

组件化思想

组件化对于各个领域都已经不是新鲜的词汇了,在Android上组件化已经遍地开花了,这里只是记录一下我对组件化的理解,有不对的地方请大家指出。

组件化是为了解决什么问题

优秀的工程项目应该满足高内聚低耦合思想,各个功能有明显的边界划分,各个模块各司其职,至少在修改的时候不是牵一发而动全身,其他人在接手的时候也能快速理解。

如果你的项目存在一下问题,可以考虑使用组件化了:

  1. 代码耦合严重,eventbus满天飞
  2. 依赖严重,编译慢
  3. 功能模块界限不清晰
  4. 多人开发的时候经常发生合并冲突

组件化的存在就是为了解决上述问题,将功能相关的作为一个单独模块,将经常使用的基础功能抽离出来作为基础模块,多人开发时每个人负责独立模块。

理想状态下,每个模块能进行独立开发、独立测试,在打包时合并为一个项目。

怎样使用组件化

大致分为以下步骤:

  1. 解决依赖问题
  2. 抽离公用模块
  3. 创建独立功能模块
  4. 模块间通信

我们的目的是搭建MVVM模式下的组件化框架,关于组件化的理论知识我们不做过多的介绍了。

参考:
https://www.jianshu.com/p/5e20a674062f
https://juejin.cn/post/6881116198889586701

搭建

解决依赖问题

组件化分为宿主App+多module,如果每个moduel都使用自己的依赖,那么管理起来相当费劲,不同module之间依赖的版本不同、升级都是问题,所以我们首先寻求的就是统一依赖管理

方式一:

在根目录下创建config.gradle文件,这个文件就是管理常用配置和依赖:

ext{

    versions = [
            compileSdkVersion: 29,
            buildToolsVersion: "29.0.2",
            minSdkVersion    : 21,
            targetSdkVersion : 29,
            versionCode      : 1,
            versionName      : "1.0"
    ]

    // 组件化与集成化切换时,设置不同的applicationId
    appId = [
            app : "com.example.modular.todo",
            shop: "com.example.modular.shop"
    ]


    dependencies = [
            appcompat       : "androidx.appcompat:appcompat:${appcompatVersion}",
            constraintlayout: "androidx.constraintlayout:constraintlayout:${constraintlayoutVersion}",
    ]


    //这里只做实例

}

然后再根目录的build.gradle中添加:

apply from: 'config.gradle'

在不同的module模块中的引用:

// build.gradle

def supports = rootProject.ext.dependencies

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

    implementation supports.appcompat
    implementation supports.constraintlayout

    // supports 依赖
    supports.each { key, value -> implementation value }

}

还有一种buildSrc管理,我并不常用这种方式,更习惯下面这种:

最终实现方式

1. 新建moduel模块,名称为version
2. 将version模块的build.gradle修改为如下:
buildscript {
    ext.kotlin_version = "1.5.20"
    repositories {
        google()
        mavenCentral()
    }
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

apply plugin: 'kotlin'
apply plugin: 'java-gradle-plugin'

repositories {
    google()
    mavenCentral()
}

dependencies {
    implementation gradleApi()
    implementation "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
}

compileKotlin {
    kotlinOptions {
        jvmTarget = 1.8
    }
}
compileTestKotlin {
    kotlinOptions {
        jvmTarget = 1.8
    }
}

gradlePlugin {
    plugins {
        version {
            // 在 app 模块需要通过 id 引用这个插件 ,修改为version模块包名
            id = 'com.xlu.version'
            // 实现这个插件的类的路径
            implementationClass = 'com.xlu.version.VersionPlugin'
        }
    }
}

在此模块下创建VersionPlugin类,如下:


import org.gradle.api.Plugin
import org.gradle.api.Project

/**
 * TODO 这里什么都不用做,创建就行了
 */
class VersionPlugin : Plugin<Project>{
    override fun apply(target: Project) {

    }
}
3. 重要的一步:
  • 打开工程根目录下的setting.gradle文件
  • 删除原来的include ':version'
  • 修改为includeBuild("version")
4. 在version模块下创建Dependency类,管理依赖

当然这个类名称自己随便起,实例如下:

object Dependency  {

    object ProjectConfig {
        const val compileSdkVersion = 30
        const val buildToolsVersion = "30.0.1"
        const val applicationId = "com.example.mvvm_develop"
        const val minSdkVersion = 16
        const val targetSdkVersion = 30

        const val versionCode = 1
        const val versionName = "1.0"
        const val isAppMode = false

    }

    object Version{
        // Android---------------------------------------------------------------
        const val Junit = "4.13"
        const val Material = "1.2.0"                        // 材料设计UI套件

        // AndroidX--------------------------------------------------------------
        const val AppCompat = "1.2.0"
        const val CoreKtx = "1.3.1"
        const val ConstraintLayout = "2.0.1"                // 约束布局
        const val TestExtJunit = "1.1.2"
        const val TestEspresso = "3.3.0"
        const val ActivityKtx = "1.1.0"
        const val FragmentKtx = "1.2.5"
        const val MultiDex = "2.0.1"

        // Kotlin----------------------------------------------------------------
        const val Kotlin = "1.5.10"
        const val Coroutines = "1.5.0"                      // 协程

        // JetPack---------------------------------------------------------------
        const val Lifecycle = "2.3.1"                       // Lifecycle相关(ViewModel & LiveData & Lifecycle)
        const val Hilt = "2.35.1"                           // DI框架-Hilt
        const val HiltAndroidx = "1.0.0"
    }

    object DependencyImp{
        //Android
        const val Junit = "junit:junit:${Version.Junit}"
        const val Material = "com.google.android.material:material:${Version.Material}"

        //AndroidX
        const val AndroidJUnitRunner = "androidx.test.runner.AndroidJUnitRunner"
        const val AppCompat = "androidx.appcompat:appcompat:${Version.AppCompat}"
        const val CoreKtx = "androidx.core:core-ktx:${Version.CoreKtx}"
        const val ConstraintLayout = "androidx.constraintlayout:constraintlayout:${Version.ConstraintLayout}"
        const val TestExtJunit = "androidx.test.ext:junit:${Version.TestExtJunit}"
        const val TestEspresso = "androidx.test.espresso:espresso-core:${Version.TestEspresso}"
        const val ActivityKtx = "androidx.activity:activity-ktx:${Version.ActivityKtx}"
        const val FragmentKtx = "androidx.fragment:fragment-ktx:${Version.FragmentKtx}"
        const val MultiDex = "androidx.multidex:multidex:${Version.MultiDex}"

        //Kotlin
        const val Kotlin = "org.jetbrains.kotlin:kotlin-stdlib:${Version.Kotlin}"
        const val CoroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Version.Coroutines}"
        const val CoroutinesAndroid = "org.jetbrains.kotlinx:kotlinx-coroutines-android:${Version.Coroutines}"

        //Jetpack
        const val ViewModel = "androidx.lifecycle:lifecycle-viewmodel-ktx:${Version.Lifecycle}"
        const val ViewModelSavedState = "androidx.lifecycle:lifecycle-viewmodel-savedstate:${Version.Lifecycle}"
        const val LiveData = "androidx.lifecycle:lifecycle-livedata-ktx:${Version.Lifecycle}"
        const val Lifecycle = "androidx.lifecycle:lifecycle-runtime-ktx:${Version.Lifecycle}"
        const val LifecycleCompilerAPT = "androidx.lifecycle:lifecycle-compiler:${Version.Lifecycle}"
        const val HiltCore = "com.google.dagger:hilt-android:${Version.Hilt}"
        const val HiltApt = "com.google.dagger:hilt-compiler:${Version.Hilt}"
        const val HiltAndroidx = "androidx.hilt:hilt-compiler:${Version.HiltAndroidx}"
    }

}

上面代码应该很好理解吧,我们在上述创建好我们常用的依赖之后,就可以在其他modue中引用了。

上面类中的依赖在编译时并不会创建,只有在module中引用才会创建。

5. 其它module中使用

我们以宿主app模块为例,打开app模块的build.gradle文件


//setup1
import com.example.plugin.Dependency

// setup2
plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'com.example.plugin' // 注意这里哦,不然上述的import会报错
}

一些配置基础:

android {
    compileSdkVersion Dependency.ProjectConfig.compileSdkVersion
    buildToolsVersion Dependency.ProjectConfig.buildToolsVersion

    defaultConfig {
        applicationId Dependency.ProjectConfig.applicationId
        minSdkVersion Dependency.ProjectConfig.minSdkVersion
        targetSdkVersion Dependency.ProjectConfig.targetSdkVersion
        versionCode Dependency.ProjectConfig.versionCode
        versionName Dependency.ProjectConfig.versionName

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    ......
    
}

依赖管理:


dependencies {

    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
    
    //Android
    api Dependency.DependencyImp.AppCompat
    api Dependency.DependencyImp.Material
    api Dependency.DependencyImp.AndroidXLeagcy
    api Dependency.DependencyImp.ActivityKtx
    api Dependency.DependencyImp.FragmentKtx
    api Dependency.DependencyImp.MultiDex
    //Kotlin
    api Dependency.DependencyImp.Kotlin
    api Dependency.DependencyImp.CoreKtx
    //Kotlin协程核心库
    api Dependency.DependencyImp.CoroutinesCore
    api Dependency.DependencyImp.CoroutinesAndroid
    //navigation
    api Dependency.DependencyImp.NavigationFragment
    api Dependency.DependencyImp.NavigationUI
    //lifecycle
    api Dependency.DependencyImp.Lifecycle
    api Dependency.DependencyImp.Livedata
    api Dependency.DependencyImp.Viewmodel

}

具体参考Demo

上面可以看到我们用Dependency.ProjectConfig.xx这种方式代替了原来的数据,在Dependency类中统一管理

注意version所在的位置应该与Project处于同一层级,而不是与app module处于同一层级:

不然MakeProjectk可能会报错:

Project directory 'C:xxx\version' is not part of the build defined by settings file 'C:xxx\project\settings.gradle'.   
If this is an unrelated build, it must have its own settings file.

setting.gradle配置如下:

include ':module_home'
include ':module_login'
include ':base'
include ':app'
includeBuild "../version"
rootProject.name = "mvvm_develop"

具体参考Demo

我这边升级了2020.03版本AndroidStudio,用的kotlin1.5.20, Gradle Version 7.0.2,上述有些使用jcenter的依赖需要注意下,新版AS已近完全抛弃了Jcenter

以上就完成了组件化的配置

参考资料
https://blog.csdn.net/yubo_725/article/details/118895551


抽离公用模块

有些功能是所有模块能公用的,比如:

  • 网络请求
  • 图片加载
  • 文件操作
  • 权限操作
  • 网页加载

这些模块一般不会去改动,和业务没有关联,但是所有模块都能公用,我们可以将其封装在base基础module中。

如果想将这个模块应用到多个项目上,可以单独发在公司的maven仓库中,也可以使用jitpack

上传到maven可以参考:https://blog.csdn.net/wang295689649/article/details/105219490
如何将单独的module发布到jitpack,参考:https://www.jianshu.com/p/b7552cf8983b,官方文档:https://jitpack.io/docs/ANDROID/

模块间通信

我们这里使用ARouter进行模块间的通信,对ARouter熟悉的同学就可以直接跳过了

基础接入:

在同一依赖管理中配置好:


        const val ARoute = "1.5.2"                          // 阿里路由
        const val ARouteCompiler = "1.5.2"                  // 阿里路由 APT
        
        
        const val ARoute = "com.alibaba:arouter-api:${Version.ARoute}"
        const val ARouteCompiler = "com.alibaba:arouter-compiler:${Version.ARouteCompiler}"

然后需要在所有使用了ARouter的模块中进行配置,注意java版本和kotlin有些不一样:

Java:

    defaultConfig {

        ...
        
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [AROUTER_MODULE_NAME: project.getName()]
            }
        }
    }
    
    
    dependencies {
        
        ...
        
        implementation Dependency.DependencyImp.ARoute
        annotationProcessor Dependency.DependencyImp.ARouteCompiler
        
    }

Kotlin:

    defaultConfig {

        ...
        
        kapt {
            arguments {
                arg("AROUTER_MODULE_NAME", project.getName())
            }
        }
    }
    
    
    dependencies {
        
        ...
        
        implementation Dependency.DependencyImp.ARoute
        kapt Dependency.DependencyImp.ARouteCompiler
    }

在公共模块中统一管理路径,我这里创建了一个Constants类去同一管理路径:

object ARouterConstant {

    const val MainActivity = "/app/MainActivity"

    const val ARouterFragment = "/app/ARouterFragment"

    const val LoginActivity = "/login/LoginActivity"
}

注意路径至少需要两级,建议使用/module name/class name这样子配置

页面跳转

在activity上或者fragment上配置:

@Route(path = ARouterConstant.LoginActivity)
class LoginActivity : BaseActivity(R.layout.activity_login) {

}

@Route(path = ARouterConstant.ARouterFragment)
class ARouterFragment : BaseFragment(R.layout.fragment_arouter) {

}
基础跳转:
//跳转到Login模块 -> LoginActivity
ARouter.getInstance().build(ARouterConstant.LoginActivity).navigation()
携带参数跳转:
ARouter.getInstance().build(ConstantARouter.LoginActivity)
        .withLong(ConstantParams.key1, 666L)
        .withString(ConstantParams.key2, "888")
        .navigation()

在目的页面获取参数:

    private fun initParams() {
        val key1 = intent?.getLongExtra(ConstantParams.key1,0L)
        val key2 = intent?.getStringExtra(ConstantParams.key2)
    }

可以使用@Autowired注解自动注入,如下:


    @Autowired
    @JvmField var key1 : Long = 0L


    @Autowired(name = ConstantParams.key2)
    @JvmField var test : String = ""


    override fun initData(savedInstanceState: Bundle?) {

        // Start auto inject.
        ARouter.getInstance().inject(this)

    }

使用@Autowired注解的时候需要配合@JVMField使用,在初始化的时候调用ARouter.getInstance().inject(this)

如果需要传递自定义对象,可以实现Serizable接口:

我们自定义一个数据bean:
data class RouterBeanSerializable(val id:Int, val name:String):Serializable


传递:
ARouter.getInstance()
    .build(ConstantARouter.LoginActivity)
    .withSerializable(ConstantParams.key3,RouterBeanSerializable(id = 123,name = "哈哈哈哈"))
    .navigation()
    
获取:
val key3 = intent?.getSerializableExtra(ConstantParams.key3)


输出结果:
key3:RouterBeanSerializable(id=123, name=哈哈哈哈)

对于没有实现序列化接口的对象,官方提供了withObject接口,我感觉没必要,有兴趣的可以参考这篇文章

ARouter获取Fragment实例:

ARouter是不支持fragment跳转的,只能获取fragment实例:

val fragment: Fragment = ARouter.getInstance().build(ConstantARouter.ARouterFragment)
            .withLong("id", key1)
            .withString("name", test)
            .navigation() as Fragment

更多关于更多ARouter的用法便不再阐述,请查看官方文档,我在Demo中也有进行演示

数据通信

上面介绍了使用ARouter进行页面跳转,和简单的页面间数据传递。如果我们不进行页面跳转,多module间有哪些数据通信的方式?

进行数据通信的方式有EventBus,数据持久化,进程间通信。大多不适用于我们的场景,我这里采用的是大佬推荐的向外暴露接口的方式。

我们新建common module,这个module主要是提供业务相关的公共依赖,我们在这里实现数据接口调用。

我们让common module使用api project(path: ':base')
这样其他模块只需要依赖一个common就行了
    

这样做的好处是隔离每个module,不用使各个module之间相互依赖才能调用对方的服务

这里以login module为例,提供获取用户信息的接口,先创建LoginInterface接口:

interface LoginInterface {

    fun getToken():String

    fun getUserName():String

    fun getUserID():Int

}

ARouter中提供了IProvider接口服务,我们需要让自定义接口继承IProvider接口。

interface LoginInterface : IProvider

接下来只需要在login module中实现此接口就行了:

这里只是一些模拟数据

class LoginImpl : LoginInterface {
    
    override fun init(context: Context?) {
        //此方法为IProvider接口提供
    }
    
    override fun getToken(): String {
        return "this is login token"
    }

    override fun getUserName(): String {
        return "name"
    }

    override fun getUserID(): Int {
        return 123
    }
    
}

我们可以创建一个工具类去统一管理:

object LoginUtil {

    fun getLoginServer():LoginInterface{
        return ARouter.getInstance().navigation(LoginInterface::class.java)//如果只有一个实现,这种方式也可以
        //return ARouter.getInstance().build(ConstantARouter.LoginItfImpl).navigation() as LoginInterface
    }
    
}

在其他module中调用:

val token:String = getLoginServer().getToken()

到此我们初步完成了组件化的搭建,更多使用的坑大家自行探索吧哈哈哈哈

参考:
https://juejin.cn/post/6881116198889586701
https://juejin.cn/post/6844903649102004231
https://juejin.cn/post/6844903687488274445、

上面三篇文章都挺值得大家参考的,感谢大佬们的付出

关于一些奇怪的问题

Moduel独立调试

moduel独立调试的时候需要设置不同的mainfest.xml文件,还要配置applicationId。

步骤如下:

1. 在Version模块中配置参数

object Dependency  {

    //是否允许module独立允许
    object RunAlone{
        const val base = false
        const val home = true   //以home独立允许为例
        const val login = false
        const val jitpack = false
    }
    
   
    ....
    
}

2.在home moduel中新建Mainfest.xml文件

作为独立app的Mainfest文件个作为module的Mainfest文件是不一样的,我们需要创建两个

3.配置mdule_home的build.gradle文件

import com.xlu.version.Dependency

plugins {
    id 'com.xlu.version'
}

//setup1:
if (Dependency.RunAlone.home.toBoolean()) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}

apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'

android {
    compileSdkVersion Dependency.ProjectConfig.compileSdkVersion
    buildToolsVersion Dependency.ProjectConfig.buildToolsVersion

    defaultConfig {
        //setup2:作为独立app需要配置applicationId
        if (Dependency.RunAlone.home.toBoolean()) {
            applicationId Dependency.ProjectConfig.applicationId_home
        }

        ......
    }

    ......
    
    //setup3:
    sourceSets {
        main {
            // 独立调试与集成调试时使用不同的 AndroidManifest.xml 文件
            if (Dependency.RunAlone.home.toBoolean()) {
                manifest.srcFile 'src/main/manifest/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
            }
        }
    }
}

dependencies {
    implementation project(path: ':base')

}

上述3步已经标明了该如何配置。

Application生命周期

我们通常在Application中执行一些初始化操作,比如sdk初始化。但是只有能独立运行的module才能拥有Application

如果我们将所有的初始化操作都方法在app application中,就会造成每次子module引入新的库都需要修改app module,有没有办法让子module自己执行初始化。

方式一:

反射实现,参考:https://juejin.cn/post/6844903649102004231#heading-12

方式二

生命周期分发,参考:
https://www.jianshu.com/p/59368ce8b670
https://juejin.cn/post/6881116198889586701#heading-26

方式三:

我们上面提到了其他module想外暴露服务,采用面向接口编程的方式,其他模块不能设计具体的实现类,我们在这里同样可以借鉴,Google提供了AutoSerive服务,一个简单的例子:

新建一个接口:

interface Display{

    fun show()

}

有很多的类实现了Display接口:


class phone :Diaplay{
    
    override fun show(){
        //show phone
    }
}


class monitor :Diaplay{
    
    override fun show(){
        //show monitor
    }
}

我们需要在所有的实现类上加上注解:@AutoService(Display::class)

我们可以用过ServiceLoader加载出此接口所有的实现类

        val mLoader: ServiceLoader<Display> = ServiceLoader.load(Display::class.java)
        
        mLoader.forEach { 
            it.show()
        }

就是这么的简单,我们可以通过@AutoService来实现,这个我们的application初始化有什么关系呢?

  1. 创建相关接口:

interface ApplicationLifecycle {

    fun onAttachBaseContext(context: Context)

    fun onCreate(application: Application)

    fun onTerminate(application: Application)

    fun init()
}
  1. 创建一个代理类来实现此接口:
//注意这里不需要注解@AutoService
class LoadModuleProxy : ApplicationLifecycle {

    private var mLoader: ServiceLoader<ApplicationLifecycle> =
        ServiceLoader.load(ApplicationLifecycle::class.java)


    override fun onAttachBaseContext(context: Context) {
        mLoader.forEach {
            Log.d("ApplicationInit", it.toString())
            it.onAttachBaseContext(context)
        }
    }

    override fun onCreate(application: Application) {
        mLoader.forEach { it.onCreate(application) }
    }

    override fun onTerminate(application: Application) {
        mLoader.forEach { it.onTerminate(application) }
    }
    
    override fun init(){
         mLoader.forEach { it.init() }
    }

}
  1. 在BaseApplication中注册:

open class BaseApp :Application() {

    private val mLoadModuleProxy by lazy(mode = LazyThreadSafetyMode.NONE) { LoadModuleProxy() }

    companion object{
        private lateinit var baseApplication: BaseApp

        fun getContext(): Context {
            return baseApplication
        }
    }

    override fun attachBaseContext(base: Context) {
        super.attachBaseContext(base)
        mLoadModuleProxy.onAttachBaseContext(base)
    }

    override fun onCreate() {
        super.onCreate()
        baseApplication = this

        mLoadModuleProxy.onCreate(this)
    }

    override fun onTerminate() {
        super.onTerminate()
        mLoadModuleProxy.onTerminate(this)
    }

}
  1. 在子moduel中Application中实现ApplicationLifecycle接口

@AutoService(ApplicationLifecycle::class)
class AppHome : ApplicationLifecycle {

    private val TAG = "AppHome"

    override fun onAttachBaseContext(context: Context) {
    }

    override fun onCreate(application: Application) {
    }

    override fun onTerminate(application: Application) {
    }

    override fun init(){
        initSdkWorker()
        initSdkMain()
    }
    
    private fun initSdkWorker() {
        xLog.d(TAG, "initSdkWorker: ")
    }

    private fun initSdkMain() {
        xLog.d(TAG, "initSdkMain: ")
    }

}

以上就可以实现子module自己愉快的玩耍了,AutoService使用参考:https://www.jianshu.com/p/086fe09188ea


以上代码都在mvvm_develop中可以找到,文章中为了简化省略了一些东西,Demo中有完整的代码。本来上周就应该搞完的,咕咕咕到了现在,项目整体还在完善中,下一步是引入JetPack组件和一些工具来帮助开发,欢迎大佬们指点。

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-08-12 16:42:49  更:2021-08-12 16:44:15 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/19 6:00:39-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码