在新的Android推荐架构中,BoundService + LiveData + ViewModel最佳实践。

91
我一直在思考在新的Android建议架构中应该把Android服务放在哪里,有很多可能的解决方案,但我无法确定哪种方法是最好的。
我进行了大量研究,但没有找到任何有用的指导或教程。唯一关于在我的应用程序架构中放置服务的提示来自@JoseAlcerreca的Medium post
引用: “理想情况下,ViewModels不应该知道任何关于Android的事情。这可以提高可测试性、泄漏安全性和模块化程度。一个基本的经验法则是确保你的ViewModels中没有android.*导入(如android.arch.*等除外)。同样适用于presenters。”
根据这个,我应该把我的Android服务放在架构组件层次结构的顶部,与我的Activities和Fragments处于同一级别。这是因为Android服务是Android框架的一部分,所以ViewModels不应该知道它们。
现在,我将简要解释一下我的场景,但只是为了使全景更清晰,而不是因为我想得到这个特定场景的答案。
我有一个Android应用程序,其中包含许多片段的MainActivity,在BottomNavBar中全部绑定在一起。
我已经将BluetoothService绑定到了我的Activity和其中一个片段上(因为我希望服务与Activty具有相同的生命周期,但我也希望直接从我的片段与之交互)。
该片段与BluetoothService交互以获取两种类型的信息:
蓝牙连接状态的信息。不需要持久保存。
来自蓝牙设备的数据(这是一个秤,因此在这种情况下是体重和身体组成)。需要持久保存。
以下是我所能想到的三种不同的架构:
LiveData位于AndroidService内部

LiveData inside Android Service arch

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

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

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

Service ViewModel Service ViewMOdel arch

  • Fragment可以触发BluetoothService中的操作(例如扫描设备)。
  • BluetoothService在自己的ViewModel中更新与蓝牙相关的LiveData。
  • Fragment在自己的ViewModel和BluetoothService的ViewModel中观察LiveData。

我相信应该将它们放在架构的顶部,并像对待Activity/Fragment一样对待它们,因为BoundServices是Android框架的一部分,受Android OS管理,并绑定到其他Activities和Fragments。在这种情况下,我不知道与LiveData、ViewModels和Activities/Fragments交互的最佳方法是什么。
有些人可能认为它们应该被视为DataSource(因为在我的情况下,它使用蓝牙从称量器获取数据),但我认为这不是一个好主意,因为在上一段中所说的所有内容以及特别因为这里所说的
避免将应用程序的入口点(例如活动、服务和广播接收器)指定为数据源。相反,它们应该只与其他组件协调,以检索与该入口点相关的数据子集。每个应用程序组件都是相当短暂的,取决于用户与其设备的交互以及系统的整体当前健康状况。
所以,最后,我的问题是:
我们应该将Android(Bound)服务放在哪里,它们与其他架构组件的关系是什么?这些替代方案中有哪些是好的方法?

1
你可以将你的服务视为 MVVM 中的 '生命周期感知组件'。如何实现呢?你已经有了一个 绑定服务,将其绑定到 生命周期所有者(在你的情况下是活动和你要绑定的一个片段),并在任何恢复或启动事件中调用或通知你的数据更改给你的生命周期所有者(即你的服务)。所以你只需要实现 LifecycleObserver 接口即可。 - Jeel Vankhede
1
@JeelVankhede 这是一种更好的管理我的服务绑定和解除绑定的方式,我之前没有考虑过,谢谢!但是,我仍然不明白这将如何与我的ViewModel和LiveData问题相关联。您会将Ble相关的LiveData放在Fragment的ViewModel中吗?如何在它们之间通知更改?因为数据在onStart或onResume中不可用,而是在它们之间收集的。 - dglozano
1
@MartinZeitler 当提及其他网站时,指出不赞成交叉发布通常是有帮助的。 - gnat
1
我投票关闭此问题,因为它属于https://softwareengineering.stackexchange.com。 - Martin Zeitler
@gnat 只是这么想,因为我在撤回的答案的评论中链接到了另一个答案...而且由于这个问题与代码没有直接关系,似乎是不相关的。在那里它甚至可能会得到比这里更好的答案。目前它还不是一个交叉发布的帖子,但应该被迁移。 - Martin Zeitler
6个回答

53

更新:

在得到 @Ibrahim Disouki 的建议后(非常感谢),我进行了更深入的挖掘并发现了一些有趣的事情!以下是背景。

O.P. 寻求解决方案:“考虑到Android Architecture Components,Android Framework的服务组件处于何种地位”。因此,这里提供一个开箱即用的(SDK)解决方案。

它与 Activity/Fragment 处于同一级别。如何实现呢?如果您正在扩展 Service 类,请改为扩展 LifecycleService。原因很简单,以前我们必须依赖 Activity/Fragment 生命周期来接收更新/执行某些上下文操作。但现在不再是这样了。

LifecycleService 现在拥有自己的生命周期注册表/维护程序称为 ServiceLifecycleDispatcher,它负责服务的生命周期,同时也使其成为 LifecycleOwner

这使得现在您可以将 ViewModel 应用于 LifecycleService 以对自身进行操作,并且如果您遵循适当的应用程序架构并采用存储库模式,则只留下一个数据源!

在 O.P. LifecycleService 的上下文中,现在已经有能力维护它的 ViewModel 以进行与存储库层相关的业务逻辑,并且稍后另一个生命周期感知组件(如 Activity/Fragment)也可以使用/重用相同的 ViewModel 并对其执行特定操作。

请注意,这样做会使您处于拥有两个不同的 LifecycleOwners (Activity 和 LifecycleServie) 的状态,这意味着您无法在 LifecycleService 和其他生命周期感知组件之间共享视图模型。如果您不喜欢此方法,则可以使用旧的回调方式,从服务中回调到 Activity/Fragment,以便在数据准备就绪时提供数据等。


已过时:

(我建议不要阅读)

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

因此,这里的困境在于有时(在您的情况下),Service 作为数据源,提供来自某些长时间运行的任务的数据到 UI。

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

为什么?因为我们通常会将其绑定到LifecycleOwner (Activity / Fragments)上,并在UI之外执行长时间运行的任务。因此,它可以像LifecycleObserver一样对待。通过这种方式,我们将我们的服务变成了“ 生命周期感知组件”!


如何实现呢?

  1. 拿出你的服务类并将LifecycleObserver接口实现到它上面。

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

  • 现在,在服务类中创建一个接口来提供从服务到UI的回调功能。每次数据更改时,检查如果您的服务至少有一个生命周期事件 createresume , 则提供回调。

  • 这样一来,我们就不需要使用 LiveData 来自服务更新,甚至不需要 ViewModel(为什么我们需要它来为服务提供什么?我们不需要配置更改来生存于服务生命周期。VM 的主要任务是在生命周期之间维护数据)


    顺便说一下:如果您认为您有长时间运行的后台操作,则考虑使用 WorkManager使用此库后,您会感觉到服务应该已经被标记为过时了!(只是随意想法)


    3
    不建议使用共享虚拟机或LiveData作为服务,我们已经将服务的生命周期观察者设置好,因此它现在能够感知活动/碎片。因此,请通过遵守生命周期所有者状态来直接回调。现在不需要使用中介者来处理服务。 - Jeel Vankhede
    2
    服务现在正在实现LifecycleOwner检查LifecycleService,但现在的问题是如何在服务和另一个Android组件(如Activity或Fragment)之间创建共享视图模型?@JeelVankhede - Ibrahim Disouki
    2
    @IbrahimDisouki,坦白地说,我早就回答过这个问题了,但现在你提到服务是“LifecycleOwner”,这意味着我的答案在这种情况下没有意义,我需要再次仔细考虑并提出解决方案。我只能说,你可以通过遵循我们过去在任何活动和服务之间使用的传统通信方式来共享数据。 - Jeel Vankhede
    2
    有人有这个答案的可用的代码示例吗? - Emad Razavi
    1
    @EmilS。抱歉给您带来不便,已更新帖子。感谢您的反馈。 - Jeel Vankhede
    显示剩余8条评论

    1
    如果我们像通常在onStart/onStop中绑定/解绑服务到/从活动中一样,那么我们就会有一个持有蓝牙相关管理器(我使用nordic库用于ble管理器)的单例实例。该实例在服务中,因此我们可以在服务被销毁时断开连接(例如当UI取消绑定时),并在服务创建时重新连接到BLE。我们还将该ble管理器单例注入到viewmodel中,以便通过livedata或rx或类似的由ble管理器提供的反应式数据更轻松地进行交互和数据监听,例如用于连接状态。这样,我们就可以从viewmodel与ble进行交互,订阅特征等等,而服务则提供了可在多个活动中生存并基本上知道何时连接或断开连接的范围。我已经在我的应用程序中尝试了这种方法,目前它运行得还不错。
    示例项目 https://github.com/uberchilly/BoundServiceMVVM

    这个解决方案对我来说似乎很有趣,但是如果没有人为您启动服务呢?基本上,您只依赖于BLE管理器,但它仅在您启动特定服务时才起作用。活动/片段如何知道这一点呢? - blow

    1
    通过接口对象,您可以避免直接与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代码放在一起的位置)。

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


    0

    这个问题让我困扰了很长时间。我不认为绑定服务应该有一个ViewModel,因为我们知道,服务不是视图层!

    顺便说一下,Service需要在Activity中启动/绑定/解绑。

    最后,我认为一种简单而不错的方法是在AndroidService内部使用LiveData。但不要使用LiveData来发送数据,使用自定义回调方法。LiveData每次只发送最新的数据,如果您需要在页面暂停时获取完整的数据,就会出现错误。(现在,我们可以使用Kotlin flow)

    此外,我们不仅需要从BLE(蓝牙低能耗)设备接收数据,还需要向BLE设备发送数据。

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


    这实际上更像是对另一个答案的评论,而不是一个独立的解决方案。此外,没有“第一”答案可以随时间稳定下来。如果你必须在答案中引用另一个答案,你应该链接到它(使用“分享”链接),和/或通过作者的名字来标识它。现在的情况是,不清楚这个评论是回应哪个答案的。 - Jeremy Caney
    @JeremyCaney,感谢您的建议,我有额外的备注:) - LuoBo

    -4

    你想把你的服务也这样对待吗?

    enter image description here


    2
    我认为OP提到了一个要求,即服务必须绑定到Activity以获取Activity的生命周期... 如果与蓝牙无关,OP可能不想扫描和观察蓝牙以避免耗电。 - MidasLefko
    2
    嗯,这是我的第一反应。但是我觉得BluetoothService不像一个webservice。它是一个Android组件,有自己的生命周期,并由Android OS管理。它还需要通过传递Context来启动。所以我认为它不能在那里。此外,“Ble数据源”的责任是什么?另外,如果你从上到下看架构,你越深入就越远离Android,直到你到达SQL或API时完全“离开它”。但是对于BluetoothService来说,情况并非如此,感觉很奇怪。 - dglozano
    1
    为了做一个类比,我认为在我的情况下,SQLite或Web服务的等价物将是我的秤内的GattServer。与之交互由蓝牙服务处理,因此在这个意义上,它将相当于DAO。所以一切似乎都很合理,直到我读到JoseAlcerreca的帖子。 - dglozano
    1
    同时,来自Android Dev's Guide的建议是:避免将您的应用程序入口点(例如活动、服务和广播接收器)指定为数据源。 相反,它们应该只与其他组件协调,检索与该入口点相关的数据子集。每个应用程序组件的生命周期都相对较短,取决于用户与设备的交互以及系统的整体当前状态。 - dglozano

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接