[Kotlin] 玩Android代码学习之-模块化+Retrofit+协程+viewModel的数据层封装

news2024/11/27 6:42:39

文章目录

    • 1:前言
      • 玩Android APP 源码
      • 本贴的目的
      • 参考贴
    • 2: kotlin下的模块化(捎带一嘴)
    • 3:Retrofit+协程+viewModel
      • 3.1基础网络层搭建
        • `lib_home:Banner`
        • `lib_common: BaseResp `
        • `lib_common:RetrofitManager`
        • `lib_home: HomeApi `
      • 3.2基础网络层接口测试
      • 3.3 基础网络层优化-koin依赖注入框架
        • 简单说一下`依赖注入`
        • koin的简单理解
        • koin的简单使用演示
      • 3.4 回到APP源码,解析 真正的homeModule
      • 3.5 结合 ViewModel 与协程,再看数据层处理
        • `HomeViewModel`的简单实现
        • `HomeRepo`的加入
        • `HomeViewModel`的最终调用
        • `HomeFragment`的调用及测试

1:前言

玩Android APP 源码

学习kotlin,学习Android,最好的方式就是看别人的APP源码, 手头有一份玩安卓APP的kotlin版本,适合学习使用,文中提到的代码都在这款APP的源码里

WanAndroid基础款(MVVM+Kotlin+Jetpack+组件化)

本贴的目的

主要目的是基于WanAndroid基础款(MVVM+Kotlin+Jetpack+组件化)这份源码, 学习kotlin开发, 顺便记录学习过程中一些相关知识.

1.了解在模块化下,使用Retrofit + 协程 + viewModel, 怎么用完成网络层 , 数据的处理
2. 复盘学习过程,记录思路的变化,加深对语言的理解
3.记录以供后期翻阅(或触类旁通,毕竟好记性不如烂笔头)

参考贴

  • Kotlin retrofit 协程 最简单的使用(一)
  • 扒一扒Koin
  • Koin简单使用

2: kotlin下的模块化(捎带一嘴)

基本和Java语言下的模块化配置差不多,主要是配置gradle,可以看这个
[Android 模块化配置实践] Java + Gradle7配置模块化实践记录

本项目的模块化截图:
在这里插入图片描述

从项目结构上看, lib_common模块里面,回提供基础的网络层封装, 在lib_home等业务模块中,依赖 lib_common提供的各项功能

//在lib_home中依赖 lib_common
dependencies {
    // 引入模块 lib_common
    implementation project(path: ':lib_common')

}

3:Retrofit+协程+viewModel

乍一看APP源码, 确实有点晕的,所以我们拆分一下,先看我们熟悉的Retrofit部分

其实看完了Retrofit部分,我才发现,Retrofit是结合了协程一起的,所以第一步先尝试使用Retrofit+协程 ,完成简单的网络调用,再去结合viewModel会更加容易,也更符合开发顺序

至于为什么要结合协程?

// 不使用协程,返回值是Call类型,或者比如Java结合RxJava返回FLow
interface IApiServices {
    @GET("getHealthCare")
      fun getAllHealthData(@Query("userId") userId: String):Call<AllHealthBean>
}

// 使用协程,返回值直接就是你定义的数据类型或者Bean,直接就是可以使用
interface IApiServices {
    @GET("getHealthCare")
  suspend  fun getAllHealthData(@Query("userId") userId: String):AllHealthBean
}

总之,Retrofit结合协程, 获取请求结果,使用起来更加方便, 这就有点类似 JS的 async/await

3.1基础网络层搭建

(http请求及数据展示)

先看看APP的接口
https://www.wanandroid.com/banner/json

{"data":[{"desc":"我们支持订阅啦~","id":30,"imagePath":"https://www.wanandroid.com/blogimgs/42da12d8-de56-4439-b40c-eab66c227a4b.png","isVisible":1,"order":2,"title":"我们支持订阅啦~","type":0,"url":"https://www.wanandroid.com/blog/show/3352"},{"desc":"","id":6,"imagePath":"https://www.wanandroid.com/blogimgs/62c1bd68-b5f3-4a3c-a649-7ca8c7dfabe6.png","isVisible":1,"order":1,"title":"我们新增了一个常用导航Tab~","type":1,"url":"https://www.wanandroid.com/navi"},{"desc":"一起来做个App吧","id":10,"imagePath":"https://www.wanandroid.com/blogimgs/50c115c2-cf6c-4802-aa7b-a4334de444cd.png","isVisible":1,"order":1,"title":"一起来做个App吧","type":1,"url":"https://www.wanandroid.com/blog/show/2"}],"errorCode":0,"errorMsg":""}

lib_home:Banner

(以上获取banner数据的bean类)

package com.example.lib_home.bean

/**
 * @author: tiannan
 * @time: 2023/6/2.
 * @email: tianNanYiHao@163.com
 * @descripetion: 此处添加描述
 */
data class Banner(
    val desc: String,
    val id: Int,
    val imagePath: String,
    val isVisible: Int,
    val order: Int,
    val title: String,
    val type: Int,
    val url: String
)

lib_common: BaseResp

( common模块下的Http请求的基本返回类)

http请求 也就是 code, msg,data, 前两个没什么好说的, 都是基本型数据, 唯独data可能是数组,可能是集合,类型不固定
只能在具体的请求数据里,定义出了相对应的Bean,才能说给出一个类型,所以这里的data,理所当然的用到泛型
APP源码中用了 T 作为data属性的泛型类型

class BaseResp<D> {
    var errorCode: Int = -1
    var errorMsg: String = ""
    var data: D? = null
    var responseState: ResponseState? = null //请求状态
    enum class ResponseState {
        REQUEST_START,
        REQUEST_SUCCESS,
        REQUEST_FAILED,
        REQUEST_ERROR
    }
}
// PS : 我自己的练习Demo中, 我把T改成了D,好方便我自己理解, D = data

lib_common:RetrofitManager

common模块下的Retrofit管理类(单例模式)
这个没太多可以说的, Retrofit的使用不是很复杂

package com.example.lib_common.net

import android.util.Log
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit

/**
 * @author: tiannan
 * @time: 2023/6/1.
 * @email: tianNanYiHao@163.com
 * @descripetion: 此处添加描述
 */


/**
 * 用object 关键字,单例模式
 */
object RetrofitManager {

    const val BASE_URL = "https://www.wanandroid.com/"

    private lateinit var retrofit: Retrofit

    //init
    init {
        // 日志拦截器
        var loggingInterceptor = HttpLoggingInterceptor {
            Log.d("loggingInterceptor: ", it.toString())
        }.setLevel(HttpLoggingInterceptor.Level.BODY)

        // 配置Retrofit
        // 创建client
        var client: OkHttpClient = OkHttpClient().newBuilder()
            .callTimeout(10, TimeUnit.SECONDS)
            .connectTimeout(10, TimeUnit.SECONDS)
            .readTimeout(10, TimeUnit.SECONDS)
            .writeTimeout(10, TimeUnit.SECONDS)
            .retryOnConnectionFailure(true)
            .followRedirects(false)
            //此处暂时不做cookie支持,后续在添加
            //.cookieJar()
            .addInterceptor(loggingInterceptor)
            .build()

        // 创建 retrofit 实例
        retrofit = Retrofit.Builder()
            .client(client)
            .baseUrl(BASE_URL)
            //添加Gson解析支持
            .addConverterFactory(GsonConverterFactory.create())
            .build()

    }

    /**
     * Retrofit 结合 API泛型 ,创建接口实例并返回
     *
     * 入参: API泛型,代指各模块的 retrofit 接口API ,如HomeApi,MyApi等接口
     * 返回值: API泛型的接口实例,
     *
     * 备注: 由于项目中使用了koin 依赖注入
     * 所以,可以直接把 create 通过koin的module挂载,
     * 然后用到的时候, 直接通过参数注入到其他类中..
     */
    fun <API> create(api: Class<API>): API {
        return retrofit.create(api)
    }

}

lib_home: HomeApi

回到home模块,编写retrofit的Api接口

就拿Home页面的获取Banner接口来举例,我们定义 一个名为HomeApi的interface接口, 使用retrofit风格去编写
这里,我们给fun 添加 suspend, 表示这是挂起函数

package com.example.lib_home.api

import com.example.lib_common.net.BaseResp
import com.example.lib_home.bean.Banner
import retrofit2.http.GET

/**
 * @author: tiannan
 * @time: 2023/6/2.
 * @email: tianNanYiHao@163.com
 * @descripetion: 此处添加描述
 */
interface HomeApi {

    //首页banner

    @GET("banner/json")
    suspend fun getBanner():BaseResp<List<Banner>>

}

这里插一个经验总结:
在HomeApi的代码内, 想引入 import retrofit2.http.GET,但是失败
按道理说,在lib_common中已经添加了对 retrofit2dependencies

   //oKHttp
    implementation("com.squareup.okhttp3:okhttp:4.9.0")
    implementation("com.squareup.okhttp3:logging-interceptor:4.9.0")
    implementation("com.squareup.retrofit2:retrofit:2.9.0")
    implementation("com.squareup.retrofit2:converter-gson:2.9.0")

lib_home模块中,应该可以直接导入 import retrofit2.http.GET才对,但实际情况是
retrofit2lib_common中正常使用, 在lib_home中无法引入, 想到之前看过 implementationapi的区别, 发现果然是 在这里插入图片描述
的问题, 既然lib_common是公共的依赖, 那可以把需要放开的依赖 改为 api
即:

// lib_common
dependencies {
    //oKHttp
    implementation("com.squareup.okhttp3:okhttp:4.9.0")
    implementation("com.squareup.okhttp3:logging-interceptor:4.9.0")
    //retrofit
    // 此处由于 retrofit2 需要提供给别的模块使用, 故用 api (引用传递)
    // implementation 则代表,只在当前模块下可以, 对外部不可见
    // 所以,如果要开放某个库, 需要改为api (目测这要是 api 关键字为啥叫api的原因)
    api("com.squareup.retrofit2:retrofit:2.9.0")
    implementation("com.squareup.retrofit2:converter-gson:2.9.0")

}

简单来说:implementation指令,在A模块中生效, 在引用了A模块的B模块中,B无法访问implementation引入的代码,即 依赖不会传递个B模块, 改为api即可

3.2基础网络层接口测试

截止到目前, 整理一下我们已经实现的

  • lib_home : Banner
  • lib_home: HomeApi
  • lib_common: BaseResp
  • lib_common: RetrofitManager

已经满足我们实现接口请求,我们在lib_test中,进行一下测试
lib_test模块中引入 lib_common和lib_home

// lib_test中
dependencies {
   implementation project(":lib_common")
   implementation project(":lib_home")
}

添加测试代码

//lib_test
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        var job: Job = GlobalScope.launch {
            var ress: BaseResp<List<Banner>> =
                RetrofitManager.create(HomeApi::class.java).getBanner()
            Log.d("TestActivity:getBanner", ress.errorMsg + "")
            Log.d("TestActivity:getBanner", ress.errorCode.toString())
            Log.d("TestActivity:getBanner", ress.data.toString())
            Log.d("TestActivity:getBanner", ress.data?.get(0)?.imagePath + "")
        }
    }
}

记得要去项目更目录中gradle.properties中把use_lib_test配置模式修改一下
在这里插入图片描述
(这里我对每个模块做了单独开关,方便灵活切换测试,以前使用一个useLib的tag, 不太灵活)
然后编译运行 lib_test,记得在清单文件中添加网络权限

在这里插入图片描述

测试结果:
在这里插入图片描述
不过,不推荐在lib_test中以com.android.application模式进行测试

1.要给lib_test配置清单文件,创建MainActivity
2.要注意在清单文件中,处理网络层权限等各种配置

所以如果是简单数据测试,用lib_test模块可以的, 要是UI测试的, 还是放app工程
(我这么干是因为跑起来有个lib_test的APP入口好看…好装逼…)

3.3 基础网络层优化-koin依赖注入框架

基础的网络请求有了,按理说数据已经可以获取了,但是既然要学习项目,那肯定要继续深入优化

简单说一下依赖注入

  • 无注入的常规方法
fun abc(){
 val abc = ABC()
 // use abc can do somethind
}
  • 有注入的方法
fun abc(private val abc:ABC){
	// use abc can do somethind
}

总之,在没有依赖的情况下,调用abc函数, 需要调用者自己主动传入 abc对象
那就意味着你必须:

  • 要持有abc对象(强耦合)
  • 要自己提供abc对象或者传递abc
  • 一旦abc需要修改,要修改多处调用

相对的,如果使用依赖注入

class AAA(private val abc:ABC){
	fun abc(){
	this.abc.toString()
	// use abc can do somethind
	}
}

在AAA类的申明中, abc对象是通过注入的方式提供

那有人会问了, 初始化AAA的过程中,不还是要传入abc对象么,这有啥区别?

依赖注入,依赖注入, 既然已经有了注入的概念, 那肯定得有依赖
别的依赖注入框架我还没有学习到, 这里拿koin这个来先回答上面的疑问, 在koin的koinApplication中,会保存有abc实例对象, 所以只要 用koin的方式获取AAA类对象实例, 那么koin会自动把abc对象注入到AAA的实例中去,从而实现了依赖

举列代码就是:

// koin方式获取AAA类的实例 aaa, aaa实例也不需要用常规初始化方法创建,用下面方法即可
val aaa:AAA by inject()

fun test(){
	aaa.abc()
}

如上述代码所示, 可以看到,

  • AAA类的构造函数所需的入参 abc,并不需要开发者手动创建,而是通过koin提供,或者说通过依赖获得
  • 借助koin : AAA对象实例aaa, 也不需要开发者手动实例化,直接通过inject()注入

koin的 无代理、无代码生成、无反射特点, 应该能体会到一些了
(依赖注入框架有很多,但是初学者对koin的上手程度相对友好)

koin的简单理解

Koin是一个依赖注入的框架。其接口可以使用DSL的形式呈现
koin的核心部分

  • KoinApplication - 提供一个容器,用于容纳实例化的对象,便于全局使用
  • Module -是类似配置文件,用于描述注入的内容对象
    koind的通常用法:
    startKoin{}(在 Application或自定义Application类中创建)创建KoinApplication,并且将其挂载到GlobalScope中,便于通过协程使用

koin的简单使用演示

  • startKoin - koinApplication的初始化
//app 工程的application类中初始化 koin, 
// 这里仅先关注 homeModule, 其他的module,类似homeModule
    private fun initKoin() {
        startKoin {
            androidLogger(level = Level.NONE)
            androidContext(this@MyApplication)
            modules(homeModule, projectModule, playgroundModule, myModule, userModule)
        }
    }
  • Module的创建
    homeModule,myModule这里都是对应lib_home lib_my的module
    homeModule举例子
//lib_home
val homeModule = module {

    // 获取ABC的实例, 单例模式,将被koinApplicaion挂载
    // single单例关键字,提供唯一的 ABC实例对象,如 `abc`
    single { ABC() }


    // 注入测试类-测试
    // factory工厂关键字, 每次都创建一个 新的HomeKoinTest实例
    factory {
        //这里, HomeKoinTest类的构造函数 ,需要入参 `abc`实例对象.
        // 我们只需要传入 get(), 就可以了, 这就是依赖注入里面的依赖二字的含义吧
        // koin会帮我们把 上面 ABC()的单例对象通过 get()依赖,注入给HomeKoinTest类
        HomeKoinTest(get())
    }
}
  • ABC类
    ABC类就一个val属性 name, 用于演示
package com.example.lib_home.koin

import android.util.Log

/**
 * @author: tiannan
 * @time: 2023/6/5.
 * @email: tianNanYiHao@163.com
 * @descripetion: 此处添加描述
 */


/**
 * ABC类,
 * 用于演示 koin
 *
 */
 // lib_home
class ABC {
    val name: String = "abc"

    init {
        Log.d("object_abc", name)
    }
}
  • HomeKoinTest
    (这里我把上面的AAA类,换成了KHomeKoinTest类,应该不影响理解)
package com.example.lib_home.koin

import android.util.Log
import com.example.lib_common.util.ToastUtil

/**
 * HomeKoinTest -lib_home模块下,对koin的测试类
 * 依赖注入了 ABC的实例对象 abc
 */
class HomeKoinTest(private val abc: ABC) {
    fun hi() {
        Log.d("home_hi", "home_hi: " + this.abc.name)
        ToastUtil.showShort("home_hi: " + this.abc.name)
    }
}
  • HomeKoinTest类的引入及测试调用
    // 在MainActivity注入 homeKoinTest, 测试效果
    val homeKoinTest: HomeKoinTest by inject<HomeKoinTest>()
    fun load() {
        homeKoinTest.hi()
    }
  • 测试结果
    在这里插入图片描述

3.4 回到APP源码,解析 真正的homeModule

一路顺下来, 我们也自己定义了homeModule,并且能够简单使用了

从现在开始,可以去 WanAndroid基础款(MVVM+Kotlin+Jetpack+组件化)

中的 lib_home/di/HomeModule.kt中看看了

val homeModule = module {
    single { RetrofitManager.getService(HomeApi::class.java) }
    single { HomeRepo(get()) }
    viewModel { HomeViewModel(get()) }
}

经过上文的讲解, 再看源码的 homeModule应该非常好理解

RetrofitManager我们已经在 3.1基础网络层搭建中实现过了
所以
第一行代码

single { RetrofitManager.getService(HomeApi::class.java) }

就是向koinApplication中挂载了 RetrofitManager的实例

那么,谁会向koin依赖它(RetrofitManager)呢?

很显然看谁get()

查看 第二行代码发现

single { HomeRepo(get()) }

发现 HomeRepo类的构造函数,注入了 api: HomeApi,即泛型 API

class HomeRepo(private val api: HomeApi) : BaseRepository() {}

(RetrofitManager的build返回值, 请自行查看3.1中的基础网络层RetrofitManager类)

至于第三行代码
HomeViewModel也被注入依赖(get())

   viewModel { HomeViewModel(get()) }

我们直接查看 HomeViewModel发现

class HomeViewModel(private val repo: HomeRepo) : BaseViewModel() {}

原来HomeViewModel中被注入的依赖是 repo:HomeRepo

再去查看HomeViewModel是怎么用的

    private val homeViewModel: HomeViewModel by viewModel()
    private fun getHomeData() {
        homeViewModel.getBanner()
        homeViewModel.getArticle(0)
    }

很显然

  • HomeAPI是获取网络数据的接口,它负责从服务器获取数据并返回
  • HomeRepo是数据仓库的意思, 它依赖注入了HomeApi,主要负责数据的装载
  • HomeViewModel是VM层,它依赖注入了HomeRepo,主要负责处理业务逻辑,从HomeRepo数据仓库要数据并结合liveData,做数据的绑定等工作

至此,整个Retrofit+协程+koin依赖注入+viewModel的核心逻辑已经梳理完成了
(个人觉得理顺这一套之后,整个APP的业务逻辑层面应该不是阻碍了, lib_home,lib_my等模块都是这样设计的)

3.5 结合 ViewModel 与协程,再看数据层处理

上面提到了HomeRepoHomeViewModel
他们之间的关系也清楚了:HomeApi->HomeRepo->HomeModel
但是不可否认的是,源码中已经封装的很好了,但是对于思路的推导, 还得一步步来

HomeViewModel的简单实现

抛开HomeRepo不谈, 直接让HomeViewModel依赖注入HomeApi,我们可以这样写

class HomeViewModel(private val api: HomeApi) : ViewModel() {

    var bannerList = MutableLiveData<List<Banner>>()
    
    fun getBanner() {
        viewModelScope.launch {
            var res: BaseResp<List<Banner>> = api.getBanner()
            // 对bannerList 赋值
            bannerList.value = res.data
        }
    }
}

HomeFragment里面, 直接就可以进行数据的获取了, 基本上如果要求不高, 整个数据层面的封装就可以到此为止了.

    private val homeViewModel: HomeViewModel by inject<HomeViewModel>()

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

        homeViewModel.getBanner()
        homeViewModel.bannerList.observe(this) {
            var banner = it[0]
            ToastUtil.showShort(banner.url)
        }

    }

HomeRepo的加入

为了优化viewmodel对数据的获取,源码中添加了Repo类,来隔离viewmodel与Retrofit,
同时也为了Repo的模块化, 又在lib_common中增加了BaseRepository基类

// 类型别名 netBlock<D>, 为了写代码简约点
typealias netBlock<D> = suspend () -> BaseResp<D>

class BaseRepo {

    /**
     * 数据仓库基类 - load函数
     * 入参:netBlock<D> , 实际为 suspend () -> BaseResp<D> 类型的函数入参 (返回BaseResp<D>数据的挂载函数)
     * 入参:vmData: 类型为  MutableLiveData<D> 的 viewModel 数据, 可以理解为就是用于给VM赋值的
     */
    suspend fun <D> load(block: netBlock<D>, vmData: MutableLiveData<BaseResp<D>>) {

        var result = MutableLiveData<BaseResp<D>>()
        result.value?.responseState = BaseResp.ResponseState.REQUEST_START
        vmData.value = result.value
        try {

            // 执行 网络请求
            result.value = block.invoke()

            // 网络请求状态处理
            when (result.value?.errorCode) {
                Constants.HTTP_SUCCESS -> {
                    result.value?.responseState = BaseResp.ResponseState.REQUEST_SUCCESS
                }

                Constants.HTTP_AUTH_INVALID -> {
                    result.value?.responseState = BaseResp.ResponseState.REQUEST_FAILED
                    ToastUtil.showShort("认证过期,请重新登录!")
                    // TODO: 添加路由跳转到登录页,ARouter未添加
                }

                else -> {
                    result.value?.responseState = BaseResp.ResponseState.REQUEST_FAILED
                    ToastUtil.showShort("code:" + result.value?.errorCode.toString() + " / msg:" + result.value?.errorMsg)
                }
            }

        } catch (e: Exception) {
            when (e) {
                is UnknownHostException,
                is HttpException,
                is ConnectException
                -> {
                    ToastUtil.showShort("网络错误!")
                }
                else -> {
                    ToastUtil.showShort("未知异常!")
                }
            }
            result.value?.responseState = BaseResp.ResponseState.REQUEST_ERROR

        } finally {
            vmData.value = result.value
        }

    }

}

简单来说, BaseRepo就做了两件事


            // 执行 网络请求
            result.value = block.invoke()
            

 finally {
            // 给 viewModel的属性赋值
            vmData.value = result.value
        }

此时,在lib_home模块中, 可以添加 HomeRepo
它依赖注入的 自然是 HomeApi

//lib_home
class HomeRepo(private val api: HomeApi) : BaseRepo() {

    suspend fun getBanner(vmData: MutableLiveData<BaseResp<List<Banner>>>) {
        load({ api.getBanner() }, vmData)
    }

}

HomeViewModel的最终调用

class HomeViewModel(private val repo: HomeRepo) : BaseViewModel() {

    var bannerList = MutableLiveData<BaseResp<List<Banner>>>()

    fun getBanner() {
//        viewModelScope.launch {
//            repo.getBanner(bannerList)
//        }
        
        // or - 通过BaseViewModel 基类 抽取    viewModelScope.launch {}
        launch { repo.getBanner(bannerList) }
    }
}

HomeFragment的调用及测试

(记得要做数据判空…)

    private val homeViewModel: HomeViewModel by inject<HomeViewModel>()

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

        homeViewModel.getBanner()
        homeViewModel.bannerList.observe(this) {
            val url = it?.data?.get(0)?.url
            ToastUtil.showShort(url.toString())
        }
    }

在这里插入图片描述
至此,整个数据层的封装已经基本OK
这个套路掌握之后,我们也可以自己尝试进行更改,优化.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/615737.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

观澜最快的旧改项目之一,鸿荣源观城项目一期。

项目&#xff1a;观湖街道观城第一期城市更新单元位置&#xff1a;4号地铁观澜地铁站0距离 规模&#xff1a;拆除范围用地面积706094㎡ 面积&#xff1a;私信咨询价格&#xff1a;3.x万/平 开发商&#xff1a;鸿荣源 合同方案&#xff1a;直接开发商签合同 目前进度&#…

Scrapy 入门教程

Scrapy Engine(引擎): 负责Spider、ItemPipeline、Downloader、Scheduler中间的通讯&#xff0c;信号、数据传递等。 Scheduler(调度器): 它负责接受引擎发送过来的Request请求&#xff0c;并按照一定的方式进行整理排列&#xff0c;入队&#xff0c;当引擎需要时&#xff0c;…

【CANN训练营机器狗系列】安装ROS环境及初体验

环境 操作系统&#xff1a;Ubuntu 20.04 CPU&#xff1a;Intel Xeon Gold 6278C CPU 2.60GHz 内存&#xff1a;16GB 准备环境 Ubuntu与ROS版本对应关系 UbuntuROS 1.0ROS2.016.04 LTSKinetic LTSArdent18.04 LTSMelodic LTSDashing LTS120.04 LTSNoetic LTSFoxy LTS 安装…

linorobot机器人-自动生成-不可用

好像还是比较混乱。 具体信息参考其官网。 https://linorobot.org/ Linorobot是一套开源的ROS兼容机器人&#xff0c;旨在为学生、开发者和研究人员提供一个低成本的平台&#xff0c;以便在ROS&#xff08;机器人操作系统&#xff09;的基础上创建新的激动人心的应用。Linor…

VMware(Ubuntu)共享文件夹设置

VMware共享文件夹设置 安装完成ubuntu虚拟机后&#xff0c;需要建立共享文件夹来方便在Host主机和虚拟机ubuntu之间分享文件。 在虚拟机设置中&#xff0c;在 选项 卡中找到 共享文件夹 项&#xff0c;在右侧添加共享文件夹。 在虚拟机中&#xff0c;在ubuntu终端中使用指令…

Three.js camera初探——转场动画实现

背景 首先简单介绍一下three.js&#xff0c;three.js是用javascript写的基于webGL的第三方3D库&#xff0c;通过它可以在网页中进行3D建模&#xff0c;结合上TweenMax.js动画库&#xff0c;在网页中实现3D动画效果就变得很简单了。 这是three.js建模的简单流程图例&#xff1…

基于B/S架构springboot框架开发的中小学智慧校园平台源码

一、智慧校园技术框架&#xff1a; 1、使用springboot框架Javavue2 B/S架构 2、JAVA语言数据库MySQL5.7 3、移动端小程序使用小程序原生语言开发 4、电子班牌固件安卓7.1&#xff1b;使用Java Android原生 5、elmentui &#xff0c;Quartz&#xff0c;jpa&#xff0c;jwt …

实现 Linux 视频会议(源码,支持信创环境,银河麒麟,统信UOS)

信创是现阶段国家发展的重要战略之一&#xff0c;面对这一趋势&#xff0c;所有的软件应用只有支持信创国产化的基础软硬件设施&#xff0c;在未来才不会被淘汰。那么&#xff0c;可以使用C#来实现支持信创环境的视频会议系统吗&#xff1f;答案是肯定的。 本文讲述如何使用C#来…

django 快速入门

快速开始 安装Django 首先安装Django包&#xff0c;现在Django已经到了2.0版本&#xff0c;如果还在使用1.11请尽快升级。旧版本以后只修复bug&#xff0c;不会添加新功能。 pip install django 复制 创建项目 Django安装好之后&#xff0c;会附带一个命令行工具django-a…

uCOSii_任务栈检测和任务栈清除

1、任务栈检测和任务栈清除 在创建任务时&#xff0c;也需要设置OSTaskCreateExt()传入opt参数。 当opt (INT16U)(OS_TASK_OPT_STK_CLR | OS_TASK_OPT_STK_CHK)&#xff0c;可以使用OSTaskStkChk()检查的任务栈的剩余空间&#xff0c;也可以使用OS_TaskStkClr()清除任务栈。 …

使用Graalvm+Swing搓了个原生桌面应用的轮子:文件差异对比工具,附轮子源码

文章目录 1、DFDiff介绍2、软件架构3、安装教程3.1、编译为jar包运行3.2、编译为原生应用运行 4、运行效果图5、项目源码地址 1、DFDiff介绍 当前已实现的功能比较两个文件夹内的文件差异&#xff0c;已支持文件差异对比。 2、软件架构 软件架构说明 开发环境是在OpenJDK17&…

docker 安装gitlab jenkins git maven

jenkins 配置git提示 问题1、Error performing git command: /usr/local/git ls-remote -h 问题2、stdout: stderr: Host key verification failed. fatal: Could not read from remote repository. Please make sure you have the correct access rights and the repositor…

【数据结构】一文带你掌握二叉树的构造与应用

文章目录 1. 构造二叉树2. 前序遍历2.1 前序遍历递归2.2 前序遍历非递归 3. 中序遍历3.1 中序遍历递归3.2 中序遍历非递归 4. 后序遍历4.1 后序遍历递归4.2 后序遍历非递归 5. 层序遍历6. 节点个数6.1 所有节点个数6.2 获得叶子节点个数 7. 检测值为value的元素是否存在8.总结 …

数据库SQL2000最基本的安装和操作教程

Chengg0769 2012年 转载请保留以下版权来源 www.mis2erp.com http://blog.csdn.net/chengg0769 http://www.haojiaocheng.cc 题外话&#xff1a; 之前写了一个项目&#xff0c;因为是国企&#xff0c;各种文档都要给到他们。当时就写了一个简单的维护管理SQL2000的文档。这…

【2352. 相等行列对】

来源&#xff1a;力扣&#xff08;LeetCode&#xff09; 描述&#xff1a; 给你一个下标从 0 开始、大小为 n x n 的整数矩阵 grid &#xff0c;返回满足 Ri 行和 Cj 列相等的行列对 (Ri, Cj) 的数目。 如果行和列以相同的顺序包含相同的元素&#xff08;即相等的数组&#…

DMBOK知识梳理for CDGA/CDGP——第四章 数据架构(附常考知识点)

关 注ghz“大数据食铁兽”&#xff0c;回复“知识点”获取《DMBOK知识梳理for CDGA/CDGP》常考知识点&#xff08;第四章 数据架构&#xff09; 第四章 数据架构 第四章是CDGA|CDGP考试的重点考核章节之一&#xff0c;分值占比高&#xff0c;知识点比较密集&#xff0c;重点…

CAN message 属性DLC和DataLength,极易混淆

&#x1f345; 我是蚂蚁小兵&#xff0c;专注于车载诊断领域&#xff0c;尤其擅长于对CANoe工具的使用&#x1f345; 寻找组织 &#xff0c;答疑解惑&#xff0c;摸鱼聊天&#xff0c;博客源码&#xff0c;点击加入&#x1f449;【相亲相爱一家人】&#x1f345; 玩转CANoe&…

STM32开发(CubeMX+HAL)——点灯(轮询、中断)

目录 1.CubeMX生成工程文件 2.点灯1——轮询 3.点灯2——中断 3.1CubeMX配置时钟 3.2中断和事件简介 3.3配置GPIO口 3.4使能中断 3.5配置工程 3.6中断函数编写 1.CubeMX生成工程文件 1.1新建工程 1.2输入STM32C8T6选择芯片 1.3双击芯片 1.4 选择System Core—SYS——…

DAY09_JQueryBootStrap实现省略号效果

目录 1 JQuery简介2 引入JQuery方式3 JS与JQ之间的转换4 选择器4.1 基本选择器4.2 层级选择器4.2.1 层级选择器相关方法 4.3 过滤选择器4.4 内容选择器4.5 可见选择器4.6 隐藏显示的相关方法4.7 属性选择器4.8 子元素选择器4.9 表单选择器 5 JQ中新增的遍历方式6 JQ中相关方法7…

vue解决浏览器中跳转新页面缓存上一页表单等内容方法

在工作中&#xff0c;有可能会遇到需要缓存页面或组件的功能。 情况1&#xff1a;比如在h5中有个一个50个表单&#xff0c;在填到第40个表单时&#xff0c;需要你去另一个新页面去选择列表项&#xff0c;然后把数据带回来。需要我们不仅把数据带回来还要保留前面已经填好的40个…