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
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_main, container, false);
    }

    @Override
    public void onViewCreated(View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        attachEventListeners();
    }

    private void attachEventListeners() {
        mNewUserRb.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                updateDependentViews();
            }
        });
        mReturningUserRb.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                updateDependentViews();
            }
        });
    }

    /** Prepares the initial state of the view upon startup */
    public void setInitialState() {
        mReturningUserRb.setChecked(true);
        updateDependentViews();
    }

    /** Shows/hides email field and sets correct text of login button depending on state of radio buttons */
    public void updateDependentViews() {
        if (mReturningUserRb.isChecked()) {
            mEmailBlock.setVisibility(View.GONE);
            mLoginOrCreateButton.setText(R.string.log_in);
        }
        else {
            mEmailBlock.setVisibility(View.VISIBLE);
            mLoginOrCreateButton.setText(R.string.create_user);
        }
    }

    public void setNumberOfLoggedIn(int numberOfLoggedIn) {
        mLoggedInUserCount.setText(""  + numberOfLoggedIn);
    }

    @OnClick(R.id.loginOrCreateButton)
    public void loginOrCreate() {
        if (mNewUserRb.isChecked()) {
            Toast.makeText(getActivity(), "Please enter a valid email address", Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(getActivity(), "Invalid username or password", Toast.LENGTH_SHORT).show();
        }
    }
}
  • 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
  • 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

MVP – PRESENTER

package com.nilzor.presenterexample;

import android.os.Handler;
import android.os.Message;
import com.hannesdorfmann.mosby.mvp.MvpPresenter;

public class MainPresenter implements MvpPresenter {
    MainModel mModel;
    private MainActivityFragment mView;

    public MainPresenter() {
        mModel = new MainModel();
    }

    @Override
    public void attachView(MainActivityFragment view) {
        mView = view;
        view.setInitialState();
        updateViewFromModel();
        ensureModelDataIsLoaded();
    }

    @Override
    public void detachView(boolean retainInstance) {
        mView = null;
    }

    private void ensureModelDataIsLoaded() {
        if (!mModel.isLoaded()) {
            mModel.loadAsync(new Handler.Callback() {
                @Override
                public boolean handleMessage(Message msg) {
                    updateViewFromModel();
                    return true;
                }
            });
        }
    }

    /** Notifies the views of the current value of "numberOfUsersLoggedIn", if any */
    private void updateViewFromModel() {
        if (mView != null && mModel.isLoaded()) {
            mView.setNumberOfLoggedIn(mModel.numberOfUsersLoggedIn);
        }
    }
}
  • 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
  • 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

MVP – MODEL

package com.nilzor.presenterexample;

import android.os.AsyncTask;
import android.os.Handler;
import java.util.Random;

public class MainModel {
    public Integer numberOfUsersLoggedIn;
    private boolean mIsLoaded;
    public boolean isLoaded() {
        return mIsLoaded;
    }

    public void loadAsync(final Handler.Callback onDoneCallback) {
        new AsyncTask() {
            @Override
            protected Void doInBackground(Void... params) {
                // Simulating some asynchronous task fetching data from a remote server
                try {Thread.sleep(2000);} catch (Exception ex) {};
                numberOfUsersLoggedIn = new Random().nextInt(1000);
                mIsLoaded = true;
                return null;
            }

            @Override
            protected void onPostExecute(Void aVoid) {
                onDoneCallback.handleMessage(null);
            }
        }.execute((Void) null);
    }
}
  • 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
  • 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

MVVM – VIEW – XML

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <data>
        <variable name="data" type="com.nilzor.presenterexample.MainModel"/>
    </data>
    <RelativeLayout
        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="@{data.numberOfUsersLoggedIn}"
            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:checked="@{data.isExistingUserChecked}"
                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"
            android:visibility="@{data.emailBlockVisibility}">

            <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="@{data.loginOrCreateButtonText}"
            android:id="@+id/loginOrCreateButton"
            android:layout_below="@+id/email_block"
            android:layout_centerHorizontal="true"/>
    </RelativeLayout>
</layout>
  • 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
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 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
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131

MVVM – VIEW – JAVA

package com.nilzor.presenterexample;

import android.app.Fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CompoundButton;
import android.widget.Toast;

import com.nilzor.presenterexample.databinding.FragmentMainBinding;

public class MainActivityFragment extends Fragment {
    private FragmentMainBinding mBinding;
    private MainModel mViewModel;

    public MainActivityFragment() {
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_main, container, false);
        mBinding = FragmentMainBinding.bind(view);
        mViewModel = new MainModel(this, getResources());
        mBinding.setData(mViewModel);
        attachButtonListener();
        return view;
    }

    private void attachButtonListener() {
        mBinding.loginOrCreateButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mViewModel.logInClicked();
            }
        });
    }

    @Override
    public void onViewCreated(View view, Bundle savedInstanceState) {
        ensureModelDataIsLodaded();
    }

    private void ensureModelDataIsLodaded() {
        if (!mViewModel.isLoaded()) {
            mViewModel.loadAsync();
        }
    }

    public void showShortToast(String text) {
        Toast.makeText(getActivity(), text, Toast.LENGTH_SHORT).show();
    }
}
  • 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
  • 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

MVVM – VIEWMODEL

package com.nilzor.presenterexample;

import android.content.res.Resources;
import android.databinding.ObservableField;
import android.os.AsyncTask;
import android.view.View;

import java.util.Random;

public class MainModel {
    public ObservableField numberOfUsersLoggedIn = new ObservableField();
    public ObservableField isExistingUserChecked = new ObservableField();
    public ObservableField emailBlockVisibility = new ObservableField();
    public ObservableField loginOrCreateButtonText = new ObservableField();
    private boolean mIsLoaded;
    private MainActivityFragment mView;
    private Resources mResources;

    public MainModel(MainActivityFragment view, Resources resources) {
        mView = view;
        mResources = resources; // You might want to abstract this for testability
        setInitialState();
        updateDependentViews();
        hookUpDependencies();
    }
    public boolean isLoaded() {
        return mIsLoaded;
    }

    private void setInitialState() {
        numberOfUsersLoggedIn.set("...");
        isExistingUserChecked.set(true);
    }

    private void hookUpDependencies() {
        isExistingUserChecked.addOnPropertyChangedCallback(new android.databinding.Observable.OnPropertyChangedCallback() {
            @Override
            public void onPropertyChanged(android.databinding.Observable sender, int propertyId) {
                updateDependentViews();
            }
        });
    }

    public void updateDependentViews() {
        if (isExistingUserChecked.get()) {
            emailBlockVisibility.set(View.GONE);
            loginOrCreateButtonText.set(mResources.getString(R.string.log_in));
        }
        else {
            emailBlockVisibility.set(View.VISIBLE);
            loginOrCreateButtonText.set(mResources.getString(R.string.create_user));
        }
    }

    public void loadAsync() {
        new AsyncTask() {
            @Override
            protected Void doInBackground(Void... params) {
                // Simulating some asynchronous task fetching data from a remote server
                try {Thread.sleep(2000);} catch (Exception ex) {};
                numberOfUsersLoggedIn.set("" + new Random().nextInt(1000));
                mIsLoaded = true;
                return null;
            }
        }.execute((Void) null);
    }

    public void logInClicked() {
        // Illustrating the need for calling back to the view though testable interfaces.
        if (isExistingUserChecked.get()) {
            mView.showShortToast("Invalid username or password");
        }
        else {
            mView.showShortToast("Please enter a valid email address");
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章