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

Android MVVM框架搭建(二)OKHttp + Retrofit + RxJava 只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是停留于皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。腾讯、字节跳动、阿里、百度等BAT大厂 2019-2021面试真题解析资料太多,全部展示会影响篇幅,暂时就先列举这些部分截图《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可免费领取!tlink; 阅读详情

前言


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

缓存数据处理


上一章我们讲到了网络数据的缓存逻辑,但是缓存之后我们还没有使用,接下来我们来看下如何使用缓存的数据;

image.png

数据的整体来源是两个方向,一个是 Server 端数据,一个是 SP 中的数据;

我们需要在底层数据分发的时候就来判断是否来自缓存数据,新增一个接口,同时在底层数据分发的时候进行判断并替换;

public interface MvvmDataObserver<F> {
    void onSuccess(F t, boolean isFromCache);
    void onFailure(String errorMsg);
}

因为我们使用了 RxJava 来做网络接口层的转换,所以我们的 BaseObserver 需要重构一下:

public class BaseObserver<T> implements Observer<T> {

    private MvvmDataObserver mvvmDataObserver;

    public BaseObserver(MvvmDataObserver mvvmDataObserver) {
        this.mvvmDataObserver = mvvmDataObserver;
    }

    @Override
    public void onSubscribe(Disposable d) {}

    @Override
    public void onNext(T t) {
        mvvmDataObserver.onSuccess(t, false);
    }

    @Override
    public void onError(Throwable e) {
        mvvmDataObserver.onFailure(e);
    }

    @Override
    public void onComplete() {}
}

也就是说将数据的分发,统一交给了 MvvmDataObserver 来分发;然后,需要 BaseMvvmModel 来实现这个 MvvmDataObserver 接口,然后子 Model 来做接口的具体实现;

public abstract class BaseMvvmModel<NETWORK_DATA, RESULT_DATA> implements MvvmDataObserver<NETWORK_DATA> {

}

NewListModel 重构如下:

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())
                .subscribe(new BaseObserver<>(this));
    }

    @Override
    public void onSuccess(NewsListBean newsListBean, boolean isFromCache) {
        ArrayList<BaseViewModel> viewModels = new ArrayList<>();
        NewsListModel.this.mNewsListBean = newsListBean;
        for (NewsListBean.Contentlist contentlist : newsListBean.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);
            }
        }
        notifyResultToListener(NewsListModel.this.mNewsListBean, viewModels, isFromCache);
    }

    @Override
    public void onFailure(Throwable throwable) {

    }
}

NewsChannelModel 重构如下:

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>(this)));
    }

    @Override
    public void onSuccess(NewsChannelsBean newsChannelsBean, boolean isFromCache) {
        notifyResultToListener(newsChannelsBean, newsChannelsBean.showapiResBody.channelList, isFromCache);
    }

    @Override
    public void onFailure(Throwable throwable) {
        mReferenceIBaseModeListener.get().onLoadFail(400, throwable.getMessage());
    }
}

然后我们的 notifyResultToListener 重构如下,增加了 isFromCache 的判断逻辑;

protected void notifyResultToListener(NETWORK_DATA networkData, RESULT_DATA data, boolean isFromCache) {
    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 && !isFromCache) {
            saveDataToPreference(networkData);
        }
    } else {
        if (mCachedPreferenceKey != null && !isFromCache) {
            saveDataToPreference(networkData);
        }
    }

    if (mIsPaging) {
        if (data != null && ((List) data).size() > 0 && !isFromCache) {
            mPageNum++;
        }
    }
    if (!isFromCache) {
        isLoading = false;
    }
}

只有非缓存的时候,我们才将数据保存到本地,根据 isFromCache 标识;

我们接下来处理 缓存的读取和网络请求的发送逻辑;同样的,我们需要在 BaseMvvmModel 中进行逻辑的判断和处理,我们增加一个 getCachedDataAndLoad 接口,先从缓存获取同时进行网络数据的获取;

public void getCachedDataAndLoad() {
    if (!isLoading) {
        isLoading = true;
    }
    String cacheStr = BasicDataPreferenceUtil.getInstance().getString(mCachedPreferenceKey);
    if (!TextUtils.isEmpty(cacheStr)) {
        try {
            // 反序列化
            NETWORK_DATA data = new Gson().fromJson(new JSONObject(cacheStr).getString("data"),
                    (Class<NETWORK_DATA>) GenericUtils.getGenericType(this));
            if (data != null) {
                onSuccess(data, true);
            }
            if (isNeedUpdate()) {
                load();
                return;
            }
        } catch (JSONException e) {
            // do nothing
        }
    }
    load();
}

然后,Fragment 中调用这个 getCachedDataAndLoad 方法进行数据的拉取;

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

运行,可以看到,我们的页面在无网状态下依然可以展示数据,但是如果我们的 App 是在无网的时候安装并运行的,那么依然是会展示一片空白,这个时候,其实是需要我们进行一些数据的预置的,我们可以增加一下预置数据的获取逻辑;

public abstract class BaseMvvmModel<NETWORK_DATA, RESULT_DATA> implements MvvmDataObserver<NETWORK_DATA> {
    
    private String mPreDefinedData;
    ```
    public BaseMvvmModel(boolean isPaging, String cachedPreferenceKey, String preDefinedData, int... initPageNumber) {
        if (isPaging && initPageNumber != null && initPageNumber.length > 0) {
            INIT_PAGE_NUMBER = initPageNumber[0];
        } else {
            INIT_PAGE_NUMBER = -1;
        }
        this.mCachedPreferenceKey = cachedPreferenceKey;
        // 赋值预置数据
        this.mPreDefinedData = preDefinedData;
    }
    
    // 获取预置数据
    ```
    public void getCachedDataAndLoad() {
        if (!isLoading) {
            isLoading = true;
        }
        if (mCachedPreferenceKey != null) {
            String cacheStr = BasicDataPreferenceUtil.getInstance().getString(mCachedPreferenceKey);
            if (!TextUtils.isEmpty(cacheStr)) {
                try {
                    // 反序列化
                    NETWORK_DATA data = new Gson().fromJson(new JSONObject(cacheStr).getString("data"),
                            (Class<NETWORK_DATA>) GenericUtils.getGenericType(this));
                    if (data != null) {
                        onSuccess(data, true);
                    }
                    if (isNeedUpdate()) {
                        load();
                        return;
                    }
                } catch (JSONException e) {
                    // do nothing
                }
            }
            // 从预置数据中取数据
            if (mPreDefinedData != null) {
                NETWORK_DATA data = new Gson().fromJson(mPreDefinedData,
                        (Class<NETWORK_DATA>) GenericUtils.getGenericType(this));
                if (data != null) {
                    onSuccess(data, true);
                }
            }
        }
        load();
    }
}

子 Model 中传入预置数据

public class NewsChannelModel extends BaseMvvmModel<NewsChannelsBean, List<NewsChannelsBean.ChannelList>> {

    public static final String PREDEFINED_CHANNELS = "{"showapi_res_body":{"channelList":[{"channelId":"5572a108b3cdc86cf39001cd","name":"国内焦点"},{"channelId":"5572a108b3cdc86cf39001ce","name":"国际焦点"},{"channelId":"5572a108b3cdc86cf39001cf","name":"军事焦点"},{"channelId":"5572a108b3cdc86cf39001d0","name":"财经焦点"},{"channelId":"5572a108b3cdc86cf39001d1","name":"互联网焦点"},{"channelId":"5572a108b3cdc86cf39001d2","name":"房产焦点"},{"channelId":"5572a108b3cdc86cf39001d3","name":"汽车焦点"},{"channelId":"5572a108b3cdc86cf39001d4","name":"体育焦点"},{"channelId":"5572a108b3cdc86cf39001d5","name":"娱乐焦点"},{"channelId":"5572a108b3cdc86cf39001d6","name":"游戏焦点"},{"channelId":"5572a108b3cdc86cf39001d7","name":"教育焦点"},{"channelId":"5572a108b3cdc86cf39001d8","name":"女人焦点"},{"channelId":"5572a108b3cdc86cf39001d9","name":"科技焦点"},{"channelId":"5572a109b3cdc86cf39001da","name":"社会焦点"},{"channelId":"5572a109b3cdc86cf39001db","name":"国内最新"},{"channelId":"5572a109b3cdc86cf39001dc","name":"台湾最新"},{"channelId":"5572a109b3cdc86cf39001dd","name":"港澳最新"},{"channelId":"5572a109b3cdc86cf39001de","name":"国际最新"},{"channelId":"5572a109b3cdc86cf39001df","name":"军事最新"},{"channelId":"5572a109b3cdc86cf39001e0","name":"财经最新"},{"channelId":"5572a109b3cdc86cf39001e1","name":"理财最新"},{"channelId":"5572a109b3cdc86cf39001e2","name":"宏观经济最新"},{"channelId":"5572a109b3cdc86cf39001e3","name":"互联网最新"},{"channelId":"5572a109b3cdc86cf39001e4","name":"房产最新"},{"channelId":"5572a109b3cdc86cf39001e5","name":"汽车最新"},{"channelId":"5572a109b3cdc86cf39001e6","name":"体育最新"},{"channelId":"5572a10ab3cdc86cf39001e7","name":"国际足球最新"},{"channelId":"5572a10ab3cdc86cf39001e8","name":"国内足球最新"},{"channelId":"5572a10ab3cdc86cf39001e9","name":"CBA最新"},{"channelId":"5572a10ab3cdc86cf39001ea","name":"综合体育最新"},{"channelId":"5572a10ab3cdc86cf39001eb","name":"娱乐最新"},{"channelId":"5572a10ab3cdc86cf39001ec","name":"电影最新"},{"channelId":"5572a10ab3cdc86cf39001ed","name":"电视最新"},{"channelId":"5572a10ab3cdc86cf39001ee","name":"游戏最新"},{"channelId":"5572a10ab3cdc86cf39001ef","name":"教育最新"},{"channelId":"5572a10ab3cdc86cf39001f0","name":"女人最新"},{"channelId":"5572a10ab3cdc86cf39001f1","name":"美容护肤最新"},{"channelId":"5572a10ab3cdc86cf39001f2","name":"情感两性最新"},{"channelId":"5572a10ab3cdc86cf39001f3","name":"健康养生最新"},{"channelId":"5572a10ab3cdc86cf39001f4","name":"科技最新"},{"channelId":"5572a10bb3cdc86cf39001f5","name":"数码最新"},{"channelId":"5572a10bb3cdc86cf39001f6","name":"电脑最新"},{"channelId":"5572a10bb3cdc86cf39001f7","name":"科普最新"},{"channelId":"5572a10bb3cdc86cf39001f8","name":"社会最新"},{"channelId":"57463656a44a13cf","name":"旅游最新"},{"channelId":"9a15d3d4b5cab17f34e6d7976b1b1f30","name":"电商最新"},{"channelId":"5b4fe11ec1239b0805568da6e90fd875","name":"物流最新"},{"channelId":"3f4794412eaa17ed","name":"创业最新"}],"ret_code":0,"totalNum":48},"showapi_res_code":0,"showapi_res_error":""}";

    public NewsChannelModel() {
        super(false, "NEWS_CHANNEL_PREF_KEY", PREDEFINED_CHANNELS);
    }
}

运行效果如下,可以看到界面上不那么空白了,通过预置数据;

image.png

Jetpack 的 ViewModel + LiveData

千呼万唤,ViewModel 来了,接下来我们要引入 ViewModel 用来监听网络数据的返回,以及结合 LiveData 实现数据更新的处理;

我们分别定义两个 ViewModel,以及对应的 ViewModel 中声明 LiveData 来观测数据的变化并及时更新到UI

HeadlineNewsViewModel 持有 LiveData 和 Model,并实现 IBaseModelListener 来接受网络请求回调结果,待结果返回之后,由 LiveData 将数据 post 到 View 层;

public class HeadlineNewsViewModel extends ViewModel implements IBaseModelListener<List<NewsChannelsBean.ChannelList>> {

    private NewsChannelModel model;
    public MutableLiveData<List<NewsChannelsBean.ChannelList>> dataList = new MutableLiveData<>();

    public HeadlineNewsViewModel() {
        model = new NewsChannelModel();
        model.register(this);
        model.getCachedDataAndLoad();
    }

    @Override
    public void onLoadSuccess(BaseMvvmModel model, List<NewsChannelsBean.ChannelList> channelLists, PageResult... pageResults) {
        if (model instanceof NewsChannelModel) {
            dataList.postValue(channelLists);
        }
    }

    @Override
    public void onLoadFail(int errorCode, String errorMsg) {

    }
}

HeadlineNewsFragment 中持有 ViewModel 并获取 LiveData 监听数据变化;

public class HeadlineNewsFragment extends Fragment {
    public HeadlineNewsFragmentAdapter adapter;
    FragmentHomeBinding viewDataBinding;
    private HeadlineNewsViewModel mHeadlineNewsViewModel = new HeadlineNewsViewModel();

    @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);
        mHeadlineNewsViewModel.dataList.observe(this, channelLists -> {
            if (adapter != null) {
                adapter.setChannels(channelLists);
            }
        });
        return viewDataBinding.getRoot();
    }
}

同样的 NewListViewModel 中也这样处理下,但是不同的地方在于这个页面需要分页和加载更多,ViewModel 提供对应的 refresh 和 loadNextPage,同时声明 ViewStatus 类型的 LiveData 用来更新界面状态;

public class NewsListViewModel extends ViewModel implements IBaseModelListener<ArrayList<BaseViewModel>> {

    private NewsListModel model;
    public MutableLiveData<ArrayList<BaseViewModel>> dataList = new MutableLiveData<>();
    public MutableLiveData<ViewStatus> viewStatus = new MutableLiveData<>();

    public NewsListViewModel(String channelId, String channelName) {
        dataList.setValue(new ArrayList());
        model = new NewsListModel(channelId, channelName);
        model.register(this);
        model.getCachedDataAndLoad();
    }

    public void refresh() {
        model.refresh();
    }

    public void loadNextPage() {
        model.loadNextPage();
    }

    @Override
    public void onLoadSuccess(BaseMvvmModel model, ArrayList<BaseViewModel> baseViewModels, PageResult... pageResults) {
        if (model instanceof NewsListModel) {
            if (pageResults[0].isFirstPage) {
                dataList.getValue().clear();
            }
            if (pageResults[0].isEmpty) {
                if (pageResults[0].isFirstPage) {
                    viewStatus.postValue(ViewStatus.EMPTY);
                } else {
                    viewStatus.postValue(ViewStatus.NO_MORE_DATA);
                }
            } else {
                dataList.getValue().addAll(baseViewModels);
                dataList.postValue(dataList.getValue());
                viewStatus.postValue(ViewStatus.SHOW_CONTENT);
            }
        }
    }

    @Override
    public void onLoadFail(int errorCode, String errorMsg) {

    }
}

NewListFragment 中获取 dataList 并 observer

public class NewsListFragment extends Fragment {
    private NewsListRecyclerViewAdapter mAdapter;
    private FragmentNewsBinding viewDataBinding;
    private NewsListViewModel mNewsListViewModel;
    ArrayList<BaseViewModel> contentList = new ArrayList<>();
    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);
        mNewsListViewModel = new NewsListViewModel(getArguments().getString(BUNDLE_KEY_PARAM_CHANNEL_ID),
                getArguments().getString(BUNDLE_KEY_PARAM_CHANNEL_NAME));
        mNewsListViewModel.dataList.observe(this, baseViewModels -> {
            mAdapter.setData(contentList);
            viewDataBinding.refreshLayout.finishRefresh();
            viewDataBinding.refreshLayout.finishLoadMore();
        });
        viewDataBinding.refreshLayout.setOnRefreshListener(refreshLayout -> mNewsListViewModel.refresh());
        viewDataBinding.refreshLayout.setOnLoadMoreListener(refreshLayout -> mNewsListViewModel.loadNextPage());
        return viewDataBinding.getRoot();
    }
}

BaseMvvmViewModel


我们前面是用来 ViewModel,但是没有抽取一个 base 层,它们其实是有公共的地方可以抽取出来的,我们来创建 BaseMvvmViewModel 并重构下;

public abstract class BaseMvvmViewModel<MODEL extends BaseMvvmModel, DATA> extends ViewModel implements IBaseModelListener<List<DATA>> {

    public MutableLiveData<List<DATA>> dataList = new MutableLiveData<>();
    public MutableLiveData<ViewStatus> viewStatus = new MutableLiveData<>();
    protected MODEL model;

    public BaseMvvmViewModel() {
        // viewStatus.postValue(ViewStatus.LOADING);
    }

    public abstract MODEL createModel();

    public void getCacheDataAndLoad() {
        createAndRegisterModel();
        if (model != null) {
            model.getCachedDataAndLoad();
        }
    }

    public void refresh() {
        createAndRegisterModel();
        if (model != null) {
            model.refresh();
        }
    }

    public void loadNextPage() {
        createAndRegisterModel();
        if (model != null) {
            model.loadNextPage();
        }
    }

    /**
     * activity/fragment 销毁的时候会调用 viewmodel 的这个方法,进行业务处理
     */
    @Override
    protected void onCleared() {
        super.onCleared();
    }

    private void createAndRegisterModel() {
        if (model == null) {
            model = createModel();
            model.register(this);
        }
    }

    @Override
    public void onLoadSuccess(BaseMvvmModel model, List<DATA> data, PageResult... pageResults) {
        if (model.isPaging()) {
            if (pageResults[0].isEmpty) {
                if (pageResults[0].isFirstPage) {
                    viewStatus.postValue(ViewStatus.EMPTY);
                } else {
                    viewStatus.postValue(ViewStatus.NO_MORE_DATA);
                }
            } else {
                if (pageResults[0].isFirstPage) {
                    dataList.postValue(data);
                } else {
                    dataList.getValue().addAll(data);
                    dataList.postValue(dataList.getValue());
                }
                viewStatus.postValue(ViewStatus.SHOW_CONTENT);
            }
        } else {
            dataList.postValue(data);
            viewStatus.postValue(ViewStatus.SHOW_CONTENT);
        }
    }

    @Override
    public void onLoadFail(int errorCode, String errorMsg) {
        viewStatus.postValue(ViewStatus.REFRESH_ERROR);
    }
}

子 ViewModel 继承这个 BaseMvvmViewModel,并修改相关逻辑;

public class NewsListViewModel extends BaseMvvmViewModel<NewsListModel, BaseViewModel> {

    private final String mChannelId;
    private final String mChannelName;

    public NewsListViewModel(String channelId, String channelName) {
        this.mChannelId = channelId;
        this.mChannelName = channelName;
    }

    @Override
    public NewsListModel createModel() {
        return new NewsListModel(mChannelId, mChannelName);
    }

    public void refresh() {
        model.refresh();
    }

    public void loadNextPage() {
        model.loadNextPage();
    }
}

HeadlineNewsViewModel 重构如下:

public class HeadlineNewsViewModel extends BaseMvvmViewModel<NewsChannelModel, NewsChannelsBean.ChannelList> {

    @Override
    public NewsChannelModel createModel() {
        return new NewsChannelModel();
    }
}

因为删除了构造方法中的调用 model 层的逻辑,所以需要我们在 Fragment 中手动调用一下数据的拉取;

mNewsListViewModel = new NewsListViewModel(getArguments().getString(BUNDLE_KEY_PARAM_CHANNEL_ID),
        getArguments().getString(BUNDLE_KEY_PARAM_CHANNEL_NAME));
mNewsListViewModel.getCacheDataAndLoad();
mHeadlineNewsViewModel = new HeadlineNewsViewModel();
mHeadlineNewsViewModel.getCacheDataAndLoad();

运行,可以正常的拉取数据并展示,但是,我们在使用 ViewPager + Fragment 的时候,通常都是懒加载的方式,也就是 Fragment 可见的时候,我们才会 load 并展示数据,我们来实现一下懒加载;

懒加载

这里我们借助 LifeCycle 来监听页面 onResume 的时候,来进行数据的加载;因此我们的 BaseMvvmViewModel 需要实现 LifeCycleObserver 接口;

public abstract class BaseMvvmViewModel<MODEL extends BaseMvvmModel, DATA> extends ViewModel implements LifecycleObserver, IBaseModelListener<List<DATA>> {

    // ...
    // 省略部分代码
    
    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    private void onResume() {
        if (dataList == null || dataList.getValue() == null || dataList.getValue().isEmpty()) {
            createAndRegisterModel();
            model.getCachedDataAndLoad();
        } else {
            dataList.postValue(dataList.getValue());
            viewStatus.postValue(viewStatus.getValue());
        }
    }
}

然后,对应的 Fragment 中添加下 observer,同时也不用主动调用了 getCacheDataAndLoad 方法了;

getLifecycle().addObserver(mNewsListViewModel);

HeadlineNewsFragment

getLifecycle().addObserver(mHeadlineNewsViewModel);

BaseView

到这里的时候,可以看到,我们的 base 下的 mvvm 中 BaseMvvmModel 和 BaseMvvmViewModel 都有了,就差 BaseMvvmView 了,我们来抽取下;

image.png

public abstract class BaseMvvmFragment<VIEW extends ViewDataBinding, VIEWMODEL extends BaseMvvmViewModel, DATA> extends Fragment implements Observer {

    protected VIEWMODEL viewmodel;
    protected VIEW viewDataBinding;

    public abstract int getLayoutId();
    public abstract VIEWMODEL getViewmodel();
    public abstract String getFragmentTag();
    public abstract void onNetworkResponded(List<DATA> dataList, boolean isDataUpdated);
    public abstract void onViewCreated();

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(getFragmentTag(), "onCreate");
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        viewDataBinding = DataBindingUtil.inflate(inflater, getLayoutId(), container, false);
        return viewDataBinding.getRoot();
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        viewmodel = getViewmodel();
        getLifecycle().addObserver(viewmodel);
        viewmodel.dataList.observe(this, this);
        viewmodel.viewStatus.observe(this, this);
        onViewCreated();
    }

    @Override
    public void onChanged(Object o) {
        if (o instanceof ArrayList) {
            onNetworkResponded((List<DATA>) o, true);
        }
    }
}

对应的子 Fragment 继承这个 Base 然后重写对应的实现即可;

public class NewsListFragment extends BaseMvvmFragment<FragmentNewsBinding, NewsListViewModel, BaseViewModel> {
    private NewsListRecyclerViewAdapter mAdapter;
    ArrayList<BaseViewModel> contentList = new ArrayList<>();
    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 int getLayoutId() {
        return  R.layout.fragment_news;
    }

    @Override
    public NewsListViewModel getViewmodel() {
        return viewmodel = new NewsListViewModel(getArguments().getString(BUNDLE_KEY_PARAM_CHANNEL_ID),
                getArguments().getString(BUNDLE_KEY_PARAM_CHANNEL_NAME));
    }

    @Override
    public String getFragmentTag() {
        return getArguments().getString(BUNDLE_KEY_PARAM_CHANNEL_ID);
    }

    @Override
    public void onNetworkResponded(List<BaseViewModel> baseViewModels, boolean isDataUpdated) {
        if (isDataUpdated) {
            mAdapter.setData(contentList);
            viewDataBinding.refreshLayout.finishRefresh();
            viewDataBinding.refreshLayout.finishLoadMore();
        }
    }

    @Override
    public void onViewCreated() {
        mAdapter = new NewsListRecyclerViewAdapter(getContext());
        viewDataBinding.listview.setHasFixedSize(true);
        viewDataBinding.listview.setLayoutManager(new LinearLayoutManager(getContext()));
        viewDataBinding.listview.setAdapter(mAdapter);

        viewDataBinding.refreshLayout.setOnRefreshListener(refreshLayout -> viewmodel.refresh());
        viewDataBinding.refreshLayout.setOnLoadMoreListener(refreshLayout -> viewmodel.loadNextPage());
    }
}

ViewModelProvider

这里我们不能每次都去创建 ViewModel,否则就会导致每次都重新请求数据,这里借助 ViewModelProvider 来控制 ViewModel 的创建;这里直接上代码了

public static class NewsListViewModelFactory implements ViewModelProvider.Factory {
    private String mChannelId;
    private String mChannelName;

    public NewsListViewModelFactory(String channelId, String channelName) {
        this.mChannelId = channelId;
        this.mChannelName = channelName;
    }

    @NonNull
    @Override
    public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
        return (T) new NewsListViewModel(mChannelId, mChannelName);
    }
}

Fragment 中 createViewModel 重构如下:

@Override
public NewsListViewModel getViewmodel() {
    return new ViewModelProvider(getActivity(), new NewsListViewModel.NewsListViewModelFactory(getArguments().getString(BUNDLE_KEY_PARAM_CHANNEL_ID),
            getArguments().getString(BUNDLE_KEY_PARAM_CHANNEL_NAME))).get(getArguments().getString(BUNDLE_KEY_PARAM_CHANNEL_ID), NewsListViewModel.class);
}

好了,MVVM 的上中下就全部讲完了;

上篇

中篇

欢迎三连


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

RK3572-Android16-记录一次OTA升级失败解决方案 这里记录一次解决RK3576 Android16 OTA 用Recovery 升级失败的一次解决案例,将部分经验作为总结。本次OTA升级耗时了几天,最终是RK给出的解决方案,再加上经验来解决了其它部分问题新平台,高一点的Android版本,可能本身有bug,多请教平台来,不要蒙着干,干不出来的。 阅读详情

相关推荐

Apollo MPC横纵向耦合控制学习笔记

Apollo MPC横纵向控制原理学习笔记

weixin_39199083的博客 1万+

ARoute+Rxjava2+Retrofit2+Okhttp+MVVM

3月份快结束了,天气也渐渐变暖,希望2020年android行情也能够逐渐回暖吧,等待之余,我们也要不断的提升自己的技术,跟上技术更新的步伐。这不眼看着2020年Google IO大会即将发布新的功能,android10.0系统还没有玩熟,android11即将来临。是不是很可怕,是不是瞬间感觉android开发好痛苦,不要慌!!!饭要一口一口吃,路要一步一步走。是不是有些同学,对AAC(andr...

qinbin2015的博客 1344

CAPL编程的催化剂——CAPLdll的实现

CANoe作为一款功能强大的总线开发及测试工具,不仅仅是因为其丰富的库函数,最重要的是其兼容了多种编程语言,可以实现灵活的开发,极大的解决了纯CAPL编程所带来的限制,如通过动态链接库(DLL)来实现复杂函数的编写,使其能够在CAPL中可以调用,下面将对DLL的实现进行分析。capldll.cpp这个文件初看相当复杂,从整体上来说,这段代码主要包含了两部分:自定义函数实现和回调函数定义,今天的主要任务是实现自定义函数,这里的函数定义与普通函数定义差不多,只是利用了CAPL的格式。在VS2017工程中打开。

车载控制器AutoSAR软件开发工程师 4646

MVVM原理(3):数据监听器Observer

一、Observer Observer的目的是利用Object.defineProperty()中的get和set,在数据变化时,可以通知Dep class Observer { constructor (data) { this.observe(data) } observe (data) { if(data && typeof data === 'object') { Object.keys(data).

qq_44852448的博客 543

集成电路设计学习笔记(二)基于IC617反相器设计之原理图绘制

博主的博客网站 欢迎大家访问! ccbirds.cn ccbirds.github.io 本文主要记录在IC617中原理图的绘制和schematic的使用方法。 新建library 要画设计自己的集成电路,需要建一个自己的库,方便对自己设计文件的管理。 打开library manager,file 下NEW下Library 选择库的保存路径,一般默认即可,并在name中填入自己库的名字。...

ccbirds 7371

【STM32Cube HAL】输入捕获(六)——脉宽测量

1

HAHAHA 4907

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

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

IT_Android的博客 1093

华为海思IC设计岗位笔试全解析:物理、电路与工艺三大方向备考指南

本文全面解析华为海思IC设计岗位笔试的物理、电路与工艺三大方向备考要点。针对物理设计方向,重点讲解半导体能带理论、载流子输运机制等核心考点;电路设计方向涵盖Verilog语法、UVM验证等关键技术;工艺方向则深入探讨FinFET工艺、光刻技术等前沿知识。提供高效备考策略和应试技巧,助力考生提升通过率。

weixin_29196613的博客 1084

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

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

CTZL123456的博客 1042

【即插即用模块】Transformer篇 | MICCAI 2025 | ReLULA:Q/K双ReLU线性注意力,既轻又准!

本文介绍了一种基于线性注意力的多实例学习模块(Lin-MIL),适用于高分辨率视觉任务。该模块通过转置计算顺序和使用ReLU激活,将计算复杂度从O(n²d)降至O(nd²),同时提升特征判别性。核心创新包括Q/K双ReLU激活、可学习温度系数和数值稳定归一化。实验表明,该模块在轻量化视觉任务中优于传统线性注意力和Mamba架构。代码实现了模块化矩阵乘法,支持即插即用替换ViT自注意力层。该模块特别适合计算病理学等需要处理大量图像补丁的任务。

主要更新底层视觉(去噪、超分等)相关的科研内容,形式为【论文精读】+【论文复现】 140

Python 爬虫实战:华尔街见闻精选文章爬取(反反爬 + 投资情绪分析)

Python 爬虫实战:华尔街见闻精选文章爬取(反反爬 + 投资情绪分析)

yansideyucsdn的博客 969

Android 14+ 模拟器抓包实录:把 Burp CA 装成系统 CA,让 targetSdk≥34 的 App 乖乖解密

本文记录了在 Android 14+ 环境下,通过将 Burp CA 安装为系统信任锚,成功抓取 targetSdk=35 App 的 HTTPS 明文流量的完整实操过程。针对“浏览器可用、App 不可用”的典型现象,精准识别出根本原因是系统级证书信任策略变更(用户安装的 CA 被屏蔽),而非 SSL Pinning。仅通过 adb 命令将 CA 推送至系统证书存储并重启生效,无需 Frida、Objection 或修改 APK,实现零侵入解密。全文命令均基于 Windows + MuMu 模拟器环境实测通

JIANqiao19931029的博客 341

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

具身智能 2026:启元人形机器人开发者生态的“安卓时刻”来了吗

一个贯穿全文的问题:人形机器人开发者生态,是否正从「封闭垂直」走向「开源平台化」?本文以开源底座的公开数据为标尺,盘点 SDK、URDF、仿真与技能分发的开放程度,并落回启元机器人的开源路径。所有关键数据均标注来源,文末附社区与学堂链接。

在北风吹起的时候加入我们的队伍 在秋雨到来的时候成为彼此的温暖 744

文档:亲测可用解决vnc灰屏,黑屏,鼠标大黑叉情况

主要解决vnc-viewer灰屏,黑屏,鼠标大黑叉等现象,详细见文档。

蓝牙模块HM-10手册

蓝牙模块HM-10手册,该手册对蓝牙模块HM-10进行了详细的介绍和使用方法以及一些AT指令。

android-ad-cleaner:android免 Root 批量关闭系统开屏与营销推送

橘子,计算机视觉方向工程师,深耕 AIGC 与多媒体处理领域,专注技术实用化落地。本文为原创内容,转载请注明来源与作者。优快云 首发 | 版权所有 © 2026。

下载资源或查看教程前,请务必阅读置顶的《免责声明》 501

Android 14 SELinux策略深度解析

init 的全局公开约束(如 neverallow init execute_no_trans,放 public 才能约束所有分区的规则)这就是"哪些可见"的强制机制。alps_s0(MTK 工程)的 device/mediatek/sepolicy/basic/ 提供。所有分区(system/system_ext/product/vendor)都可引用。init的公开约束、跨分区共用的 type/attribute/neverallow。——vendor 的 .cil 只能引用 plat 的。

新时代农民工 102

Android 开发入门实践(二):Kotlin 空安全,从可空类型到安全访问

延续 NewsApp 项目中的新闻标题练习,记录 Kotlin 安全调用。

Easyhood的博客 276

Android 模块化收口:能力组合框架、手写 LifecycleOwner 与热修复共存

二十多个模块业务高度相似却各有差异,核心矛盾在于“统一与灵活”的平衡。通过角色接口组合(如IProjectAware、IVoiceAware)实现能力按需注入,新增功能零侵入;用模板方法锁定生命周期流程,确保顺序一致;结合@CallSuper与工厂方法,既约束关键逻辑,又保留子类自由。最终实现:骨架统一、填空自由,管得住业务,又不侵入业务。

mmsx990的博客 395

Android Message源码阅读

本文梳理了Android Handler机制中Message的核心设计:链表结构、消息复用池(sPool)及回收机制,重点解析了异步消息与同步屏障(SyncBarrier)的协同机制。当主线程需优先处理UI绘制等高优先级任务时,系统通过postSyncBarrier插入屏障,使Looper跳过普通同步消息,优先执行标记为异步的UI渲染任务,确保帧率流畅。该机制是保障Android界面响应性的关键技术之一。

u012116089的博客 186

Android中Gradle网络下载超时、更改存放位置(解决方案)

在国内开发Android应用,需要使用到一些外网才能正常访问的环境依赖。除了挂梯子,这里再提供几个常见问题的配置方案,可以已定程度减少下载超时问题。但是要注意,不同的开发环境、网络环境、时间段,出现的问题都是不可控的,遇到配置问题,建议保持耐心,多做尝试。

indeedes的博客 122

安卓部署Linux性能实测:一颗移动处理器,能否胜任日常 Linux 开发?

本文通过在一加Ace5上部署NimoteCode的ARM64 Linux环境,测试了其在CoreMark、SQLite编译及并行构建中的表现。结果显示,尽管单核性能约2.34GHz,多核扩展比达6.15×,但优化编译耗时仍高达35秒,反映移动处理器虽具备一定开发能力,却受限于文件系统开销与运行环境(如PRoot)。实际开发中,非优化构建效率较高,而并行编译可显著提速。结论:安卓手机可作为轻量级开发辅助工具,但无法替代桌面环境,真正价值在于便携性与即时响应,而非全功能替代。

一个不知名码农的专栏 283

云手机私有化部署完整方案:基于ARM服务器搭建私有安卓云集群

ARM 服务器是私有化安卓云集群最优硬件底座,原生架构匹配 Android,高密度、低损耗,非常适合企业内网场景。整套私有化方案的核心,从来不是 “跑起来安卓”,而是隔离可控、可审计、可运维、可扩容。两条路线各有取舍:开源自建灵活、门槛高;商用私有化交付开箱即用、安全能力完备,是当前企业生产环境主流方案。六边云手机私有化方案,补齐了自建方案在驱动适配、管控平台、审计合规、信创适配上的短板,适合对数据主权、内网隔离有强需求的政企、自研 App、垂直行业客户。

a598652的博客 308

CTF 命令执行题学习笔记:终端特殊字符 $IFS 与 %09 的用法(HelloCTF RCE-labs)

空格被过滤时:用%09(TAB)或$IFS代替空格关键词被过滤时:用通配符?或字符类[f]拼出被禁的词,让 WAF 在原始字符串里看不到它,shell 在执行时再展开这类技巧在后续的命令执行题里会反复用到,建议亲手在靶机上敲一遍感受一下。

YY888888_的博客 538

Android 17 全栈性能分析关键字与源码手册

FrameTimeline 找异常帧,ATrace 找阶段,thread_state 找等待类型,ftrace 找内核原因,Simpleperf 找 CPU 热点,驱动源码找最终触发条件;任何根因结论都必须回到同一对象、同一时间窗口和同一条依赖链。

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

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值