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

字节跳动Android客户端实习 3+1 面经,内部面试官透露通关秘籍 身边很多朋友知道我在字节担任面试官,写文章的初衷之一也是希望能多一个为团队网络人才的渠道。但是一段时间下来发现真正找我咨询、内推的人并不多,不少同学对字节的面试有所忌惮,总担心准备不够。 实际上字节特别是客户端的面试没有网上传说的那么可怕。碍于公司制度,我不能把我面试别人的具体内容写出来,但是可以分享一些候选人自己写的面经供大家参考。 比如今天分享的这个面经,内容靠谱,能够反映出抖音客户端面试的难度。当然面试内容是因人而异的,所以主要是帮大家了解面试范围和难度,无需拘泥于具体题目 背景介绍 我是一名本.. 阅读详情

前言


本章我们基于重构的方式进行一个 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);
    }

    @Override
    public 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> {
    
    @Override
    public 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);
    }

    @Override
    public int getLayoutId() {
        return R.layout.title_view;
    }

    @Override
    public void onRootClick(View view) {

    }

    @Override
    public void setDataToView(TitleViewModel titleViewModel) {
        mBinding.setTitleViewModel(titleViewModel);
    }
}

PictureTitleView

public class PictureTitleView extends BaseView<PictureTitleViewBinding, PictureTitleViewModel> {

    public PictureTitleView(Context context) {
        super(context);
    }

    @Override
    public int getLayoutId() {
        return R.layout.picture_title_view;
    }

    @Override
    public void onRootClick(View view) {

    }

    @Override
    public 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>() {
                    @Override
                    public void onSuccess(NewsChannelsBean newsChannelsBean) {
                        mListener.onLoadSuccess(newsChannelsBean.showapiResBody.channelList);
                    }

                    @Override
                    public 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;
    @Override
    public 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();
    }

    @Override
    public void onLoadSuccess(List<NewsChannelsBean.ChannelList> channelLists) {
        if (adapter != null) {
            adapter.setChannels(channelLists);
        }
    }

    @Override
    public 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>>>() {
                    @Override
                    public 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>>() {
                    @Override
                    public 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;
    }

    @Override
    public 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() {
            @Override
            public void onRefresh(@NonNull RefreshLayout refreshLayout) {
               mNewsListModel.refresh();
            }
        });
        viewDataBinding.refreshLayout.setOnLoadMoreListener(new OnLoadMoreListener() {
            @Override
            public void onLoadMore(@NonNull RefreshLayout refreshLayout) {
                mNewsListModel.load();
            }
        });
        return viewDataBinding.getRoot();
    }

    @Override
    public void onLoadSuccess(ArrayList<BaseViewModel> baseViewModels) {
        contentList.addAll(baseViewModels);
        mAdapter.setData(contentList);
        viewDataBinding.refreshLayout.finishRefresh();
        viewDataBinding.refreshLayout.finishLoadMore();
    }

    @Override
    public 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>>>() {
                    @Override
                    public 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>>() {
                    @Override
                    public 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>() {
                    @Override
                    public void onSuccess(NewsChannelsBean newsChannelsBean) {
                        mReferenceIBaseModeListener.get().onLoadSuccess(newsChannelsBean.showapiResBody.channelList);
                    }

                    @Override
                    public 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>>() {
    @Override
    public void accept(ArrayList<BaseViewModel> baseViewModels) throws Exception {
        notifyResultToListener(baseViewModels);
    }
});

NewsChannelModel

.compose(TecentNetworkApi.getInstance().applySchedulers(new BaseObserver<NewsChannelsBean>() {
    @Override
    public void onSuccess(NewsChannelsBean newsChannelsBean) {
        notifyResultToListener(newsChannelsBean.showapiResBody.channelList);
    }

    @Override
    public 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;
        }
        // 增加缓存key
        this.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();
            // 这里可以调换成自己的 spUtils
            BasicDataPreferenceUtil.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");
    }

    @Override
    public void load() {
        TecentNetworkApi.getService(NewsApiInterface.class)
                .getNewsChannels()
                .compose(TecentNetworkApi.getInstance().applySchedulers(new BaseObserver<NewsChannelsBean>() {
                    @Override
                    public void onSuccess(NewsChannelsBean newsChannelsBean) {
                        notifyResultToListener(newsChannelsBean, newsChannelsBean.showapiResBody.channelList);
                    }

                    @Override
                    public 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;
    }

    @Override
    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>>>() {
                    @Override
                    public 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>>() {
                    @Override
                    public void accept(ArrayList<BaseViewModel> baseViewModels) throws Exception {
                        notifyResultToListener(NewsListModel.this.mNewsListBean, baseViewModels);
                    }
                });
    }
}

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

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

下一章预告


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

欢迎三连


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

【面经分享】字节抖音Android客户端3+1轮已通过 不少同学对字节的面试有所忌惮,不愿轻易尝试。其实字节尤其是客户端的面试并没有大家想象的那么难,今天分享一篇面经,内容真实,希望帮助大家更好地了解字节客户端面试的出题范围和难度。 我是一名本科应届生,学校专业是软件开发。距离毕业之期不到半年,学校也在催促我们早点出校实习,年前通过一名学长内推去了字节跳动的抖音面试。12 月 31 号投的简历,1 月 6 号收到 HR 电话。隔这么长时间还是挺害怕的,以为简历都被筛掉了。 下面是我在字节抖音的面经分享,一二面和 HR 面都是凭印象回忆的,可能会漏掉一些问题。 阅读详情

相关推荐

Android统一开发标准深度解析:从MVVM架构到工程化实践

在移动开发领域,架构模式是构建可维护、可扩展应用的核心基础。MVVM(Model-View-ViewModel)作为一种主流架构,通过数据绑定和生命周期感知机制,实现了UI逻辑与业务逻辑的清晰分离,其原理在于利用观察者模式确保数据状态与界面呈现的一致性。这种架构的技术价值在于显著提升了代码的可测试性和团队协作效率,是应对复杂业务场景的工程化基石。在实际应用场景中,结合Android Jetpack组件库(如ViewModel、LiveData/StateFlow)和统一的代码规范,能够有效解决项目碎片化与维

weixin_33802505的博客 422

Android面试题:MVC、MVP、MVVM

具有控制的部分功能,若想无限增强它的功能,甚至控制器的全部功几乎都可以迁移到各个View上(不过这样不可取,那样View干了不属于它职责范围的事情)。你看到界面异常了,有可能是你 View 的代码有 Bug,也可能是 Model 的代码有问题。客户端开发最常用的重用是View,但是数据双向绑定技术,让你在一个View都绑定了一个model,不同模块的model都不同。2.Model:对数据库的操作、对网络等的操作都应该在Model里面处理,当然对业务计算,变更等操作也是必须放在的该层的。

Demons的博客 2490

Android 开发网易面试凉凉经,面试官:基础不牢,技术不够深入,无缘offer

前言 网易的面试结果已经出来好几天了,一直拖着不是很想写面经,反正这会儿闲着无聊,又总是要写的(一来呢是当做一种记录吧,二来呢留给自己和需要的人看,好有个方向)就这会儿写了吧... 首先还是感谢吧,感谢网易面试官的肯定和建议。 网易一共经历了四轮面试,前三轮技术面,第四轮总监面。二面完了之后,HR 邀约去杭州参加现场面的,但是因为距离原因拒绝了,后来改成视频面,说实话,我现在有点后悔当初的决定了... 俗话说,见面三分情嘛哈哈哈哈 网易一面(28 min) 自我介绍 项目经历(老老实实一五一

weixin_44339238的博客 1811

Android mvvm模式原理 面试

MVVM模式的原理 MVVM模式的核心是数据绑定 MVVM模式的优点:

YDHIT的博客 1048

Android面经_安卓面经(25/30)之MVC、MVP、MVVM全解析

MVC、MVP、MVVM是安卓开发中使用较多的软件架构设计模式,特别是MVVM更是面试官喜欢提问的模式

a763450633的博客 920

Android面试指南与参考答案——基于领星安卓开发职位要求

本文基于领星安卓开发职位要求,整理Android开发岗位常见面试问题与参考答案。核心内容包括:1.基础知识:Android四层架构、SDK组件及API使用;2.编程语言:Kotlin优势、Jetpack组件应用及MVVM/MVP架构对比;3.开发工具:性能优化方法及版本适配策略;4.实战经验:第三方SDK集成和APP发布流程。针对1-3年经验者及应届生,提供代码示例和项目实例说明,覆盖框架搭建、性能优化等关键技术点,帮助候选人系统准备面试,突出技术能力和问题解决思维。

郑伟强dev的专栏 1193

Android中MVVM架构设计模式面试问题讲解

MVVM面试内容初步扫盲

水月洞天 3733

Android新闻App开发实战:从架构设计到性能优化的完整项目指南

在移动应用开发领域,Android客户端开发是核心技术方向之一,其核心在于构建高效、可维护的应用架构。现代Android开发通常采用分层架构思想,结合Jetpack组件库实现数据驱动UI的响应式编程。通过ViewModel管理界面数据、LiveData实现数据观察,开发者能够有效处理配置变更导致的数据丢失问题。Repository模式作为业务逻辑层,统一管理网络请求与本地缓存策略,结合Retrofit处理HTTP请求、Room实现本地持久化,为应用提供稳定可靠的数据源。这种架构在新闻类App等数据驱动型应用

weixin_30846347的博客 208

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

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

IT_Android的博客 959

ArchDroid-MVP框架-Android开发效率提升工具-封装Activity与Fragment基类支持ViewBinding泛型与MVP模式生命周期监听绑定解决内存泄漏问题.zip

Server性能优化实战项目ArchDroid_MVP框架_Android开发效率提升工具_封装Activity与Fragment基类支持ViewBinding泛型与MVP模式生命周期监听绑定解决内存泄漏问题.zip

【全开源】旅游系统源码(Uniapp+FastAdmin+ThinkPHP)

通过源码的灵活配置和定制开发,我们可以根据旅游企业的实际需求,快速构建出符合其业务特点的旅游系统。旅游系统源码,作为这一变革的基石,为旅游业的数字化转型提供了强有力的支撑。通过深入研究和应用旅游系统源码,我们能够构建出更加智能、便捷、个性化的旅游服务平台,满足游客日益增长的旅游需求。国际化发展:随着全球化的深入发展,旅游系统源码也将迎来国际化的发展机遇,为国内外游客提供更加便捷、高效的旅游服务。跨界融合:旅游系统源码将与更多领域进行跨界融合,如与交通、餐饮、购物等行业的结合,为游客提供更加丰富的旅游体验。

2401_84413757的博客 2147

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

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

IT_Android的博客 1093

STM32 USB AUDIO实战指南——从零构建音频设备

本文详细介绍了如何使用STM32F407构建USB音频设备,从硬件准备到软件配置,再到音频播放和录制功能的实现。通过STM32CubeMX配置和实战代码示例,帮助开发者快速掌握USB Audio Class规范,实现低延迟、高质量的音频传输,适用于语音交互、音乐播放等多种场景。

weixin_42549037的博客 484

android mvvm livedata_各种版本的 玩Android 来啦!

一直说给大家整理一波各个版本的 wanandroid 客户端,好了,来啦!由于优秀客户端实在太多,本文只能按照技术栈的不同,选出几个具有代表性的项目。大家可以根据自己感兴趣的技术栈选择客户端进行学习,当然也可以根据体验效果好坏进行选择,文末有文中提到的所有 Apk下载。1Java 版本(一)基于 :Material Design + MVP + Rxjava2 + Retrofit +...

weixin_39883374的博客 313

Hcaptcha验证码破解深度解析:从环境检测到n值算法的攻防实战

本文深入解析Hcaptcha验证码的攻防技术,从环境检测到n值算法的全面突破策略。详细介绍了浏览器指纹模拟、图像识别工程化解决方案及协议层逆向分析,为安全研究人员提供高效破解Hcaptcha的实战指南。

weixin_42563415的博客 371

OpenClaw Browser Relay Extension 鉴权失败与 invalid handshake 问题的终极解决方案

在使用 OpenClaw 浏览器插件 (Browser Relay Extension) 时,许多用户可能会遇到两个主要问题:一是配置端口错误导致的连接失败,二是鉴权失败 (Gateway token rejected)。本文将详细解析这两个问题并提供解决方案。

gerryke的blog 5556

字节跳动抖音安卓客户端日常实习 3+1 面经(已 OC)

一二面和 HR 面都是凭印象回忆的,可能会漏掉一些问题。 一面(1 月 13 日,68 min) 提问 自我介绍 简单讲讲你项目的整个设计过程 你提到项目中用的是 MVVM 架构,讲讲你对 MVVM 的认识? MVVM 的优点有什么呢? MVP 中 Model、View 和 Presenter 的持有关系是怎么样的? MVVM 中的持有关系呢? MVVM 中各个层在 Android 开发中的对应关系? 你的 MVVM 用到了 Jetpack 里的 ViewModel,那么它是如何保持数据的呢? V

Androidbye的博客 559

大语言模型(LLM)在组合优化中的新突破:LLM-LNS与FunSearch性能对比

本文深入探讨了大语言模型(LLM)在组合优化中的创新应用,重点对比了LLM-LNS与FunSearch两种方法的性能差异。研究表明,LLM-LNS在物流路径规划、资源调度等场景中展现出显著优势,其双层自演化架构能有效平衡探索与利用,相比传统方法提升效率约43%。文章还提供了实践选择指南,帮助开发者根据具体需求选用合适的技术方案。

weixin_42526415的博客 404

字节跳动(抖音)Android客户端实习 3+1 面经

或者说,不同的操作会触发不同的调用吗,比如有的调用,有的不调用?这个没答上来,然后面试官很耐心地解释了这个概念,实际上就是两个 View 叠在一起,下层被上层挡住的部分是看不见的,但还是会进行绘制。这个没答上来,然后面试官很耐心地解释了这个概念,实际上就是两个 View 叠在一起,下层被上层挡住的部分是看不见的,但还是会进行绘制。如果用 Kotlin 去调用一个具有返回值的 Java 函数,那么这个返回值是可空的还是非空的呢?3. 你提到项目中用的是 MVVM 架构,讲讲你对 MVVM 的认识?

2401_84149724的博客 900

基于STM32L4XX、HAL库的MCP4561数字电位器驱动程序设计

/ HAL库需要左移一位。* @param address: 设备地址(0x2C-0x2F)* @param hmcp: MCP4561句柄。* @param hmcp: MCP4561句柄。* @param hmcp: MCP4561句柄。* @param hmcp: MCP4561句柄。* @param hmcp: MCP4561句柄。* @param hmcp: MCP4561句柄。* @param hmcp: MCP4561句柄。* @param hi2c: I2C句柄。

zhusl6688的博客 340

【DRAM存储器五十六】LPDDR5介绍--WCK2CK Leveling和Read gate training

简述LPDDR5 WCK2CK leveling和read gate traning基本原理与操作步骤

highman110的博客 8787
上一篇: 如何应对 Android 面试官 -> MVVM 实战一个新闻客户端 (上)
下一篇: 如何应对 Android 面试官 -> MVVM 实战一个新闻客户端 (下)
老A说
博客等级 码龄14年 1181粉丝 85原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值