天天看點

玩轉Android之MVVM開發模式實戰,炫酷的DataBinding!1.基本使用2.綁定ImageView3.綁定ListView4.點選事件處理5. 資料更新處理

C# 很早就有了MVVM的開發模式,Android手機中的MVVM一直到去年Google的I\O大會上才推出,姗姗來遲。MVVM這中開發模式的優點自不必多說,可以實作視圖和邏輯代碼的解耦,而且,按照Google的說法,使用了MVVM的開發模式,還可以提高布局檔案的解析速度,個人覺得這一點非常重要。我們在安卓開發中經常需要寫很多個findViewById,讓人心煩,很多人不想寫這個于是用了一些注解架構,可是注解架構無論性能多好,效率總是要低于findViewById的,是以,Android中的MVVM也即databinding可以幫助我們徹底解決這個問題。OK,廢話不多說,我們來看看具體要怎麼在Android開發中使用MVVM。

在低版本的AndroidStudio中使用DataBinding稍微有點麻煩,這裡不做介紹。我這裡以AndroidStuido2.1為例來介紹DataBinding。本文主要包含以下幾方面内容:

1.基本使用

2.綁定ImageView

3.綁定ListView

4.點選事件處理

5.資料更新處理

好了,那就開始吧!

1.基本使用

建立好一個Android Project之後,在gradle檔案中添加如下幾行代碼,表示開啟databinding:

android {
    ...
	...
	...
    dataBinding{
        enabled true
    }
}
           

就是這麼簡單,一個簡單的databinding配置之後,就可以開始使用資料綁定了。

要使用資料綁定,我們得首先建立一個實體類,比如User實體類,如下:

/**
 * Created by 王松 on 2016/7/31.
 */
public class UserEntity {
    private String username;
    private String nickname;
    private int age;

    public UserEntity() {
    }

    public int getAge() {
        return age;
    }

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

    public String getNickname() {
        return nickname;
    }

    public void setNickname(String nickname) {
        this.nickname = nickname;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public UserEntity(int age, String nickname, String username) {
        this.age = age;
        this.nickname = nickname;
        this.username = username;
    }
}
           

然後我們來看看布局檔案該怎麼寫,首先布局檔案不再是以傳統的某一個容器作為根節點,而是使用<layout></layout>作為根節點,在<layout>節點中我們可以通過<data>節點來引入我們要使用的資料源,如下:

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

    <data>

        <variable
            name="user"
            type="org.lenve.databinding1.UserEntity"/>
    </data>

    <LinearLayout
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context="org.lenve.databinding1.MainActivity">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.username}"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.nickname}"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{String.valueOf(user.age)}"/>
    </LinearLayout>
</layout>
           

在data中定義的variable節點,name屬性表示變量的名稱,type表示這個變量的類型,執行個體就是我們實體類的位置,當然,這裡你也可以換一種寫法,如下:

<data>

        <import type="org.lenve.databinding1.UserEntity"/>
        <variable
            name="user"
            type="UserEntity"/>
    </data>
           

先使用import節點将UserEntity導入,然後直接使用即可。但是如果這樣的話又會有另外一個問題,假如我有兩個類都是UserEntity,這兩個UserEntity分屬于不同的包中,又該如何?看下面:

<data>

        <import type="org.lenve.databinding1.UserEntity" alias="Lenve"/>
        <variable
            name="user"
            type="Lenve"/>
    </data>
           

在import節點中還有一個屬性叫做alias,這個屬性表示我可以給該類取一個别名,我給UserEntity這個實體類取一個别名叫做Lenve,這樣我就可以在variable節點中直接寫Lenve了。

看完data節點我們再來看看布局檔案,TextView的text屬性被我直接設定為了@{user.username},這樣,該TextView一會直接将UserEntity實體類的username屬性的值顯示出來,對于顯示age的TextView,我用了String.valueOf來顯示,因為大家知道TextView并不能直接顯示int型資料,是以需要一個簡單的轉換,事實上,我們還可以在{}裡邊進行一些簡單的運算,這些我一會再說。

最後,我們來看看Activity中該怎麼寫,setContentView方法不能夠再像以前那樣來寫了,換成下面的方式:

DataBindingUtil.setContentView(this, R.layout.activity_main)
           

該方法有一個傳回值,這個傳回值就是系統根據我們的activity_main.xml布局生成的一個ViewModel類,是以完整寫法如下:

ActivityMainBinding activityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
           

有了ViewModel,再把資料綁定上去就可以了,如下:

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityMainBinding activityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        UserEntity user = new UserEntity();
        user.setAge(34);
        user.setUsername("zhangsan");
        user.setNickname("張三");
        activityMainBinding.setUser(user);
    }
           

運作,顯示效果如下:

玩轉Android之MVVM開發模式實戰,炫酷的DataBinding!1.基本使用2.綁定ImageView3.綁定ListView4.點選事件處理5. 資料更新處理

OK,那我們剛才還說到可以在@{}進行簡單的計算,都有哪些計算呢?我們來看看:

1.基本的三目運算

<TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.username??user.nickname}"/>
           

兩個??表示如果username屬性為null則顯示nickname屬性,否則顯示username屬性。

2.字元拼接

<TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{`username is :`+user.username}"/>
           

大家注意,這裡的字元拼接不是用單引号哦,用的是ESC按鍵下面那個按鍵按出來的。目前DataBinding中的字元拼接還不支援中文。

3.根據資料來決定顯示樣式

<TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@{user.age &lt; 30 ? 0xFF0000FF:0xFFFF0000}"
            android:text="@{String.valueOf(user.age)}"/>
           

我在這裡給TextView設定背景的時候,做了一個簡單的判斷,如果使用者的年齡小于30,背景就顯示為藍色,否則背景就顯示為紅色,DataBinding裡支援小于号但是不支援大于号,索性,大于小于号我都用轉義字元來表示。

另外,DataBinding對于基本的四則運算、邏輯與、邏輯或、取反位移等都是支援的,我這裡不再舉例。

2.綁定ImageView

OK,上文隻是一個簡單的綁定文本,下面我們來看看怎麼樣綁定圖檔,這裡我們還得介紹DataBinding的另一項新功能,就是關于DataBinding自定義屬性的問題,事實上,在我們使用DataBinding的時候,可以給一個控件自定義一個屬性,比如我們下面即将說的這個綁定ImageView的案例。假設我現在想要通過Picasso顯示一張網絡圖檔,正常情況下這個顯示很簡單,可是如果我要通過DataBinding來實作,該怎麼做呢?我們可以使用

@BindingAdapter
           

注解來建立一個自定義屬性,同時還要有一個配套的注解的方法。當我們在布局檔案中使用這個自定義屬性的時候,會觸發這個被我們注解的方法,這樣說大家可能還有一點模糊,我們來看看新的實體類:

/**
 * Created by 王松 on 2016/7/31.
 */
public class User {
    private String username;
    private String userface;

    public User() {
    }

    public User(String userface, String username) {
        this.userface = userface;
        this.username = username;
    }

    @BindingAdapter("bind:userface")
    public static void getInternetImage(ImageView iv, String userface) {
        Picasso.with(iv.getContext()).load(userface).into(iv);
    }

    public String getUserface() {
        return userface;
    }

    public void setUserface(String userface) {
        this.userface = userface;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }
}
           

新類裡邊隻有兩個屬性,分别是使用者名和使用者圖像,使用者圖像中存儲的實際上是一個網絡圖檔位址,這裡除了基本的get/set方法之外還多了一個叫做getInternetImage的網絡方法,這個方法有一個注解@BindAdapter("bind:userface"),該注解表示當使用者在ImageView中使用自定義屬性userface的時候,會觸發這個方法,我在這個方法中來為這個ImageView加載一張圖檔,這裡有一點需要注意,就是該方法必須為靜态方法。OK,我們再來看看這次的布局檔案:

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

    <data>

        <variable
            name="user"
            type="org.lenve.databinding2.User"/>
    </data>

    <LinearLayout
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context="org.lenve.databinding2.MainActivity">

        <ImageView
            android:id="@+id/iv"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:user></ImageView>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{user.username}"/>
    </LinearLayout>
</layout>
           

大家注意我在ImageView控件中使用userface屬性的時候,使用的字首不是android而是app哦。再來看看Activity中的代碼:

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityMainBinding dataBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        dataBinding.setUser(new User("http://img2.cache.netease.com/auto/2016/7/28/201607282215432cd8a.jpg", "張三"));
    }
           

就是這麼簡單,加上網絡權限就可以運作了,運作效果如下:

玩轉Android之MVVM開發模式實戰,炫酷的DataBinding!1.基本使用2.綁定ImageView3.綁定ListView4.點選事件處理5. 資料更新處理

3.綁定ListView

好了,看完了簡單使用之後,不知道你有沒有喜歡上DataBinding,如果還沒有,那就再來看看使用DataBinding來給ListView綁定資料吧,這個你一定會喜歡上的。因為使用這中方式來綁定太簡單了。

先來看看我們要做的效果吧:

玩轉Android之MVVM開發模式實戰,炫酷的DataBinding!1.基本使用2.綁定ImageView3.綁定ListView4.點選事件處理5. 資料更新處理

就是一個ListView,左邊顯示圖檔,右邊顯示文本,這樣一個效果。OK,那就一步一步來吧,先是主布局:

<?xml version="1.0" encoding="utf-8"?>
<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"
    tools:context="org.lenve.databinding3.MainActivity">

    <ListView
        android:id="@+id/lv"
        android:layout_width="match_parent"
        android:layout_height="match_parent"></ListView>
</RelativeLayout>
           

主布局很簡單,就是一個ListView,再來看看ListView的item布局:

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

    <data>

        <variable
            name="food"
            type="org.lenve.databinding3.Food"/>
    </data>

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="96dp"
        android:orientation="vertical">

        <ImageView
            android:id="@+id/iv"
            android:layout_width="96dp"
            android:layout_height="96dp"
            android:padding="6dp"
            app:img="@{food.img}"/>

        <TextView
            android:id="@+id/description"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="8dp"
            android:layout_toRightOf="@id/iv"
            android:ellipsize="end"
            android:maxLines="3"
            android:text="@{food.description}"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="8dp"
            android:layout_toRightOf="@id/iv"
            android:layout_alignParentBottom="true"
            android:layout_marginBottom="2dp"
            android:text="@{food.keywords}"
            android:textStyle="bold"/>
    </RelativeLayout>
</layout>
           

圖檔加載、文本加載前兩節都已經說過了,這裡的東西就沒有什麼難度了,我們再來看看實體類Food:

/**
 * Created by 王松 on 2016/7/31.
 */
public class Food {
    private String description;
    private String img;
    private String keywords;
    private String summary;

    public Food() {
    }

    public Food(String description, String img, String keywords, String summary) {
        this.description = description;
        this.img = img;
        this.keywords = keywords;
        this.summary = summary;
    }

    @BindingAdapter("bind:img")
    public static void loadInternetImage(ImageView iv, String img) {
        Picasso.with(iv.getContext()).load(img).into(iv);
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public String getImg() {
        return img;
    }

    public void setImg(String img) {
        this.img = img;
    }

    public String getKeywords() {
        return keywords;
    }

    public void setKeywords(String keywords) {
        this.keywords = keywords;
    }

    public String getSummary() {
        return summary;
    }

    public void setSummary(String summary) {
        this.summary = summary;
    }
}
           

這個實體類中有一個加載圖檔的方法,加載方式我們上文都已經介紹過了,不多說。好了,再來看看我們的終極Adapter類:

/**
 * Created by 王松 on 2016/7/31.
 */
public class MyBaseAdapter<T> extends BaseAdapter {
    private Context context;
    private LayoutInflater inflater;
    private int layoutId;
    private int variableId;
    private List<T> list;

    public MyBaseAdapter(Context context, int layoutId, List<T> list, int resId) {
        this.context = context;
        this.layoutId = layoutId;
        this.list = list;
        this.variableId = resId;
        inflater = LayoutInflater.from(context);
    }

    @Override

    public int getCount() {
        return list.size();
    }

    @Override
    public Object getItem(int position) {
        return list.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewDataBinding dataBinding;
        if (convertView == null) {
            dataBinding = DataBindingUtil.inflate(inflater, layoutId, parent, false);
        }else{
            dataBinding = DataBindingUtil.getBinding(convertView);
        }
        dataBinding.setVariable(variableId, list.get(position));
        return dataBinding.getRoot();
    }
}
           

這個大概算是Adapter的終極寫法了,如果你按這種方式來寫Adapter,那麼如果沒有非常奇葩的需求,你這個App中可能就隻有這一個給ListView使用的Adapter了,為什麼這麼說呢?因為這個Adapter中沒有一個變量和我們的ListView沾邊,解釋一下幾個變量吧:layoutId這個表示item布局的資源id,variableId是系統自動生成的,根據我們的實體類,直接從外部傳入即可。另外注意布局加載方式為DataBindingUtil類中的inflate方法。OK,最後再來看看Activity:

public class MainActivity extends AppCompatActivity {

    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            MyBaseAdapter<Food> adapter = new MyBaseAdapter<>(MainActivity.this, R.layout.listview_item, foods, org.lenve.databinding3.BR.food);
            lv.setAdapter(adapter);
        }
    };
    private List<Food> foods;
    private ListView lv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        lv = ((ListView) findViewById(R.id.lv));
        initData();
    }

    private void initData() {
        OkHttpClient client = new OkHttpClient.Builder().build();
        Request request = new Request.Builder().url("http://www.tngou.net/api/food/list?id=1").build();
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {

            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                if (response.isSuccessful()) {
                    parseJson(response.body().string());
                }
            }
        });
    }

    private void parseJson(String jsonStr) {
        foods = new ArrayList<>();
        try {
            JSONObject jo = new JSONObject(jsonStr);
            JSONArray tngou = jo.getJSONArray("tngou");
            for (int i = 0; i < tngou.length(); i++) {
                JSONObject item = tngou.getJSONObject(i);
                String description = item.getString("description");
                String img = "http://tnfs.tngou.net/image"+item.getString("img");
                String keywords = "【關鍵詞】 "+item.getString("keywords");
                String summary = item.getString("summary");
                foods.add(new Food(description, img, keywords, summary));
            }
            mHandler.sendEmptyMessage(0);
        } catch (JSONException e) {
            e.printStackTrace();
        }
    }
}
           

OkHttp下載下傳資料和Json解析自不用多說,在構造MyAdapter的時候傳入的最後一個參數,是BR中的,這個BR和我們項目中的R檔案類似,都是系統自動生成的。

至此,我們使用DataBinding的方式來給ListView加載資料就算完成了。so easy~~~

4.點選事件處理

如果你使用DataBinding,我們的點選事件也會有新的處理方式,首先以ListView為例來說說如何綁定點選事件,在listview_item布局檔案中每一個item的根節點添加如下代碼:

<?xml version="1.0" encoding="utf-8"?>
<layout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    >
	....
	....
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="96dp"
        android:onClick="@{food.onItemClick}"
        android:orientation="vertical">

        <ImageView
            android:id="@+id/iv"
            android:layout_width="96dp"
            android:layout_height="96dp"
            android:padding="6dp"
            app:img="@{food.img}"/>
		....
		....
		....
    </RelativeLayout>
</layout>
           

OK,我給RelativeLayout容器添了onClick屬性,屬性的值為food.onItemClick,那麼這個onItemClick到底是什麼呢?其實就是在實體類Food中定義的一個方法,如下:

public void onItemClick(View view) {
        Toast.makeText(view.getContext(), getDescription(), Toast.LENGTH_SHORT).show();
    }
           

點選item擷取目前position的資料,擷取方式也是非常簡單,直接get方法擷取即可,比傳統的ListView的點選事件通過position來擷取資料友善多了。如果我想為關鍵字這個TextView添加點選事件也很簡單,和上面一樣,這裡我就不再貼代碼了,文末可以下載下傳源碼。

5. 資料更新處理

單純的更新Food對象并不能改變ListView的UI顯示效果,那該怎麼做呢?Google給我們提供了三種解決方案,分别如下:

1.讓實體類繼承自BaseObservable

讓實體類繼承自BaseObservable,然後給需要改變的字段的get方法添加上@Bindable注解,然後給需要改變的字段的set方法加上notifyPropertyChanged(org.lenve.databinding3.BR.description);一句即可,比如我想點選item的時候把description字段的資料全部改為111,我可以修改Food類變為下面的樣子:

public class Food extends BaseObservable {
    private String description;
    private String img;
    private String keywords;
    private String summary;

    public Food() {
    }

    public Food(String description, String img, String keywords, String summary) {
        this.description = description;
        this.img = img;
        this.keywords = keywords;
        this.summary = summary;
    }

    @BindingAdapter("bind:img")
    public static void loadInternetImage(ImageView iv, String img) {
        Picasso.with(iv.getContext()).load(img).into(iv);
    }

    public void onItemClick(View view) {
//        Toast.makeText(view.getContext(), getDescription(), Toast.LENGTH_SHORT).show();
        setDescription("111");
    }

    public void clickKeywords(View view) {
        Toast.makeText(view.getContext(), getKeywords(), Toast.LENGTH_SHORT).show();
    }


    @Bindable
    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
        notifyPropertyChanged(org.lenve.databinding3.BR.description);
    }

    public String getImg() {
        return img;
    }

    public void setImg(String img) {
        this.img = img;
    }

    public String getKeywords() {
        return keywords;
    }

    public void setKeywords(String keywords) {
        this.keywords = keywords;
    }

    public String getSummary() {
        return summary;
    }

    public void setSummary(String summary) {
        this.summary = summary;
    }
}
           

OK,這是第一種解決方案,也是比較簡單常用的一種。

2.使用DataBinding提供的ObservableFields來建立實體類

這種方式使用起來略微麻煩,除了繼承BaseObservable之外,建立屬性的方式也變成下面這種:

private final ObservableField<String> description = new ObservableField<>();
           

屬性的讀寫方式也變了,讀取方式如下:

description.get()
           

寫入方式如下:

this.description.set(description);
           

OK,依據上面幾個規則,我新定義的實體類如下:

/**
 * Created by 王松 on 2016/7/31.
 */
public class Food extends BaseObservable {
    private final ObservableField<String> description = new ObservableField<>();
    private final ObservableField<String> img = new ObservableField<>();
    private final ObservableField<String> keywords = new ObservableField<>();
    private final ObservableField<String> summary = new ObservableField<>();
    public Food() {
    }

    public Food(String description, String img, String keywords, String summary) {
        this.description.set(description);
        this.keywords.set(keywords);
        this.img.set(img);
        this.summary.set(summary);
    }

    @BindingAdapter("bind:img")
    public static void loadInternetImage(ImageView iv, String img) {
        Picasso.with(iv.getContext()).load(img).into(iv);
    }

    public void onItemClick(View view) {
//        Toast.makeText(view.getContext(), getDescription(), Toast.LENGTH_SHORT).show();
        setDescription("111");
    }

    public void clickKeywords(View view) {
        Toast.makeText(view.getContext(), getKeywords(), Toast.LENGTH_SHORT).show();
    }


    @Bindable
    public String getDescription() {
        return description.get();
    }

    public void setDescription(String description) {
        this.description.set(description);
        notifyPropertyChanged(org.lenve.databinding3.BR.description);
    }

    public String getImg() {
        return img.get();
    }

    public void setImg(String img) {
        this.img.set(img);
    }

    public String getKeywords() {
        return keywords.get();
    }

    public void setKeywords(String keywords) {
        this.keywords.set(keywords);
    }

    public String getSummary() {
        return summary.get();
    }

    public void setSummary(String summary) {
        this.summary.set(summary);
    }
}
           

這種方式實作的功能和第一個實體類實作的功能一模一樣。

3.使用DataBinding中提供的集合來存儲資料即可

DataBinding中給我們提供了一些現成的集合,用來存儲資料,比如ObservableArrayList,ObservableArrayMap,因為這些用的少,我這裡就不做介紹了。

本文共涉及到三個Demo,由于CSDN對上傳檔案大小的限制,我分三次上傳,下載下傳位址如下:

1.http://download.csdn.net/detail/u012702547/9591142

2.http://download.csdn.net/detail/u012702547/9591150

3.http://download.csdn.net/detail/u012702547/9591160

以上。