今天我想分享我在Android上實現MVP(Model-View-Presenter)模式的方法。如果你對MVP模式還不熟悉,或者不瞭解為什麼要在Android應用中使用MVP模式,推薦你先閱讀這篇維基百科文章和這篇部落格。
使用Activity和Fragment作為View合適麼?
目前,在很多使用了MVP模式的Android專案中,主流做法是將Activity和Fragment作為檢視層來進行處理。而Presenters通常是通過繼承被檢視層例項化或者注入的物件來得到的。我認可這種方式可以節省掉那些讓人厭煩的”import android.*”語句,並且將Presenters從Activity的生命週期中分離出來, 這使專案後續的維護會變得簡便很多。但另一方面, Activity有一個很複雜的生命週期(Fragment的生命週期可能會更復雜)。而這些生命週期很有可能對專案的業務邏輯有非常重要的影響。Activity可以獲取Context和各種Android系統服務。Activity可以傳送Intent,啟動Service和執行FragmentTransisitons等等。在我看來,這些錯綜複雜的方面不應該是檢視層涉及的領域(檢視的功能只是顯示資料,從使用者那裡獲取輸入資料。在理想情況下,檢視應該避免業務邏輯,無需單元測試)。基於上述原因,我對目前的主流做法並不贊同,所以我嘗試使用Activity和Fragment作為Presenters。
使用Activity和Fragment作為Presenters
1、去除所有的view
將Activity和Fragment作為Presenter最大的困難就是如何將關於UI的邏輯分離出來。我的解決方案是:讓需要作為Presenter的Activity或者Fragment來繼承一個抽象的類。這樣關於View各種元件的初始化以及邏輯,都可以在繼承了抽象類的方法中進行操作。而當繼承了該抽象類的class需要對某些元件進行操作的時候,只需要呼叫繼承自抽象類的方法而不必考慮Presenter型別。在抽象類裡面會有一個例項化的介面,這個介面裡面的初始化方法就會對view進行例項化,這個介面我稱為Vu,如下所示:
1 2 3 4 |
public interface Vu { void init(LayoutInflater inflater, ViewGroup container); View getView(); } |
如你所見,Vu定義了一個通用的初始化例程,我可以通過它來傳遞一個填充器和一個容器檢視。它也有一個方法可以獲得一個View的例項,每一個presenter將會和它自己的Vu關聯,這個presenter將會實現這個介面(直接或間接地去實現一個繼承自Vu的介面)。
2、建立Presenter基類
現在我有了抽象的View的基礎,我可以著手定義一個Activity或者Fragment基類來充分利用Vu從而實現View的例項化。我是通過利用普通型別和抽象方法來實現的,它定義了一個特殊的表示Presenter的Vu類。這是實現中最單調乏味的部分,因為我需要重新實現想要的相似邏輯或者每一個Presente基類。
下面是我實現的Activity例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
public abstract class BasePresenterActivity<V extends Vu> extends Activity { protected V vu; @Override protected final void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); try { vu = getVuClass().newInstance(); vu.init(getLayoutInflater(), null); setContentView(vu.getView()); onBindVu(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } @Override protected final void onDestroy() { onDestroyVu(); vu = null; super.onDestroy(); } protected abstract Class<V> getVuClass(); protected void onBindVu(){}; protected void onDestroyVu() {}; } |
下面是我實現的Fragment例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
public abstract class BasePresenterFragment<V extends Vu> extends Fragment { protected V vu; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } @Override public final View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = null; try { vu = getVuClass().newInstance(); vu.init(inflater, container); onBindVu(); view = vu.getView(); } catch (java.lang.InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } return view; } @Override public final void onDestroyView() { onDestroyVu(); vu = null; super.onDestroyView(); } protected void onDestroyVu() {}; protected void onBindVu(){}; protected abstract Class<V> getVuClass(); } |
相同的邏輯可以用在Activity和Fragment型別上,比如支援庫中Activity和Fragment等等。
可以看到,我重寫了建立檢視view的方法(onCreate、onCreateView)和銷燬檢視view的方法(onDestroy、onDestroyView)。我選擇重寫這些方法目的是強制使用抽象例項Vu。一旦它們被重寫,我就可以建立新的生命週期方法,來精確控制對其初始化和銷燬,即onBindVu和onDestroyVu。這樣做的好處就是,兩種型別的presenter都可以利用同樣的生命週期事件簽名來實現。這也消除了Activity和Fragemnt生命週期差異的影響,使得兩者之間的轉換更加容易。 (你也可能會注意到,我並沒有真正的利用InstantiationException 或者IllegalAccessException做一些異常處理。這僅僅是我比較懶罷了,因為如果我正確地使用這些類就不會丟擲這些異常。)
3、寫一個可以工作的例子
現在,我們可以使用剛才構建的框架。簡單起見,我寫一個“Hello World”的例子。我會從建立一個實現了Vu介面的類開始寫:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class HelloVu implements Vu { View view; TextView helloView; @Override public void init(LayoutInflater inflater, ViewGroup container) { view = inflater.inflate(R.layout.hello, container, false); helloView = (TextView) view.findViewById(R.id.hello); } @Override public View getView() { return view; } public void setHelloMessage(String msg){ helloView.setText(msg); } } |
下一步,我會建立一個Presenter來操作這個view:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class HelloActivity extends BasePresenterActivity<HelloVu> { @Override protected void onBindVu() { vu.setHelloMessage("Hello World!"); } @Override protected Class<MainVu> getVuClass() { return HelloVu.class; } } |
等等……有耦合警告!
你可能注意到了,HelloVu類直接實現了Vu介面,Presenter的getVuClass()方法直接引用了實現類。常規的MVP模式中,Presenter要通過介面與他們的View解耦。當然,你也可以這麼做。為了避免直接實現Vu介面,我們可以建立一個擴充套件了Vu的IHelloView介面,然後使用這個介面作為Presenter的泛型型別。那麼Presenter看起來應該是這樣的:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class HelloActivity extends BasePresenterActivity<IHelloVu> { @Override protected void onBindVu() { vu.setHelloMessage("Hello World!"); } @Override protected Class<MainVu> getVuClass() { return HelloVuImpl.class; } } |
在我使用強大的模擬工具過程中,並沒有看到一個介面下面實現Vu所帶來的好處。但是對於我來說一個好的方面是,即使沒有定義Vu介面它也能夠工作,唯一的需求就是你最終還要實現Vu。
4、測試
通過以上幾步我們可以發現,在去除了UI邏輯之後Activity變得非常簡潔。同時,相關的測試也變的異常簡單。請看如下單元測試:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public class HelloActivityTest { HelloActivity activity; HelloVu vu; @Before public void setup() throws Exception { activity = new HelloActivity(); vu = Mockito.mock(HelloVu.class); activity.vu = vu; } @Test public void testOnBindVu(){ activity.onBindVu(); verify(vu).setHelloMessage("Hello World!"); } } |
以上程式碼是一段標準的JUnit單元測試的程式碼,不需要在Android裝置中部署執行。當然我們測試的Activity要足夠簡單。特殊情況下,在測試需要某些硬體支援的方法的時候,你可能需要使用Android裝置。例如當你想測試Activity生命週期中的onResume()方法。在缺乏硬體裝置支援環境的時候,super.onResume()會報錯。還好我們可以使用一些工具,例如Robolectric、還有Android Studio 中的Gradle 1.1 外掛中內建的testOptions { unitTests.returnDefaultValues = true }選項。此外,你仍然可以將這些生命週期按照下面的方式抽離出來:
1 2 3 4 5 6 7 8 9 10 11 |
... @Override protected final void onResume() { super.onResume(); afterResume(); } protected void afterResume(){} ... |
現在,你可以把應用程式中特定的邏輯程式碼轉移到生命週期事件中,並且在沒有Android裝置的情況下執行測試了。
意外收穫:使用Adapter作為Presenter
將Activity作為Presenter已經足夠巧妙了吧,如果是adapter,情況會更復雜。它們可以是View或者Presenter麼?廢話不多說,請看如下的程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
public abstract class BasePresenterAdapter<V extends Vu> extends BaseAdapter { protected V vu; @Override public final View getView(int position, View convertView, ViewGroup parent) { if(convertView == null) { LayoutInflater inflater = (LayoutInflater) parent.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); try { vu = (V) getVuClass().newInstance(); vu.init(inflater, parent); convertView = vu.getView(); convertView.setTag(vu); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } else { vu = (V) convertView.getTag(); } if(convertView!=null) { onBindListItemVu(position); } return convertView; } protected abstract void onBindListItemVu(int position); protected abstract Class<V> getVuClass(); } |
正如你看到的,實現方式和Activity和Fragment的Presenter是一樣的。然而,我不是用空的onBindVu方法,而是用引數為整型的position的onBindListItemVu方法。同時,我仍然沿用了View Holder模式。
總結和Demo專案
這篇文章介紹了一種實現MVP模式的方法。從中我發現唯一的途徑就是網上尋找答案。我非常期待其他Android開發者的反饋,是否有人在用這個方法?你發現它有用麼?我是否過於大膽(瘋狂)?如果是的話,這是一個好辦法嗎?
我已經把這套方法(和一些其他的比如Dagger開源庫)整合在一個開源框架上,並且即將公佈。與此同時,我在Github上面有一個demo專案,望各位不吝賜教。