Android MVI框架搭建与使用

news2024/11/27 0:40:03

MVI框架搭建与使用

  • 前言
  • 正文
    • 一、创建项目
      • ① 配置AndroidManifest.xml
      • ② 配置app的build.gradle
    • 二、网络请求
      • ① 生成数据类
      • ② 接口类
      • ③ 网络请求工具类
    • 三、意图与状态
      • ① 创建意图
      • ② 创建状态
    • 四、ViewModel
      • ① 创建存储库
      • ② 创建ViewModel
      • ③ 创建ViewModel工厂
    • 五、UI
      • ① 列表适配器
      • ② 数据渲染
    • 六、源码

前言

  有一段时间没有去写过框架了,最近新的框架MVI,其实出来有一段时间了,只不过大部分项目还没有切换过去,对于公司的老项目来说,之前的MVC、MVP也能用,没有替换的必要,而对于新建的项目来说还是可以替换成功MVVM、MVI等框架的。本文完成后的效果图:

在这里插入图片描述

正文

  每当一个新的框架出来,都会解决掉上一个框架所存在的问题,但同时也会产生新的问题,瑕不掩瑜,可以在实际开发中,解决掉产生的问题,就能够更好的使用框架,那么MVI解决了MVVM的什么问题呢?

  MVI同样是基于观察者模式,只不过数据通信方面是单向的,解决了MVVM双向通信所带来的问题,实际上MVVM也能做成单向通讯,但是这样就不是纯粹的MVVM,当然了,仁者见仁,智者见智。MVI框架适用于UI变化很多的项目,通过数据去驱动UI,MVI就是Model、View、Intent。

  • Model 这里的Model有所不同,里面还包含UI的状态。
  • View 还是视图,例如Activity、Fragment等。
  • Intent 意图,这个和Activity的意图要区分开,我觉得说成是行为可能更妥当,表示去做什么。

多说无益,我们还是进入实操环节吧。

一、创建项目

首先创建一个名为MviDemo的项目

在这里插入图片描述

项目创建好了,下面我们需要先进行项目的基本配置。

① 配置AndroidManifest.xml

  文章中会通过一个网络API接口,拿到数据来进行MVI框架的搭建与使用,接口地址如下:

http://service.picasso.adesk.com/v1/vertical/vertical?limit=30&skip=180&adult=false&first=0&order=hot

通过浏览器打开可以得到很多数据,如图所示:

在这里插入图片描述

  这些数据都是JSON格式的,后面我们还会用到这些数据。因为接口使用的是http,而不是https,所以在xml文件夹下新建一个network_security_config.xml,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="true" />
</network-security-config>

然后在AndroidManifest.xml中的application标签中配置它,如图所示:

在这里插入图片描述

  从Android 9.0起,默认使用https进行网络访问,如果要进行http访问则需要添加这个配置。还需要添加一个网络访问静态权限:

<uses-permission android:name="android.permission.INTERNET"/>

添加位置如下图所示:

在这里插入图片描述

项目正常搭建还需要一些依赖库和其他的一些设置,下面我们配置app模块下的build.gradle。

② 配置app的build.gradle

  请注意,这里是配置app的build.gradle,而不是项目的build.gradle,很多人会配置错误,所以我再次强调一下,将你的项目切换到Android模式,如下图所示:

在这里插入图片描述

  这里我标注了一下,你看到有两个build.gradle文件,两个文件的后面有灰色的文字说明,就很清楚的知道这两个build.gradle分别是项目和模块的。下面打开app模块下的build.gradle,在里面找到dependencies{}闭包,闭包中添加如下依赖:

    // lifecycle
    implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'
    //glide
    implementation 'com.github.bumptech.glide:glide:4.14.2'
    //retrofit
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    //retrofit moshi
    implementation "com.squareup.retrofit2:converter-moshi:2.6.2"
    //moshi used KotlinJsonAdapterFactory
    implementation "com.squareup.moshi:moshi-kotlin:1.9.3"
    //Coroutine
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1"

添加位置如下图所示:

在这里插入图片描述

然后再打开viewBinding,在android{}闭包下添加如下代码:

    buildFeatures {
        viewBinding true
    }

添加位置如下图所示:

在这里插入图片描述

  添加之后你会看到右上角有一个Sync Now,点击它进行依赖的载入配置,配置好之后进入下一步,为了确保你的项目没有问题,你可以现在运行一下看看。

二、网络请求

  当我们使用Kotlin时,网络访问就变得更简单了,只需要Retrofit和协程即可,首先我们在com.llw.mvidemo包下新建一个data包,然后在data包下新建一个model包,model包下我们可以通过刚才使用网页访问API拿到的JSON数据来生成一个数据类。

① 生成数据类

生成数据类,这里我们可以使用一个插件,搜索JSON To Kotlin Class,如下图所示:

在这里插入图片描述

  下载安装之后,如果需要重启,你就重启AS,重启之后,右键点击model → New → Kotlin data class File from JSON,如图所示:

在这里插入图片描述

在出现的弹窗中复制通过网页请求得到的JSON数据字符串,如图所示:

在这里插入图片描述

  这里如果觉得看起来不舒服,点击 Format 进行JSON数据格式化,然后我们需要设置数据类的名称,这里输入Wallpaper,因为我们需要使用Moshi,将JSON数据直接转成数据类,所以这里我们点击Advanced,如图所示:

在这里插入图片描述

  这里默认是None,选择MoShi(Reflect),其他的不用更改,点击OK,此弹窗关闭,回到之前的弹窗,然后点击 Generate 生成数据类,你会发现有三个数据类,分别是Wallpaper、Res和Vertical,我们看一下Wallpaper的代码:

package com.llw.mvidemo.data.model

import com.squareup.moshi.Json

data class Wallpaper(
    @Json(name = "code")
    val code: Int,
    @Json(name = "msg")
    val msg: String,
    @Json(name = "res")
    val res: Res
)

  这里每一个字段上都有一个@Json注解,这里是MoShi依赖库的注解,主要检查一下导包的问题,这里还有一个小故事,Google 的Gson库,算是推出比较早的,从事Gson库的开发人员,后面离职去了Square,也就是OkHttp、Retrofit的开发者。Retrofit一开始是支持Gson转换的,后面增加了MoShi的转换,Moshi拥有出色的Kotlin支持以及编译时代码生成功能,可以使应用程序更快更小。这个故事我也是听说的,你可以自己去求证,下面继续。

② 接口类

  现在数据类有了,那么我们就需要根据这个数据类来写一个接口类,在com.llw.mvidemo包下新建一个network包,network包下创建一个接口类ApiService,代码如下所示:

interface ApiService {

    /**
     * 获取壁纸
     */
    @GET("v1/vertical/vertical?limit=30&skip=180&adult=false&first=0&order=hot")
    suspend fun getWallPaper(): Wallpaper
}

这里属于Retrofit的使用方式,增加了协程的使用而已,就取代了RxJava的线程调度。

③ 网络请求工具类

现在有接口,下面我们来做网络请求,在network包下新建一个NetworkUtils类,代码如下:

package com.llw.mvidemo.network

import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory

/**
 * 网络工具类
 */
object NetworkUtils {

    private const val BASE_URL = "http://service.picasso.adesk.com/"

    /**
     * 通过Moshi 将JSON转为为 Kotlin 的Data class
     */
    private val moshi: Moshi = Moshi.Builder()
        .add(KotlinJsonAdapterFactory())
        .build()

    /**
     * 构建Retrofit
     */
    private fun getRetrofit() = Retrofit.Builder()
        .baseUrl(BASE_URL)
        .addConverterFactory(MoshiConverterFactory.create(moshi))
        .build()

    /**
     * 创建Api网络请求服务
     */
    val apiService: ApiService = getRetrofit().create(ApiService::class.java)
}

  由于担心你看的时候导错包,现在贴代码我会将导包的信息也贴出来,这样你总不会再导错包了吧。下面简单说明一下这个类,首先我定义了一个常量BASE_URL。作为网络接口请求的地址头,然后构建了MoShi,通过MoShi去进行JSON转Kotlin数据类的处理,之后就是构建Retrofit,将MoShi设置进去,最后就是通过Retrofit创建一个网络请求服务。

三、意图与状态

  之前我们说MVI的I 是Intent,表示意图或行为,和ViewModel一样,我们在使用Intent的时候,也是一个Intent对应一个Activity/Fragment。

① 创建意图

data包下创建一个intent包,intent包下新建一个MainIntent类,代码如下所示:

package com.llw.mvidemo.data.intent

/**
 * 页面意图
 */
sealed class MainIntent {
    /**
     * 获取壁纸
     */
    object GetWallpaper : MainIntent()
}

  这里只有一个GetWallpaper,表示获取壁纸的动作,你还可以添加其他的,例如保存图片、下载图片等,现在意图有了,下面来创建状态,一个意图有用多个状态。

② 创建状态

data包下创建一个state包,state包下新建一个MainState类,代码如下:

package com.llw.mvidemo.data.state

import com.llw.mvidemo.data.model.Wallpaper

/**
 * 页面状态
 */
sealed class MainState {
    /**
     * 空闲
     */
    object Idle : MainState()

    /**
     * 加载
     */
    object Loading : MainState()

    /**
     * 获取壁纸
     */
    data class Wallpapers(val wallpaper: Wallpaper) : MainState()

    /**
     * 错误信息
     */
    data class Error(val error: String) : MainState()
}

  这里可以看到四个状态,获取壁纸属于其中的一个状态,通过状态可以去更改页面中的UI,后面我们会看到这一点,这里的状态你还可以再进行细分,例如每一个网络请求你可以增加一个请求中、请求成功、请求失败。

四、ViewModel

  在MVI模式中,ViewModel的重要性又提高了,不过我们同样要添加Repository,作为数据存储库。

① 创建存储库

data包下创建一个repository包,repository包下新建一个MainRepository类,代码如下:

package com.llw.mvidemo.data.repository

import com.llw.mvidemo.network.ApiService

/**
 * 数据存储库
 */
class MainRepository(private val apiService: ApiService) {

    /**
     * 获取壁纸
     */
    suspend fun getWallPaper() = apiService.getWallPaper()
}

  这里的代码就没什么好说的,下面我们写ViewModel,和MVVM模式中没什么两样的。

② 创建ViewModel

  下面在com.llw.mvidemo包下新建一个ui包,ui包下新建一个adapter包,adapter包下新建一个MainViewModel类,代码如下:

package com.llw.mvidemo.ui.viewmodel

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.llw.mvidemo.data.repository.MainRepository
import com.llw.mvidemo.data.intent.MainIntent
import com.llw.mvidemo.data.state.MainState
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.launch

/**
 * @link MainActivity
 */
class MainViewModel(private val repository: MainRepository) : ViewModel() {

    //创建意图管道,容量无限大
    val mainIntentChannel = Channel<MainIntent>(Channel.UNLIMITED)

    //可变状态数据流
    private val _state = MutableStateFlow<MainState>(MainState.Idle)

    //可观察状态数据流
    val state: StateFlow<MainState> get() = _state

    init {
        viewModelScope.launch {
            //收集意图
            mainIntentChannel.consumeAsFlow().collect {
                when (it) {
                    //发现意图为获取壁纸
                    is MainIntent.GetWallpaper -> getWallpaper()
                }
            }
        }
    }

    /**
     * 获取壁纸
     */
    private fun getWallpaper() {
        viewModelScope.launch {
            //修改状态为加载中
            _state.value = MainState.Loading
            //网络请求状态
            _state.value = try {
                //请求成功
                MainState.Wallpapers(repository.getWallPaper())
            } catch (e: Exception) {
                //请求失败
                MainState.Error(e.localizedMessage ?: "UnKnown Error")
            }
        }
    }
}

  这里首先创建一个意图管道,然后是一个可变的状态数据流和一个不可变观察状态数据流,观察者模式。在初始化的时候就进行意图的收集,你可以理解为监听,当收集到目标意图MainIntent.GetWallpaper时就进行相应的意图处理,调用getWallpaper()函数,这里面修改可变的状态_state,而当_state发生变化,state就观察到了,就会进行相应的动作,这个通过是在View中进行,也就是Activity/Fragment中进行。这里对_state首先赋值为Loading,表示加载中,然后进行一个网络请求,结果就是成功或者失败,如果成功,则赋值Wallpapers,View中收集到这个状态后就可以进行页面数据的渲染了,请求失败,也要更改状态。

③ 创建ViewModel工厂

在viewmodel包下新建一个ViewModelFactory类,代码如下:

package com.llw.mvidemo.ui.viewmodel

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.llw.mvidemo.network.ApiService
import com.llw.mvidemo.data.repository.MainRepository

/**
 * ViewModel工厂
 */
class ViewModelFactory(private val apiService: ApiService) : ViewModelProvider.Factory {

    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        // 判断 MainViewModel 是不是 modelClass 的父类或接口
        if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
            return MainViewModel(MainRepository(apiService)) as T
        }
        throw IllegalArgumentException("UnKnown class")
    }
}

五、UI

  前面我们写好基本的框架内容,下面来进行使用,简单来说,请求数据然后渲染出来,因为这里请求的是壁纸数据,所以我需要写一个适配器。

① 列表适配器

  在创建适配器之前首先我们需要创建一个适配器所对应的item布局,在layout下新建一个item_wallpaper_rv.xml,代码如下图所示:

<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.imageview.ShapeableImageView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/iv_wall_paper"
    android:layout_width="match_parent"
    android:layout_height="300dp"
    android:layout_margin="4dp"
    android:scaleType="centerCrop"
    app:shapeAppearanceOverlay="@style/roundedImageStyle" />

这里使用了ShapeableImageView,这个控件的优势就在于可以自己设置圆角,在themes.xml中添加如下代码:

    <!-- 圆角图片 -->
    <style name="roundedImageStyle">
        <item name="cornerFamily">rounded</item>
        <item name="cornerSize">24dp</item>
    </style>

添加位置如下图所示:

在这里插入图片描述

下面进行我们在ui包下新建一个adapter包,adapter包下新建一个WallpaperAdapter类,里面的代码如下所示:

package com.llw.mvidemo.ui.adapter

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.llw.mvidemo.data.model.Vertical
import com.llw.mvidemo.databinding.ItemWallpaperRvBinding

/**
 * 壁纸适配器
 */
class WallpaperAdapter(private val verticals: ArrayList<Vertical>) :
    RecyclerView.Adapter<WallpaperAdapter.ViewHolder>() {

    fun addData(data: List<Vertical>) {
        verticals.addAll(data)
    }

    class ViewHolder(itemWallPaperRvBinding: ItemWallpaperRvBinding) :
        RecyclerView.ViewHolder(itemWallPaperRvBinding.root) {

        var binding: ItemWallpaperRvBinding

        init {
            binding = itemWallPaperRvBinding
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
        ViewHolder(ItemWallpaperRvBinding.inflate(LayoutInflater.from(parent.context), parent, false))

    override fun getItemCount() = verticals.size

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        //加载图片
        verticals[position].img.let {
            Glide.with(holder.itemView.context).load(it).into(holder.binding.ivWallPaper)
        }
    }
}

这里的代码相对比较简单,就不做说明了,属于适配器的基本操作了。

② 数据渲染

适配器写好之后,我们需要修改一下activity_main.xml中的内容,修改后代码如下所示:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ui.MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv_wallpaper"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingStart="2dp"
        android:paddingEnd="2dp"
        android:visibility="gone" />

    <ProgressBar
        android:id="@+id/pb_loading"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:visibility="gone"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/btn_get_wallpaper"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="获取壁纸"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

下面我们进入MainActivity,修改里面的代码如下所示:

package com.llw.mvidemo.ui

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Toast
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import com.llw.mvidemo.network.NetworkUtils
import com.llw.mvidemo.databinding.ActivityMainBinding
import com.llw.mvidemo.data.intent.MainIntent
import com.llw.mvidemo.data.state.MainState
import com.llw.mvidemo.ui.adapter.WallpaperAdapter
import com.llw.mvidemo.ui.viewmodel.MainViewModel
import com.llw.mvidemo.ui.viewmodel.ViewModelFactory
import kotlinx.coroutines.launch

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    
    private lateinit var mainViewModel: MainViewModel
    
    private var wallPaperAdapter = WallpaperAdapter(arrayListOf())

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //使用ViewBinding
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        //绑定ViewModel
        mainViewModel = ViewModelProvider(this, ViewModelFactory(NetworkUtils.apiService))[MainViewModel::class.java]
        //初始化
        initView()
        //观察ViewModel
        observeViewModel()
    }

    /**
     * 观察ViewModel
     */
    private fun observeViewModel() {
        lifecycleScope.launch {
            //状态收集
            mainViewModel.state.collect {
                when(it) {
                    is MainState.Idle -> {

                    }
                    is MainState.Loading -> {
                        binding.btnGetWallpaper.visibility = View.GONE
                        binding.pbLoading.visibility = View.VISIBLE
                    }
                    is MainState.Wallpapers -> {     //数据返回
                        binding.btnGetWallpaper.visibility = View.GONE
                        binding.pbLoading.visibility = View.GONE

                        binding.rvWallpaper.visibility = View.VISIBLE
                        it.wallpaper.let { paper ->
                            wallPaperAdapter.addData(paper.res.vertical)
                        }
                        wallPaperAdapter.notifyDataSetChanged()
                    }
                    is MainState.Error -> {
                        binding.pbLoading.visibility = View.GONE
                        binding.btnGetWallpaper.visibility = View.VISIBLE
                        Log.d("TAG", "observeViewModel: $it.error")
                        Toast.makeText(this@MainActivity, it.error, Toast.LENGTH_LONG).show()
                    }
                }
            }
        }
    }

    /**
     * 初始化
     */
    private fun initView() {
        //RV配置
        binding.rvWallpaper.apply {
            layoutManager = GridLayoutManager(this@MainActivity, 2)
            adapter  = wallPaperAdapter
        }
        //按钮点击
        binding.btnGetWallpaper.setOnClickListener {
            lifecycleScope.launch{
                //发送意图
                mainViewModel.mainIntentChannel.send(MainIntent.GetWallpaper)
            }
        }
    }
}

  说明一下,首先声明变量并在onCreate()中进行初始化,这里绑定ViewModel采用的是ViewModelProvider(),而不是ViewModelProviders.of,这是因为这个API已经被移除了,在之前的版本中是过时弃用,在最新的版本中你都找不到这个API了,所以使用ViewModelProvider(),然后通过ViewModelFactory去创建对应的MainViewModel

  initView()函数中是控件的一些配置,比如给RecyclerView添加布局管理器和设置适配器,给按钮添加点击事件,在点击的时候发送意图,发送的意图被MainViewModel中mainIntentChannel收集到,然后执行网络请求操作,此时意图的状态为Loading

  observeViewModel()函数中是对状态的收集,在状态为Loading,隐藏按钮,显示加载条,然后网络请求会有结果,如果是成功,则在UI上隐藏按钮和加载条,显示列表控件,并添加数据到适配器中,然后刷新适配器,数据就会渲染出来;如果是失败则显示按钮,隐藏加载条,打印错误信息并提示一下。这样就完成了通过状态更新UI的环节,MVI的框架就是这样设计的。

页面UI(点击事件发送意图) → ViewModel收集意图(确定内容) →
	ViewModel更新状态(修改_state) → 页面观察ViewModel状态(收集state,执行相关的UI)

这是一个环,从UI页面出发,最终回到UI页面中进行数据渲染,我们看看效果。

在这里插入图片描述

六、源码

欢迎Star 或 Fork,山高水长,后会有期~

源码地址:MviDemo

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

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

相关文章

【3D目标检测】基于伪雷达点云的单目3D目标检测方法研宄

目录概述细节基准模型点云置信度生成网络特征聚合 DGCNN概述 本文是基于单目图像的3D目标检测方法&#xff0c;是西安电子科技大学的郭鑫宇学长的硕士学位论文。 【2021】【单目图像的3D目标检测方法】 细节 基准模型 作者还是按照伪雷达点云算法的流程设计的&#xff0c;并…

多传感器融合定位十四-基于图优化的定位方法

多传感器融合定位十四-基于图优化的定位方法1. 基于图优化的定位简介1.1 核心思路1.2 定位流程2. 边缘化原理及应用2.1 边缘化原理2.2 从滤波角度理解边缘化3. 基于kitti的实现原理3.1 基于地图定位的滑动窗口模型3.2 边缘化过程4. lio-mapping 介绍4.1 核心思想4.2 具体流程4.…

lamada表达式、stream、collect整理

lamada表达式格式 格式&#xff1a;( parameter-list ) -> { expression-or-statements } 实例&#xff1a;简化匿名内部类的写法 原本写法&#xff1a; public class LamadaTest { public static void main(String[] args) { new Thread(new Runnable() { …

基于PYTHON django四川旅游景点推荐系统

摘 要基于四川旅游景点推荐系统的设计与实现是一个专为四川旅游景点为用户打造的旅游网站。该课题基于网站比较流行的Python 语言系统架构,B/S三层结构模式&#xff0c;通过Maven项目管理工具进行Jar包版本的控制。本系统用户可以发布个人游记&#xff0c;查看景点使用户达到良…

树莓派安装虚拟键盘matchbox-keyboard,解决虚拟键盘乱码问题,解决MIPI DSI触摸屏触控漂移问题

安装虚拟键盘&#xff0c;解决乱码问题 当我们买了触摸屏后&#xff0c;会发现没有键盘&#xff0c;还是无法输入&#xff0c;因此需要虚拟键盘 如果你的语言和地区是中文&#xff0c;那么安装虚拟键盘后可能显示乱码&#xff0c;所以还需要安装中文字体 sudo apt install ttf…

音视频开发—FFMpeg编码解码

FFMpeg 作为音视频领域的开源工具&#xff0c;它几乎可以实现所有针对音视频的处理&#xff0c;本文主要利用 FFMpeg 官方提供的 SDK 实现音视频最简单的几个实例&#xff1a;编码、解码、封装、解封装、转码、缩放以及添加水印。 接下来会由发现问题&#xff0d;&#xff1e;分…

Elasticsearch5.5.1 自定义评分插件开发

文本相似度插件开发&#xff0c;本文基于Elasticsearch5.5.1&#xff0c;Kibana5.5.1 下载地址为&#xff1a; Past Releases of Elastic Stack Software | Elastic 本地启动两个服务后&#xff0c;localhost:5601打开Kibana界面&#xff0c;点击devTools&#xff0c;效果图…

koa ts kick off 搭建项目的基本架子

koa ts kick off 使用ts开发koa项目的基本架子&#xff0c;便于平时随手调研一些技术 项目结构 ├── src │ ├── controller //controller层 │ ├── service //service层 │ ├── routes.ts //路由 │ └── index.ts //项目入…

【图像配准】多图配准/不同特征提取算法/匹配器比较测试

前言 本文首先完成之前专栏前置博文未完成的多图配准拼接任务&#xff0c;其次对不同特征提取器/匹配器效率进行进一步实验探究。 各类算法原理简述 看到有博文[1]指出&#xff0c;在速度方面SIFT<SURF<BRISK<FREAK<ORB&#xff0c;在对有较大模糊的图像配准时&…

04 react css上下浮动动画效果

react css上下浮动动画效果html原生实现上下浮动react 实现上下浮动思路分析实现步骤1.引入useRef2.在所属组件内定义—个变量3.在按钮上添加事件4.定义点击事件对window.scrollTo()进行了解&#xff1a;在react中实现效果图&#xff1a;html原生实现上下浮动 我们有一个导向箭…

【分享】订阅金蝶KIS集简云连接器同步OA付款审批数据至金蝶KIS

方案简介 集简云基于钉钉连接平台完成与钉钉的深度融合&#xff0c;实现钉钉OA审批与数百款办公应用软件&#xff08;如金蝶KIS、用友等&#xff09;的数据互通&#xff0c;让钉钉的OA审批流程与企业内部应用软件的采购、付款、报销、收款、人事管理、售后工单、立项申请等环节…

【2023面试秘籍】 测试工程师的简历该怎么写?

作为软件测试的垂直领域深耕者&#xff0c;面试或者被面试都是常有的事&#xff0c;可是不管是啥&#xff0c;总和简历有着理不清的关系&#xff0c;面试官要通过简历了解面试者的基本信息、过往经历等&#xff0c;面试者希望通过简历把自己最好的一面体现给面试官&#xff0c;…

【Java 面试合集】重写以及重载有什么区别能简单说说嘛

重写以及重载有什么区别能简单说说嘛 前述 这是一道非常基础的面试题&#xff0c;我们在回答的过程中一定要逐一横向比较。 从方法的 修饰符&#xff0c;返回值&#xff0c;方法名&#xff0c;含义&#xff0c;参数等方面进行逐一分析来比较不同。 话不多话&#xff0c;看下…

什么样的台灯适合学生做作业的?开学季,适合孩子写作业的台灯

学生在做作业时&#xff0c;是离不开台灯的&#xff0c;在台灯下学习三四个小时&#xff0c;如果台灯质量不好&#xff0c;那对视力造成很大影响&#xff0c;研究表明&#xff0c;儿童在过亮或者过暗的环境中长时间学习&#xff0c;会导致视力下降等&#xff0c;那么什么样的台…

瀚博半导体载天VA1 加速卡安装过程

背景&#xff1a; 想用 瀚博半导体载天VA1 加速卡 代替 NVIDIA 显卡跑深度学习模型 感谢瀚博的周工帮助解答。 正文&#xff1a; 小心拔出 NVIDIA 显卡&#xff0c;在PCIe 接口插上瀚博半导体载天VA1加速卡&#xff0c;如图&#xff1a; 这时显示屏连接主板的集成显卡 卸载…

cookie和Session的作用和比较

目录 什么是cookie cookie的工作原理 什么是session Session的工作原理 为什么会有session和cookie cookie和session如何配合工作 cookie和Session作用 什么是会话 什么是cookie cookie是web服务器端向我们客户端发送的一块小文件&#xff0c;该文件是干嘛的呢&#xf…

Java基础知识疑难点

1. 基础 1.1. 正确使用 equals 方法1.2. 整型包装类值的比较1.3. BigDecimal 1.3.1. BigDecimal 的用处1.3.2. BigDecimal 的大小比较1.3.3. BigDecimal 保留几位小数1.3.4. BigDecimal 的使用注意事项1.3.5. 总结 1.4. 基本数据类型与包装数据类型的使用标准 2. 集合 2.1. Arr…

Docker-用Jenkins发版Java项目-(1)Docke安装Jenkins

文章目录前言环境背景操作流程docker安装及jenkins软件安装jenkins配置登录配置安装插件及创建账号前言 学海无涯&#xff0c;旅“途”漫漫&#xff0c;“途”中小记&#xff0c;如有错误&#xff0c;敬请指出&#xff0c;在此拜谢&#xff01; 最近新购得了M2的MAC&#xff0c…

LeetCode刷题--- 138. 复制带随机指针的链表(哈希表+迭代)

文章目录一、编程题&#xff1a;430. 扁平化多级双向链表&#xff08;双指针&#xff09;1.题目描述2.示例1&#xff1a;3.示例2&#xff1a;4.示例3&#xff1a;5.提示&#xff1a;二、解题思路1. 题目分析2. 方法1&#xff08;哈希表&#xff09;思路&#xff1a;复杂度分析&…

备考 PMP 考试时需要着重注意什么?

PMP考试难度并不是很大。科学备考一定没有问题的&#xff5e;这里在和大家说说2023年PMP的考试时间&#xff1a;3月、5月、8月、11月&#xff08;其中3月不开启新报名&#xff09;需要注意的地方还是蛮多的。我就根据自己考试的经验和大家分享一下在考试整个过程中注意啥&#…