Android架構(gòu)模式之MVC、MVP、MVVM

在開始講解各種架構(gòu)模式時(shí),我們先來(lái)看下沒(méi)有經(jīng)過(guò)設(shè)計(jì)的代碼是如何編寫的。為了不分散重點(diǎn),筆者舉的例子會(huì)比較簡(jiǎn)單,初始時(shí)從數(shù)據(jù)庫(kù)緩存中獲取用戶信息展示到界面上,點(diǎn)擊刷新按鈕可以從服務(wù)器上拉取最新的用戶信息并進(jìn)行展示。

由于從數(shù)據(jù)庫(kù)和服務(wù)器上獲取數(shù)據(jù)都屬于更底層的邏輯,因此這兩個(gè)操作一開始就會(huì)進(jìn)行封裝,不會(huì)列入討論范圍,并且為了使程序更加簡(jiǎn)單,這兩個(gè)操作都是使用的測(cè)試代碼進(jìn)行模擬。

User.java

// User實(shí)體類,再?zèng)]有封裝意識(shí)的人,實(shí)體類總會(huì)有一個(gè)吧
public class User {
    public String name;
    public int age;
}

DbUtils.java

// 數(shù)據(jù)庫(kù)工具
public class DbUtils {
    // 查詢數(shù)據(jù)庫(kù)記錄并返回cursor,這里使用測(cè)試代碼直接返回null
    public static Cursor query(String sql) {
        return null;
    }
    
    // 更新數(shù)據(jù)庫(kù)記錄,這里使用測(cè)試代碼不進(jìn)行任何實(shí)際處理
    public static void update(String sql) {
        
    }
}

HttpUtils.java

// http工具
public class HttpUtils {
    private static Handler sHandler = new Handler(Looper.getMainLooper());

    public interface ResponseCallback {
        void onResponseSuccessed(String json);
        void onResponseFailed(int reason);
    }

    // 發(fā)起http請(qǐng)求,這里使用模擬的數(shù)據(jù),并有一定機(jī)率請(qǐng)求失敗
    public static void request(Map params, final ResponseCallback callback) {
        sHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                int value = new Random(System.currentTimeMillis()).nextInt(5);
                if(callback != null) {
                    if(value == 2) {
                        callback.onResponseFailed(1);
                    } else {
                        callback.onResponseSuccessed("{\"name\": \"純爺們\", \"age\": 20}");
                    }
                }
            }
        }, 500);
    }
}

activity_user.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="10dp">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/tv_name"/>

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:id="@+id/tv_age"/>

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:layout_marginTop="20dp"
        android:text="刷新"
        android:id="@+id/btn_refresh"/>

</LinearLayout>

UserActivity.java

public class UserActivity extends Activity {
    private TextView mNameView;
    private TextView mAgeView;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_user);

        mNameView = (TextView)findViewById(R.id.tv_name);
        mAgeView = (TextView)findViewById(R.id.tv_age);
        
        // 加載緩存的用戶數(shù)據(jù)并展示
        User user = loadUser();
        if(user != null) {
            mNameView.setText("昵稱:" + user.name);
            mAgeView.setText("年齡:" + user.age);
        }

        findViewById(R.id.btn_refresh).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 從服務(wù)器拉取最新用戶數(shù)據(jù)并顯示
                refresh();
            }
        });
    }

    private User loadUser() {
        // 這里本應(yīng)從cursor中獲取數(shù)據(jù),但為求程序盡量簡(jiǎn)單,我們直接使用模擬數(shù)據(jù)。之所以要加入query這段代碼,是為了盡可能模擬真實(shí)的流程。
        Cursor cursor = DbUtils.query(null);
        if(cursor != null) {
            try {

            } catch (Exception e) {

            } finally {
                cursor.close();
            }
        }
        User user = new User();
        user.name = "小蝦米";
        user.age = 19;
        return user;
    }

    private void refresh() {
        HttpUtils.request(null, new HttpUtils.ResponseCallback() {
            @Override
            public void onResponseSuccessed(String json) {
                try {
                    User user = new Gson().fromJson(json, User.class);
                    
                    // 用戶信息更新了,要同步更新數(shù)據(jù)庫(kù)中的記錄
                    String updateSql = null;
                    DbUtils.update(updateSql);
                   
                    mNameView.setText("昵稱:" + user.name);
                    mAgeView.setText("年齡:" + user.age);
                } catch (Exception e) {
                }
            }

            @Override
            public void onResponseFailed(int reason) {
                Toast.makeText(UserActivity.this, "刷新失敗", Toast.LENGTH_SHORT).show();
            }
        });
    }
}

點(diǎn)擊刷新前顯示如下界面

image

點(diǎn)擊刷新后顯示如下界面
image

上面的例子請(qǐng)讀者務(wù)必記勞,后續(xù)講到的幾種架構(gòu)模式全部使用的都是這個(gè)例子。

上述例子一個(gè)非常突出的問(wèn)題是,用戶信息可能在多個(gè)界面上都需要顯示,而在這些界面上,從數(shù)據(jù)庫(kù)和服務(wù)器上獲取用戶信息的流程都要寫一遍,重復(fù)編寫不僅容易出錯(cuò)也不容易維護(hù)。解決該問(wèn)題的方法就是封裝一個(gè)可復(fù)用的Model,MXX模式也由此產(chǎn)生。

MVC

MVC由Model、View、Controller組成,Android提供的xml布局是View層的主要部分,View本身已經(jīng)是相對(duì)比較獨(dú)立的了,因此MVC中主要考慮的就是Model的設(shè)計(jì)。我們來(lái)看下MVC中各層在Android中的主要應(yīng)用:

  1. Model:表示模型層,模型的一個(gè)核心特點(diǎn)就是可復(fù)用。包含數(shù)據(jù)業(yè)務(wù)實(shí)體(entity/bean)本身,以及圍繞該實(shí)體進(jìn)行的業(yè)務(wù)操作(本地或遠(yuǎn)程增刪改查操作)。即Model層主要針對(duì)數(shù)據(jù),包含數(shù)據(jù)實(shí)體和數(shù)據(jù)訪問(wèn),如果非要以模型來(lái)稱呼和理解的話,前者為數(shù)據(jù)模型,后者為業(yè)務(wù)模型。
  2. View:表示視圖層,負(fù)責(zé)界面數(shù)據(jù)的展示,以及響應(yīng)用戶操作。
  3. Controller:表示控制層,負(fù)責(zé)邏輯處理,由其連接Model和View。Controller通過(guò)Model獲取數(shù)據(jù)并傳遞給View進(jìn)行展示;通過(guò)響應(yīng)View傳遞過(guò)來(lái)的用戶事件調(diào)用Model的接口進(jìn)行業(yè)務(wù)處理。

后續(xù)內(nèi)容都使用簡(jiǎn)稱,M代表Model,V代表View,C代表Controller。

其中,V和C一般又統(tǒng)稱為UI層,由于Android已經(jīng)提供了xml布局,因此在Android中V和C并不需要刻意區(qū)分,可以統(tǒng)一以UI層來(lái)理解,UI層的核心代碼包含xml和Activity(或Fragment,或另外封裝的控制器),后者既扮演著部分V的角色,又扮演著全部的C角色。我們來(lái)看下使用MVC重構(gòu)后的例子,增加了UserBusiness類,修改了UserActivity的代碼,以下只貼出更新的部分代碼,其余代碼請(qǐng)參考之前的例子。

UserBusiness.java

public class UserBusiness {
    private static final UserBusiness INSTANCE = new UserBusiness();

    private List<UserListener> mListeners = new LinkedList<>();

    public static UserBusiness get() {
        return INSTANCE;
    }

    public void addListener(UserListener listener) {
        if(listener == null) {
            return;
        }
        synchronized (mListeners) {
            if(!mListeners.contains(listener)) {
                mListeners.add(listener);
            }
        }
    }

    public void removeListener(UserListener listener) {
        if(listener == null) {
            return;
        }
        synchronized (mListeners) {
            mListeners.remove(listener);
        }
    }

    public User getUser() {
        Cursor cursor = DbUtils.query(null);
        if(cursor != null) {
            try {

            } catch (Exception e) {

            } finally {
                cursor.close();
            }
        }
        User user = new User();
        user.name = "小蝦米";
        user.age = 19;
        return user;
    }

    public void requestUser() {
        HttpUtils.request(null, new HttpUtils.ResponseCallback() {
            @Override
            public void onResponseSuccessed(String json) {
                User user = null;
                try {
                    user = new Gson().fromJson(json, User.class);
                } catch (Exception e) {
                }
                if(user != null) {
                    String updateSql = null;
                    DbUtils.update(updateSql);
                    notifyRequestUser(0, user);
                } else {
                    notifyRequestUser(1, null);
                }
            }

            @Override
            public void onResponseFailed(int reason) {
                notifyRequestUser(reason, null);
            }
        });
    }

    private void notifyRequestUser(int code, User user) {
        List<UserListener> listeners = new LinkedList<>();
        synchronized (mListeners) {
            listeners.addAll(mListeners);
        }
        for(UserListener listener : listeners) {
            listener.onRequestUserResult(code, user);
        }
    }

    public interface UserListener {
        void onRequestUserResult(int code, User user);
    }
}

UserActivity.java

public class UserActivity extends Activity implements UserBusiness.UserListener {
    private TextView mNameView;
    private TextView mAgeView;
    private UserBusiness mUserBusiness = UserBusiness.get();

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_user);

        mNameView = (TextView)findViewById(R.id.tv_name);
        mAgeView = (TextView)findViewById(R.id.tv_age);

        // 加載緩存的用戶數(shù)據(jù)并展示
        User user = mUserBusiness.getUser();
        if(user != null) {
            mNameView.setText("昵稱:" + user.name);
            mAgeView.setText("年齡:" + user.age);
        }

        findViewById(R.id.btn_refresh).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 從服務(wù)器拉取最新用戶數(shù)據(jù)并顯示
                mUserBusiness.requestUser();
            }
        });

        mUserBusiness.addListener(this);
    }

    @Override
    protected void onDestroy() {
        mUserBusiness.removeListener(this);
        super.onDestroy();
    }

    @Override
    public void onRequestUserResult(int code, User user) {
        if(code == 0) {
            mNameView.setText("昵稱:" + user.name);
            mAgeView.setText("年齡:" + user.age);
        } else {
            Toast.makeText(UserActivity.this, "刷新失敗", Toast.LENGTH_SHORT).show();
        }
    }
}

重構(gòu)后的代碼有如下優(yōu)點(diǎn):

  1. Activity的代碼變得簡(jiǎn)單和整潔了,Activity現(xiàn)在只需要處理控制邏輯(UI邏輯),以及作為V和M通信的橋梁。
  2. 業(yè)務(wù)代碼封裝在UserBusiness中,一是隱藏了數(shù)據(jù)操作(業(yè)務(wù)流程)的具體細(xì)節(jié),使得UI層在訪問(wèn)時(shí)更簡(jiǎn)單了;二是可以復(fù)用,任何模塊都可以輕松訪問(wèn),且可以通過(guò)在UserBusiness中注冊(cè)一個(gè)監(jiān)聽(tīng)器來(lái)監(jiān)聽(tīng)用戶業(yè)務(wù)的相關(guān)事件。

但MVC仍具有以下缺點(diǎn):

  1. V不可復(fù)用,然而在Android中V復(fù)用沒(méi)有意義,需要復(fù)用的話完全可以封裝可復(fù)用的控件,然后V組裝這些控件。
  2. C不可復(fù)用。
  3. V和C之間還存在部分耦合,因此除了M外V和C都無(wú)法進(jìn)行單元測(cè)試。

為了解決以上缺點(diǎn),便有了MVP。

MVP

MVP由Model、View和Presenter組成,M和V就不再重復(fù)解釋了,P和C一樣,承擔(dān)著控制層的責(zé)任。MVP相比MVC作了如下改進(jìn)(或者說(shuō)變化,是否改進(jìn)視情況而定):

  1. P可復(fù)用,這意味著不能再使用Activity(或...)作為P了,很簡(jiǎn)單,Activity不能復(fù)用(使用繼承達(dá)到復(fù)用的場(chǎng)景不在這討論范圍之內(nèi))。由此,Activity從控制層的角色轉(zhuǎn)向了視圖層,即在MVP中V由xml和Activity組成。

    也可以另外抽離一個(gè)V,然后將Activity作為創(chuàng)建V和P的管理器,并負(fù)責(zé)將V和P進(jìn)行綁定。但不建議采用這種方式,額外增加了代碼,并且也沒(méi)帶來(lái)多少益處,除非想要復(fù)用V或者界面異常復(fù)雜而拆分了多個(gè)V和P。

  2. MVP三者皆可以獨(dú)立完成單元測(cè)試,為了達(dá)到這個(gè)目的,P和V需要做到完全解耦,解耦一般使用接口。

我們來(lái)看下使用MVP重構(gòu)過(guò)的代碼,在mvc的基礎(chǔ)上主要是改動(dòng)了UserActivity.java,然后增加了幾個(gè)類。

PresenterContext.java

public interface PresenterContext {
    Activity getActivity();
}

UserPresenter.java

public interface UserPresenter {
    void onRefresh();
    void onInited();
    void onDestroyed();
}

UserPresenterImpl.java

public class UserPresenterImpl implements UserPresenter, UserBusiness.UserListener {
    private PresenterContext mContext;
    private UserView mView;
    private UserBusiness mUserBusiness = UserBusiness.get();

    public UserPresenterImpl(PresenterContext context, UserView view) {
        mContext = context;
        mView = view;
    }

    @Override
    public void onRefresh() {
        mUserBusiness.requestUser();
    }

    @Override
    public void onInited() {
        mUserBusiness.addListener(this);
        User user = mUserBusiness.getUser();
        if(user != null) {
            mView.updateName(user.name);
            mView.updateAge(user.age);
        }

    }

    @Override
    public void onDestroyed() {
        mUserBusiness.removeListener(this);
    }

    @Override
    public void onRequestUserResult(int code, User user) {
        if(code == 0) {
            mView.updateName(user.name);
            mView.updateAge(user.age);
        } else {
            Toast.makeText(mContext.getActivity(), "刷新失敗", Toast.LENGTH_SHORT).show();
        }
    }
}

UserView.java

public interface UserView {
    void updateName(String name);
    void updateAge(int age);
}

UserActivity.java

public class UserActivity extends Activity implements UserView, PresenterContext {
    private TextView mNameView;
    private TextView mAgeView;
    private UserPresenter mPresenter;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_user);

        mNameView = (TextView)findViewById(R.id.tv_name);
        mAgeView = (TextView)findViewById(R.id.tv_age);

        mPresenter = new UserPresenterImpl(this, this);

        findViewById(R.id.btn_refresh).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mPresenter.onRefresh();
            }
        });

        mPresenter.onInited();
    }

    @Override
    protected void onDestroy() {
        mPresenter.onDestroyed();
        super.onDestroy();
    }

    @Override
    public void updateName(String name) {
        mNameView.setText("昵稱:" + name);
    }

    @Override
    public void updateAge(int age) {
        mAgeView.setText("年齡:" + age);
    }

    @Override
    public Activity getActivity() {
        return this;
    }
}

我們來(lái)講解下:

  1. 由于Presenter不像Activity一樣,它沒(méi)有上下文信息,因此增加了PresenterContext類作為Presenter的上下文信息。

    這里PresenterContext只是用來(lái)獲取Activity,是因?yàn)槭纠橇η蠛?jiǎn)單,實(shí)際項(xiàng)目中它可以獲取的信息會(huì)更多。

  2. UserActivity的代碼被拆分成了兩部分,一部分仍然在UserActivity中,作為View的代碼,一部分抽離到UserPresenterImpl中,作為Presenter的代碼。至此,將視圖和控制層的代碼完全分離了。
  3. 新建了UserViewUserPresenter兩個(gè)接口用來(lái)表示V和P,V的實(shí)現(xiàn)方UserActivity持有UserPresenter接口而非具體的實(shí)現(xiàn)類,P的實(shí)現(xiàn)方持有UserView接口而非具體的實(shí)現(xiàn)類,從而達(dá)到V和P解耦。

    需要對(duì)P進(jìn)行單元測(cè)試時(shí),只需要?jiǎng)?chuàng)建一個(gè)類簡(jiǎn)單地實(shí)現(xiàn)UserView的類,然后和UserPresenterImpl綁定即可;需要對(duì)V進(jìn)行單元測(cè)試時(shí),只需要?jiǎng)?chuàng)建一個(gè)簡(jiǎn)單實(shí)現(xiàn)UserPresenter的類,然后在UserActivity中將構(gòu)建P的那行代碼修改下即可。

MVP相對(duì)MVC具有P復(fù)用及方便做單元測(cè)試的優(yōu)點(diǎn),然而,在實(shí)際Android項(xiàng)目中,P復(fù)用的場(chǎng)景基本不存在,且多數(shù)公司并沒(méi)有做單元測(cè)試。因此,多數(shù)情況下,MVC可能比MVP更適合Android項(xiàng)目,畢竟MVP多引入了不入類和代碼,且?guī)?lái)解耦的同時(shí)也使得代碼更加“繞”。

MVVM

不管是MVC還是MVP都存在幾下問(wèn)題:

  1. 在每個(gè)界面都要編寫不少的findViewById、setOnClickListener之類的代碼。
  2. 數(shù)據(jù)更新后,要手動(dòng)調(diào)用setText之類的代碼刷新視圖。

以上幾點(diǎn)都不是什么大問(wèn)題,編寫這些代碼也不會(huì)輕易出錯(cuò),但總有達(dá)人追求極致,MVVM便由此產(chǎn)生了。Android解決第問(wèn)題1的方法是在xml中直接嵌入代碼(類似JSX的寫法),解決問(wèn)題2的方法是提供了DataBinding方案綁定視圖和數(shù)據(jù)。MVVM真正地將V層完全地體現(xiàn)在xml上,M層還是老樣子(模式怎么變它都不變),VM(ViewModel)用來(lái)代替C和P,以MVC作為改造,VM包括ActivityDataBinding(自動(dòng)生成)。

本文主要講解幾種架構(gòu)模式的應(yīng)用場(chǎng)景和區(qū)別,因此不會(huì)過(guò)多講解MVVM在Android中如何使用,沒(méi)接觸過(guò)MVVM的建議先看下這篇入門文章或查閱官方文檔。

使用DataBinding需要在build.gradle中加入如下代碼:

android {
    dataBinding {
        enabled = true
    }
}

配置了之后build時(shí)會(huì)下載DataBinding的依賴包以及自動(dòng)生成部分代碼,自動(dòng)生成的代碼后續(xù)遇到時(shí)會(huì)提到。

接著我們來(lái)看下在MVC的基礎(chǔ)上變更后的MVVM代碼。

activity_user.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <variable
            name="user"
            type="com.sean.mvvm.model.entity.User" />

        <variable
            name="host"
            type="com.sean.mvvm.UserActivity"/>
    </data>

    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="10dp">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text='@{"昵稱:" + user.name}' />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:text='@{"年齡:" + String.valueOf(user.age)}'/>

        <Button
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:layout_marginTop="20dp"
            android:text="刷新"
            android:onClick="@{host.onRefresh}"/>

    </LinearLayout>

</layout>

User.java

public class User extends BaseObservable {
    @Bindable
    public String name;

    @Bindable
    public int age;

    public void setName(String name) {
        this.name = name;
        notifyPropertyChanged(BR.name);
    }

    public void setAge(int age) {
        this.age = age;
        notifyPropertyChanged(BR.age);
    }
}

UserActivity.java

public class UserActivity extends Activity implements UserBusiness.UserListener {
    private User mUser;
    private UserBusiness mUserBusiness = UserBusiness.get();

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        ActivityUserBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_user);
        mUser = mUserBusiness.getUser();
        if(mUser == null) {
            mUser = new User();
        }
        binding.setUser(mUser);
        binding.setHost(this);

        mUserBusiness.addListener(this);
    }

    @Override
    protected void onDestroy() {
        mUserBusiness.removeListener(this);
        super.onDestroy();
    }

    @Override
    public void onRequestUserResult(int code, User user) {
        if(code == 0) {
            mUser.setName(user.name);
            mUser.setAge(user.age);
        } else {
            Toast.makeText(UserActivity.this, "刷新失敗", Toast.LENGTH_SHORT).show();
        }
    }

    public void onRefresh(View v) {
        mUserBusiness.requestUser();
    }
}

分析一下:

  1. xml布局的頂部標(biāo)簽變成了layoutdata標(biāo)簽存儲(chǔ)非布局相關(guān)代碼,variable定義變量。
  2. 編譯時(shí)會(huì)根據(jù)xml名稱和內(nèi)容自動(dòng)生成binding類,如例子中的ActivityUserBinding,variable定義的變量在binding中都有對(duì)應(yīng)的set、get方法。
  3. 數(shù)據(jù)改變時(shí)要做到自動(dòng)刷新UI,實(shí)體類必須繼承BaseObservable,且需要自動(dòng)觸發(fā)的字段必須以Bindable注解,并在字段值變化時(shí)調(diào)用notifyPropertyChanged方法。

PS:強(qiáng)烈建議至少熟讀一個(gè)自動(dòng)生成的binding類,綁定的所有原理都在這里,代碼很容易理解,也不需要去網(wǎng)絡(luò)上尋求答案。

上面的例子實(shí)現(xiàn)了數(shù)據(jù)的單向綁定(數(shù)據(jù)更新觸發(fā)UI更新)和事件的綁定,而Android是支持?jǐn)?shù)據(jù)雙向綁定的,現(xiàn)在來(lái)看下當(dāng)UI更新時(shí)如何觸發(fā)數(shù)據(jù)的更新。使用方式其實(shí)很簡(jiǎn)單,在xml中小小修改下就行:

android:text='@={user.name}'

注意到@后面多了個(gè)=,同時(shí)昵稱:去掉了,=表示數(shù)據(jù)雙向綁定,但當(dāng)=存在時(shí),右側(cè)的表達(dá)式只能是個(gè)變量,因此昵稱:只能去掉了。當(dāng)xml改成這樣后,TextView的文本發(fā)生變化了,user.name的值也會(huì)隨之更新。

總結(jié)下MVVM的優(yōu)缺點(diǎn):

  1. 新型xml更加成熟,可以獨(dú)立支撐View層。然而,這可能也是缺點(diǎn),畢竟這種xml編程方式和Android傳統(tǒng)的方式差異較大。

    如果不想改變xml的編寫方式,又希望使用MVVM,那么可以仿照自動(dòng)生成的binding類自己編寫一個(gè)ViewModel,但是有沒(méi)有這個(gè)必要呢。。。

  2. 只關(guān)心數(shù)據(jù)變化,而不需要關(guān)注視圖的刷新,刷新由自動(dòng)生成的binding處理了。
  3. 數(shù)據(jù)雙向綁定是個(gè)偽命題,實(shí)際上并沒(méi)有完全做到自動(dòng)化,還是需要手動(dòng)編寫額外的代碼,并且也有條件限制。
  4. 在非主module(一般為app)中無(wú)法編譯新型xml(這個(gè)也可能是筆者使用不當(dāng),有待確認(rèn))。

綜上,MVVM相比MVC、MVP并沒(méi)有多大優(yōu)勢(shì),但可以通知配置減少一些重復(fù)的邏輯代碼。使用哪種模式根據(jù)實(shí)際情況而定,沒(méi)有誰(shuí)比誰(shuí)更好。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容