【问题标题】:Flowable inside of ViewHolder可在 ViewHolder 内部流动
【发布时间】:2019-02-26 23:36:53
【问题描述】:

我又开始用 android 编程了,因为在过去的 4 年里它发生了很大的变化,我有点困惑。我正在尝试使用 ViewHolder 中的 RxJava 和 Flowable 从服务器异步加载我的数据。我正在使用第三方库来处理适配器MultiViewAdaptor 中的视图。这是我的 Binder

中的代码
class FooBinder : ItemBinder<Foo, FooBinder.ViewHolder>() {

        @Inject
        protected lateinit var requestFavoriteUseCase: SendFooFavoriteUseCase

        @Inject
        protected lateinit var compositeDisposable: CompositeDisposable

        @Inject
        protected lateinit var retrieveFooFavoriteCountUseCase: RetrieveFooFavoriteCountUseCase

        override fun create(inflater: LayoutInflater, parent: ViewGroup) =
            ViewHolder(inflater.inflate(R.layout.item_foo_content, parent, false))

        override fun bind(holder: ViewHolder, item: Foo) {
            holder.itemView.foo_content_creator.text = with(item.creator) { "$firstName $lastName" }

            GlideApp.with(holder.itemView)
                .load(item.creator.avatar)
                .dontAnimate()
                .placeholder(R.drawable.ic_action_person)
                .error(R.drawable.ic_action_person)
                .into(holder.itemView.foo_content_profile)

            retrieveFooFavoriteCountUseCase
                    .execute(item.id)
                    .applyComputationScheduler()
                    .subscribe { count ->
                        holder.itemView.foo_content_like_count.text = "$count"
                    }
                    .addTo(compositeDisposable)
        }

        inner class ViewHolder(view: View) : ViewHolder<Foo>(view) {

            private var favorite = false

            init {

                view
                    .foo_content_button_layout
                    .clicks()
                    .flatMap {
                        favorite = !favorite
                        requestFavoriteUseCase.execute(FavoriteVM(item.id, favorite)).toObservable()
                    }
                    .subscribe {
                        view.foo_content_like_count.text = it.toString()

                        var res = R.drawable.ic_action_like_default
                        if (favorite) {
                            res = R.drawable.ic_action_like_enabled
                        }
                        view.foo_content_like_icon.setImageResource(res)
                    }
                    .addTo(compositeDisposable)
            }

        }

    }

如您所见,我必须在 bind 方法中调用服务器,这并不理想,并且每次用户滚动时都会调用它,我也无法取消订阅它们,直到 Activity 被销毁并且 compositeDisposable 被调用,这意味着我将为一个视图提供多个一次性 :(

我的问题是,如何在 ViewHolder 中使用 observable 并在屏幕上未显示时停止喂它?

【问题讨论】:

    标签: android kotlin android-recyclerview rx-java rx-java2


    【解决方案1】:

    你能做的最好的事情是:

    1. 依靠onViewAttachedToWindowonViewDetachedToWindow 来控制您的订阅生命周期(订阅/处置)。
    2. onBindViewHolder 上创建/检索您的 observables
    3. 在父视图(片段/活动)中取消您在回收器上的适配器,以强制适配器为所有窗口调用onViewDetachedToWindow,以确保您没有泄漏任何内容(例如片段,在onDestoryView 中调用@987654330 @)
    4. delaySubscription(例如 400 毫秒)添加到您的 observables 以减少快速滚动时的滞后(您不想在列表滑动动画时订阅任何内容)
    5. 确保你的 observables 的第一次发射是同步的,这样缓存的数据可以在屏幕旋转后布局之前传递。这样您就可以避免在旋转过程中出现动画故障,并且 Android 将能够正确恢复 View 状态。

    第 5 点可能有点令人困惑,因此有一些资源可以更广泛地解释这一点:

    所以基本上同步发射意味着在订阅方法返回之前,项目将在订阅 observable 时发射。例如,这种行为可以通过在缓存后不修改订阅发射线程的链中的某种缓存(如 replay(1)、startWith() 等)来实现。

    例子:

    Observable.create(2)
        .subscribeOn(Schedulers.computation())
        .observeOn(AndroidSchedulers.mainThread())
        .startWith(0)
        .subscribe(println(it))
    
    println("after subscribe")
    

    假设我们订阅了一个主线程。在这种情况下,我们将打印:

        0
        after subscribe
        2
    

    如您所见,即使这两个项目都将在主线程上发出,其中一个项目将在订阅时立即到达,而第二个项目将发布在 looper 上。

    【讨论】:

    • 谢谢@Than,您描述的方式非常有帮助并给了我想法,但是如果您有关于第 3 和第 5 项的信息,请您解释更多,甚至给我举个例子吗?
    • @navid_gh 至于第 5 点,这很棘手,我已经为您提供了一些关于这个东西的文章,因为它需要更多关于 Rx 和 Android 中的线程的知识。但是,您可以尝试在没有它的情况下实现它,我提出的解决方案是针对每个列表项发出多个值的流,并且旋转故障不太明显。因此,您可能不需要项目中的所有内容
    【解决方案2】:

    bind 方法(最初是 onBindViewHolder)在每次视图尝试向用户显示时调用。 (确实,当用户滚动到底部和向上时,此方法会再次调用)

    因此,就生命周期问题或性能而言,在 onBindViewHolder 上调用网络进程是个坏主意

    我建议用这种方法来实现这一点。

    1. 在初始化适配器之前获取所有关联item.id 的数据。
    2. 将数据与 Foo 和检索到的数据进行匹配
    3. 使用第 2 步的结果初始化适配器。
    4. onBindViewHolderholder.itemView.foo_content_like_count.text = item.count 中订阅retrieveFooFavoriteCountUseCase。

    示例代码,它可能无法正常工作。我只是给出它是如何工作的。)

      val items = ...
      retrieveFooFavoriteCountUseCase
          .execute(items.map { it.id }) // 1)
          .applyComputationScheduler()
          .subscribe { counts -> 
              for ((index, count) in counts.indexed()) { // 2)
                  items[index] = items[index].apply { this.count = count } // 3)
              }
    
              // TODO: initialize adapter with items
          }.addTo(compositeDisposable)
    

    1) 你应该用 List 实现执行方法吗?我不知道 Foo 的具体类型。

    2) counts 包含您需要的计数列表。在这个示例代码中,我使用了 for-each 来匹配数据。但您可以使用自己的匹配代码。

    3) 同上,可以使用自己的匹配码。

    因此整个过程中subscribe只会被调用一次。

    【讨论】:

    • 感谢您花时间回复。因此,如果我理解正确,我需要操作发送到绑定方法的项目(Foo),而不是在 Binder 或 ViewHolder 内部进行网络调用?正确的?如果是这样,它对我不起作用,因为我希望项目也可以流动,我的数据正在由服务器定期更改,并且由于它正在流式传输,它应该反映更改,在初始化适配器之前获取所有数据不起作用,我也没有将列表发送给适配器我将 Foo(我的班级)一一发送并将其添加到适配器项目
    • 如果能观察到'从服务器改变数据的时序',可以这样解决。 1. 初始化适配器。 2.观察Activity中的“数据变化时机”。当数据到来时,从服务器获取更改数据。 3.更新项目并通过notifyItemChanged(position)通知适配器。它可以反映来自服务器的更改。我觉得这种方法比在 RecyclerView 的适配器中管理 Disposable 对象要好。
    猜你喜欢
    • 1970-01-01
    • 2019-01-07
    • 1970-01-01
    • 2018-08-05
    • 2018-02-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多