Android MVVM 入門教程

1. MVVM 模式

架構(gòu)理解

MVVM 模式,即指 Model-View-ViewModel。它將 View 的狀態(tài)和行為完全抽象化,把邏輯與界面的控制完全交給 ViewModel 處理。如下圖:


官方:https://github.com/googlesamples/android-architecture/tree/todo-mvvm-databinding/
MVVM 由下面三層組成:

  • View:主要進行視圖控件的一些初始設(shè)置,不應該有任何的數(shù)據(jù)邏輯操作。
  • Model:定義實體類,以及獲取業(yè)務(wù)數(shù)據(jù)模型,比如通過數(shù)據(jù)庫或者網(wǎng)絡(luò)來操作數(shù)據(jù)等。
  • ViewModel:作為連接 View 與 Model 的中間橋梁,ViewModel 與 Model 直接交互,處理完業(yè)務(wù)邏輯后,通過 DataBinding 將數(shù)據(jù)變化反應到用戶界面上。
優(yōu)點
  1. 低耦合度
    在 MVVM 模式中,數(shù)據(jù)處理邏輯是獨立于 UI 層的。ViewModel 只負責提供數(shù)據(jù)和處理數(shù)據(jù),不會持有 View 層的引用。而 View 層只負責對數(shù)據(jù)變化的監(jiān)聽,不會處理任何跟數(shù)據(jù)相關(guān)的邏輯。在 View 層的 UI 發(fā)生變化時,也不需要像 MVP 模式那樣,修改對應接口和方法實現(xiàn),一般情況下ViewModel 不需要做太多的改動。
  2. 數(shù)據(jù)驅(qū)動
    MVVM 模式的另外一個特點就是數(shù)據(jù)驅(qū)動。UI 的展現(xiàn)是依賴于數(shù)據(jù)的,數(shù)據(jù)的變化會自然的引發(fā) UI 的變化,而 UI 的改變也會使數(shù)據(jù) Model 進行對應的更新。ViewModel 只需要處理數(shù)據(jù),而 View 層只需要監(jiān)聽并使用數(shù)據(jù)進行 UI 更新。
  3. 異步線程更新 Model
    Model 數(shù)據(jù)可以在異步線程中發(fā)生變化,此時調(diào)用者不需要做額外的處理,數(shù)據(jù)綁定框架會將異步線程中數(shù)據(jù)的變化通知到 UI 線程中交給 View 去更新。
  4. 方便協(xié)作
    View 層和邏輯層幾乎沒有耦合,在團隊協(xié)作的過程中,可以一個人負責 UI,一個人負責數(shù)據(jù)處理。并行開發(fā),保證開發(fā)進度。
  5. 易于單元測試
    MVVM 模式比較易于進行單元測試。ViewModel 層只負責處理數(shù)據(jù),在進行單元測試時,測試不需要構(gòu)造一個 fragment/Activity/TextView 等等來進行數(shù)據(jù)層的測試。同理 View 層也一樣,只需要輸入指定格式的數(shù)據(jù)即可進行測試,而且兩者相互獨立,不會互相影響。
  6. 數(shù)據(jù)復用
    ViewModel 層對數(shù)據(jù)的獲取和處理邏輯,尤其是使用 Repository 模式時,獲取數(shù)據(jù)的邏輯完全是可以復用的。開發(fā)者可以在不同的模塊,多次方便的獲取同一份來源的數(shù)據(jù)。同樣的一份數(shù)據(jù),在版本功能迭代時,邏輯層不需要改變,只需要改變 View 層即可。

2. DataBinding

在使用 MVVM 模式之前,我們必須了解 DataBinding。

簡介

首先要明確一個 DataBinding 與 MVVM 之間的關(guān)系 ↓
MVVM 是一種思想,一種架構(gòu)模式,而 DataBinding 是谷歌推出的方便實現(xiàn) MVVM 的工具。
在 DataBinding 庫之前,我們經(jīng)常會寫一些重復性很高而且毫無營養(yǎng)的代碼,比如:findViewById()、setText()、setOnClickListener() 等。直到2015谷歌 I/O大會推出了 DataBinding,一個實現(xiàn)視圖和數(shù)據(jù)雙向綁定的工具。使用 DataBinding 庫以后,可以使用聲明式布局文件來減少粘結(jié)業(yè)務(wù)邏輯和布局文件的膠水代碼,有利于開發(fā)者更方便地實現(xiàn) MVVM 模式。

環(huán)境配置

在 Module:app 的 build.gradle 文件添加如下代碼:

android {
    // ...
    dataBinding {
        enabled = true
    }
}
使用方法

使用 DataBinding 的布局文件和普通的布局文件有點不同,DataBinding 布局文件的根標簽是 layout 標簽,layout 里面有一個 data 元素和 View 元素,這個 View 元素就是我們沒使用DataBinding時候的布局文件。例子代碼如下:

 <layout xmlns:android="http://schemas.android.com/apk/res/android">  
  
    <data>  
        <variable  
            name="user"  
            type="com.example.mvvmdemo.UserBean"/>  
    </data>  
  
    <LinearLayout  
        android:orientation="vertical" android:layout_width="match_parent"  
        android:layout_height="match_parent">  
        <TextView  
            android:layout_width="match_parent"  
            android:layout_height="wrap_content"  
            android:text="@{user.name}"/>  
        <TextView  
            android:layout_width="match_parent"  
            android:layout_height="wrap_content"  
            android:text="@{user.sex}"/>  
    </LinearLayout>

</layout>

data 元素里面的 user 就是我們自定義的 user 實體類,當我們向 DataBinding 中設(shè)置好 user 類以后,我們的兩個 TextView 會自動設(shè)置 text 的值。
UserBean實體類代碼如下:

public class UserBean {  
  
    public ObservableField<String> name = new ObservableField<>();  
    public ObservableField<String> sex = new ObservableField<>();  
 
    public UserBean(){  
        name.set("王小明");  
        sex.set("男");  
    }  
}

這個實體類的元素是 DataBinding 中的 ObservableField 類,ObservableField 的作用是,當我們實體類中的值發(fā)生改變時,會自動通知View刷新。所以使用 DataBinding 的時候,建議使用 ObservableField 來定義實體類。
之后,我們只需要在 Activity 中綁定 layout 就可以了。下面是使用代碼:

ActivityMainBinding activityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);  
UserBean user = new UserBean();  
activityMainBinding.setUser(user);

在使用 DataBinding 的時候,我們設(shè)置布局使用 DataBindingUtil 工具類中的 setContentView() 方法。設(shè)置好了 user 后,layout 中的 TextView便顯示為"王小明"和"男"。

優(yōu)點
  1. 再也不需要編寫 findViewById
  2. 更新 UI 數(shù)據(jù)時不需再切換至 UI 線程

在某篇博客上看到這樣一段評價,直接引用:

針對第一個優(yōu)點,有人說,已經(jīng)有 ButterKnife 了。針對第二個優(yōu)點,也有人說,有 RxJava 了。但是 DataBinding,不僅僅能解決這2個問題,它的核心優(yōu)勢在于,它解決了將數(shù)據(jù)分解映射到各個 view 的問題。針對每個 Activity 或者 Fragment 的布局,在編譯階段,它會生成一個ViewDataBinding 類的對象,該對象持有 Activity 要展示的數(shù)據(jù)和布局中的各個 view 的引用。同時還有如下優(yōu)勢:將數(shù)據(jù)分解到各個 view、在 UI 線程上更新數(shù)據(jù)、監(jiān)控數(shù)據(jù)的變化,實時更新,這樣一來,你要展示的數(shù)據(jù)已經(jīng)和展示它的布局緊緊綁定在了一起。這才是 DataBinding 真正的魅力所在。

PS:個人感覺有點像前端 React Redux 的單向數(shù)據(jù)流。

3. 簡單實踐

項目

Demo:使用 MVVM 模式,利用 Retrofit 獲取今日頭條首頁10條熱門新聞推薦,并以 RecyclerView 展示在 APP 布局界面。數(shù)據(jù)用 DataBinding 進行綁定響應。
目的:希望通過實踐,對 MVVM 模式能夠理解得更深刻。



項目文件目錄如下:


布局文件

activity_main.xml

<?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"
        xmlns:tools="http://schemas.android.com/tools">

    <data>
        <variable
            name="viewModel"
            type="com.example.chenguiyan.toutiaofeed.viewmodel.MainViewModel"/>
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <android.support.v7.widget.RecyclerView
            android:id="@+id/recycler_view"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>
    </LinearLayout>

</layout>

item_news.xml

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

<data>
    <variable
        name="news"
        type="com.example.chenguiyan.toutiaofeed.model.News"/>
</data>

<LinearLayout
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/news_title"
        android:text="@{news.title}"
        android:paddingTop="5dp"
        android:paddingLeft="5dp"
        android:paddingRight="5dp"
        android:textSize="15sp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

    <LinearLayout
        android:paddingBottom="5dp"
        android:paddingLeft="5dp"
        android:paddingRight="5dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <TextView
            android:textColor="#acacac"
            android:text="來源:"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
        <TextView
            android:text="@{news.source}"
            android:textColor="#acacac"
            android:id="@+id/news_source"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
    </LinearLayout>

    <ImageView
        android:background="#acacac"
        android:layout_width="match_parent"
        android:layout_height="1dp"/>

</LinearLayout>

</layout>
View

MainActivity.java

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";

    public ActivityMainBinding mActivityMainBinding;
    private MainViewModel mViewModel;

    public NewsAdapter mNewsAdapter;
    public List<News> mNewsList = new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 設(shè)置dataBinding、viewModel
        mActivityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        mViewModel = new MainViewModel(this);
        mActivityMainBinding.setViewModel(mViewModel);
        // 初始化RecyclerView
        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
        mActivityMainBinding.recyclerView.setLayoutManager(layoutManager);
        mNewsAdapter = new NewsAdapter(this, mNewsList);
        mActivityMainBinding.recyclerView.setAdapter(mNewsAdapter);
        // 加載數(shù)據(jù)
        mViewModel.loadNews();
    }
}
Model

Feed.java

public class Feed {
    private boolean has_more;
    private String message;
    private List<News> data;

    public void setHas_more(boolean has_more) {
        this.has_more = has_more;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public void setData(List<News> data) {
        this.data = data;
    }

    public boolean isHas_more() {
        return has_more;
    }

    public String getMessage() {
        return message;
    }

    public List<News> getData() {
        return data;
    }

    // 通過傳進來的url,利用retrofit獲取網(wǎng)絡(luò)數(shù)據(jù),回調(diào)給viewModel
    public void loadData(String feedUrl, final LoadListener<News> loadListener) {
        OkHttpClient okHttpClient = new OkHttpClient();
        Retrofit retrofit = new Retrofit.Builder()
                .client(okHttpClient)
                .baseUrl(feedUrl)
                .addConverterFactory(GsonConverterFactory.create())
                .build();
        INews iNews = retrofit.create(INews.class);
        Call<Feed> feed = iNews.getFeed();
        feed.enqueue(new Callback<Feed>() {
            @Override
            public void onResponse(Call<Feed> call, Response<Feed> response) {
                // 獲取成功
                List<News> newsList = new ArrayList<>();
                for (int i = 0; i < response.body().getData().size(); i++) {
                    newsList.add(response.body().getData().get(i));
                }
                loadListener.loadSuccess(newsList);
            }

            @Override
            public void onFailure(Call<Feed> call, Throwable t) {
                // 獲取失敗
                loadListener.loadFailure(t.getMessage());
            }
        });
    }
}

News.java

public class News {
    private String title;
    private String item_id;
    private String source;

    public News(String title, String item_id, String source) {
        this.title = title;
        this.item_id = item_id;
        this.source = source;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public void setItem_id(String item_id) {
        this.item_id = item_id;
    }

    public void setSource(String source) {
        this.source = source;
    }

    public String getTitle() {
        return title;
    }

    public String getItem_id() {
        return item_id;
    }

    public String getSource() {
        return source;
    }
}
ViewModel

MainViewModel.java

public class MainViewModel {

    private static final String TAG = "MainViewModel";
    private MainActivity mActivity;
    private String feedUrl;

    public MainViewModel(MainActivity activity) {
        mActivity = activity;
    }

    public void loadNews() {
        // 獲取url
        feedUrl = mActivity.getResources().getString(R.string.feed_api_url);
        // 加載數(shù)據(jù)
        Feed feed = new Feed();
        feed.loadData(feedUrl, new LoadListener<News>() {
            @Override
            public void loadSuccess(List<News> list) {
                // 加載數(shù)據(jù)成功
                mActivity.mNewsList.addAll(list);
                mActivity.mNewsAdapter.notifyDataSetChanged();
            }
            @Override
            public void loadFailure(String message) {
                // 加載數(shù)據(jù)失敗
            }
        });
    }
}
Other

NewsAdapter.java

public class NewsAdapter extends RecyclerView.Adapter {

    private Context mContext;
    private List<News> newsList;
//    private OnItemClickListener mOnItemClickListener = null;

    public static class ViewHolder extends RecyclerView.ViewHolder {
        ItemNewsBinding mItemNewsBinding;

        public ViewHolder(ItemNewsBinding itemNewsBinding) {
            super(itemNewsBinding.getRoot());
            this.mItemNewsBinding = itemNewsBinding;
        }
    }

    public NewsAdapter(Context mContext, List<News> newsList) {
        this.mContext = mContext;
        this.newsList = newsList;
    }

    @NonNull

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
        ItemNewsBinding itemNewsBinding = DataBindingUtil.inflate(LayoutInflater.from(mContext), R.layout.item_news, viewGroup, false);
        // View view = LayoutInflater.from(mContext).inflate(R.layout.item_news, viewGroup, false);
        return new ViewHolder(itemNewsBinding);
    }

    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, final int position) {
        ViewHolder mViewHolder = (ViewHolder) viewHolder;
        // dataBinding綁定
        News news = newsList.get(position);
        mViewHolder.mItemNewsBinding.setNews(news);
        // 設(shè)置點擊事件,將接口方法回調(diào)給MainActivity
//        if (mOnItemClickListener != null) {
//            mViewHolder.mItemNewsBinding.getRoot().setOnClickListener(new View.OnClickListener() {
//                @Override
//                public void onClick(View v) {
//                    mOnItemClickListener.onShortClick(position);
//                }
//            });
//            mViewHolder.mItemNewsBinding.getRoot().setOnLongClickListener(new View.OnLongClickListener() {
//                @Override
//                public boolean onLongClick(View v) {
//                    mOnItemClickListener.onLongClick(position);
//                    return false;
//                }
//            });
//        }
        // 直接在adapter里設(shè)置點擊事件
        mViewHolder.mItemNewsBinding.getRoot().setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String newsUrlPrefix = mContext.getResources().getString(R.string.news_url_prefix);
                String httpUrl = newsUrlPrefix + newsList.get(position).getItem_id();
                Intent intent = new Intent(mContext, WebViewActivity.class);
                intent.putExtra("httpUrl", httpUrl);
                mContext.startActivity(intent);
            }
        });
        mViewHolder.mItemNewsBinding.getRoot().setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                return false;
            }
        });
    }

    @Override
    public int getItemCount() {
        return newsList.size();
    }

    // 定義點擊事件的接口
//    public interface OnItemClickListener {
//        void onShortClick(int position); // 單擊
//        void onLongClick(int position); // 長按
//    }

//    public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
//        this.mOnItemClickListener = onItemClickListener;
//    }
}

INews.java

public interface INews {
    @GET(".")
    Call<Feed> getFeed();
}

LoadListener.java

public interface LoadListener<T> {
    void loadSuccess(List<T> list);
    void loadFailure(String message);
}

strings.xml

<resources>
    <string name="feed_api_url">https://www.toutiao.com/api/pc/feed/</string>
    <string name="news_url_prefix">https://www.toutiao.com/a</string>
</resources>
Json解析

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容