MVVM + data-binding 快速入門

前言

  1. 簡書上data-binding 的文章不少,但真正用來實(shí)現(xiàn)MVVM架構(gòu)的文章不多。有些是官方的guide(https://developer.android.com/topic/libraries/data-binding/index.html) 的翻譯版本,且官方的guide的架構(gòu)主要采用 data-binding + mvp 的形式。 本文講述一個(gè)快速入門的data- binding + mvvm架構(gòu)。

基本配置

詳見官方
gradle ,當(dāng)然 gradle版本需要在1.5.0-alpha1 or higher

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

快速demo入門

  1. 具體就是用了個(gè)gank.io的接口展示了一組妹子圖片。不多說,看結(jié)果:


    ezgif.com-gif-maker.gif
  2. gradle配置
    compile fileTree(include: ['*.jar'], dir: 'libs')
    compile 'com.android.support:appcompat-v7:24.1.1'
    compile 'com.squareup.retrofit2:retrofit:2.1.0'
    compile 'com.github.bumptech.glide:glide:3.7.0'
    compile 'com.squareup.retrofit2:converter-gson:2.1.0'
    compile 'com.android.support:cardview-v7:24.1.1'

這里本來demo不應(yīng)該引入其他的庫的,但為了方便寫demo,故添加了retrofit來請(qǐng)求數(shù)據(jù),glide來加載圖片。

  1. 數(shù)據(jù)請(qǐng)求封裝Model 省略getter和setter
/**
 * @Description: error: false,
 * results: [
 * {
 * _id: "57bc5238421aa9125fa3ed70",
 * createdAt: "2016-08-23T21:40:08.159Z",
 * desc: "8.24",
 * publishedAt: "2016-08-24T11:38:48.733Z",
 * source: "chrome",
 * type: "福利",
 * url: "http://ww3.sinaimg.cn/large/610dc034jw1f740f701gqj20u011hgo9.jpg",
 * used: true,
 * who: "daimajia"
 * },
 * ]
 */
public class MeiZiModel  implements  Serializable{
    private String error;
    private List<Result> results;
    public static class Result{
        private String _id;
        private String desc;
        private String publishedAt;
        private String createdAt;
        private String source;
        private String url;
        private String used;
        private String who;
    }
}
  1. 使用retrofit 請(qǐng)求數(shù)據(jù)如下:
public interface MeiZiService {
    @GET("api/data/福利/{page}/{number}")
    Call<MeiZiModel> getMeiZi(@Path("page") int page, @Path("number") int number);
}

簡單解釋下,@GET當(dāng)然是指get的方式請(qǐng)求,后面是具體的路徑。{page}/{number}這個(gè)是我隨便填的參數(shù)名,測試了下gank的接口,確實(shí)可以通過修改這兩個(gè)參數(shù)請(qǐng)求不同頁面的數(shù)據(jù)。
@path 就是path路徑上的變量。

  1. 回調(diào)獲取返回結(jié)果:
public class ServiceGenerator {

    public static final String API_BASE_URL = "http://gank.io";

    private static Retrofit retrofit =
            new Retrofit.Builder()
                    .baseUrl(API_BASE_URL)
                    .addConverterFactory(GsonConverterFactory.create())
                    .build();

    public static void getMeiZi(int page, int number, final MeiZiCallBack callBack) {
        MeiZiService service = retrofit.create(MeiZiService.class);
        Call<MeiZiModel> meiziCall = service.getMeiZi(page, number);
        meiziCall.enqueue(new Callback<MeiZiModel>() {
            @Override
            public void onResponse(Call<MeiZiModel> call, Response<MeiZiModel> response) {
                if (callBack != null) {
                    callBack.onSuccess(response.body().getResults());
                }
            }

            @Override
            public void onFailure(Call<MeiZiModel> call, Throwable t) {
                if (callBack != null) {
                    callBack.onFail(t.getMessage());
                }
            }
        });
    }
}

注意整個(gè)url 為http://gank.io/ api/data/福利/{page}/{number} 后面的兩個(gè)為需要傳入的參數(shù),然后把retrofit請(qǐng)求結(jié)果回調(diào)即可。

6 MainActivity

public class MainActivity extends AppCompatActivity {
    private ListView mListView;
    private MeiziAdapter mMeiziAdapter;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mListView = (ListView) findViewById(R.id.list);
        mMeiziAdapter = new MeiziAdapter(this);

        ServiceGenerator.getMeiZi(11, 2, new MeiZiCallBack() {
            @Override
            public void onSuccess(List<MeiZiModel.Result> result) {
                mMeiziAdapter.setDatas(result);
                mListView.setAdapter(mMeiziAdapter);
            }

            @Override
            public void onFail(String error) {
               //TODO 沒處理失敗的情況。
            }
        });
    }
}

很簡單,將數(shù)據(jù)塞給adpter。

ViewModel 和View

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

    <data>
        <import type="android.view.View"/>
        <variable
            name="viewModel"
            type="com.example.xxx.mvvmdemo.ViewModel.ItemViewModel">
        </variable>
    </data>

    <android.support.v7.widget.CardView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <com.example.xxx.mvvmdemo.View.CustomImage
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            bind:load = "@{viewModel.imageUrl}"
            android:onClick="@{viewModel::onItemClick}"
            />
        
        <TextView
            android:layout_width="60dp"
            android:layout_height="wrap_content" 
            android:visibility="@{viewModel.isVisibility ? View.VISIBLE : View.GONE }"
            android:text="@{viewModel.text}"
            />

    </android.support.v7.widget.CardView>
</layout>

這里需要注意的幾點(diǎn):
(1) 先看整個(gè)頭部,添加了 layout 和 data 部分。 data 部分import 導(dǎo)入需要使用的包,如textView的 visibility需要 View.VISIBLE 常數(shù),存在于 android.view.View類。
(2) 引入自定義的viewModel 注意,這里變量name 為viewModel ,type具體的類。
(3) 針對(duì)簡單的TextView 的text 直接使用@{viewModel.text},viewModel.text 這個(gè)默認(rèn)回去訪問text的getter方法getText(),但找不到時(shí),會(huì)去找text的公共屬性text。
(4) data- binding 支持三元運(yùn)算符 @{viewModel.isVisibility ? View.VISIBLE : View.GONE } 還支持邏輯運(yùn)算等等,詳見
https://developer.android.com/topic/libraries/data-binding/index.html#expression_language
(5) 特殊情形,如這里是imageView,我想讓其自動(dòng)加載圖片。這里是使用三方庫Glide加載,怎么辦呢?使用BindingMethod 。自定義 bind:load = "@{viewModel.imageUrl}"
load方法時(shí),我們通過url參數(shù),直接在網(wǎng)上下載該圖片。具體見CustomImageAdapter類通過注解 @BindingAdapter("load") 來指明需要?jiǎng)討B(tài)加載的圖片url。這里還有點(diǎn)特殊,imageView沒有使用Glide加載圖片的接口,故擴(kuò)展了load方法,詳見CustomImage。

public class ItemViewModel {
    private final ObservableBoolean isVisibility = new ObservableBoolean(false);
    private final ObservableField<String> mImageUrl = new ObservableField<>();
    private final ObservableField<String> text = new ObservableField<>();


    public Context mContext;
    public String mUrl;


    public ItemViewModel(Context mContext) {
        this.mContext = mContext;
    }
    public void setData(MeiZiModel.Result result,boolean isShowText){
        if(result == null ){
            return;
        }
        mImageUrl.set(result.getUrl());
        mUrl = result.getUrl();
        text.set(result.getDesc());
        isVisibility.set(isShowText);
    }

    public ObservableBoolean getIsVisibility() {
        return isVisibility;
    }
    public ObservableField<String> getImageUrl() {
        return mImageUrl;
    }
    public ObservableField<String> getText() {
        return text;
    }

}

public class MeiziAdapter extends BaseAdapter {

    private Context mContext;
    private List<MeiZiModel.Result> mDatas;

    public void setDatas(List<MeiZiModel.Result> mDatas) {
        this.mDatas = mDatas;
    }

    public MeiziAdapter(Context mContext) {
        this.mContext = mContext;
    }

    @Override
    public int getCount() {
        return mDatas == null ? 0 : mDatas.size();
    }

    @Override
    public Object getItem(int i) {
        return mDatas == null || mDatas.size() == 0 ? null : mDatas.get(i);
    }

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

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ItemViewModel viewModel;
        if (convertView == null) {
            ItemViewBinding binding = DataBindingUtil.inflate((LayoutInflater) mContext.getApplicationContext().getSystemService
                    (Context.LAYOUT_INFLATER_SERVICE), R.layout.item_view, null, false);
            viewModel = new ItemViewModel(mContext);
            binding.setViewModel(viewModel);
            convertView = binding.getRoot();
            convertView.setTag(viewModel);
        } else {
            viewModel = (ItemViewModel) convertView.getTag();
        }
        viewModel.setData(mDatas.get(position),position % 2 == 0);
        return convertView;
    }
}

這里需要注意ItemViewBinding 類 是data-binding給我們生成的,采用我們布局的xml下劃線改為駝峰命名,最后加上Binding作為區(qū)分。 其實(shí)這里跟holder類似, 不過這里的優(yōu)點(diǎn)是可以重用ViewModel的邏輯。

public class CustomImage extends ImageView {
    public CustomImage(Context context) {
        super(context);
    }

    public CustomImage(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public CustomImage(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    public void load(String url){
        Glide.with(this.getContext().getApplicationContext()).load(url).fitCenter().into(this);
    }
}
public class CustomImageAdapter {
    @BindingAdapter("load")
    public static void load(CustomImage imageView,String url){
        imageView.load(url);
    }
}

more

整個(gè)工程結(jié)構(gòu)講完了,這里mvvm的形式,為什么比一般的mvc更好呢?
答案是ViewModel和View的弱耦合,這里就可以多次重用。比如重用xml或重用ViewModel或者整個(gè)重用。最關(guān)鍵的是這樣減輕了Activity的工作量,可以模塊化去拆分每個(gè)頁面的邏輯,做到更多的復(fù)用。
ps: data binding 報(bào)錯(cuò)
Cannot find the setter for attribute '' with parameter type int on class.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容