这是 MVI 架构的第三篇,系列文章目录如下:
Android 架构之 MVI 完全体 | 重新审视 MVVM 之殇,PartialChange & Reducer 来拯救
Android 架构之 MVI 究极体 | 状态和事件分道扬镳,粘性不再是问题 其中第一篇剖析了 MVI 的概念,第二篇是 MVI 在项目实战中的初级应用,而这一篇将重构上篇的代码,以展示 MVI 的完全体。
MVI 架构有三大关键词:“唯一可信数据源”+“单向数据流”+“响应式编程”,以及一些关键概念,比如Intent,State。理解这些概念之后,能更轻松地阅读本文。(强烈建议从第一篇开始阅读)
在上一篇中,用 MVI 重构了“新闻流”这个业务场景。本篇在此基础上进一步拓展,引入 MVI 中两个重要的概念PartialChange和Reducer。
假设“新闻流”这个业务场景,用户可以触发如下行为:
在 MVVM 中,这些行为被表达为 ViewModel 的一个方法调用。在 MVI 中被称为意图Intent,它们不再是一个方法调用,而是一个数据。通常可被这样定义:
sealed class FeedsIntent {
data class Init(val type: Int, val count: Int) : FeedsIntent()
data class More(val timestamp: Long, val count: Int) : FeedsIntent()
data class Report(val id: Long) : FeedsIntent()
}
这样做使得界面意图都以数据的形式流入到一个流中,好处是,可以用流的方式统一管理所有意图。更详细的讲解可以点击Android 架构之 MVI | 响应式编程 + 单向数据流 + 唯一可信数据源。
产品文档定义了所有的用户意图Intent,而设计稿定义了所有的界面状态State:
data class NewsState(
val data: List<News>, // 新闻列表
val isLoading: Boolean, // 是否正在首次加载
val isLoadingMore: Boolean, // 是否正在上拉加载更多
val errorMessage: String, // 加载错误信息 toast
val reportToast: String, // 举报结果 toast
) {
companion object {
// 新闻流的初始状态
val initial = NewsState(
data = emptyList(),
isLoading = true,
isLoadingMore = false,
errorMessage = "",
reportToast = ""
)
}
}
在 MVI 中,把界面的一次展示理解为单个 State 的一次渲染。相较于 MVVM 中一个界面可能被分拆为多个 LiveData,State 这种唯一数据源降低了复杂度,使得代码容易维护。
有了 Intent 和 State,整个界面刷新的过程就形成了一条单向数据流,如下图所示:

MVI 就是用“响应式编程”的方式将这条数据流中的若干 Intent 转换成唯一 State。初级的转换方式是直接将 Intent 映射成 State,详细分析可以点击如何把业务代码越写越复杂?(二)| Flow 替换 LiveData 重构数据链路,更加 MVI。
理论上 Intent 是无法直接转换为 State 的。因为 Intent 只表达了用户触发的行为,而行为产生的结果才对应一个 State。更具体的说,“上拉加载更多新闻”可能产生三个结果:
其中每一个结果都对应一个 State。“单向数据流”内部的数据变换详情如下:

每一个意图会产生若干个结果,每个结果对应一个界面状态。
上图看着有“很多条”数据流,但同一时间只可能有一条起作用。上图看着会在 ViewModel 内部形成各种 State,但暴露给界面的还是唯一 State。
因为所有意图产生的所有可能的结果都对应于一个唯一 State 实例,所以每个意图产生的结果只引起 State 部分字段的变化。比如 Init.Success 只会影响 NewsState.data 和 NewsState.isLoading。
在 MVI 框架中,意图 Intent 产生的结果称为部分变化PartialChange。
总结一下:
- MVI 框架中用数据流来理解界面刷新。
- 数据流的起点是界面发出的意图(Intent),一个意图会产生若干结果,它们称为 PartialChange,一个 PartialChange 对应一个 State 实例。
- 数据流的终点是界面对 State 的观察而进行的一次渲染。
界面展示的变化是“连续的”,即界面新状态总是由上一次状态变化而来。就像连环画一样,下一帧是基于上一帧的偏移量。
这种基于老状态产生新状态的行为称为Reduce,用一个 lambda 表达即是(oldState: State) -> State。
界面发出的不同意图会生成不同的结果,每种结果都有各自的方法进行新老状态的变换。比如“上拉加载更多新闻”和“举报新闻”,前者在老状态的尾部追加数据,而后者是在老状态中删除数据。
基于此,Reduce 的 lambda 可作如下表达:(oldState: State, change: PartialChange) -> State,即新状态由老状态和 PartialChange 共同决定。
通常 PartialChange 被定义成密封接口,而 Reduce 定义为内部方法:
// 新闻流的部分变化
sealed interface FeedsPartialChange {
// 描述如何从老状态变化为新状态
fun reduce(oldState: NewsState): NewsState
}
这是 PartialChange 的抽象定义,新闻流场景中,它应该有三个实现类,分别是 Init,More,Report。其中 Init 的实现如下:
sealed class Init : FeedsPartialChange {
// 在初始化新闻流流场景下,老状态如何变化成新状态
override fun reduce(oldState: NewsState): NewsState =
// 对初始化新闻流能产生的所有结果分类讨论,并基于老状态拷贝构建新状态
when (this) {
Loading -> oldState.copy(isLoading = true)
is Success -> oldState.copy(
data = news,//方便地访问Success携带的数据
isLoading = false,
isLoadingMore = false,
errorMessage = ""
)
is Fail -> oldState.copy(
data = emptyList(),
isLoading = false,
isLoadingMore = false,
errorMessage = error
)
}
// 加载中
object Loading : Init()
// 加载成功
data class Success(val news: List<News>) : Init()
// 加载失败
data class Fail(val error: String) : Init()
}
初始化新闻流的 PartialChange 也被实现为密封的,密封产生的效果是,在编译时,其子类的全集就已经全部确定,不允许在运行时动态新增子类,且所有子类必须内聚在一个包名下。
这样做的好处是降低界面刷新的复杂度,即有限个 Intent 会产生有限个 PartialChange,且它们唯一对应一个 State。出 bug 的时候只需从三处找问题:1. Intent 是否发射? 2. 是否生成了既定的 PartialChange? 3. reduce 算法是否有问题?
将 reduce 算法定义在 PartialChange 内部,就能很方便地获取 PartialChange 携带的数据,并基于它构建新状态。
用同样的思路,More 和 Report 的定义如下:
sealed class More : FeedsPartialChange {
override fun reduce(oldState: NewsState): NewsState = when (this) {
Loading -> oldState.copy(
isLoading = false,
isLoadingMore = true,
errorMessage = ""
)
is Success -> oldState.copy(
data = oldState.data + news,// 新数据追加在老数据后
isLoading = false,
isLoadingMore = false,
errorMessage = ""
)
is Fail -> oldState.copy(
isLoadingMore = false,
isLoading = false,
errorMessage = error
)
}
object Loading : More()
data class Success(val news: List<News>) : More()
data class Fail(val error: String) : More()
}
sealed class Report : FeedsPartialChange {
override fun reduce(oldState: NewsState): NewsState = when (this) {
is Success -> oldState.copy(
// 在老数据中删除举报新闻
data = oldState.data.filterNot { it.id == id },
reportToast = "举报成功"
)
Fail -> oldState.copy(reportToast = "举报失败")
}
class Success(val id: Long) : Report()
object Fail : Report()
}
Intent,PartialChange,Reduce,State 定义好了,是时候看看如何用流的方式把它们串联起来!
总体来说,状态是这样变换的:Intent -> PartialChange -(Reduce)-> State
class StateFlowActivity : AppCompatActivity() {
private val newsViewModel by lazy {
ViewModelProvider(
this,
NewsViewModelFactory(NewsRepo(this))
)[NewsViewModel::class.java]
}
// 将所有意图通过 merge 进行合流
private val intents by lazy {
merge(
flowOf(FeedsIntent.Init(1, 5)),// 初始化新闻
loadMoreFlow(), // 加载更多新闻
reportFlow()// 举报新闻
)
}
// 将上拉加载更多转换成数据流
private fun loadMoreFlow() = callbackFlow {
recyclerView.setOnLoadMoreListener {
trySend(FeedsIntent.More(111L, 2))
}
awaitClose { recyclerView.removeOnLoadMoreListener(null) }
}
// 将举报新闻转换成数据流
private fun reportFlow() = callbackFlow {
reportView.setOnClickListener {
val news = newsAdapter.dataList[i] as? News
news?.id?.let { trySend(FeedsIntent.Report(it)) }
}
awaitClose { reportView.setOnClickListener(null) }
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(contentView)
// 订阅意图流
intents
// Intent 流入 ViewModel
.onEach(newsViewModel::send)
.launchIn(lifecycleScope)
// 订阅状态流
newsViewModel.newState
// State 流出 ViewModel,并绘制界面
.collectIn(this) { showNews(it) }
}
}
class NewsViewModel(private val newsRepo: NewsRepo) : ViewModel() {
// 用于接收意图的 SharedFlow
private val _feedsIntent = MutableSharedFlow<FeedsIntent>()
// 意图被变换为状态
val newState =
_feedsIntent.map {} // 伪代码,省略了 将 Intent 变换为 State 的细节
// 将意图发送到流
fun send(intent: FeedsIntent) {
viewModelScope.launch { _feedsIntent.emit(intent) }
}
}
界面可以发出的所有意图都被组织到一个流中,并且罗列在一起。intents流可以作为理解业务逻辑的入口。同时 ViewModel 提供了一个 State 流,供界面订阅。
class NewsViewModel(private val newsRepo: NewsRepo) : ViewModel() {
private val _feedsIntent = MutableSharedFlow<FeedsIntent>()
// 供界面观察的唯一状态
val newState =
_feedsIntent
.toPartialChangeFlow()
.flowOn(Dispatchers.IO)
.stateIn(viewModelScope, SharingStarted.Eagerly,NewsState.initial)
)
}
各种 Intent 转换为 PartialChange 的逻辑被封装在toPartialChangeFlow()中:
// NewsViewModel.kt
// 将 Intent 流变换为 PartialChange 流
private fun Flow<FeedsIntent>.toPartialChangeFlow(): Flow<FeedsPartialChange> = merge(
// 过滤出初始化新闻意图并将其变换为对应的 PartialChange
filterIsInstance<FeedsIntent.Init>().flatMapConcat { it.toPartialChangeFlow() },
// 过滤出上拉加载更多意图并将其变换为对应的 PartialChange
filterIsInstance<FeedsIntent.More>().flatMapConcat { it.toPartialChangeFlow() },
// 过滤出举报新闻意图并将其变换为对应的 PartialChange
filterIsInstance<FeedsIntent.Report>().flatMapConcat { it.toPartialChangeFlow() },
)
toPartialChangeFlow() 被定义为扩展方法。
filterIsInstance() 用于过滤出Flow<FeedsIntent>中的子类型并分类讨论,因为每种 Intent 变换为 PartialChange 的方式有所不同。
最后用 merge 进行合流,它会将每个 Flow 中的数据合起来并发地转发到一个新的流上。merge + filterIsInstance的组合相当于流中的 if-else。
其中的 toPartialChangeFlow() 是各种意图的扩展方法:
// NewsViewModel.kt
private fun FeedsIntent.Init.toPartialChangeFlow() =
flowOf(
// 本地数据库新闻
newsRepo.localNewsOneShotFlow,
// 网络新闻
newsRepo.remoteNewsFlow(this.type.toString(), this.count.toString())
)
// 并发合流
.flattenMerge()
.transformWhile {
emit(it.news)
!it.abort
}
// 将新闻数据变换为成功或失败的 PartialChange
.map { news ->
if (news.isEmpty()) Init.Fail("no news") else Init.Success(news)
}
// 发射展示 Loading 的 PartialChange
.onStart { emit(Init.Loading) }
该扩展方法描述了如何将 FeedsIntent.Init 变换为对应的 PartialChange。同样地,FeedsIntent.More 和 FeedsIntent.Report 的变换逻辑如下:
// NewsViewModel.kt
private fun FeedsIntent.More.toPartialChangeFlow() =
newsRepo.remoteNewsFlow("news", "10")
.map {news ->
if(it.news.isEmpty()) More.Fail("no more news") else More.Success(it.news)
}
.onStart { emit(More.Loading) }
.catch { emit(More.Fail("load more failed by xxx")) }
private fun FeedsIntent.Report.toPartialChangeFlow() =
newsRepo.reportNews(id)
.map { if(it >= 0L) Report.Success(it) else Report.Fail}
.catch { emit((Report.Fail)) }
经过 toPartialChangeFlow() 的变换,现在流中流动的数据是各种类型的 PartialChange。接下来就要将其变换为 State:
// NewsViewModel.kt
val newState =
_feedsIntent
.toPartialChangeFlow()
// 将 PartialChange 变换为 State
.scan(NewsState.initial){oldState, partialChange -> partialChange.reduce(oldState)}
.flowOn(Dispatchers.IO)
.stateIn(viewModelScope, SharingStarted.Eagerly,NewsState.initial)
)
使用scan()进行变换:
// 从 Flow<T> 变换为 Flow<R>
public fun <T, R> Flow<T>.scan(
initial: R, // 初始值
operation: suspend (accumulator: R, value: T) -> R // 累加算法
): Flow<R> = runningFold(initial, operation)
public fun <T, R> Flow<T>.runningFold(
initial: R,
operation: suspend (accumulator: R, value: T) -> R): Flow<R> = flow {
// 累加器
var accumulator: R = initial
emit(accumulator)
collect { value ->
// 进行累加
accumulator = operation(accumulator, value)
// 向下游发射累加值
emit(accumulator)
}
}
从 scan() 的签名看,是将一个流变换为另一个流,看似和 map() 相似。但它的变换算法是带累加的。用 lambda 表达为(accumulator: R, value: T) -> R。
这不正好就是上面提到的 Reduce 吗!即基于老状态和新 PartialChange 生成新状态。
就新闻流这个场景,用图来对比下 MVVM 和 MVI 复杂度的区别。

这张图表达了三种复杂度:
再来看一下让人耳目一新的 MVI 吧:

完美化解上述三个没有必要的复杂度。
总之,用上 MVI 后,新需求不再破坏老逻辑,出 bug 了能更快速定位到问题。
还有一个问题有待解决,那就是 MVI 框架下,刷新界面时持久性状态 State 和 一次性事件 Event 的区别对待。
在 MVVM 中,因为 LiveData 的粘性,导致一次性事件被界面多次消费。对此有多种解决方案。详情可点击LiveData 面试题库、解答、源码分析
但 MVI 的解题思路略有不同,限于篇幅原因,只能下回分析,欢迎持续关注~
MVI 框架中用单向数据流来理解界面刷新。整个数据流中包含的数据依次如下:Intent,PartialChange,State
数据流的起点是界面发出的意图(Intent),一个意图会产生若干结果,它们称为 PartialChange,一个 PartialChange 对应一个 State 实例。
数据流的终点是界面对 State 的观察而进行的一次渲染。
MVI 就是用“响应式编程”的方式将单向数据流中的若干 Intent 转换成唯一 State。
MVI 强调的单向数据流表现在两个层面:
完整代码如下,也可以从这个地址克隆。
class StateFlowActivity : AppCompatActivity() {
private val newsAdapter2 by lazy {
VarietyAdapter2().apply {addProxy(NewsProxy())}
}
private val intents by lazy {
merge(
flowOf(FeedsIntent.Init(1, 5)),
loadMoreFlow(),
reportFlow()
)
}
private fun loadMoreFlow() = callbackFlow {
recyclerView.setOnLoadMoreListener {
trySend(FeedsIntent.More(111L, 2))
}
awaitClose { recyclerView.removeOnLoadMoreListener(null) }
}
private fun reportFlow() = callbackFlow {
reportView.setOnClickListener {
val news = newsAdapter.dataList[i] as? News
news?.id?.let { trySend(FeedsIntent.Report(it)) }
}
awaitClose { reportView.setOnClickListener(null) }
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(contentView)
intents
.onEach(newsViewModel::send)
.launchIn(lifecycleScope)
newsViewModel.newState
.collectIn(this) { showNews(it) }
}
private fun showNews(state: NewsState) {
state.apply {
if (isLoading) showLoading() else dismissLoading()
if (isLoadingMore) showLoadingMore() else dismissLoadingMore()
if (reportToast.isNotEmpty()) Toast.makeText(
this@StateFlowActivity,
state.reportToast,
Toast.LENGTH_SHORT
).show()
if (errorMessage.isNotEmpty()) tv.text = state.errorMessage
if (data.isNotEmpty()) newsAdapter2.dataList = state.data
}
}
}
class NewsViewModel(private val newsRepo: NewsRepo) : ViewModel() {
private val _feedsIntent = MutableSharedFlow<FeedsIntent>()
val newState =
_feedsIntent
.toPartialChangeFlow()
.scan(NewsState.initial) { oldState, partialChange -> partialChange.reduce(oldState) }
.flowOn(Dispatchers.IO)
.stateIn(viewModelScope, SharingStarted.Eagerly,NewsState.initial)
fun send(intent: FeedsIntent) {
viewModelScope.launch { _feedsIntent.emit(intent) }
}
private fun Flow<FeedsIntent>.toPartialChangeFlow(): Flow<FeedsPartialChange> = merge(
filterIsInstance<FeedsIntent.Init>().flatMapConcat { it.toPartialChangeFlow() },
filterIsInstance<FeedsIntent.More>().flatMapConcat { it.toPartialChangeFlow() },
filterIsInstance<FeedsIntent.Report>().flatMapConcat { it.toPartialChangeFlow() },
)
private fun FeedsIntent.More.toPartialChangeFlow() =
newsRepo.remoteNewsFlow("", "10")
.map { if (it.news.isEmpty()) More.Fail("no more news") else More.Success(it.news) }
.onStart { emit(More.Loading) }
.catch { emit(More.Fail("load more failed by xxx")) }
private fun FeedsIntent.Init.toPartialChangeFlow() =
flowOf(
newsRepo.localNewsOneShotFlow,
newsRepo.remoteNewsFlow(this.type.toString(), this.count.toString())
)
.flattenMerge()
.transformWhile {
emit(it.news)
!it.abort
}
.map { news -> if (news.isEmpty()) Init.Fail("no more news") else Init.Success(news) }
.onStart { emit(Init.Loading) }
.catch {
if (it is SSLHandshakeException)
emit(Init.Fail("network error,show old news"))
}
private fun FeedsIntent.Report.toPartialChangeFlow() =
newsRepo.reportNews(id)
.map { if(it >= 0L) Report.Success(it) else Report.Fail}
.catch { emit((Report.Fail)) }
}
data class NewsState(
val data: List<News> = emptyList(),
val isLoading: Boolean = false,
val isLoadingMore: Boolean = false,
val errorMessage: String = "",
val reportToast: String = "",
) {
companion object {
val initial = NewsState(isLoading = true)
}
}
sealed interface FeedsPartialChange {
fun reduce(oldState: NewsState): NewsState
}
sealed class Init : FeedsPartialChange {
override fun reduce(oldState: NewsState): NewsState = when (this) {
Loading -> oldState.copy(isLoading = true)
is Success -> oldState.copy(
data = news,
isLoading = false,
isLoadingMore = false,
errorMessage = ""
)
is Fail -> oldState.copy(
data = emptyList(),
isLoading = false,
isLoadingMore = false,
errorMessage = error
)
}
object Loading : Init()
data class Success(val news: List<News>) : Init()
data class Fail(val error: String) : Init()
}
sealed class More : FeedsPartialChange {
override fun reduce(oldState: NewsState): NewsState = when (this) {
Loading -> oldState.copy(
isLoading = false,
isLoadingMore = true,
errorMessage = ""
)
is Success -> oldState.copy(
data = oldState.data + news,
isLoading = false,
isLoadingMore = false,
errorMessage = ""
)
is Fail -> oldState.copy(
isLoadingMore = false,
isLoading = false,
errorMessage = error
)
}
object Loading : More()
data class Success(val news: List<News>) : More()
data class Fail(val error: String) : More()
}
sealed class Report : FeedsPartialChange {
override fun reduce(oldState: NewsState): NewsState = when (this) {
is Success -> oldState.copy(
data = oldState.data.filterNot { it.id == id },
reportToast = "举报成功"
)
Fail -> oldState.copy(reportToast = "举报失败")
}
class Success(val id: Long) : Report()
object Fail : Report()
}
如何把业务代码越写越复杂? | MVP - MVVM - Clean Architecture
如何把业务代码越写越复杂?(二)| Flow 替换 LiveData 重构数据链路,更加 MVI
Android 架构之 MVI | 响应式编程 + 单向数据流 + 唯一可信数据源
最近因为项目需要,需要将Android手机系统自带的某个系统软件反编译并更改里面某个资源,并重新打包,签名生成新的自定义的apk,下面我来介绍一下我的实现过程。APK修改,分为以下几步:反编译解包,修改,重打包,修改签名等步骤。安卓apk修改准备工作1.系统配置好JavaJDK环境变量2.需要root权限的手机(针对系统自带apk,其他软件免root)3.Auto-Sign签名工具4.apktool工具安卓apk修改开始反编译本文拿Android系统里面的Settings.apk做demo,具体如何将apk获取出来在此就不过多介绍了,直接进入主题:按键win+R输入cmd,打开命令窗口,并将路
我是一名决定学习Ruby和RubyonRails的ASP.NETMVC开发人员。我已经有所了解并在RoR上创建了一个网站。在ASP.NETMVC上开发,我一直使用三层架构:数据层、业务层和UI(或表示)层。尝试在RubyonRails应用程序中使用这种方法,我发现没有关于它的信息(或者也许我只是找不到它?)。也许有人可以建议我如何在RubyonRails上创建或使用三层架构?附言我使用ruby1.9.3和RubyonRails3.2.3。 最佳答案 我建议在制作RoR应用程序时遵循RubyonRails(RoR)风格。Rails
当我将IO::popen与不存在的命令一起使用时,我在屏幕上打印了一条错误消息:irb>IO.popen"fakefake"#=>#irb>(irb):1:commandnotfound:fakefake有什么方法可以捕获此错误,以便我可以在脚本中进行检查? 最佳答案 是:升级到ruby1.9。如果您在1.9中运行它,则会引发Errno::ENOENT,您将能够拯救它。(编辑)这是在1.8中的一种hackish方式:error=IO.pipe$stderr.reopenerror[1]pipe=IO.popen'qwe'#
我尝试用Ruby设计一个基于Web的应用程序。我开发了一个简单的核心应用程序,在没有框架和数据库的情况下在六边形架构中实现DCI范例。核心六边形中有小六边形和网络,数据库,日志等适配器。每个六边形都在没有数据库和框架的情况下自行运行。在这种方法中,我如何提供与数据库模型和实体类的关系作为独立于数据库的关系。我想在将来将框架从Rails更改为Sinatra或数据库。事实上,我如何在这个核心Hexagon中实现完全隔离的rails和mongodb的数据库适配器或框架适配器。有什么想法吗? 最佳答案 ROM呢?(Ruby对象映射器)。还有
“架设一个亿级高并发系统,是多数程序员、架构师的工作目标。许多的技术从业人员甚至有时会降薪去寻找这样的机会。但并不是所有人都有机会主导,甚至参与这样一个系统。今天我们用12306火车票购票这样一个业务场景来做DDD领域建模。”开篇要实现软件设计、软件开发在一个统一的思想、统一的节奏下进行,就应该有一个轻量级的框架对开发过程与代码编写做一定的约束。虽然DDD是一个软件开发的方法,而不是具体的技术或框架,但拥有一个轻量级的框架仍然是必要的,为了开发一个支持DDD的框架,首先需要理解DDD的基本概念和核心的组件。一.什么是领域驱动设计(DDD)首先要知道DDD是一种开发理念,核心是维护一个反应领域概
我在当前项目中使用由Oracle数据库和memcached支持的RubyonRails。有一个非常常用的功能,它依赖于单个数据库View作为数据源,并且该数据源内部有其他数据库View和表。这是一个虚拟数据库View,能够从一个地方访问所有内容,而不是物化数据库View。大多数情况下,如果用户正在使用他们希望更新的功能,那么让数据保持最新很重要。从这个View获取数据时,我将安全表内部连接到View(安全表不是View本身的一部分),其中包含一些我们用来在更细粒度级别上控制数据访问的字段。例如,安全表有user_id,prop_1,prop_2列,其中prop_1,prop_2是数据库
运行有问题或需要源码请点赞关注收藏后评论区留言一、利用ContentResolver读写联系人在实际开发中,普通App很少会开放数据接口给其他应用访问。内容组件能够派上用场的情况往往是App想要访问系统应用的通讯数据,比如查看联系人,短信,通话记录等等,以及对这些通讯数据及逆行增删改查。首先要给AndroidMaifest.xml中添加响应的权限配置 下面是往手机通讯录添加联系人信息的例子效果如下分成三个步骤先查出联系人的基本信息,然后查询联系人号码,再查询联系人邮箱代码 ContactAddActivity类packagecom.example.chapter07;importandroid
1.前言 在10.0的系统rom定制化开发中,在系统中有多个launcher的时候,会在开机进入launcher的时候弹窗launcher列表,让用户选择进入哪个launcher,这样显得特别的不方便所以产品开发中,要求用RoleManager的相关api来设置默认Launcher,但是在设置完默认Launcher以后,在安装一款Launcher的时候,默认Launcher就会失效,在系统设置的默认应用中Launcher选项就为空,点击home键的时候会弹出默认Launcher列表,让选择进入哪个默认Launcher.所以需要从安装Launcher的流程来分析相关的设置。来解决问题设置默认La
Ai-Bot基于流行的Node.js和JavaScript语言的一款新自动化框架,支持Windows和Android自动化。1、Windowsxpath元素定位算法支持支持Windows应用、.NET、WPF、Qt、Java和Electron客户端程序和ie、edgechrome浏览器2、Android支持原生APP和H5界面,元素定位速度是appium十倍,无线远程自动化操作多台安卓设备3、基于opencv图色算法,支持找图和多点找色,1080*2340全分辨率找图50MS以内4、内置免费OCR人工智能技术,无限制获取图片文字和找字功能。5、框架协议开源,除官方node.jsSDK外,用户可
前一段时间由于工作需要把可爱的小雪狐舍弃了,找到了小蜜蜂。但是新版本的小蜜蜂出现了很多和旧版本不一样的位置。1.功能位置迁移,原来在工程build.gradle的buildscript和allprojects移动至setting.gradle并改名为pluginManagement和dependencyResolutionManagement。里面的东西依旧可以按照原来的copy过来。pluginManagement{repositories{gradlePluginPortal()google()mavenCentral()}}dependencyResolutionManagement{r