MVC、MVP、MVVM,談談我對Android應用架構的理解

Android入墳之路發表於2019-04-10
  1. MVC:Model-View-Controller,經典模式,很容易理解,主要缺點有兩個:
  • View對Model的依賴,會導致View也包含了業務邏輯;

  • Controller會變得很厚很複雜。
   2.MVP:Model-View-Presenter,MVC的一個演變模式,將Controller換成了Presenter,主要為了解決上述第一個缺點,將View和Model解耦,不過第二個缺點依然沒有解決。

   3.MVVM:Model-View-ViewModel,是對MVP的一個優化模式,採用了雙向繫結:View的變動,自動反映在ViewModel,反之亦然。

MVC

簡單的說:我們平時寫的Demo都是MVC,controller就是我們的activity,model(資料提供者)就是讀取資料庫,網路請求這些我們一般有專門的類處理,View一般用自定義控制元件。

但這一切,只是看起來很美。

想象實際開發中,我們的activity程式碼其實是越來越多,model和controller根本沒有分離,控制元件也需要關係資料和業務。

MVC、MVP、MVVM,談談我對Android應用架構的理解

所以說,MVC的真實存在是MC(V),Model和Controller根本沒辦法分開,並且資料和View嚴重耦合。這就是它的問題。舉個簡單例子 :獲取天氣資料展示在介面上

MVC、MVP、MVVM,談談我對Android應用架構的理解
  • Model層
public interface WeatherModel { 
    void getWeather(String cityNumber, OnWeatherListener listener); 
} 
................ 
public class WeatherModelImpl implements WeatherModel { 
        @Override 
    public void getWeather(String cityNumber, final OnWeatherListener listener) { 
        /*資料層操作*/ 
        VolleyRequest.newInstance().newGsonRequest(http://www.weather.com.cn/data/sk/ + cityNumber + .html, 
                Weather.class, new Response.Listener<weather>() { 
                    @Override 
                    public void onResponse(Weather weather) { 
                        if (weather != null) { 
                            listener.onSuccess(weather); 
                        } else { 
                            listener.onError(); 
                        } 
                    } 
                }, new Response.ErrorListener() { 
                    @Override 
                    public void onErrorResponse(VolleyError error) { 
                        listener.onError(); 
                    } 
                }); 
    } 
}
複製程式碼
  • Controllor(View)層
public class MainActivity extends ActionBarActivity implements OnWeatherListener, View.OnClickListener { 
    private WeatherModel weatherModel; 
    private EditText cityNOInput; 
    private TextView city; 
    ... 
    @Override 
    protected void onCreate(Bundle savedInstanceState) { 
        super.onCreate(savedInstanceState); 
        setContentView(R.layout.activity_main); 
        weatherModel = new WeatherModelImpl(); 
        initView(); 
    } 
    //初始化View 
    private void initView() { 
        cityNOInput = findView(R.id.et_city_no); 
        city = findView(R.id.tv_city); 
        ... 
        findView(R.id.btn_go).setOnClickListener(this); 
    } 
    //顯示結果 
    public void displayResult(Weather weather) { 
        WeatherInfo weatherInfo = weather.getWeatherinfo(); 
        city.setText(weatherInfo.getCity()); 
        ... 
    } 
    @Override 
    public void onClick(View v) { 
        switch (v.getId()) { 
            case R.id.btn_go: 
                weatherModel.getWeather(cityNOInput.getText().toString().trim(), this); 
                break; 
        } 
    } 
    @Override 
    public void onSuccess(Weather weather) { 
        displayResult(weather); 
    } 
    @Override 
    public void onError() { 
        Toast.makeText(this, 獲取天氣資訊失敗, Toast.LENGTH_SHORT).show(); 
    } 
    private T findView(int id) { 
        return (T) findViewById(id); 
    } 
}
複製程式碼

簡單分析下這個例子:

  • activity裡面的控制元件必須關心業務和資料,才能知道自己怎麼展示。換句話說,我們很難讓兩個人在不互相溝通的情況下,一人負責獲取資料,一人負責展示UI,然後完成這個功能。

  • 所以的邏輯都在activity裡面。

完美的體現了MVC的兩大缺點,下面看看MVP怎麼解決第一個缺點的

MVP

MVC、MVP、MVVM,談談我對Android應用架構的理解

看上圖可以看出,從MVC中View被拆成了Presenter和View,真正實現了邏輯處理和View的分離。下面寫一個例項:模擬一個登入介面,輸入使用者名稱和密碼,可以登入以及清除密碼

  • Model層
/** 
定義業務介面 
*/ public interface IUserBiz { 
    public void login(String username, String password, OnLoginListener loginListener); 
} 
/** 
結果回撥介面 
*/ public interface OnLoginListener { 
    void loginSuccess(User user); 
    void loginFailed(); 
} 
/** 
具體Model的實現 
*/ public class UserBiz implements IUserBiz { 
    @Override 
    public void login(final String username, final String password, final OnLoginListener loginListener) 
    { 
        //模擬子執行緒耗時操作 
        new Thread() 
        { 
            @Override 
            public void run() 
            { 
                try 
                { 
                    Thread.sleep(2000); 
                } catch (InterruptedException e) 
                { 
                    e.printStackTrace(); 
                } 
                //模擬登入成功 
                if ("zhy".equals(username) && "123".equals(password)) 
                { 
                    User user = new User(); 
                    user.setUsername(username); 
                    user.setPassword(password); 
                    loginListener.loginSuccess(user); 
                } else 
                { 
                    loginListener.loginFailed(); 
                } 
            } 
        }.start(); 
    } 
}
複製程式碼
  • View

上面說到View層是以介面的形式定義,我們不關心資料,不關心邏輯處理!只關心和使用者的互動,那麼這個登入介面應該有的操作就是(把這個介面想成一個容器,有輸入和輸出)。

獲取使用者名稱,獲取密碼,現實進度條,隱藏進度條,跳轉到其他介面,展示失敗dialog,清除使用者名稱,清除密碼。接下來定義介面:

public interface IUserLoginView  {  
    String getUserName();  
    String getPassword();  
    void clearUserName();  
    void clearPassword();  
    void showLoading();  
    void hideLoading();  
    void toMainActivity(User user);  
    void showFailedError();  
}
複製程式碼

然後Activity實現這個這個介面:

public class UserLoginActivity extends ActionBarActivity implements IUserLoginView { 
    private EditText mEtUsername, mEtPassword; 
    private Button mBtnLogin, mBtnClear; 
    private ProgressBar mPbLoading; 
    private UserLoginPresenter mUserLoginPresenter = new UserLoginPresenter(this); 
    @Override 
    protected void onCreate(Bundle savedInstanceState) 
    { 
        super.onCreate(savedInstanceState); 
        setContentView(R.layout.activity_user_login); 
        initViews(); 
    } 
    private void initViews() 
    { 
        mEtUsername = (EditText) findViewById(R.id.id_et_username); 
        mEtPassword = (EditText) findViewById(R.id.id_et_password); 
        mBtnClear = (Button) findViewById(R.id.id_btn_clear); 
        mBtnLogin = (Button) findViewById(R.id.id_btn_login); 
        mPbLoading = (ProgressBar) findViewById(R.id.id_pb_loading); 
        mBtnLogin.setOnClickListener(new View.OnClickListener() 
        { 
            @Override 
            public void onClick(View v) 
            { 
                mUserLoginPresenter.login(); 
            } 
        }); 
        mBtnClear.setOnClickListener(new View.OnClickListener() 
        { 
            @Override 
            public void onClick(View v) 
            { 
                mUserLoginPresenter.clear(); 
            } 
        }); 
    } 
    @Override 
    public String getUserName() 
    { 
        return mEtUsername.getText().toString(); 
    } 
    @Override 
    public String getPassword() 
    { 
        return mEtPassword.getText().toString(); 
    } 
    @Override 
    public void clearUserName() 
    { 
        mEtUsername.setText(""); 
    } 
    @Override 
    public void clearPassword() 
    { 
        mEtPassword.setText(""); 
    } 
    @Override 
    public void showLoading() 
    { 
        mPbLoading.setVisibility(View.VISIBLE); 
    } 
    @Override 
    public void hideLoading() 
    { 
        mPbLoading.setVisibility(View.GONE); 
    } 
    @Override 
    public void toMainActivity(User user) 
    { 
        Toast.makeText(this, user.getUsername() + 
                " login success , to MainActivity", Toast.LENGTH_SHORT).show(); 
    } 
    @Override 
    public void showFailedError() 
    { 
        Toast.makeText(this, 
                "login failed", Toast.LENGTH_SHORT).show(); 
    } 
}
複製程式碼
  • Presenter

Presenter的作用就是從View層獲取使用者的輸入,傳遞到Model層進行處理,然後回撥給View層,輸出給使用者!

public class UserLoginPresenter { 
    private IUserBiz userBiz; 
    private IUserLoginView userLoginView; 
    private Handler mHandler = new Handler(); 
//Presenter必須要能拿到View和Model的實現類 
    public UserLoginPresenter(IUserLoginView userLoginView) 
    { 
        this.userLoginView = userLoginView; 
        this.userBiz = new UserBiz(); 
    } 
    public void login() 
    { 
        userLoginView.showLoading(); 
        userBiz.login(userLoginView.getUserName(), userLoginView.getPassword(), new OnLoginListener() 
        { 
            @Override 
            public void loginSuccess(final User user) 
            { 
                //需要在UI執行緒執行 
                mHandler.post(new Runnable() 
                { 
                    @Override 
                    public void run() 
                    { 
                        userLoginView.toMainActivity(user); 
                        userLoginView.hideLoading(); 
                    } 
                }); 
            } 
            @Override 
            public void loginFailed() 
            { 
                //需要在UI執行緒執行 
                mHandler.post(new Runnable() 
                { 
                    @Override 
                    public void run() 
                    { 
                        userLoginView.showFailedError(); 
                        userLoginView.hideLoading(); 
                    } 
                }); 
            } 
        }); 
    } 
    public void clear() 
    { 
        userLoginView.clearUserName(); 
        userLoginView.clearPassword(); 
    } 
}
複製程式碼

分析下這個例子:

  • 我們有了IUserLoginView 這個介面(協議),activity裡面的控制元件根本不需要關心資料,只要實現這個介面在每個方法中“按部就班”的展示UI就行了。換句話說,我們讓兩個人一起開發這個功能,一人要處理資料並且制定介面(協議),另一人直接用activity實現這個介面,閉著眼睛就可以在每個回撥裡展示UI,合作很愉快。

  • MVP成功解決了MVC的第一個缺點,但是邏輯處理還是雜糅在Activity。

MVC到MVP簡單說,就是增加了一個介面降低一層耦合。那麼,用樣的MVP到MVVM就是再加一個介面唄。實際專案我建議用MVP模式,MVVM還是複雜了對於中小型專案有點過度設計,這裡就不展開講。

模組化

MVC、MVP、MVVM,談談我對Android應用架構的理解

上圖是一個專案常見的架構方式

  • 最底層是基礎庫,放置與業務無關的模組:比如基礎網路請求,圖片壓縮等等,可以按需分為邏輯模組,通用UI模組和第三方庫。(建議採用獨立的svn分支)

  • 中間層是通用業務層,放置公司多個android專案的通用業務模組(和業務相關的),比如登入流程,檔案上傳/下載等。

  • 最上層就是應用層了,比如公司有三個android專案:LbBoss,BV和BVHD。我們還可以針對相似的專案再抽取通用層(比如這裡的BV和BV PAD版,通用層為BVCommon)。

新建一個app,我們往往有兩種模組劃分方法:

  • 按照型別劃分
MVC、MVP、MVVM,談談我對Android應用架構的理解
  • 按照業務劃分

每一個包都是一個業務模組,每個模組下再按照型別來分。

  • 怎麼選

我建議中小型的新專案按照型別比較好,因為開始程式碼量不多按照業務來分不切實際,一個包只放幾個檔案?? 況且前期業務不穩定,等到開發中期業務定型了,再進行重構難度也不大。

上面講的模組劃分既不屬於模組化也不屬於外掛化,僅僅是一個簡單package結構不同而已,app還是一個app並沒有產生什麼變化。通常講的模組化,是指把業務劃分為不同的moduler(型別是library),每個moduler之間都不依賴,app(型別是application)只是一個空殼依賴所有的moduler。

MVC、MVP、MVVM,談談我對Android應用架構的理解
每個紅色箭頭都是一個業務模組,紅色框是我們的app裡面只包含簡單的業務:自定義Application,入口Activity,build.gradle編譯打包配置。看下專案的依賴關係:
MVC、MVP、MVVM,談談我對Android應用架構的理解

這樣架構後,帶來最大的不同就是:不同業務模組完全分離,好處就是不同模組的開發絕對不會互相耦合了,因為你在模組A 根本訪問不到模組B的API。此時模組間通訊急需解決,Intent隱式跳轉可以處理部分Activity的跳轉,但真正的業務場景遠不止兩個介面跳一跳。你之前封裝的業務通用方法,工具類,資料快取現在其他模組都拿不到了,本本來可以複用的控制元件,fragment都不能共享,而這些都是和業務耦合沒辦法拿到底層基礎庫。

模組間通訊

針對上面問題有兩個解決辦法,根據自己專案實際情況,如果專案的前期搭建已經很優秀,有完善的基礎庫,不同模組間的通訊不是很多,可以自己實現。如果專案比較龐大,不同業務間頻繁呼叫建議使用阿里巴巴的開源庫。

自己實現

首先每個moduler有個目錄叫include,裡面有三個類,此處以一個bbs論壇模組為例說明,

  • IBBSNotify:裡面是一堆interface,作用是該模組對外的回撥,只能被動被觸發。

  • IBBService:裡面是一堆interface,作用是對外暴露的方法,讓別的模組來主動調,比如enterBbsActivity

  • IBBSServiceImpl:很明顯是IBBService的實現,比如enterBbsActivity就是具體怎麼跳轉到論壇介面,傳遞什麼資料。

MVC、MVP、MVVM,談談我對Android應用架構的理解

每個模組方法和回撥都有了,in和out都具備了,別的模組怎麼使用呢?就該app該上場了,app不能只是一個殼裡面要定義一個ModulerManager implements 所有模組的對外interface,作為每個模組的中轉站,A模組告訴ModulerManager我想跳轉到論壇模組,接著ModulerManager呼叫IBBService.enterBbsActivity,IBBSServiceImpl是IBBService的具體實現(多型)然後呼叫IBBSServiceImpl.enterBbsActivity跳轉到BBS介面。

通訊是解決了,其實踩坑才剛剛開始:

  • 這裡的app是我們新建的,那麼之前專案的app模組要降為library:
apply plugin: 'com.android.library'
複製程式碼

app的build.gradle配置:

apply plugin: 'com.android.application'
複製程式碼

性質發生巨大變化。裡面的自定義application,build.gradle,程式碼混淆配置等全部移到app

  • R.java在Lib型別的moduler中不是final的,所有switch case語句全部替換成if else

  • 一定要再建一個common模組,放置通用資料,快取等

  • 還有很多通用功能,例如分享,推送,儘量剝離業務放到common

  • 其他與專案相關的細節

結語

外掛化其實最後釋出的產品也是一個apk,只不過大小可以控制(可以隨意去掉某些模組),支援使用者動態載入子apk。因此,外掛化就是動態載入apk。有人說我用intent隱式可以直接跳轉到另一個apk啊,幹嘛還要外掛化。

其實是兩碼事,intent只是指定一個Activity跳過去,後面的互動完成不受你控制,2個apk也是執行在獨立的程式資料無法共享。而外掛化可以讓兩個apk執行在一個程式,可以完全像同一個apk一樣開發。不過,我覺得外掛化只適合需要多部門並行開發的那種,比如支付寶這種超級app,一般的app開發除非特殊需要,否則用不到。

外掛化也有成熟的框架,在此不詳細說了。另外,每個人的習慣不一樣,元件化,模組化在我看來差不多,沒必要糾結兩個名詞。



相關文章