1. 程式人生 > >Android DataBinding:再見Presenter,你好ViewModel!

Android DataBinding:再見Presenter,你好ViewModel!

@author ASCE1885的 Github 簡書 微博 CSDN 
原文連結

最近一段時間MVP模式已經成為Android應用開發UI層架構設計的主流趨勢。類似TED MOSBYnucleusmortar之類的框架都引入了Presenters來幫助我們搭建簡潔的app架構。它們也(在不同的程度上)幫助我們處理Android平臺上臭名昭著的裝置旋轉和狀態持久化等問題。MVP模式也有助於隔離樣板程式碼,雖然這並不是MVP模式的設計初衷。

在Google I/O 2015上,伴隨著Android M預覽版釋出的Data Binding相容函式庫改變了這一切。

根據維基百科上關於MVP的詞條描述,Presenter作用如下:

Presenter作用於model和view,它從倉庫(Model)中獲取資料,並格式化後讓view進行顯示。

Data Binding框架將會接管Presenter的主要職責(作用於model和view上),Presenter的其他剩餘職責(從倉庫中獲取資料並進行格式化處理)則由ViewModel(一個增強版的Model)接管。ViewModel是一個獨立的Java類,它的唯一職責是表示一個View後面的資料。它可以合併來自多個數據源(Models)的資料,並將這些資料加工後用於展示。我之前寫過一篇關於ViewModel的短文,講述了它與Data Model或者Transport Model之間的區別。

我們今天要講述的架構是MVVM(Model-View-ViewModel),它最初是在2005年(不要嚇到哦)由微軟提出的一個被證明可用的概念。下面我將舉例說明從MVP到MVVM的改變,容我盜用下Hanne Dorfmann在他介紹TED MOSBY框架的文章中的插圖。

可以看到對view中資料的所有繫結和更新操作都是通過Data Binding框架實現的。通過ObservableField類,View在model發生變化時會作出反應,在XML檔案中對屬性的引用使得框架在使用者操作View時可以將變化推送給對應的ViewModel。我們也可以通過程式碼訂閱屬性的變化,這樣可以實現例如當CheckBox被點選後,TextView被禁用這樣的功能。像這樣使用標準Java類來表示View的視覺狀態的一個很大優勢是明顯的:你可以很容易對這種視覺行為進行單元

測試

上面關於MVP的插圖中有一個名為Presenter.loadUsers()的方法,這是一個命令。在MVVM中這些方法定義在ViewModel中。從維基百科文章中可以看到:

view model是一個抽象的view,它對外暴露公有的屬性和命令。

因此這可能跟你以前熟悉的東西有些不同。在MVP模式中models很可能只是純粹用於儲存資料的“啞”類。對於把業務邏輯放到Models或者View Models中的行為不要感到害怕。這是面向物件程式設計的核心準則。回到Presenter.loadUsers()函式,現在它是一個放在ViewModel中的函式,它可能被View的後置程式碼(code-behind)呼叫,或者被位於View的XML檔案中的資料繫結命令呼叫。如果android-developer-preview問題跟蹤裡面這個issue描述的問題得到支援的話。如果我們沒能得到資料繫結到命令功能的支援,那就只能使用以前的android:onClick語法,或者手動在view中新增監聽器了。

程式碼後置(code-behind),微軟的一個概念,經常與早期的ASP.NET或者WinForms聯絡在一起。我想它也可以作為Android上的一個描述術語,View由兩個元素組成:View的佈局檔案(XML)和後置程式碼(Java),這通常是指Fragments,Activities或者繼承自View.java的其他類。

處理系統呼叫

View的後置程式碼還需要完成一系列用例-初始化系統,開啟對話方塊的函式,或者任何需要引用Android Context物件的呼叫。但不要把這樣的程式碼呼叫放到ViewModel中。如果ViewModel包含

import android.content.Context;
  • 1
  • 1

這段程式碼,說明你用錯了,千萬不要這麼做,好奇害死貓。

我還沒有完全決定解決這個問題的最好辦法,不過這是因為有幾個好的選擇。一個方法是通過在ViewModel中持有View的一個引用來儲存Mosby中的presenter元素。這個方案不會降低可測試性。但跟在Mosby中持有一個單獨的Presenter類不同,我堅持認為將View作為介面的具體實現可以起到簡化程式碼的作用。另一個方法可能是使用Square的Otto之類的事件匯流排機制來初始化類似

new ShowToastMessage("hello world")
  • 1
  • 1

的命令。這將會很好的分離view和viewmodel,不過這是一件好事嗎?

我們不需要框架了嗎?

那麼Data Binding框架已經接管了類似Mosby或者Mortar等框架的工作了嗎?只是一部分。我希望看到的是這些框架進化或者新增分支變成MVVM型別的框架,這樣我們在充分利用Data Binding的同時,可以最低限度依賴第三方框架,並保持框架的小而美。雖然Presenter的時代可能已經結束了,但這些框架在管理宣告週期和view(或者ViewModel)的狀態持久化方面還在發揮作用,這一點並沒有改變。(如果Google引入一個LifeCycleAffected介面讓Fragment, Activity 和 View進行實現,那將是多麼酷的一件事!這個介面由一個名為addOnPauseListener()和addOnResumeListener()的函式,在我們例子中如何使用這個介面將留給你來實現。)

更新:最近了解到AndroidViewModel框架,它實際上可能很適合MVVM和Android的Data Binding。不過我還沒有時間試用它。

總結

當我首次聽說Android M致力於改進SDK並重點關注開發者時,我真的很激動。當我聽說他們引入了Data Binding,我被震驚了。在其他平臺如WinForms, WPF, Silverlight 和 Windows Phone上面我已經用了好幾年Data Binding技術。我知道這可以幫助我們寫出簡潔的架構和更少的樣板程式碼。這個框架是站在開發者這邊的,而不是阻礙我們的,很久以前我就感受到這一點了。

但Data Binding不是銀彈,它也有缺點。在XML檔案中定義繫結本身就是一個問題。XML不會被編譯,它也不能進行單元測試。因此你將會經常在執行時才發現錯誤,而不是在編譯期間。忘記將屬性繫結到View了?很不幸。但工具可以發揮很大的幫助-這是為什麼我希望Google能夠儘量讓Android Studio最大程度支援Data Binding。XML繫結的語法和引用檢查,自動完成和導航支援。XML欄位的重新命名支援。從我測試Android Studio 1.3 beta來看,我至少可以肯定他們有在考慮這件事情。某些功能已經支援了,但還有很多沒有支援,不過1.3版本仍然處於beta階段,我們可以有更多的期待。

程式碼示例

接下來我將給出一個示例,演示從MVP架構遷移到MVVM架構的結果。在MVP版本工程中,我使用Mosby框架並使用Butterknife實現檢視注入。在MVVM例子中我使用Android M Data Binding並移除工程中對Mosby和Butterknife的依賴。結果是Presenter可以丟掉了,Fragment中程式碼減少了,不過ViewModel接管了很多程式碼。

在這個例子中我直接引用View來生成toast訊息。這也許不是我以後提倡的一種方法, 但理論上這麼做沒什麼問題。使用Robolectric和Mockito來對Fragment進行mock,這樣是可測試的,而且不會洩露記憶體,除非你錯誤的引用了ViewModels。

下面這個app只是起一個演示的作用,它具有一個簡單的登陸頁面,後臺會載入一些非同步資料,views之間會有一些依賴。

如果你希望在Android Studio中閱讀程式碼,可以到Github上分別檢出MVP和MVVM的標籤。

下面準備好接受程式碼轟炸吧��

MVP – VIEW – XML

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                xmlns:tools="http://schemas.android.com/tools"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:paddingLeft="@dimen/activity_horizontal_margin"
                android:paddingRight="@dimen/activity_horizontal_margin"
                android:paddingTop="@dimen/activity_vertical_margin"
                android:paddingBottom="@dimen/activity_vertical_margin"
                tools:context=".MainActivityFragment">

    <TextView
        android:text="..."
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentEnd="true"
        android:id="@+id/loggedInUserCount"/>

    <TextView
        android:text="# logged in users:"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentEnd="false"
        android:layout_toLeftOf="@+id/loggedInUserCount"/>

    <RadioGroup
        android:layout_marginTop="40dp"
        android:id="@+id/existingOrNewUser"
        android:gravity="center"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:orientation="horizontal">

        <RadioButton
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Returning user"
            android:id="@+id/returningUserRb"/>

        <RadioButton
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="New user"
            android:id="@+id/newUserRb"
            />

    </RadioGroup>

    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/username_block"
        android:layout_below="@+id/existingOrNewUser">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textAppearance="?android:attr/textAppearanceMedium"
            android:text="Username:"
            android:id="@+id/textView"
            android:minWidth="100dp"/>

        <EditText
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/username"
            android:minWidth="200dp"/>
    </LinearLayout>

    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentStart="false"
        android:id="@+id/password_block"
        android:layout_below="@+id/username_block">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textAppearance="?android:attr/textAppearanceMedium"
            android:text="Password:"
            android:minWidth="100dp"/>

        <EditText
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:inputType="textPassword"
            android:ems="10"
            android:id="@+id/password"/>

    </LinearLayout>

    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@+id/password_block"
        android:id="@+id/email_block">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textAppearance="?android:attr/textAppearanceMedium"
            android:text="Email:"
            android:minWidth="100dp"/>

        <EditText
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:inputType="textEmailAddress"
            android:ems="10"
            android:id="@+id/email"/>
    </LinearLayout>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Log in"
        android:id="@+id/loginOrCreateButton"
        android:layout_below="@+id/email_block"
        android:layout_centerHorizontal="true"/>
</RelativeLayout>
  • 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
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 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
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124

MVP – VIEW – JAVA

package com.nilzor.presenterexample;

import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.CompoundButton;
import android.widget.RadioButton;
import android.widget.TextView;
import android.widget.Toast;
import com.hannesdorfmann.mosby.mvp.MvpFragment;
import com.hannesdorfmann.mosby.mvp.MvpView;
import butterknife.InjectView;
import butterknife.OnClick;

public class MainActivityFragment extends MvpFragment implements MvpView {
    @InjectView(R.id.username)
    TextView mUsername;

    @InjectView(R.id.password)
    TextView mPassword;

    @InjectView(R.id.newUserRb)
    RadioButton mNewUserRb;

    @InjectView(R.id.returningUserRb)
    RadioButton mReturningUserRb;

    @InjectView(R.id.loginOrCreateButton)
    Button mLoginOrCreateButton;

    @InjectView(R.id.email_block)
    ViewGroup mEmailBlock;

    @InjectView(R.id.loggedInUserCount)
    TextView mLoggedInUserCount;

    public MainActivityFragment() {
    }

    @Override
    public MainPresenter createPresenter() {
        return new MainPresenter();
    }

    @Override
    
            
           

相關推薦

Android DataBinding再見Presenter你好ViewModel

@author ASCE1885的 Github 簡書 微博 CSDN  原文連結 最近一段時間MVP模式已經成為Android應用開發UI層架構設計的主流趨勢。類似TED MOSBY,nucleus和mortar之類的框架都引入了Presente

再見2016你好2017

時間過得好快,運維派從2012年創辦以來,釋出和轉載了1000多篇文章,微信公眾號有2萬訂閱。 一年前我從華為離職創業,創辦了碼客幫,運維派屬於兼職運營,並沒有太多的精力投入,但16年依然在深圳、廣州、廈門舉辦了多場運維技術沙龍,非常感謝大家的捧場。 接下來我們的創業方向有所調整,後續將投入新專案:

再見2018你好2019

堅持 記得 col 學習c++ 等等 太陽 聯系 rdquo 意義 時間過得好快,一眨眼,日歷上的年份已經從2018變為了2019。 記得有個梗是“都8012”年的,恐怕現在應該要說“9012”年了。 回

再見2018你好2019 -- 致 Mac 背後的自己

  轉眼間 2018 年即將過去,心有萬千感慨,真的感覺到時間如白駒過隙,成長沒有跟上時間的腳步,這叫老了一歲,如果跟上了,那就叫成熟了一歲。很遺憾,2018年我老了一歲。   新年之初,立過好幾個 Flag,只實現了部分,連最基本的每個月寫日誌的習慣,11月份也中斷了。不需要找藉口,向前看。   年近3

再見2018你好2019

文章連結:https://mp.weixin.qq.com/s/MoZ2_3syo9PyCW-HN_4fbw 遲來的18年總結來了,趁著週末,寫下總結。 18年過去,19年來了,去年的現在,還沒有習慣用文字記錄這一年的得失,每到這個時候,各大新聞網站就開始盤點2018年大事記,各大app開始總結

【總結】2016.09-2017.09 再見過去你好未來

引言        回顧一年的歷程,看著自己一點一點成長。感謝不為人知的過去,感激還沒有遇見的未來。 思想        思想上移,行動上移,是一直都在貫徹的。 技術        這一年從C/S跨到了B/S,接觸到了不同的世界。 英語        一直都沒有放棄的英語,

Android開發安裝NDK移植OpenCV2.3.1JNI呼叫OpenCV全過程

開發環境:Fedora14 , 操作中以root許可權操作,這年頭Android移植上OpenCV就強大了,可以做很多複雜的視訊分析、影象處理工作了!如火災、人臉、視訊行為的分析等。歡迎Android愛好者,加群:248217350。備註:yanzi 一:NDK的安裝 首

android開發如果處理同樣的安卓應用程式在不同機器上執行正常但是與後臺伺服器互動響應時間不一樣的問題?

情景問題 專案中用到一個安卓應用程式,在不同的機器上,執行正常,與後臺伺服器互動響應時間不一樣,當是安卓應用程式在接受到請求,處理一下耗時操作,比如操作s qlite3,沒有即時反饋資訊給

深度 | 螞蟻金服金融智慧技術更安全更智慧

​小螞蟻說: 在今年9月20日螞蟻金服ATEC科技大會的主論壇上,螞蟻金服副CTO胡喜宣佈,螞蟻金服的金融科技正式全面開放,為行業提供完整的數字金融解決方案。包括容災系統在內的多項核心技術和解決方案,如金融安全、區塊鏈等都將對合作夥伴開放。 目前,螞蟻金融科技正式宣佈進入了3.0時代:支付寶對內延續BAS

12月報告Python稱王C++敗北

點選上方“程式人生”,選擇“置頂公眾號” 第一時間關注程式猿(媛)身邊的故事 圖片來源:qarea.com 在2018年底,我們迎來了程式語言的最後一波洗禮。近期 TIOBE 公佈了 12 月程式語言排行榜,前三名變為: Java、C、Python。 而在PYPL

產品經理根據使用者手機殼變換顏色程式設計師別說了拔劍吧

在錯綜複雜的團隊關係網中,存在著兩種關係十分微妙的群體,那就是——產品汪和程式猿。任何一個公司裡都有因為工作而糾纏不休、愛恨交織的一張大網,網際網路公司亦不例外。這不,產品汪和程式猿們又火了,引來一大波的吃瓜群眾! 那麼究竟是怎麼回事呢?據知情人爆料,皆因產品經理給研發提出了一個需求:要求 Ap

大資料學習現在學剛剛好

隨著網際網路技術的飛速發展,“大資料時代”呼嘯而來。大資料與政府、企業、個人的關係越來越密切。阿里巴巴創辦人馬雲就曾說過,未來的時代將不是IT時代,而是DT(Data Technology)的時代。     早在2012年3月,美國政府就宣佈投資2億美元拉動大資

大多數人並不知道的祕密接地氣壽命長

“接地氣”,是老年人最愛說的。其實真是這樣。從前筆者並未注意到這個問題,因為從退休後,在家裡電腦前一坐就是幾小時,結果,全身動脈都硬化了,尤其腿腳症狀嚴重,俗話說“老人先老腿”,一點不假。有時候多走點路,腳都疼的不行。後來,因為健康問題,每天早晚都外出走上半小時至一小時,真

七年阿裏老人談新程序員成長先做事在做人

load web前端 擔心 兩種 不可 領域 麻煩 角度 成交 新程序員常有這些困惑,到底是做技術還是做業務?是每天加班加點寫代碼還是空點時間來看書學習?是先追求廣度還是先鉆研一門技術?是兩耳不聞窗外事一心埋頭做事還是和同事打成一片參加各種活動?這些問題也曾困擾著我。 我

Android命令Monkey壓力測試詳解

語句 shel gre href 輸入 white option blacklist 文件 停止Monkey命令: 1. ps命令 查找uiautomator的進程 打開cmd命令行窗口 輸入: adb shell ps | grep monkey 返回來的第一個數字,即

Android零基礎入門第15節掌握Android Studio項目結構揚帆起航

str ems undle int 兼容 總結 local cati 它的 經過前面的學習,Android Studio開發環境已準備OK,運行Android應用程序的原生模擬器和Genymotion模擬器都準備妥當。在之前簡單講過Eclipse中Android工程的項目結

oi再見你好明天。

文件 你好 進出 們的 修改 動態規劃 再見 凸包周長 body oi再見,你好明天。錄Menci大佬的翻唱《模你抄》如下:屏幕在深夜微微發亮思想在那虛樹路徑上仿徨平面的向量交錯生長織成 憂傷的網 剪枝剪去我們的瘋狂SPFA告訴我前途在何方01背包裝下了憂傷笑顏 洋溢臉龐

android studio解決方法數超過65536的方法三步

text app multi 一行代碼 dex ide 解決 andro com 1.在build.gradle(Module: app) 中的defaultConfig{}中添加 multiDexEnabled true 2.在build.gradle(Modul

android開發在Macbook環境android studio 配置git環境

第一步:對專案啟用git管理 這步是將專案納入git管理之下,點選android studio選單欄的VCS後,選擇Enable Version Control Integration. 在彈出的框裡選擇Git 然後可以發現在快捷工具圖示裡面多了兩個版本控制的按鈕,並且

Android開發int型別資料按照高低位存放到byte型別的陣列

int型別的資料—>byte型別陣列轉換 //byte陣列"按高位在前,低位在後"的方式存放int型別資料 int src = 123; int[] dec = new int[4]; dec[0] = (src /256/256/256); dec[1] = (src /25