安卓& Kotlin Books 用kotlin的反应性编程

4
可观察到&实践中的主题 由Alex Sullivan撰写& Marin Todorov

到目前为止,您了解可观察到的观察和不同类型的主题工作,您已经学习了如何在Intellij项目中创建和实验。

然而,有点具有挑战性,以便在日常开发情况下看到可观察到的实际使用,例如将UI绑定到数据模型,显示新的活动或片段并将其输出出来。

有点不确定如何将这些新获得的技能应用于现实世界。在本书中,您将通过理论章节,如第2章,“可观察到”和第3章“主题”以及实际逐步章节(如此)

在“实践中”章节中,您将在完整的应用程序上工作。 Starter Android Studio项目将包括所有非RX和其他设置代码。您的任务将是使用新获得的无功技巧添加其他功能。

这并不意味着说你不会在路上学习很少的新事物 - Au对比度 !

在本章中,您将使用Rxjava和新的可观察到的Superpowers创建一个应用程序,让用户创建漂亮的照片拼贴 - 反应方式。

入门

为本章打开Starter项目, combinestagram. ,在Android Studio 4.0或更新中。卷起你的舌头需要几点试图说出这个名字,不是吗?这可能不是最具市场性的名称,但它会这样做。

添加依赖项 rxjava. , rxkotlin. rxandroid. 在里面 app / build.gradle. file:

implementation "io.reactivex.rxjava3:rxkotlin:3.0.0"
implementation "io.reactivex.rxjava3:rxandroid:3.0.0"
implementation "io.reactivex.rxjava3:rxjava:3.0.2"

Since RxJava 3.0 uses Java 8 features, you’ll also need to let gradle know that you intend to use those features. Add the following in the 和 roid block of the same file:

compileOptions {
  sourceCompatibility JavaVersion.VERSION_1_8
  targetCompatibility JavaVersion.VERSION_1_8
}

同步Gradle文件,构建和运行应用程序,您将看到您带来的项目的开始:

在此屏幕中,用户可以在应用程序构建它时看到它们的拼贴。他们可以将新照片添加到拼贴,清除拼贴的内容或将其保存到手机上。

随意偷看该实用程序 X.Kt. file where a list of Bitmaps is converted into a collage.

您还将在项目中注意到一些其他课程。有一个 photosbottomdialogfragragment. 为拼贴和一个选择照片 SharedViewModel., which is a ViewModel that that the 主要活动 photosbottomdialogfragragment. will share.

在本章中,您将专注于将您的新技能练习。是时候开始了!

Using a BehaviorSubject in a ViewModel

Start by adding a BehaviorSubject, a CompositeDisposable, and a MutableLiveData to the SharedViewModel. class:

// 1
private val disposables = CompositeDisposable()
// 2
private val imagesSubject: BehaviorSubject<MutableList<Photo>>
    = BehaviorSubject.createDefault(mutableListOf())
// 3
private val selectedPhotos = MutableLiveData<List<Photo>>()
  1. CompositeDisposable for the subscriptions.
  2. imagesSubject will emit MutableList<Photo> values.
  3. Finally, you’ll use the selectedPhotos variable, that is a LiveData. object, to stream a list of photos to the 主要活动 .

笔记 : It may seem a little strange to use LiveData. 和 RxJava in the same project, since they’re both streaming libraries that implement the 观察者 图案。但是,它们实际上都具有与建立更好的应用程序的独特优势和弱点。在第22章“构建​​完整的rxjava应用程序”中,您将看到有关此内容的更多详细信息。

Take a look at the Photo 数据 class. It contains a Drawable resource ID. You’ll use that later on to actually build the collage.

接下来,将以下代码添加到 SharedViewModel. class:

init {
  imagesSubject.subscribe { photos ->
    selectedPhotos.value = photos
  }.addTo(disposables)
}

fun getSelectedPhotos(): LiveData<List<Photo>> {
  return selectedPhotos
}

You’re subscribing to the imagesSubject stream and updating the selectedPhotos value with the items emitted by the subject. Since you’re a responsible RxJava user, you’re adding the Disposable returned by the subscribe() method to the CompositeDisposable you created earlier.

Speaking of being responsible RxJava users, add the following code below the init block:

override fun onCleared() {
  disposables.dispose()
  super.onCleared()
}

ViewModels onCleared() method is a great place to dispose of any disposables you may have lying around. Since a ViewModel 是 only cleared when the Activity that created it finishes, you won’t prematurely finish your Observables and Subjects, and you won’t leak any memory.

添加照片

现在是时候开始向拼贴添加一些照片了。将以下代码添加到 SharedViewModel.:

fun addPhoto(photo: Photo) {
  imagesSubject.value?.add(photo)
  imagesSubject.onNext(imagesSubject.value!!)
}

addPhoto() takes a Photo object and adds it to the current list of photos in the collage.

Since you’re using a BehaviorSubject, you can easily extract the current list of photos from it and add this new photo to that list. You then emit that list again to notify any observers of the newly updated list of photos.

导航 主要活动 和 replace the println() call in actionAdd() with the following:

viewModel.addPhoto(PhotoStore.photos[0])

目前,您始终使用应用程序附带的静态照片中的第一张照片。别担心,你会稍后更新。

It’s time to hook everything up and see a collage! Add the following to the bottom of the onCreate() method of 主要活动 , importing 和 roidx.lifecycle.Observer when prompted:

// 1
viewModel.getSelectedPhotos().observe(this, Observer { photos ->
  photos?.let {
    // 2
    if (photos.isNotEmpty()) {
      val bitmaps = photos.map {
        BitmapFactory.decodeResource(resources, it.drawable)
      }
      // 3
      val newBitmap = combineImages(bitmaps)
      // 4
      collageImage.setImageDrawable(
        BitmapDrawable(resources, newBitmap))
    }
  }
})

代码可能看起来很复杂,但它实际上非常简单:

  1. You’re observing the selectedPhotos live data, which emits lists of Photo objects.
  2. 这 n, if there are any photos, you’re mapping each Photo object to a Bitmap using the BitmapFactory.decodeResource() method.
  3. Next up, you’re combining that list of bitmaps using the combineImages() method.
  4. Finally, you’re setting the collageImage image view with the combined bitmap.

运行应用程序。当你点击 添加 按钮您应该在中央拼贴图像视图中看到图像。选项按钮再次添加更多图像。

看起来不错!现在尝试点击 清除 button.

你没有挂钩 清除 行动尚未发生任何东西。如果发生了一些事情,那么这是神奇的,你已经发现了一种在没有写任何代码的情况下建立应用程序的新方式!

添加以下功能 SharedViewModel. class:

fun clearPhotos() {
  imagesSubject.value?.clear()
  imagesSubject.onNext(imagesSubject.value!!)
}

clearPhotos() works very similarly to addPhotos(), except instead of adding a new photo into the existing list you’re clearing out that list and emitting the now empty list.

现在,导航回来 the 主要活动 class and replace the println() statement in the actionClear() method with the following:

viewModel.clearPhotos()

Last but not least, add this else statement to the if statement in the selected photos observing code in the onCreate():

if (photos.isNotEmpty()) {
  // ...
} else {
  collageImage.setImageResource(android.R.color.transparent)
}

Now if the photos list has no photo objects in it, you’re clearing out the image in the collageImage image view.

再次运行应用程序。这次你应该能够清楚照片。

重新介绍无功规划

有时可以努力追随反应性编程,所以这是到目前为止应用程序发生的事情的回顾:

  1. 每当用户点击 添加 button, the 主要活动 class is calling the addPhoto() method in SharedViewModel. with a single static photo.
  2. SharedViewModel. class then updates a list of photos that is stored in imagesSubject, and it calls onNext() with the updated list of photos.
  3. Since the view model is subscribed to imagesSubject, it receives the onNext() notification and forwards the new list of photos through to the selectedPhotos live data.
  4. Since the 主要活动 class is subscribing to the selectedPhotos live data, it’s notified of the new list of photos. It then creates the combined bitmap of photos and sets it on the collageImage image view. If the list of photos is empty, it instead clears that image view.

在这个应用程序的这个阶段,这似乎是矫枉过正。但是,随着你继续改善 combinestagram. 应用程序,您将看到这种基于反应的流的方法有很多优点!

驾驶一个复杂的UI

当您使用当前应用程序时,您会发现UI可能有点聪明地改善用户体验。例如:

  • 你可以禁用 清除 如果在用户刚刚清除选择的情况下没有选择任何照片,则按钮。
  • 同样,没有必要 如果没有选择任何照片,则会启用按钮。
  • 您还可以禁用奇数照片的保存功能,因为它会在拼贴中留下一个空点。
  • 将单个拼贴画的照片数量限制为六个,因为更多照片看起来有点奇怪。
  • 最后,如果活动标题反映了当前选择,这将是很好的。

让我们现在举行,以增加这些改进 combinestagram. .

打开 mainactivity.kt. 和 add an updateUi() method below onCreate():

private fun updateUi(photos: List<Photo>) {
  saveButton.isEnabled =
      photos.isNotEmpty() && (photos.size % 2 == 0)
  clearButton.isEnabled = photos.isNotEmpty()
  addButton.isEnabled = photos.size < 6
  title= if (photos.isNotEmpty()) {
    resources.getQuantityString(R.plurals.photos_format,
      photos.size, photos.size)
  } else {
    getString(R.string.collage)
  }
}

In the above code, you update the complete UI according to the ruleset we’ve talked about. All the logic is in a single place and easy to read through. Now add a call to updateUi() to the bottom of the 观察者 lambda observing for selectedPhotos:

if (photos.isNotEmpty()) {
  // ...
} else {
  // ...
}
updateUi(photos)

再次运行应用程序,您将在您与UI播放时查看所有规则:

到目前为止,您可能开始在应用于您的Android应用程序时看到Rx的真正优势。如果您查看本章中写的所有代码,您将看到只有几条简单的线路驱动整个UI!

通过主题与其他观点沟通

combinestagram. 几乎 完美的。但用户可能希望实际从多个照片中选择,而不是一个硬编码的照片。可能是。

而不是向上服务一个静态图像,而是显示底部对话框片段,其中用户可以从照片列表中进行选择。

First off, delete the addPhoto() method in SharedViewModel..

Next up, replace the contents of actionAdd() in 主要活动 with the following:

val addPhotoBottomDialogFragment =
  photosbottomdialogfragragment..newInstance()
addPhotoBottomDialogFragment
  .show(supportFragmentManager, "photosbottomdialogfragragment.")

上面的代码简单地显示了 photosbottomdialogfragragment. 用户点击时对话框 添加 按钮。运行应用程序现在尝试。敲击后,您应该看到以下内容 添加 button:

This is the stage in which you’d normally use an interface to have the photosbottomdialogfragragment. communicate that the user selected a photo. However, that’s not very reactive — so, instead, you’ll use an observable.

导航 photosbottomdialogfragragment. 和 create a new PublishSubject<Photo> variable:

private val selectedPhotosSubject =
    PublishSubject.create<Photo>()

val selectedPhotos: Observable<Photo>
  get() = selectedPhotosSubject.hide()

You’ll see this pattern employed often. You created a new PublishSubject, but you don’t want to expose that subject to other classes because you want to make sure that you know what’s being put into it. Instead of directly exposing selectedPhotosSubject, you create a new public selectedPhotos property that returns selectedPhotosSubject.hide(). The hide() method simply returns an Observable version of the same subject.

添加 the following to the empty photosClicked() method:

selectedPhotosSubject.onNext(photo)

您现在正在转发通过您的主题选择的用户的照片。

所有这些都是要做的就是订阅这个新的可观察到。

导航到 SharedViewModel. 并添加以下方法:

fun subscribeSelectedPhotos(selectedPhotos: Observable<Photo>) {
  selectedPhotos
      .doOnComplete {
        Log.v("SharedViewModel.", "Completed selecting photos")
      }
      .subscribe { photo ->
        imagesSubject.value?.add(photo)
        imagesSubject.onNext(imagesSubject.value!!)
      }
      .addTo(disposables)
}

subscribeSelectedPhotos() takes an Observable<Photo> 和 subscribes to that observable, forwarding the photos it receives through to the imagesSubject.

现在,导航回来 主要活动 和 add the following line in the bottom of the actionAdd() method:

viewModel.subscribeSelectedPhotos(
  addPhotoBottomDialogFragment.selectedPhotos)

你准备好了!

运行应用程序,您应该能够将不同的照片添加到拼贴上:

清理观察到:审查

代码看似正常工作,但尝试以下内容:将几张照片添加到拼贴,返回主屏幕并检查Logcat。

Do you see a message saying “completed selecting photos”? No? You added a Log statement to that last subscription using the doOnComplete() operator that should notify you that the provided selectedPhotos has completed.

Since the selectedPhotos observable never completes, the memory it’s utilizing will not be freed until the SharedViewModel. 是 itself cleared.

If the user keeps going back and forth adding new photos and presenting that bottom dialog fragment, that means more and more observables will be created, since one’s created for every instance of photosbottomdialogfragragment.. Those observables are taking up precious memory!

打开 photosbottomdialogfragragment. 并添加以下方法:

override fun onDestroyView() {
  selectedPhotosSubject.onComplete()
  super.onDestroyView()
}

Now, whenever the view is destroyed, the selectedPhotosSubject will be completed and its memory will be reclaimed. You can see that this is true if you run the app, select a photo, then dismiss the bottom sheet. The log statement now prints out.

完美的!您现在已经准备好了本章的最后一部分:拍摄普通的旧无聊功能,并将其转换为超级令人敬畏和幻想的反应性。

创建自定义可观察到

您可能已经注意到应用程序的一个方面,它不起作用又节省照片。是时候修复了!

打开 SharedViewModel. 和 take a look at the saveBitmapFromImageView() method. It’s pretty simple — it just takes an ImageView 和 saves its bitmap to the external files directory.

只有一个问题:它很无聊。实际上,真正的问题是,在保存照片后,没有办法弄清楚它被保存到的地方 - 它是一个阻止的呼叫!这两个问题都可以通过使这个功能成为一个令人敬畏的反应函数来修复。

First, change the return type of saveBitmapFromImageView() to Observable<String>. Then, wrap the existing function body in an Observable.create call like so:

fun saveBitmapFromImageView(
    imageView: ImageView,
    context: Context
): Observable<String> {
  return Observable.create { emitter ->
    // Body of the method
    // ...
  }
}

你现在又回来了一个可观察。然而,可观察到的,从未发出任何东西,从未完成过。并非所有有用的。

添加 this to the end of the try block:

emitter.onNext(tmpImg)
emitter.onComplete()

这 n, add this to the end of the catch block:

emitter.onError(e)

您正在发出新创建的文件的名称然后完成。如果该文件无法保存,则您刷您将发出该错误。

导航回来 主要活动 和 update the actionSave() method to the following:

viewModel.saveBitmapFromImageView(collageImage, this)
    .subscribeBy(
        onNext = { file ->
          Toast.makeText(this, "$file saved",
            Toast.LENGTH_SHORT).show()
        },
        onError = { e ->
          Toast.makeText(this,
            "Error saving file :${e.localizedMessage}",
            Toast.LENGTH_SHORT).show()
        }
    )

构建并运行应用程序以测试 functionality.

所以你创建了一个可观察到的,可以发出一个项目并完成或发出错误。这听起来很熟悉......

点评:单身,也许,完全

在第2章“可观察到”中,您有机会了解一些专业的RXJava类型。

在本章中,您将进行快速查看,并查看您如何在应用程序中使用这些类型,然后使用其中一个类型 combinestagram. project! Starting with 单身的 :

单身的

As you already know, 单身的 是 an Observable specialization. It represents a sequence, which can emit just once either a success event or an error.

This kind of type is useful in situations such as saving a file, downloading a file, loading data from disk or basically any asynchronous operation that yields a value. You can categorize two distinct use-cases of 单身的 :

  1. For wrapping operations that emit exactly one element upon success, just like saveBitmapFromImageView() earlier in this chapter. You can directly create a 单身的 instead of an Observable. In fact, you will update the saveBitmapFromImageView() method in SharedViewModel. to create a 单身的 shortly.

  2. To better express your intention to consume a single element from a sequence and ensure if the sequence emits more than one element the subscription will error out. To achieve this, you can subscribe to any observable and use singleOrError() operator to convert it to a 单身的 .

可能是

可能是 是 quite similar to 单身的 with the only difference that the observable 可能 或者 不得 成功完成后发出价值。

If you keep to the photograph-related examples, imagine this use-case for 可能是 : your app is storing photos in its own custom photo album. You persist the album identifier in SharedPreferences 和 use that ID each time to “open” the album and write a photo inside.

You would design an open(albumId): Maybe<String> method to handle the following situations:

  • In case the album with the given ID still exists, just emit a 完全的 event.
  • In case the user has deleted the album in the meanwhile, create a new album and emit a next event with the new ID, so you can persist it in SharedPreferences.
  • In case something is wrong and you can’t access the Photos library at all, emit an error event.

Just like other the specialized types, you can achieve the same functionality by using a “vanilla” Observable, but 可能是 gives more context both to you as you’re writing your code and to the programmers coming to alter the code later on.

Just as with 单身的 , you can create a 可能是 directly by using 可能是 .create { ... }. Or, if you have an existing observable, you can use the firstElement() 或者 lastElement() methods to get a 可能是 of that element.

合适的

这 final type to cover is 合适的 . This variation of Observable allows only for a single 完全的 或者 error event to be emitted before the subscription is disposed of.

You can create a 合适的 sequence by using 合适的 .create { ... } with code very similar to that which you’d use to create other observables. You can also use the ignoreElements() method on an Observable to get a completable version of it that ignores all the elements.

You might notice that 合适的 simply doesn’t allow for emitting any values and wonder why would you need a sequence like that. You’d be surprised at the number of use-cases in which you only need to know whether an asynchronous operation succeeded or not.

Here’s an example before going back to combinestagram. . Let’s say your app auto-saves a document while the user is working on it. You’d like to asynchronously save the document in a background queue, and when completed, show a small notification or an alert box onscreen if the operation fails.

Let’s say you wrapped the saving logic into a function fun saveDocument(): Completable. This is how easy it is to then express the rest of the logic:

saveDocument()
  .andThen(Observable.just(createMessage))
  .subscribeBy(onNext = { message ->
    message.display()
  }, onError = { e ->
    showError(e.localizedDescription())
  })

和 Then() operator allows you to chain more completables or observables upon an event and subscribe for the final result. In case, any of them emits an error, your code will fall through to the final onError lambda.

拥有 完全的 ( :] ) a review of the specialized observable types, you can now update saveBitmapFromImageView() to return a more specialized and appropriate type.

回到 combinestagram. 和手头的问题!

在应用程序中使用单个

Update saveBitmapFromImageView() to return a 单身的 <Photo> 和 replace the relevant calls to Observable with the sibling calls to 单身的 :

fun saveBitmapFromImageView(imageView: ImageView, context: Context): Single<String> {
  return Single.create { emitter ->
    // ...
    try {
      // ..
      emitter.onSuccess(tmpImg)
    } catch(e: IOException) {
      Log.e(" 主要活动 ", " pro blem saving collage", e)
      emitter.onError(e)
    }
  }
}

想要!

所有剩余的都是将订阅更改为此单曲并保存照片。

导航回来 主要活动 和 update the actionSave() method to the following:

viewModel.saveBitmapFromImageView(collageImage, this)
    .subscribeBy(
        onSuccess = { file ->
          // ...
        },
        onError = { e ->
          // ...
        }
    )

You’re utilizing the new reactive version of the saveBitmapFromImageView() method and subscribing to the 单身的 that it produces. If the single succeeds, you’re showing a toast indicating that it finished. If it fails, you’re showing a toast with the error message.

给应用程序一个最后一个胜利的运行才能保存拼贴。

要查看已保存的文件,请使用 设备文件资源管理器 从中获得的 查看▸工具窗口 在Android Studio中的菜单:

然后导航到应用程序 数据 设备中的目录 SD卡 要查看文件的文件夹。您可以双击图像文件以打开它。

在我们继续前进之前,还有一个问题来解决。

你可能已经注意到了,当你保存拼贴时, 禁止状态下的按钮冻结,UI停止响应。将照片保存到存储可能需要很长时间,并且最好在后台线程完成。

为实现这一目标,您会看到RX的一个凉爽部件之一的潜行偷看: 调度员 .

In the actionSave() method, add the following code after the call to saveBitmapFromImageView() 和 before the call to subscribeBy():

.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())

subscribeOn() method instructs the 单身的 to do its subscription work on the IO scheduler. The observeOn() method instructs the single to run the subscribeBy() code on the Android main thread.

您将在第13章“介绍调度器”中的计划员中了解更多信息。现在,运行应用程序。您应该看到“保存”按钮立即返回到“自行数据”之后返回其正常状态,并且UI不应再阻止UI。

有了这个书,你已经完成了这本书的第I节 - 祝贺!

你不是一个年轻的帕瓦兰,而是一个经验丰富的rxjava jedi。但是,不要觉得刚刚拍摄黑暗的一面。您将获得战斗网络,线程切换和交易很快!

Before that, you must continue your training and learn about one of the most powerful aspects of RxJava. In Section 2, “Operators and Best Practices,” operators will allow you to take your Observable superpowers to a whole new level!

关键点

  • 可观察到 主题 不仅仅是为了理论:你在真实的应用中使用它们!
  • rxjava可观察到可以与之相结合 LiveData. 将事件从视图模型传递给UI。
  • rxjava可用于创建 复杂UI交互 少量 宣言 code.
  • 重构现有的非RX代码是可能的,并且有用 自定义可观察到 using Observable.create.
  • 这 specialized observable types 单身的 , 可能是 , and 合适的 should be used when possible to make your intentions clear to both future you and your teammates.

然后去哪儿?

接下来,回到一些理论,因为你开始第II部分。

在您身后的第一个启用RX的应用程序项目中,它是时候深入了解Rxjava并查看如何使用可观察流的方式使用 运营商 .

有一个技术问题?想报告一个错误吗? 您可以向官方书籍论坛中的书籍作者提出问题和报告错误 这里 .

有反馈分享在线阅读体验吗? 如果您有关于UI,UX,突出显示或我们在线阅读器的其他功能的反馈,您可以将其发送到设计团队,其中表格如下所示:

© 2021 Razeware LLC