Android databinding

上一个MVVM demo改成用databinding绑定数据测试下。

app/build.gradle.kts 启用dataBinding:

布局修改下,最外层用layout标签,data标签设置变量。绑定数据和点击事件。@{}是单项绑定。即数据变化会自动刷新UI。

XML 复制代码
<?xml version="1.0" encoding="utf-8"?>
<layout
    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">
    <data>
        <variable
            name="viewModel"
            type="com.example.mvvmdemo.WuxiaCharacterViewModel"/>
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="20dp"
        tools:context=".MainActivity">

        <!-- 人物名称 -->
        <TextView
            android:id="@+id/tv_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="段正淳"
            android:textSize="36sp"
            android:textStyle="bold"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"/>

        <!-- 魅力区域 -->
        <TextView
            android:id="@+id/tv_charm_label"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="魅力:"
            android:textSize="20sp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/tv_name"
            android:layout_marginTop="20dp"/>

        <!--@{} 绑定点击事件-->
        <Button
            android:id="@+id/btn_charm_minus"
            android:layout_width="40dp"
            android:layout_height="40dp"
            android:onClick="@{()->viewModel.decrementCharm()}"
            android:text="-"
            app:layout_constraintStart_toEndOf="@id/tv_charm_label"
            app:layout_constraintTop_toTopOf="@id/tv_charm_label"
            app:layout_constraintBottom_toBottomOf="@id/tv_charm_label"/>

        <!-- 魅力值 @{}是单向绑定,即数据变化会刷新UI  -->
        <TextView
            android:id="@+id/tv_charm_value"
            android:layout_width="60dp"
            android:layout_height="wrap_content"
            android:text="@{viewModel.charm.toString()}"
            android:textSize="20sp"
            android:gravity="center"
            app:layout_constraintStart_toEndOf="@id/btn_charm_minus"
            app:layout_constraintTop_toTopOf="@id/tv_charm_label"
            app:layout_constraintBottom_toBottomOf="@id/tv_charm_label"/>

        <Button
            android:id="@+id/btn_charm_plus"
            android:layout_width="40dp"
            android:layout_height="40dp"
            android:text="+"
            android:onClick="@{()->viewModel.incrementCharm()}"
            app:layout_constraintStart_toEndOf="@id/tv_charm_value"
            app:layout_constraintTop_toTopOf="@id/tv_charm_label"
            app:layout_constraintBottom_toBottomOf="@id/tv_charm_label"/>

        <!-- 武力区域 -->
        <TextView
            android:id="@+id/tv_force_label"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="武力:"
            android:textSize="20sp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/tv_charm_label"
            android:layout_marginTop="10dp"/>

        <Button
            android:id="@+id/btn_force_minus"
            android:layout_width="40dp"
            android:layout_height="40dp"
            android:text="-"
            android:onClick="@{()->viewModel.decrementForce()}"
            app:layout_constraintStart_toEndOf="@id/tv_force_label"
            app:layout_constraintTop_toTopOf="@id/tv_force_label"
            app:layout_constraintBottom_toBottomOf="@id/tv_force_label"/>

        <TextView
            android:id="@+id/tv_force_value"
            android:layout_width="60dp"
            android:layout_height="wrap_content"
            android:text="@{viewModel.force.toString()}"
            android:textSize="20sp"
            android:gravity="center"
            app:layout_constraintStart_toEndOf="@id/btn_force_minus"
            app:layout_constraintTop_toTopOf="@id/tv_force_label"
            app:layout_constraintBottom_toBottomOf="@id/tv_force_label"/>

        <Button
            android:id="@+id/btn_force_plus"
            android:layout_width="40dp"
            android:layout_height="40dp"
            android:text="+"
            android:onClick="@{()->viewModel.incrementForce()}"
            app:layout_constraintStart_toEndOf="@id/tv_force_value"
            app:layout_constraintTop_toTopOf="@id/tv_force_label"
            app:layout_constraintBottom_toBottomOf="@id/tv_force_label"/>

        <!-- 财富区域 -->
        <TextView
            android:id="@+id/tv_wealth_label"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="财富:"
            android:textSize="20sp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/tv_force_label"
            android:layout_marginTop="10dp"/>

        <Button
            android:id="@+id/btn_wealth_minus"
            android:layout_width="40dp"
            android:layout_height="40dp"
            android:text="-"
            android:onClick="@{()->viewModel.decrementWealth()}"
            app:layout_constraintStart_toEndOf="@id/tv_wealth_label"
            app:layout_constraintTop_toTopOf="@id/tv_wealth_label"
            app:layout_constraintBottom_toBottomOf="@id/tv_wealth_label"/>

        <TextView
            android:id="@+id/tv_wealth_value"
            android:layout_width="100dp"
            android:layout_height="wrap_content"
            android:text="@{viewModel.formatWealth(viewModel.wealth)}"
            android:textSize="20sp"
            android:gravity="center"
            app:layout_constraintStart_toEndOf="@id/btn_wealth_minus"
            app:layout_constraintTop_toTopOf="@id/tv_wealth_label"
            app:layout_constraintBottom_toBottomOf="@id/tv_wealth_label"/>

        <Button
            android:id="@+id/btn_wealth_plus"
            android:layout_width="40dp"
            android:layout_height="40dp"
            android:text="+"
            android:onClick="@{()->viewModel.incrementWealth()}"
            app:layout_constraintStart_toEndOf="@id/tv_wealth_value"
            app:layout_constraintTop_toTopOf="@id/tv_wealth_label"
            app:layout_constraintBottom_toBottomOf="@id/tv_wealth_label"/>

        <!-- 武功 -->
        <TextView
            android:id="@+id/tv_skill"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="武功:一阳指"
            android:textSize="20sp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/tv_wealth_label"
            android:layout_marginTop="10dp"/>

        <!-- 介绍 -->
        <TextView
            android:id="@+id/tv_intro"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="介绍:少女收割机"
            android:textSize="20sp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/tv_skill"
            android:layout_marginTop="10dp"/>
    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>

修改MainActivity:

Kotlin 复制代码
package com.example.mvvmdemo

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.ViewModelProvider
import com.example.mvvmdemo.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
    // 武侠人物ViewModel
    private lateinit var characterViewModel: WuxiaCharacterViewModel

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

        // databinding加载布局
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)

        // 注册LifecycleObserver(Activity是LifecycleOwner)
        lifecycle.addObserver(MyLifecycleObserver()) // 这里没啥用

        // 通过ViewModelProvider获取ViewModel(配置变化不丢失数据)
        //this即Activity,实现了ViewModelStoreOwner 接口, ViewModel 会和 Activity 的生命周期绑定
        characterViewModel = ViewModelProvider(this)[WuxiaCharacterViewModel::class.java]

        // 绑定ViewModel到布局。
        binding.viewModel = characterViewModel

        // 设置生命周期所有者
        binding.lifecycleOwner = this
    }
}

可以看出,代码简洁了一些。删除了观察数据变化刷新UI的代码,以及删除了设置点击事件。

运行,测试ok,还是这个页面,点击加减都能修改属性值:

再试下双向绑定, 即实现修改UI能自动修改viewModel中的数据。

修改viewModel,加一个健康指数的字段:

修改布局,添加textView显示健康指数,旁边一个输入框可以修改值,并双向绑定数据:

XML 复制代码
<!--健康指数-->
        <TextView
            android:id="@+id/tv_health_label"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="健康:"
            android:textSize="20sp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/tv_wealth_label"
            android:layout_marginTop="10dp"/>

        <TextView
            android:id="@+id/tv_health"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{viewModel.health.toString()}"
            android:textSize="20sp"
            android:gravity="center"
            app:layout_constraintStart_toEndOf="@id/tv_health_label"
            app:layout_constraintTop_toTopOf="@id/tv_health_label"
            app:layout_constraintBottom_toBottomOf="@id/tv_health_label"
            android:layout_marginStart="10dp"/>

        <EditText
            android:id="@+id/et_health"
            android:text="@={viewModel.health.toString()}"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            app:layout_constraintStart_toEndOf="@id/tv_health"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="@id/tv_health"
            app:layout_constraintBottom_toBottomOf="@id/tv_health"
            android:layout_marginStart="10dp"
            android:hint="请修改健康指数" />

但是绑定数据编译不过,报错:The expression 'viewModel.health.toString()' cannot be inverted, so it cannot be used in a two-way bindin

原因是viewModel.health.toString() 是数据正向传递,即从viewModel -->UI, 缺少反向传递,即从UI -->viewModel.

数据是int,显示需要string,折腾了一圈数据转换,各种报错。找了一个折衷方案,监听输入框内容变化时,调用一个函数修改viewModel中的数据,这样实现反向绑定。改完后如下:

activity_main 布局, 健康值health的反向绑定是增加了android:onTextChanged:

XML 复制代码
<?xml version="1.0" encoding="utf-8"?>
<layout
    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">
    <data>
        <import type="com.example.mvvmdemo.WuxiaCharacterViewModel" />
        <variable
            name="viewModel"
            type="com.example.mvvmdemo.WuxiaCharacterViewModel"/>
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="20dp"
        tools:context=".MainActivity">

        <!-- 人物名称 -->
        <TextView
            android:id="@+id/tv_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="段正淳"
            android:textSize="36sp"
            android:textStyle="bold"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"/>

        <!-- 魅力区域 -->
        <TextView
            android:id="@+id/tv_charm_label"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="魅力:"
            android:textSize="20sp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/tv_name"
            android:layout_marginTop="20dp"/>

        <!--@{} 绑定点击事件-->
        <Button
            android:id="@+id/btn_charm_minus"
            android:layout_width="40dp"
            android:layout_height="40dp"
            android:onClick="@{()->viewModel.decrementCharm()}"
            android:text="-"
            app:layout_constraintStart_toEndOf="@id/tv_charm_label"
            app:layout_constraintTop_toTopOf="@id/tv_charm_label"
            app:layout_constraintBottom_toBottomOf="@id/tv_charm_label"/>

        <!-- 魅力值 @{}是单向绑定,即数据变化会刷新UI  -->
        <TextView
            android:id="@+id/tv_charm_value"
            android:layout_width="60dp"
            android:layout_height="wrap_content"
            android:text="@{viewModel.charm.toString()}"
            android:textSize="20sp"
            android:gravity="center"
            app:layout_constraintStart_toEndOf="@id/btn_charm_minus"
            app:layout_constraintTop_toTopOf="@id/tv_charm_label"
            app:layout_constraintBottom_toBottomOf="@id/tv_charm_label"/>

        <Button
            android:id="@+id/btn_charm_plus"
            android:layout_width="40dp"
            android:layout_height="40dp"
            android:text="+"
            android:onClick="@{()->viewModel.incrementCharm()}"
            app:layout_constraintStart_toEndOf="@id/tv_charm_value"
            app:layout_constraintTop_toTopOf="@id/tv_charm_label"
            app:layout_constraintBottom_toBottomOf="@id/tv_charm_label"/>

        <!-- 武力区域 -->
        <TextView
            android:id="@+id/tv_force_label"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="武力:"
            android:textSize="20sp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/tv_charm_label"
            android:layout_marginTop="10dp"/>

        <Button
            android:id="@+id/btn_force_minus"
            android:layout_width="40dp"
            android:layout_height="40dp"
            android:text="-"
            android:onClick="@{()->viewModel.decrementForce()}"
            app:layout_constraintStart_toEndOf="@id/tv_force_label"
            app:layout_constraintTop_toTopOf="@id/tv_force_label"
            app:layout_constraintBottom_toBottomOf="@id/tv_force_label"/>

        <TextView
            android:id="@+id/tv_force_value"
            android:layout_width="60dp"
            android:layout_height="wrap_content"
            android:text="@{viewModel.force.toString()}"
            android:textSize="20sp"
            android:gravity="center"
            app:layout_constraintStart_toEndOf="@id/btn_force_minus"
            app:layout_constraintTop_toTopOf="@id/tv_force_label"
            app:layout_constraintBottom_toBottomOf="@id/tv_force_label"/>

        <Button
            android:id="@+id/btn_force_plus"
            android:layout_width="40dp"
            android:layout_height="40dp"
            android:text="+"
            android:onClick="@{()->viewModel.incrementForce()}"
            app:layout_constraintStart_toEndOf="@id/tv_force_value"
            app:layout_constraintTop_toTopOf="@id/tv_force_label"
            app:layout_constraintBottom_toBottomOf="@id/tv_force_label"/>

        <!-- 财富区域 -->
        <TextView
            android:id="@+id/tv_wealth_label"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="财富:"
            android:textSize="20sp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/tv_force_label"
            android:layout_marginTop="10dp"/>

        <Button
            android:id="@+id/btn_wealth_minus"
            android:layout_width="40dp"
            android:layout_height="40dp"
            android:text="-"
            android:onClick="@{()->viewModel.decrementWealth()}"
            app:layout_constraintStart_toEndOf="@id/tv_wealth_label"
            app:layout_constraintTop_toTopOf="@id/tv_wealth_label"
            app:layout_constraintBottom_toBottomOf="@id/tv_wealth_label"/>

        <TextView
            android:id="@+id/tv_wealth_value"
            android:layout_width="100dp"
            android:layout_height="wrap_content"
            android:text="@{viewModel.formatWealth(viewModel.wealth)}"
            android:textSize="20sp"
            android:gravity="center"
            app:layout_constraintStart_toEndOf="@id/btn_wealth_minus"
            app:layout_constraintTop_toTopOf="@id/tv_wealth_label"
            app:layout_constraintBottom_toBottomOf="@id/tv_wealth_label"/>

        <Button
            android:id="@+id/btn_wealth_plus"
            android:layout_width="40dp"
            android:layout_height="40dp"
            android:text="+"
            android:onClick="@{()->viewModel.incrementWealth()}"
            app:layout_constraintStart_toEndOf="@id/tv_wealth_value"
            app:layout_constraintTop_toTopOf="@id/tv_wealth_label"
            app:layout_constraintBottom_toBottomOf="@id/tv_wealth_label"/>

        <!--健康指数-->
        <TextView
            android:id="@+id/tv_health_label"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="健康:"
            android:textSize="20sp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/tv_wealth_label"
            android:layout_marginTop="10dp"/>

        <TextView
            android:id="@+id/tv_health"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{String.valueOf(viewModel.health)}"
            android:textSize="20sp"
            android:gravity="center"
            app:layout_constraintStart_toEndOf="@id/tv_health_label"
            app:layout_constraintTop_toTopOf="@id/tv_health_label"
            app:layout_constraintBottom_toBottomOf="@id/tv_health_label"
            android:layout_marginStart="10dp"/>

        <EditText
            android:id="@+id/et_health"
            android:inputType="number"
            android:text="@{String.valueOf(viewModel.health)}"
            android:onTextChanged="@{(s, start, before, count) -> viewModel.onHealthTextChanged(s)}"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            app:layout_constraintStart_toEndOf="@id/tv_health"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="@id/tv_health"
            app:layout_constraintBottom_toBottomOf="@id/tv_health"
            android:layout_marginStart="10dp"
            android:hint="请修改健康指数" />

        <!-- 武功 -->
        <TextView
            android:id="@+id/tv_skill"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="武功:一阳指"
            android:textSize="20sp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/tv_health_label"
            android:layout_marginTop="10dp"/>

        <!-- 介绍 -->
        <TextView
            android:id="@+id/tv_intro"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="介绍:少女收割机"
            android:textSize="20sp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/tv_skill"
            android:layout_marginTop="10dp"/>
    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>

viewModel 增加了一个onHealthTextChanged函数实现方向绑定,代码:

Kotlin 复制代码
package com.example.mvvmdemo

import androidx.databinding.PropertyChangeRegistry
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

// 武侠人物ViewModel,存储所有可调整的属性。
// 业务逻辑 + 数据管理
class WuxiaCharacterViewModel : ViewModel() {
    // 用于管理属性变化通知的工具类
    private val callbacks = PropertyChangeRegistry()
    // 魅力值(初始99)
    private val _charm = MutableLiveData<Int>(99) // MutableLiveData可变。 这里只在内部修改数据
    val charm: LiveData<Int> = _charm // LiveData 不可变。 暴露给外部,只读

    // 武力值(初始85)
    private val _force = MutableLiveData<Int>(85)
    val force: LiveData<Int> = _force

    // 财富值(初始1亿,用Long存储)
    private val _wealth = MutableLiveData<Long>(100000000)
    val wealth: LiveData<Long> = _wealth

    // 健康指数(初始99)
    val health = MutableLiveData<Int>(99)

    // 新增:手动处理文本变化
    fun onHealthTextChanged(s: CharSequence?) {
        val newValue = s?.toString()?.toIntOrNull() ?: 0
        // 只有当新值确实不同时才更新,防止死循环
        if (health.value != newValue) {
            health.value = newValue
        }
    }

    // 调整魅力值
    fun incrementCharm() {
        _charm.value = (_charm.value ?: 99) + 1
    }
    fun decrementCharm() {
        val current = _charm.value ?: 99
        if (current > 0) {
            _charm.value = current - 1
        }
    }

    // 调整武力值
    fun incrementForce() {
        _force.value = (_force.value ?: 85) + 1
    }
    fun decrementForce() {
        val current = _force.value ?: 85
        if (current > 0) {
            _force.value = current - 1
        }
    }

    // 调整财富值(每次增减100万)
    fun incrementWealth() {
        _wealth.value = (_wealth.value ?: 100000000) + 1000000
    }
    fun decrementWealth() {
        val current = _wealth.value ?: 100000000
        if (current > 1000000) { // 至少保留100万
            _wealth.value = current - 1000000
        }
    }

    // 格式化财富显示(转成"X亿"或"X万")
    fun formatWealth(wealth: Long): String {
        return when {
            wealth >= 100000000 -> "${wealth / 100000000.0}亿"
            wealth >= 10000 -> "${wealth / 10000}万"
            else -> "$wealth"
        }
    }


    // 加载状态(告诉UI是否在加载中,比如显示加载动画)
//    private val _isLoading = MutableLiveData<Boolean>(false)
//    val isLoading: LiveData<Boolean> = _isLoading

    // 错误信息(新增:请求失败时提示用户)
//    private val _errorMsg = MutableLiveData<String>("")
//    val errorMsg: LiveData<String> = _errorMsg

    fun refreshCharacterData() {
        // 模拟从服务端获取数据
        // 显示加载状态(UI会感知到,显示加载动画)
//        _isLoading.value = true
//        _errorMsg.value = ""

        // viewModelScope启动协程(自动跟随ViewModel生命周期,避免内存泄漏)
//        viewModelScope.launch {
//            // 调用model层代码,获取数据。。。
//        }
    }
}

MainActivity ,延迟2每秒修改健康值,测试下输入框的数字是否改变:

Kotlin 复制代码
package com.example.mvvmdemo

import android.os.Bundle
import android.os.Handler
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.ViewModelProvider
import com.example.mvvmdemo.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
    // 武侠人物ViewModel
    private lateinit var characterViewModel: WuxiaCharacterViewModel

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

        // databinding加载布局
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)

        // 注册LifecycleObserver(Activity是LifecycleOwner)
        lifecycle.addObserver(MyLifecycleObserver()) // 这里没啥用

        // 通过ViewModelProvider获取ViewModel(配置变化不丢失数据)
        //this即Activity,实现了ViewModelStoreOwner 接口, ViewModel 会和 Activity 的生命周期绑定
        characterViewModel = ViewModelProvider(this)[WuxiaCharacterViewModel::class.java]

        // 绑定ViewModel到布局。
        binding.viewModel = characterViewModel

        // 设置生命周期所有者
        binding.lifecycleOwner = this

        Handler().postDelayed({
            characterViewModel.health.value = 10000
        }, 2000)
    }
}

ok. 数据变化,输入框值也修改。修改输入框,数据也改动。实现了双向绑定。 还有个方案是数据用string类型,和输入框需要的数据类型一致。

相关推荐
草莓熊Lotso1 小时前
Linux 进程间通信之命名管道(FIFO):跨进程通信的实用方案
android·java·linux·运维·服务器·数据库·c++
草莓熊Lotso2 小时前
MySQL 表约束核心指南:从基础约束到外键关联(含实战案例)
android·运维·服务器·数据库·c++·人工智能·mysql
鹏多多2 小时前
Flutter使用pretty_qr_code生成高颜值二维码
android·前端·flutter
XiaoLeisj2 小时前
Android 文件与数据存储实战:SharedPreferences、SQLite 与 Room 的渐进式实现
android·java·数据库·ui·sqlite·room·sp
耶叶2 小时前
Android开发:基于SharedPreferences实现的状态缓存
android·kotlin
萝卜大战僵尸2 小时前
Android Studio
android·ide·android studio
三少爷的鞋2 小时前
为什么我不建议UI 直接访问 Repository
android
一只特立独行的Yang12 小时前
Android graphics - 框架摘要
android
AC赳赳老秦14 小时前
DeepSeek优化多智能体指令:避免协同冲突,提升自动化流程稳定性
android·大数据·运维·人工智能·自然语言处理·自动化·deepseek