1. MVVM 模式
架構(gòu)理解
MVVM 模式,即指 Model-View-ViewModel。它將 View 的狀態(tài)和行為完全抽象化,把邏輯與界面的控制完全交給 ViewModel 處理。如下圖:
官方:https://github.com/googlesamples/android-architecture/tree/todo-mvvm-databinding/
- 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)點
-
低耦合度
在 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 不需要做太多的改動。 -
數(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 更新。 -
異步線程更新 Model
Model 數(shù)據(jù)可以在異步線程中發(fā)生變化,此時調(diào)用者不需要做額外的處理,數(shù)據(jù)綁定框架會將異步線程中數(shù)據(jù)的變化通知到 UI 線程中交給 View 去更新。 -
方便協(xié)作
View 層和邏輯層幾乎沒有耦合,在團隊協(xié)作的過程中,可以一個人負責 UI,一個人負責數(shù)據(jù)處理。并行開發(fā),保證開發(fā)進度。 -
易于單元測試
MVVM 模式比較易于進行單元測試。ViewModel 層只負責處理數(shù)據(jù),在進行單元測試時,測試不需要構(gòu)造一個 fragment/Activity/TextView 等等來進行數(shù)據(jù)層的測試。同理 View 層也一樣,只需要輸入指定格式的數(shù)據(jù)即可進行測試,而且兩者相互獨立,不會互相影響。 -
數(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)點
- 再也不需要編寫 findViewById
- 更新 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>