前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >「 Flutter 项目实战 」设计企业级项目入口 main.dart 设计与实现 ( GSYGithubApp 源码解读·二 )

「 Flutter 项目实战 」设计企业级项目入口 main.dart 设计与实现 ( GSYGithubApp 源码解读·二 )

作者头像
圆号本昊
发布2021-12-30 16:33:35
1.1K0
发布2021-12-30 16:33:35
举报
文章被收录于专栏:github@hornhuang

提示:温馨提示一下哈,这篇文章主要是针对 GitHub 上 12+k 顶级项目「 CarGuo/gsy_github_app_flutter 」 的源码解读,因为这是我目前见过最棒、最具有企业级水平的 Flutter 开源项目,整个项目的设计令我倾佩,所以我希望与大家一起分享它 注意:我并非什么大神,只是一个热爱分享,并希望带大家一起进步的码者,所以我也无法保证本文的方案就一定是最好的,如果有更好的方案,也希望大家在评论区分享。那么与君共勉,我们开始吧 ~

一、前言

  • 初始化 Flutter project 时,系统会给我们一个默认的 main.dart 文件,但在世纪开发中我不建议直接使用,因为它的功能过于简单(只是加载了界面),并不能满足实际复杂的开发需求
  • 我将给大家呈现的 main.dart 设计方案讲具有:失败页、错误日志获取、数据共享和网络监听等功能,下面我们正式进入

二、main.dart

  • 由于相比默认 main.dart 文件,新方案功能要多很多,所以我们需要拆分为:main.dart 和 app.dart 两个文件来实现
  • 在 main.dart 中需要实现三个功能:异常捕获、错误页展示、主页面加载

2.1 异常捕获 - runZoned

  • 在 Flutter 中,还无法捕获的异常,如调用空对象方法异常、Futurer 中的异常等
  • 同样,对于在 Dart 中的同步异常和异步异常,同步异常可以通过 try/catch 捕获,但异步异常则比较麻烦
  • 举个异步异常的栗子:
代码语言:javascript
复制
try{
    Future.delayed(Duration(seconds: 1)).then((e) => Future.error("asynchronous error"));
}catch (e){
    // TODO Report
}
  • Dart 中有一个 runZoned(…) 方法( Zone 表示一个代码执行的环境范围)
  • 在 Zone 中可以捕获日志输出、Timer 创建、微任务调度的行为,同时 Zone 也可以捕获所有未处理的异常
  • 将上面代码结合 runZoned 实现就是:
代码语言:javascript
复制
  runZoned(() {
    Future.delayed(Duration(seconds: 1)).then((e) => Future.error("asynchronous error"));
  }, onError: (Object obj, StackTrace stack) {
    // crash 日志打印与上报
  })
  • 所以 main() 方法就可以相应的写作:
代码语言:javascript
复制
void main() {
  runZoned(() {
  	// TODO some init
    runApp(FlutterReduxApp());
  }, onError: (Object obj, StackTrace stack) {
    // crash 日志打印与上报
    print(obj);
    print(stack);
  });
}

2.2 错误页展示 - ErrorWidget

  • Flutter 在很多关键的方法进行了异常捕获
  • 举个例子,当布局发生越界或不和规范时,会自动弹出一个错误界面:
  • 现网环境中,我们不能直接给用户展示这个页面,这时就需要 ErrorWidget。让它处于最底层来覆盖这个这样的页面
  • 添加上 ErrorWidget 后如下所示:
代码语言:javascript
复制
void main() {
  runZoned(() {
    ErrorWidget.builder = (FlutterErrorDetails details) {
      Zone.current.handleUncaughtError(details.exception, details.stack);
      return ErrorPage(
          details.exception.toString() + "\n " + details.stack.toString(), details);
    };
    runApp(FlutterReduxApp());
  }, onError: (Object obj, StackTrace stack) {
  	// crash 日志打印与上报
    print(obj);
    print(stack);
  });
}
  • 其中 ErrorWidget 所创建的错误页:ErrorPage 是我们自定义的
  • 其主要功能应包括:错误日志上传、返回上一界面
  • 具体逻辑需根据实际环境设计,由于异常上报跟本文主题关系无关,大家可以参照 error_page 源码 进行设计

2.3 数据共享 - InheritedWidget

  • 由于Flutter采用节点树的方式组织页面,以致于一个普通页面的节点层级会很深。
  • 此时,我们如果还是一层层传递数据,当需要修改数据时,就会比较麻烦。
  • 《Flutter 实战》中讲到:InheritedWidget 是 Flutter 中非常重要的一个功能型组件,它提供了一种数据在 widget 树中从上到下传递、共享的方式
  • 比如我们在应用的根 widget 中通过 InheritedWidget共享了一个数据,那么我们便可以在任意子 widget 中来获取该共享的数据!
  • 这个特性在一些需要在 widget 树中共享数据的场景中非常方便!如Flutter SDK 中正是通过 InheritedWidget 来共享应用主题(Theme)和 Locale (当前语言环境)信息的。

InheritedWidget 基本使用: 还没有学会 使用的同学可以先查看这篇文章进行学习 「flutter 必知必会」详细解析数据共享 InheritedWidget 完整使用

2.3.1 共享的数据
  • 根据 OOP 原则,我们将需共享的数据独立出一个类 EnvConfig
  • 新建 env_config.dart 文件内容如下
代码语言:javascript
复制
///环境配置
@JsonSerializable(createToJson: false)
class EnvConfig {
  final String env;
  final bool debug;

  EnvConfig({
    this.env,
    this.debug,
  });

  factory EnvConfig.fromJson(Map<String, dynamic> json) => _$EnvConfigFromJson(json);
}
  • 由于这些配置一般是通过本地存储,或者联网时拉取
  • 所以其实例化采用 fromJson 方法,同时用户更新后也可以在转为 json 串存储到本地进行覆盖
2.3.2 封装与管理 ConfigWrapper

数据绑定的作用分两种:跟 UI 结合的内容刷新(如页面文字内容),全局共享的配置数据(如用户登录状态,系统颜色等) 由于本文是对 main.dart 的解析,所以我们针对第二种情况进行分析即可 对第一种情况感兴趣的同学可以点击上面链接查看

  • 我们知道 runApp 的参数是 WIdget 类型,同时我们需要将界面 MaterialApp 和数据进行绑定
  • 所以为了方便管理,我们新建一个 config_wrapper 文件,同时在这个文件里实现类 ConfigWrapper
代码语言:javascript
复制
///往下共享环境配置
class ConfigWrapper extends StatelessWidget {
  ConfigWrapper({Key key, this.config, this.child});

  @override
  Widget build(BuildContext context) {
    ///设置 Config.DEBUG 的静态变量
    Config.DEBUG = this.config.debug;
    print("ConfigWrapper build ${Config.DEBUG}");
    return new _InheritedConfig(config: this.config, child: this.child);
  }

  static EnvConfig of(BuildContext context) {
    final _InheritedConfig inheritedConfig =
    context.dependOnInheritedWidgetOfExactType<_InheritedConfig>();
    return inheritedConfig.config;
  }

  final EnvConfig config;

  final Widget child;
}
  • 这个类的作用主要是对功能的封住,比如获取/更新数据,就可以通过 ConfigWrapper.of(…).methed(),来进行操作
2.3.3 绑定数据与视图 _InheritedConfig
  • 其中,将数据与视图(MaterialApp)绑定需要使用到 InheritedWidget
  • 同样在 config_wrapper.dart 文件中,我们新建一个类 _InheritedConfig
代码语言:javascript
复制
class _InheritedConfig extends InheritedWidget {
  const _InheritedConfig(
      {Key key, @required this.config, @required Widget child})
      : assert(child != null),
        super(key: key, child: child);

  final EnvConfig config;

  @override
  bool updateShouldNotify(_InheritedConfig oldWidget) =>
      config != oldWidget.config;
}
  • 每一次调用/修改绑定的数据,都会调用 updateShouldNotify 这个方法
  • 这方法是用来判断是否需要通知视图,可更具具体场景进行设定
  • 比如数字数据变化时修改,或者数据变不变都修改

updateShouldNotify : Whether the framework should notify widgets that inherit from this widget.

2.3.4 获取数据与更新
  • 就如前面的流程图所示,我们通过 ConfigWrapper.of(…) 方法就能获得配置的数据对象
  • 之后其中数据,进行对应的修改、赋值操作即可,修改后会调用到 updateShouldNotify 来确认是否通知 UI 更新

2.4 页面编写 MaterialApp

  • 页面的编写主要注意两个
  • 一方面是页面的更新(flutter_redux / InheritedWidget)
  • 另一方面是诸如网络异常、登录成功之类,各种提示的显示(eventBus)
2.4.1 页面独立
  • 首先根据 oop 六大原则,我们需要将 app 的页面独立出一个类
  • 这里建议将其命名为 app.dart 更为合理,因为有入口的意思
代码语言:javascript
复制
class FlutterReduxApp extends StatefulWidget {
  @override
  _FlutterReduxAppState createState() => _FlutterReduxAppState();
}

class _FlutterReduxAppState extends State<FlutterReduxApp> {
  // ...
}

2.5 页面更新 flutter_redux

  • 关于数据与页面的绑定/更新,前面已经介绍了 InheritedWidget
  • flutter_redux 是在 InheritedWidget 的基础上封装的,对于 UI 上数据的更新与管理更加方便高效,但是如果数据很简单,或者不涉及 UI 那么使用 InheritedWidget 更简单一些也就比较适合

这里如果是还不会使用 flutter_redux 的同学可以先看这篇文章 「 flutter 必知必会 」最强数据管理方案 flutter_redux 使用解析

  • OK,那么一个企业级项目的 main.dart 木块中该如何使用 flutter_redux 呢?
  • 下面我们就以 GSYGitHubApp 为例,看看优秀的 app 是怎么实现的
2.4.1 创建 store
  • 要使用 flutter_redux 来对页面进行管理,就系要实例化 store
代码语言:javascript
复制
  /// 创建Store,引用 GSYState 中的 appReducer 实现 Reducer 方法
  /// initialState 初始化 State
  final store = new Store<GSYState>(
    appReducer,

    ///拦截器
    middleware: middleware,

    ///初始化数据
    initialState: new GSYState(
        userInfo: User.empty(),
        login: false,
        themeData: CommonUtils.getThemeData(GSYColors.primarySwatch),
        locale: Locale('zh', 'CH')),
  );
  • 但是使用 store 需要准备 appReducer、middleware、GSYState 四个模块,下面我们逐个来看看
2.4.2 创建 Reducer
  • 源码中 Reducer 是一个方法 typedef State Reducer(State state, dynamic action);
  • 我们自定义了 appReducer 用于创建 store
代码语言:javascript
复制
GSYState appReducer(GSYState state, action) {
  return GSYState(
    ///通过 UserReducer 将 GSYState 内的 userInfo 和 action 关联在一起
    userInfo: UserReducer(state.userInfo, action),

    ///通过 ThemeDataReducer 将 GSYState 内的 themeData 和 action 关联在一起
    themeData: ThemeDataReducer(state.themeData, action),

    ///通过 LocaleReducer 将 GSYState 内的 locale 和 action 关联在一起
    locale: LocaleReducer(state.locale, action),
    login: LoginReducer(state.login, action),
  );
}
  • 那么 GSYState 需要如何设计呢?
2.4.3 创建 State
  • 全局Redux store 的对象,保存State数据
代码语言:javascript
复制
class GSYState {
  ///用户信息
  User userInfo;

  ///主题数据
  ThemeData themeData;

  ///语言
  Locale locale;

  ///当前手机平台默认语言
  Locale platformLocale;

  ///是否登录
  bool login;

  ///构造方法
  GSYState({this.userInfo, this.themeData, this.locale, this.login});
}
2.4.4 拦截器 middleware
  • 拦截器顾名思义就是拦截消息是否再往下传递
代码语言:javascript
复制
final List<Middleware<GSYState>> middleware = [
  EpicMiddleware<GSYState>(loginEpic),
  EpicMiddleware<GSYState>(userInfoEpic),
  EpicMiddleware<GSYState>(oauthEpic),
  UserInfoMiddleware(),
  LoginMiddleware(),
];
  • 可以看到 GSYGitHubApp 中设置了 5 个拦截器, 如果均满足其中的筛选条件,就可以进行后续的 UI 刷新操作
  • 就比如第一个‘登录’,如果用户没登录,自然不用再往后了,按照 app 设计的逻辑,这时需要先跳转登录才行
2.4.5 全局注册
  • 在 _HomePageState 的 build 方法中, 配置 store 方便后续使用
代码语言:javascript
复制
  @override
  Widget build(BuildContext context) {
    return StoreBuilder<GSYState>(
      builder: (context, store) {
        double size = 200;
        return Material(
          ...
        );
      },
    );
  }
2.4.6 注册更新监听
  • 在需要 Store 中数据的地方/页面,通过 StoreConnector 进行注册
代码语言:javascript
复制
class DemoUseStorePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    ///通过 StoreConnector 关联 GSYState 中的 User
    return new StoreConnector<GSYState, User>(
      ///通过 converter 将 GSYState 中的 userInfo返回
      converter: (store) => store.state.userInfo,
      ///在 userInfo 中返回实际渲染的控件
      builder: (context, userInfo) {
        return new Text(
          userInfo.name,
          style: Theme.of(context).textTheme.headline4,
        );
      },
    );
  }
}
2.4.7 触发
  • 通过 store.dispatch(…) 可以调用到 appReducer ,并对 store 中的数据进行刷新
  • 同时可以通知到注册了这些数据的地方,让他们自动取刷新对应的 ui
代码语言:javascript
复制
  ///初始化用户信息
  static initUserInfo(Store store) async {
    var token = await LocalStorage.get(Config.TOKEN_KEY);
    var res = await getUserInfoLocal();
    if (res != null && res.result && token != null) {
      store.dispatch(UpdateUserAction(res.data));
    }

    ///读取主题
    String themeIndex = await LocalStorage.get(Config.THEME_COLOR);
    if (themeIndex != null && themeIndex.length != 0) {
      CommonUtils.pushTheme(store, int.parse(themeIndex));
    }

    ///切换语言
    String localeIndex = await LocalStorage.get(Config.LOCALE);
    if (localeIndex != null && localeIndex.length != 0) {
      CommonUtils.changeLocale(store, int.parse(localeIndex));
    } else {
      CommonUtils.curLocale = store.state.platformLocale;
      store.dispatch(RefreshLocaleAction(store.state.platformLocale));
    }

    return new DataResult(res.data, (res.result && (token != null)));
  }

2.5 引入 event_bus

  • 由于 HomePage 是最原始/基础的 page ,所以我们可以吧网络请求的 Toast 显示在这个页面上来
  • 这样无论是哪个模块发生问题,HomePage 监听到后都能统一的显示 Toast
  • 很明显这是一个多对一的情形(多个发送方对一个接收方 HomePage),而且发送事件的逻辑是分散在不同功能模块中的,所以我们不要采用 event_bus(事件总线来实现)

如果还不熟悉 event_bus 的同学,可以先看这篇博客 https://blog.csdn.net/qq_43377749/article/details/115050851?spm=1001.2014.3001.5501

2.5.1 实例化 eventbus
  • 使用 event_bus 进行事件的发送和接收都是通过eventBus 对象进行的
  • 所以我们需要先实例化一个 eventBus 对象
  • 为了方便管理,我们先新建一个文件 index.dart 来用于管理项目中的 eventBus 对象
  • 具体实例化过程如下
代码语言:javascript
复制
import 'package:event_bus/event_bus.dart';

EventBus eventBus = new EventBus();
2.5.2 定义消息 event 对象
  • 在传递网络请求结果的事件时,我们将其内容封装在一个对象中传递
  • 通常情况下我们只需要在请求错误时,向用户反馈结果
  • 所以这里我们只需封装一个 HttpErrorEvent 对象(当然如果需要,我们也可以添加更多的类型对象)
  • 这里我们新建一个类:http_error_event.dart 来专门管理相关对象
代码语言:javascript
复制
class HttpErrorEvent {
  final int code;

  final String message;

  HttpErrorEvent(this.code, this.message);
}
2.5.3 创建监听器
  • 我们需要为 HomePage 建立一个监听器,来监听传来的各种事件
  • 这里一般采用混合注入的方式
  • 首先我们采用 mixin 方式建立,同时让他 on State
代码语言:javascript
复制
mixin HttpErrorListener on State<FlutterReduxApp> {
  StreamSubscription stream;

  ///这里为什么用 _context 你理解吗?
  ///因为此时 State 的 context 是 FlutterReduxApp 而不是 MaterialApp
  ///所以如果直接用 context 是会获取不到 MaterialApp 的 Localizations 哦。
  BuildContext _context;

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

    ///Stream演示event bus
    stream = eventBus.on<HttpErrorEvent>().listen((event) {
      errorHandleFunction(event.code, event.message);
    });
  }

  @override
  void dispose() {
    super.dispose();
    if (stream != null) {
      stream.cancel();
      stream = null;
    }
  }

  ///网络错误提醒
  errorHandleFunction(int code, message) {
    switch (code) {
      case Code.NETWORK_ERROR:
        showToast(GSYLocalizations.i18n(_context).network_error);
        break;
      case 401:
        showToast(GSYLocalizations.i18n(_context).network_error_401);
        break;
      case 403:
        showToast(GSYLocalizations.i18n(_context).network_error_403);
        break;
      case 404:
        showToast(GSYLocalizations.i18n(_context).network_error_404);
        break;
      case 422:
        showToast(GSYLocalizations.i18n(_context).network_error_422);
        break;
      case Code.NETWORK_TIMEOUT:
        //超时
        showToast(GSYLocalizations.i18n(_context).network_error_timeout);
        break;
      case Code.GITHUB_API_REFUSED:
        //Github API 异常
        showToast(GSYLocalizations.i18n(_context).github_refused);
        break;
      default:
        showToast(GSYLocalizations.i18n(_context).network_error_unknown +
            " " +
            message);
        break;
    }
  }

  showToast(String message) {
    Fluttertoast.showToast(
        msg: message,
        gravity: ToastGravity.CENTER,
        toastLength: Toast.LENGTH_LONG);
  }
}
2.5.4 发送事件
  • 发送消息时只要调用 eventBus.fire(…) 即可
  • 参数是需要传递的消息对象,这里也就是 HttpErrorEvent
  • 带这个项目中,使用的部分诸如
代码语言:javascript
复制
  static errorHandleFunction(code, message, noTip) {
    if (noTip) {
      return message;
    }
    if(message != null && message is String && (message.contains("Connection refused") || message.contains("Connection reset"))) {
      code = GITHUB_API_REFUSED;
    }
    eventBus.fire(new HttpErrorEvent(code, message));
    return message;
  }
2.5.5 接受事件
  • 消息发送后,经过过滤器等步骤的传递
  • 最后会传递到上面‘监听器’的 listen 方法下
  • 再由 listen 的回调进行后续操作(比如这个项目中,监听器是捆绑在 _HomePage 上的,因此可以在页面上显示 Toast 等等)
代码语言:javascript
复制
  @override
  void initState() {
    super.initState();

    ///Stream演示event bus
    stream = eventBus.on<HttpErrorEvent>().listen((event) {
      errorHandleFunction(event.code, event.message);
    });
  }

  ///网络错误提醒
  errorHandleFunction(int code, message) {
    switch (code) {
      case Code.NETWORK_ERROR:
        showToast(GSYLocalizations.i18n(_context).network_error);
        break;
      case 401:
        showToast(GSYLocalizations.i18n(_context).network_error_401);
        break;
        // ...
      default:
        showToast(GSYLocalizations.i18n(_context).network_error_unknown +
            " " +
            message);
        break;
    }
  }

  showToast(String message) {
    Fluttertoast.showToast(
        msg: message,
        gravity: ToastGravity.CENTER,
        toastLength: Toast.LENGTH_LONG);
  }
2.5.6 完整代码

三、总结

  • 限于篇幅原因,这里就不展开讲了,后续会出一个相关的视频进行更详细的解析 bilibili@黎明韭菜
  • 设计一个完美的程序入口不是件容易的事情,也希望大家有更完美的方案能在评论区分享
  • 最后感谢大家的三连或者关注支持,我们下期博客再见
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021/03/24 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、前言
  • 二、main.dart
    • 2.1 异常捕获 - runZoned
      • 2.2 错误页展示 - ErrorWidget
        • 2.3 数据共享 - InheritedWidget
          • 2.3.1 共享的数据
          • 2.3.2 封装与管理 ConfigWrapper
          • 2.3.3 绑定数据与视图 _InheritedConfig
          • 2.3.4 获取数据与更新
        • 2.4 页面编写 MaterialApp
          • 2.4.1 页面独立
        • 2.5 页面更新 flutter_redux
          • 2.4.1 创建 store
          • 2.4.2 创建 Reducer
          • 2.4.3 创建 State
          • 2.4.4 拦截器 middleware
          • 2.4.5 全局注册
          • 2.4.6 注册更新监听
          • 2.4.7 触发
        • 2.5 引入 event_bus
          • 2.5.1 实例化 eventbus
          • 2.5.2 定义消息 event 对象
          • 2.5.3 创建监听器
          • 2.5.4 发送事件
          • 2.5.5 接受事件
          • 2.5.6 完整代码
      • 三、总结
      相关产品与服务
      事件总线
      腾讯云事件总线(EventBridge)是一款安全,稳定,高效的云上事件连接器,作为流数据和事件的自动收集、处理、分发管道,通过可视化的配置,实现事件源(例如:Kafka,审计,数据库等)和目标对象(例如:CLS,SCF等)的快速连接,当前 EventBridge 已接入 100+ 云上服务,助力分布式事件驱动架构的快速构建。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档