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 模式的单向数据流特性。

相关推荐
renxhui1 小时前
Dart 速通攻略(面向 Android 工程师)
android·flutter·dart
m***9821 小时前
Redis6.2.6下载和安装
android·前端·后端
未来之窗软件服务1 小时前
幽冥大陆(三十九)php二维数组去重——东方仙盟筑基期
android·开发语言·算法·php·仙盟创梦ide·东方仙盟·东方仙盟sdk
w***4242 小时前
【mysql部署】在ubuntu22.04上安装和配置mysql教程
android·mysql·adb
e***75392 小时前
MySQL错误-this is incompatible with sql_mode=only_full_group_by完美解决方案
android·sql·mysql
道路与代码之旅2 小时前
“变量也能是函数?——论 Kotlin 中那些会说话的变量,以及它们如何让代码少说废话”
android·开发语言·kotlin
x***01062 小时前
Mysql之主从复制
android·数据库·mysql
千里马学框架2 小时前
wms开发常用调试开发技巧之WMShell的实用命令
android·framework·wms·安卓framework开发·systemui·proto·wmshell
龙之叶3 小时前
MT8766平台Android 12系统ESIM功能实现指南
android