玩轉Android之MVVM開發模式實戰,炫酷的DataBinding!

C# 很早就有了MVVM的開發模式,Android手機中的MVVM一直到去年Google的I\O大會上才推出,姍姍來遲。MVVM這中開發模式的優勢自沒必要多說,能夠實現視圖和邏輯代碼的解耦,並且,按照Google的說法,使用了MVVM的開發模式,還能夠提升佈局文件的解析速度,我的以爲這一點很是重要。咱們在安卓開發中常常須要寫不少個findViewById,讓人心煩,不少人不想寫這個因而用了一些註解框架,但是註解框架不管性能多好,效率老是要低於findViewById的,所以,Android中的MVVM也即databinding能夠幫助咱們完全解決這個問題。OK,廢話很少說,咱們來看看具體要怎麼在Android開發中使用MVVM。android

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

 

1.基本使用api

2.綁定ImageView網絡

3.綁定ListViewapp

4.點擊事件處理框架

5.數據更新處理ide

 

好了,那就開始吧!佈局

1.基本使用

建立好一個Android Project以後,在gradle文件中添加以下幾行代碼,表示開啓databinding:性能

 

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

就是這麼簡單,一個簡單的databinding配置以後,就能夠開始使用數據綁定了。

 

要使用數據綁定,咱們得首先建立一個實體類,好比User實體類,以下:gradle

 

/**
 * 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);
    }

 

 

運行,顯示效果以下:

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:userface="@{user.userface}"></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", "張三"));
    }

就是這麼簡單,加上網絡權限就能夠運行了,運行效果以下:

 

 

3.綁定ListView

好了,看完了簡單使用以後,不知道你有沒有喜歡上DataBinding,若是尚未,那就再來看看使用DataBinding來給ListView綁定數據吧,這個你必定會喜歡上的。由於使用這中方式來綁定太簡單了。

先來看看咱們要作的效果吧:

就是一個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,由於這些用的少,我這裏就不作介紹了。

以上。

相關文章
相關標籤/搜索