V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
KunMinX
V2EX  ›  Android

各位有遇到过 LiveData 在共享 ViewModel 中发生旧数据倒灌的情况吗?

  •  
  •   KunMinX · 2019-09-23 12:52:56 +08:00 · 14342 次点击
    这是一个创建于 1895 天前的主题,其中的信息可能已经有所发展或是发生改变。
    当我在 作用域 为 Activity 的 共享 ViewModel 中设置了一个 LiveData,

    FragmentA observe 这个 LiveData,那么在收到一次通知、并关闭 FragmentA 后,再进入 FragmentA,

    由于 该 ViewModel 是共享的,其中的 LiveData 并没有随着 FragmentA 的退出而清除,那么再次进入后,会自动走一遍 observe 回调,倒灌旧数据。

    我知道这么设计的缘由是为了处理生命周期从 unActive 到 active 转变时,跟进“最新” 的状态,但在共享 ViewModel 中,这算不算是一个 bug ?因为 LiveData 封装死了,我似乎没有其他办法来干预这种现象的发生。

    看看万能的 V 友有没什么好招
    第 1 条附言  ·  2020-03-13 22:08:46 +08:00
    对我在 13 楼对 8 楼 v 友 GLee9507 的过度反应表示抱歉。

    现如今,我对 LiveData + ViewModel 的观点,与该帖最终讨论的结果一脉相承,

    但如果要说有什么巨大转变的话,我想那就是 我不再要求别人应该应该怎样,正如我不再理会别人的要求。很高兴和 GLee9507 共享了这一人生智慧 😉
    30 条回复    2020-03-04 20:10:54 +08:00
    bkmi
        1
    bkmi  
       2019-09-23 13:31:49 +08:00 via Android   ❤️ 1
    既然 ViewModel 还在,说明数据还是有效的,并不是什么旧数据。
    如果你想把 ViewModel 和 FragmentA 绑定,那作用域就该用 Fragment 的
    zyqf
        2
    zyqf  
       2019-09-23 13:46:19 +08:00 via Android
    其中的 LiveData 并没有随着 FragmentA 的退出而清除。

    我是不是可以理解为你的需求是 FragmentA 退出要清除 LiveData ?

    如果是的话,在 FragmentA 销毁阶段 postValue 空对象不就好了。
    wsxyeah
        3
    wsxyeah  
       2019-09-23 13:56:53 +08:00 via iPhone
    类似于 BehaviorSubject
    KunMinX
        4
    KunMinX  
    OP
       2019-09-23 14:00:26 +08:00
    @bkmi @zyqf

    使用共享作用域,是为了能够跨页面通信,只不过第一次使用了,第二次它自动就回调了,不符合我预期。

    手动 postValue,容易因人工操作的疏忽而造成一致性问题,因为项目有数十个 Fragment,这种事情务必在后台自动完成。

    我刚刚找到的线索是,每次进入 fragment,livedata 的 observe.lastVersion 都被赋值为 -1,而 version 仍然是 0,那我觉得 bug 可能就在这里
    KunMinX
        5
    KunMinX  
    OP
       2019-09-23 14:04:28 +08:00   ❤️ 1
    好消息,找到解决方案了。

    通过反射,阻止用户回调的 liveData 第一次创建时的推送。这个推送实在太坑了。

    http://www.pianshen.com/article/4994394892/
    sanousun
        6
    sanousun  
       2019-09-23 15:11:52 +08:00   ❤️ 1
    美团有一篇关于 LiveDataBus 的文章,你可以看看 https://tech.meituan.com/2018/07/26/android-livedatabus.html
    Gehrman
        7
    Gehrman  
       2019-09-23 17:33:09 +08:00
    viewModel 里加多个缓存,跟缓存一样的无视掉?
    GLee9507
        8
    GLee9507  
       2019-09-23 17:40:43 +08:00   ❤️ 1
    我认为你是没有真正的理解 ViewModel 和 LiveData
    HangoX
        9
    HangoX  
       2019-09-23 19:23:51 +08:00
    其实你直接在 ViewOnCreated 的时候 getLiveData.getValue 拿出来设置就好了,既然是一次性的东西
    KunMinX
        10
    KunMinX  
    OP
       2019-09-23 20:43:51 +08:00
    @HangoX

    就用 bus-LiveData 的方案了。

    不手动编写的原因,楼上我已经讲过。

    不直接用 LiveDataBus 的原因是,需要维持 通过 唯一可信源 分发状态的 开发模式,来避免不可预期、难以追溯和排查的错误。
    bkmi
        11
    bkmi  
       2019-09-23 21:44:03 +08:00 via Android
    你这是把 LiveData 当 EventBus 用了啊…
    KunMinX
        12
    KunMinX  
    OP
       2019-09-23 22:06:18 +08:00   ❤️ 1
    我很后悔开了这个贴。

    除了 6 楼的 @sanousun 同学给出了建设性的答复,其他人始终慢了好几拍。

    我已经把状况讲得这么清楚了。

    为避免这些离题的回复 给后来者造成困扰,这里统一回复一下:

    任何技术绝非凭空存在,既然存在了,就有存在的缘由。

    ViewModel 被如此设计,当初就是有考虑过 使其作为跨页面共享的存在。

    当 lifeCyclerOwner 传入的是 fragment 的宿主 Activity,那么这个 ViewModel 就是被该 Activity 所持有、可以被旗下 Fragments 所共享。

    共享 ViewModel 的作用多是为了跨页面的通信,例如回调、实时通信等等。

    LiveData 的存在是为了确立 通过唯一可信源分发状态 的开发模式,以避免旧时候滥用 EventBus 导致的 难以追溯、数据过时 等问题。
    同时在 ViewModel 或单例的配合下,它使单向依赖成为了可能。
    再者它也可以规避一些生命周期问题。

    这也就是为什么我选择标准化的 Jetpack MVVM 架构来完成状态管理。

    除了状态管理,MVVM 同样有这个条件,甚至是更好的条件,来支持消息推送。这也就是为什么我要用 LiveData 来实现跨页面通信,而拒绝使用 EventBus。

    而既然考虑到 共享 ViewModel,那么 LiveData 也理应考虑到应解除对默认 粘性设计的支持。

    这个粘性设计或许在 视图控制器 重建 恢复状态时有用,在消息通信时则是个麻烦,所以我说这是个 bug,务必解决。
    KunMinX
        13
    KunMinX  
    OP
       2019-09-23 22:17:23 +08:00
    当然,上面我说到了 “ViewModel” 包含共享的考虑,肯定会有杠精不放过一丝添堵的机会。

    首先,遇到像 8 楼这种纯粹来恶心人、不就事论事 的评论,我第一个就选择 block,

    其次,我可以分享一下自己在项目中是怎么使用 ViewModel 的:

    因为 ViewModel 的本职主要是状态的托管,可以托管 DataBinding 或 视图控制器 所依赖的 state,
    也即这种 ViewModel 是 fragment 独享的,我会为每个 Fragment 都配一个,反正我有自己编写的自动化脚本工具,配几个 state 字段就能生成对应的 ViewModel。

    第二种就是专门用于数据请求,这些我也设计成让他们被 fragment 独占。因为 ViewModel 就是设计为,你传入什么 owner,这个 ViewModel 就是被哪个 owner 所持有。而且反正就算独占,fragment 重建时这个 ViewModel 被 retain 保留了因而可以支持视图状态的恢复。

    第三种就是本帖提到的这种消息通信的用法。

    我就说这么多了。
    GLee9507
        14
    GLee9507  
       2019-09-23 22:18:19 +08:00
    @KunMinX #13 没恶心你,说的是实话,真的。
    maninfog
        15
    maninfog  
       2019-09-23 22:43:49 +08:00 via iPhone   ❤️ 1
    @GLee9507 哇 你这种人真的恶心到我😫 本来就是寻求讨论的,你有何高见倒是说出来阿,啥建设性意见没有上来就是一句别人不懂,被怼了还要继续杠,shame on you !
    colaman
        16
    colaman  
       2019-09-23 22:44:28 +08:00
    @KunMinX 我当初是自己写了一个包装类处理这个粘性问题,我是觉得这种属性 Google 应该给开发者自己决定才比较合理
    Didadida
        17
    Didadida  
       2019-09-23 22:53:12 +08:00
    试试 SingleLiveEvent ?
    mxalbert1996
        18
    mxalbert1996  
       2019-09-23 22:57:21 +08:00 via Android   ❤️ 1
    @KunMinX 说到底就是 LiveData 不是为了你这种用途而设计的。LiveData 相当于 Rx 里的 BehaviorSubject,而你却想把它当 Observable 用。
    另外解决这种问题还要用反射也太不优雅了吧。写个新类继承 MutableLiveData,覆写 observe 方法在最前面加一条 setValue(null)不就行了?
    zeroDev
        19
    zeroDev  
       2019-09-23 23:02:37 +08:00 via Android   ❤️ 1
    @maninfog 网友没有教会你的责任,指出来哪里有问题就是好心了
    KunMinX
        20
    KunMinX  
    OP
       2019-09-23 23:20:07 +08:00
    @colaman

    是的。类似的令人头疼的问题也存在于 Navigation。

    Navigation 写死了切换 fragment 必须是通过 replace,而不能使用者自己选择,所以当前对于 Navigation 态度,也是保持欣赏但观望的态度(对它的声明式编程理念比较欣赏)。

    网上有给出 keep-fragment-state-Navigation 的解法,这里顺带分享一下:

    https://github.com/STAR-ZERO/navigation-keep-fragment-sample

    https://github.com/lwj1994/navigation-keep-state-fragment

    @mxalbert1996

    反射的方式影响最小。因为目标只是 为了解决 不自动倒灌 旧数据,要是先 set Null 了,那么其他已观察的页面再想要取 liveData 临时存的数据,就莫得治了
    KunMinX
        21
    KunMinX  
    OP
       2019-09-23 23:26:40 +08:00
    @KunMinX
    笔误,应该说,要是先 set Null 了,那么其他已观察的页面 就有被迫接受一次推送 null 的风险了。(因为毕竟是 一对多的推送、共享的 ViewModel 和 liveData 的情况)

    数十个页面,人工操作一定会存在疏忽、需要各种判空,所以这样其实从快速开发的角度来看,埋下的隐患要大一点。
    KunMinX
        22
    KunMinX  
    OP
       2019-09-23 23:38:53 +08:00
    @KunMinX
    唉,感觉这个答案还可以更恰当一点:observe 前 set null 一次,容易造成 其他页面 遭受容易被使用者疏忽的 被迫推送,而产生不可预期的错误。

    感谢 @mxalbert1996 提出的观点,让我不断地反思 设计成 反射方式 其背后的考虑。
    GLee9507
        23
    GLee9507  
       2019-09-23 23:43:33 +08:00 via Android
    @maninfog 我表达了一下我的主观想法,我只是主观的认为楼主理解有误,或许楼主可以另辟蹊径分析问题发生的原因。造成大家不爽深感抱歉。
    janus77
        24
    janus77  
       2019-09-24 00:25:59 +08:00 via iPhone
    问个沾边的问题,livedata 能用做 application 全局缓存么?
    DioV
        25
    DioV  
       2019-09-24 11:51:22 +08:00
    “在消息通信时则是个麻烦,所以我说这是个 bug,务必解决。”
    因为 LiveData 就不是为了解决通讯而存在的,哪里谈得上 bug...
    举个不恰当的例子,就像用 Retrofit 做 gRPC 的请求,能实现但绝对不是最合适的方式。
    KunMinX
        26
    KunMinX  
    OP
       2019-09-24 12:05:22 +08:00 via iPhone
    @DioV 断章取义烦不烦... MVVM 是一个整体,既然 ViewModel 被设计为支持共享作用域,那么 LiveData 就应考虑到这种跨页面通信的情况,这是官方文档自己都承认的需求。现在我就给 Google 小组留言这个问题。
    KunMinX
        27
    KunMinX  
    OP
       2019-09-24 12:20:23 +08:00
    楼上 @colaman 同学提到的包装类的办法,经调查,如果是我这样依然托管给 ViewModel 从而维持 唯一可信源状态分发 的话,那包装类的方式其实更合适。

    如果是用作纯粹的 BUS,那可以考虑反射的方式。

    这里分享一篇包装类处理的办法:

    https://juejin.im/post/5b2b1b2cf265da5952314b63
    DioV
        28
    DioV  
       2019-09-24 15:23:25 +08:00
    @KunMinX
    戾气真重...
    如果你理解下的 MVVM 必须提供这样的功能。那我是不是可以理解 Jetpack Compose 作为一套 Declarative Toolkit 而不必实现这样的需求?
    当然我希望你的 Feature Request 能被官方接受。毕竟我也只是个想说说自己看法的围观群众🤷‍♂️
    KunMinX
        29
    KunMinX  
    OP
       2019-09-24 15:44:38 +08:00
    @DioV

    统一回复一下:

    我对自己的每一条评论负责。

    通过讨论,我不仅要解决自己的问题,也要考虑到对后来者有无帮助。

    在和就事论事的 V 友的讨论中,对 LiveData 粘性问题的解决方案得以不断刷新,因而这次讨论算是成功的,有价值的。

    对于 LiveData 的状况,我在 12 楼给出了多达 500 字的翔实的分析,依然还有人只顾自己爽,来不及阅读理解,就急忙发表一些断章取义的抬杠,事后又以各种理由推卸责任。🤷‍♂️
    KunMinX
        30
    KunMinX  
    OP
       2020-03-04 20:10:54 +08:00
    对我在 13 楼对 8 楼 v 友 @GLee9507 的过度反应表示抱歉。

    现如今,我对 LiveData + ViewModel 的观点,与该帖最终讨论的结果一脉相承,

    但如果要说有什么巨大转变的话,我想那就是 我不再要求别人应该应该怎样,正如我不再理会别人的要求。很高兴和 @GLee9507 共享了这一人生智慧 😉
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1022 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 20ms · UTC 19:12 · PVG 03:12 · LAX 11:12 · JFK 14:12
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.