Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

在 SnackBar,Navigation 和其他事件中使用 LiveData(SingleLiveEvent 案例) #3945

Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,26 @@
> * 原文作者:[Jose Alcérreca](https://medium.com/@JoseAlcerreca?source=post_header_lockup)
> * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner)
> * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO1/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case.md](https://github.com/xitu/gold-miner/blob/master/TODO1/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case.md)
> * 译者:
> * 译者:[wzasd](github.com/wzasd)
> * 校对者:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

校对者信息


## LiveData with SnackBar, Navigation and other events (the SingleLiveEvent case)
## 在 SnackBar,Navigation 和其他事件中使用 LiveData(SingleLiveEvent 案例)
视图层(Activity 或者 Fragment)与 ViewModel 层进行通讯的一种便捷的方式就是使用 [`LiveData`](https://developer.android.com/topic/libraries/architecture/livedata) 来进行观察。这个视图层订阅 Livedata 的数据变化并对其变化做出反应。这适用于连续不断显示在屏幕的数据。

A convenient way for a view (activity or fragment) to communicate with a ViewModel is to use `[LiveData](https://developer.android.com/topic/libraries/architecture/livedata)` observables. The view subscribes to changes in LiveData and reacts to them. This works well for data that is displayed in a screen continuously.

![](https://cdn-images-1.medium.com/max/800/1*vbhP6Sw61MAK335gEubwHA.png)

**However, some data should be consumed only once,** like a Snackbar message, a navigation event or a dialog trigger.
**但是,有一些数据只会消费一次,**就像是 Snackbar 消息,导航事件或者对话框。

![](https://cdn-images-1.medium.com/max/800/1*WwhYg9sscdYQgLvC3xks4g.png)

Instead of trying to solve this with libraries or extensions to the Architecture Components, it should be faced as a design problem. **We recommend you treat your events as part of your state**. In this article we show some common mistakes and recommended approaches.
这应该被视为设计问题,而不是试图通过架构组件的库或者扩展来解决这个问题。**我们建议您将您的事件视为您的状态的一部分**。在本文中,我们将展示一些常见的错误方法,以及推荐的方式。

### ❌ Bad: 1. Using LiveData for events
### ❌ 错误:1。 使用 LiveData 来解决事件

This approach holds a Snackbar message or a navigation signal directly inside a LiveData object. Although in principle it seems like a regular LiveData object can be used for this, it presents some problems.
这种方法来直接的在 LiveData 对象的内部持有 Snackbar 消息或者导航信息。尽管原则上看起来像是普通的 LiveData 对象可以用在这里,但是会出现一些问题。

In a master/detail app, here is the master’s ViewModel:
在一个主/从应用程序中,这里是主 ViewModel

```
// Don't use this for events
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

注释也需要翻译

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wzasd 注释需要翻译

Expand All @@ -38,22 +38,22 @@ class ListViewModel : ViewModel {
}
```

In the View (activity or fragment):
在视图层(activity 或者 fragment):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

上面用到(activity 或者 fragment)的地方都做了首字母大写处理,这里也建议改成(Activity 或者 Fragment),保持上下文一致。


```
myViewModel.navigateToDetails.observe(this, Observer {
if (it) startActivity(DetailsActivity...)
})
```

The problem with this approach is that the value in `_navigateToDetails` stays true for a long time and it’s not possible to go back to the first screen. Step by step:
这种方法的问题是 `_navigateToDetails` 中的值会长时间保持为真,并且无法返回到第一个屏幕。一步一步进行分析:

1. The user clicks the button so the Details Activity starts
2. The user presses back, coming back to the master activity
3. The observers become active again, after being inactive while activity was in the back stack
4. The value is still `true` so the Details activity is incorrectly started again
5. 用户点击按钮 Details Activity 启动。
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

注意这里的编号,应该还是1,2,3,4而不是编程顺延的5,6,7,8

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wzasd 编号问题注意看

6. 用户用户按下返回,回到主 Activity。
7. 观察者在活动处于回退状态时从非监听状态再次变成监听状态。
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

活动 => Activity 这里不要翻译比较好

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

回退状态 => 回退栈,注意这里是 back stack 而不是 back state

8. 但是该值仍然为 “真”,因此 Detail Activity 启动出错。
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

启动出错 => 被错误地再次启动。


A solution would be to fire the navigation from the ViewModel and immediately set the flag to false:
解决方法是从 ViewModel 中将导航的标志点击后立刻设为 false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

从 ViewModel 中将导航的标志点击后立刻设为 false => 从 ViewModel 中触发导航事件,之后立即把标志设为 false


```
fun userClicksOnButton() {
Expand All @@ -62,17 +62,17 @@ fun userClicksOnButton() {
}
```

However, one important thing to remember is that LiveData holds values but doesn’t guarantee to emit every value that it receives. For example: a value can be set when no observers are active, so a new one will just replace it. Also, setting values from different threads could lead to race conditions that would only generate one call to the observers.
但是,需要记住的一件事就是 LiveData 储存这个值,但是不保证发出它接受到的每个值。例如:当没有观察者处于监听状态时,可以设置一个值,因此新的值将会替换它。此外,从不同线程设置值的时候可能会导致竞争,只会向观察者发出一次呼叫。
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

需要记住的一件事 => 需要记住的一件很重要的事,important 其强调作用,这里如果不把这个意思翻译出来感觉不太好。

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

竞争 => 资源竞争,race conditions 是线程安全方面的术语,有翻译成“资源竞争”的,也有翻译成“竞争条件”、“竞态条件”的,我感觉看过线程安全的应该都懂 race conditions,不用翻译就好,翻译的话我还是推荐翻译成“资源竞争”,感觉更好理解一点。

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

只会向观察者发出一次呼叫 => 只保证调用观察者一次。这里的 call 是不是应该翻译成调用啊,总之感觉翻译成“呼叫”不太专业。

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

还是发出信号吧,观察者模式,是被观察者发出信号,观察者调用,而不是被观察者调用观察者


But the main problem with this approach is that **it’s hard to understand and plain ugly**. How do we make sure the value is reset after the navigation event has happened?
但是这种方法的主要问题是**难以理解和不简洁**。在导航时间发生后,我们如何确保值被重置呢?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

导航时间 => 导航事件


### **❌ Better: 2. Using LiveData for events, resetting event values in observer**
### **❌ 可能更好一些:2。使用 LiveData 进行事件处理,在观察者中重置事件的初始值**

With this approach you add a way to indicate from the View that you already handled the event and that it should be reset.
通过这种方法,您可以添加一种方法来从视图中支出您已经处理了该事件,并且重置该事件。

#### Usage
#### 用法

With a small change to our observers we might have a solution for this:
对我们的观察者进行一些小改动,我们就有了这样的解决方案:

```
listViewModel.navigateToDetails.observe(this, Observer {
Expand All @@ -83,7 +83,7 @@ listViewModel.navigateToDetails.observe(this, Observer {
})
```

Adding the new method in the ViewModel as follows:
ViewModel 中添加新的方法:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

“在”前面加上“像下面这样”,因为感觉 as follows 的意思没有表达出来


```
class ListViewModel : ViewModel {
Expand All @@ -103,15 +103,15 @@ class ListViewModel : ViewModel {
}
```

#### Issues
#### 问题

The problem with this approach is that there’s some boilerplate (one new method in the ViewModel per event) and it’s error prone; it’s easy to forget the call to the ViewModel from the observer.
这种方法的问题是有一些死板(每个事件在 ViewModel 中有一个新的方法),并且很容易出错,观察者很容易忘记调用这个 ViewModel 的方法。

### **✔️ OK: Use SingleLiveEvent**
### **✔️ 正确解决方法: 使用 SingleLiveEvent**

The [SingleLiveEvent](https://github.com/googlesamples/android-architecture/blob/dev-todo-mvvm-live/todoapp/app/src/main/java/com/example/android/architecture/blueprints/todoapp/SingleLiveEvent.java) class was created for a sample as a solution that worked for that particular scenario. It is a LiveData that will only send an update once.
这个 [SingleLiveEvent](https://github.com/googlesamples/android-architecture/blob/dev-todo-mvvm-live/todoapp/app/src/main/java/com/example/android/architecture/blueprints/todoapp/SingleLiveEvent.java) 类是为了适用于特定场景的解决方法。这是一个只会发送一次更新的 LiveData

#### Usage
#### 用法

```
class ListViewModel : ViewModel {
Expand All @@ -133,17 +133,17 @@ myViewModel.navigateToDetails.observe(this, Observer {
})
```

#### Issues
#### 问题

The problem with SingleLiveEvent is that it’s restricted to one observer. If you inadvertently add more than one, only one will be called and there’s no guarantee of which one.
SingleLiveEvent 的问题在于它仅限于一个观察者。如果您无意中添加了多个,则只会调用一个,并且不能保证哪一个。

![](https://cdn-images-1.medium.com/max/800/1*TLeVFNJwRpXCeS7NaF1EaA.png)

### **✔️ Recommended: Use an Event wrapper**
### **✔️ 推荐: 使用事件包装器**

In this approach you manage explicitly whether the event has been handled or not, reducing mistakes.
在这种方法中,您可以明确地管理事件是否已经被处理,从而减少错误。

#### Usage
#### 用法

```
/**
Expand Down Expand Up @@ -195,19 +195,19 @@ myViewModel.navigateToDetails.observe(this, Observer {
})
```

The advantage of this approach is that the user needs to specify the intention by using `getContentIfNotHandled()` or `peekContent()`. This method models the events as part of the state: they’re now simply a message that has been consumed or not.
这种方法的优点在于用户使用 `getContentIfNotHandled()` 或者 `peekContent()` 来指定意图。这个方法将事件建模为状态的一部分:他们现在只是一个消耗或者不消耗的消息。

![](https://cdn-images-1.medium.com/max/800/1*b0z9Flj04zVW_UGsDPQyOA.png)

With an Event wrapper, you can add multiple observers to a single-use event
使用事件包装器,您可以将多个观察者添加到一次性事件中。

* * *

In summary: **design events as part of your state**. Use your own [Event](https://gist.github.com/JoseAlcerreca/5b661f1800e1e654f07cc54fe87441af) wrapper in LiveData observables and customize it to fit your needs.
总之:**把事件设计成你的状态的一部分**。使用您自己的[事件](https://gist.github.com/JoseAlcerreca/5b661f1800e1e654f07cc54fe87441af)包装器并根据您的需求进行定制。

Bonus! Use this [EventObserver](https://gist.github.com/JoseAlcerreca/e0bba240d9b3cffa258777f12e5c0ae9) to remove some repetitive code if you end up having lots of events.
银弹!若您最终发生大量事件,请使用这个 [EventObserver](https://gist.github.com/JoseAlcerreca/e0bba240d9b3cffa258777f12e5c0ae9) 可以删除很多无用的代码。
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bonus 的意思是额外奖励,我想不到一个更合适的中文译词,不过这里为什么翻译成 “银弹” 呢?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

银弹就是程序猿的额外奖励呀~~


Thanks to [Don Turner](https://medium.com/@donturner?source=post_page), [Nick Butcher](https://medium.com/@crafty?source=post_page), and [Chris Banes](https://medium.com/@chrisbanes?source=post_page).
感谢 [Don Turner](https://medium.com/@donturner?source=post_page)[Nick Butcher](https://medium.com/@crafty?source=post_page),和 [Chris Banes](https://medium.com/@chrisbanes?source=post_page)

> 如果发现译文存在错误或其他需要改进的地方,欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。

Expand Down