MVI模式

MVI 是 Model-View-Intent 的缩写,它是一种架构模式,通常用于构建用户界面,尤其在 Android 开发中越来越受欢迎。它强调单向数据流不可变性,这使得应用的状态变化更加可预测和易于调试。

核心思想

MVI 的核心思想是将应用的状态和用户交互分离开来,并通过一个明确的循环来管理它们。

  1. Model (模型):

    • 是什么? Model 代表了应用的状态 (State)业务逻辑
    • 状态 (State) : 这是 MVI 中最核心的概念。UI 的每一个可能的表现都应该被一个不可变的 State 对象所描述。例如,一个列表界面的 State 可能包含 isLoading (是否正在加载)、data (列表数据)、error (错误信息) 等字段。
    • 不可变性: 状态对象一旦创建,就不能被修改。当需要更新 UI 时,必须创建一个包含新状态的全新对象。这使得状态变化轨迹非常清晰。
    • 业务逻辑: Model 也包含了处理数据、执行操作(如网络请求、数据库读写)的逻辑。
  2. View (视图):

    • 是什么? View 就是用户界面,负责展示 Model 中的状态将用户的交互意图 (Intent) 传递出去
    • 被动性: View 本身不包含任何业务逻辑。它就像一个 "渲染器",收到什么 State 就展示什么。
    • 观察状态: View 会订阅(观察)Model 提供的状态流。当状态发生变化时,View 会自动收到通知并更新自己。
  3. Intent (意图):

    • 是什么? Intent 代表了用户的一次交互行为一个操作请求。它是一个不可变的数据类,封装了用户想要做什么。
    • 例子 : 点击按钮、输入文本、下拉刷新等。这些都可以被封装成一个个 Intent 对象,如 ClickLoginButtonIntentUpdateUsernameIntentPullToRefreshIntent
    • 单向触发: 用户操作产生 Intent,Intent 被发送出去,触发后续的状态变化。

MVI 的工作流程 (单向数据流)

MVI 的数据流是单向的,形成一个闭环,非常容易理解和追踪:

  1. 用户交互: 用户在 View 上进行操作,比如点击一个 "加载数据" 的按钮。
  2. 触发 Intent : View 捕获到这个点击事件,将其封装成一个特定的 Intent(例如 LoadDataIntent),然后发送给 ViewModel (或类似的中间层)。
  3. 处理 Intent : ViewModel 接收 Intent。它会根据 Intent 的类型,调用相应的业务逻辑(可能在 Model 层中)。例如,LoadDataIntent 会触发一个网络请求去获取数据。
  4. 产生新 State : 业务逻辑执行后(可能是同步的也可能是异步的),会产生一个新的应用状态(State)。例如,网络请求开始时,会产生一个 LoadingState;请求成功后,会产生一个包含数据的 DataLoadedState;请求失败则会产生一个 ErrorState
  5. 发送 State 到 View : ViewModel 将这个新的 State 发送给 View。
  6. View 渲染 : View 接收到新的 State,根据 State 中的数据和标志位来更新 UI。例如,如果是 LoadingState 就显示进度条,如果是 DataLoadedState 就显示数据列表,如果是 ErrorState 就显示错误提示。

这个流程可以简化为: View --(用户操作)--> Intent --(触发)--> ViewModel --(处理 / 调用 Model)--> 新State --(通知)--> View --(渲染)--> ... (循环)

MVI 的优势

  1. 高度可预测性: 由于状态是不可变的且数据流是单向的,应用的行为变得非常可预测。任何状态的变化都可以追溯到一个具体的 Intent。
  2. 易于调试: 因为所有状态变化都遵循一个清晰的路径,你可以轻松地打印出每一个 Intent 和每一次 State 的变化,从而快速定位问题。时间旅行调试(Time Travel Debugging)在 MVI 中也更容易实现。
  3. 关注点分离 (Separation of Concerns): View 只负责展示,ViewModel 只负责处理逻辑和管理状态,Model 负责数据和业务规则。职责清晰,代码结构更清晰。
  4. UI 与逻辑解耦: View 不依赖于任何具体的业务逻辑实现,它只关心如何根据 State 来绘制界面。这使得 UI 测试变得非常简单,你可以模拟各种 State 来测试 View 的表现。
  5. 状态集中管理: 应用的所有状态都集中在 State 对象中,避免了状态分散在各个 View 或控件中的情况。

MVI 的挑战

  1. 样板代码: 相比其他模式(如 MVP),MVI 可能需要编写更多的模板代码,特别是大量的 Intent 和 State 类。
  2. 学习曲线: 对于习惯了双向绑定或更松散架构的开发者来说,理解和适应单向数据流和不可变性需要一定的学习成本。
  3. 状态设计: 设计一个好的、细粒度的 State 模型是关键。如果 State 设计得过于粗糙,可能会导致不必要的 UI 重绘;如果过于精细,又会增加复杂性。

总结

MVI 是一种非常强大和优雅的架构模式,特别适合构建复杂且状态多变的应用。它通过强制单向数据流和不可变性,让应用的状态管理变得简单、可预测和易于测试。虽然有一些初始的学习和代码成本,但从长远来看,它能极大地提高代码的质量和可维护性。

如何在 MVI 模式中进行数据持久化?

在 MVI 模式中进行数据持久化,核心原则是将数据持久化操作封装在 Model 层,确保数据流的单向性和可预测性。以下是具体的实现思路和步骤:


1. 数据持久化的核心原则

  • 单一数据源:Model 层是唯一的数据来源,View 层不直接访问持久化存储。
  • 单向数据流
    • View 发送 Intent → ViewModel 处理 → Model 层执行持久化操作 → Model 更新状态 → View 渲染新状态。
  • 可测试性:持久化逻辑应易于模拟和测试,避免直接在 ViewModel 或 View 中操作数据库 / 文件。

2. 分层架构设计

建议采用以下分层结构,清晰分离关注点:

复制代码
┌─────────────┐     ┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│     View    │     │  ViewModel  │     │    Model    │     │ Persistence │
│ (UI 组件)   │────▶│ (状态管理)   │────▶│ (业务逻辑)   │────▶│ (数据库/文件) │
└─────────────┘     └─────────────┘     └─────────────┘     └─────────────┘
       ▲                  ▲                  ▲                  │
       │                  │                  │                  │
       └──────────────────┴──────────────────┴──────────────────┘
              (状态反馈,单向数据流)

3. 具体实现步骤

步骤 1:定义数据模型(Entity)

创建数据类,代表需要持久化的数据。

复制代码
// 数据模型
data class User(
    val id: String,
    val name: String,
    val email: String
)
步骤 2:定义持久化接口(Repository)

在 Model 层定义一个 Repository 接口,封装持久化操作(如增删改查)。这样可以隔离具体的持久化实现(如 Room、SharedPreferences、Retrofit)。

复制代码
// 持久化仓库接口
interface UserRepository {
    suspend fun saveUser(user: User)
    suspend fun getUserById(id: String): User?
    suspend fun deleteUser(id: String)
    suspend fun getAllUsers(): List<User>
}
步骤 3:实现 Repository

根据需求选择持久化方案(Room、DataStore、文件等),实现 UserRepository 接口。

示例:使用 Room 数据库
复制代码
// Room 实体
@Entity(tableName = "users")
data class UserEntity(
    @PrimaryKey val id: String,
    val name: String,
    val email: String
)

// Room DAO
@Dao
interface UserDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insert(user: UserEntity)

    @Query("SELECT * FROM users WHERE id = :id")
    suspend fun getUserById(id: String): UserEntity?

    @Delete
    suspend fun delete(user: UserEntity)

    @Query("SELECT * FROM users")
    suspend fun getAllUsers(): List<UserEntity>
}

// Room 数据库
@Database(entities = [UserEntity::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
}

// Repository 实现
class UserRepositoryImpl(
    private val userDao: UserDao
) : UserRepository {
    override suspend fun saveUser(user: User) {
        userDao.insert(user.toEntity())
    }

    override suspend fun getUserById(id: String): User? {
        return userDao.getUserById(id)?.toDomain()
    }

    override suspend fun deleteUser(id: String) {
        userDao.getUserById(id)?.let { userDao.delete(it) }
    }

    override suspend fun getAllUsers(): List<User> {
        return userDao.getAllUsers().map { it.toDomain() }
    }

    // 转换函数:Domain → Entity
    private fun User.toEntity(): UserEntity {
        return UserEntity(id, name, email)
    }

    // 转换函数:Entity → Domain
    private fun UserEntity.toDomain(): User {
        return User(id, name, email)
    }
}
步骤 4:在 Model 层集成 Repository

Model 层负责调用 Repository 进行数据持久化,并更新应用状态。

复制代码
// 应用状态
data class AppState(
    val users: List<User> = emptyList(),
    val isLoading: Boolean = false,
    val error: String? = null
)

// Model 层(业务逻辑)
class UserModel(
    private val userRepository: UserRepository
) {
    // 状态流
    private val _state = MutableStateFlow(AppState())
    val state: StateFlow<AppState> = _state.asStateFlow()

    // 处理 Intent
    suspend fun handleIntent(intent: UserIntent) {
        when (intent) {
            is UserIntent.SaveUser -> saveUser(intent.user)
            is UserIntent.LoadUsers -> loadUsers()
            is UserIntent.DeleteUser -> deleteUser(intent.userId)
        }
    }

    private suspend fun saveUser(user: User) {
        _state.update { it.copy(isLoading = true) }
        try {
            userRepository.saveUser(user)
            // 保存成功后,重新加载用户列表
            loadUsers()
        } catch (e: Exception) {
            _state.update { it.copy(error = e.message, isLoading = false) }
        }
    }

    private suspend fun loadUsers() {
        _state.update { it.copy(isLoading = true) }
        try {
            val users = userRepository.getAllUsers()
            _state.update { it.copy(users = users, isLoading = false, error = null) }
        } catch (e: Exception) {
            _state.update { it.copy(error = e.message, isLoading = false) }
        }
    }

    private suspend fun deleteUser(userId: String) {
        _state.update { it.copy(isLoading = true) }
        try {
            userRepository.deleteUser(userId)
            loadUsers()
        } catch (e: Exception) {
            _state.update { it.copy(error = e.message, isLoading = false) }
        }
    }
}

// Intent 定义
sealed class UserIntent {
    data class SaveUser(val user: User) : UserIntent()
    object LoadUsers : UserIntent()
    data class DeleteUser(val userId: String) : UserIntent()
}
步骤 5:ViewModel 调用 Model 层

ViewModel 接收 View 发送的 Intent,调用 Model 层处理,并将状态暴露给 View。

复制代码
class UserViewModel(
    private val userModel: UserModel
) : ViewModel() {
    // 状态流(暴露给 View)
    val state: StateFlow<AppState> = userModel.state

    // 处理 View 发送的 Intent
    fun onIntent(intent: UserIntent) {
        viewModelScope.launch {
            userModel.handleIntent(intent)
        }
    }
}
步骤 6:View 层触发 Intent 并渲染状态

View 层通过 ViewModel 发送 Intent,并观察状态变化以更新 UI。

复制代码
// Activity/Fragment 中
class UserActivity : AppCompatActivity() {
    private val viewModel: UserViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_user)

        // 观察状态变化
        lifecycleScope.launchWhenStarted {
            viewModel.state.collect { state ->
                if (state.isLoading) {
                    // 显示加载中
                    progressBar.visibility = View.VISIBLE
                } else {
                    progressBar.visibility = View.GONE
                    if (state.error != null) {
                        // 显示错误信息
                        Toast.makeText(this@UserActivity, state.error, Toast.LENGTH_SHORT).show()
                    } else {
                        // 更新 UI 列表
                        updateUserList(state.users)
                    }
                }
            }
        }

        // 按钮点击:保存用户
        saveButton.setOnClickListener {
            val user = User(
                id = UUID.randomUUID().toString(),
                name = nameInput.text.toString(),
                email = emailInput.text.toString()
            )
            viewModel.onIntent(UserIntent.SaveUser(user))
        }

        // 页面加载时:加载用户列表
        viewModel.onIntent(UserIntent.LoadUsers)
    }

    private fun updateUserList(users: List<User>) {
        // 更新 RecyclerView 适配器
        userAdapter.submitList(users)
    }
}

4. 其他持久化方案

除了 Room,还可以根据需求选择:

  • DataStore:用于存储键值对(替代 SharedPreferences)。
  • 文件存储:适用于存储大文件或复杂数据结构。
  • 远程 API + 本地缓存:结合 Retrofit 和 Room,实现数据同步。

5. 总结

在 MVI 中进行数据持久化的关键是:

  • 封装:将持久化操作封装在 Model 层的 Repository 中。
  • 单向数据流:确保所有数据操作都通过 Intent 触发,状态变化通过 State 反馈。
  • 可测试性:通过接口隔离具体实现,便于单元测试。

通过以上步骤,可以在 MVI 模式中优雅地实现数据持久化,同时保持代码的清晰和可维护性。

如何在Model层实现数据持久化?

在 MVI 模式中,Model 层 是负责数据管理、业务逻辑和持久化操作的核心。数据持久化的实现主要依赖于 Repository 模式数据源封装,确保数据操作的一致性、可测试性和可扩展性。以下是具体的实现步骤和最佳实践:


1. Model 层的核心组件

在 MVI 中,Model 层通常包含以下组件:

  • 数据模型(Data Class/Entity):定义数据结构(如用户、商品等)。
  • 仓库(Repository):封装数据操作逻辑,作为数据源的统一入口。
  • 数据源(Data Source):负责实际的持久化操作(如数据库、文件、网络)。
  • 状态管理(State):维护应用状态(如加载中、成功、错误)。

2. 数据持久化的实现步骤

步骤 1:定义数据模型

首先,创建数据类来表示需要持久化的数据。这些类通常是 不可变的 (使用 data classval)。

复制代码
// 领域模型(Domain Model)
data class User(
    val id: String,
    val name: String,
    val email: String,
    val createdAt: Long = System.currentTimeMillis()
)

步骤 2:定义数据源接口

为了隔离持久化实现(如 Room、DataStore、文件等),定义 数据源接口。每个数据源负责一种数据存储方式(本地或远程)。

复制代码
// 本地数据源接口
interface LocalDataSource {
    suspend fun saveUser(user: User)
    suspend fun getUserById(id: String): User?
    suspend fun deleteUser(id: String)
    suspend fun getAllUsers(): List<User>
}

// 远程数据源接口(如果需要从网络获取数据)
interface RemoteDataSource {
    suspend fun fetchUserFromApi(id: String): User
}

步骤 3:实现数据源

根据实际需求,实现本地或远程数据源。以下是常见的持久化方案:

方案 A:使用 Room 数据库(推荐)

Room 是 Android 官方推荐的本地数据库解决方案,适合结构化数据存储。

1. 添加依赖
复制代码
// build.gradle (Module level)
dependencies {
    def room_version = "2.5.2"

    implementation "androidx.room:room-runtime:$room_version"
    kapt "androidx.room:room-compiler:$room_version"
    implementation "androidx.room:room-ktx:$room_version" // 协程支持
}
2. 定义实体(Entity)

将领域模型转换为 Room 实体(如果需要)。

复制代码
import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "users")
data class UserEntity(
    @PrimaryKey val id: String,
    val name: String,
    val email: String,
    val createdAt: Long
)

// 扩展函数:Domain Model ↔ Room Entity
fun User.toEntity(): UserEntity {
    return UserEntity(id, name, email, createdAt)
}

fun UserEntity.toDomain(): User {
    return User(id, name, email, createdAt)
}
3. 定义 DAO(Data Access Object)
复制代码
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query

@Dao
interface UserDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insert(user: UserEntity)

    @Query("SELECT * FROM users WHERE id = :id")
    suspend fun getUserById(id: String): UserEntity?

    @Delete
    suspend fun delete(user: UserEntity)

    @Query("SELECT * FROM users")
    suspend fun getAllUsers(): List<UserEntity>
}
4. 实现 Room 数据库
复制代码
import androidx.room.Database
import androidx.room.RoomDatabase

@Database(entities = [UserEntity::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao

    companion object {
        // 单例模式
        @Volatile
        private var INSTANCE: AppDatabase? = null

        fun getInstance(context: Context): AppDatabase {
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    AppDatabase::class.java,
                    "app_database"
                ).build()
                INSTANCE = instance
                instance
            }
        }
    }
}
5. 实现 LocalDataSource
复制代码
class LocalDataSourceImpl(
    private val userDao: UserDao
) : LocalDataSource {
    override suspend fun saveUser(user: User) {
        userDao.insert(user.toEntity())
    }

    override suspend fun getUserById(id: String): User? {
        return userDao.getUserById(id)?.toDomain()
    }

    override suspend fun deleteUser(id: String) {
        userDao.getUserById(id)?.let { userDao.delete(it) }
    }

    override suspend fun getAllUsers(): List<User> {
        return userDao.getAllUsers().map { it.toDomain() }
    }
}
方案 B:使用 DataStore(替代 SharedPreferences)

DataStore 是 Google 推出的用于存储键值对或类型化数据的现代解决方案。

1. 添加依赖
复制代码
dependencies {
    implementation "androidx.datastore:datastore-preferences:1.0.0"
}
2. 实现 LocalDataSource
复制代码
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

class LocalDataSourceImpl(
    private val dataStore: DataStore<Preferences>
) : LocalDataSource {
    private companion object {
        val USER_KEY = stringPreferencesKey("user")
    }

    override suspend fun saveUser(user: User) {
        dataStore.edit { preferences ->
            preferences[USER_KEY] = Json.encodeToString(user) // 使用 JSON 序列化
        }
    }

    override suspend fun getUserById(id: String): User? {
        // 假设 DataStore 中存储单个用户,实际使用时可能需要调整
        return dataStore.data.map { preferences ->
            preferences[USER_KEY]?.let { Json.decodeFromString<User>(it) }
        }.firstOrNull()
    }

    override suspend fun deleteUser(id: String) {
        dataStore.edit { preferences ->
            preferences.remove(USER_KEY)
        }
    }

    override suspend fun getAllUsers(): List<User> {
        // 示例:返回单个用户
        return getUserById("")?.let { listOf(it) } ?: emptyList()
    }
}

步骤 4:实现 Repository

Repository 是 Model 层的核心,负责协调本地和远程数据源,并提供统一的数据访问接口。

复制代码
class UserRepositoryImpl(
    private val localDataSource: LocalDataSource,
    private val remoteDataSource: RemoteDataSource? = null // 可选:远程数据源
) : UserRepository {
    override suspend fun saveUser(user: User) {
        localDataSource.saveUser(user)
    }

    override suspend fun getUserById(id: String): User? {
        // 策略:先从本地获取,本地没有再从远程获取
        return localDataSource.getUserById(id) 
            ?: remoteDataSource?.fetchUserFromApi(id)?.also {
                localDataSource.saveUser(it) // 缓存到本地
            }
    }

    override suspend fun deleteUser(id: String) {
        localDataSource.deleteUser(id)
    }

    override suspend fun getAllUsers(): List<User> {
        return localDataSource.getAllUsers()
    }
}

// Repository 接口
interface UserRepository {
    suspend fun saveUser(user: User)
    suspend fun getUserById(id: String): User?
    suspend fun deleteUser(id: String)
    suspend fun getAllUsers(): List<User>
}

步骤 5:集成到 Model 层

在 MVI 的 Model 层中,使用 Repository 来处理数据持久化,并更新应用状态。

复制代码
// 应用状态
data class AppState(
    val users: List<User> = emptyList(),
    val isLoading: Boolean = false,
    val error: String? = null
)

// Model 层(业务逻辑 + 状态管理)
class UserModel(
    private val userRepository: UserRepository
) {
    private val _state = MutableStateFlow(AppState())
    val state: StateFlow<AppState> = _state.asStateFlow()

    suspend fun handleIntent(intent: UserIntent) {
        when (intent) {
            is UserIntent.SaveUser -> saveUser(intent.user)
            is UserIntent.LoadUsers -> loadUsers()
            is UserIntent.DeleteUser -> deleteUser(intent.userId)
        }
    }

    private suspend fun saveUser(user: User) {
        _state.update { it.copy(isLoading = true) }
        try {
            userRepository.saveUser(user)
            loadUsers() // 保存后重新加载列表
        } catch (e: Exception) {
            _state.update { it.copy(error = e.message, isLoading = false) }
        }
    }

    private suspend fun loadUsers() {
        _state.update { it.copy(isLoading = true) }
        try {
            val users = userRepository.getAllUsers()
            _state.update { it.copy(users = users, isLoading = false, error = null) }
        } catch (e: Exception) {
            _state.update { it.copy(error = e.message, isLoading = false) }
        }
    }

    private suspend fun deleteUser(userId: String) {
        _state.update { it.copy(isLoading = true) }
        try {
            userRepository.deleteUser(userId)
            loadUsers() // 删除后重新加载列表
        } catch (e: Exception) {
            _state.update { it.copy(error = e.message, isLoading = false) }
        }
    }
}

// Intent 定义
sealed class UserIntent {
    data class SaveUser(val user: User) : UserIntent()
    object LoadUsers : UserIntent()
    data class DeleteUser(val userId: String) : UserIntent()
}

3. 依赖注入(推荐)

为了简化组件间的依赖关系,推荐使用 依赖注入框架(如 Hilt)。

示例:使用 Hilt 注入 Repository

复制代码
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object AppModule {

    @Provides
    @Singleton
    fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase {
        return AppDatabase.getInstance(context)
    }

    @Provides
    fun provideUserDao(database: AppDatabase): UserDao {
        return database.userDao()
    }

    @Provides
    fun provideLocalDataSource(userDao: UserDao): LocalDataSource {
        return LocalDataSourceImpl(userDao)
    }

    @Provides
    @Singleton
    fun provideUserRepository(localDataSource: LocalDataSource): UserRepository {
        return UserRepositoryImpl(localDataSource)
    }
}

在 ViewModel 中注入 Repository:

复制代码
import javax.inject.Inject

class UserViewModel @Inject constructor(
    private val userRepository: UserRepository
) : ViewModel() {
    // ...
}

4. 总结

在 MVI 模式的 Model 层实现数据持久化的关键是:

  1. 分层设计:通过 Repository 隔离数据源,封装业务逻辑。
  2. 接口驱动:定义数据源接口,便于替换不同的持久化方案。
  3. 不可变性:确保数据模型和状态对象不可变,避免副作用。
  4. 依赖注入:使用 Hilt 等框架简化组件间的依赖管理。

通过以上步骤,可以实现一个清晰、可测试且易于维护的数据持久化层,同时保持 MVI 模式的单向数据流特性。

相关推荐
爱笑鱼16 小时前
Binder(八):远端进程死了,BinderProxy 为什么还能收到通知?
android
Android-Flutter16 小时前
Kotlin 冷流与热流详解
android·kotlin
zhangphil16 小时前
Android ContentProvider/ContentResolver读图片,跨进程 IPC慢
android
yueqc116 小时前
Android .so 文件压缩
android·so·包体积
龚礼鹏17 小时前
深入解析 Android Automotive (AAOS) 启动流程与 CarService 核心机制(基于 Android 16 最新源码视角)
android
rosmis17 小时前
agent各指标定义
android·java·开发语言
风样滴男人哟18 小时前
PHP特性之反射类ReflectionClass机制
android·开发语言·php
小孔龙19 小时前
RenderNode 与 DisplayList:Android 硬件加速的绘制记录与复用
android·性能优化
qizayaoshuap20 小时前
鸿蒙 ArkTS 实战:搜索列表 城市实时过滤(示例 94)
android·华为·harmonyos
朝星20 小时前
Framework筑基之Handler消息机制5(应用层)
android