一、简介
VIPER
是视图 (View),交互器 (Interactor),主持人 (Presenter),实体 (Entity) 以及路由 (Routing) 的首字母缩写,各个部分职责明确,遵循单一职责原则,在iOS开发中应用较早。
VIPER-clean
是由VIPER
改进而来的方案,是基于The Clean Architecture
的思想,可以参考和阅读Uncle Bob
的《整洁架构之道》。随着项目需求的增加,可以使用更多的usecase(用例)
来处理逻辑,不会显著增加单个类的代码量,使得代码看起来整洁,易于维护。
二、经验
如果一个页面,承载了太多的逻辑,比如: 音视频页面、派对房等,属于功能超多的页面,可以使用这个架构。如果是简单的页面,比如: 登录、注册或者列表展示的页面,使用常规的mvc架构,就足够了,代码也不会乱到哪里去。对于小团队来说,可以灵活点。就像Uncle Bob说的,“不能嫁给框架”。
三、基类
- BaseView
public interface BaseView<T extends BasePresenter> {
void setPresenter(T presenter);
}
- BaseInteractor
public interface BaseInteractor {
}
- BasePresenter
public interface BasePresenter {
}
- BaseRouter
public interface BaseRouter {
}
- ThreadUtils
public class ThreadUtils {
public static void runOnUI(Runnable runnable){
if (runnable==null){
return;
}
if (Looper.myLooper()!=Looper.getMainLooper()){
new Handler(Looper.getMainLooper()).post(runnable);
}else{
runnable.run();
}
}
}
- UICallBackWrapper
public class UICallBackWrapper<P> implements BaseUseCase.UseCaseCallback<P> {
private final BaseUseCase.UseCaseCallback<P> mCallback;
public UICallBackWrapper(BaseUseCase.UseCaseCallback<P> mCallback) {
this.mCallback = mCallback;
}
@Override
public void onSuccess(final P response) {
ThreadUtils.runOnUI(new Runnable() {
@Override
public void run() {
mCallback.onSuccess(response);
}
});
}
@Override
public void onError(final String error) {
ThreadUtils.runOnUI(new Runnable() {
@Override
public void run() {
mCallback.onError(error);
}
});
}
}
- BaseUseCase
public abstract class BaseUseCase<Q,P> {
private Q mRequestValues;
private UseCaseCallback<P> mUseCaseCallback;
/**
* 若包含多个请求数据,则需要封装传入
* @param requestValues
* @return
*/
@CallSuper
public BaseUseCase<Q,P> setRequestValues(Q requestValues){
mRequestValues=requestValues;
return this;
}
public Q getRequestValues(){
return mRequestValues;
}
protected UseCaseCallback<P> getUseCaseCallback(){
return mUseCaseCallback;
}
/**
* 数据处理结束后的回调
* @param useCaseCallback
* @return
*/
public BaseUseCase<Q,P> setUseCaseCallback(UseCaseCallback<P> useCaseCallback){
mUseCaseCallback=new UICallBackWrapper<>(useCaseCallback);
return this;
}
/**
* 可在任意线程执行,回调时返回主线程
*/
public void run(){
executeUseCase(mRequestValues);
}
/**
* 处理数据
* @param requestValues
*/
protected abstract void executeUseCase(Q requestValues);
public interface RequestValues{}
public interface ResponseValues{}
public interface UseCaseCallback<P>{
void onSuccess(P response);
void onError(String error);
}
}
四、练习
做一个简单的项目,左边是页面功能,右边是项目代码结构,如下图:
- 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"
android:orientation="vertical"
tools:context=".MainActivity">
<LinearLayout
android:id="@+id/ll_demand_1"
android:layout_margin="20dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20dp"
android:textColor="@android:color/black"
android:text="需求1: 1 + 1 = "/>
<TextView
android:id="@+id/tv_result_1"
android:layout_weight="1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:textSize="20dp"
android:textColor="@android:color/black"
android:text="__"/>
<Button
android:id="@+id/btn_1"
android:onClick="clickBtn1"
android:layout_marginLeft="30dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="开始计算"/>
</LinearLayout>
<LinearLayout
android:id="@+id/ll_demand_2"
android:layout_margin="20dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20dp"
android:textColor="@android:color/black"
android:text="需求2: 3 * 3 = "/>
<TextView
android:id="@+id/tv_result_2"
android:layout_weight="1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:textSize="20dp"
android:textColor="@android:color/black"
android:text="__"/>
<Button
android:id="@+id/btn_2"
android:onClick="clickBtn2"
android:layout_marginLeft="30dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="开始计算"/>
</LinearLayout>
<LinearLayout
android:layout_margin="20dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20dp"
android:textColor="@android:color/black"
android:text="需求3:100内 = "/>
<TextView
android:id="@+id/tv_result_3"
android:layout_weight="1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:textSize="20dp"
android:textColor="@android:color/black"
android:text="__"/>
<Button
android:id="@+id/btn_3"
android:onClick="clickBtn3"
android:layout_marginLeft="30dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="开始随机"/>
</LinearLayout>
<Button
android:id="@+id/btn_detail"
android:onClick="clickBtnDetail"
android:layout_marginLeft="30dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="跳转到详情"/>
</LinearLayout>
- MainActivity
public class MainActivity extends AppCompatActivity implements MContact.View {
private TextView tvResult1;
private TextView tvResult2;
private TextView tvResult3;
private MContact.Presenter mPresenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
setPresenter(new MPresenter(new MInteractor(),this,new MRouter()));
}
private void initView(){
tvResult1 =findViewById(R.id.tv_result_1);
tvResult2 =findViewById(R.id.tv_result_2);
tvResult3=findViewById(R.id.tv_result_3);
}
public void clickBtn1(View v){
mPresenter.calculateFirst();
}
public void clickBtn2(View v){
mPresenter.calculateSecond();
}
public void clickBtn3(View v){
mPresenter.exeRandom();
}
public void clickBtnDetail(View v){
mPresenter.openDetail();
}
@Override
public Context getContext() {
return this;
}
@Override
public void showToast(String msg) {
Toast.makeText(this,msg,Toast.LENGTH_SHORT).show();
}
@Override
public void setFirstView(int result) {
tvResult1.setText(String.valueOf(result));
}
@Override
public void setSecondView(int result) {
tvResult2.setText(String.valueOf(result));
}
@Override
public void setThirdView(int result) {
tvResult3.setText(String.valueOf(result));
}
@Override
public void setPresenter(MContact.Presenter presenter) {
this.mPresenter=presenter;
}
}
- MContact
public class MContact {
interface Presenter extends BasePresenter{
/** 计算第一个*/
void calculateFirst();
/**计算第二个*/
void calculateSecond();
/**随机*/
void exeRandom();
/** 跳转到详情*/
void openDetail();
}
interface View extends BaseView<Presenter>{
Context getContext();
void showToast(String msg);
void setFirstView(int result);
void setSecondView(int result);
void setThirdView(int result);
}
interface Interactor extends BaseInteractor{
FirstUseCase getFirstUseCase();
SecondUseCase getSecondUseCase();
ThirdUseCase getThirdUseCase();
}
interface Router extends BaseRouter{
void openDetailActivity(Context context);
}
}
- MInteractor
public class MInteractor implements MContact.Interactor {
private FirstUseCase firstUseCase;
private SecondUseCase secondUseCase;
private ThirdUseCase thirdUseCase;
@Override
public FirstUseCase getFirstUseCase() {
if (firstUseCase==null){
synchronized(this){
if (firstUseCase==null){
firstUseCase=new FirstUseCase();
}
}
}
return firstUseCase;
}
@Override
public SecondUseCase getSecondUseCase() {
if (secondUseCase==null){
synchronized(this){
if (secondUseCase==null){
secondUseCase=new SecondUseCase();
}
}
}
return secondUseCase;
}
@Override
public ThirdUseCase getThirdUseCase() {
if (thirdUseCase==null){
synchronized(this){
if (thirdUseCase==null){
thirdUseCase=new ThirdUseCase();
}
}
}
return thirdUseCase;
}
}
- MPresenter
public class MPresenter implements MContact.Presenter {
private MContact.Interactor mInteractor;
private MContact.View mView;
private MContact.Router mRouter;
public MPresenter(MContact.Interactor mInteractor, MContact.View mView, MContact.Router mRouter) {
this.mInteractor = mInteractor;
this.mView = mView;
this.mRouter = mRouter;
}
@Override
public void calculateFirst() {
mInteractor.getFirstUseCase().setRequestValues(new FirstUseCase.RequestValues(1,1))
.setUseCaseCallback(new BaseUseCase.UseCaseCallback<FirstUseCase.ResponseValues>() {
@Override
public void onSuccess(FirstUseCase.ResponseValues response) {
//显示到屏幕上
mView.setFirstView(response.getResult());
}
@Override
public void onError(String error) {
mView.showToast(error);
}
}).run();
}
@Override
public void calculateSecond() {
mView.showToast("延时计算...");
mInteractor.getSecondUseCase().setRequestValues(new SecondUseCase.RequestValues(3,3))
.setUseCaseCallback(new BaseUseCase.UseCaseCallback<SecondUseCase.ResponseValues>() {
@Override
public void onSuccess(SecondUseCase.ResponseValues response) {
mView.setSecondView(response.getResult());
}
@Override
public void onError(String error) {
mView.showToast(error);
}
}).run();
}
@Override
public void exeRandom() {
mInteractor.getThirdUseCase().setRequestValues(new ThirdUseCase.RequestValues(100))
.setCustomCallBack(new ThirdUseCase.CustomCallBack() {
@Override
public void ok(ThirdUseCase.ResponseValues result) {
mView.setThirdView(result.getResult());
}
@Override
public void fail(String err) {
mView.showToast(err);
}
}).run();
}
@Override
public void openDetail() {
mRouter.openDetailActivity(mView.getContext());
}
}
- MRouter
public class MRouter implements MContact.Router {
@Override
public void openDetailActivity(Context context) {
Intent intent=new Intent(context,DetailActivity.class);
context.startActivity(intent);
}
}
- FirstUseCase
public class FirstUseCase extends BaseUseCase<FirstUseCase.RequestValues, FirstUseCase.ResponseValues> {
@Override
protected void executeUseCase(RequestValues requestValues) {
//假设,不能大于1000,大的话报错
int maxError=1000;
if (requestValues.num1>maxError||requestValues.num2>maxError){
getUseCaseCallback().onError("不能大于1000");
}else{
int result=requestValues.num1+requestValues.num2;
getUseCaseCallback().onSuccess(new ResponseValues(result));
}
}
public static class RequestValues implements BaseUseCase.RequestValues{
private int num1;
private int num2;
public RequestValues(int num1, int num2) {
this.num1 = num1;
this.num2 = num2;
}
}
public static class ResponseValues implements BaseUseCase.ResponseValues{
private int result;
public ResponseValues(int result) {
this.result = result;
}
public int getResult() {
return result;
}
}
}
- SecondUseCase
public class SecondUseCase extends BaseUseCase<SecondUseCase.RequestValues, SecondUseCase.ResponseValues> {
@Override
protected void executeUseCase(RequestValues requestValues) {
//假设,不能大于1000,大的话报错
int maxError=1000;
if (requestValues.num1>maxError||requestValues.num2>maxError){
getUseCaseCallback().onError("不能大于1000");
}else{
//假装耗时操作
new Thread(new Runnable() {
@Override
public void run() {
//等待2秒
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//开始计算
int result=requestValues.num1*requestValues.num2;
//这里会自动把线程切换到主线程,请看父类中的定义UICallBackWrapper
getUseCaseCallback().onSuccess(new ResponseValues(result));
}
}).start();
}
}
public static class RequestValues implements BaseUseCase.RequestValues{
private int num1;
private int num2;
public RequestValues(int num1, int num2) {
this.num1 = num1;
this.num2 = num2;
}
}
public static class ResponseValues implements BaseUseCase.ResponseValues{
private int result;
public ResponseValues(int result) {
this.result = result;
}
public int getResult() {
return result;
}
}
}
- ThirdUseCase
/**
* 如果BaseUseCase的回调方法满足不了需求,这里可以重新自定义回调方法
*/
public class ThirdUseCase extends BaseUseCase<ThirdUseCase.RequestValues, ThirdUseCase.ResponseValues> {
@Override
protected void executeUseCase(RequestValues requestValues) {
//假设,不能大于1000,大的话报错
int maxError=1000;
if (requestValues.num1>maxError){
//自定义回调
getCustomCallBack().fail("不能大于1000");
}else{
int a = (int) (Math.random() * requestValues.num1);
//自定义回调
getCustomCallBack().ok(new ResponseValues(a));
}
}
public static class RequestValues implements BaseUseCase.RequestValues{
private int num1;
public RequestValues(int num1) {
this.num1 = num1;
}
}
public static class ResponseValues implements BaseUseCase.ResponseValues{
private int result;
public ResponseValues(int result) {
this.result = result;
}
public int getResult() {
return result;
}
}
@Override
public ThirdUseCase setRequestValues(RequestValues requestValues) {
super.setRequestValues(requestValues);
return this;
}
/** 自定义回调方法*/
public interface CustomCallBack{
void ok(ResponseValues result);
void fail(String err);
}
private CustomCallBack customCallBack;
public ThirdUseCase setCustomCallBack(CustomCallBack callBack){
this.customCallBack=callBack;
return this;
}
private CustomCallBack getCustomCallBack(){
return customCallBack;
}
}
- 这里省去了DetailActivity,自己可以随便写一个Activity组件。
五、实践
每个页面有一个契约类*Contract
,主要用来定义接口。优点:逻辑会比较清晰,便于维护。
- 以下是老项目的视频通话页面的契约类,仅供参考,如有不足之处,还请指教:
public class NvcContract {
interface Presenter extends BasePresenter{
/** 获取初始化数据*/
void getVideoInitData();
/** 权限获取成功*/
void getPermissionSuccess();
/** 调取聊天流程接口-调取完成了才能显示页面*/
void startChatApi();
/** 播放铃声*/
void playRing();
/** 停止播放铃声*/
void stopRing();
/** 接受邀请信令*/
void acceptSignalInComing();
/** 拒绝邀请信令*/
void rejectSignalInComing();
/** 取消信令*/
void cancelSignalOutGoing();
/** 我是主叫,对方接听电话*/
void otherAcceptOutGoing();
/** 我是主叫,对方拒绝电话*/
void otherRejectOutGoing();
/** 我是被叫,对方取消了邀请信令*/
void otherCancelInComing();
/** 信令超时未处理*/
void timeOutHandleSignal();
/** 调取handleChat接口*/
void handleChatApi(int handleType,String remark);
/** 挂断电话-还没有开始聊天就异常*/
void hungUpBeforeEnterRoom(String error);
/** 初始化房间*/
void initRtcRoom();
/** 正常进入房间*/
void enterRtcRoom();
/** 加入房间的人是否是对方*/
void checkCorrectSession(int uid);
/** 开始计时*/
void startVideoTimeCount();
/** 计算通话费用*/
void calculateAvConsumption(int minuteDuration);
/** 本机有来电*/
void phoneSelfBusy();
/** 销毁房间*/
void destroyRoom();
/** 大小屏幕切换*/
void switchSmallAndBigVideo();
/** 获取大图视频流*/
SurfaceView getRemoteSurfaceView();
/** 设置默认的视图-从后台恢复页面*/
void recoverActivityVideoView();
/** 在房间内挂断电话*/
void clickHungUpInRoom();
/** 切换前后摄像头*/
void cameraSwitch();
/** 切换喇叭-听筒和喇叭切换*/
void speakerSwitch();
/** 展示礼物*/
void showGiftPop();
/** 展示充值页面*/
void showCandyPop();
/** 调取finishChat接口*/
void finishChatApi(String remark);
}
interface View extends BaseView<Presenter>{
/** 获取activity*/
Activity getMyActivity();
/** 大图view*/
FrameLayout getBigFrameVideo();
/** 小图view*/
FrameLayout getSmallFrameVideo();
/** 获取权限-必要权限不同意-挂断*/
void getVideoPermission();
/** 是来电还是去电*/
boolean isIncomingType();
/** 展示等待页面-拨出-拨进*/
void showWaitView(String headUrl,String name,int price);
/** 禁止用户点击按钮-对方已经取消*/
void forbidClickWaitView();
/** 展示视频通话页面*/
void showRoomView();
/** 设置通话时长*/
void setChatTotalTime(String totalTime);
/** 余额不足提示*/
void showGlobalDiamondTip();
/** 关闭activity*/
void finishView();
/** 开启悬浮窗服务*/
void openVideoFloatWindow();
/** 取消悬浮窗-展示activity*/
void removeFloatWindowAndEnterActivity();
/** 监听home按键-需要在进入房间之后监听*/
void listenHomeBtn();
/** 解绑service*/
void myUnbindService();
void showMyWaitDialog(String msg);
void hideMyWaitDialog();
void showToast(String msg,boolean isLongTime);
}
interface Interactor extends BaseInteractor{
void destroy();
/** 初始化数据的用例*/
InitDataUseCase getInitDataUseCase();
/** 调取开始接口的用例*/
StartChatApiUseCase getStartChatApiUseCase();
/** 检查余额的用例*/
CheckAmountUseCase getCheckAmountUseCase();
/** 调取通话状态接口的用例*/
HandleChatApiUseCase getHandleChatApiUseCase();
/** RTC房间的用例*/
RtcRoomUseCase getRtcRoomUseCase();
/** 调用结束通话接口的用例*/
FinishChatApiUseCase getFinishChatApiUseCase();
/** 计算通话时长的用例*/
CallTimeCountUseCase getCallTimeCountUseCase();
/** 通话时长消费金额的用例*/
AvConsumptionUseCase getAvConsumptionUseCase();
/** 礼物消费的用例*/
GiftConsumeUseCase getGiftConsumeUseCase();
}
interface Router extends BaseRouter{
/** 更新用户个人信息*/
void runUserInfoService(Context context);
/** 打开评论页面*/
void openCommentActivity(Context context, AvExtra avExtra);
}
}