Android Flux初體驗

前言

  • 在一次日常逛知乎的時候發(fā)現(xiàn)了Android Flux,相比與之前的開發(fā),自己還從未接觸到這樣的開發(fā)模式,于是經(jīng)過查閱資料后,自己動手嘗試體驗了一番,并加以總結(jié)記錄。

簡介

  • Flux源于Facebook在14年提出的一種Web前端架構(gòu),主要是用來處理復(fù)雜的UI邏輯的一致性問題。經(jīng)過實踐后發(fā)現(xiàn),這種架構(gòu)可以很好的應(yīng)用于Android平臺,相對于其他的MVC/MVP/MVVM等模式,擁有良好的文檔和更具體的設(shè)計,比較適合于快速開發(fā)實現(xiàn)。

核心思想

單向數(shù)據(jù)流

Android Flux
  • 如圖所示,F(xiàn)lux的核心思想是單向的數(shù)據(jù)流,所謂單向數(shù)據(jù)流,就是當用戶進行操作的時候,會從View層發(fā)出一個Action,這個Action通過Dispatcher流向Store里面,觸發(fā)Store對狀態(tài)進行改動,然后再由Store觸發(fā)新的狀態(tài)通知到View進行重新渲染。

Action

  • 用戶操作界面時會觸發(fā)一個Action,這個Action即是數(shù)據(jù)的封裝,Dispatcher會將這個Action分發(fā)到Store
  • Action的創(chuàng)建一般被封裝到一個有語義的Helper類,(ActionCreator),所謂有語義,就是根據(jù)不同的業(yè)務(wù)創(chuàng)建不同的Action,然后把這個Action傳遞到Dispatcher,進行Action的分發(fā)

Dispatcher

  • 一個應(yīng)用中只有一個Dispatcher,Dispatcher管理所有的數(shù)據(jù)流,(即內(nèi)部管理的是所有的Store),它是Action與Store聯(lián)系的中心樞紐
  • 實際上,它管理的是Store注冊的一系列回調(diào)接口,本身沒有其他的邏輯,僅僅是把Action分發(fā)到Store
  • 由此可知,所有的數(shù)據(jù)流必須從這里經(jīng)過,依次分發(fā)到已注冊的Store中

Store

  • Store包含應(yīng)用的狀態(tài)和邏輯,通常在此類中實現(xiàn)對Action的邏輯處理,并把處理的結(jié)果通知到View中
  • Store會把自己注冊在Dispatcher上并提供一個回調(diào)的接口,用于處理Action,當Dispatcher分發(fā)Action時,內(nèi)部管理的Store就會回調(diào)這個用于處理Action的接口
  • 通過Dispatcher發(fā)送Action來更新Store的內(nèi)部狀態(tài),當Store更新后,它會發(fā)送一個事件聲明自己的狀態(tài)已經(jīng)發(fā)送了改變,(通常是View接收),然后View會讀取這些變化并更新自己

View

  • 即是Activity和Fragment,負責監(jiān)聽Store發(fā)送的事件并更新界面
  • 通常一個Activity對應(yīng)一個Store,但是如果Activity包含許多Fragment,也可以讓每個Fragment對應(yīng)自己的Store

代碼實踐

  • 本項目模擬了一個登陸的過程,Activity由兩個EditText和一個Button組成,當username和password均為123時,模擬登陸成功;否則模擬登陸失敗。
Flux Test

Action

/**
 * 簡單的POJO類型,只提供兩個字段:type 和 data, 分別記錄Action的類型和數(shù)據(jù)
 * Created by ckerv on 16/12/4.
 */
public class Action<T> {

    private String type;

    private T data;

    public Action(String type, T data) {
        this.type = type;
        this.data = data;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}
/**
 * Created by ckerv on 16/12/5.
 */
public class LoginAction extends Action<LoginBean> {

    public static final String LOGIN_ACTION = "login.action";

    public LoginAction(String type, LoginBean data) {
        super(type, data);
    }
}
  • Action是簡單的POJO類型,采用泛型對數(shù)據(jù)進行封裝,只提供了兩個字段 typedata ,分別記錄Action的類型和數(shù)據(jù)
  • LoginAction是Action的具體業(yè)務(wù)實現(xiàn),實現(xiàn)非常簡單,只添加了一個Action類型字段 LOGIN_ACTION

Dispatcher

/**
 * 一個app通常只有一個Dispatcher類,內(nèi)部進行對Store的管理
 * 此類的作用是將action分發(fā)到store,是連接action和store的中心樞紐
 * Created by ckerv on 16/12/4.
 */
public class Dispatcher {

    private static Dispatcher INSTANCE;

    private List<Store> mStores;

    private Dispatcher() {
        mStores = new ArrayList<>();
    }

    public static Dispatcher getInstance() {
        if(INSTANCE == null) {
            synchronized (Dispatcher.class) {
                if(INSTANCE == null) {
                    INSTANCE = new Dispatcher();
                }
            }
        }
        return INSTANCE;
    }

    /**
     * 注冊store,即把store添加到list中
     * @param store
     */
    public void register(Store store) {
        this.mStores.add(store);
    }

    /**
     * 注銷store,從list中移除
     * @param store
     */
    public void unRegister(Store store) {
        this.mStores.remove(store);
    }

    /**
     * 分發(fā)action
     * @param action
     */
    public void dispatch(Action action) {
        post(action);
    }

    /**
     * 向注冊的store list依次分發(fā)acion
     * @param action
     */
    private void post(Action action) {
        for (Store store : mStores) {
            store.onAction(action);
        }
    }
}
  • Store會在這里注冊自己的回調(diào)接口,Dispatcher會把Action分發(fā)到注冊的Store,所以它會提供一些公有方法來注冊監(jiān)聽和分發(fā)消息
  • Dispatcher對外僅暴露3個公有方法:
    • register(final Store store) 用來注冊每個Store的回調(diào)接口
    • unregister(final Store store) 用來接觸Store的回調(diào)接口
    • dispatch(Action action) 用來觸發(fā)Store注冊的回調(diào)接口
  • 用一個ArrayList來管理Stores,對于一個更復(fù)雜的app可能需要精心設(shè)計store所管理的數(shù)據(jù)結(jié)構(gòu)

Store

/**
 * 處理action的基類,通常可根據(jù)不同的業(yè)務(wù)邏輯實現(xiàn){@link #onAction(Action)}
 * 通過EventBus傳遞數(shù)據(jù)
 * Created by ckerv on 16/12/4.
 */
public abstract class Store {

    private static final EventBus mBus = EventBus.getDefault();

    protected Store() {

    }

    public void register(Object view) {
        this.mBus.register(view);
    }

    public void unRegister(Object view) {
        this.mBus.unregister(view);
    }

    /**
     * post事件到view層,進行UI的后續(xù)處理
     */
    protected void emitStoreChange() {
        this.mBus.post(changeEvent());
    }


    /**
     * 處理Action的邏輯,子類必須實現(xiàn)此方法
     * @param action
     */
    public abstract void onAction(Action action);

    /**
     * post事件
     * @return
     */
    protected abstract StoreChangeEvent changeEvent();

    public class StoreChangeEvent{}
}
  • 這里Store是個抽象類,處理具體業(yè)務(wù)邏輯的子類必須實現(xiàn)onAction(Action action)changeEvent()方法
  • 采用EventBus來向view發(fā)送事件,因此必須提供register(Object view)unRegister(Object view)方法給外部注冊和注銷EventBus

LoginStore

/**
 * Created by ckerv on 16/12/5.
 */
public class LoginStore extends Store {

    public LoginStore() {
        super();
        loginResponseBean = new LoginResponseBean();
    }

    private LoginResponseBean loginResponseBean;

    public LoginResponseBean getLoginResponseBean() {
        return loginResponseBean;
    }

    @Override
    public void onAction(Action action) {
        switch (action.getType()) {
            case LoginAction.LOGIN_ACTION:
                LoginBean loginBean = (LoginBean) action.getData();
                if(loginBean.getUserName().equals("123") && loginBean.getPassWord().equals("123")) {
                    loginResponseBean.setSuccess(true);
                } else {
                    loginResponseBean.setSuccess(false);
                }
                break;
            default:break;
        }
        emitStoreChange();
    }

    @Override
    protected StoreChangeEvent changeEvent() {
        return new StoreChangeEvent();
    }
}
  • 處理登錄邏輯的LoginStore類,繼承自Store,可以看到,在onAction(Action)進行登錄邏輯的驗證
  • 在登錄邏輯的處理過程中,通過改變LoginReponseBean的狀態(tài)來區(qū)分登錄成功與否,處理完邏輯之后,調(diào)用父類的emitStoreChange()方法發(fā)送事件通知view更新
  • 請注意,這里只提供LoginReponseBean的get方法,Store內(nèi)部狀態(tài)的更新只能是通過Dispatcher改變

MainActivity

public class MainActivity extends AppCompatActivity implements View.OnClickListener{

    private EditText mEtUsername;
    private EditText mEtPassword;
    private Button mBtnLogin;

    private ActionCreator mActionCreator;
    private Dispatcher mDispatcher;
    private LoginStore mStore;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initVariables();
        initViews();

    }

    private void initVariables() {
        mStore = new LoginStore();
        mDispatcher = Dispatcher.getInstance();
        mActionCreator = ActionCreator.getInstance(mDispatcher);
        mDispatcher.register(mStore);
    }

    @Override
    protected void onResume() {
        super.onResume();
        mStore.register(this);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mStore.unRegister(this);
    }

    private void initViews() {

        mEtUsername = (EditText) findViewById(R.id.main_et_username);
        mBtnLogin = (Button) findViewById(R.id.main_btn_login);
        mEtPassword = (EditText) findViewById(R.id.main_et_password);
        mBtnLogin.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.main_btn_login :
                mActionCreator.login(mEtUsername.getText().toString(), mEtPassword.getText().toString());
                break;
            default : break;
        }
    }

    @Subscribe
    public void onChangeEvent(Store.StoreChangeEvent changeEvent) {
       if(mStore.getLoginResponseBean().isSuccess()) {
           Toast.makeText(MainActivity.this, "登錄成功", Toast.LENGTH_LONG).show();
       } else {
           Toast.makeText(MainActivity.this, "登錄失敗", Toast.LENGTH_LONG).show();
       }
    }
}

  • 一般說來,一個Activity有多少個業(yè)務(wù)就有多少個Store
  • initVariables()進行Store、Dispatcher和ActionCreator的初始化和注冊
  • onResume()onDestroy()方法里進行EventBus的注冊和注銷
  • 當用戶點擊登錄按鈕時,由ActionCreator調(diào)用具體的業(yè)務(wù)方法login(String username, String password)使Dispatcher分發(fā)LoginAction到LoginStore,當LoginStore處理完LoginAction發(fā)送事件到MainActivity,MainActivity層接收到事件,獲取LoginStore的狀態(tài),更新界面。

流程總結(jié)

流程總結(jié)

小結(jié)

  • 以上是我對Android Flux開發(fā)模式的第一次實踐,可以看到,采用Android Flux的開發(fā)模式有如下好處:
    • View層只負責渲染界面和觸發(fā)Action,具體業(yè)務(wù)邏輯由Store實現(xiàn),高度解耦
    • 要理解一個Store可能發(fā)生的狀態(tài)變化,只看onAction(Action action)中的邏輯處理即可
    • 由于數(shù)據(jù)(Action)是單向流動,因此Debug的時候變得輕松很多,可以快速定位Bug的發(fā)生地點
  • 以上是個人對于Anroid Flux的理解 ,有不足之處望指出一起討論,一起學(xué)習(xí)~O(∩_∩)O哈哈

關(guān)于Android Flux

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,698評論 6 539
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,202評論 3 426
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,742評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,580評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,297評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,688評論 1 327
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,693評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,875評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,438評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,183評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,384評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,931評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,612評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,022評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,297評論 1 292
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,093評論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,330評論 2 377

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,702評論 25 708
  • Android項目做了不少,難免遇到因為在項目架構(gòu)上設(shè)計不合理或者根本沒有形成統(tǒng)一的編程思想,導(dǎo)致各種意外的情況出...
    Forrest32閱讀 6,022評論 11 34
  • ##Flux與面向組件化開發(fā)首先要明確的是,F(xiàn)lux并不是一個前端框架,而是前端的一個設(shè)計模式,其把前端的一個交互...
    吳小蛆閱讀 319評論 0 0
  • 時至今日,我仍沒辦法像那些大牛一樣回憶高考時,說什么是人生中難忘又珍貴的一筆財富,回憶起來云淡風輕。高考于我而言是...
    浪矢老人閱讀 541評論 3 2
  • 感恩~孩子的好朋友cmh來找他玩,我們一起去河邊騎車聊天,度過了一個美好的晚上。 感恩~今天集中精力完成了半年總結(jié)...
    毛毛細雨mmxy閱讀 71評論 0 0