flutter学习:flutter MVP demo项目

参考文档:https://medium.com/@develodroid/flutter-iv-mvp-architecture-e4a979d9f47e
原GitHub地址:https://github.com/fabiomsr/Flutter-StepByStep/tree/master/step3
英文好的同学,可以直接看参考文档。这篇文章只是记录下我自己学习的笔记。

概述

本篇文章,旨在利用MVP模型来搭建一个Flutter demo项目,其中的UI设计都挺简单的,采用一个ListView来显示联系人列表页面。其中的涉及到的知识有:

  • 什么是MVP?其中的View视图层,Presenter层,以及Model层如何设计,层级之间如何进行通信回调。
  • Flutter的异步加载机制
  • Flutter第三方http框架的简单使用
  • Flutter Future对象的使用

MVP模型总述

MVP模型包含三个重要的模块:Model层,View层,Presenter层。分为这三层主要的作用就是将业务逻辑代码与视图布局界面显示代码分离。

  • Model层:主要负责数据的准备,从网络上请求数据,缓存数据等;一般向Presenter层提供一个通用的Repository,具体里面的数据是怎么获得的,Presenter层并不关心。
  • Presenter层:从Model层得到原始数据之后,对数据进行一些业务处理,然后将处理后的数据传递到View层展示。
  • View层:主要负责页面数据的展示。
    从上面可以看到,Model层和View层并不直接通信,都是通过Presenter层进行数据交互。所以,一般情况下,Presenter层持有Model的对象引用,以及View的对象引用。View层也会持有Presenter的对象引用。

MVP框架图

MVP demo项目

数据层

对应代码在lib/data文件夹中,新建一个contact_data.dart文件。里面主要做的工作是:

  • 创建一个Contact类,用来封装数据类型
  • 创建一个抽象类ContactRepository,向外提供拉取数据的公共接口
  • 数据拉取异常类FetchException
import 'dart:async';

/// 联系人的类基本信息
class Contact {
  final String fullName;
  final String email;

  const Contact({this.fullName, this.email});

  Contact.fromMap(Map<String, dynamic> map)
      :
        fullName="${map['name']['first']} ${map['name']['last']}",
        email="${map['email']}";

}

/// 联系人的仓库,可以从网络上拉取信息,也可以从本地拉取,看实现的方式了
abstract class ContactRepository {
  Future<List<Contact>> fetch();
}

/// 信息拉取异常类
class FetchException implements Exception {
  String _message;

  FetchException(this._message);

  @override
  String toString() {
    return "Exception:$_message";
  }
}

其中import ‘dart:async’;是用来引入dart语言提供的异步框架,具体使用可以看官网教程dart异步框架

Mock Repository

本地测试的数据仓库。contact_data_mock.dart文件中。

import 'contact_data.dart';

const kContacts = <Contact>[
  Contact(fullName: 'Romain Hoogmoed', email: 'romain.hoogmoed@example.com'),
  Contact(fullName: 'Emilie Olsen', email: 'emilie.olsen@example.com'),
  Contact(fullName: 'Téo Lefevre', email: 'téo.lefevre@example.com'),
  Contact(fullName: 'Nicole Cruz', email: 'nicole.cruz@example.com'),
  Contact(fullName: 'Ramna Peixoto', email: 'ramna.peixoto@example.com'),
  Contact(fullName: 'Jose Ortiz', email: 'jose.ortiz@example.com'),
  Contact(fullName: 'Alma Christensen', email: 'alma.christensen@example.com'),
  Contact(fullName: 'Sergio Hill', email: 'sergio.hill@example.com'),
  Contact(fullName: 'Malo Gonzalez', email: 'malo.gonzalez@example.com'),
  Contact(fullName: 'Miguel Owens', email: 'miguel.owens@example.com'),
  Contact(fullName: 'Lilou Dumont', email: 'lilou.dumont@example.com'),
  Contact(fullName: 'Ashley Stewart', email: 'ashley.stewart@example.com'),
  Contact(fullName: 'Roman Zhang', email: 'roman.zhang@example.com'),
  Contact(fullName: 'Ryan Roberts', email: 'ryan.roberts@example.com'),
  Contact(fullName: 'Sadie Thomas', email: 'sadie.thomas@example.com'),
  Contact(fullName: 'Belen Serrano', email: 'belen.serrano@example.com ')
];

class ContactMockRepository implements ContactRepository {
  @override
  Future<List<Contact>> fetch() {
    // 直接返回一个Future
    return Future.value(kContacts);
  }
}
从网络上加载数据

利用http框架,通过url从网络上拉取json数据,然后用JsonDecoder进行解析。contact_data_impl.dart文件中。

import 'dart:async';
import 'dart:convert';

// http请求框架
import 'package:http/http.dart' as http;

import 'contact_data.dart';

class RandomUserRepository implements ContactRepository {
  // 请求地址
  static const _kRandomUserUrl = 'http://api.randomuser.me/?results=15';
  final _jsonDecoder = JsonDecoder();

  @override
  Future<List<Contact>> fetch() async {
    var response = await http.get(_kRandomUserUrl);
    var jsonBody = response.body;
    var statusCode = response.statusCode;

    if (statusCode < 200 || statusCode >= 300 || jsonBody == null) {
      throw new FetchException(
          "Error while getting contacts. status:$statusCode, Error:${response.reasonPhrase}");
    }

    final container = _jsonDecoder.convert(jsonBody);
    final List contactItems = container['results'];
    return contactItems.map((contactItem)=>Contact.fromMap(contactItem)).toList();
  }
}

关于http框架依赖引入问题,需要在项目配置文件pubspec.yaml中添加一行,配置http的版本号。

dependencies:
  flutter:
    sdk: flutter

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^0.1.2
  # http网络请求框架
  http: ^0.12.0

View层

主要就是在presenter进行数据处理之后,进行页面数据显示。为了更好的体现代码的逻辑,通常需要把View层和Presenter层中的逻辑抽离出接口。所以,就需要另外的Contract接口类。体现在contact_contract.dart文件中。

import '../../data/contact_data.dart';

/// ListView 的接口类
abstract class ContactListViewContract {
  /// 在联系人信息加载完成之后
  void onLoadContactComplete(List<Contact> items);

  /// 信息加载发生错误
  void onLoadContactError();
}

abstract class ContactPresenterContract {
  /// 加载联系人信息,一般是访问model层封装的数据请求方法
  void loadContacts();
}

由于页面需要请求等待数据,然后再刷新列表显示的内容,所以需要继承StatefulWidget。具体代码在contact_view.dart中。

import 'package:flutter/material.dart';
import '../../data/contact_data.dart';
import 'contact_contract.dart';
import 'contact_presenter.dart';

class ContactPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Contacts"),
      ),
      body: ContactListView(),
    );
  }
}

class ContactListView extends StatefulWidget {
  @override
  _ContactListViewState createState() => _ContactListViewState();
}

class _ContactListViewState extends State<ContactListView>
    implements ContactListViewContract {
  ContactPresenterContract _presenter;
  List<Contact> _contacts;

  /// 是否在查找联系人数据
  bool _isSearching;

  _ContactListViewState() {
    _presenter = new ContactPresenter(this);
  }

  @override
  void initState() {
    super.initState();
    _isSearching = true;
    // 异步加载联系人数据
    _presenter.loadContacts();
  }

  @override
  Widget build(BuildContext context) {
    Widget widget;
    if (_isSearching) {
      widget = Center(
        child: Padding(
          padding: EdgeInsets.all(10.0),
          child: CircularProgressIndicator(),
        ),
      );
    } else {
      widget = ListView.builder(
          itemCount: _contacts.length,
          itemBuilder: (BuildContext context, int index) {
            return ListTile(
              leading: CircleAvatar(
                child: Center(
                  child: Text(_contacts[index].fullName.substring(0, 1)),
                ),
              ),
              title: Text(_contacts[index].fullName),
              subtitle: Text(_contacts[index].email),
            );
          });
    }
    return widget;
  }

  @override
  void onLoadContactComplete(List<Contact> items) {
    setState(() {
      _contacts = items;
      _isSearching = false;
    });
  }

  @override
  void onLoadContactError() {
    //TODO 加载失败后进行的操作
  }
}

Presenter层

在这一层,沟通model层和View层,具体的代码逻辑在contact_presenter.dart中。

import '../../data/contact_data.dart';
import '../../injection/injection.dart';
import 'contact_contract.dart';

class ContactPresenter implements ContactPresenterContract {
  ContactListViewContract _view;
  ContactRepository _repository;

  ContactPresenter(this._view) : _repository = new Injector().contactRepository;

  @override
  void loadContacts() {
    assert(_view != null);

    _repository
        .fetch()
        .then((items) => _view.onLoadContactComplete(items))
        .catchError((onError) {
      print(onError);
      _view.onLoadContactError();
    });
  }
}

依赖注入

根据环境(测试环境或者生产环境,当然该项目只是模拟一下),配置不同的参数,例如数据源等。所以,本项目提供了一个injector配置数据源。在injection/injection.dart中。

import '../data/contact_data.dart';
import '../data/contact_data_impl.dart';
import '../data/contact_data_mock.dart';

enum Flavor {
  // 生产环境
  PRO,
  // 测试环境
  MOCK,
}

/// 简单的依赖注入
class Injector {
  static final Injector _injector = Injector._internal();
  static Flavor _flavor;

  static void configure(Flavor flavor) {
    _flavor = flavor;
  }

  factory Injector() {
    return _injector;
  }

  // 内部初始化
  Injector._internal();

  ContactRepository get contactRepository {
    switch (_flavor) {
      case Flavor.PRO:
        return new RandomUserRepository();
      case Flavor.MOCK:
        return new ContactMockRepository();
      default:
        return null;
    }
  }
}

app页面显示

需要在main.dart中配置当前采用的环境。具体代码如下:main.dart

import 'package:flutter/material.dart';

import 'injection/injection.dart';
import 'modules/contract/contact_view.dart';

void main() => runApp(MyApp(Flavor.PRO));

@immutable
class MyApp extends StatelessWidget {
  final Flavor _flavor;

  MyApp(this._flavor) {
    Injector.configure(_flavor);
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "Flutter MVP Demo",
      theme: ThemeData(
        primarySwatch: Colors.lightBlue,
      ),
      home: ContactPage(),
    );
  }
}

效果

页面

资料

官网异步加载教程:https://dart.dev/tutorials/language/futures?source=post_page---------------------------
完整项目代码提供下载:https://github.com/shengleiRain/flutter_app/tree/master/mvp_demo

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值