Flutter的MVP开发框架搭建

写给读者

本人是一名安卓开发工程师(兼职IOS),最近学习了一下Flutter,整理了一套MVP开发框架。该框架我是基于Android的mvp思想编写的,还没在正式项目上使用,供大家参考。

项目介绍

包含mvp基础类,网络层封装(RxDart+Dio),数据解析等相关类的封装。

项目结构

image.png

common

  • application.dart 项目全局属性
  • base 项目基类
  • constants 常量
  • network 网络封装库
  • route 路由管理
  • utils 工具类
  • widget 常用控件封装

contract

页面的view层接口和presenter层接口定义

main.dart

项目启动类

代码

application.dart 项目全局属性

class Application {
  //路由管理
  static Router router;
  //依赖注入,全局单例
  static GetIt getIt = GetIt.instance;
  //全局用户信息
  static UserEntity userEntity;

  static SharedPreferences sp;

  static initSp() async {
    sp = await SharedPreferences.getInstance();
  }

  /// 是否登录
  static bool isLogin() {
    return userEntity != null;
  }

  static initScreenUtil(BuildContext context){
//    ScreenUtil.init(context, width: 720, height: 1280);
//    print('设置像素密度:${ScreenUtil.pixelRatio}');
//    print('设置的高度:${ScreenUtil.screenHeight}');
//    print('设置的宽度:${ScreenUtil.screenWidth}');
  }

  /// 依赖注入,全局单例对象
  static setup(){
    getIt.registerSingleton(new DioRequest());
    getIt.registerSingleton(new UserModel());
  }
}

【Model层相关类】

base_entity.dart Bean基类

class BaseEntity {

}

base_model.dart 发送网络请求(待完善)

class BaseModel {

  //接口请求操作类-全局单例
  DioRequest dioRequest = GetIt.instance<DioRequest>();
  //请求列表接口,每页数量
  int pageCount = 10;
  //参数
  Map<String, dynamic> params = new Map();

  void sendRequest(Stream stream, RxObserver observer) {
    //发送请求
    observer.requestHttp(stream);
  }
}

dio_request.dart 封装的网络请求,结合了RxDart+Dio

class DioRequest {

  /// http request methods
  static const String GET = 'GET';
  static const String POST = 'POST';
  static const String PUT = 'PUT';
  static const String PATCH = 'PATCH';
  static const String DELETE = 'DELETE';

  static DioFactory _dioFactory = DioFactory.instance;

  Future request(String action, String url, {Map<String, dynamic> params, bool isJson}) async {
    var formData;

    if (!isJson) {
      formData = params != null ? FormData.fromMap(params) : null;
    } else {
      formData = params;
    }

    // 获取本地token,添加请求头
    if(Application.userEntity != null){
      UserEntity user = Application.userEntity;

      /// 动态添加headers
      Map<String, String> headers = new Map();
      //token
      headers['authorization'] = '${user.userId}_${user.token}';
      //时间戳
      headers['timestamp'] = Util.currentTimeMillis().toString();
      //版本号
      headers['version'] = '2.0';
      _dioFactory.dio.options.headers.addAll(headers);
    } else {
      _dioFactory.dio.options.headers.remove('authorization');
    }
    Response response;

    try {
      switch (action) {
        case GET:
          response =
          await _dioFactory.dio.get(url, queryParameters: params);
          break;
        case POST:
          response = await _dioFactory.dio.post(url, data: formData);
          break;
      }
    } on DioError catch (error) {
      print('请求出错:' + error.toString());

      // 请求错误处理
      Response errorResponse;
      if (error.response != null) {
        errorResponse = error.response;
      } else {
        errorResponse = new Response(statusCode: ResultCode.NO_NETWORK);
      }
      // 请求超时
      if (error.type == DioErrorType.CONNECT_TIMEOUT) {
        errorResponse.statusCode = ResultCode.CONNECT_TIMEOUT;
      }
      // 一般服务器错误
      else if (error.type == DioErrorType.RECEIVE_TIMEOUT) {
        errorResponse.statusCode = ResultCode.RECEIVE_TIMEOUT;
      }

      throw CommonException(errorMsg: "网络异常");
    }


    return response.data;
  }

  Stream _get(String url, {Map<String, dynamic> params}) =>
      Stream.fromFuture(request(GET, url, params: params)).asBroadcastStream();

  Stream _post(String url, Map<String, dynamic> params, bool isJson) {
    return Stream.fromFuture(request(POST, url, params: params, isJson: isJson)).asBroadcastStream();
  }


  Stream handlerGet(String requestUrl, {Map params}) {
    return _get(requestUrl);
  }

  Stream handlerFormPost(String requestUrl, {Map<String, dynamic> params}) {
    return _post(requestUrl, params, false);
  }

  Stream handlerJsonPost(String requestUrl, {Map<String, dynamic> params}) {
    return _post(requestUrl, params, true);
  }
}

rx_observer.dart 订阅网络请求,处理请求开始与结束的统一逻辑

typedef void OnSuccess(dynamic data);

typedef void OnFailure(String error);

class RxObserver<T> {

  //页面引用对象(显示进度框、提示语)
  BaseView view;
  //提示语
  String msg;
  //是否显示进度框
  bool isShowDialog;
  //成功回调
  OnSuccess onSuccess;
  //失败回调
  OnFailure onFailure;

  RxObserver(this.view, {this.onSuccess, this.onFailure, this.isShowDialog = true});


  ///http网络请求 [request] 接收 Stream 类型
  void requestHttp(Stream request) {

    request.doOnListen(() {

      //开始网络请求,根据提示语显示弹框
      if (view != null && isShowDialog) {
        view.showLoading(msg: msg);
      }

    }).listen((data) {
      //请求成功,返回结果,关闭弹框
      if (view != null && isShowDialog) {
        view.closeLoading();
      }

      //请求成功 获取数据data, data是返回结果json
      BaseResponse<T> response = BaseResponse.fromJson(data);

      if (response.resultCode != 100) {
        handlerError(response.resultMsg);
      } else {
        onSuccess(response.getData());
      }

    }, onError: (error) {
      if (view != null && isShowDialog) {
        view.closeLoading();
      }

      //请求失败
      DioError e = error;
      handlerError(e.message);
    }, onDone: () {
      //执行结束

    });

//    subject.cancel();

  }

  void handlerError(String error) {
    if (view != null && isShowDialog) {
      view.showError(errorMsg: error);
    }
    //请求失败
    onFailure(error);
  }
}

base_response.dart 接口返回结果基类

class BaseResponse<T> {
  dynamic data;
  bool success;
  int resultCode;
  String resultMsg;

  BaseResponse({this.data, this.success, this.resultCode, this.resultMsg});

  BaseResponse.fromJson(Map<String, dynamic> json) {
    data = json['data'];
    success = json['success'];
    resultCode = json['resultCode'];
    resultMsg = json['resultMsg'];
  }

  /// 获取results对象
  T getData() {
    return EntityFactory.generateOBJ<T>(data); //使用EntityFactory解析对象
  }

  /// 获取results数组
  List<T> getList() {
    var newList = new List<T>();
    if (data != null) {
      data.forEach((v) { //拼装List
        newList.add(EntityFactory.generateOBJ<T>(v));//使用EntityFactory解析对象
      });
    }
    return newList;
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['data'] = this.data;
    data['success'] = this.success;
    data['resultCode'] = this.resultCode;
    data['resultMsg'] = this.resultMsg;
    return data;
  }
}

【View层相关】

base_state.dart 页面基类(等同android/ios的BaseActivity/BaseViewController)

/// state基类
abstract class BaseState<P extends BasePresenter, W extends StatefulWidget>
    extends State<W> with AutomaticKeepAliveClientMixin implements BaseView {
  
  P mPresenter;
  //是否初始化
  bool _isPrepared = false;
  //请求dialog
  LoadingDialog dialog;

  @override
  void initState() {
    mPresenter = createPresenter();
    _attachView();
    super.initState();
  }

  ///构建页面
  @override
  Widget build(BuildContext context) {
    super.build(context);
//    ScreenUtil.init(context, width: 750, height: 1334);
    if (!_isPrepared) {
      Timer.run(() => preparePage());
      _isPrepared = true;
    }
    return Scaffold(
      appBar: buildAppBar(),
      body: buildPageLayout(),
    );
  }

  Widget buildAppBar();

  Widget buildPageLayout();

  @mustCallSuper
  @override
  void dispose() {
    super.dispose();
    _detachView();
  }

  P createPresenter();

  /// 初始化一次 =》 用于 presenter 请求网络数据后调用 showDialog 拿不到合适的 context 报错
  void preparePage();

  @override
  void reload() {}

  @override
  void renderPage(Object o) {}

  @override
  void showDisConnect() {}

  @override
  void showError({String errorMsg}) {
    if (errorMsg != null) {
      ToastUtil.showToast(errorMsg);
    }
  }

  @override
  void showLoading({String msg}) {
    /// 把 dialog 的 show 从 普通页面里分离
    if (dialog == null) {
      dialog = LoadingDialog(
        text: msg ?? '加载中',
      );
    }
    showDialog(
        context: context,
        barrierDismissible: true,
        builder: (BuildContext context) {
          return dialog;
        });
  }

  @override
  void closeLoading() {
    /// 必须和 showLoading 方法配对使用 ,避免 pop 当前页面
    if (dialog != null) {
      Navigator.pop(context);
      dialog = null;
    }
  }

  void _attachView(){
    if(null != mPresenter){
      mPresenter.attachView(this);
    }
  }

  void _detachView(){
    if(null != mPresenter){
      mPresenter.detachView();
    }
  }
  
  //不会被销毁,占内存中
  @override
  bool get wantKeepAlive => true;

}

base_view.dart

abstract class BaseView {

  void showLoading({String msg});

  void closeLoading();

  void renderPage(Object object);

  void reload();

  void showError({String errorMsg});

  void showDisConnect();

}

【presenter相关】

base_presenter.dart

class BasePresenter<V extends BaseView> {
  V view;


  /// 绑定视图,页面创建时调用
  void attachView(V view) {
    this.view = view;
  }

  /// 解绑视图,页面销毁时调用
  void detachView() {
    if (null != view) {
      this.view = null;
    }
  }
}

以上是项目结构及相关重要的几个类介绍,源码在文章最后,下面看下实际使用:

main.dart 项目启动类

void main() {
  //初始化路由管理
  Router router = new Router();
  Routes.configureRoutes(router);
  Application.router = router;
  //注册
  Application.setup();

  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        brightness: Brightness.light,
        primaryColor: Color(0xFF00A0E9),
        splashColor: Colors.transparent,
        backgroundColor: Color(0xFFFFFFFF),
      ),
      home: SplashPage(),
      onGenerateRoute: Application.router.generator,
//      routes: {
//        "/test" : (context) => new TestPage(),
//      },
    );
  }
}

以登录页为例

【model层】

user_entity.dart 登录信息bean

class UserEntity {
    var appId;
    var redisKey;
    var userId;
    var token;

    UserEntity({this.appId, this.redisKey, this.userId, this.token});

    UserEntity.fromJson(Map<String, dynamic> json) {
        appId = json['appId'];
        redisKey = json['redisKey'];
        userId = json['userId'];
        token = json['token'];
    }

    Map<String, dynamic> toJson() {
        final Map<String, dynamic> data = new Map<String, dynamic>();
        data['appId'] = this.appId;
        data['redisKey'] = this.redisKey;
        data['userId'] = this.userId;
        data['token'] = this.token;
        return data;
    }
}

user_model.dart 用户model操作类

class UserModel extends BaseModel {


  /// 登录
  void login(String account, String password, RxObserver observer){
    password = Util.generateMd5(password);

    params.clear();
    params['username'] = account;
    params['password'] = password;

    Stream stream = dioRequest.handlerFormPost(Url.login, params: params);
    sendRequest(stream, observer);
  }
}

【view层】

page_login.dart 登录页面

class LoginPage extends StatefulWidget {
  @override
  LoginPageState createState() => LoginPageState();
}

class LoginPageState extends BaseState<LoginPresenter, LoginPage> implements ILoginView{

  bool isPasswordLogin = false; //是否密码登录,默认验证码登录

  String _phoneNumber;
  String _password;


  @override
  void initState() {
    super.initState();
  }

  @override
  Widget buildAppBar() {

    return AppBar(
      backgroundColor: Colors.white,
      iconTheme: IconThemeData(color: Colors.black),
      elevation: 0,
    );
  }

  @override
  Widget buildPageLayout() {

    return Container(
      color: Colors.white,
      height: double.infinity,
      padding: EdgeInsets.only(left: 20, right: 20),
      child: Stack(
        alignment: AlignmentDirectional.center,
        children: <Widget>[

          Positioned(
            top: 0,
            left: 0,
            bottom: 0,
            right: 0,
            child: SingleChildScrollView(

              child: Column(
                mainAxisSize: MainAxisSize.max,
                crossAxisAlignment: CrossAxisAlignment.start, //左侧对齐
                children: <Widget>[

                  //登录图标
                  Row(
                    children: <Widget>[
                      Image.asset("images/login_logo.png", width: UIUtil.getWidth(61), height: UIUtil.getHeight(61),),
                      UIUtil.createText("登录", 24, AppColor.black4, isBold: true, margin: EdgeInsets.only(left: 7)),
                      InkWell(
                        child: UIUtil.createText(isPasswordLogin ? "验证码登录" : "密码登录",
                            12, AppColor.grayText, isBold: true, margin: EdgeInsets.only(left: 10)),
                        onTap: (){
                          setState(() {
                            isPasswordLogin = !isPasswordLogin;
                          });
                        },
                      )

                    ],
                  ),

                  //提示
                  UIUtil.createText("未注册手机号验证后即完成注册", 12, AppColor.grayText, margin: EdgeInsets.only(top: 56, bottom: 10)),

                  //手机号
                  ITextField(
                    keyboardType: ITextInputType.phone,
                    hintText: '请输入手机号',
                    hintStyle: TextStyle(color: AppColor.grayText),
                    textStyle: TextStyle(color: AppColor.black4),
                    fieldCallBack: (content) {
                      _phoneNumber = content;
                      print(_phoneNumber);
                    },
                  ),
                  //密码
                  Container(
                    child: Stack(
                      children: <Widget>[
                        ITextField(
                          margin: EdgeInsets.only(top: 20),
                          keyboardType: ITextInputType.password,
                          hintText: isPasswordLogin ? '请输入密码' : '请输入验证码',
                          hintStyle: TextStyle(color: AppColor.grayText),
                          textStyle: TextStyle(color: AppColor.black4),
                          fieldCallBack: (content) {
                            _password = content;
                            print(_password);
                          },
                        ),
                        Positioned(
                          bottom: 0,
                          right: 10,
                          child: CustomCountdown((){
                            print("请求验证码");
                          }),
                        )
                      ],
                    ),
                  ),

                  //登录
                  Container(
                    margin: EdgeInsets.only(top: 120),
                    width: double.infinity,
                    height: UIUtil.getHeight(50),
                    child: RaisedButton(
                      child: Text("登录", style: TextStyle(fontSize: UIUtil.getSp(16)),),
                      textColor: AppColor.black4,
                      color: AppColor.theme,
                      shape:RoundedRectangleBorder(borderRadius: BorderRadius.circular(25.0)),
                      onPressed: _login,
                    ),
                  ),

                  //注册视图
                  isPasswordLogin ?
                  Container(
                    margin: EdgeInsets.only(top: 10, left: 10, bottom: UIUtil.getHeight(82)),
                    child: Row(
                      children: <Widget>[
                        UIUtil.createText("没有账号,", 12, AppColor.black6, isBold: true),
                        InkWell(
                          child: UIUtil.createText("去注册", 12, AppColor.theme, isBold: true),
                          onTap: (){
                            print("去注册");
                          },
                        ),
                        Spacer(
                          flex: 1,
                        ),
                        InkWell(
                          child: UIUtil.createText("忘记密码?", 12, AppColor.grayText, isBold: true, textAlign: TextAlign.end),
                          onTap: (){
                            print("忘记密码");
                          },
                        ),
                      ],
                    ),
                  ) : Text(""),
                ],
              ),
            ),
          ),

          //底部按钮
          Positioned(
//            width: double.infinity,
            bottom: 30,
            child: Row(

              children: <Widget>[
                Text.rich(TextSpan(
                    children: [
                      TextSpan(
                        text: "《服务协议》",
                        style: TextStyle(
                            color: AppColor.theme
                        ),
//                      recognizer: _tapRecognizer
                      ),
                      TextSpan(
                        text: "和",
                        style: TextStyle(
                            color: AppColor.grayText
                        ),
                      ),
                      TextSpan(
                        text: "《隐私政策》",
                        style: TextStyle(
                            color: AppColor.theme
                        ),
//                      recognizer: _tapRecognizer
                      ),
                    ]
                )),
              ],
            ),
          )
        ],
      ),
    );
  }

  void _login() {
    mPresenter.login();
  }

  @override
  void preparePage() {
    // TODO: implement preparePage
  }

  @override
  String getAccount() {
    return _phoneNumber;
  }

  @override
  String getPassword() {
    return _password;
  }

  @override
  LoginPresenter createPresenter() {
    return LoginPresenter();
  }

  @override
  void loginSuccess(UserEntity user) {
    Application.userEntity = user;
    //存储用户信息
    String userJson = Util.json2String(user.toJson());
    SpUtil.setString(SPKey.USER, userJson);
    RouteUtil.goMainPage(context, replace: true);
  }

}

login_page_contract.dart 登录页面的view接口和presenter接口定义

abstract class ILoginView extends BaseView {
  /// 获取用户输入账号
  String getAccount();

  /// 获取用户输入密码
  String getPassword();

  /// 登录成功
  void loginSuccess(UserEntity entity);

}

abstract class ILoginPresenter<V extends BaseView>  extends BasePresenter<V> {

  /// 登录
  void login();

  /// 获取验证码
  void getPhoneCode();

}

【presenter层】

login_presenter.dart

class LoginPresenter extends ILoginPresenter<ILoginView> {

  @override
  void login() {
    // TODO: implement login
    String account = view.getAccount();
    String password = view.getPassword();

    if (account == null || account.length == 0) {
      view.showError(errorMsg: '请输入手机号');
      return;
    }
    if (password == null || password.length == 0) {
      view.showError(errorMsg: '请输入密码');
      return;
    }

    Application.getIt.get<UserModel>().login(account, password, new RxObserver<UserEntity>(view,
      onSuccess: (data) {
        print(data);
        view.loginSuccess(data);
      },
      onFailure: (error) {

      }
    ));
  }

  @override
  void getPhoneCode() {
    // TODO: implement getPhoneCode
  }

}

补充介绍 base_list_page.dart 列表基类

开发中,经常有列表页面,所有会存在一些公共逻辑,比如下拉刷新,加载更多,空视图、失败视图等。base_list_page可以省去一些公共代码,减少开发工作量。

/// 列表页面的state基类
abstract class BaseListPageState<P extends BasePresenter, W extends StatefulWidget, T extends BaseEntity> extends BaseState<P, W> implements BaseListView {

  List<T> list = [];

  RefreshController _refreshController = RefreshController(initialRefresh: false);

  int page = 1;

  int total;

  bool isFailure = false;

  void _onRefresh() async {
    page = 1;
    requestListData();
  }

  void _onLoading() async {
    requestListData();
  }

  @override
  int getPage() {
    return page;
  }

  @override
  void listFailure() {
    if (page == 1) {
      isFailure = true;
      _refreshController.refreshCompleted();
    } else {
      _refreshController.loadFailed();
    }

    setState(() {

    });
  }

  @override
  void listSuccess(ListEntity result) {
    total = result.count;
    if (page == 1) {
      isFailure = false;
      setEnableLoadMore(true);
      list.clear();
      list.addAll(result.getList<T>());
      _refreshController.refreshCompleted();
    } else {
      list.addAll(result.getList<T>());
      _refreshController.loadComplete();
    }
    page++;

    if (list == null || list.length == 0) { //列表为空

      return;
    }
    if (list.length >= total) {//没有更多数据了
      _refreshController.loadNoData();
    }

    setState(() {

    });
  }


  //是否开启下拉刷新
  bool enableRefresh = true;
  //是否开启上拉加载
  bool enableLoadMore = true;

  /// 构建一个带刷新的列表视图
  Widget buildListView() {
    return SmartRefresher (
      enablePullDown: enableRefresh,
      enablePullUp: enableLoadMore,
      // WaterDropHeader、ClassicHeader、CustomHeader、LinkHeader、MaterialClassicHeader、WaterDropMaterialHeader
      header: ClassicHeader(
        height: 45.0,
        releaseText: '松开手刷新',
        refreshingText: '刷新中',
        completeText: '刷新完成',
        failedText: '刷新失败',
        idleText: '下拉刷新',
      ),

      // ClassicFooter、CustomFooter、LinkFooter、LoadIndicator
      footer: CustomFooter(
        builder: (BuildContext context, LoadStatus mode) {
          Widget body;
          if (mode == LoadStatus.idle) {
            body = Text("上拉加载更多");
          }
          else if (mode == LoadStatus.loading) {
            body = CupertinoActivityIndicator();
          }
          else if (mode == LoadStatus.failed) {
            body = Text("加载失败,点击重试");
          }
          else {
            body = Text("已经到底了");
          }
          return Container(
            height: 55.0,
            child: Center(child: body),
          );
        },
      ),
      controller: _refreshController,
      onRefresh: _onRefresh,
      onLoading: _onLoading,
      child: contentView(),
    );
  }

  ///有返回列表视图。无数据,返回空视图
  Widget contentView() {
    if (page == 1 && isFailure) {
      return HintWidget(HintWidget.ERROR, function: () {
        /// 列表请求失败时,重新请求
        requestListData();
      });
    }

    if (list == null || list.length == 0) {
      //第一次返回加载中,total有值时,list=0返回空视图
      return HintWidget(total != null ? HintWidget.EMPTY : HintWidget.LOADING);
    } else {
      //返回列表视图
      return ListView.builder(
        itemBuilder: (BuildContext context, int index){
          return GestureDetector(
            onTap: () {
              //item Click
              onItemClick(context, index);
            },
            child: buildItem(context, index),
          );
        },
        itemCount: list.length,
//        itemExtent: 100.0,
//        cacheExtent: 10,
      );
    }
  }

  //构建列表行布局
  Widget buildItem(BuildContext context, int index);

  //列表行点击事件
  void onItemClick(BuildContext context, int index);

  //请求列表数据
  void requestListData();

  //是否开启加载更多
  void setEnableLoadMore(enable) {
    enableLoadMore = enable;
  }

  //是否开启下拉刷新
  void setEnableRefresh(enable) {
    enableRefresh = enable;
  }

  // don't forget to dispose refreshController
  @override
  void dispose() {
    _refreshController.dispose();
    super.dispose();
  }

}

base_list_view.dart

abstract class BaseListView extends BaseView {

  //加载成功
  void listSuccess(ListEntity result);
  //加载失败
  void listFailure();
  //获取当前页数
  int getPage();

}

使用示例

/// 车辆列表
class CarListPage extends StatefulWidget {
  @override
  _CarListPageState createState() => _CarListPageState();
}

/// 指定泛型 ,最后一个类型为列表对应的实体类
class _CarListPageState extends BaseListPageState<CarListPresenter, CarListPage, CarEntity> implements ICarListView{

  @override
  Widget buildAppBar() {
    // TODO: implement buildAppBar
    return new AppBar(title: Text("列表页"),);
  }

  @override
  Widget buildItem(BuildContext context, int index) {
    // TODO: implement buildItem
    return OnlineCarItem(list[index], (carEntity){

      if (Application.isLogin()) {
        //根据收藏状态,调用不同接口
        if (Util.equals(carEntity.isFollow, "1")) {
          mPresenter.cancelCollectionCar(carEntity);
        } else {
          mPresenter.collectionCar(carEntity);
        }
      } else {
        RouteUtil.goLoginPage(context);
      }

    });
  }

  @override
  Widget buildPageLayout() {
    // TODO: implement buildPageLayout
    return buildListView();
  }

  @override
  CarListPresenter createPresenter() {
    // TODO: implement createPresenter
    return new CarListPresenter();
  }

  @override
  void onItemClick(BuildContext context, int index) {
    // TODO: implement onItemClick
    CarEntity carEntity = list[index];
    print("点击:$carEntity");
    RouteUtil.goDetailPage(context, carEntity.id.toString());
  }

  @override
  void preparePage() {
    // TODO: implement preparePage
    requestListData();
  }

  @override
  void requestListData() {
    // TODO: implement requestListData
    mPresenter.getList();
  }

  @override
  void collectionSuccess() {
    // TODO: implement likeSuccess
    // 操作成功,刷新列表
    setState(() {

    });
  }

  @override
  void cancelCollectionSuccess() {
    // TODO: implement cancelCollectionSuccess
    // 操作成功,刷新列表
    setState(() {

    });
  }

}

car_list_page_contract.dart

abstract class ICarListView extends BaseListView {

  /// 收藏成功
  void collectionSuccess();
  /// 取消收藏成功
  void cancelCollectionSuccess();
}

abstract class IListPresenter<V extends BaseView>  extends BasePresenter<V> {

  /// 获取列表
  void getList();

  /// 收藏车辆
  void collectionCar(CarEntity carEntity);

  /// 取消收藏车辆
  void cancelCollectionCar(CarEntity carEntity);
}

car_list_presenter.dart

class CarListPresenter extends IListPresenter<ICarListView> {
  @override
  void getList() {
    Application.getIt.get<UserModel>().getCarList(view.getPage(), new RxObserver<ListEntity>(view,
      onSuccess: (data) {
        view.listSuccess(data);
      },
      onFailure: (error) {
        view.listFailure();
      }
    ));
  }

  @override
  void collectionCar(CarEntity carEntity) {
    Application.getIt.get<UserModel>().collection(carEntity.id.toString(), new RxObserver<EmptyEntity>(view,
        onSuccess: (data) {
          //车辆关注状态更改
          carEntity.isFollow = carEntity.isFollow.toString().endsWith("1") ? "2" : "1";
          view.collectionSuccess();
        },
        onFailure: (error) {

        }
    ));
  }

  @override
  void cancelCollectionCar(CarEntity carEntity) {
    Application.getIt.get<UserModel>().cancelCollection(carEntity.id.toString(), new RxObserver<EmptyEntity>(view,
        onSuccess: (data) {
          //车辆关注状态更改
          carEntity.isFollow = carEntity.isFollow.toString().endsWith("1") ? "2" : "1";
          view.cancelCollectionSuccess();
        },
        onFailure: (error) {

        }
    ));
  }
}

【用到的一些框架】

页面路由管理

https://github.com/theyakka/fluro

简单数据存储

https://pub.dev/packages/shared_preferences

网络请求

https://github.com/flutterchina/dio

Rx

https://pub.dev/packages/rxdart

依赖注入(单例)

https://pub.flutter-io.cn/packages/get_it

下拉刷新 加载

https://pub.dev/packages/pull_to_refresh

图片加载

https://pub.dev/packages/cached_network_image

【最后】

现阶段对Flutter的很多地方理解不够,能力有限,如有不合适需要改善的地方,可以多提宝贵意见!

【项目地址】

https://github.com/zhengqiyao93/Flutter_Mvp

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350