public class Observable { private boolean changed = false; private Vector obs; public void notifyObservers(Object arg) { Object[] arrLocal; synchronized (this) { if (!hasChanged()) return; arrLocal = obs.toArray(); clearChanged(); } // 无条件地遍历所有观察者并通知 for (int i = arrLocal.length-1; i>=0; i--) ((Observer)arrLocal[i]).update(this, arg); } } // 观察者 public interface Observer { void update(Observable o, Object arg); }
LiveData 在常规的观察者模式上附加了条件,若生命周期未达标,即使数据发生变化也不通知观察者。这是如何实现的?
生命周期
生命周期是一个对象从构建到消亡过程中的各个状态的统称。
比如 Activity 的生命周期用如下函数依次表达:
onCreate() onStart() onResume() onPause() onStop() onDestroy()
要观察生命周期就不得不继承 Activity 重写这些方法,想把生命周期的变化分发给其他组件就很麻烦。
于是 Jetpack 引入了 Lifecycle,以让任何组件都可方便地感知生命周期的变化:
public abstract class Lifecycle {AtomicReference<>(); // 添加生命周期观察者 public abstract void addObserver(LifecycleObserver observer); // 移除生命周期观察者 public abstract void removeObserver(LifecycleObserver observer); // 获取当前生命周期状态 public abstract State getCurrentState(); // 生命周期事件 public enum Event { ON_CREATE, ON_START, ON_RESUME, ON_PAUSE, ON_STOP, ON_DESTROY, ON_ANY; } // 生命周期状态 public enum State { DESTROYED, INITIALIZED, CREATED, STARTED, RESUMED; } // 判断至少到达了某生命周期状态 public boolean isAtLeast(State state) { return compareTo(state) >= 0; } }
Lifecycle 即是生命周期对应的类,提供了添加/移除生命周期观察者的方法,在其内部还定义了全部生命周期的状态及对应事件。
生命周期状态是有先后次序的,分别对应着由小到大的 int 值。
生命周期拥有者
描述生命周期的对象已经有了,如何获取这个对象需要个统一的接口(不然直接在 Activity 或者 Fragment 中新增一个方法吗?),这个接口叫LifecycleOwner:
public interface LifecycleOwner { Lifecycle getLifecycle(); }
Activity 和 Fragment 都实现了这个接口。
只要拿到 LifecycleOwner,就能拿到 Lifecycle,然后就能注册生命周期观察者。
生命周期 & 数据观察者
生命周期观察者是一个接口:
// 生命周期观察者(空接口,用于表征一个类型) public interface LifecycleObserver {} // 生命周期事件观察者 public interface LifecycleEventObserver extends LifecycleObserver { void onStateChanged(LifecycleOwner source, Lifecycle.Event event); }
要观察生命周期只要实现LifecycleEventObserver接口,并注册给LifeCycle即可。
除了生命周期观察者外,LiveData 场景中还有一个数据观察者:
// 数据观察者 public interface Observer { // 数据发生变化时回调 void onChanged(T t); }
数据观察者 会和 生命周期拥有者 进行绑定:
public abstract class LiveData { // 数据观察者容器 private SafeIterableMap<Observer<? super T>, ObserverWrapper> mObservers = new SafeIterableMap<>();
public void observe( LifecycleOwner owner, // 被绑定的生命周期拥有者 Observer<? super T> observer // 数据观察者 ) { ... // 将数据观察者包装成 LifecycleBoundObserver LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer); // 存储观察者到 map 结构 ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper); ... // 注册生命周期观察者。 owner.getLifecycle().addObserver(wrapper); } }
在观察 LiveData 时,需传入两个参数,生命周期拥有者和数据观察者。这两个对象经过LifecycleBoundObserver的包装被绑定在了一起:
class LifecycleBoundObserver extends ObserverWrapper implements LifecycleEventObserver { // 持有生命周期拥有者 final LifecycleOwner mOwner;
LifecycleBoundObserver(LifecycleOwner owner, Observer<? super T> observer) { super(observer); mOwner = owner; } // 生命周期变化回调 @Override public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) { ... activeStateChanged(shouldBeActive()) ... } }
// 观察者包装类型 private abstract class ObserverWrapper { // 持有原始数据观察者 final Observer<? super T> mObserver; // 注入数据观察者 ObserverWrapper(Observer<? super T> observer) {mObserver = observer;} // 尝试将最新值分发给当前数据观察者 void activeStateChanged(boolean newActive) {...} ... }
LifecycleBoundObserver 实现了LifecycleEventObserver接口,并且它被注册给了绑定的生命周期对象,遂具备了生命周期感知能力。同时它还持有了数据观察者,所以它还具备了数据观察能力。
2. LiveData 是如何避免内存泄漏的?
先总结,再分析:
LiveData 的数据观察者通常是匿名内部类,它持有界面的引用,可能造成内存泄漏。
LiveData 内部会将数据观察者进行封装,使其具备生命周期感知能力。当生命周期状态为 DESTROYED 时,自动移除观察者。
内存泄漏是因为长生命周期的对象持有了短生命周期对象,阻碍了其被回收。
观察 LiveData 数据的代码通常这样写:
class LiveDataActivity : AppCompatActivity() { private val viewModel by lazy { ViewModelProviders.of(this@LiveDataActivity).get(MyViewModel::class.java) }
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) viewModel.livedata.observe(this@LiveDataActivity) { // 观察 LiveData 数据更新(匿名内部类) } } }
Observer 作为界面的匿名内部类,它会持有界面的引用,同时 Observer 被 LiveData 持有,LivData 被 ViewModel 持有,而 ViewModel 的生命周期比 Activity 长。
最终的持有链如下:NonConfigurationInstances 持有 ViewModelStore 持有 ViewModel 持有 LiveData 持有 Observer 持有 Activity。
所以得在界面生命周期结束的时候移除 Observer,这件事情,LiveData 帮我们做了。
在 LiveData 内部 Observer 会被包装成LifecycleBoundObserver:
class LifecycleBoundObserver extends ObserverWrapper implements LifecycleEventObserver { final LifecycleOwner mOwner;
LifecycleBoundObserver(LifecycleOwner owner, Observer<? super T> observer) { super(observer); mOwner = owner; }
@Override public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) { // 获取当前生命周期 Lifecycle.State currentState = mOwner.getLifecycle().getCurrentState(); // 若生命周期为 DESTROYED 则移除数据观察者并返回 if (currentState == DESTROYED) { removeObserver(mObserver); return } ... } ... }
3. LiveData 是粘性的吗?若是,它是怎么做到的?
先总结,再分析:
LiveData 的值被存储在内部的字段中,直到有更新的值覆盖,所以值是持久的。
两种场景下 LiveData 会将存储的值分发给观察者。一是值被更新,此时会遍历所有观察者并分发之。二是新增观察者或观察者生命周期发生变化(至少为 STARTED),此时只会给单个观察者分发值。
LiveData 的观察者会维护一个“值的版本号”,用于判断上次分发的值是否是最新值。该值的初始值是-1,每次更新 LiveData 值都会让版本号自增。
LiveData 并不会无条件地将值分发给观察者,在分发之前会经历三道坎:1. 数据观察者是否活跃。2. 数据观察者绑定的生命周期组件是否活跃。3. 数据观察者的版本号是否是最新的。
“新观察者”被“老值”通知的现象叫“粘性”。因为新观察者的版本号总是小于最新版号,且添加观察者时会触发一次老值的分发。
如果把 sticky 翻译成“持久的”,会更好理解一些。数据是持久的,意味着它不是转瞬即逝的,不会因为被消费了就不见了,它会一直在那。而且当新的观察者被注册时,持久的数据会将最新的值分发给它。
“持久的数据”是怎么做到的?
显然是被存起来了。以更新 LiveData 数据的方法为切入点找找线索:
public abstract class LiveData { // 存储数据的字段 private volatile Object mData; // 值版本号 private int mVersion; // 更新值 protected void setValue(T value) { assertMainThread("setValue"); // 版本号自增 mVersion++; // 存储值 mData = value; // 分发值 dispatchingValue(null); } }
setValue() 是更新 LiveData 值时必然会调用的一个方法,即使是通过 postValue() 更新值,最终也会走这个方法。
LiveData 持有一个版本号字段,用于标识“值的版本”,就像软件版本号一样,这个数字用于判断“当前值是否是最新的”,若版本号小于最新版本号,则表示当前值需要更新。
LiveData 用一个 Object 字段mData存储了“值”。所以这个值会一直存在,直到被更新的值覆盖。
LiveData 分发值即是通知数据观察者:
public abstract class LiveData { // 用键值对方式持有一组数据观察者 private SafeIterableMap<Observer<? super T>, ObserverWrapper> mObservers = new SafeIterableMap<>(); void dispatchingValue(ObserverWrapper initiator) { ... // 指定分发给单个数据观察者 if (initiator != null) { considerNotify(initiator); initiator = null; } // 遍历所有数据观察者分发值 else { for (Iterator<Map.Entry<Observer<? super T>, ObserverWrapper>> iterator = mObservers.iteratorWithAdditions(); iterator.hasNext(); ) { considerNotify(iterator.next().getValue()); } } ... }
// 真正地分发值 private void considerNotify(ObserverWrapper observer) { // 1. 若观察者不活跃则不分发给它 if (!observer.mActive) { return; } // 2. 根据观察者绑定的生命周期再次判断它是否活跃,若不活跃则不分发给它 if (!observer.shouldBeActive()) { observer.activeStateChanged(false); return; } // 3. 若值已经是最新版本,则不分发 if (observer.mLastVersion >= mVersion) { return; } // 更新观察者的最新版本号 observer.mLastVersion = mVersion; // 真正地通知观察者 observer.mObserver.onChanged((T) mData); }
}
分发值有两种情况:“分发给单个观察者”和“分发给所有观察者”。当 LiveData 值更新时,需分发给所有观察者。
所有的观察者被存在一个 Map 结构中,分发的方式是通过遍历 Map 并逐个调用considerNotify()。在这个方法中需要跨过三道坎,才能真正地将值分发给数据观察者,分别是:
- 数据观察者是否活跃。
- 数据观察者绑定的生命周期组件是否活跃。
- 数据观察者的版本号是否是最新的。
跨过三道坎后,会将最新的版本号存储在观察者的 mLastVersion 字段中,即版本号除了保存在LiveData.mVersion,还会在每个观察者中保存一个副本mLastVersion,最后才将之前暂存的mData的值分发给数据观察者。
每个数据观察者都和一个组件的生命周期对象绑定(见第一节),当组件生命周期发生变化时,会尝试将最新值分发给该数据观察者。
每一个数据观察者都会被包装(见第一节),包装类型为ObserverWrapper:
// 原始数据观察者 public interface Observer { void onChanged(T t); }
// 观察者包装类型 private abstract class ObserverWrapper { // 持有原始数据观察者 final Observer<? super T> mObserver; // 当前观察者是否活跃 boolean mActive; // 当前观察者最新值版本号,初始值为 -1 int mLastVersion = START_VERSION; // 注入原始观察者 ObserverWrapper(Observer<? super T> observer) {mObserver = observer;} // 当数据观察者绑定的组件生命周期变化时,尝试将最新值分发给当前观察者 void activeStateChanged(boolean newActive) { // 若观察者活跃状态未变,则不分发值 if (newActive == mActive) { return; } // 更新活跃状态 mActive = newActive; // 若活跃,则将最新值分发给当前观察者 if (mActive) { dispatchingValue(this); } } // 是否活跃,供子类重写 abstract boolean shouldBeActive(); }
观察者的包装类型通过组合的方式持有了一个原始观察者,并在此基础上为其扩展了活跃状态和版本号的概念。
观察者包装类型是抽象的,是否活跃由子类定义:
class LifecycleBoundObserver extends ObserverWrapper implements LifecycleEventObserver { final LifecycleOwner mOwner;
LifecycleBoundObserver(LifecycleOwner owner, Observer<? super T> observer) { super(observer); mOwner = owner; }
// 当与观察者绑定的生命周期组件至少为STARTED时,表示观察者活跃 @Override boolean shouldBeActive() { return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED); }
@Override public void onStateChanged( LifecycleOwner source, Lifecycle.Event event) { Lifecycle.State currentState = mOwner.getLifecycle().getCurrentState(); // 当生命周期状态发生变化,则尝试将最新值分发给数据观察者 while (prevState != currentState) { prevState = currentState; // 调用父类方法,进行分发 activeStateChanged(shouldBeActive()); currentState = mOwner.getLifecycle().getCurrentState(); } } }
总结一下,LiveData 有两次机会通知观察者,与之对应的有两种分发值的方式:
- 当值更新时,遍历所有观察者将最新值分发给它们。
- 当与观察者绑定组件的生命周期发生变化时,将最新的值分发给指定观察者。
假设这样一种场景:LiveData 的值被更新了一次,随后它被添加了一个新的数据观察者,与之绑定组件的生命周期也正好发生了变化(变化到RESUMED),即数据更新在添加观察者之前,此时更新值会被分发到新的观察者吗?
会!首先,更新值会被存储在 mData 字段中。
其次,在添加观察者时会触发一次生命周期变化:
// androidx.lifecycle.LifecycleRegistry public void addObserver(@NonNull LifecycleObserver observer) { State initialState = mState == DESTROYED ? DESTROYED : INITIALIZED; ObserverWithState statefulObserver = new ObserverWithState(observer, initialState); ... // 将生命周期事件分发给新进的观察者 statefulObserver.dispatchEvent(lifecycleOwner, upEvent(statefulObserver.mState)); ... }
// LifecycleBoundObserver 又被包了一层 static class ObserverWithState { State mState; GenericLifecycleObserver mLifecycleObserver;
ObserverWithState(LifecycleObserver observer, State initialState) { mLifecycleObserver = Lifecycling.getCallback(observer); mState = initialState; }
void dispatchEvent(LifecycleOwner owner, Event event) { State newState = getStateAfter(event); mState = min(mState, newState); // 分发生命周期事件给 LifecycleBoundObserver mLifecycleObserver.onStateChanged(owner, event); mState = newState; } }
最后,这次尝试必然能跨过三道坎,因为新建观察者版本号总是小于 LiveData 的版本号(-1 < 0,LiveData.mVersion 经过一次值更新后自增为0)。
这种“新观察者”会被“老值”通知的现象称为粘性。
4. 粘性的 LiveData 会造成什么问题?怎么解决?
购物车-结算场景:假设有一个购物车界面,点击结算后跳转到结算界面,结算界面可以回退到购物车界面。这两个界面都是 Fragment。
结算界面和购物车界面通过共享ViewModel的方式共享商品列表:
class MyViewModel:ViewModel() { // 商品列表 val selectsListLiveData = MutableLiveData<List>() // 更新商品列表 fun setSelectsList(goods:List){ selectsListLiveData.value = goods } }
下面是俩 Fragment 界面依托的 Activity
class StickyLiveDataActivity : AppCompatActivity() { // 用 DSL 构建视图 private val contentView by lazy { ConstraintLayout { layout_id = "container" layout_width = match_parent layout_height = match_parent } }
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(contentView) // 加载购物车界面 supportFragmentManager.beginTransaction() .add("container".toLayoutId(), TrolleyFragment()) .commit() } }
其中使用了 DSL 方式声明性地构建了布局。
购物车页面如下:
class TrolleyFragment : Fragment() { // 获取与宿主 Activity 绑定的 ViewModel private val myViewModel by lazy { ViewModelProvider(requireActivity()).get(MyViewModel::class.java) }
override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { return ConstraintLayout { layout_width = match_parent layout_height = match_parent // 向购物车添加两件商品 onClick = { myViewModel.setSelectsList(listOf("meet","water")) }
TextView { layout_id = "balance" layout_width = wrap_content layout_height = wrap_content text = "balance" gravity = gravity_center // 跳转结算页面 onClick = { parentFragmentManager.beginTransaction() .replace("container".toLayoutId(), BalanceFragment()) .addToBackStack("trolley") .commit() } } } }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // 观察商品列表变化 myViewModel.selectsListLiveData.observe(viewLifecycleOwner) { goods -> // 若商品列表超过2件商品,则 toast 提示已满 goods.takeIf { it.size >= 2 }?.let { Toast.makeText(context,"购物车已满",Toast.LENGTH_LONG).show() } } } }
在 onViewCreated() 中观察购物车的变化,如果购物车超过 2 件商品,则 toast 提示。
下面是结算页面:
class BalanceFragment:Fragment() { private val myViewModel by lazy { ViewModelProvider(requireActivity()).get(MyViewModel::class.java) }
override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { return ConstraintLayout { layout_width = match_parent layout_height = match_parent } }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // 结算界面获取购物列表的方式也是观察商品 LiveData myViewModel.selectsListLiveData.observe(viewLifecycleOwner) {...} } }
跑一下 demo,当跳转到结算界面后,点击返回购物车,toast 会再次提示购物车已满。
因为在跳转结算页面之前,购物车列表 LiveData 已经被更新过。当购物车页面重新展示时,onViewCreated()会再次执行,这样一个新观察者被添加,因为 LiveData 是粘性的,所以上一次购物车列表会分发给新观察者,这样 toast 逻辑再一次被执行。
解决方案一:带消费记录的值
// 一次性值 open class OneShotValue(private val value: T) { // 值是否被消费 private var handled = false // 获取值,如果值未被处理则返回,否则返回空 fun getValue(): T? { return if (handled) { null } else { handled = true value } } // 获取上次被处理的值 fun peekValue(): T = value }
在值的外面套一层,新增一个标记位标识是否被处理过。
用这个方法重构下 ViewModel:
class MyViewModel:ViewModel() { // 已选物品列表 val selectsListLiveData = MutableLiveData<OneShotValue<List>>() // 更新已选物品 fun setSelectsList(goods:List){ selectsListLiveData.value = OneShotValue(goods) } }
观察购物车的逻辑也要做修改:
class TrolleyFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) myViewModel.selectsListLiveData.observe(viewLifecycleOwner) { goods -> goods.getValue()?.takeIf { it.size >= 2 }?.let { Toast.makeText(context,"购物车满了",Toast.LENGTH_LONG).show() } } } }
重复弹 toast 的问题是解决了,但引出了一个新的问题:当购物车满弹出 toast 时,购物车列表已经被消费掉了,导致结算界面就无法再消费了。
这时候只能用peekValue()来获取已经被消费的值:
class BalanceFragment:Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) myViewModel.selectsListLiveData.observe(viewLifecycleOwner) { val list = it.peekValue()// 使用 peekValue() 获取购物车列表 } } }
bug 全解完了。但不觉得这样处理有一些拧巴吗?
用“一次性值”封装 LiveData 的值,以去除其粘性。使用该方案得甄别出哪些观察者需要粘性值,哪些观察者需要非粘性事件。当观察者很多的时候,就很难招架了。若把需要粘性处理和非粘性处理的逻辑写在一个观察者中,就 GG,还得新建观察者将它们分开。
解决方案二:带有最新版本号的观察者
通知观察者前需要跨过三道坎(详见第三节),其中有一道坎是版本号的比对。若新建的观察者版本号小于最新版本号,则表示观察者落后了,需要将最新值分发给它。
LiveData 源码中,新建观察者的版本号总是 -1。
// 观察者包装类型 private abstract class ObserverWrapper { // 当前观察者最新值版本号,初始值为 -1 int mLastVersion = START_VERSION; ... }
若能够让新建观察者的版本号被最新版本号赋值,那版本号对比的那道坎就过不了,新值就无法分发到新建观察者。
所以得通过反射修改 mLastVersion 字段。
该方案除了倾入性强之外,把 LiveData 粘性彻底破坏了。但有的时候,我们还是想利用粘性的。。。
解决方案三:SingleLiveEvent
这是谷歌给出的一个解决方案
public class SingleLiveEvent extends MutableLiveData { // 标志位,用于表达值是否被消费 private final AtomicBoolean mPending = new AtomicBoolean(false);
public void observe(LifecycleOwner owner, final Observer observer) { // 中间观察者 super.observe(owner, new Observer() { @Override public void onChanged(@Nullable T t) { // 只有当值未被消费过时,才通知下游观察者 if (mPending.compareAndSet(true, false)) { observer.onChanged(t); } } }); }
public void setValue(@Nullable T t) { // 当值更新时,置标志位为 true mPending.set(true); super.setValue(t); }
public void call() { setValue(null); } }
专门设立一个 LiveData,它不具备粘性。它通过新增的“中间观察者”,拦截上游数据变化,然后在转发给下游。拦截之后通常可以做一点手脚,比如增加一个标记位mPending是否消费过的判断,若消费过则不转发给下游。
在数据驱动的 App 界面下,存在两种值:1. 非暂态数据 2. 暂态数据
demo 中用于提示“购物车已满”的数据就是“暂态数据”,这种数据是一次性的,转瞬即逝的,可以消费一次就扔掉。
demo 中购物车中的商品列表就是“非暂态数据”,它的生命周期要比暂态数据长一点,在购物车界面和结算界面存活的期间都应该能被重复消费。
SingleLiveEvent 的设计正是基于对数据的这种分类方法,即暂态数据使用 SingleLiveEvent,非暂态数据使用常规的 LiveData。
这样尘归尘土归土的解决方案是符合现实情况的。将 demo 改造一下:
class MyViewModel : ViewModel() { // 非暂态购物车列表 LiveData val selectsListLiveData = MutableLiveData<List>() // 暂态购物车列表 LiveData val singleListLiveData = SingleLiveEvent<List>() // 更新购物车列表,同时更新暂态和非暂态 fun setSelectsList(goods: List) { selectsListLiveData.value = goods singleListLiveData.value = goods } }
在购物车界面做相应的改动:
class TrolleyFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // 只观察非暂态购物车列表 myViewModel.singleListLiveData.observe(viewLifecycleOwner) { goods -> goods.takeIf { it.size >= 2 }?.let { Toast.makeText(context,"full",Toast.LENGTH_LONG).show() } } } }
但该方案有局限性,若为 SingleLiveEvent 添加多个观察者,则当第一个观察者消费了数据后,其他观察者就没机会消费了。因为mPending是所有观察者共享的。
解决方案也很简单,为每个中间观察者都持有是否消费过数据的标记位:
open class LiveEvent : MediatorLiveData() { // 持有多个中间观察者 private val observers = ArraySet<ObserverWrapper>()
@MainThread override fun observe(owner: LifecycleOwner, observer: Observer) { observers.find { it.observer === observer }?.let { _ -> return } // 构建中间观察者 val wrapper = ObserverWrapper(observer) observers.add(wrapper) super.observe(owner, wrapper) }
@MainThread override fun observeForever(observer: Observer) { observers.find { it.observer === observer }?.let { _ -> return } val wrapper = ObserverWrapper(observer) observers.add(wrapper) super.observeForever(wrapper) }
@MainThread override fun removeObserver(observer: Observer) { if (observer is ObserverWrapper && observers.remove(observer)) { super.removeObserver(observer) return } val iterator = observers.iterator() while (iterator.hasNext()) { val wrapper = iterator.next() if (wrapper.observer == observer) { iterator.remove() super.removeObserver(wrapper) break } } }
@MainThread override fun setValue(t: T?) { // 通知所有中间观察者,有新数据 observers.forEach { it.newValue() } super.setValue(t) }
// 中间观察者 private class ObserverWrapper(val observer: Observer) : Observer { // 标记当前观察者是否消费了数据 private var pending = false
override fun onChanged(t: T?) { // 保证只向下游观察者分发一次数据 if (pending) { pending = false observer.onChanged(t) } }
fun newValue() { pending = true } } }
解决方案四:Kotlin Flow
限于篇幅原因及主题的原因(主题是 LiveData),直接给出代码(当前做法有问题)
class MyViewModel : ViewModel() { // 商品列表流 val selectsListFlow = MutableSharedFlow<List>() // 更新商品列表 fun setSelectsList(goods: List) {
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!