新 Android 推荐架构中的 BoundService + LiveData + ViewModel 最佳实践
BoundService + LiveData + ViewModel best practice in new Android recommended architecture
我一直在思考在新的 Android recommended Architecture 中放置 Android 服务的位置。我想出了很多可能的解决方案,但我无法确定哪一个是最好的方法。
我做了很多研究,但找不到任何有用的指南或教程。我发现关于在我的应用程序架构中放置服务的唯一提示是这个,来自@JoseAlcerreca Medium post
Ideally, ViewModels shouldn’t know anything about Android. This improves testability, leak safety and modularity. A general rule of thumb is to make sure there are no android.* imports in your ViewModels (with exceptions like android.arch.*). The same applies to presenters.
据此,我应该将我的 Android 服务放在我的架构组件层次结构的顶部,与我的活动和片段处于同一级别。那是因为 Android 服务是 Android 框架的一部分,所以 ViewModels 不应该知道它们。
现在,我将简要说明我的场景,但只是为了让全景更清晰,而不是因为我想要针对这个特定场景的答案。
- 我有一个 Android 应用程序,它有一个包含许多片段的 MainActivity,所有片段都捆绑在一个 BottomNavBar 中。
- 我有一个 BluetoothService 绑定到 myActivity 及其片段之一(因为我希望该服务具有与 Activty 相同的生命周期,但我也想直接从我的片段与其交互)。
- 片段与BluetoothService交互获取两类信息:
- 关于蓝牙连接状态的信息。不需要持久化。
- 来自蓝牙设备的数据(在这种情况下,它是一个秤,因此重量和 body 组成)。需要坚持。
以下是我能想到的 3 种不同的架构:
LiveData inside AndroidService
更新:这是我当时个人采用的方法,因为它运作良好并且让我能够相对快速地完成它。但是,我建议遵循 Jeel Vankhede 的更新答案,以获得似乎更“惯用”的实现。
- 具有连接状态和权重的 LiveData
来自蓝牙设备的测量值在 BluetoothService 中。
- Fragment可以触发BluetoothService中的操作(例如scanDevices)
- Fragment观察关于连接状态的LiveData
并相应地调整 UI(例如,如果
状态已连接)。
- Fragment 观察新体重测量的 LiveData。如果新的重量测量值来自 BluetoothDevice,则 Fragment 会告诉自己的 ViewModel 保存新数据。它是通过存储库 class.
完成的
片段和 AndroidService 之间的共享 ViewModel
- Fragment可以触发BluetoothService中的操作(例如scanDevices)
- BluetoothService更新共享ViewModel中的蓝牙相关LiveData。
- Fragment 在自己的 ViewModel 中观察 LiveData。
服务视图模型
- Fragment可以触发BluetoothService中的操作(例如scanDevices)
- BluetoothService在自己的ViewModel中更新蓝牙相关的LiveData。
- Fragment 在自己的 ViewModel 和 BluetoothService ViewModel 中观察 LiveData。
我很确定我应该将它们放在架构的顶部并像 Activity/Fragment 一样对待它们,因为 BoundServices 是 Android 框架的一部分,它们由 Android OS 并且它们绑定到其他 Activity 和 Fragments。在那种情况下,我不知道与 LiveData、ViewModel 和 Activities/Fragments.
交互的最佳方式是什么
有些人可能认为它们应该被视为数据源(因为在我的例子中它是使用蓝牙从秤中获取数据),但我认为这不是一个好主意,因为我已经知道了在上一段中特别提到 because of what it says here:
Avoid designating your app's entry points—such as activities,
services, and broadcast receivers—as sources of data. Instead, they should only coordinate with other components to retrieve the
subset of data that is relevant to that entry point. Each app
component is rather short-lived, depending on the user's interaction
with their device and the overall current health of the system.
所以,最后,我的问题是:
我们应该将 Android(绑定)服务放在哪里,它们与其他架构组件的关系是什么?这些替代方案中的任何一个都是好的方法吗?
这样对待你们的服务怎么样?
已更新:
在得到@Ibrahim Disouki 的建议后(谢谢你)我深入挖掘并发现了一些有趣的东西!这是背景。
O.P.寻求解决方案“Android框架的服务组件站在考虑Android架构组件”。所以,这是开箱即用的(SDK)解决方案。
与Activity/Fragment
处于同一水平。如何?如果您要扩展服务 class 而不是那样,请开始扩展 LifecycleService。这背后的原因很简单,以前我们不得不依赖 Activity/Fragment 生命周期来接收 updates/do 服务上的一些上下文操作。但现在不是这样了。
LifecycleService
现在有自己的生命周期 registry/maintainer,称为 ServiceLifecycleDispatcher
,它负责服务的生命周期,这也使它成为 LifecycleOwner
。
从现在开始,您可以让 ViewModel
到 LifecycleService
为自己执行操作,并且如果您遵循正确的应用程序架构并且拥有存储库模式,那么您可以单一事实来源!
在 O.P 的上下文中。 LifecycleService 现在可以维护它的 ViewModel
来执行与存储库层相关的业务逻辑,稍后在另一个生命周期感知组件上,Activity/Fragment 也可以 consume/reuse 相同 ViewModel
到有他们的具体操作。
请注意,通过这样做,您将处于拥有两个不同 LifecycleOwner
的状态 (Activity & LifecycleServie),这意味着您可以在 LifecycleService
和其他生命周期感知组件之间共享视图模型。如果您不喜欢这种方法,那么可以使用旧的回调方法,当数据准备好提供服务时,可以从服务中回调 Activity/Fragment 等
已过时:
(建议不要通读)
在我看来,服务应该与[=129=处于同一水平],因为它是框架组件而不是 MVVM。但由于该服务未实现 LifecycleOwner 并且它是 Android 框架组件,因此不应将其视为 数据源 因为它可以作为应用程序的入口点。
所以,这里的困境是有时 (在你的情况下),服务充当数据源,它提供从一些长 运行 任务到 UI.
那么在Android架构组件中应该是什么?我想你可以把它当作LifecycleObserver。因为,无论您在后台做什么,您都需要考虑 LifecycleOwner.
的生命周期
为什么?因为,我们通常会将它绑定到 LifecycleOwner (Activity/Fragments) 并执行 运行 长任务 UI。因此,它可以被视为 LifecycleObserver。通过这种方式,我们将服务作为“生命周期感知组件”!
How you can implement it?
获取你的服务class并实现LifecycleObserver接口。
当您将服务绑定到 Activity/Fragment
时,在您的服务 class 的服务连接期间,通过调用方法将您的服务作为 LifecycleObserver 添加到您的 activity getLifecycle().addObserver(service class obj)
现在,在服务 class 中使用一个接口来提供从服务到您的 UI 的回调,并且每次您的数据更改时,检查您的服务是否至少有用于提供回调的生命周期事件 create or resume。
这样一来,我们就不需要LiveData
更新到from服务,甚至不需要ViewModel
(为什么我们需要服务?我们不不需要更改配置来在服务生命周期中存活。VM 的主要任务是在生命周期之间包含数据).
旁注:如果您认为自己有长时间的 运行 后台操作,请考虑使用 WorkManager
。 使用此库后,您你会觉得现在应该将服务标记为已弃用! (随便想的)
避免直接接触 Android 服务同时仍然能够使用它的一种方法是通过接口对象。这是 "I" 的一部分,用于 Interface Segregation 的缩写,SOLID。这是一个小例子:
public interface MyFriendlyInterface {
public boolean cleanMethodToAchieveBusinessFunctionality();
public boolean anotherCleanMethod();
}
public class MyInterfaceObject implements MyFriendlyInterface {
public boolean cleanMethodToAchieveBusinessFunctionality() {
BluetoothObject obj = android.Bluetooth.nastySubroutine();
android.Bluetooth.nastySubroutineTwo(obj);
}
public boolean anotherCleanMethod() {
android.Bluetooth.anotherMethodYourPresentersAndViewModelsShouldntSee();
}
}
public class MyViewModel {
private MyFriendlyInterface _myInterfaceObject;
public MyViewModel() {
_myInterfaceObject = new MyInterfaceObject();
_myInterfaceObject.cleanMethodToAchieveBusinessFunctionality();
}
}
鉴于上述范例,您可以自由地将您的服务放在包含 POJO 代码的包之外的包中。没有 "right" 位置来放置您的服务——但肯定有错误的位置来放置它们(例如,您的 POJO 代码所在的位置)。
我认为在服务中使用 LiveData 很舒服
class OneBreathModeTimerService : Service() {
var longestHoldTime = MutableLiveData<Int>().apply { value = 0 }
...
}
然后在片段
override fun onCreate(savedInstanceState: Bundle?) {
mServiceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, binder: IBinder) {
mOneBreathModeService = (binder as OneBreathModeTimerService.MyBinder).service
mOneBreathModeService!!.longestHoldTime.observe(this@OneBreathModeFragment, androidx.lifecycle.Observer {
binding.tvBestTime.text = "Best $it"
})
}
override fun onServiceDisconnected(name: ComponentName) {}
}
我不是 LiveData 专家,但这种方法有什么问题?
如果我们 bind/unbind to/from 服务来自 activity 或 onStart/onStop 中的多个活动如常,那么我们有包含蓝牙相关管理器的单例实例(我使用ble 管理器的北欧库)。该实例正在服务中,因此我们可以断开连接,例如当服务被销毁时,因为 ui 与它解除绑定,并在创建服务时重新连接到 ble。我们还将那个 ble 管理器单例注入到视图模型中,以通过 livedata 或 rx 或 ble 管理器提供的类似反应数据(例如连接状态)使交互和数据监听更容易。通过这种方式,我们可以从视图模型与 ble 进行交互,订阅特性等,并且服务可以提供可以在多个活动中生存的范围,并且基本上知道何时连接或断开连接。我已经在我的应用程序中尝试过这种方法,到目前为止它工作正常。
这个问题困惑了我很久。我认为绑定服务不应该有 viewModle,正如我们所知,服务不是视图层!
顺便说一句,服务需要start/bind,解绑在activity
最后,我认为一个简单且不错的方法是AndroidService 中的LiveData。 但不是使用Livedata 发送数据,而是使用自定义回调方法。
实时数据每次只发送最新的数据,如果你需要获取完整的数据而且页面处于暂停状态,这是错误的。(现在,我们可以使用kotlin flow)
此外,我们不仅要从 ble(bluetooth low energe) 设备接收数据,我们还需要将数据发送到 ble 设备。
我一直在思考在新的 Android recommended Architecture 中放置 Android 服务的位置。我想出了很多可能的解决方案,但我无法确定哪一个是最好的方法。
我做了很多研究,但找不到任何有用的指南或教程。我发现关于在我的应用程序架构中放置服务的唯一提示是这个,来自@JoseAlcerreca Medium post
Ideally, ViewModels shouldn’t know anything about Android. This improves testability, leak safety and modularity. A general rule of thumb is to make sure there are no android.* imports in your ViewModels (with exceptions like android.arch.*). The same applies to presenters.
据此,我应该将我的 Android 服务放在我的架构组件层次结构的顶部,与我的活动和片段处于同一级别。那是因为 Android 服务是 Android 框架的一部分,所以 ViewModels 不应该知道它们。
现在,我将简要说明我的场景,但只是为了让全景更清晰,而不是因为我想要针对这个特定场景的答案。
- 我有一个 Android 应用程序,它有一个包含许多片段的 MainActivity,所有片段都捆绑在一个 BottomNavBar 中。
- 我有一个 BluetoothService 绑定到 myActivity 及其片段之一(因为我希望该服务具有与 Activty 相同的生命周期,但我也想直接从我的片段与其交互)。
- 片段与BluetoothService交互获取两类信息:
- 关于蓝牙连接状态的信息。不需要持久化。
- 来自蓝牙设备的数据(在这种情况下,它是一个秤,因此重量和 body 组成)。需要坚持。
以下是我能想到的 3 种不同的架构:
LiveData inside AndroidService
更新:这是我当时个人采用的方法,因为它运作良好并且让我能够相对快速地完成它。但是,我建议遵循 Jeel Vankhede 的更新答案,以获得似乎更“惯用”的实现。
- 具有连接状态和权重的 LiveData 来自蓝牙设备的测量值在 BluetoothService 中。
- Fragment可以触发BluetoothService中的操作(例如scanDevices)
- Fragment观察关于连接状态的LiveData 并相应地调整 UI(例如,如果 状态已连接)。
- Fragment 观察新体重测量的 LiveData。如果新的重量测量值来自 BluetoothDevice,则 Fragment 会告诉自己的 ViewModel 保存新数据。它是通过存储库 class. 完成的
片段和 AndroidService 之间的共享 ViewModel
- Fragment可以触发BluetoothService中的操作(例如scanDevices)
- BluetoothService更新共享ViewModel中的蓝牙相关LiveData。
- Fragment 在自己的 ViewModel 中观察 LiveData。
服务视图模型
- Fragment可以触发BluetoothService中的操作(例如scanDevices)
- BluetoothService在自己的ViewModel中更新蓝牙相关的LiveData。
- Fragment 在自己的 ViewModel 和 BluetoothService ViewModel 中观察 LiveData。
我很确定我应该将它们放在架构的顶部并像 Activity/Fragment 一样对待它们,因为 BoundServices 是 Android 框架的一部分,它们由 Android OS 并且它们绑定到其他 Activity 和 Fragments。在那种情况下,我不知道与 LiveData、ViewModel 和 Activities/Fragments.
交互的最佳方式是什么有些人可能认为它们应该被视为数据源(因为在我的例子中它是使用蓝牙从秤中获取数据),但我认为这不是一个好主意,因为我已经知道了在上一段中特别提到 because of what it says here:
Avoid designating your app's entry points—such as activities, services, and broadcast receivers—as sources of data. Instead, they should only coordinate with other components to retrieve the subset of data that is relevant to that entry point. Each app component is rather short-lived, depending on the user's interaction with their device and the overall current health of the system.
所以,最后,我的问题是:
我们应该将 Android(绑定)服务放在哪里,它们与其他架构组件的关系是什么?这些替代方案中的任何一个都是好的方法吗?
这样对待你们的服务怎么样?
已更新:
在得到@Ibrahim Disouki 的建议后(谢谢你)我深入挖掘并发现了一些有趣的东西!这是背景。
O.P.寻求解决方案“Android框架的服务组件站在考虑Android架构组件”。所以,这是开箱即用的(SDK)解决方案。
与Activity/Fragment
处于同一水平。如何?如果您要扩展服务 class 而不是那样,请开始扩展 LifecycleService。这背后的原因很简单,以前我们不得不依赖 Activity/Fragment 生命周期来接收 updates/do 服务上的一些上下文操作。但现在不是这样了。
LifecycleService
现在有自己的生命周期 registry/maintainer,称为 ServiceLifecycleDispatcher
,它负责服务的生命周期,这也使它成为 LifecycleOwner
。
从现在开始,您可以让 ViewModel
到 LifecycleService
为自己执行操作,并且如果您遵循正确的应用程序架构并且拥有存储库模式,那么您可以单一事实来源!
在 O.P 的上下文中。 LifecycleService 现在可以维护它的 ViewModel
来执行与存储库层相关的业务逻辑,稍后在另一个生命周期感知组件上,Activity/Fragment 也可以 consume/reuse 相同 ViewModel
到有他们的具体操作。
请注意,通过这样做,您将处于拥有两个不同 LifecycleOwner
的状态 (Activity & LifecycleServie),这意味着您可以在 LifecycleService
和其他生命周期感知组件之间共享视图模型。如果您不喜欢这种方法,那么可以使用旧的回调方法,当数据准备好提供服务时,可以从服务中回调 Activity/Fragment 等
已过时:
(建议不要通读)
在我看来,服务应该与[=129=处于同一水平],因为它是框架组件而不是 MVVM。但由于该服务未实现 LifecycleOwner 并且它是 Android 框架组件,因此不应将其视为 数据源 因为它可以作为应用程序的入口点。
所以,这里的困境是有时 (在你的情况下),服务充当数据源,它提供从一些长 运行 任务到 UI.
那么在Android架构组件中应该是什么?我想你可以把它当作LifecycleObserver。因为,无论您在后台做什么,您都需要考虑 LifecycleOwner.
的生命周期为什么?因为,我们通常会将它绑定到 LifecycleOwner (Activity/Fragments) 并执行 运行 长任务 UI。因此,它可以被视为 LifecycleObserver。通过这种方式,我们将服务作为“生命周期感知组件”!
How you can implement it?
获取你的服务class并实现LifecycleObserver接口。
当您将服务绑定到
Activity/Fragment
时,在您的服务 class 的服务连接期间,通过调用方法将您的服务作为 LifecycleObserver 添加到您的 activitygetLifecycle().addObserver(service class obj)
现在,在服务 class 中使用一个接口来提供从服务到您的 UI 的回调,并且每次您的数据更改时,检查您的服务是否至少有用于提供回调的生命周期事件 create or resume。
这样一来,我们就不需要LiveData
更新到from服务,甚至不需要ViewModel
(为什么我们需要服务?我们不不需要更改配置来在服务生命周期中存活。VM 的主要任务是在生命周期之间包含数据).
旁注:如果您认为自己有长时间的 运行 后台操作,请考虑使用 WorkManager
。 使用此库后,您你会觉得现在应该将服务标记为已弃用! (随便想的)
避免直接接触 Android 服务同时仍然能够使用它的一种方法是通过接口对象。这是 "I" 的一部分,用于 Interface Segregation 的缩写,SOLID。这是一个小例子:
public interface MyFriendlyInterface {
public boolean cleanMethodToAchieveBusinessFunctionality();
public boolean anotherCleanMethod();
}
public class MyInterfaceObject implements MyFriendlyInterface {
public boolean cleanMethodToAchieveBusinessFunctionality() {
BluetoothObject obj = android.Bluetooth.nastySubroutine();
android.Bluetooth.nastySubroutineTwo(obj);
}
public boolean anotherCleanMethod() {
android.Bluetooth.anotherMethodYourPresentersAndViewModelsShouldntSee();
}
}
public class MyViewModel {
private MyFriendlyInterface _myInterfaceObject;
public MyViewModel() {
_myInterfaceObject = new MyInterfaceObject();
_myInterfaceObject.cleanMethodToAchieveBusinessFunctionality();
}
}
鉴于上述范例,您可以自由地将您的服务放在包含 POJO 代码的包之外的包中。没有 "right" 位置来放置您的服务——但肯定有错误的位置来放置它们(例如,您的 POJO 代码所在的位置)。
我认为在服务中使用 LiveData 很舒服
class OneBreathModeTimerService : Service() {
var longestHoldTime = MutableLiveData<Int>().apply { value = 0 }
...
}
然后在片段
override fun onCreate(savedInstanceState: Bundle?) {
mServiceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, binder: IBinder) {
mOneBreathModeService = (binder as OneBreathModeTimerService.MyBinder).service
mOneBreathModeService!!.longestHoldTime.observe(this@OneBreathModeFragment, androidx.lifecycle.Observer {
binding.tvBestTime.text = "Best $it"
})
}
override fun onServiceDisconnected(name: ComponentName) {}
}
我不是 LiveData 专家,但这种方法有什么问题?
如果我们 bind/unbind to/from 服务来自 activity 或 onStart/onStop 中的多个活动如常,那么我们有包含蓝牙相关管理器的单例实例(我使用ble 管理器的北欧库)。该实例正在服务中,因此我们可以断开连接,例如当服务被销毁时,因为 ui 与它解除绑定,并在创建服务时重新连接到 ble。我们还将那个 ble 管理器单例注入到视图模型中,以通过 livedata 或 rx 或 ble 管理器提供的类似反应数据(例如连接状态)使交互和数据监听更容易。通过这种方式,我们可以从视图模型与 ble 进行交互,订阅特性等,并且服务可以提供可以在多个活动中生存的范围,并且基本上知道何时连接或断开连接。我已经在我的应用程序中尝试过这种方法,到目前为止它工作正常。
这个问题困惑了我很久。我认为绑定服务不应该有 viewModle,正如我们所知,服务不是视图层!
顺便说一句,服务需要start/bind,解绑在activity
最后,我认为一个简单且不错的方法是AndroidService 中的LiveData。 但不是使用Livedata 发送数据,而是使用自定义回调方法。 实时数据每次只发送最新的数据,如果你需要获取完整的数据而且页面处于暂停状态,这是错误的。(现在,我们可以使用kotlin flow)
此外,我们不仅要从 ble(bluetooth low energe) 设备接收数据,我们还需要将数据发送到 ble 设备。