**PS:這是我第一次寫文章荤西,小弟才疏學(xué)淺邪锌,如果有寫的不好請(qǐng)見諒噢秃流,與此同時(shí)有什么好的建議和意見都可以下面留言噢概说,謝謝大家!文章代碼有點(diǎn)多萍丐,本想只貼關(guān)鍵代碼的逝变,為了能讓大家看得明白就把全部代碼貼出來了壳影!
先給大家展示下咱們要實(shí)現(xiàn)效果:
1. MVVM的簡(jiǎn)單介紹:
說起實(shí)現(xiàn)mvvm這個(gè)架構(gòu)可以說是最近比較火了,mvvm可以說是mvp的升級(jí)版掺栅,M是Model,V是View(Activity氧卧,F(xiàn)ragment 假抄,xml等等)宿饱,VM就是ViewModel谬以。
要想學(xué)會(huì)使用mvvm架構(gòu)由桌,就必須要了解DataBinding铭乾,DataBinding的入門教程炕檩,我就暫時(shí)轉(zhuǎn)載下郭霖大神分享的文章吧:——> 點(diǎn)我 ,不會(huì)的一定要看噢泉沾!
2. 簡(jiǎn)單說下mvvm中各個(gè)模塊的作用:
- Model是用來獲取本地或者網(wǎng)絡(luò)的數(shù)據(jù);
- ViewModel是來操作Model獲取的數(shù)據(jù)俊马;
- View當(dāng)然就是用來顯示UI的啦;
而mvvm****最主要的宗旨理念就是數(shù)據(jù)驅(qū)動(dòng)UI肩杈。大致意思是ViewModel操作Model的數(shù)據(jù)柴我,當(dāng)數(shù)據(jù)變化時(shí)會(huì)自動(dòng)同步到View的UI上(單向綁定)
掌握這些基本理念就差不多了,開始實(shí)戰(zhàn)吧锋恬!
3. 實(shí)戰(zhàn):
1. 項(xiàng)目結(jié)構(gòu):
2. 添加項(xiàng)目所需要的依賴
implementation 'com.android.support:design:28.0.0'
implementation 'com.squareup.retrofit2:retrofit:2.5.0'
implementation 'com.squareup.retrofit2:retrofit-converters:2.5.0'
implementation 'com.squareup.retrofit2:converter-gson:2.5.0'
implementation 'com.github.bumptech.glide:glide:4.9.0'
3. 主要界面編寫
我這里是使用了ViewPager+TabLayout+Fragment實(shí)現(xiàn)的,我就暫時(shí)貼上NewsFragment的界面吧编丘!而NewsFragment布局中使用了RecyclerView,那么順便也貼下news_item布局与学。
<!--NewsFrgament布局-->
<?xml version="1.0" encoding="utf-8"?>
<layout>
<data>
<variable
name="NewViewModel"
type="com.bunny.swipelayoutdemo.viewmodel.NewsViewModel"/>
</data>
<LinearLayout 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=".view.fragment.NewsFragment">
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/rv_sl"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/rv_news"
android:layout_width="match_parent"
android:layout_height="match_parent">
</android.support.v7.widget.RecyclerView>
</android.support.v4.widget.SwipeRefreshLayout>
</LinearLayout>
</layout>
注意咱們?cè)贜ewsItem布局里設(shè)置的數(shù)據(jù)的時(shí)候使用了@{NewsViewModel.xxx};
<!--NewsItem布局-->
<?xml version="1.0" encoding="utf-8"?>
<layout>
<data>
<variable
name="NewsViewModel"
type="com.bunny.swipelayoutdemo.viewmodel.NewsViewModel"/>
</data>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:padding="10dp">
<TextView
android:id="@+id/title"
android:textSize="18sp"
app:layout_constraintEnd_toStartOf="@id/image"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:textColor="@android:color/black"
android:layout_width="0dp"
app:layout_constraintHorizontal_weight="7"
android:text="@{NewsViewModel.title}"
android:singleLine="true"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:maxLines="2"
android:textSize="15sp"
app:layout_constraintTop_toBottomOf="@id/title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/image"
app:layout_constraintHorizontal_weight="7"
android:text="@{NewsViewModel.content}"
android:ellipsize="end"/>
<ImageView
android:paddingTop="2dp"
android:id="@+id/image"
android:layout_width="0dp"
app:imgUrl="@{NewsViewModel.imgUrl}"
android:scaleType="fitXY"
app:layout_constraintHorizontal_weight="3"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/title"
android:layout_height="70dp" />
<TextView
android:layout_marginTop="3dp"
android:text="@{NewsViewModel.author}"
app:layout_constraintTop_toBottomOf="@id/content"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"/>
<TextView
android:layout_marginTop="3dp"
android:text="@{NewsViewModel.time}"
android:layout_width="0dp"
app:layout_constraintTop_toBottomOf="@id/image"
app:layout_constraintEnd_toEndOf="parent"
android:layout_height="wrap_content" />
</android.support.constraint.ConstraintLayout>
</layout>
其中ImageView在設(shè)置圖片的時(shí)候很特別,這個(gè)Demo里面使用了Glide圖片加載框架,因?yàn)榉?wù)器返回的是圖片的URL地址所以用Glide更簡(jiǎn)單。
public class BindAdapter {
@BindingAdapter("imgUrl")
public static void setImage(ImageView iv, String imgUrl){
Glide.with(iv.getContext()).load(imgUrl).into(iv);
}
}
@BinderAdapter注解的作用:相當(dāng)于一個(gè)綁定的數(shù)據(jù)源監(jiān)聽器,如果發(fā)現(xiàn)數(shù)據(jù)源改變蚓峦,那么他的函數(shù)體方法會(huì)被立即調(diào)用一汽;而是用@BinderAdapter注解其方法必須是public static類型沾谓;
舉個(gè)栗子:咱們?cè)诤竺娴腣iewModel中會(huì)設(shè)置ImageView的URL妇穴,那么BindAdapter下面的方法就會(huì)立即被調(diào)用死讹。
4. Model中通過Retrofit+OkHttp獲取網(wǎng)絡(luò)數(shù)據(jù):
這是咱們要使用Retrofit必須設(shè)置的,沒啥好說的世剖;
//Api:
public interface Api {
public static final String BASE_URL="http://xxxx.xxxx.xxxx.xxx";
//pi是頁數(shù)酬凳,ps是當(dāng)前服務(wù)器要返回的新聞條數(shù)
@GET("CPAPI/V1/NewsList")
Call<News> getNews(@Query("pi") int pi,@Query("ps") int ps);
}
通過枚舉單例創(chuàng)建一個(gè)Http請(qǐng)求的工具類台诗,里面封裝了Retrofit。當(dāng)然使用枚舉類單例能防止反射攻擊,避免序列化等等好處漫雷,大家具體可以百度下;
//Http工具類:通過枚舉單例創(chuàng)建
public enum HttpManagerSingleton {
INSTANCE{
public OkHttpClient getClient(){
return new OkHttpClient.Builder()
.readTimeout(3, TimeUnit.SECONDS)
.connectTimeout(1,TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.build();
}
public Retrofit getRetrofit(OkHttpClient client){
return new Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(Api.BASE_URL)
.client(client)
.build();
}
};
public abstract Retrofit getRetrofit(OkHttpClient client);
public abstract OkHttpClient getClient();
public static HttpManagerSingleton getInstance(){
return INSTANCE;
}
}
在此NewsModel用來通過網(wǎng)絡(luò)請(qǐng)求數(shù)據(jù)涡戳,通過回調(diào)將數(shù)據(jù)傳遞給ViewModel來處理推正;
//Model類獲取數(shù)據(jù):
public class NewsModel {
private static Retrofit getRetrofit(){
HttpManagerSingleton singleton=HttpManagerSingleton.getInstance();
return singleton.getRetrofit(singleton.getClient());
}
public static void getNews(int pi, int ps, final NewsCallBack newsCallBack){
final Call<News> newsCall = getRetrofit().create(Api.class).getNews(pi, ps);
newsCall.enqueue(new Callback<News>() {
@Override
public void onResponse(Call<News> call, Response<News> response) {
newsCallBack.onSuccess(response.body().getData());
newsCall.cancel();
}
@Override
public void onFailure(Call<News> call, Throwable t) {
newsCallBack.onError(t.getMessage());
newsCall.cancel();
}
});
}
}
5. ViewModel操作數(shù)據(jù):
在ViewModel中處理Model通過回調(diào)傳來的數(shù)據(jù),進(jìn)行操作;
public class NewsViewModel {
public ObservableField<String> title=new ObservableField<>();
public ObservableField<String> author=new ObservableField<>();
public ObservableField<String> imgUrl=new ObservableField<>();
public ObservableField<String> time=new ObservableField<>();
public ObservableField<String> content=new ObservableField<>();
public ObservableArrayList<News.DataBean> localList=new ObservableArrayList();
//設(shè)置默認(rèn)加載第一頁8條數(shù)據(jù)
public int pi=1,ps=8;
//滑動(dòng)監(jiān)聽回調(diào)
private FreshCallBack mFreshCallBack;
/**
* 將服務(wù)器中返回的時(shí)間戳轉(zhuǎn)換為日期格式:
*/
private String parseTimeToString(long time,String format){
Date date=new Date(time*1000);
SimpleDateFormat simpleDateFormat=new SimpleDateFormat(format);
String form = simpleDateFormat.format(date);
return form;
}
/**
* 設(shè)置數(shù)據(jù):
* @param result
*/
public void setData(final News.DataBean result){
new Thread(new Runnable() {
@Override
public void run() {
if (result==null)
return;
title.set(result.getTitle());
content.set(result.getTitle());
time.set(parseTimeToString(result.getTime(),"yyyy-MM-dd"));
imgUrl.set(result.getPic());
author.set(result.getAuthor());
}
}).start();
}
/**
* 獲取新聞數(shù)據(jù):
*/
public void loadNews(int pi, int ps, final FreshCallBack mFreshCallBack){
NewsModel.getNews(pi, ps, new NewsCallBack() {
@Override
public void onSuccess(List<News.DataBean> newsList) {
localList.addAll(newsList);
mFreshCallBack.freshSuccess();
}
@Override
public void onError(String error) {
mFreshCallBack.freshError();
}
});
}
/**
* 上拉加載更多
*/
public void loadMoreNews(FreshCallBack mFreshCallBack){
pi=pi+1;
loadNews(pi,ps,mFreshCallBack);
}
/**
* 下拉刷新
*/
public void loadFreshNews(final FreshCallBack mFreshCallBack){
pi=1;
NewsModel.getNews(pi, ps, new NewsCallBack() {
@Override
public void onSuccess(List<News.DataBean> newsList) {
localList.clear();
localList.addAll(0,newsList);
mFreshCallBack.freshSuccess();
}
@Override
public void onError(String error) {
mFreshCallBack.freshError();
}
});
}
}
}
6. Adapter的編寫:
方法解釋:
- 因?yàn)槭褂昧薕bservableArrayList所以我們?cè)赼dapter里的構(gòu)造方法中添加監(jiān)聽器這樣當(dāng)數(shù)據(jù)源發(fā)生改變的時(shí)候,我們就不需要在View中手動(dòng)通知適配器了;
- 在onCreateViewHolder()里面使用DataBinding將news_item界面進(jìn)行綁定,從而再創(chuàng)建ViewHoder;
- 在ViewHolder中我們只需要更改下構(gòu)造方法里的參數(shù)將Binding傳入進(jìn)去纹坐;
- 在onBindViewHolder中讓Binding與ViewModel進(jìn)行綁定瓷马,這樣就調(diào)用里面的方法設(shè)置數(shù)據(jù);
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
private ObservableArrayList<News.DataBean> newsList;
public MyAdapter(ObservableArrayList<News.DataBean> newsList) {
this.newsList = newsList;
//這里使用ObservableArrayList的監(jiān)聽器蒋伦,這樣當(dāng)數(shù)據(jù)源發(fā)生改變的時(shí)候锤窑,我們就不需要在View中手動(dòng)通知適配器了
newsList.addOnListChangedCallback(new ObservableList.OnListChangedCallback<ObservableList<News.DataBean>>() {
@Override
public void onChanged(ObservableList<News.DataBean> sender) {
notifyDataSetChanged();
}
@Override
public void onItemRangeChanged(ObservableList<News.DataBean> sender, int positionStart, int itemCount) {
notifyItemChanged(positionStart,itemCount);
}
@Override
public void onItemRangeInserted(ObservableList<News.DataBean> sender, int positionStart, int itemCount) {
notifyItemRangeInserted(positionStart,itemCount);
}
@Override
public void onItemRangeMoved(ObservableList<News.DataBean> sender, int fromPosition, int toPosition, int itemCount) {
if (itemCount==1)
notifyItemMoved(fromPosition,toPosition);
notifyDataSetChanged();
}
@Override
public void onItemRangeRemoved(ObservableList<News.DataBean> sender, int positionStart, int itemCount) {
notifyItemRangeRemoved(positionStart,itemCount);
}
});
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
NewsItemBinding newsItemBinding = DataBindingUtil.inflate(LayoutInflater.from(viewGroup.getContext()), R.layout.news_item, viewGroup, false);
return new ViewHolder(newsItemBinding);
}
@Override
public int getItemCount() {
return newsList.size();
}
@Override
public void onBindViewHolder(@NonNull ViewHolder viewHolder, int i) {
NewsViewModel viewModel = new NewsViewModel();
//將newsItemBinding和NewsModel綁定
viewHolder.getBinding().setNewsViewModel(viewModel);
//設(shè)置數(shù)據(jù):
viewModel.setData(newsList.get(i));
//避免RecyclerView界面閃爍
viewHolder.getBinding().executePendingBindings();
}
public class ViewHolder extends RecyclerView.ViewHolder {
NewsItemBinding binding;
public ViewHolder(@NonNull NewsItemBinding binding ) {
super(binding.getRoot());
this.binding =binding;
}
public NewsItemBinding getBinding() {
return binding;
}
}
}
7. 在View中(這里代表NewsFragment)與Model進(jìn)行綁定:
我們可以看到在onCreateView()方法里的bindViewModel()中綁定了ViewModel;這樣在后面我們要實(shí)了下拉刷新和上拉加載的時(shí)候就可以手動(dòng)設(shè)置下數(shù)據(jù);
public class NewsFragment extends Fragment {
private MyAdapter mAdapter;
private ProgressDialog mProgressDialog;
private RecyclerView mRvNews;
private SwipeRefreshLayout mSrl;
private FragmentNewsBinding mBinding;
private NewsViewModel mNewsViewModel;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
mBinding=DataBindingUtil.inflate(inflater,R.layout.fragment_news, null, false);
bindViewModel();
initUi();
return mBinding.getRoot();
}
private void bindViewModel() {
mNewsViewModel=new NewsViewModel();
mBinding.setNewViewModel(mNewsViewModel);
}
private void initUi() {
mRvNews=mBinding.rvNews;
mSrl=mBinding.rvSl;
mProgressDialog=new ProgressDialog(getActivity());
mProgressDialog.setMessage("正在加載中...");
mProgressDialog.setCancelable(false);
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
setRecyclerViewAdapter();
autoLoad();
loadMore();
loadFreshNews();
}
private void setRecyclerViewAdapter() {
mRvNews.setLayoutManager(new LinearLayoutManager(getActivity(),LinearLayoutManager.VERTICAL,false));
mRvNews.addItemDecoration(new DividerItemDecoration(getActivity(),DividerItemDecoration.VERTICAL));
mAdapter=new MyAdapter(mNewsViewModel.localList);
mRvNews.setAdapter(mAdapter);
}
//首次進(jìn)入自動(dòng)刷新
private void autoLoad() {
mSrl.measure(0,0);
mSrl.setRefreshing(true);
fresh();
}
/**
* 上拉加載
*/
public void loadMore(){
mRvNews.setOnScrollListener(new RecyclerView.OnScrollListener() {
//判斷是否向最后一個(gè)item進(jìn)行滑動(dòng)
boolean isLoading=false;
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
LinearLayoutManager manager = (LinearLayoutManager) mRvNews.getLayoutManager();
switch (newState){
case RecyclerView.SCROLL_STATE_IDLE:
//獲取最后一個(gè)item的索引值
int lastPosition = manager.findLastCompletelyVisibleItemPosition();
//獲取item總數(shù)
int itemCount = manager.getItemCount();
if (lastPosition>=(itemCount-1) && isLoading){
mProgressDialog.show();
//加載更多
mNewsViewModel.loadMoreNews(new FreshCallBack() {
@Override
public void freshSuccess() {
if (mProgressDialog.isShowing())
mProgressDialog.dismiss();
}
@Override
public void freshError() {
if (mProgressDialog.isShowing())
mProgressDialog.dismiss();
Toast.makeText(getActivity(), "數(shù)據(jù)加載失斊敲肌晃择!", Toast.LENGTH_SHORT).show();
}
});
}
break;
}
}
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (dy>0){
isLoading=true;
}else{
isLoading=false;
}
}
});
}
/**
* 下拉刷新
*/
public void loadFreshNews(){
mSrl.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
fresh();
}
});
}
private void fresh(){
mNewsViewModel.loadFreshNews(new FreshCallBack() {
@Override
public void freshSuccess() {
if (mSrl.isRefreshing())
mSrl.setRefreshing(false);
}
@Override
public void freshError() {
if (mSrl.isRefreshing())
mSrl.setRefreshing(false);
Toast.makeText(getActivity(), "數(shù)據(jù)加載失斀呋洹!", Toast.LENGTH_SHORT).show();
}
});
}
}