目录
- 后端数据接口
- 数据格式
- App客户端
- 布局逻辑
- 主界面布局
- M(Model)
- V(View)
- P(Presenter)
- Okhttp+Retrofit+RxJava网络http请求
- Mvp架构-初学者
- MVP架构的契约者
后端数据接口
接口地址:https://apis.tianapi.com/caipu/index
请求示例:https://apis.tianapi.com/caipu/index?key=你的APIKEY&word=黄瓜
支持协议:http/https
请求方式:get/post
返回格式:utf-8 json
数据格式
{
"code": 200,
"msg": "success",
"result": {
"curpage": 1,
"allnum": 5,
"newslist": [
{
"id": "90e761e707dd996c431992c8e2a0a88b",
"ctime": "2023-07-07 08:29",
"title": "为避免轨道碰撞 SpaceX星链卫星6个月内“让路”",
"description": "7月7日消息,在过去6个月里,为了避免轨道碰撞,埃隆·马斯克(ElonMusk)旗下SpaceX公司的星链卫星机动次数激增,这引发了人们对卫星长期可持续性运行的担忧,因为未来几年将有数万颗新卫星进入轨道。在最近向美国联邦通信委员会(FCC...[]",
"source": "网易科技",
"picUrl": "https://nimg.ws.126.net/?url=http%3A%2F%2Fcms-bucket.ws.126.net%2F2023%2F0707%2F0f703849p00rxeh90001cc0009c0070c.png&thumbnail=200y140&quality=100&type=jpg",
"url": "https://www.163.com/tech/article/I91GDFVG00097U81.html"
},
{
"id": "e8e49d6dd77be1a6ea82929a14abe6ef",
"ctime": "2023-07-07 06:32",
"title": "推特指责 Meta 挖角员工创建 Threads",
"description": "",
"source": "网易科技",
"picUrl": "https://nimg.ws.126.net/?url=http%3A%2F%2Fbjnewsrec-cv.ws.126.net%2Flittle730693cc23fj00rxebr3004xc000fw00lcg.jpg&thumbnail=200y140&quality=100&type=jpg",
"url": "https://www.163.com/dy/article/I919NRS10511B8LM.html"
},
{
"id": "55cf44c545e02e8d9fc30e8b07980d3d",
"ctime": "2023-07-06 21:08",
"title": "分析人士:Meta的Threads对马斯克的Twitter构成",
"description": "",
"source": "网易科技",
"picUrl": "https://nimg.ws.126.net/?url=http%3A%2F%2Fbjnewsrec-cv.ws.126.net%2Flittle32088c42c3cj00rxdlrp000xc000sg00hgg.jpg&thumbnail=200y140&quality=100&type=jpg",
"url": "https://www.163.com/dy/article/I909EBMH0511B8LM.html"
},
{
"id": "0bbc1f1f13121586804bb72e245448b2",
"ctime": "2023-07-06 20:26",
"title": "中国电信研究院副院长李安民:发展元宇宙,要提",
"description": "",
"source": "网易科技",
"picUrl": "https://nimg.ws.126.net/?url=http%3A%2F%2Fbjnewsrec-cv.ws.126.net%2Flittle64910ee1b6bj00rxctez005jd000j600anp.jpg&thumbnail=200y140&quality=100&type=jpg",
"url": "https://www.163.com/dy/article/I9071U0D0519DFFO.html"
},
{
"id": "1962b01db3569398109fe336d5f8ff29",
"ctime": "2023-07-06 20:42",
"title": "XR“失宠”?头显出货量下降超三成,元宇宙再降",
"description": "",
"source": "网易科技",
"picUrl": "https://nimg.ws.126.net/?url=http%3A%2F%2Fbjnewsrec-cv.ws.126.net%2Flittle252508775e3j00rxdev60080c000se00fsg.jpg&thumbnail=200y140&quality=100&type=jpg",
"url": "https://www.163.com/dy/article/I907V12305199NPP.html"
}
]
}
}
App客户端
MVP规范接口:IView、IModel、IPresenter,用于规范接口
package com.xzln.eatwhatjava.view;
public interface IView {} // 用于规范View层接口
package com.xzln.eatwhatjava.model;
public interface IModel {} // 用于规范Model层接口
package com.xzln.eatwhatjava.presenter;
import com.xzln.eatwhatjava.view.IView;
/**
* 抽离公共接口
* 所有的P层对象都公有
* 而V层和M层对象则都是根据具体的业务进行定制
* @param <V>
*/
public interface IPresenter<V extends IView> {
/**
* 依附生命view
* @param view v层对象
*/
void attachView(V view);
/**
* 分离View
*/
void detachView();
/**
* 判断View是否已经销毁
* @return 是否销毁
*/
boolean isViewAttached();
}
回调规范接口:ICallBack
package com.xzln.eatwhatjava.contact;
public interface ICallBack<T, K> {
void onSuccess(T data);
void onFail(K data);
}
布局逻辑
ViewPager2+Fragment
其中Fragment由refreshLayout+RecycleView组成,RecycleView放新闻条目。
ViewPager2用于对新闻进行分类
主界面布局
// activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".view.MainActivity"
android:orientation="vertical">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="40dp"
android:background="@color/colorPrimary"
app:tabGravity="fill"
app:tabMode="fixed"
app:tabTextColor="@android:color/white" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager2"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
M(Model)
// INewsModel.java
package com.xzln.eatwhatjava.model;
import com.xzln.eatwhatjava.contact.ICallBack;
/**
* 定义规范接口:(新闻)
* 单一接口的M层
*/
public interface INewsModel extends IModel {
// post请求
void getData(int page, int num, final ICallBack callback);
}
// NewsModel.java
package com.xzln.eatwhatjava.model;
import android.util.Log;
import com.xzln.eatwhatjava.api.NewsApi;
import com.xzln.eatwhatjava.bean.NewsListBean;
import com.xzln.eatwhatjava.bean.Result;
import com.xzln.eatwhatjava.config.NewsConfig;
import com.xzln.eatwhatjava.contact.ICallBack;
import com.xzln.eatwhatjava.utils.RetrofitUtils;
import io.reactivex.Observable;
import io.reactivex.Observer;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
/**
* 定义了News功能模块的M层类
* 调用retrofit网络API,通过CallBack将数据传到P层
* TODO: onNext中发生错误的情况 和 onError中发生错误的情况 都有哪些
*/
public class NewsModel implements INewsModel {
public static final String TAG = "com.xzln.eatwhatjava.model.NewsModel";
@Override
public void getData(int page, int num, ICallBack callback) {
fetchKejiNews(page, num)
.observeOn(AndroidSchedulers.mainThread()) // Android 主线程观察
.subscribeOn(Schedulers.io()) // 消费
.subscribe(new Observer<Result<NewsListBean>>() { // URL访问成功
@Override
public void onSubscribe(Disposable d) {}
@Override
public void onNext(Result<NewsListBean> newsListBeanResult) { // 数据迭代器?
if (null == newsListBeanResult) { callback.onFail("出现错误: null == newsListBeanResult"); }
else if (newsListBeanResult.getCode() != 200) { callback.onFail(newsListBeanResult.getMsg());}
else { callback.onSuccess(newsListBeanResult); }
}
@Override
public void onError(Throwable e) { // 访问错误或者数据解析错误
e.printStackTrace();
callback.onFail("出现错误: onError");
}
@Override
public void onComplete() { Log.d(TAG, "onComplete"); }
});
}
protected Observable<Result<NewsListBean>> fetchKejiNews(int page, int num) {
return RetrofitUtils.getRetrofit().create(NewsApi.class).fetchKejiNews(NewsConfig.getKejiNewsFieldMap(page, num));
}
}
V(View)
// INewsView.java
package com.xzln.eatwhatjava.view;
import com.xzln.eatwhatjava.presenter.IPresenter;
/**
* 定义News功能模块的View类的规范接口
* (P层和V层的桥梁)
* @param <P> P层对象类型
* @param <T> P层访问成功时返回的类型
* @param <V> P层访问失败时返回的类型
*/
public interface INewsView<P extends IPresenter, T, V> extends IView{
void showNewsSuccess(T newsBeans);
void showNewsFail(V data);
}
package com.xzln.eatwhatjava.base;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.xzln.eatwhatjava.presenter.IPresenter;
import com.xzln.eatwhatjava.view.IView;
/**
* 抽象类,Fragment的公共抽象类
* @param <P>
*/
public abstract class BaseFragment<P extends IPresenter> extends Fragment implements IView {
protected P mPresenter; // P层对象,用于和V层、M层进行联系
protected View mView; // V层对象,通过V层对象完成P和V之间的联系
/**
* 完成P层对象的创建和初始化
* @param savedInstanceState If the fragment is being re-created from
* a previous saved state, this is the state.
*/
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initPresenter();
init();
}
/**
*
* @param inflater The LayoutInflater object that can be used to inflate
* any views in the fragment,
* @param container If non-null, this is the parent view that the fragment's
* UI should be attached to. The fragment should not add the view itself,
* but this can be used to generate the LayoutParams of the view.
* @param savedInstanceState If non-null, this fragment is being re-constructed
* from a previous saved state as given here.
*
* @return 视图对象
*/
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
mView = inflater.inflate(getLayoutId(), container, false);
return mView;
}
/**
* Fragment销毁时,先取消P层对象和Fragment的绑定,避免空指针异常
*/
@Override
public void onDestroy() {
if (mPresenter != null && mPresenter.isViewAttached()) {
mPresenter.detachView();
}
super.onDestroy();
}
/**
* 创建和初始化P层对象,将P层对象生命周期与当前Fragment绑定
*/
protected void initPresenter() {
mPresenter = createPresenter();
//绑定生命周期
if (mPresenter != null) {
mPresenter.attachView(this);
}
}
/**
* 抽象方法,由子类根据对应的布局来实现,返回其布局ID,用于Fragment的OnCreateView
* @return 布局ID
*/
public abstract int getLayoutId();
/**
* 创建一个Presenter
*
* @return P层对象
*/
protected abstract P createPresenter();
/**
* 用于子类的其他数据的初始化
*/
protected abstract void init();
}
//NewsFragment.java
package com.xzln.eatwhatjava.view;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.xzln.eatwhatjava.R;
import com.xzln.eatwhatjava.adapter.NewsRecycleViewAdapter;
import com.xzln.eatwhatjava.base.BaseFragment;
import com.xzln.eatwhatjava.bean.NewsBean;
import com.xzln.eatwhatjava.presenter.NewsPresenter;
import java.util.LinkedList;
import java.util.List;
/**
* News的布局碎片
*/
public class NewsFragment extends BaseFragment<NewsPresenter> implements INewsView<NewsPresenter, List<NewsBean>, String> {
private static final String TAG = "com.xzln.eatwhatjava.view.NewsFragment";
protected int mNewsType; // News的类别,由创建者传入
protected SwipeRefreshLayout mSwipeRefreshLayout; // 顶部的刷新布局
protected RecyclerView mRecycleView;
protected List<NewsBean> mNewsBeans; // 数据列表
protected int mCurPage = 0; // 当前页码
protected int mNumOfPage = 25; // 每页中数据数目
protected NewsRecycleViewAdapter mNewsRecycleViewAdapter; // RecycleView适配器
protected LinearLayoutManager mLinearLayoutManager;
private int mTotalItemCount;/*recycleView下的总item数目*/
private int mFirstVisibleItem;/*当前可见区内最后一个item的position*/
private int mLastVisibleItem;/*当前可见区内最后一个item的position*/
/*Return the current number of child views attached to the parent RecyclerView.*/
private int mVisibleItemCount;/*当前recycleView下可见的item数*/
public NewsFragment(int newsType) { mNewsType = newsType; }
/**
* 刷新时调用:页码清零、数据清空、重新获取数据
*/
protected void refreshView() {
mCurPage = 0;
mNewsBeans.clear();
mNewsRecycleViewAdapter.clearData();
mPresenter.getNewsData(mCurPage++, mNumOfPage);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return super.onCreateView(inflater, container, savedInstanceState);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mSwipeRefreshLayout = mView.findViewById(R.id.layout_swipe_refresh);
mRecycleView = mView.findViewById(R.id.view_recycle);
// mPresenter.getNewsData(mCurPage++, mNumOfPage);
refreshView();
setupListener();
}
/**
* 用于创建P层对象,由子类具体定制实现
* @return P层对象
*/
@Override
protected NewsPresenter<NewsFragment> createPresenter() { return new NewsPresenter<>(this); }
/**
* 由子类定制,用于完成子类中的数据初始化
* 视图id的绑定不可在这完成。因为视图View对象的生成在OnCreateView,而init函数在OnCreate中
*/
@Override
protected void init() {
mNewsBeans = new LinkedList<>();
mNewsRecycleViewAdapter = new NewsRecycleViewAdapter(getActivity(), mNewsBeans);
}
/**
* @return 视图布局文件的ID
*/
@Override
public int getLayoutId() { return R.layout.activity_fragment_main; }
/**
* 启动监听器
* 1. 刷新头
* 2. RecycleView适配器、布局管理器
*/
private void setupListener() {
mSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
Log.d(TAG, "mSwipeRefreshLayout.setOnRefreshListener.onRefresh");
// mSwipeRefreshLayout.setRefreshing(false);
refreshView();
new Handler().postDelayed(new Runnable() {
@Override
public void run() { mSwipeRefreshLayout.setRefreshing(false); }
},2000);
}
});
mLinearLayoutManager = new LinearLayoutManager(getActivity());
mRecycleView.setLayoutManager(mLinearLayoutManager);
mRecycleView.setAdapter(mNewsRecycleViewAdapter);
/*recycleView*/
mRecycleView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
Log.d(TAG, "onScrollStateChanged --> newState" + newState);
// 获取数据逻辑
// 滚动状态改变 && 最后一个position + 1 >= total
if (newState == RecyclerView.SCROLL_STATE_DRAGGING &&
mLastVisibleItem + 1 >= mTotalItemCount &&
mVisibleItemCount <= mTotalItemCount) {
mPresenter.getNewsData(mCurPage++, mNumOfPage);
}
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
mLinearLayoutManager.findFirstVisibleItemPosition();
mTotalItemCount = mLinearLayoutManager.getItemCount();
mFirstVisibleItem = mLinearLayoutManager.findFirstVisibleItemPosition();
mLastVisibleItem = mLinearLayoutManager.findLastVisibleItemPosition();/*当前可见区内最后一个item的position*/
/*Return the current number of child views attached to the parent RecyclerView.*/
mVisibleItemCount = mLinearLayoutManager.getChildCount();/*当前recycleView下可见的item数*/
}
});
}
/**
* 与P层联系的函数
* @param newsBeans P层成功返回的数据
*/
@Override
public void showNewsSuccess(List<NewsBean> newsBeans) {
Log.d(TAG, "showNewsSuccess newsBeans.size()=" + newsBeans.size());
mNewsRecycleViewAdapter.addData(newsBeans);
}
/**
* 与P层联系的函数
* @param data P层失败返回的数据
*/
@Override
public void showNewsFail(String data) {
Log.d(TAG, "showNewsSuccess");
}
}
P(Presenter)
// IPresenter.java
package com.xzln.eatwhatjava.presenter;
import com.xzln.eatwhatjava.view.IView;
/**
* 抽离公共接口
* 所有的P层对象都公有
* @param <V>
*/
public interface IPresenter<V extends IView> {
/**
* 依附生命view
* @param view v层对象
*/
void attachView(V view);
/**
* 分离View
*/
void detachView();
/**
* 判断View是否已经销毁
* @return 是否销毁
*/
boolean isViewAttached();
}
// BasePresenter.java
package com.xzln.eatwhatjava.base;
import com.xzln.eatwhatjava.presenter.IPresenter;
import com.xzln.eatwhatjava.view.IView;
/**
* IPresenter接口需要所有的P层实现类继承
* 对于生命周期这部分功能P层都是通用的,因此可以抽出来一个抽象基类BasePresenter,去实现IPresenter的接口.
* @param <V>
*/
public abstract class BasePresenter<V extends IView> implements IPresenter<V> {
protected V mView;
@Override
public void attachView(V view) { mView = view; }
@Override
public void detachView() { mView = null; }
@Override
public boolean isViewAttached() { return mView != null; }
}
P层对象中持有V层对象和M层对象
P层调用M层获取数据,并设置CallBack。通过CallBack完成M层和P层间的联系。
在CallBack中调用View对象,完成P层与V层的联系。
// NewsPresenter.java
package com.xzln.eatwhatjava.presenter;
import android.util.Log;
import com.xzln.eatwhatjava.base.BasePresenter;
import com.xzln.eatwhatjava.bean.NewsListBean;
import com.xzln.eatwhatjava.bean.Result;
import com.xzln.eatwhatjava.contact.ICallBack;
import com.xzln.eatwhatjava.model.INewsModel;
import com.xzln.eatwhatjava.model.NewsModel;
import com.xzln.eatwhatjava.view.INewsView;
import com.xzln.eatwhatjava.view.IView;
/**
* News功能模块P层逻辑
* @param <V>
*/
public class NewsPresenter<V extends IView> extends BasePresenter<V> {
private static final String TAG = "com.xzln.eatwhatjava.presenter.NewsPresenter";
protected INewsView mNewsView;
protected INewsModel mNewsModel;
public NewsPresenter(V view) {
mNewsView = (INewsView) view;
createModel();
}
private void createModel() {
if (mNewsModel == null) { mNewsModel = new NewsModel(); }
}
public void getNewsData(int page, int num) {
mNewsModel.getData(page, num,
new ICallBack<Result<NewsListBean>, String>() { // P层定义CallBack,从而将数据从
@Override
public void onSuccess(Result<NewsListBean> data) {
Log.d(TAG, ".getData.onSuccess");
mNewsView.showNewsSuccess(data.getResult().getNewslist());
}
@Override
public void onFail(String data) {
mNewsView.showNewsFail("数据获取失败");
Log.d(TAG, ".getData.onFail --> " + data);
}
});
}
}
// ICallBack.java
package com.xzln.eatwhatjava.contact;
public interface ICallBack<T, K> {
void onSuccess(T data);
void onFail(K data);
}
Okhttp+Retrofit+RxJava网络http请求
RetrofitUtil
//RetrofitUtils.java
package com.xzln.eatwhatjava.utils;
import com.xzln.eatwhatjava.config.NewsConfig;
import java.util.concurrent.TimeUnit;
import okhttp3.OkHttpClient;
import retrofit2.Retrofit;
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
import retrofit2.converter.gson.GsonConverterFactory;
public class RetrofitUtils {
private static Retrofit retrofit;
private static OkHttpClient okhttp;
/**
* 自定义okhttp客户端
*
* @return okhttp客户端
*/
public static OkHttpClient getOkhttp() {
if (okhttp == null) {
synchronized (RetrofitUtils.class) {
if (okhttp == null) {
okhttp = new OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
.build();
}
}
}
return okhttp;
}
/**
* 单例retrofit客户端
*
* @return retrofit客户端
*/
public static Retrofit getRetrofit() {
if (retrofit == null) {
synchronized (RetrofitUtils.class) {
if (retrofit == null) {
retrofit = new Retrofit.Builder()
.baseUrl(NewsConfig.newsBaseUrl)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.client(getOkhttp())
.build();
}
}
}
return retrofit;
}
}
//NewsApi.java
package com.xzln.eatwhatjava.api;
import com.xzln.eatwhatjava.bean.NewsListBean;
import com.xzln.eatwhatjava.bean.Result;
import java.util.Map;
import io.reactivex.Observable;
import retrofit2.http.FieldMap;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.POST;
public interface NewsApi {
// POST请求数据
@FormUrlEncoded
@POST("keji/index")
Observable<Result<NewsListBean>> fetchKejiNews(@FieldMap Map<String, Object> map);
// Observable<Result<List<NewsBean>>> fetchKejiNews(@FieldMap Map<String, Object> map);
}
Mvp架构-初学者
MVP架构是为了让各个模块之间降低耦合,方便维护,也可以让代码更简洁,让代码简洁的意思是让代码更清晰,并不是让代码更少;MVP契约者是为了进一步的低耦合、接口统一管理。
MVP对接口灵活的调用可以轻松的应对产品的变更。
presenter
类与View 和 Model 通信,做到视图和逻辑的解耦。
MVP架构的契约者
TODO