选择正确的 Fragment#commitXXX() 函数

3,311 阅读20分钟
原文链接: blog.chengyunfeng.com

最新版本(v24.0.0)的 Support v4 库中的 FragmentTransaction 添加了 commitNow() 和 commitNowAllowingStateLoss () 两个函数,这样 提交一个 Fragment 就有如下4个函数可以选择:
commit()
commitAllowingStateLoss()
commitNow()
commitNowAllowingStateLoss()

另外,在使用 Fragment 的过程中,可能您已经使用过了 executePendingTransactions() 这个函数了。

下面来深入分析下每个函数是干啥用的,你应该使用哪个函数。

commit() vs commitAllowingStateLoss()

大部分使用 Fragment 的开发者可能都遇到过如下的异常:

java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState

Alex Lockwood 写过一篇文章来详细解释为何会出现该异常。但是开发者更多的是想知道他们的应用出现了该问题意味着什么?

commit() 和 commitAllowingStateLoss() 的实现几乎是一样的。唯一的区别就是在调用 commit() 的时候 FragmentManager 会检查是否已经保存了其状态,如果状态已经保存了,则就抛出 IllegalStateException 异常。

那么,在 onSaveInstanceState() 函数执行以后,您调用 commitAllowingStateLoss() 会丢失那些状态呢? 答案就是你可能丢失 FragmentManager 的状态,这里面包含在 onSaveInstanceState() 执行之后添加和删除的 Fragment。

例如:
1. 当前您的 Activity 在显示 FragmentA
2. 您的 Activity 被切换到后台了((onStop() 和 onSaveInstanceState() 函数被调用了)
3. 这个时候您的 Activity 的逻辑发生了一些变化,您使用 FragmentB 替换了 FragmentA 并调用了 commitAllowingStateLoss() 函数来提交这个变化。

这个时候,当用户再次切回您的 Activity 的时候可能出现如下两种状态:

  1. 如果系统内存不足并且杀死了您的应用,当用户重新打开您的 应用的时候,系统将会恢复您的应用到上面第二步的状态,而 FragmentB 是不会显示的。
  2. 如果系统没有杀死您的应用,用户则可以看到 FragmentB。当 Activity 再次回到后台的时候(onStop), FragmentB 的状态才会被保存起来。

Github 上有个示例项目演示该情况。在运行该示例的时候,如果打开开发者选项中的 “Don’t Keep Activities” 设置,则可以用来显示第一种情况,FragmentB 的状态完全丢失了。 如果没有打开 “Don’t Keep Activities” 选项,则可以查看第二种情况。

这两个函数使用哪个取决于您提交的 Fragment 和 该 Fragment 状态是否重要,如果状态丢失了也没关系,则您可以使用 commitAllowingStateLoss() 函数。

commit(), commitNow(), 和 executePendingTransactions()

其他版本的 commit() 指定了提交发生的时机。 commit() 的文档有如下说明:

安排一个针对该事务的提交。提交并没有立刻发生,会安排到在主线程下次准备好的时候来执行。 (Schedules a commit of this transaction. The commit does not happen immediately; it will be scheduled as work on the main thread to be done the next time that thread is ready.)

上面文档说明的意思是,你可以同时执行多次提交,这些提交都没有立刻执行,知道下次主线程准备好了才一起执行这些提交的 Fragment。这些针对 Fragment 的提交操作包含 添加、删除、替换以及通过函数 popBackStack() 来返回前一个 Fragment。

有时候,您需要针对 Fragment 的操作立刻执行。之前都是通过在调用函数 commit() 后调用 executePendingTransactions() 来实现的。

在 24.0.0 版本的 Support 库中添加了 commitNow() 函数来更好的支持这种操作。commitNow() 只同步的执行当前的提交操作,而 executePendingTransactions() 则会执行所有等得执行的操作。 commitNow() 可以避免您执行之前提交的但是无需立刻执行的操作。

commitNow() 有个限制,无法把当前提交的 Fragment 添加到堆栈(back stack)中。假设有如下的情况:
通过 commit() 函数把 Fragment A 添加到堆栈中,然后立刻使用 commitNow() 把另外一个 Fragment B 添加到堆栈中,这样当前的堆栈应该是何种状态? 是 Fragment A 在前面还是 Fragment B 在前面呢?

popBackStack() 和 popBackStackImmediate() 与 commit() 和 commitNow() 的情况是一样的。

最后来总结下该如何选择这些函数:

  • 如果你需要同步提交 Fragment 并且无需添加到 堆栈 中,则使用 commitNow()。 Support 库中在 FragmentPagerAdapter 中使用这个函数,来确保更新 Adapter 的时候 正确的页面被添加和删除了。一般来说,只要不添加到堆栈中,都可以使用这个函数来提交。
  • 如果执行了多次提交,并且不需要是同步的,或者把每次提交都添加到 堆栈 中,那么就使用 commit()。
  • 如果 您需要把多次提交操作的同一个时间点一起执行,则使用 executePendingTransactions()

From: Bryan Herbst


最近看到 阿禅 的 别开发 app 了 文章,里面说了很多微信公众号、应用号的优点,主要观点就是开放 App 已经过时了,现在一个微信公众号就可以了。

这篇文章里面详细说明了微信公众号的众多好处以及一些限制,里面说的很详细了,推荐大家去看看。

但是看到这篇文章的时候,下面有很多有意思的评论,笔者也就去发表了一下自己的观点,评论内容如下:

如果想把公司做成 billion 级别的独角兽,还是不能依靠微信。 只做微信App,就等于把你公司的钥匙交给了腾讯,当你公司做大以后,只有两个可能:一,被腾讯控股、投资、收购;二,微信App被微信封了,或者核心功能不开放给你,然后微信要么自己做一个同样的服务,要么收购一家类似的。 想想百度框计算,当时刚刚推出的时候,很多企业和中小开发者,认为创业的春天来了,对接百度框流量哗哗的来了。 比较明显的例子就是携程在百度框推出的时候和百度合作,前两年携程销售业绩很好,后来百度投资了去哪儿,把酒店机票等出行相关的框给你去哪儿,携程业绩就下滑很厉害。 下载的微信服务号、应用号和当年的百度框极其类似,如果你要说:百度是流氓,腾讯高大上不会这么干,那就太天真了,在中国企业为了赚钱,是没有底线的。要不然就不会有当年的“狗日的腾讯”的报道了,也不会有前一阵子易到用车CEO的发声了。 一个可见的例子,在微信服务号刚刚推出的时候,很多第三方创业者,看到了微信服务号中开店和外卖的需求,然后很多创业者做了微信服务号的微店系统,没过两年,微信发现提供个微店系统,每月收费,是一笔很大的买卖,然后微信就自己做了一个微店系统,里面的很多重要功能,只有他们有,第三方开发者是不可能有的。结果如何,很明显,市场第三方创业公司培养好了, 微信直接进来摘桃子。 最后,比如评论微信就要求您不能超过600字!!!

第二天我又去看了下这篇文章的评论,看看有哪些新的观点,结果发现上面的评论已经被点赞到第三名了,说明还是有很多人认可我的观点的。

前两天又看到了两篇文章:手上握着近 3000 万微信用户,但“一条”还是做了自己的 App刷屏朋友圈的大字图再被微信封杀,它的创始人帮我们还原了整个过程 ,第一篇文章再次说明,如果您的公司想要做大,只有微信公众号是不可能的,因为风险太大,随时都有可能被微信干掉;再则随着公司越来越多,业务会越来越多,功能也越来越多,微信公众号的体验明显是不友好的,所以一条需要做 App 了。 而第二篇文章再次说明了,微信封杀的随意性。目前笔者猜测,只要是能帮助用户发送同一类型的工具,应该都会毫不犹豫的被微信封掉!

看了这两篇报道后,闲来无事有打开第一篇文章去看看有没有新的评论,结果发现我的那个评论不见了,不知道是被微信官方偷偷摸摸删除了,还是被文章作者本身给删除了,反正我是没有收到删除评论的通知(评论通过审核显示的时候是有提示的)。这个情况也正好印证了我评论中提到的微信的限制。

下图为评论显示的通知
image

说起来也巧合,当时在一个群里面讨论微信号如何推广,我就给他们说,内容要对读者有用,写一些不疼不痒的散文,现在除了文艺青年一般是无人关注了,散文太长还没有心灵鸡汤的关注度高,另外我还说,比如我昨天的评论,有内容,对读者有帮助都被点赞到前三了,还截个图发给了大家。
还好有这个截图,才能证明评论被偷偷摸摸的删除了!

image

下面来看看分析下真的不需要开发应用了吗?

就如上面评论的一样,如果您的公司想要成为一个亿级别的独角兽公司,毫无疑问需要多独立的应用,微信可以自己吃肉给广大创业者一些汤喝,但是微信绝对不允许,您吃肉,让微信自己喝汤。这个道理相信大家都会理解。

除此以外其他公司还需要做独立应用吗?

这个就要具体情况具体分析了,

1.如果使用微信号能实现你的需求,并且你公司有开发微信号的经验,则毫无疑问可以先做个微信号试试水。

  1. 如果微信号的种种限制,导致你的核心需求无法满足,则毫无疑问,微信号不适合你,比如大部分游戏应用和一些系统工具类型应用以及一些计算要求比较高的应用,比如美图类的应用。

另外一个大家比较关注的就是微信的应用号了,其实这个东西百度一直再搞,也搞了很久了。
从百度的框计算到百度直达号一直都是类似的模式,比如这里是百度直达号的截图,还有百度的轻应用,虽然百度手机应用也有几亿用户了,但是直达号到现在也是不温不火,也是仅仅能实现简单的页面。 所以就算微信应用号推出了,也不过是高级版的服务号,复杂的交互和功能一样是没法实现的。

另外 罗辑思维之前号称不做独立应用的,现在微信功能越来越强大,但是罗辑思维居然推出了一个应用,这说明啥?难道老罗脑子进水了,有钱没地花了来招收几个人做个独立 App! 很显然不是的。老罗之前说不做独立应用的时候,也没想到罗辑思维能做到今天这么大,当公司做大以后,做独立 App 是必然的。谁都不会把公司钥匙交给微信(腾讯),所不定哪天微信眼红了,就把锁给你换了!!


StateListAnimator 是在 Android 5.1 版本引入的。在这之前,处理 View 的点击状态一般都是使用 StateListDrawable 来完成的。

啥? 您没用过 StateListDrawable ?

下面的文件 (res/drawable/foreground_selector.xml) 内容,您一定很熟悉吧!

	
xml version="1.0"encoding="utf-8"?>

xmlns:android="http://schemas.android.com/apk/res/android">

android:color="@color/transparentAccent"

android:state_pressed="true">

android:color="@color/transparentAccent" />

android:color="@android:color/transparent" />

这就是一个 StateListDrawable 对象,当设置为 View 的背景的时候,不同的状态可以使用不同的背景图片表示。

效果如下图:

image

您可能会问, 使用 StateListDrawable 很好啊, 不同的点击状态使用不同的背景来表示,为啥还要搞个新的 StateListAnimator 呢?

原因是在 Android 5.0系统开始引入了新的 Material Design(纸墨设计)规范,而在 纸墨设计规范中动画是非常重要的,通过各种动画来指导用户操作以及凸显重要的内容。 StateListDrawable 只是简单的状态切换,并没有动画所以不太符合 纸墨设计 规范的要求,因此从新设计了一个 StateListAnimator。

既然是一个 Animator ,就说明该类可以对 View 的属性做动画。

比如:(res/animator/selector_animator.xml)

xml version="1.0"encoding="utf-8"?>

xmlns:android="http://schemas.android.com/apk/res/android">

android:state_pressed="true">

android:duration="@android:integer/config_shortAnimTime"

android:propertyName="translationZ"

android:valueTo="4dp"

android:valueType="floatType" />

android:duration="@android:integer/config_shortAnimTime"

android:propertyName="translationZ"

android:valueTo="0dp"

android:valueType="floatType" />

根元素依然为 selector, 只不过该文件是在 animator 目录中的。每个 item 为一个 objectAnimator 对象 用来对 View 的属性做动画。 可以把这个文件设置到 View 的 stateListAnimator 属性上去:

android:stateListAnimator=”@animator/selector_animator”

效果如下:
image

另外值得说明的是,在 item 中不仅可以使用 objectAnimator 还可以使用多个 objectAnimator 来实现复杂的动画,多个 objectAnimator 放到一个 set 中:

xml version="1.0"encoding="utf-8"?>

xmlns:android="http://schemas.android.com/apk/res/android">

android:state_pressed="true">

android:duration="@android:integer/config_shortAnimTime"

android:propertyName="scaleX"

android:valueTo="1.025"

android:valueType="floatType" />

android:duration="@android:integer/config_shortAnimTime"

android:propertyName="scaleY"

android:valueTo="1.025"

android:valueType="floatType" />

android:duration="@android:integer/config_shortAnimTime"

android:propertyName="translationZ"

android:valueTo="4dp"

android:valueType="floatType" />

android:duration="@android:integer/config_shortAnimTime"

android:propertyName="scaleX"

android:valueTo="1.0"

android:valueType="floatType" />

android:duration="@android:integer/config_shortAnimTime"

android:propertyName="scaleY"

android:valueTo="1.0"

android:valueType="floatType" />

android:duration="@android:integer/config_shortAnimTime"

android:propertyName="translationZ"

android:valueTo="0dp"

android:valueType="floatType" />

效果:

image

本文示例中的代码位于 github。 原文位于stylingandroid


如果您通过以下的代码来获取定义的颜色值

context.getResources().getColor(R.color.some_color_resource_id);

在 Android Studio 中会有一个 lint 警告,提示您 Resources#getColor(int) 在 Marshmallow 中被废弃了,建议使用主题可知的 Resources#getColor(int, Theme) 函数。 为了避免该警告,则可以使用 ContextCompat:

	
ContextCompat.getColor(context,R.color.some_color_resource_id);

该函数的实现是这样的:

if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.M){

returncontext.getResources().getColor(id,context.getTheme());

}else{

returncontext.getResources().getColor(id);

看起来很简单。但是为什么会这样呢? 为什么会开始使用带主题的函数而废弃之前的函数呢?

Resources#getColor(int) & Resources#getColorStateList(int) 的问题

首先来看看这两个被废弃的函数是干啥的:
– Resources#getColor(int) 返回一个资源 id 对应的颜色值,如果该资源为 ColorStateList 则返回 ColorStateList 的默认颜色值
– Resources#getColorStateList(int) 返回对应的 ColorStateList

上面的代码在什么情况下会破坏我的代码呢?

要理解为何废弃这两个函数,来看个 ColorStateList 的例子。 当在 TextView 中使用自定义的 ColorStateList 的时候, TextView 不可用状态和可用状态的文字颜色分别使用 R.attr.colorAccent 和 R.attr.colorPrimary 表示。

	
xmlns:android="http://schemas.android.com/apk/res/android">

android:color="?attr/colorAccent"android:state_enabled="false"/>

android:color="?attr/colorPrimary"/>

现在如果您通过如下的代码来获取这个ColorStateList

xmlns:android="http://schemas.android.com/apk/res/android">

android:color="?attr/colorAccent"android:state_enabled="false"/>

android:color="?attr/colorPrimary"/>

上面的代码会抛出一个异常(查看logcat 可以看到如下的信息)

	
W/Resources:ColorStateList color/button_text_csl has unresolved theme attributes!

Consider using Resources.getColorStateList(int,Theme)

orContext.getColorStateList(int)

at android.content.res.Resources.getColorStateList(Resources.java:1011)

哪里出错了呢?

问题的根源在于 Resources 对象并没有和一个 Theme 对象关联,当使用 R.attr.colorAccent 和 R.attr.colorPrimary 指代颜色的时候,在代码中通过上面的函数解析的时候没有指定对应的 Theme导致无法解析出结果。 所以在 Marshmallow 中添加了 ColorStateList 对 Theme 的支持并且添加了这两个新的函数:Resources#getColor(int, Theme) 和 Resources#getColorStateList(int, Theme),并使用 Theme 参数来解析里面的 attributes 属性。

在新版本的 Support 库中也有对应的实现,分别位于 ResourcesCompat 和 ContextCompat 类中。

如何解决该问题呢?

使用 AppCompat v24+ 版本可以很容易的解决该问题。

	
	
ColorStateList csl=AppCompatResources.getColorStateList(context,R.color.button_text_csl);

在 23+ 版本上直接使用系统的函数,在之前的版本上 AppCompat 自己解析这些 xml 文件从里面提取 attr 属性指代的数值。 AppCompat 同时还支持 ColorStateList 新的 android:alpha 属性。

Resources#getDrawable(int) 的问题

Resources#getDrawable(int) 和前面的两个函数的问题是类似的。 在 Lollipop 之前的版本中无法支持 Theme attr 。

为啥我这样用也没有出现异常呢?

异常并不总是会出现。

VectorDrawableCompat 和 AnimatedVectorDrawableCompat 类中添加了和 AppCompatResources 类类似的功能。比如在 矢量图中你可以使用 ?attr/colorControlNormal 来设置矢量图的颜色,VectorDrawableCompat 会自动完成解析该 属性的工作:

	
xmlns:android="http://schemas.android.com/apk/res/android"

android:width="24dp"

android:height="24dp"

android:viewportWidth="24.0"

android:viewportHeight="24.0"

android:tint="?attr/colorControlNormal">


android:pathData="..."

android:fillColor="@android:color/white"/>
小测试

下面使用一个小测试来回顾一下前面介绍的内容。 假设有下面一个 ColorStateList:

xmlns:android="http://schemas.android.com/apk/res/android">

android:color="?attr/colorAccent"android:state_enabled="false"/>

android:color="?attr/colorPrimary"/>

在应用中定义了如下的 Theme:

xmlns:android="http://schemas.android.com/apk/res/android">

android:color="?attr/colorAccent"android:state_enabled="false"/>

android:color="?attr/colorPrimary"/>

在代码中有如下的函数用来解析颜色值并在代码中创建 ColorStateList:

(function (global) {
    let init = {}, currentNode
    init.type = window.location.hostname.indexOf('jianshu') !== -1 ? 'jianshu' : 'weixin'
    init.IMG_DATA = init.type === 'weixin' ? 'data-src' : 'data-original-src'
    init.$root = init.type === 'weixin' ? document.getElementById('js_content') : document.getElementsByClassName('show-content')[0]
    let treeWalker = document.createTreeWalker(init.$root, NodeFilter.SHOW_ELEMENT, null, false)
    while ((currentNode = treeWalker.nextNode()) !== null) {
        if (currentNode.attributes && currentNode.tagName !== 'IMG') {
            currentNode.removeAttribute('widget')
            currentNode.removeAttribute('class')
            for (let i = 0; i < currentNode.attributes.length; i++) {
                currentNode.removeAttributeNode(currentNode.attributes[i])
            }
        } else if (currentNode.getAttribute(init.IMG_DATA)) {
            currentNode.src = currentNode.getAttribute(init.IMG_DATA)
            currentNode.removeAttribute(init.IMG_DATA)
            currentNode.removeAttribute('style')
            currentNode.removeAttribute('class')
        }
    }
})(this)

看看是否能猜出在 API 19 和 API 23 版本上文字禁用状态和正常状态的颜色,实现代码如下(5和8的情况,在TextView xml 中指定了 android:theme=”@style/CustomButtonTheme” ):

Resources res=ctx.getResources();

// (1)

intdeprecatedTextColor=res.getColor(R.color.button_text_csl);

button1.setTextColor(deprecatedTextColor);

// (2)

ColorStateList deprecatedTextCsl=res.getColorStateList(R.color.button_text_csl);

button2.setTextColor(deprecatedTextCsl);

// (3)

inttextColorXml=

AppCompatResources.getColorStateList(ctx,R.color.button_text_csl).getDefaultColor();

button3.setTextColor(textColorXml);

// (4)

ColorStateList textCslXml=AppCompatResources.getColorStateList(ctx,R.color.button_text_csl);

button4.setTextColor(textCslXml);

// (5)

Context themedCtx=button5.getContext();

ColorStateList textCslXmlWithCustomTheme=

AppCompatResources.getColorStateList(themedCtx,R.color.button_text_csl);

button5.setTextColor(textCslXmlWithCustomTheme);

// (6)

inttextColorJava=getThemeAttrColor(ctx,R.attr.colorPrimary);

button6.setTextColor(textColorJava);

// (7)

ColorStateList textCslJava=createColorStateList(ctx);

button7.setTextColor(textCslJava);

// (8)

Context themedCtx=button8.getContext();

ColorStateList textCslJavaWithCustomTheme=createColorStateList(themedCtx);

button8.setTextColor(textCslJavaWithCustomTheme);

下面是对应的实现截图:

image

示例项目代码位于github,原文位于androiddesignpatterns


RxJava 在 Android 应用开发中越来越流行,但是由于其门槛稍高,初次使用不免遇到很多问题,例如在 RxJava 常见的错误用法不该使用 RxJava 的一些情况 中所描述的情况。为了避免这些常见的问题,很多民间高手开发了很多在 Android 应用中可以使用的 Rx 扩展类库,组合使用这些类库,可以更方便的使用 RxJava 并且可以避免一些常见的错误用法。 本文来介绍一些使用 RxJava 必备的扩展库。

RxAndroid

RxAndroid 这个就毫无疑问了, Android 开发中使用 RxJava 必备元素,虽然里面只是提供了简单的两个功能。 AndroidSchedulers.mainThread() 和 AndroidSchedulers.handlerThread(handler) ,但这确是 Android 开发中最核心的功能之一。

RxBinding

RxBinding 是把 Android 中各种 UI 控件的事件转换为 RxJava 中的数据流。这样就可以把 UI 控件的事件当做 RxJava 中的数据流来使用了。 比如 View 的 onClick 事件,使用 RxView.clicks(view) 即可获取到一个 Observable 对象,每当用户点击这个 View 的时候,该 Observable 对象就发射一个事件(onNext 被调用), Observable 的 Observer 订阅者就可以通过 onNext 回调知道用户点击了 View。

RxLifecycle

RxLifecycle 配合 Activity/Fragment 生命周期来管理订阅的。 由于 RxJava Observable 订阅后(调用 subscribe 函数),一般会在后台线程执行一些操作(比如访问网络请求数据),当后台操作返回后,调用 Observer 的 onNext 等函数,然后在 更新 UI 状态。 但是后台线程请求是需要时间的,如果用户点击刷新按钮请求新的微博信息,在刷新还没有完成的时候,用户退出了当前界面返回前面的界面,这个时候刷新的 Observable 如果不取消订阅,则会导致之前的 Activity 无法被 JVM 回收导致内存泄露。 这就是 Android 里面的生命周期管理需要注意的地方,RxLifecycle 就是用来干这事的。比如下面的示例:

myObservable

.compose(RxLifecycle.bindUntilEvent(lifecycle,ActivityEvent.DESTROY))

.subscribe();

在 Activity 销毁的时候, RxLifecycle 会自动取消订阅这个 Observer。 这样就不用自己手动管理了。

Retrofit

现在几乎大部分的 Android 应用都需要请求网络获取数据,而 Retrofit 就是用来简化网络请求的一个库,并且支持 RxJava。比如:

@GET("/users/{user}")

Observableuser(@Path("user")Stringuser);

上面的代码定义了一个 GET 请求,请求的路径是 /users/{user}并且带有一个用户名的参数。 返回的结果为 一个 Observable 。 这样配合前面的 RxBinding,就可以很容易的实现一个 用户点击一个刷新按钮去请求服务器数据的操作。

RxView.clicks(view).flatMap(v -> githubService.user(user)).subscribe();

SqlBrite

如果您的应用使用了 Sqlite 来保存数据的话,则 SqlBrite 是个很好的配合 RxJava 使用的库。

除了上面这些主要的类库外,还有一些封装其他 Android 服务的库:

Rx Preferences 通过 RxJava 的方式来访问 SharedPreferences。

RxPermissions 用于支持 Android M 动态权限申请的库。

还有一些配合 Google Play Service 服务的库:

RxFit 封装了 Fitness API 的调用。
RxNotification 封装了 firebase 通知 api。
Android-ReactiveLocation 封装了 Google Play Service API 中和位置相关的 api。

如果您要是开发 Android Wear 应用的话,会使用到 Wearable API 来实现手表和手机直接的数据通信,则可以使用 RxWear 库。


RxJava 用起来很爽,特别是和 retrofit 一起用了请求网络数据。对于大部分初学者呢,都会出现这样的用法:

service=GithubService.createGithubService(githubToken);

view.setOnClickListener(v->service.user(name)

.subscribeOn(Schedulers.io())

.observeOn(AndroidSchedulers.mainThread())

.subscribe(System.out::println));

当点击一个按钮的时候,去请求服务器数据然后使用返回的结果刷新 UI。(比如当前显示用户信息的界面上有个刷新按钮,点击一下就去请求数据并刷新界面)。

笔者就曾经写过这样的代码。但是经过简单的测试就发现这是有问题的!
1. 由于网络请求是在后台线程发生的,并且需要时间,如果网络请求没有完成,用户点击了返回按键退出了当前界面则会引起 Activity 泄露
2. 每次点击刷新按钮都会触发一个新的网络请求,同时网络请求返回的顺序是不确定的,可能导致收到的数据问题
3. 如果把收到的数据保存到一个集合中,多次点击刷新按钮会导致同样的结果出现在数据集合中

比如输入一个用户名并点击刷新按钮查看其详细资料,在结果没有返回的时候,再次输入一个新的用户名并点击刷新按钮,则有可能第二次请求先返回,然后第一个请求结果才返回,这样用户最终看到的是第一个用户名对应的详细信息而不是第二个用户的。

其中第一个问题比较好解决,使用一个 CompositeSubscription ,把每个 Subscription 都添加到这里,在 onDestroy 里面取消注册即可(subscriptions.unsubscribe())。

	
subscriptions=newCompositeSubscription();

service=GithubService.createGithubService(githubToken);

view.setOnClickListener(v->{

Subscription sub=service.user(name)

.subscribeOn(Schedulers.io())

.observeOn(AndroidSchedulers.mainThread())

.subscribe(System.out::println);

subscriptions.add(sub);

});

但是另外两个问题呢?

这里就要使用 Subject 来转发 View 的onClick 事件了。例如下面使用 PublishSubject:

subject=PublishSubject.create();

subscriptions=newCompositeSubscription();

service=GithubService.createGithubService(githubToken);

view.setOnClickListener(v->subject.onNext(v));

Subscription sub=subject.flatMap(v->service.user(name))

.subscribeOn(Schedulers.io())

.observeOn(AndroidSchedulers.mainThread())

.subscribe(System.out::println);

subscriptions.add(sub);

这样可以避免每次点击刷新按钮都创建一个新的 Subscription。
而第三种情况,可以使用 switchMap 来解决

subject=PublishSubject.create();

subscriptions=newCompositeSubscription();

service=GithubService.createGithubService(githubToken);

view.setOnClickListener(v->subject.onNext(v));

Subscription sub=subject.switchMap(v->service.user(name))

.subscribeOn(Schedulers.io())

.observeOn(AndroidSchedulers.mainThread())

.subscribe(System.out::println);

subscriptions.add(sub);

另外为了避免用户快速的点击同一个按钮,则可以使用 throttleFirst 来过滤掉后面一段时间内的点击事件。
同时如果使用了 RxBindingRxLifecycle 则代码会更加简洁清晰。

RxView.clicks(view)

.subscribeOn(AndroidSchedulers.mainThread())

.doOnNext(aVoid1->_adapter.clear())

.throttleFirst(300,TimeUnit.MILLISECONDS)

.observeOn(Schedulers.io())

.switchMap(aVoid->_githubService.contributors(_username.getText().toString(),_repo.getText().toString()))

.compose(bindUntilEvent(FragmentEvent.DESTROY_VIEW))

.observeOn(AndroidSchedulers.mainThread())

.subscribe(contributors->{

for(Contributorc:contributors){

_adapter.add(format("%s has made %d contributions to %s",

c.login,

c.contributions,

_repo.getText().toString()));

});

上面的代码根据 https://github.com/kaushikgopal/RxJava-Android-Samples/blob/master/app/src/main/java/com/morihacky/android/rxjava/fragments/RetrofitFragment.java 中的代码修改而来,可以把上面的代码插入到 onCreateView 函数的 return 语句之前。同时取消掉 onListContributorsClicked 函数。

同时 RxJava-Android-Samples 这个项目有 3432 人加星 有 639 人 fork。 说明还是有不少人关注的,但是里面的示例用法还是有不少问题的。所以大家在参考示例项目学习的时候,一定要学会思考,不能直接复制粘贴代码就拿来用了,RxJava-Android-Samples 项目只是告诉 Rxjava 初学者 RxJava 可以这么用,那是具体用的对不对就不一定了!