html手机网站如何制作wordpress论坛模版
html手机网站如何制作,wordpress论坛模版,淘宝店铺去哪里买,买个网站域名要多少钱前言 本章我们基于重构的方式进行一个 MVVM 的实战#xff0c;我们将一个新闻列表的普通实现#xff0c;一步一步的改造成 MVVM 的架构模式#xff0c;一共分为上中下三个章节#xff0c;本章继续上一章#xff0c;开始中篇的讲解#xff1b;
控件化 我们本章向控件化进…前言 本章我们基于重构的方式进行一个 MVVM 的实战我们将一个新闻列表的普通实现一步一步的改造成 MVVM 的架构模式一共分为上中下三个章节本章继续上一章开始中篇的讲解
控件化 我们本章向控件化进一步迈进
BaseView 重构
我们上一章将 TitleView 和 PictureTitleView 抽取了一个 BaseView 来抽取公共的 setData 逻辑我们还可以继续精进一步
我们现将我们上一章定义的接口 BaseView 重命名成 IBaseView
public interface IBaseViewDATA extends BaseViewModel {void setData(DATA data);
}然后我们定义一个 IBaseView 的实现类BaseView
public abstract class BaseView extends LinearLayout implements IBaseViewBaseViewModel {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 BaseViewVIEW_BINDING extends ViewDataBinding extends LinearLayout implements IBaseViewBaseViewModel {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 BaseViewVIEW_BINDING extends ViewDataBinding, DATA extends BaseViewModel extends LinearLayout implements IBaseViewDATA {}然后我们发现当我们在 setData 中去 setViewModel 的时候报错了并没有 setViewModel 的接口那么我们就需要进行剥离出来交给子类实现 public abstract class BaseViewVIEW_BINDING extends ViewDataBinding, DATA extends BaseViewModel extends LinearLayout implements IBaseViewDATA {Overridepublic void setData(DATA baseViewModel) {setDataToView(baseViewModel);mBinding.executePendingBindings();}public abstract void setDataToView(DATA data);
}我们接下来重构 TitleView 和 PictureTitleView来让它们继承 BaseView
public class TitleView extends BaseViewTitleViewBinding, 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 BaseViewPictureTitleViewBinding, 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 IBaseModelListenerDATA {void onLoadSuccess(DATA data);void onLoadFail(int errorCode, String errorMsg);
}然后我们定义一个我们用来获取频道列表的 model
public class NewsChannelModel {private IBaseModelListenerListNewsChannelsBean.ChannelList mListener;public NewsChannelModel(IBaseModelListenerListNewsChannelsBean.ChannelList mListener) {this.mListener mListener;}public void load() {TecentNetworkApi.getService(NewsApiInterface.class).getNewsChannels().compose(TecentNetworkApi.getInstance().applySchedulers(new BaseObserverNewsChannelsBean() {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 IBaseModelListenerListNewsChannelsBean.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(ListNewsChannelsBean.ChannelList channelLists) {if (adapter ! null) {adapter.setChannels(channelLists);}}Overridepublic void onLoadFail(int errorCode, String errorMsg) {}
}Fragment 也清爽了很多我们接下来重构下 NewsListFragment重构逻辑一样我们封装一个 NewListModel
public class NewsListModel {private IBaseModelListenerArrayListBaseViewModel mListener;private int mPageNum;private String mChannelId;private String mChannelName;public NewsListModel(IBaseModelListenerArrayListBaseViewModel 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 FunctionNewsListBean, ObservableSourceArrayListBaseViewModel() {Overridepublic ObservableSourceArrayListBaseViewModel apply(NewsListBean newsChannelsBean) throws Exception {ArrayListBaseViewModel 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 ConsumerArrayListBaseViewModel() {Overridepublic void accept(ArrayListBaseViewModel baseViewModels) throws Exception {mPageNum;mListener.onLoadSuccess(baseViewModels);}});}public void refresh() {mPageNum 1;load();}
}然后 NewListFragment 重构如下
public class NewsListFragment extends Fragment implements IBaseModelListenerArrayListBaseViewModel {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(ArrayListBaseViewModel 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 IBaseModelListenerDATA {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(ArrayListBaseViewModel 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 WeakReferenceIBaseModelListener 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 FunctionNewsListBean, ObservableSourceArrayListBaseViewModel() {Overridepublic ObservableSourceArrayListBaseViewModel apply(NewsListBean newsChannelsBean) throws Exception {ArrayListBaseViewModel 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 ConsumerArrayListBaseViewModel() {Overridepublic void accept(ArrayListBaseViewModel 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 BaseObserverNewsChannelsBean() {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 WeakReferenceIBaseModelListener 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 BaseMvvmModelRESULT_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 ConsumerArrayListBaseViewModel() {Overridepublic void accept(ArrayListBaseViewModel baseViewModels) throws Exception {notifyResultToListener(baseViewModels);}
});NewsChannelModel
.compose(TecentNetworkApi.getInstance().applySchedulers(new BaseObserverNewsChannelsBean() {Overridepublic void onSuccess(NewsChannelsBean newsChannelsBean) {notifyResultToListener(newsChannelsBean.showapiResBody.channelList);}Overridepublic void onFailure(Throwable e) {e.printStackTrace();loadFail(404, e.getMessage());}
}));数据缓存
如果我们在无网的情况下打开 app通常会展示一片空白体验很不好所以就需要我们提前将数据缓存下来当用户无网打开的时候不至于显示一片白
首先我们来提供一个 CacheData
public class BaseCachedDataDATA {public long updateTimeInMills;public DATA data;
}然后我们需要对 BaseMvvmModel 进行重构增加缓存逻辑
public abstract class BaseMvvmModelNETWORK_DATA, RESULT_DATA {private String mCachedPreferenceKey;private BaseCachedDataNETWORK_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) {BaseCachedDataNETWORK_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 BaseMvvmModelNewsChannelsBean, ListNewsChannelsBean.ChannelList {public NewsChannelModel() {super(false, NEWS_CHANNEL_PREF_KEY);}Overridepublic void load() {TecentNetworkApi.getService(NewsApiInterface.class).getNewsChannels().compose(TecentNetworkApi.getInstance().applySchedulers(new BaseObserverNewsChannelsBean() {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 BaseMvvmModelNewsListBean, ArrayListBaseViewModel {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 FunctionNewsListBean, ObservableSourceArrayListBaseViewModel() {Overridepublic ObservableSourceArrayListBaseViewModel apply(NewsListBean newsChannelsBean) throws Exception {ArrayListBaseViewModel 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 ConsumerArrayListBaseViewModel() {Overridepublic void accept(ArrayListBaseViewModel baseViewModels) throws Exception {notifyResultToListener(NewsListModel.this.mNewsListBean, baseViewModels);}});}
}OK运行可以看到数据已经存到了 SP 中
好了中篇文章就讲解到这里吧
下一章预告 MVVM 实战一个新闻客户端 (下)
欢迎三连 来都来了点个关注点个赞吧你的支持是我前进的最大动力
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/88007.shtml
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!