如何应对 Android 面试官 -> MVVM 实战一个新闻客户端 (中)

前言


本章我们基于重构的方式进行一个 MVVM 的实战,我们将一个新闻列表的普通实现,一步一步的改造成 MVVM 的架构模式,一共分为上中下三个章节,本章继续上一章,开始中篇的讲解;

控件化


我们本章向控件化进一步迈进

BaseView 重构

我们上一章,将 TitleView 和 PictureTitleView 抽取了一个 BaseView 来抽取公共的 setData 逻辑,我们还可以继续精进一步;

我们现将我们上一章定义的接口 BaseView 重命名成 IBaseView

public interface IBaseView<DATA extends BaseViewModel> {void setData(DATA data);
}

然后我们定义一个 IBaseView 的实现类,BaseView

public abstract class BaseView extends LinearLayout implements IBaseView<BaseViewModel> {public BaseView(Context context) {super(context);}public BaseView(Context context, @Nullable AttributeSet attrs) {super(context, attrs);}public BaseView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);}public BaseView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {super(context, attrs, defStyleAttr, defStyleRes);}@Overridepublic void setData(BaseViewModel baseViewModel) {}
}

然后我们定义公共的 init 方法,方法中我们进行布局的加载,但是布局 id 我们无法从自身获取,需要通过 工厂方法 模式,提供一个抽象接口来从子类中获取布局 id;同时,通过 泛型 来接收 DataBinding 在 inflater 之后返回的 ViewDataBinding 对象,整体如下:

public abstract class BaseView<VIEW_BINDING extends ViewDataBinding> extends LinearLayout implements IBaseView<BaseViewModel> {protected VIEW_BINDING mBinding;private void init() {LayoutInflater layoutInflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);mBinding = DataBindingUtil.inflate(layoutInflater, getLayoutId(), this, false);addView(mBinding.getRoot());}public abstract int getLayoutId();
}

通常,View 可能会需要一个点击事件,我们这里也提供一下

private void init() {LayoutInflater layoutInflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);mBinding = DataBindingUtil.inflate(layoutInflater, getLayoutId(), this, false);mBinding.getRoot().setOnClickListener(this::onRootClick);addView(mBinding.getRoot());
}public abstract int getLayoutId();public abstract void onRootClick(View view);

然后,我们来实现 setData 部分,这里我们将泛型的实现交给 BaseView

public abstract class BaseView<VIEW_BINDING extends ViewDataBinding, DATA extends BaseViewModel> extends LinearLayout implements IBaseView<DATA> {}

然后,我们发现,当我们在 setData 中去 setViewModel 的时候,报错了,并没有 setViewModel 的接口,那么我们就需要进行剥离出来,交给子类实现;

image.png

public abstract class BaseView<VIEW_BINDING extends ViewDataBinding, DATA extends BaseViewModel> extends LinearLayout implements IBaseView<DATA> {@Overridepublic void setData(DATA baseViewModel) {setDataToView(baseViewModel);mBinding.executePendingBindings();}public abstract void setDataToView(DATA data);
}

我们接下来重构 TitleView 和 PictureTitleView,来让它们继承 BaseView

public class TitleView extends BaseView<TitleViewBinding, TitleViewModel> {public TitleView(Context context) {super(context);}@Overridepublic int getLayoutId() {return R.layout.title_view;}@Overridepublic void onRootClick(View view) {}@Overridepublic void setDataToView(TitleViewModel titleViewModel) {mBinding.setTitleViewModel(titleViewModel);}
}

PictureTitleView

public class PictureTitleView extends BaseView<PictureTitleViewBinding, PictureTitleViewModel> {public PictureTitleView(Context context) {super(context);}@Overridepublic int getLayoutId() {return R.layout.picture_title_view;}@Overridepublic void onRootClick(View view) {}@Overridepublic void setDataToView(PictureTitleViewModel pictureTitleViewModel) {mBinding.setPictureTitleViewModel(pictureTitleViewModel);}@BindingAdapter("loadImageUrl")public static void loadImageUrl(ImageView imageView, String imgUrl) {Glide.with(imageView.getContext()).load(imgUrl).transition(withCrossFade()).into(imageView);}
}

可以看到,我们的 TitleView 和 PictureTitleView 也清爽了很多;到这里,对齐了我们在讲 MVx 的时候的 控件化 的重要性;

Model

我们接下来,终于可以向 MVVM 的架构来迈进了,我们先来看下我们的 Fragment,一开始我们把数据的加载直接放在了 Fragment 中,这其实并不合理,我们需要将数据的获取放到 model 层,我们来进行重构;

首先我们在 base 层定义下我们的 baseModel,创建一个 IBaseModelListener,用来将 model 获取的数据回调到 View

public interface IBaseModelListener<DATA> {void onLoadSuccess(DATA data);void onLoadFail(int errorCode, String errorMsg);
}

然后我们定义一个我们用来获取频道列表的 model

public class NewsChannelModel {private IBaseModelListener<List<NewsChannelsBean.ChannelList>> mListener;public NewsChannelModel(IBaseModelListener<List<NewsChannelsBean.ChannelList>> mListener) {this.mListener = mListener;}public void load() {TecentNetworkApi.getService(NewsApiInterface.class).getNewsChannels().compose(TecentNetworkApi.getInstance().applySchedulers(new BaseObserver<NewsChannelsBean>() {@Overridepublic void onSuccess(NewsChannelsBean newsChannelsBean) {mListener.onLoadSuccess(newsChannelsBean.showapiResBody.channelList);}@Overridepublic void onFailure(Throwable e) {e.printStackTrace();mListener.onLoadFail(400, e.getMessage());}}));}
}

然后,我们在 Fragment 中调用这个 model 的 load 方法;

public class HeadlineNewsFragment extends Fragment implements IBaseModelListener<List<NewsChannelsBean.ChannelList>> {public HeadlineNewsFragmentAdapter adapter;private FragmentHomeBinding viewDataBinding;private NewsChannelModel model;@Overridepublic View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {viewDataBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_home, container, false);adapter = new HeadlineNewsFragmentAdapter(getChildFragmentManager());viewDataBinding.tablayout.setTabMode(TabLayout.MODE_SCROLLABLE);viewDataBinding.viewpager.setAdapter(adapter);viewDataBinding.tablayout.setupWithViewPager(viewDataBinding.viewpager);viewDataBinding.viewpager.setOffscreenPageLimit(1);model = new NewsChannelModel(this);model.load();return viewDataBinding.getRoot();}@Overridepublic void onLoadSuccess(List<NewsChannelsBean.ChannelList> channelLists) {if (adapter != null) {adapter.setChannels(channelLists);}}@Overridepublic void onLoadFail(int errorCode, String errorMsg) {}
}

Fragment 也清爽了很多,我们接下来重构下 NewsListFragment,重构逻辑一样,我们封装一个 NewListModel

public class NewsListModel {private IBaseModelListener<ArrayList<BaseViewModel>> mListener;private int mPageNum;private String mChannelId;private String mChannelName;public NewsListModel(IBaseModelListener<ArrayList<BaseViewModel>> mListener, String channelId, String channelName) {this.mListener = mListener;this.mChannelId = channelId;this.mChannelName = channelName;}public void load() {TecentNetworkApi.getService(NewsApiInterface.class).getNewsList(mChannelId,mChannelName, String.valueOf(mPageNum)).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).flatMap(new Function<NewsListBean, ObservableSource<ArrayList<BaseViewModel>>>() {@Overridepublic ObservableSource<ArrayList<BaseViewModel>> apply(NewsListBean newsChannelsBean) throws Exception {ArrayList<BaseViewModel> viewModels = new ArrayList<>();for (NewsListBean.Contentlist contentlist : newsChannelsBean.showapiResBody.pagebean.contentlist) {if (contentlist.imageurls != null && contentlist.imageurls.size() > 0) {PictureTitleViewModel pictureTitleViewModel = new PictureTitleViewModel();pictureTitleViewModel.imgUrl = contentlist.imageurls.get(0).url;pictureTitleViewModel.title = contentlist.title;pictureTitleViewModel.jumpUrl = contentlist.link;viewModels.add(pictureTitleViewModel);} else {TitleViewModel titleViewModel = new TitleViewModel();titleViewModel.title = contentlist.title;titleViewModel.jumpUrl = contentlist.link;viewModels.add(titleViewModel);}}return Observable.just(viewModels);}}).subscribe(new Consumer<ArrayList<BaseViewModel>>() {@Overridepublic void accept(ArrayList<BaseViewModel> baseViewModels) throws Exception {mPageNum++;mListener.onLoadSuccess(baseViewModels);}});}public void refresh() {mPageNum = 1;load();}
}

然后 NewListFragment 重构如下:

public class NewsListFragment extends Fragment implements IBaseModelListener<ArrayList<BaseViewModel>> {private NewsListRecyclerViewAdapter mAdapter;private FragmentNewsBinding viewDataBinding;private NewsListModel mNewsListModel;protected final static String BUNDLE_KEY_PARAM_CHANNEL_ID = "bundle_key_param_channel_id";protected final static String BUNDLE_KEY_PARAM_CHANNEL_NAME = "bundle_key_param_channel_name";public static NewsListFragment newInstance(String channelId, String channelName) {NewsListFragment fragment = new NewsListFragment();Bundle bundle = new Bundle();bundle.putString(BUNDLE_KEY_PARAM_CHANNEL_ID, channelId);bundle.putString(BUNDLE_KEY_PARAM_CHANNEL_NAME, channelName);fragment.setArguments(bundle);return fragment;}@Overridepublic View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {viewDataBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_news, container, false);mAdapter = new NewsListRecyclerViewAdapter(getContext());viewDataBinding.listview.setHasFixedSize(true);viewDataBinding.listview.setLayoutManager(new LinearLayoutManager(getContext()));viewDataBinding.listview.setAdapter(mAdapter);mNewsListModel = new NewsListModel(this, getArguments().getString(BUNDLE_KEY_PARAM_CHANNEL_ID),getArguments().getString(BUNDLE_KEY_PARAM_CHANNEL_NAME));mNewsListModel.load();viewDataBinding.refreshLayout.setOnRefreshListener(new OnRefreshListener() {@Overridepublic void onRefresh(@NonNull RefreshLayout refreshLayout) {mNewsListModel.refresh();}});viewDataBinding.refreshLayout.setOnLoadMoreListener(new OnLoadMoreListener() {@Overridepublic void onLoadMore(@NonNull RefreshLayout refreshLayout) {mNewsListModel.load();}});return viewDataBinding.getRoot();}@Overridepublic void onLoadSuccess(ArrayList<BaseViewModel> baseViewModels) {contentList.addAll(baseViewModels);mAdapter.setData(contentList);viewDataBinding.refreshLayout.finishRefresh();viewDataBinding.refreshLayout.finishLoadMore();}@Overridepublic void onLoadFail(int errorCode, String errorMsg) {}
}

我们这个页面的数据加载,涉及到了分页的逻辑,我们需要将回调的结果带上分页的结果,要告知 View 是加载的哪一页,我们需要一个分页结果;

public class PageResult {private boolean isFirstPage;private boolean isEmpty;private boolean hasNextPage;public PageResult(boolean isFirstPage, boolean isEmpty, boolean hasNextPage) {this.isFirstPage = isFirstPage;this.isEmpty = isEmpty;this.hasNextPage = hasNextPage;}
}

然后,我们修改回调结果的 Listener,这个接口是公共的,也就是说,有的需要分页结果,有的不需要分页结果,那么就需要一个可变参数;

public interface IBaseModelListener<DATA> {void onLoadSuccess(DATA data, PageResult... pageResults);void onLoadFail(int errorCode, String errorMsg);
}

然后,NewsListModel 的回调带上这个 PageResult;

mListener.onLoadSuccess(baseViewModels,new PageResult(mPageNum == 1, baseViewModels.isEmpty(), baseViewModels.size() >= 10));

onLoadSuccess 的回调改造如下:

@Override
public void onLoadSuccess(ArrayList<BaseViewModel> baseViewModels, PageResult... pageResults) {if (pageResults != null && pageResults.length > 0 && pageResults[0].isFirstPage) {contentList.clear();}contentList.addAll(baseViewModels);mAdapter.setData(contentList);viewDataBinding.refreshLayout.finishRefresh();viewDataBinding.refreshLayout.finishLoadMore();
}

到这的时候,我们的 model 层就抽离出来了,可能看到这的时候,好多人就疑问了,你这也没使用 Jetpack 的 ViewModel 和 LiveData 呀,别着急,我们精彩的还在后面;

BaseModel

可以看到,NewChannelModel 和 NewListModel 中都有 IBaseModelListener,这个是可以抽取到 base 层的,所以我们可以创建一个 BaseMvvmModel 来抽取它;

public abstract class BaseMvvmModel {protected WeakReference<IBaseModelListener> mReferenceIBaseModeListener;public void register(IBaseModelListener listener) {if (listener != null) {mReferenceIBaseModeListener = new WeakReference<>(listener);}}
}

然后,NewListModel 和 NewsChannelModel 分别继承 BaseMvvmModel,同时移除子类 model 中的 IBaseModelListener 的声明;

public class NewsListModel extends BaseMvvmModel {private int mPageNum;private final String mChannelId;private final String mChannelName;public NewsListModel(String channelId, String channelName) {this.mChannelId = channelId;this.mChannelName = channelName;}public void load() {TecentNetworkApi.getService(NewsApiInterface.class).getNewsList(mChannelId,mChannelName, String.valueOf(mPageNum)).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).flatMap(new Function<NewsListBean, ObservableSource<ArrayList<BaseViewModel>>>() {@Overridepublic ObservableSource<ArrayList<BaseViewModel>> apply(NewsListBean newsChannelsBean) throws Exception {ArrayList<BaseViewModel> viewModels = new ArrayList<>();for (NewsListBean.Contentlist contentlist : newsChannelsBean.showapiResBody.pagebean.contentlist) {if (contentlist.imageurls != null && contentlist.imageurls.size() > 0) {PictureTitleViewModel pictureTitleViewModel = new PictureTitleViewModel();pictureTitleViewModel.imgUrl = contentlist.imageurls.get(0).url;pictureTitleViewModel.title = contentlist.title;pictureTitleViewModel.jumpUrl = contentlist.link;viewModels.add(pictureTitleViewModel);} else {TitleViewModel titleViewModel = new TitleViewModel();titleViewModel.title = contentlist.title;titleViewModel.jumpUrl = contentlist.link;viewModels.add(titleViewModel);}}return Observable.just(viewModels);}}).subscribe(new Consumer<ArrayList<BaseViewModel>>() {@Overridepublic void accept(ArrayList<BaseViewModel> baseViewModels) throws Exception {mPageNum++;mReferenceIBaseModeListener.get().onLoadSuccess(baseViewModels,new PageResult(mPageNum == 1, baseViewModels.isEmpty(), baseViewModels.size() >= 10));}});}public void refresh() {mPageNum = 1;load();}
}

NewsChannelModel 重构如下:

public class NewsChannelModel extends BaseMvvmModel {public NewsChannelModel() {}public void load() {TecentNetworkApi.getService(NewsApiInterface.class).getNewsChannels().compose(TecentNetworkApi.getInstance().applySchedulers(new BaseObserver<NewsChannelsBean>() {@Overridepublic void onSuccess(NewsChannelsBean newsChannelsBean) {mReferenceIBaseModeListener.get().onLoadSuccess(newsChannelsBean.showapiResBody.channelList);}@Overridepublic void onFailure(Throwable e) {e.printStackTrace();mReferenceIBaseModeListener.get().onLoadFail(400, e.getMessage());}}));}
}

然后对应的 Fragment 也需要重构下:

mNewsListModel = new NewsListModel(getArguments().getString(BUNDLE_KEY_PARAM_CHANNEL_ID),getArguments().getString(BUNDLE_KEY_PARAM_CHANNEL_NAME));
mNewsListModel.register(this);
model = new NewsChannelModel();
model.register(this);

然后,我们的 page 其实也可以抽取到 base 层,通过可变参数的形式来决定分页的内容以及是否要分页,所以 BaseMvvmModel 继续重构;

public abstract class BaseMvvmModel {protected WeakReference<IBaseModelListener> mReferenceIBaseModeListener;protected boolean mIsPaging;protected final int INIT_PAGE_NUMBER;public BaseMvvmModel(boolean isPaging, int... initPageNumber) {if (isPaging && initPageNumber != null && initPageNumber.length > 0) {INIT_PAGE_NUMBER = initPageNumber[0];} else {INIT_PAGE_NUMBER = -1;}}public void register(IBaseModelListener listener) {if (listener != null) {mReferenceIBaseModeListener = new WeakReference<>(listener);}}
}

子类实现中,也需要重构下

NewsChannelModel 的构造方法重构如下:

public NewsChannelModel() {super(false);
}

NewsListModel 的构造方法重构如下:

public NewsListModel(String channelId, String channelName) {super(true, 1);this.mChannelId = channelId;this.mChannelName = channelName;
}

到这里的时候,有人可能会问,INIT_PAGE_NUMBER 没有用到呀,定义它做什么?别急,它来了,我们其实也可以把 NewListModel 中的 refresh 和 load 提取到 base 层;

public abstract class BaseMvvmModel {public void refresh() {mPageNum = INIT_PAGE_NUMBER;load();}public abstract void load();
}

这样,我们定义的 INIT_PAGE_NUMBER 是不是就使用到了;然后我们还可以给 refresh 加上多次加载控制,如果正在加载中则不触发二次请求,所以需要我们定义一个变量来控制是否正在加载,同时我们来区分下加载下一页和加载;

public abstract class BaseMvvmModel {protected boolean isLoading;public void refresh() {if (!isLoading) {if (mIsPaging) {mPageNum = INIT_PAGE_NUMBER;}isLoading = true;load();}}public void loadNextPage() {if (!isLoading) {isLoading = true;load();}}
}

另外,我们在子类 model 中分别操作了 BaseMvvmModel 中的 mPageNum 和 mReferenceIBaseModeListener 这两个的操作也是需要抽取到 base 层的,我们来继续重构;

public abstract class BaseMvvmModel<RESULT_DATA> {//protected void notifyResultToListener(RESULT_DATA data) {IBaseModelListener listener = mReferenceIBaseModeListener.get();if (listener != null) {if (mIsPaging) {listener.onLoadSuccess(this, data, new PageResult(mPageNum == INIT_PAGE_NUMBER, data == null?true: ((List)data).isEmpty(), ((List)data).size() > 10));} else {listener.onLoadSuccess(this, data);}}if (mIsPaging) {if (data != null && ((List)data).size() > 0) {mPageNum ++;}}isLoading = false;}protected void loadFail(int errorCode, String errorMsg) {IBaseModelListener listener = mReferenceIBaseModeListener.get();if (listener != null) {listener.onLoadFail(errorCode, errorMsg);}isLoading = false;}
}

然后子类 model 中直接调用对应的 notifyResultToListener 和 loadFail

NewsListModel

subscribe(new Consumer<ArrayList<BaseViewModel>>() {@Overridepublic void accept(ArrayList<BaseViewModel> baseViewModels) throws Exception {notifyResultToListener(baseViewModels);}
});

NewsChannelModel

.compose(TecentNetworkApi.getInstance().applySchedulers(new BaseObserver<NewsChannelsBean>() {@Overridepublic void onSuccess(NewsChannelsBean newsChannelsBean) {notifyResultToListener(newsChannelsBean.showapiResBody.channelList);}@Overridepublic void onFailure(Throwable e) {e.printStackTrace();loadFail(404, e.getMessage());}
}));

数据缓存

如果我们在无网的情况下打开 app,通常会展示一片空白,体验很不好,所以,就需要我们提前将数据缓存下来,当用户无网打开的时候,不至于显示一片白;

首先,我们来提供一个 CacheData;

public class BaseCachedData<DATA> {public long updateTimeInMills;public DATA data;
}

然后我们需要对 BaseMvvmModel 进行重构,增加缓存逻辑;

public abstract class BaseMvvmModel<NETWORK_DATA, RESULT_DATA> {private String mCachedPreferenceKey;private BaseCachedData<NETWORK_DATA> mData;public BaseMvvmModel(boolean isPaging, String cachedPreferenceKey, int... initPageNumber) {if (isPaging && initPageNumber != null && initPageNumber.length > 0) {INIT_PAGE_NUMBER = initPageNumber[0];} else {INIT_PAGE_NUMBER = -1;}// 增加缓存keythis.mCachedPreferenceKey = cachedPreferenceKey;}protected void notifyResultToListener(NETWORK_DATA networkData, RESULT_DATA data) {IBaseModelListener listener = mReferenceIBaseModeListener.get();if (listener != null) {if (mIsPaging) {listener.onLoadSuccess(this, data, new PageResult(mPageNum == INIT_PAGE_NUMBER, data == null ? true : ((List) data).isEmpty(), ((List) data).size() > 10));} else {listener.onLoadSuccess(this, data);}}// 增加缓存逻辑,将网络数据缓存到本地if (mIsPaging) {if (mCachedPreferenceKey != null && mPageNum == INIT_PAGE_NUMBER) {saveDataToPreference(networkData);}} else {if (mCachedPreferenceKey != null) {saveDataToPreference(networkData);}}if (mIsPaging) {if (data != null && ((List) data).size() > 0) {mPageNum++;}}}private void saveDataToPreference(NETWORK_DATA networkData) {if (networkData != null) {BaseCachedData<NETWORK_DATA> baseCachedData = new BaseCachedData<>();baseCachedData.data = networkData;baseCachedData.updateTimeInMills = System.currentTimeMillis();// 这里可以调换成自己的 spUtilsBasicDataPreferenceUtil.getInstance().setString(mCachedPreferenceKey, new Gson().toJson(baseCachedData));}}
}

同时 NewsChannelModel 和 NewsListModel 的构造方法以及 load 实现也需要重构下,增加需要缓存的数据的传入;

public class NewsChannelModel extends BaseMvvmModel<NewsChannelsBean, List<NewsChannelsBean.ChannelList>> {public NewsChannelModel() {super(false, "NEWS_CHANNEL_PREF_KEY");}@Overridepublic void load() {TecentNetworkApi.getService(NewsApiInterface.class).getNewsChannels().compose(TecentNetworkApi.getInstance().applySchedulers(new BaseObserver<NewsChannelsBean>() {@Overridepublic void onSuccess(NewsChannelsBean newsChannelsBean) {notifyResultToListener(newsChannelsBean, newsChannelsBean.showapiResBody.channelList);}@Overridepublic void onFailure(Throwable e) {e.printStackTrace();mReferenceIBaseModeListener.get().onLoadFail(400, e.getMessage());}}));}
}

NewsListModel 重构如下:

public class NewsListModel extends BaseMvvmModel<NewsListBean, ArrayList<BaseViewModel>> {private final String mChannelId;private final String mChannelName;private NewsListBean mNewsListBean;public NewsListModel(String channelId, String channelName) {super(true, channelId + channelName + "pref_key", 1);this.mChannelId = channelId;this.mChannelName = channelName;}@Overridepublic void load() {TecentNetworkApi.getService(NewsApiInterface.class).getNewsList(mChannelId,mChannelName, String.valueOf(mPageNum)).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).flatMap(new Function<NewsListBean, ObservableSource<ArrayList<BaseViewModel>>>() {@Overridepublic ObservableSource<ArrayList<BaseViewModel>> apply(NewsListBean newsChannelsBean) throws Exception {ArrayList<BaseViewModel> viewModels = new ArrayList<>();NewsListModel.this.mNewsListBean = newsChannelsBean;for (NewsListBean.Contentlist contentlist : newsChannelsBean.showapiResBody.pagebean.contentlist) {if (contentlist.imageurls != null && contentlist.imageurls.size() > 0) {PictureTitleViewModel pictureTitleViewModel = new PictureTitleViewModel();pictureTitleViewModel.imgUrl = contentlist.imageurls.get(0).url;pictureTitleViewModel.title = contentlist.title;pictureTitleViewModel.jumpUrl = contentlist.link;viewModels.add(pictureTitleViewModel);} else {TitleViewModel titleViewModel = new TitleViewModel();titleViewModel.title = contentlist.title;titleViewModel.jumpUrl = contentlist.link;viewModels.add(titleViewModel);}}return Observable.just(viewModels);}}).subscribe(new Consumer<ArrayList<BaseViewModel>>() {@Overridepublic void accept(ArrayList<BaseViewModel> baseViewModels) throws Exception {notifyResultToListener(NewsListModel.this.mNewsListBean, baseViewModels);}});}
}

OK,运行可以看到,数据已经存到了 SP 中;

好了,中篇文章就讲解到这里吧;

下一章预告


MVVM 实战一个新闻客户端 (下)

欢迎三连


来都来了,点个关注点个赞吧,你的支持是我前进的最大动力

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/860027.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2024年华东杯B题数学建模论文:基于车辆运动学转弯模型的自动驾驶规划问题

摘要 随着自动驾驶技术的发展&#xff0c;车辆转弯问题成为关键挑战。本文针对自动驾驶车辆在转弯过程中的数学建模、路径规划及避障策略进行了深入研究&#xff0c;旨在提升自动驾驶车辆的行驶安全性与效率。 针对问题1&#xff0c;对于四轮前轮驱动车辆的转弯问题&#xff0c…

【C++LeetCode】【热题100】两数相加【中等】-不同效率的题解【1】

题目&#xff1a; 暴力方法&#xff1a; /*** Definition for singly-linked list.* struct ListNode {* int val;* ListNode *next;* ListNode() : val(0), next(nullptr) {}* ListNode(int x) : val(x), next(nullptr) {}* ListNode(int x, ListNo…

常见硬件工程师面试题(二)

大家好&#xff0c;我是山羊君Goat。 对于硬件工程师&#xff0c;学习的东西主要和电路硬件相关&#xff0c;所以在硬件工程师的面试中&#xff0c;对于经验是十分看重的&#xff0c;像PCB设计&#xff0c;电路设计原理&#xff0c;模拟电路&#xff0c;数字电路等等相关的知识…

ps基础入门

1.基础 1.1新建文件 1.2创建指定形状 1.4移动工具 1.41移动画布中的任意元素 1.42移动画布 1.43修改画布大小 1.44修改图像大小 1.5框选工具 1.6矩形工具 1.7图层 1.71图层颜色修改 1.72…

Spring事务介绍、Spring集成MyBatis

目录 1.Spring的事务1.1 什么是事务&#xff1f;1.2 事务的特性&#xff08;ACID&#xff09;1.3 Spring 事务实现方式有哪些&#xff1f;1.4 Spring事务管理接口介绍1.4.1 PlatformTransactionManager:事务管理接口1.4.2 TransactionDefinition:事务属性事务管理器接口1.4.3 T…

《昇思25天学习打卡营第1天|ghqt》

参与这个类活动&#xff0c;我会坚持完成它的。目前MindSpore文档里面的内容还看的不是很懂&#xff0c;希望自己在能不断进步。 第一天学到的内容—— 昇腾应用使能&#xff1a;华为各大产品线基于MindSpore提供的AI平台或服务能力MindSpore&#xff1a;支持端、边、云独立的…

HarmonyOS开发 弹窗组件

1.HarmonyOS开发 弹窗组件 弹窗是移动应用中常见的一种用户界面元素&#xff0c;常用于显示一些重要的信息、提示用户进行操作或收集用户输入。ArkTS提供了多种内置的弹窗供开发者使用&#xff0c;除此之外还支持自定义弹窗&#xff0c;来满足各种不同的需求。 1.1. 示例 1.…

STM32G070休眠例程-STOP模式

一、简介 主控是STM32G070&#xff0c;在低功耗休眠模式时采用Stop0模式&#xff0c;通过外部中断唤醒&#xff0c;唤醒之后&#xff0c;即可开启对应的功能输出&#xff0c;另外程序中设计有看门狗8S溢出&#xff0c;这个采用RTC定时6S周期唤醒去喂狗&#xff0c;RTC唤醒喂狗的…

在线样机生成器,制作精美的电脑手机壁纸图片展示

在线样机生成器&#xff0c;可以制作精美的电脑手机壁纸图片展示。在线样机生成器支持不同的模型如浏览器、手机、笔记本电脑、手表等结合使用&#xff0c;帮助用户快速生成样机展示图片。下面小编就来和大家分享一款免费的在线样机生成器-壁纸样机生成器。 壁纸样机生成器是一…

观测云「可观测性解决方案」荣耀登入华为云官网

继成功上架华为云云商店联营商品后&#xff0c;「观测未来可观测性解决方案」已进一步正式登陆华为云官网&#xff0c;标志着双方合作的深化与拓展。这一全新上架的解决方案是观测云技术实力的集大成之作&#xff0c;为企业提供了一个全面升级的数字化监控观测服务。 观测云&am…

LeetCode 算法:二叉树的直径 c++

原题链接&#x1f517;&#xff1a;二叉树的直径 难度&#xff1a;简单⭐️ 题目 给你一棵二叉树的根节点&#xff0c;返回该树的 直径 。 二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root 。 两节点之间路径的 长度 由…

【后端】Nginx+lua+OpenResty高性能实践

文章目录 9. HTTPS安全认证9.1 证书9.2 证书获取方式9.3 自签证书-openssl工具9.4 Nginx配置HTTPS 10. websocket转发配置 【后端&网络&大数据&数据库目录贴】 9. HTTPS安全认证 http协议问题&#xff1a; 明文传输&#xff0c;有被第三方截取到数据信息的风险 &a…

Java代码操作MySQL数据库——JDBC编程

本篇会加入个人的所谓鱼式疯言 ❤️❤️❤️鱼式疯言:❤️❤️❤️此疯言非彼疯言 而是理解过并总结出来通俗易懂的大白话, 小编会尽可能的在每个概念后插入鱼式疯言,帮助大家理解的. &#x1f92d;&#x1f92d;&#x1f92d;可能说的不是那么严谨.但小编初心是能让更多人…

LangChain入门学习笔记(六)—— Model I/O之Output Parsers

当大模型产生输出返回后&#xff0c;它的内容更像是一段平铺的文字没有结构。在传给下游节点处理时可能并不能符合输入要求&#xff0c;LangChain提供了一套机制使得模型返回的内容可以按照开发者定义的那样结构化。 在官网文档中可以看到LangChain提供了丰富的输出解析器&…

二叉树-左叶子之和(easy)

目录 一、问题描述 二、解题思路 三、代码实现 四、刷题链接 一、问题描述 二、解题思路 此题属于树遍历的简单题&#xff0c;用递归深度遍历的方式&#xff0c;当遇到左叶子结点(在递归函数中加上一个判断当前结点是左结点还是右结点的标记位)&#xff0c;此时加上当前结点…

数字图像处理实验报告小论文(Matlab语言)

1.课题分析 在当今信息化社会&#xff0c;图像处理技术已成为众多领域不可或缺的一部分&#xff0c;从医学影像分析到安防监控&#xff0c;再到日常生活中的图片美化&#xff0c;图像处理技术都发挥着至关重要的作用。本次课题主要聚焦于图像灰度处理、图像小波变换和图像分割这…

Python基础系列教程:从零开始学习Python

Python有很多功能强大的机器学习和大数据分析包&#xff0c;适合对大数据和人工智能感兴趣的同学学习。要想了解一门语言&#xff0c;首先需要了解它的语法。本文将介绍Python的一些基础语法&#xff0c;包括数据类型、变量类型、条件控制、循环结构等内容。废话少说&#xff0…

第二十四节:带你梳理Vue2 : Vue具名插槽/作用域插槽/v-slot指令

1. 具名插槽 1.1 没有使用具名插槽的问题 有的时候我们在使用子组件时,在子组件模板上不同的位置插入不同的内容, 只有一个插槽显然没法满足我们的需求,看示例: 需求如下: 子组件是一篇文章的结构父组件在调用子组件是给文章插入标题,正文,时间信息 示例代码如下: <di…

【强化学习的数学原理】课程笔记--1(基本概念,贝尔曼公式)

目录 基本概念State, Action, State transitionPolicy, Reward, Trajectory, Discount ReturnEpisodeMarkov decision process 贝尔曼公式推导确定形式的贝尔曼公式推导一般形式的贝尔曼公式State ValueAction Value 一些例子贝尔曼公式的 Matric-vector form贝尔曼公式的解析解…

Elasticsearch 索引与文档操作实践指南

上节我们部署了elasticsearch8.4.1和kibana8.4.1以及ik分词器&#xff0c;本节我们来学习下elasticsearch的相关索引&#xff0c;文档操作。 首先我们kinana的主界面找到开发者工具在里面我们来编写相关操作。 标题查看集群情况 GET /_cluster/health详细解释&#xff1a; …