android 狀態模式應用場景及分析

狀態模式

作爲java設計模式中常見的行爲型設計模式,一問到大家就說

知道嘛,就是上下文裏面切換狀態嘛,不同狀態幹不同事情嘛

那具體呢,怎樣個落地呢,又是這樣的說法

這個沒法用在我們項目裏,我們項目太大了,一改很麻煩。很多問題的,不適合

就巴拉巴拉一堆不知道或者不想落地到生產環境裏。平時學是學了,但是大家都知道技術這種東西,特別是程序員的事情,沒得投入生產環境進行有效產出,都是假技術。

好,我們上來百度搜一下“java 設計模式”,上網一搜,嘿嘿,一堆結果,大家都能找到,我們上一張圖先,哇,一看親媽爆炸還沒穿復活甲,這什麼玩意。
狀態模式UML圖
來,我給大家分析一下。演示類或者上下文類持有一個狀態的引用state,但是這個引用的類型是接口類型,該引用的具體賦值交給set方法進行。而這個接口到底定義了什麼,doAction行爲,那其實就是交給不同的擴展類進行擴展,不同的擴展類對於行爲方法有不同的實現
有的人一聽這不就是多態麼,又想到里氏替換。不,這是多態的一種體現,但不是里氏替換,這個叫依賴倒轉,因爲這裏不強調一個子類擴展後對父類原有功能的擴展。好扯遠了,說回現在這個類圖。
總的來看,這是一種典型的行爲型模式。什麼意思?即類的行爲是基於它的狀態改變的,但平時在生產環境中的應用又真的沒見到太多,今天在這裏我給大家深入淺出地分享一哈,便於大家改善一下自己的代碼質量

實際應用場景

談完了前面的理論讓我們現在進入生產環境,接下來我將模擬大家項目中常見情況,做咱們狀態模式的實際應用介紹.包括雙重狀態和多狀態模式的管理.掌聲有請,pia pia pia,爲什麼不是papapa?因爲水多,水生財。講了這些鋪墊,總算可以上代碼了

雙重狀態下綁定和解綁

在開發過程中,不知道大家是怎麼去防止一個服務或者廣播被重複綁定註冊的。或者說別的場景,應用在用戶未登錄的情況下,要使用某些功能需要跳轉到登錄界面,而不是具體的功能頁。這裏大家腦海裏已經有個大概的思路了,但我想在座的各位肯定見過這種代碼

private boolean mReceiverTag = false;
private void initBroadcastReceiver(Context context) {
        if (context == null || mReceiverTag) {
            return;
        }
        mNetworkConnectChangedReceiver = new NetworkConnectChangedReceiver();
        IntentFilter filter = new IntentFilter();
        filter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
        filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
        context.registerReceiver(mNetworkConnectChangedReceiver, filter);
        mReceiverTag = true;
    }

是不是覺得似曾相識?用一個變量來作爲標記位。那麼同樣道理,怎麼防止重複解綁呢?鍵盤一頓操作

    public void unRegister() {
        if (mReceiverTag) {
            getActivity().unregisterReceiver(mNetworkConnectChangedReceiver);
			mReceiverTag = false;
        }
    }

爽不爽,是挺爽的,代碼簡單易懂

場景分析

現在我們來看下這種方案的一個優點,綁定與否只向一個標記位變量進行get獲取,不需要關注太多。但是隨之的問題就來,我註冊綁定時候改變,註銷解綁的時候也需要做對應改變。如果中間有其他操作的話,我也需要改變,那變來變去,就出現個問題,萬一我哪天發現出問題了,我就一個一個情況去斷點,看什麼時候會導致這個變量的標記變了,導致重複綁定解綁。是能解決問題,但是,太慢了。那原本今天的活怎麼辦,沒辦法加班,一天又那麼過去自己也累。那麼怎麼解決這種問題,狀態模式!!!

解決方案

定義一個狀態基類或者接口

推薦用接口方便解耦。從這裏看就有一個註冊和綁定的行爲。我們將這個接口對象交給上下文去持有,也就是上下文中會持有一個IRegisteredState類型的變量

public interface IRegisteredState {
    void registerReceiver(Context context, BroadcastReceiver receiver);

    void unregisterReceiver(Context context, BroadcastReceiver receiver);
}

到這裏我們可以開始相關的行爲,在該執行註冊的時候調用對應registerReceiver方法,註銷同理。但是這裏會問,不是沒賦值麼,NPE了。別急,這裏我們就來說說賦值,前面提到我們依靠set方法對這個引用進行賦值,那賦的值就應該是IRegisteredState接口的的擴展對象。再定義一個RegistedState跟UnRegistedState類,對對應的行爲方法進行實現。

狀態接口擴展
public class UnRegistedState implements IRegisteredState {
    @Override
    public void registerReceiver(Context context, BroadcastReceiver receiver) {
        IntentFilter filter = new IntentFilter();
        filter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
        filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
        context.registerReceiver(receiver, filter);
    }

    @Override
    public void unregisterReceiver(Context context, BroadcastReceiver receiver) {
        LogUtil.log("unregisterReceiver","未註冊狀態下對解注操作忽略");
    }
}

public class RegistedState implements IRegisteredState {
    @Override
    public void registerReceiver(Context context, BroadcastReceiver receiver) {
        LogUtil.log("unregisterReceiver","註冊狀態下對綁定操作忽略");
    }

    @Override
    public void unregisterReceiver(Context context,BroadcastReceiver receiver) {
        context.unregisterReceiver(receiver);
    }
}

那麼到這裏我想你猜的七七八八了,就是在未綁定的情況下對IRegisteredState引用賦值UnRegistedState對象,如果已經綁定了,就賦值RegistedState對象。由不同的狀態對象去做該狀態的對應行爲。對於上下文對象本身而言,就只知道IRegisteredState引用進行了替換,對外的方法無論是registerReceiver還是unregisterReceiver,都是交給IRegisteredState引用去執行。具體行爲交給具體類。

對應時機替換狀態
public void registerReceiver(Context context) {
        if (context != null) {
            mReceiverState.registerReceiver(context, mNetworkConnectChangedReceiver);
            setReceiverState(new RegistedState());
        }
    }

    public void unregisterReceiver(Context context) {
        if (context !=null){
            mReceiverState.unregisterReceiver(context, mNetworkConnectChangedReceiver);
            setReceiverState(new UnRegistedState());
        }
    }

那麼從上面的邏輯看,當我們完成一次註冊以後,多次執行綁定,由於處理對象已經發生改變,所以會得到的結果就是“註冊狀態下對綁定操作忽略”。多次註銷同理得到"未註冊狀態下對解注操作忽略"。這樣依賴我們只需要關注State引用的值是否是對應當前狀態,該值內部是否做的是對應當前狀態的行爲即可。不需要再使用標記變量去引導當前的邏輯判斷。畢竟狀態模式的特點就是幫助消除多餘的if…else 等條件選擇語句

多重狀態下界面更新

一樣迴歸到開發過程中,當我們不再只是上面只管理開關狀態,而是需要大量的狀態並同時根據狀態的改變來更新UI時候。狀態模式也是個好的方案。比如說,當我們從文章或者個人資料的閱讀模式,變成編輯模式,有的人會選擇啓動一個新的activity或者fragment,或者稍加考慮會選擇打開一個大點的dialog來解決。又比如說,項目裏面常常有加載中,加載完成,空狀態和錯誤狀態的做法。怎麼做的呢,來我曬一下大概的代碼,具體大家腦部。

    override fun showLoading() = activity?.runOnUiThread {
        LogUtils.i("${this::class.java.simpleName} showLoading")
        (viewModel as BaseViewModel<*>).loadingState.postValue(true)
        (viewModel as BaseViewModel<*>).loadedState.postValue(false)
        (viewModel as BaseViewModel<*>).emptyState.postValue(false)
        (viewModel as BaseViewModel<*>).errorState.postValue(false)
    }

    override fun showContentView() = activity?.runOnUiThread {
        LogUtils.i("${this::class.java.simpleName} showContentView")
        (viewModel as BaseViewModel<*>).loadingState.postValue(false)
        (viewModel as BaseViewModel<*>).loadedState.postValue(true)
        (viewModel as BaseViewModel<*>).emptyState.postValue(false)
        (viewModel as BaseViewModel<*>).errorState.postValue(false)
    }

    override fun showError() = activity?.runOnUiThread {
        LogUtils.i("${this::class.java.simpleName} showError")
        (viewModel as BaseViewModel<*>).loadingState.postValue(false)
        (viewModel as BaseViewModel<*>).loadedState.postValue(false)
        (viewModel as BaseViewModel<*>).emptyState.postValue(false)
        (viewModel as BaseViewModel<*>).errorState.postValue(true)
    }

    override fun showEmpty() = activity?.runOnUiThread {
        LogUtils.i("${this::class.java.simpleName} showEmpty")
        (viewModel as BaseViewModel<*>).loadingState.postValue(false)
        (viewModel as BaseViewModel<*>).loadedState.postValue(false)
        (viewModel as BaseViewModel<*>).emptyState.postValue(true)
        (viewModel as BaseViewModel<*>).errorState.postValue(false)
    }

場景分析

方案本身沒有問題,終歸還是有更好的解決方案。乍看是不是也覺得很正常,當展示時候切換對應狀態的控件展示或者隱藏,leader一問起來還振振有詞

沒辦法就是狀態太多了我們得想辦法優化

但是,如果我狀態多了起來,我有10個,那是不是我得有10個方法,而這10個方法裏要對10個狀態下的view做處理,複雜度10*10?或者說我不小心寫漏了show或者hide,我就要去一個個看,是寫漏了還是寫錯了。

解決方案

要知道,我們是面向對象的思想,一切皆對象。怎麼解決,這裏我用前陣子重構公司項目的一個方案作爲例子給大家分析一下。多重狀態的管理,核心是依賴十六進制進行狀態集管理,切換模式使用狀態集,子狀態判斷是否屬於該狀態來進行子狀態的對應行爲。拿UI管理來說,就是根據當前狀態集是否包含當前狀態來做對應的UI切換。前言鋪墊完,再次上代碼

爲什麼是十六進制

這裏借鑑掘金上的一篇文章對十六進制應用的實踐講解就算不去火星種土豆,也請務必掌握的 Android 狀態管理最佳實踐。因爲當時就是因爲看了這篇纔有了對項目實踐的衝動,十分感謝大佬的分享。

十六進制可以做到

  • 通過狀態集的注入,一行代碼即可完成模式的切換。
  • 無論再多的狀態,都只需要一個字段來存儲。狀態被存放在 int 類型的狀態集中,可以直接向數據庫寫入或讀取。

例如 0x0001,0x0002,而十六進制的計算,我們可以藉助二進制的 “按位計算” 方式來理解,即 與、或、異或、取反等

a & b,a | b,a ^ b,~a

十六進制數 0x0004 | 0x0008,可以理解爲

0100
|
1000
=
1100

十六進制 (0x0004 | 0x0008) & 0x0004 可以得到

1100
&
0100
=
0100

所以狀態集中包含某狀態時,再與上該狀態,就會得到非 0 的結果,從而利用這個特性來完成狀態管理

定義狀態並加入狀態集

首先我們需要定義各個狀態,用剛學一陣子的kotlin作爲示範,肯定會寫的不太優雅,但是如果寫得不好我將在後面的博客中將這段代碼作爲重構的一個例子,希望大家可以看到一起交流,最後實現代碼質量的優化。好迴歸正題。
例如說這裏我有4個基本狀態,對應的控件有展示或者隱藏,那麼這裏有8個子狀態。

companion object {
        const val SHOW_LOADING = 0x00000001
        const val HIDE_LOADING: Int = 0x00000001.shl(1)
        const val SHOW_ERROR: Int = 0x00000001.shl(2)
        const val HIDE_ERROR: Int = 0x00000001.shl(3)
        const val SHOW_EMPTY: Int = 0x00000001.shl(4)
        const val HIDE_EMPTY: Int = 0x00000001.shl(5)
        const val SHOW_CONTENT: Int = 0x00000001.shl(6)
        const val HIDE_CONTENT: Int = 0x00000001.shl(7)

        const val LOADING: Int = (SHOW_LOADING or HIDE_ERROR or HIDE_EMPTY or HIDE_CONTENT)
        const val LOADED: Int = (HIDE_LOADING or HIDE_ERROR or HIDE_EMPTY or SHOW_CONTENT)
        const val EMPTY: Int = (HIDE_LOADING or HIDE_ERROR or SHOW_EMPTY or HIDE_CONTENT)
        const val ERROR: Int = (HIDE_LOADING or SHOW_ERROR or HIDE_EMPTY or HIDE_CONTENT)
    }

這裏對於狀態集的操作上,添加子集使用或,此處kotlin的or對應java的|,移除使用取反,即kotlin的inv對應java的&~。

定義各個子狀態下所需要的行爲

如果是控件操作,那麼就是根據各個子狀態對應來做UI的隱藏顯示。大概是這樣的,這裏用了jetpack裏的livedata來實現UI更新,但是換成mvp模式裏的也是對應的V層回調,細節忽略,咱們注意一下思路就可以了


    internal val loadingState: MutableLiveData<Boolean> = MutableLiveData()
    internal val errorState: MutableLiveData<Boolean> = MutableLiveData()
    internal val emptyState: MutableLiveData<Boolean> = MutableLiveData()
    internal val loadedState: MutableLiveData<Boolean> = MutableLiveData()
	(viewModel as BaseViewModel<*>).apply {
            loadingState.observe(this@AbsMvvmFragment, Observer { shouldShow ->
                shouldShow?.run {
                    activity?.runOnUiThread {
                        loadingView?.apply {
                            visibility = if (shouldShow) View.VISIBLE else View.GONE
                        }
                    }
                }
            })

            loadedState.observe(this@AbsMvvmFragment, Observer { shouldShow ->
                shouldShow?.run {
                    activity?.runOnUiThread {
                        dataBinding.root.apply {
                            visibility = if (shouldShow) View.VISIBLE else View.GONE
                        }
                    }
                }
            })

            emptyState.observe(this@AbsMvvmFragment, Observer { shouldShow ->
                shouldShow?.run {
                    activity?.runOnUiThread {
                        emptyView?.apply {
                            visibility = if (shouldShow) View.VISIBLE else View.GONE
                        }
                    }
                }
            })

            errorState.observe(this@AbsMvvmFragment, Observer { shouldShow ->
                shouldShow?.run {
                    activity?.runOnUiThread {
                        errorView?.apply {
                            visibility = if (shouldShow) View.VISIBLE else View.GONE
                        }
                    }
                }
            })
        }

在完成了各子狀態對應的控件操作之後。開始留下問題,這裏還是跟大的狀態集沒什麼聯繫呀。來,這裏開始進行對狀態集的管理

管理狀態集

同樣,我們管理這個狀態。什麼意思,當狀態改變的時候,我們判斷子狀態是否屬於這個狀態集,來確定該狀態集對應的子狀態操作

internal val pageState: MutableLiveData<Int> = MutableLiveData()

(viewModel as BaseViewModel<*>).apply {
            pageState.observe(this@AbsMvvmFragment, observerStatus())
        }

fun observerStatus(): Observer<Int> = Observer { status ->
        status?.run {
            loadingState.postValue(statusEnabled(status, SHOW_LOADING))
            loadedState.postValue(statusEnabled(status, SHOW_CONTENT))
            emptyState.postValue(statusEnabled(status, SHOW_EMPTY))
            errorState.postValue(statusEnabled(status, SHOW_ERROR))
        }
    }

private fun statusEnabled(statuses: Int, status: Int): Boolean = (statuses and status) != 0

上面代碼意思就是狀態集本身發生改變的時候,如下面操作showLoading,showContentView方法對狀態集進行修改

override fun showLoading() = activity?.runOnUiThread {
        LogUtils.i("${this::class.java.simpleName} showLoading")
        (viewModel as BaseViewModel<*>).pageState.postValue(BaseViewModel.LOADING)
    }

    override fun showContentView() = activity?.runOnUiThread {
        LogUtils.i("${this::class.java.simpleName} showContentView")
        (viewModel as BaseViewModel<*>).pageState.postValue(BaseViewModel.LOADED)
    }

    override fun showError() = activity?.runOnUiThread {
        LogUtils.i("${this::class.java.simpleName} showError")
        (viewModel as BaseViewModel<*>).pageState.postValue(BaseViewModel.ERROR)
    }

    override fun showEmpty() = activity?.runOnUiThread {
        LogUtils.i("${this::class.java.simpleName} showEmpty")
        (viewModel as BaseViewModel<*>).pageState.postValue(BaseViewModel.EMPTY)
    }

對各個負責子狀態UI的對象進行回調,回調傳回的內容是什麼呢,就是子狀態的行爲是否在這個狀態集內。如加載狀態的展示與否(下面附上狀態集內容)取決於展示加載這個子狀態是否在當前發生變化的狀態集裏面。

const val LOADING: Int = (SHOW_LOADING or HIDE_ERROR or HIDE_EMPTY or HIDE_CONTENT)

如果是這個狀態集裏的子狀態,傳true,觸發前面的回調,loadingView顯示。其餘狀態完成判斷後也將判斷結果傳給對應的回調方法,以此完成各個視圖控件的更新。

loadingState.postValue(true)
?
(viewModel as BaseViewModel<*>).apply {
            loadingState.observe(this@AbsMvvmFragment, Observer { shouldShow ->
                shouldShow?.run {
                    activity?.runOnUiThread {
                        loadingView?.apply {
                            visibility = if (shouldShow) View.VISIBLE else View.GONE
                        }
                    }
                }
            })

這裏所體現出來的狀態模式思想,通過一個狀態集,來控制多個子狀態。相較以往的設置true跟false有什麼區別呢,從代碼上,肯定是逃不開set(true)或者set(false)的原理。不同的是,我們將各個view的設置歸於各個狀態,展示與否跟其他view的展示與否互不關聯。以前會出現在某個狀態下set(true)和set(false)混了的情況,現在我們只關注這個狀態下view該怎麼做即可。就算刪除某個子狀態的操作,也只需要完成子狀態的移除,不需要去每個操作view的地方進行移除

這裏使用簡單的頁面狀態來進行演示,如果是有更多的狀態,如網絡失敗,服務器失敗要求另外的展示,同理,將這個對應狀態添加到狀態集中或者創建新的狀態集,緊接着綁定該子狀態對應的UI操作即可。

優勢及缺點

經過前面的一番演示,可能對大家實現如何在生產環境中應用狀態模式有了一個更清楚的瞭解,知道如何落地,畢竟學了要有產出這個是我學習一切基礎的目的,也是鞏固所學的一個好方式。那現在我們來複盤一下狀態模式有什麼好處呢。
根據在菜鳥教程上所搜索到的下面這幾個點,大家都可以在這個講解中找到對照。

優勢

  • 封裝了轉換規則
  • 將所有與某個狀態有關的行爲放到一個類中,並且可以方便地增加新的狀態,只需要改變對象狀態即可改變對象的行爲(兩個例子都有體現)
  • 允許狀態轉換邏輯與狀態對象合成一體,而不是某一個巨大的條件語句塊(根據前面第一個例子)

缺點

  • 狀態模式的使用必然會增加系統類和對象的個數(狀態增加)
  • 對"開閉原則"的支持並不太好,對於可以切換狀態的狀態模式,增加新的狀態類需要修改那些負責狀態轉換的源代碼,否則無法切換到新增狀態,而且修改某個狀態類的行爲也需修改對應類的源代碼(狀態集改動)

好,希望上面的介紹能對大家有些許用戶,如果有哪些不夠清楚的地方也歡迎跟我討論,讓我做的更好,感謝。

本篇參考內容有,如果有侵權冒犯的地方請與我聯繫

https://www.runoob.com/design-pattern/state-pattern.html
https://juejin.im/post/5d1a148e6fb9a07ea6488ba3?utm_source=gold_browser_extension#heading-0

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章