MVVM+DataBinding 实现一个简单的加载新闻列表De
**PS:这是我第一次写文章,小弟才疏学浅,如果有写的不好请见谅噢,与此同时有什么好的建议和意见都可以下面留言噢,谢谢大家!文章代码有点多,本想只贴关键代码的,为了能让大家看得明白就把全部代码贴出来了!
先给大家展示下咱们要实现效果:
实现效果展示.jpg1. MVVM的简单介绍:
说起实现mvvm这个架构可以说是最近比较火了,mvvm可以说是mvp的升级版,M是Model,V是View(Activity,Fragment ,xml等等),VM就是ViewModel。
要想学会使用mvvm架构,就必须要了解DataBinding,DataBinding的入门教程,我就暂时转载下郭霖大神分享的文章吧:——> 点我 ,不会的一定要看噢!
2. 简单说下mvvm中各个模块的作用:
- Model是用来获取本地或者网络的数据;
- ViewModel是来操作Model获取的数据;
- View当然就是用来显示UI的啦;
而mvvm****最主要的宗旨理念就是数据驱动UI。大致意思是ViewModel操作Model的数据,当数据变化时会自动同步到View的UI上(单向绑定)
掌握这些基本理念就差不多了,开始实战吧!
3. 实战:
1. 项目结构:
项目结构.jpg2. 添加项目所需要的依赖
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实现的,我就暂时贴上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>
注意咱们在NewsItem布局里设置的数据的时候使用了@{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在设置图片的时候很特别,这个Demo里面使用了Glide图片加载框架,因为服务器返回的是图片的URL地址所以用Glide更简单。
public class BindAdapter {
@BindingAdapter("imgUrl")
public static void setImage(ImageView iv, String imgUrl){
Glide.with(iv.getContext()).load(imgUrl).into(iv);
}
}
@BinderAdapter注解的作用:相当于一个绑定的数据源监听器,如果发现数据源改变,那么他的函数体方法会被立即调用;而是用@BinderAdapter注解其方法必须是public static类型;
举个栗子:咱们在后面的ViewModel中会设置ImageView的URL,那么BindAdapter下面的方法就会立即被调用。
4. Model中通过Retrofit+OkHttp获取网络数据:
这是咱们要使用Retrofit必须设置的,没啥好说的;
//Api:
public interface Api {
public static final String BASE_URL="http://xxxx.xxxx.xxxx.xxx";
//pi是页数,ps是当前服务器要返回的新闻条数
@GET("CPAPI/V1/NewsList")
Call<News> getNews(@Query("pi") int pi,@Query("ps") int ps);
}
通过枚举单例创建一个Http请求的工具类,里面封装了Retrofit。当然使用枚举类单例能防止反射攻击,避免序列化等等好处,大家具体可以百度下;
//Http工具类:通过枚举单例创建
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用来通过网络请求数据,通过回调将数据传递给ViewModel来处理;
//Model类获取数据:
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操作数据:
在ViewModel中处理Model通过回调传来的数据,进行操作;
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();
//设置默认加载第一页8条数据
public int pi=1,ps=8;
//滑动监听回调
private FreshCallBack mFreshCallBack;
/**
* 将服务器中返回的时间戳转换为日期格式:
*/
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;
}
/**
* 设置数据:
* @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();
}
/**
* 获取新闻数据:
*/
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的编写:
方法解释:
- 因为使用了ObservableArrayList所以我们在adapter里的构造方法中添加监听器这样当数据源发生改变的时候,我们就不需要在View中手动通知适配器了;
- 在onCreateViewHolder()里面使用DataBinding将news_item界面进行绑定,从而再创建ViewHoder;
- 在ViewHolder中我们只需要更改下构造方法里的参数将Binding传入进去;
- 在onBindViewHolder中让Binding与ViewModel进行绑定,这样就调用里面的方法设置数据;
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {
private ObservableArrayList<News.DataBean> newsList;
public MyAdapter(ObservableArrayList<News.DataBean> newsList) {
this.newsList = newsList;
//这里使用ObservableArrayList的监听器,这样当数据源发生改变的时候,我们就不需要在View中手动通知适配器了
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);
//设置数据:
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进行绑定:
我们可以看到在onCreateView()方法里的bindViewModel()中绑定了ViewModel;这样在后面我们要实了下拉刷新和上拉加载的时候就可以手动设置下数据;
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);
}
//首次进入自动刷新
private void autoLoad() {
mSrl.measure(0,0);
mSrl.setRefreshing(true);
fresh();
}
/**
* 上拉加载
*/
public void loadMore(){
mRvNews.setOnScrollListener(new RecyclerView.OnScrollListener() {
//判断是否向最后一个item进行滑动
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:
//获取最后一个item的索引值
int lastPosition = manager.findLastCompletelyVisibleItemPosition();
//获取item总数
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(), "数据加载失败!", 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(), "数据加载失败!", Toast.LENGTH_SHORT).show();
}
});
}
}