BoundService + LiveData + ViewModel 新Android推荐架构最佳实践

我一直在努力思考在新的 Android recommended Architecture 中放置 Android 服务的位置。我想出了许多可能的解决方案,但我无法决定哪一个是最好的方法。

我做了很多研究,但找不到任何有用的指南或教程。我发现的关于在我的应用程序架构中放置服务的唯一提示是这个,来自@JoseAlcerreca Medium post

理想情况下,ViewModel 不应该对 Android 有任何了解。这提高了可测试性、泄漏安全性和模块化。一般的经验法则是确保您的 ViewModel 中没有 android. 导入(android.arch. 等例外情况除外)。这同样适用于演讲者。

据此,我应该将我的 Android 服务放在架构组件层次结构的顶部,与我的活动和片段处于同一级别。那是因为 Android 服务是 Android 框架的一部分,所以 ViewModel 不应该知道它们。

现在,我将简要解释一下我的场景,但只是为了让全景更清晰,而不是因为我想要这个特定场景的答案。

  • 我有一个 Android 应用程序,它有一个 MainActivity,里面有很多片段,所有这些片段都捆绑在一个 BottomNavBar 中。
  • 我有一个绑定到 myActivity 及其片段之一的 BluetoothService(因为我希望服务具有与 Activty 相同的生命周期,但我也想直接从我的片段与其交互)。
  • Fragment与BluetoothService交互,获取两种信息:
    *有关蓝牙连接状态的信息。不需要坚持。
    • 来自蓝牙设备的数据(它是一个秤,在这种情况下是体重和身体成分)。需要坚持。

以下是我能想到的 3 种不同的架构:

AndroidService 中的 LiveData

**UPDATE: This is the approach I personally went with at the time because it worked well and allowed me to get it done relatively fast. However, I suggest following the updated answer by Jeel Vankhede for what seems to be a more "idiomatic" implementation.**

LiveData inside Android Service arch

  • 带有连接状态和来自蓝牙设备的重量测量值的 LiveData 在 BluetoothService 中。
  • Fragment可以触发BluetoothService中的操作(例如scanDevices)
  • Fragment 观察有关连接状态的 LiveData 并相应地调整 UI(例如,如果状态为连接,则启用按钮)。
  • Fragment 观察新体重测量的 LiveData。如果来自 BluetoothDevice 的新重量测量值,则 Fragment 会告诉它自己的 ViewModel 保存新数据。它是通过 Repository 类完成的。

fragment 和 AndroidService 之间的共享 ViewModel Shared ViewModel arch

  • Fragment可以触发BluetoothService中的操作(例如scanDevices)
  • BluetoothService 更新共享 ViewModel 中的蓝牙相关 LiveData。
  • Fragment 在自己的 ViewModel 中观察 LiveData。

服务视图模型 Service ViewMOdel arch

  • Fragment可以触发BluetoothService中的操作(例如scanDevices)

  • BluetoothService 在自己的 ViewModel 中更新蓝牙相关的 LiveData。

  • Fragment 在自己的 ViewModel 和 BluetoothService ViewModel 中观察 LiveData。

    • *

我很确定我应该将它们放在架构之上,并将它们视为 Activity/Fragment,因为 BoundServices 是 Android 框架的一部分,它们由 Android 操作系统管理,并且绑定到其他 Activity 和 Fragment。在这种情况下,我不知道与 LiveData、ViewModel 和活动/片段交互的最佳方式是什么。

有些人可能认为它们应该被视为数据源(因为在我的例子中,它是使用蓝牙从秤中获取数据),但我认为这不是一个好主意,因为我在上一段中说过特别是 because of what it says here

避免将应用程序的入口点(例如活动、服务和广播接收器)指定为数据源。相反,它们应该只与其他组件协调以检索与该入口点相关的数据子集。每个应用程序组件的寿命都很短,具体取决于用户与其设备的交互以及系统当前的整体健康状况。

所以,最后,我的问题是:

我们应该在哪里放置我们的 Android(绑定)服务以及它们与其他架构组件的关系是什么?这些替代方案中的任何一个都是好方法吗?

stack overflow BoundService + LiveData + ViewModel best practice in new Android recommended architecture
原文答案
author avatar

接受的答案

更新:

在得到@Ibrahim Disouki (Thank you for that) 的建议后,我深入挖掘并发现了一些有趣的东西!这里是背景。

O.P. 寻求解决方案“ Where Service component of Android Framework stands considering Android Architecture Components ”。所以,这里是开箱即用(SDK)解决方案。

它与 Activity/Fragment 处于同一级别。如何?如果您要扩展 Service 类而不是那样,请开始扩展 LifecycleService 。这背后的原因很简单,以前我们必须依赖 Activity/Fragment 生命周期才能接收更新/对 Service 进行一些上下文操作。但现在情况并非如此。

LifecycleService 现在有它自己的生命周期注册表/维护器,称为 ServiceLifecycleDispatcher ,它负责服务的生命周期,这也使它成为 LifecycleOwner

它给我们留下的条件是,从现在开始,您可以让 ViewModelLifecycleService 为自己执行操作,如果您遵循正确的应用程序架构并且拥有存储库模式,那么您将获得单一的事实来源!

在 O.P. LifecycleService 的上下文中,现在可以维护它的 ViewModel 来执行与存储库层相关的业务逻辑,然后在另一个生命周期感知组件上,如 Activity/Fragment 也可以使用/重用相同的 ViewModel 来拥有它们的对其进行具体操作。

请注意,这样做,您处于拥有两个不同 LifecycleOwner(Activity & LifecycleServie) 的状态,这意味着您无法在 LifecycleService 和其他生命周期感知组件之间共享视图模型。如果你不喜欢这种方法,那么最好使用旧的回调方法,当数据准备好服务时,从服务中回调到 Activity/Fragment 等。


过时的:

(I suggest not to read through)

在我看来, **Service** 应该与 **Activity/Fragment** 处于同一级别,因为它是框架组件而不是 MVVM。但由于该服务没有实现 LifecycleOwner 并且它是 Android 框架组件,因此不应将其视为 数据源,因为它可以成为应用程序的入口点。

因此,这里的困境是有时 (In your case) ,Service 充当数据源,将来自某个长时间运行的任务的数据提供给 UI。

那么 Android 架构组件 中应该是什么?我认为您可以将其视为 LifecycleObserver 。因为,无论您在后台做什么,都需要考虑 LifecycleOwner 的生命周期。

为什么?因为,我们通常会将它绑定到 LifecycleOwner (Activity/Fragments) 并在 UI 之外执行长时间运行的任务。因此,它可以被视为 LifecycleObserver。通过这种方式,我们将我们的服务设为“ Lifecycle aware component ”!


如何实施?

  1. 获取您的 service class 并为其实现 LifecycleObserver 接口。

2.当你将你的服务绑定到 Activity/Fragment 时,在你的服务类的服务连接期间,通过调用方法 getLifecycle().addObserver(service class obj) 将你的服务作为LifecycleObserver添加到你的活动中

  1. 现在,在服务类中使用一个接口来提供从服务到您的 UI 的回调,并且每次您的数据更改时,检查您的服务是否至少有生命周期事件 createresume 来提供回调。

这样一来,我们就不需要 LiveData 从服务更新,甚至不需要 ViewModel (Why do we need it for service? We don't need configuration changes to survive on service lifecycle. And main task to VM is that to consist data between lifecycles)


Side Note: If you think you're having long running background operations, then consider using WorkManager . 使用此库后,您会觉得现在服务应该被标记为已弃用! (Just a random thought)


答案:

作者头像

在仍然能够使用它的同时避免直接接触 Android 服务的一种方法是通过接口对象。这是首字母缩略词 SOLID 中接口隔离**“I”的一部分。这是一个小例子:

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 代码的包之外的包中。没有“正确”的位置来放置您的服务——但肯定有放置它们的错误位置(例如,您的 POJO 代码所在的位置)。

作者头像

如果我们像往常一样在 onStart/onStop 中从活动或多个活动中绑定/取消绑定服务,那么我们将拥有包含蓝牙相关管理器的单例实例(我使用北欧库作为 ble 管理器)。该实例正在服务中,因此我们可以在服务被销毁时断开连接,因为 ui 未绑定它,并在创建服务时重新连接到 ble。我们还将 ble 管理器单例注入到视图模型中,以便通过 livedata 或 rx 或 ble 管理器提供的类似反应数据(例如连接状态)更轻松地进行交互和数据侦听。通过这种方式,我们可以从 viewmodel 与 ble 进行交互,订阅特性等,并且服务可以提供可以在多个活动中生存的范围,并且基本上知道何时连接或断开连接。我已经在我的应用程序中尝试过这种方法,到目前为止它工作正常。

示例项目 https://github.com/uberchilly/BoundServiceMVVM

作者头像

像这样对待你的服务怎么样?

enter image description here

作者头像

在我看来,在服务中使用 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 的专业人士,但这种方法有什么问题?

作者头像

这个问题让我困惑了很久。我认为绑定服务不应该有 viewModle,众所周知,服务不是视图层!

By the way, Service need to start/bind,unbind at activity

最后,我认为一个简单且不错的方法是AndroidService内部的LiveData。但不使用Livedata发送数据,使用自定义回调方法。实时数据每次只发送最新数据,如果您需要完整数据并且页面处于暂停状态,则会出错。(现在,我们可以使用kotlin flow)

此外,我们不仅从 ble(蓝牙低功耗)设备接收数据,我们还需要将数据发送到 ble 设备。

有一个简单的项目代码: https://github.com/ALuoBo/TestTemp/blob/main/bluetooth/src/main/java/com/lifwear/bluetooth/BLECommService.java