通过对flutter的基础学习,我用flutter做了一个功能比较小的app。通过这个app,我总结一下在用flutter开发过程中的一些问题。


一、基础架构


以上是我的文件目录:

1、app.dart

flutter的入口方法是main(),因为我这里使用到了多环境配置,所以在environment中我建立了3个文件:

分别对应:测试版本、预发版本、线上版本。
以main_dev.dart代码举例:

void main() {
  //环境配置
  AppConfig.currentChannel = APPBuildConfig.DEV;
  appMain();
}

通过AppConfig记录环境变量,然后调用appMain(),而appMain就是app.dart 中写的伪入口方法。

//入口
void appMain() {
  //路由配置
  routerConfig();
  //android配置(主要设置Android头部的导航栏透明)
  androidConfig();
  WidgetsFlutterBinding.ensureInitialized();
  //方向配置
  SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp])
      .then((_) {
    runApp(MyApp());
  });
}

那么我就把app.dart当作了伪入口来使用了。

2、config


主要根据不同环境做一些公用的配置,比如baseurl,h5的固定地址,是否打印log 等等。

3、constants

主要用来设置一些公用的常量,比如 http的请求api,颜色值等等。

4、manager

根据单一职责原则,定义比如用户管理类,请求管理类等管理类。

5、base


可以看到,对base层的封装,主要采用了类似android的常规mvp的设计模式。

6、utils 工具类

比如颜色rgb和16进制互转的工具类、屏幕相关计算的工具类、时间转行的工具类等等。

7、router 路由

路由的管理类

8、widget

比如loading、默认页、input封装 等公用的组件。

9、module

对应的每一个业务模块

10、common

公用的业务模块

11、generated和l1on

多语言配置

二、base层

不管开发哪个端,工厂设计模式都是必不可少的。也比如oc有分类,swift有协议扩展,可以更多的去取代继承方案。但是对于dart来说,更多的是面向对象的思想,并且和android非常类似。
所以我对base_page的封装基本上是参考了android的BaseActivity的封装。而base_page其实就是一个BasePageState,是一个抽象类。下面具体说下base层的封装:

1、mvp

首先我创建一个BasePageState这个抽象类,同时继承了State。作为每一个StatefulWidget中state的基类。
然后创建一个BasePresenter,作为业务处理的基类。
最后创建一个IBaseView抽象类作为BasePageState的接口,供presenter向pageState通信。

其中BasePageState的定义:

abstract class BasePageState<V extends StatefulWidget, T extends BasePresenter>
    extends State<V>
    with WidgetsBindingObserver, RouteAware
    implements IBaseView {
    }

通过泛型指定StatefulWidget 和 BasePresenter,然后实现IBaseView接口的方法。


abstract class IBaseView {

  BuildContext buildContext();
  ///pageLoading
  void showPageLoading();
  void showPageLoadingBy(bool isCanTouchOutSide);
  void hiddenPageLoading();
  ///commitLoading
  void showCommitLoading(String msg);
  void hiddenCommitLoading();
  ///pageState
  void showPageState(TKPageState state,{String msg,EdgeInsets margin,bool isCanTouch});
  ///reload
  void onReload();
  ///call back
  void callBack(bool success,{String identer});
  ///only refresh
  void refreshData(bool isPullRefresh);
}

2、基本组件集成

在BasePageState类中的build方法如下:

 ///MARK: build
  @override
  Widget build(BuildContext context) {
    if (AppConfig.lifePrint) {
      print("life =>" + _getClassName() + ":build");
    }
    return Scaffold(
      backgroundColor:bgColor,
      resizeToAvoidBottomInset: false,
      appBar: isFullScreen() ? null : buildAppBar("", "icon_w_back"),
      body:body(),
    );
  }

分发一个body()方法,在body()方法中去集成loading组件、默认页等基本组件。

其中mustBuildBoy(),是一个抽象方法,子类必须实现的。通过mustBuildBoy来设置子页面。

3、生命周期

我们先来看下flutter state的基本生命周期:

大致可以分为三个状态:(1)初始化 (2)状态改变 (3)销毁
这些生命周期在大部分的场景下,是满足我们的开发需求的。但是像ios中的viewDidAppear,viewDidDisappear 或者 android中的onResume、onPaused的生命周期方法并没有提供呢。
由于我项目整体是使用了fluro的路由框架。
那么我通过在BasePageState中实现fluro这个库中RouteAware的抽象类的方法

abstract class RouteAware {
  /// Called when the top route has been popped off, and the current route
  /// shows up.
  void didPopNext() { }

  /// Called when the current route has been pushed.
  void didPush() { }

  /// Called when the current route has been popped off.
  void didPop() { }

  /// Called when a new route has been pushed, and the current route is no
  /// longer visible.
  void didPushNext() { }
}

其中didPopNext,比如从A push到B,再从B回到A的时候,会调用。那么就可以满足再次回到A的时候去做一些刷新工作的需求。
那么还有一个比较常见的场景:
点击app底部tabbar进行页面切换的时候,比如android是切换fragment,ios是切换UIViewController,它们都有相应的生命周期方法可以监听到切换显示的回调。那么flutter并没有提供,于是这里我通过eventBus进行了切换通知。这里可能还有更多好的方案,以后再探索了。


三、EventBus

在ios中我们做通知的时候,系统提供了NSNotificationCenter。在android中,系统提供了BroadcastReceiver,同时也经常会使用eventbus。
那么在flutter中做一对多的通知时,我们有哪些方案呢?比如有类似RN的redux,flutter自己的Provider。这些做前端的同学可能会比较熟悉。那么对于我这种移动端出身的来说,我就先尝试使用EventBus来做了。其用法也很简单,网上随便都能查到。

四、Key

我目前的项目中只用到了GlobalKey,使用GlobalKey的目的是为了能够访问到部件的state,从而进行多处操作。
这里后面具体分析Key底层原理以及具体的作用。


五、多语言

1、首先安装Flutter Intl插件,会在pubspec.yaml中增加:

flutter_intl:
  enabled: true

2、会在lib目录下增加 generated 和 l10n两个包

3、可以通过Tools -> Flutter Intl -> Add Locale 增加语言

我这里增加了中文和英文。

4、设置国际化语言

 @override
  Widget build(BuildContext context) {
    return MaterialApp(
      navigatorKey: appNavigatorKey,
      localizationsDelegates: [
        GlobalMaterialLocalizations.delegate, // 指定本地化的字符串和一些其他的值
        GlobalCupertinoLocalizations.delegate, // 对应的Cupertino风格
        GlobalWidgetsLocalizations.delegate, // 指定默认的文本排列方向, 由左到右或由右到左
        S.delegate
      ],
      locale: TKLocalizations.shared().locale,
      supportedLocales: [Locale("en"), Locale("zh")],
      //页面监听
      navigatorObservers: [routeObserver],
      title: 'xxx',
      color: bgColor,
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        backgroundColor: Color.fromRGBO(247, 248, 250, 1),
        primarySwatch: createMaterialColor(mainColor),
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: Scaffold(
        body: SplashPage(),
      ),
    );
  }

5、使用

在需要配置国际化的地方调用S.of(context).key即可.
没有context 时可以使用S.current.key

6、设置默认显示语言和切换语言

由上面的代码可以看到,通过 locale: TKLocalizations.shared().locale, 来设置默认语言。
TKLocalizations是我们封装的一个语言管理类:

class TKLocalizations {

  //英文
  static const enLocale = Locale("en");
  //中文
  static const zhLocale = Locale("zh");

  Locale locale;
  String language;

  factory TKLocalizations() => shared();
  static  TKLocalizations _instance;

  // 私有构造函数
  TKLocalizations._() {
    // 具体初始化代码
    this.locale = zhLocale;
    switchLanguage(locale);
  }

  void switchLanguage(Locale locale) {
    if (locale == enLocale) {
      this.language = "English";
    } else if (locale == zhLocale) {
      this.language = "简体中文";
    }
  }

  static TKLocalizations shared(){
    if(_instance == null){
      _instance = TKLocalizations._();
    }
    return _instance;
  }
}

那么如果要实现切换语言,需要把MyApp设置成StatefulWidget。然后监听语言管理类中语言的变化,最后setState()刷新整体app。


六、路由


七、动画


八、CustomClipper


九、tabbar优化


十、网络请求dio


十一、三方库总结


十二、android坑点