<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
不管是早期的MVC、MVP,還是最新的MVVM和MVI架構,這些框架一直解決的都是一個資料流的問題。一個良好的資料流框架,每一層的職責是單一的。例如,我們可以在表現層(Presentation Layer)的基礎上新增一個領域層(Domain Layer) 來儲存業務邏輯,使用資料層(Data Layer)對上層遮蔽資料來源(資料可能來自遠端服務,可能是本地資料庫)。
在Android中,一個典型的Android分層架構圖如下:
其中,我們需要重點看下Presenter 和 ViewModel, Presenter 和 ViewModel向 View 提供資料的機制是不同的。
目前,官方提供的可觀察的資料元件有LiveData、StateFlow和SharedFlow。可能大家對LiveData比較熟悉,配合ViewModel可以很方便的實現資料流的流轉。不過,LiveData也有很多常見的缺陷,並且使用場景也比較固定,如果網上出現了KotlinFlow 替代 LiveData的聲音。那麼 Flow 真的會替代 LiveData嗎?Flow 真的適合你的專案嗎?看完下面的分析後,你定會有所收穫。
ViewModel的作用是將檢視和邏輯進行分離,Activity或者Fragment只負責UI顯示部分,網路請求或者資料庫操作則有ViewModel負責。ViewModel旨在以注重生命週期的方式儲存和管理介面相關的資料,讓資料可在發生螢幕旋轉等設定更改後繼續留存。並且ViewModel不持有View層的範例,通過LiveData與Activity或者Fragment通訊,不需要擔心潛在的記憶體漏失問題。
而LiveData 則是一種可觀察的資料記憶體類,與常規的可觀察類不同,LiveData 具有生命週期感知能力,它遵循其他應用元件(如 Activity、Fragment 或 Service)的生命週期。這種感知能力可確保LiveData當資料來源發生變化的時候,通知它的觀察者更新UI介面。同時它只會通知處於Active狀態的觀察者更新介面,如果某個觀察者的狀態處於Paused或Destroyed時那麼它將不會收到通知,所以不用擔心記憶體漏失問題。
下面是官方釋出的架構元件庫的生命週期的說明:
通過前面的介紹可以知道,LiveData 是 Android Jetpack Lifecycle 元件中的內容,具有生命週期感知能力。一句話概括就是:LiveData 是可感知生命週期的,可觀察的,資料持有者。
特點如下:
因為LiveData 是被用來更新 UI的,因此 Observer 介面的 onChanged() 方法必須在主執行緒回撥。
public interface Observer<T> { void onChanged(T t); }
背後的道理也很簡單,LiveData 的 setValue() 發生在主執行緒(非主執行緒呼叫會拋異常),而如果呼叫postValue()方法,則它的內部會切換到主執行緒呼叫 setValue()。
protected void postValue(T value) { boolean postTask; synchronized (mDataLock) { postTask = mPendingData == NOT_SET; mPendingData = value; } if (!postTask) { return; } ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable); }
可以看到,postValue()方法的內部呼叫了postToMainThread()實現執行緒的切換,之後遍歷所有觀察者的 onChanged() 方法。
作為資料持有者,LiveData僅持有【單個且最新】的資料。單個且最新,意味著 LiveData 每次只能持有一個資料,如果有新資料則會覆蓋上一個。並且,由於LiveData具備生命週期感知能力,所以觀察者只會在活躍狀態下(STARTED 到 RESUMED)才會接收到 LiveData 最新的資料,在非活躍狀態下則不會收到。
可感知生命週期的重要優勢就是可以自動取消訂閱,這意味著開發者無需手動編寫那些取消訂閱的模板程式碼,降低了記憶體漏失的可能性。背後的實現邏輯是在生命週期處於 DESTROYED 時,移除觀察者。
@Override public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) { Lifecycle.State currentState = mOwner.getLifecycle().getCurrentState(); if (currentState == DESTROYED) { removeObserver(mObserver); return; } ... //省略其他程式碼 }
LiveData 提供了setValue() 和 postValue()兩種方式來操作實體資料,而為了細化許可權,LiveData又提供了mutable(MutableLiveData) 和 immutable(LiveData) 兩個類,前者「可讀可寫」,後者則「僅可讀」。
LiveData 配合 DataBinding 可以實現更新資料自動驅動UI變化,如果使用「雙向繫結」還能實現 UI 變化影響資料的變化功能。
正如前面說的,LiveData有自己的使用場景,只有滿足使用場景才會最大限度的發揮它的功能,而下面這些則是在設計時將自帶的一些缺陷:
由於LiveData的getValue() 是可空的,所以在使用時應該注意判空,否則容易出現空指標的報錯。
@Nullable public T getValue() { Object data = mData; if (data != NOT_SET) { return (T) data; } return null; }
Fragment 呼叫 LiveData的observe() 方法時傳入 this 和 viewLifecycleOwner 的含義是不一樣的。因為Fragment與Fragment中的View的生命週期並不一致,有時候我們需要的讓observer感知Fragment中的View的生命週期而非Fragment。
粘性事件的定義是,發射的事件如果早於註冊,那麼註冊之後依然可以接收到的事件,這一現象稱為粘性事件。解決辦法是:將事件作為狀態的一部分,在事件被消費後,不再通知觀察者。推薦兩種解決方式:
當setValue()/postValue() 傳入相同的值且多次呼叫時,觀察者的 onChanged() 也會被多次呼叫。不過,嚴格來講,這也不算一個問題,我們只需要在呼叫 setValue()/postValue() 前判斷一下 vlaue 與之前是否相同即可。
有些時候,我們需要對從Repository 層得到的資料進行處理。例如,從資料庫獲得 User列表,我們需要根據 id 獲取某個 User, 那麼就需要用到MediatorLiveData 和 Transformatoins 來實現。
並且,map 和 switchMap 內部均是使用 MediatorLiveData的addSource() 方法實現的,而該方法會在主執行緒呼叫,使用不當會有效能問題。
@MainThread public <S> void addSource(@NonNull LiveData<S> source, @NonNull Observer<? super S> onChanged) { Source<S> e = new Source<>(source, onChanged); Source<?> existing = mSources.putIfAbsent(source, e); if (existing != null && existing.mObserver != onChanged) { throw new IllegalArgumentException( "This source was already added with the different observer"); } if (existing != null) { return; } if (hasActiveObservers()) { e.plug(); } }
LiveData 是一種可觀察的資料記憶體類,與常規的可觀察類不同,LiveData 具有生命週期感知能力,它遵循其他應用元件(如 Activity、Fragment 或 Service)的生命週期。這種感知能力可確保LiveData當資料來源發生變化的時候,通知它的觀察者更新UI介面。同時它只會通知處於Active狀態的觀察者更新介面,如果某個觀察者的狀態處於Paused或Destroyed時那麼它將不會收到通知,所以不用擔心記憶體漏失問題。
同時,LiveData 專注單一功能,因此它的一些方法使用上是有侷限性的,並且需要配合 ViewModel 使用才能顯示其價值。
Flow是Google官方提供的一套基於kotlin協程的響應式程式設計模型,它與RxJava的使用類似,但相比之下Flow使用起來更簡單,另外Flow作用在協程內,可以與協程的生命週期繫結,當協程取消時,Flow也會被取消,避免了記憶體漏失風險。
協程是輕量級的執行緒,本質上協程、執行緒都是服務於並行場景下,其中協程是共同作業式任務,執行緒是搶佔式任務。預設協程用來處理實時性不高的資料,請求到結果後整個協程就結束了。比如,有下面一個例子:
其中,紅框中需要展示的內容實時性不高,而需要互動的,比如轉發和點贊屬於實時性很高的資料需要定時重新整理。對於實時性不高的場景,直接使用 Kotlin 的協程處理即可,比如。
suspend fun loadData(): Data uiScope.launch { val data = loadData() updateUI(data) }
而對於實時性要求較高的場景,上面的方式就不起作用了,此時需要用到Kotlin提供的Flow資料流。
fun dataStream(): Flow<Data>uiScope.launch { dataStream().collect { data -> updateUI(data) } }
Kotlin的資料流主要由三個成員組成,分別是生產者、消費者和中介。 生產者:生成新增到資料流中的資料,可以配合得協程使用,使用非同步方式生成資料。 中介(可選):可以修改傳送到資料流的值,或修正資料流本身。 消費者:使用方則使用資料流中的值。
其中,中介可以對資料流中的資料進行更改,甚至可以更改資料流本身,他們的架構示意圖如下。
在Kotlin中,Flow 是一種冷流,不過有一種特殊的Flow( StateFlow/SharedFlow) 是熱流。什麼是冷流,他和熱流又有什麼關係呢?
冷流:只有訂閱者訂閱時,才開始執行發射資料流的程式碼。並且冷流和訂閱者只能是一對一的關係,當有多個不同的訂閱者時,訊息是重新完整傳送的。也就是說對冷流而言,有多個訂閱者的時候,他們各自的事件是獨立的。 熱流:無論有沒有訂閱者訂閱,事件始終都會發生。當 熱流有多個訂閱者時,熱流與訂閱者們的關係是一對多的關係,可以與多個訂閱者共用資訊。
前面說過,冷流和訂閱者只能是一對一的關係,當我們要實現一個流多個訂閱者的場景時,就需要使用熱流了。
StateFlow 是一個狀態容器式可觀察資料流,可以向其收集器發出當前狀態更新和新狀態更新。可以通過其 value 屬性讀取當前狀態值,如需更新狀態並將其傳送到資料流,那麼就需要使用MutableStateFlow。
在Android 中,StateFlow 非常適合需要讓可變狀態保持可觀察的類。由於StateFlow並不是系統API,所以使用前需要新增依賴:
dependencies { ... //省略其他 implementation "androidx.activity:activity-ktx:1.3.1" implementation "androidx.fragment:fragment-ktx:1.4.1" implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1' }
接著,我們需要建立一個ViewModel,比如:
class StateFlowViewModel: ViewModel() { val data = MutableStateFlow<Int>(0) fun add(v: View) { data.value++ } fun del(v: View) { data.value-- } }
可以看到,我們使用MutableStateFlow包裹需要操作的資料,並新增了add()和del()兩個方法。然後,我們再編寫一段測試程式碼實現資料的修改,並自動重新整理資料。
class StateFlowActivity : AppCompatActivity() { private val viewModel by viewModels<StateFlowViewModel>() private val mBinding : ActivityStateFlowBinding by lazy { ActivityStateFlowBinding.inflate(layoutInflater) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(mBinding.root) initFlow() } private fun initFlow() { mBinding.apply { btnAdd.setOnClickListener { viewModel.add(it) } btnDel.setOnClickListener { viewModel.del(it) } } } }
上面程式碼中涉及到的佈局程式碼如下:
<layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <data> <variable name="stateFlowViewModel" type="com.xzh.demo.flow.StateFlowViewModel" /> </data> <FrameLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="200dp" android:layout_marginTop="30dp" android:text="@{String.valueOf(stateFlowViewModel.data)}" android:textSize="24sp" /> <com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/btn_add" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|start" android:layout_marginStart="10dp" android:layout_marginBottom="10dp" android:contentDescription="start" android:src="@android:drawable/ic_input_add" /> <com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/btn_del" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|end" android:layout_marginEnd="10dp" android:layout_marginBottom="10dp" android:contentDescription="cancel" android:src="@android:drawable/ic_menu_close_clear_cancel" /> </FrameLayout> </layout>
上面程式碼中,我們使用了DataBing寫法,因此不需要再手動的繫結資料和重新整理資料。
SharedFlow提供了SharedFlow 與 MutableSharedFlow兩個版本,平時使用較多的是MutableSharedFlow。它們的區別是,SharedFlow可以保留歷史資料,MutableSharedFlow 沒有起始值,傳送資料時需要呼叫 emit()/tryEmit() 方法。
首先,我們來看看SharedFlow的建構函式:
public fun <T> MutableSharedFlow( replay: Int = 0, extraBufferCapacity: Int = 0, onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND ): MutableSharedFlow<T>
可以看到,MutableSharedFlow需要三個引數:
//ViewModel val sharedFlow=MutableSharedFlow<String>() viewModelScope.launch{ sharedFlow.emit("Hello") sharedFlow.emit("SharedFlow") } //Activity lifecycleScope.launch{ viewMode.sharedFlow.collect { print(it) } }
SharedFlow並不是系統API,所以使用前需要新增依賴:
dependencies { ... //省略其他 implementation "androidx.activity:activity-ktx:1.3.1" implementation "androidx.fragment:fragment-ktx:1.4.1" implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1' }
接下來,我們建立一個SharedFlow,由於需要一對多的進行通知,所以我們MutableSharedFlow,然後重寫postEvent()方法,
程式碼如下:
object LocalEventBus { private val events= MutableSharedFlow< Event>() suspend fun postEvent(event: Event){ events.emit(event) } } data class Event(val timestamp:Long)
接下來,我們再建立一個ViewModel,裡面新增startRefresh()和cancelRefresh()兩個方法,
如下:
class SharedViewModel: ViewModel() { private lateinit var job: Job fun startRefresh(){ job=viewModelScope.launch (Dispatchers.IO){ while (true){ LocalEventBus.postEvent(Event(System.currentTimeMillis())) } } } fun cancelRefresh(){ job.cancel() } }
前面說過,一個典型的Flow是由三部分構成的。所以,此處我們先新建一個用於資料消費的Fragment
程式碼如下:
class FlowFragment: Fragment() { private val mBinding : FragmentFlowBinding by lazy { FragmentFlowBinding.inflate(layoutInflater) } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { return mBinding.root } override fun onStart() { super.onStart() lifecycleScope.launchWhenCreated { LocalEventBus.events.collect { mBinding.tvShow.text=" ${it.timestamp}" } } } }
FlowFragment的主要作用就是接收LocalEventBus的資料,並顯示到檢視上。接下來,我們還需要建立一個資料的生產者,為了簡單,我們只在生產者頁面中開啟協程,
程式碼如下:
class FlowActivity : AppCompatActivity() { private val viewModel by viewModels<SharedViewModel>() private val mBinding : ActivityFlowBinding by lazy { ActivityFlowBinding.inflate(layoutInflater) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(mBinding.root) initFlow() } private fun initFlow() { mBinding.apply { btnStart.setOnClickListener { viewModel.startRefresh() } btnStop.setOnClickListener { viewModel.cancelRefresh() } } } }
其中,FlowActivity程式碼中涉及的佈局如下:
<layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <data> </data> <FrameLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".fragment.SharedFragment"> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <fragment android:name="com.xzh.demo.FlowFragment" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" /> </LinearLayout> <com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/btn_start" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|start" android:layout_marginStart="10dp" android:layout_marginBottom="10dp" android:src="@android:drawable/ic_input_add" android:contentDescription="start" /> <com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/btn_stop" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|end" android:layout_marginEnd="10dp" android:layout_marginBottom="10dp" android:src="@android:drawable/ic_menu_close_clear_cancel" android:contentDescription="cancel" /> </FrameLayout> </layout>
最後,當我們執行上面的程式碼時,就會在FlowFragment的頁面上顯示當前的時間戳,並且頁面的資料會自動進行重新整理。
前文說過,Kotlin的Flow是一種冷流,而StateFlow/SharedFlow則屬於熱流。那麼有人會問:怎麼將冷流轉化為熱流呢?答案就是kotlin提供的shareIn()和stateIn()兩個方法。
首先,來看一下StateFlow的shareIn的定義:
public fun <T> Flow<T>.stateIn( scope: CoroutineScope, started: SharingStarted, initialValue: T ): StateFlow<T>
shareIn方法將流轉換為SharedFlow,需要三個引數,我們重點看一下started引數,表示流啟動的條件,支援三種:
接下來,我們在看一下SharedFlow的shareIn的定義:
public fun <T> Flow<T>.shareIn( scope: CoroutineScope, started: SharingStarted, replay: Int = 0 ): SharedFlow<T>
此處,我們重點看下replay引數,該參數列示轉換為SharedFlow之後,當有新的訂閱者的時候傳送快取中值的個數。
從前文的介紹可以知道,StateFlow與SharedFlow都是熱流,都是為了滿足流的多個訂閱者的使用場景的,一時間讓人有些傻傻分不清,那StateFlow與SharedFlow究竟有什麼區別呢?總結起來,大概有以下幾點:
從上面的描述可以看出,StateFlow為我們做了一些預設的設定,而SharedFlow澤新增了一些預設約束。總的來說,SharedFlow相比StateFlow更靈活。
目前,官方提供的可觀察的資料元件有LiveData、StateFlow和SharedFlow。LiveData是Android早期的資料流元件,具有生命週期感知能力,需要配合ViewModel才能實現它的價值。不過,LiveData也有很多使用場景缺陷,常見的有粘性事件、不支援防抖等。
於是,Kotlin在1.4.0版本,陸續推出了StateFlow與SharedFlow兩個元件,StateFlow與SharedFlow都是熱流,都是為了滿足流的多個訂閱者的使用場景,不過它們也有微妙的區別,具體參考前面內容的說明。
到此這篇關於一文讀懂Android Kotlin的資料流的文章就介紹到這了,更多相關Android Kotlin內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45