Flutter了解之入门篇3(路由管理、资源管理)

目录
  1. 路由管理(页面跳转)
  2. 资源管理

1. 路由管理(页面跳转)

路由Route
  在移动开发中通常指页面(iOS的UIViewController、Android的Activity)。

导航器Navigator(路由管理)
  管理路由之间如何跳转。
  维护一个路由栈,路由入栈操作(push)会打开新页面,路由出栈操作(pop)会关闭页面,路由管理主要是指如何管理路由栈。
/*
嵌套导航器
  比如Tabbar,每个Tab内都有一个独立的导航器。
  应用里的所有导航器为树状结构,有一个根导航器,同一节点下的各兄弟并行导航。
*/
1. 跳转到下一页面(Navigator.push)
  // push方法返回值类型为Future。
  Navigator.push(
    context,
    new MaterialPageRoute(builder: (context) => new HelloWidget()),  // 需要一个Route
  );
  等价(第一个参数是context都可以这样等价替换)
  Navigator.of(context).push(
    new MaterialPageRoute(builder: (context) => new HelloWidget()),
  );
2. 返回(Navigator.pop)
  返回到上一页面
    Navigator.pop(context);
  返回到根页面
    Navigator.of(context,rootNavigator:true).pop();

示例(在模版计数器示例中,做如下修改)

1. 创建一个新路由
class NewRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("New route"),
      ),
      body: new Center(
        child: new RaisedButton(
          onPressed: () {
            Navigator.pop(context);
          },
          child: new Text('Go back!'),
        ),
      ),
    );
  }
}
2. 在_MyHomePageState.build方法中添加一个按钮:
  Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
      ... // 省略无关代码
      FlatButton(
         child: Text("open new route"),
         textColor: Colors.blue,
         onPressed: () {
          // 导航到新路由   
          Navigator.push( context,
           MaterialPageRoute(builder: (context) {
              return NewRoute();
           }));
          },
         ),
       ],
 )
  1. Navigator导航器

通过栈来管理路由,提供了打开和退出路由的方法。
通常当前屏幕显示的页面是栈顶路由。

1. Future push(BuildContext context, Route route)
  将指定路由入栈(打开新页面)。
  返回值是一个Future对象,用以接收新路由出栈(页面关闭)时的返回数据。
2. bool pop(BuildContext context, [result])
  将栈顶路由出栈(关闭当前页)。
  result为页面关闭时返回给上一页面的数据。
3. 其他方法
  Navigator.replace、Navigator.popUntil
  1. MaterialPageRoute (继承自PageRoute抽象类)
  MaterialPageRoute({
    // 构建路由页,返回值是一个widget。
    WidgetBuilder builder,  
    // 路由页的配置信息(路由页名称、是否首页)
    RouteSettings settings,
    // 设置为false后,当入栈一个新路由后会释放内存中原来的路由页。
    bool maintainState = true,
    // 是否全屏
    // 在iOS中,如果为true新页面会从屏幕底部滑入(而不是水平方向)。
    bool fullscreenDialog = false,
  })
/*
    Android当打开新页面时,新的页面会从屏幕底部滑动到屏幕顶部;当关闭页面时,当前页面会从屏幕顶部滑动到屏幕底部后消失,同时上一个页面会显示到屏幕上。
    iOS当打开页面时,新的页面会从屏幕右侧边缘一致滑动到屏幕左边,直到新页面全部显示到屏幕上,而上一个页面则会从当前屏幕滑动到屏幕左侧而消失;当关闭页面时,正好相反,当前页面会从屏幕右侧滑出,同时上一个页面会从屏幕左侧滑入。
    可以针对不同平台,实现动画风格一致的路由切换动画。
*/

PageRoute类
  占有整个屏幕空间的一个模态路由页面,定义了路由构建及切换时过渡动画的相关接口及属性。
  如果想自定义路由切换动画,可以继承PageRoute来实现。
  1. 路由传值(跳转时通常需要传递数据)

例如:打开商品详情页时需要一个商品id展示对应商品详情;填写订单时需要选择收货地址,打开地址选择页并选择地址后,将所选地址返回给订单页。

示例(双向传递数据)

class RouterTestRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: RaisedButton(
        onPressed: () async {
          var result = await Navigator.push(
            context,
            MaterialPageRoute(
              builder: (context) {
                return TipRoute(
                  // 路由参数
                  text: "hello world",
                );
              },
            ),
          );
          // 点击返回按钮,命令行输出 路由返回值: 我是返回值。点击箭头,命令行输出 路由返回值: null。
          print("路由返回值: $result");
        },
        child: Text("打开提示页"),
      ),
    );
  }
}
// 接受一个文本参数用来展示。点击“返回”按钮后返回上一个路由时 传递了文本数据。
class TipRoute extends StatelessWidget {
  TipRoute({this.text});
  final String text;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("提示"),
      ),
      body: Padding(
        padding: EdgeInsets.all(18),
        child: Center(
          child: Column(
            children: <Widget>[
              Text(text),
              RaisedButton(
                onPressed: () => Navigator.pop(context, "我是返回值"),
                child: Text("返回"),
              )
            ],
          ),
        ),
      ),
    );
  }
}
  1. 命名路由(有名字的路由)

给路由起名字后,可以通过路由名来打开新路由。

通过路由名打开新路由时,应用会根据路由名在【路由表】中查找并调用对应的WidgetBuilder回调函数生成并返回路由。

路由表的定义如下:
  // MaterialApp的一个属性。
  // key为路由名,value是个builder回调函数(用于生成并返回相应的路由)。
  Map<String, WidgetBuilder> routes; 

好处:
    1. 语义化更明确。
    2. 维护更方便。
      如果使用匿名路由:不仅需要import新路由页的dart文件,而且代码非常分散不方便管理。还需要知道页面的构建方式,且变化后需要涉及该页面的跳转都需要修改,代码耦合性严重。
    3. 可以通过onGenerateRoute(路由拦截器)做一些全局的路由跳转前的处理逻辑。
使用:

1. 首先注册一个路由表,提供对应关系(路由名--->路由)
在MaterialApp中添加routes属性:
  MaterialApp(
    title: ProjectConfig.packageInfo.appName,
    theme: ProjectTheme.theme,
    routes: {
      '/':(context)=>BootstrapPage(),
      '/login':(context)=>LoginPage(),
      '/register':(context)=>RegisterPage(),
      '/tab':(context)=>TabPage(),
    },
    // navigatorKey: // GlobalKey<NavigatorState>对象,全局存储导航的状态,
  ),

2. 通过路由名打开新路由页
// 第三个参数可选,用于传递值。
// 在导航栏上会显示返回按钮
Navigator.pushNamed(context, "/new_page");

3. 在新路由页可以
使用替换路由打开新页面:
  // 点击返回按钮或者pop后会返回到“上上页”,即调用Navigator.pushNamed的页面
  Navigator.pushReplacementNamed(context, "/new_page");
返回到上一页面:
  Navigator.pop(context);
返回根路由:
  Navigator.of(context,rootNavigator:true).pop();
返回根路由:
Navigator.pushAndRemoveUntil(
  context, 
  new MaterialPageRoute(builder: (context)=>new HomeRoute()), 
  (route)=>route==null
);

4. 路由生成钩子(onGenerateRoute)
  1. MaterialApp的一个属性,只会对命名路由生效。
  2. 可用来统一处理跳转逻辑(参数传递、无效路由、权限访问)。
  3. 当调用Navigator.pushNamed(...)打开命名路由时,如果指定的路由名在路由表中已注册,则会调用路由表中的builder函数来生成路由组件;如果路由表中没有注册,才会调用onGenerateRoute来生成路由。

5. 命名路由参数传递(早期版本并不支持)
  1. 注册路由表
  routes:{
     "new_page":(context) => NewRoute(),
  } ,
  2. 传递值
  Navigator.of(context).pushNamed("new_page", arguments: "hi");
  3. 获取值
  方式1:(NewRoute的build方法中)
  Widget build(BuildContext context) {
      // 获取路由参数  
      var args=ModalRoute.of(context).settings.arguments;
      //...省略无关代码
  }
  方式2: 
  如果NewRoute接受一个text 参数,在不改变NewRoute源码的前提下,使用路由名来打开它需要:
  routes: {
    "new_page": (context){
       return TipRoute(text: ModalRoute.of(context).settings.arguments);
    },
  },
/*
看一下pushNamed方法和pop方法的实现:
Future<T?> pushNamed<T extends Object?>(
  String routeName, {
  Object? arguments,
}) {
  return push<T>(_routeNamed<T>(routeName, arguments: arguments)!);
}
void pop<T extends Object?>([ T? result ]) {
  ...
}
*/

示例(创建了一个路由管理类来集中管理Route。使用onGenerateRoute来统一处理跳转)

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '../app.dart';
import '../login.dart';
import '../not_found.dart';
import '../splash.dart';
class RouterTable {
  static String splashPath = 'splash';
  static String loginPath = 'login';
  static String homePath = '/';
  static String notFoundPath = '404';
  static Map<String, WidgetBuilder> routeTables = {
    // 404页面
    notFoundPath: (context) => NotFound(),
    // 启动页
    splashPath: (context) => Splash(),
    // 登录
    loginPath: (context) => LoginPage(),
    // 首页
    homePath: (context) => AppHomePage(),
  };
  // 路由拦截
  static Route onGenerateRoute<T extends Object>(RouteSettings settings) {
    return CupertinoPageRoute<T>(
      settings: settings,
      builder: (context) {
        String name = settings.name;
        if (routeTables[name] == null) {
          name = notFoundPath;
        }
        Widget widget = routeTables[name](context);
        return widget;
      },
    );
  }
}

====================
使用

import 'package:flutter/material.dart';
import 'routers/router_table.dart';
void main() {
  runApp(MyApp());
}
class MyApp extends StatelessWidget {
  final GlobalKey navigationKey = GlobalKey<NavigatorState>();
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      navigatorKey: navigationKey,
      onGenerateRoute: RouterTable.onGenerateRoute,
      initialRoute: RouterTable.splashPath,
    );
  }
}

示例(routes注册路由表、initialRoute首页)

MaterialApp(
  title: 'Flutter Demo',
  theme: ThemeData(
    primarySwatch: Colors.blue,
  ),
  // 注册路由表
  routes:{
   "new_page":(context) => NewRoute(),
    ... // 省略其它路由注册信息
  } ,
  home: MyHomePage(title: 'Flutter Demo Home Page'),
);

修改FlatButton的onPressed,修改跳转逻辑为:
onPressed: () {
  Navigator.pushNamed(context, "/new_page");
  //Navigator.push(context,
  //  MaterialPageRoute(builder: (context) {
  //  return NewRoute();
  //}));  
},
/*
也可以将首页注册为命名路由

MaterialApp(
  title: 'Flutter Demo',
  // 设置初始路由:将名为"/"的路由作为应用的首页(默认就是‘/’,不用再次赋值)。
  // 设置initialRoute后不再需要设置home。
  initialRoute:"/", 
  theme: ThemeData(
    primarySwatch: Colors.blue,
  ),
  // 注册路由表
  routes:{
   "/new_page":(context) => NewRoute(),
   "/":(context) => MyHomePage(title: 'Flutter Demo Home Page'), //注册首页路由
  } 
);
*/

示例(onGenerateRoute统一设置权限)

MaterialApp(
  // 在该回调中进行统一的权限控制
  // Route<dynamic> Function(RouteSettings settings)
  onGenerateRoute:(RouteSettings settings){
      return MaterialPageRoute(builder: (context){
           String routeName = settings.name;  
       // 如果访问的路由页需要登录,但当前未登录,则直接返回登录页路由,
       // 引导用户登录;其它情况则正常打开路由。
     }
   );
  }
);

示例(onGenerateRoute统一处理传参)

import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
  // 通常放在单独的文件中
  final routes={
    '/page1':(context)=>HomePageWidget(),
    '/page2':(context,{arguments})=>MyPageWidget(arguments:arguments),
  };
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        primarySwatch: Colors.lightBlue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      initialRoute:"/page1", 
      // 通常将该方法抽离和routes位于单独的文件中
      onGenerateRoute: (RouteSettings settings){
        final String name = settings.name;  // 路由名
        final Function pageContentBuilder = this.routes[name];
        if(settings.arguments!=null){
          final Route route=MaterialPageRoute(
            builder: (context)=>pageContentBuilder(context,arguments:settings.arguments),
          );
          return route;
        }else{
          final Route route=MaterialPageRoute(
            builder: (context)=>pageContentBuilder(context),
          );
          return route;
        }
      },
    );
  }
}
class HomePageWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('hello'),
      ),
      body: new Text('hello'),
      floatingActionButton: FloatingActionButton(
        onPressed: _pushNewPage,
        child: Icon(Icons.add),
      ), 
    );
  }
  void _pushNewPage() {
    Navigator.of(context).pushNamed("/page2", arguments: {
      "id":110,
    });
  }
}
class MyPageWidget extends StatelessWidget {
  final arguments;
  MyPageWidget({this.arguments});
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('MyPageWidget'),
      ),
      body: new Text('${arguments!=null?arguments['id']:'hello'}'),
    );
  }
}
/*
class MyPageWidget extends StatefulWidget {
  final arguments;
  MyPageWidget({Key key,this.arguments}):super(key:key);
  @override
 _MyPageWidgetState createState()=>_MyPageWidgetState(arguments:arguments);
}
class _MyPageWidgetState extends State<MyPageWidget> {
  final arguments;
  _MyPageWidgetState({this.arguments});
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('MyPageWidget'),
      ),
      body: new Text('${arguments!=null?arguments['id']:'hello'}'),
    );
  }
}
*/

示例(onGenerateRoute)

Navigator.push( 
  context,
  MaterialPageRoute(builder: (context) {
    return HelloPageWidget();
  })
);
class HelloPageWidget extends StatelessWidget{
  final _navigatorKey=GlobalKey<NavigatorState>();
  Future bool _onWillPopFunc() async{
    final maybePop=await _navigatorKey.currentState.maybePop();
    return Future.value(maybePop);
  }
  @override
  Widget build(BuildContext context) {
    return WillPopScope(  // 使用WillPopScope来使内层导航器响应Android实体键返回。
      onWillPop: _onWillPopFunc,
      child: Navigator(
        key: _navigatorKey,
        initialRoute: '/helloPage',
        onGenerateRoute: (settings){
          WidgetBuilder builder:{
            switch(settings.name){  // 路由名
              case '/register':
                builder=(_)=>RegisterPage();
              default:
                throw Exception('error');
            }
            return MaterialPageRoute(
              builder: builder,
              settings: settings,
            );
          }
        },
      ),
    );
  }
}
  1. fluro路由库

使用

第1步. 创建FluroRouter路由实例

class RouterManager {
  static String splashPath = '/';
  static String loginPath = '/login';
  static String homePath = '/home';
  static String dynamicPath = '/dynamic';
  static String dynamicDetailPath = '$dynamicPath/:id';
  //
  static FluroRouter router;
  static void initRouter() {
    if (router == null) {
      router = FluroRouter();
      defineRoutes();
    }
  }
  static var loginHandler =
      Handler(handlerFunc: (BuildContext context, Map<String, dynamic> params) {
    return LoginPage();
  });
  static var dynamicDetailHandler =
      Handler(handlerFunc: (BuildContext context, Map<String, dynamic> params) {
    return DynamicDetailPage(params['id'][0]);
  });
  static var splashHandler =
      Handler(handlerFunc: (BuildContext context, Map<String, dynamic> params) {
    return Splash();
  });
  static var homeHandler =
      Handler(handlerFunc: (BuildContext context, Map<String, dynamic> params) {
    return AppHomePage();
  });
  static var notFoundHandler =
      Handler(handlerFunc: (BuildContext context, Map<String, dynamic> params) {
    return NotFound();
  });
  // Fluro的路由匹配次序是按照定义路由的先后次序进行匹配的,因此需要把更具体的路由放置在范围匹配的前面
  static void defineRoutes() {
    router.define(splashPath, handler: splashHandler);
    router.define(homePath, handler: homeHandler);
    router.define(loginPath, handler: loginHandler);
    router.define(dynamicDetailPath, handler: dynamicDetailHandler);
    // 路由不存在时,设置错误路由处理器
    router.notFoundHandler = notFoundHandler;
  }
}
/*
看一下Handler类的定义:

class Handler {
  Handler({this.type = HandlerType.route, required this.handlerFunc});
  // route(默认)、function。
  final HandlerType type;  
  // 返回值为将要跳转的页面。parameters为路由参数。
  // typedef Widget? HandlerFunc(BuildContext? context, Map<String, List<String>> parameters);
  final HandlerFunc handlerFunc;  
}
*/
第2步. 根Widget中

在build方法开始位置加上:
  RouterManager.initRouter();
在MaterialApp中把onGenerateRoute设置为RouterManager.router.generator。
第3步. 跳转

1. RouterManager.router.navigateTo(context, RouterManager.loginPath);
    直接跳转(无参数)
2. RouterManager.router.navigateTo(context, '${RouterManager.dynamicPath}/$id?event=a&event=b')
    跳转(带参数)
3. RouterManager.router.navigateTo(context, RouterManager.homePath, clearStack: true);
    清除路由堆栈跳转:即跳转后的页面作为根页面(没有返回按钮)。

设置转场动画

enum TransitionType {   // 转场动画类型
  native,      // 原生形式,和原生的保持一致(默认)
  nativeModal, // 原生模态跳转
  inFromLeft,  // 从左滑入
  inFromTop,   // 从顶部滑入
  inFromRight, // 从右滑入
  inFromBottom,// 从底部滑入
  fadeIn,      // 渐现
  custom,      // 自定义,需要配合 transitionBuilder 使用
  material,    // 安卓风格跳转
  materialFullScreenDialog, // 安卓风格全屏对话框(左上角带有关闭按钮)
  cupertino,   // iOS 风格跳转
  cupertinoFullScreenDialog,// iOS风格全屏对话框(左上角带有关闭按钮)
  none,        // 无转场动画
}

2种设置方式
  1. 定义路由处理器Handler时设置transitionType参数。
  router.define(transitionPath,handler: transitionHandler,transitionType: TransitionType.inFromBottom);
  2. 使用navigateTo跳转时设置transition参数。
  RouterManager.router.navigateTo(context, RouterManager.transitionPath,transition: TransitionType.inFromRight,transitionDuration: Duration(milliseconds: 1000));
自定义转场动画

先看看fluro转场部分源码是怎么实现转场动画的:
  // 根据不同的枚举类型返回不同的动画。
  // TransitionType.fadeIn使用的是Flutter自带的FadeTransition。上下左右滑入使用的是Flutter自带的SlideTransition。
  RouteTransitionsBuilder _standardTransitionsBuilder(
      TransitionType? transitionType) {
    return (BuildContext context, Animation<double> animation,
        Animation<double> secondaryAnimation, Widget child) {
      if (transitionType == TransitionType.fadeIn) {
        return FadeTransition(opacity: animation, child: child);
      } else {
        const Offset topLeft = const Offset(0.0, 0.0);
        const Offset topRight = const Offset(1.0, 0.0);
        const Offset bottomLeft = const Offset(0.0, 1.0);
        Offset startOffset = bottomLeft;
        Offset endOffset = topLeft;
        if (transitionType == TransitionType.inFromLeft) {
          startOffset = const Offset(-1.0, 0.0);
          endOffset = topLeft;
        } else if (transitionType == TransitionType.inFromRight) {
          startOffset = topRight;
          endOffset = topLeft;
        } else if (transitionType == TransitionType.inFromBottom) {
          startOffset = bottomLeft;
          endOffset = topLeft;
        } else if (transitionType == TransitionType.inFromTop) {
          startOffset = Offset(0.0, -1.0);
          endOffset = topLeft;
        }
        return SlideTransition(
          position: Tween<Offset>(
            begin: startOffset,
            end: endOffset,
          ).animate(animation),
          child: child,
        );
      }
    };
  }
自定义转场动画只需要transition设置为TransitionType.custom,然后transitionBuilder返回相应动画就可以了。
/*
Flutter常用转场动画(继承自AnimatedWidget)
  FadeTransition(透明)
  SlideTransition(上下左右滑入)
  RotationTransition(旋转)
  ScaleTransition(缩放)

看一下RotationTransition的定义:
  RotationTransition({
    Key? key,
    // 旋转角度。推荐的起始值0.2至0.3之间,结束值为0表示回到正常位置。起始值如果为负,则是顺时针;如果为正则是逆时针。
    required Animation<double> turns, 
    this.alignment = Alignment.center,  // 旋转中心点
    this.child,
  })  : assert(turns != null),
        super(key: key, listenable: turns);
*/

路由拦截

fluro没有提供类似onGenerateRoute方法来在每次跳转时进行路由拦截。
路由拦截(两种方式)
  1. 在定义路由时,对于未授权的路由地址跳转到403未授权页面。
  2. 继承FluroRouter类,重写navigateTo跳转方法拦截路由。

示例(自定义转场动画:逆时针围绕中心旋转)

RouterManager.router.navigateTo(
  context,
  RouterManager.transitionPath,
  transition: TransitionType.custom,
  transitionBuilder:
      (context, animation, secondaryAnimation, child) {
    return RotationTransition(
      turns: Tween<double>(
        begin: 0.25,
        end: 0.0,
      ).animate(animation),
      child: child,
    );
  },
);

示例(自定义转场动画:缩放)

RouterManager.router.navigateTo(
  context,
  RouterManager.transitionPath,
  transition: TransitionType.custom,
  transitionBuilder:
      (context, animation, secondaryAnimation, child) {
    return ScaleTransition(
      scale: Tween<double>(
        begin: 0.5,
        end: 1.0,
      ).animate(animation),
      child: child,
    );
  },
);

示例(自定义转场动画:通过继承AnimatedWidget自定义动画)

class SkewTransition extends AnimatedWidget {
  const SkewTransition({
    Key key,
    Animation<double> turns,
    this.alignment = Alignment.center,
    this.child,
  })  : assert(turns != null),
        super(key: key, listenable: turns);
  Animation<double> get turns => listenable as Animation<double>;
  final Alignment alignment;
  final Widget child;
  @override
  Widget build(BuildContext context) {
    final double turnsValue = turns.value;
    final Matrix4 transform =
        Matrix4.skew(turnsValue * pi * 2.0, turnsValue * pi * 2.0);
    // 返回一个 Transform 对象
    return Transform(
      transform: transform,
      alignment: alignment,
      child: child,
    );
  }
}
RouterManager.router.navigateTo(
  context,
  RouterManager.transitionPath,
  transition: TransitionType.custom,
  transitionBuilder:
      (context, animation, secondaryAnimation, child) {
    return SkewTransition(
      turns: Tween<double>(
        begin: -0.05,
        end: 0.0,
      ).animate(animation),
      child: child,
    );
  },
);

示例(定义路由时拦截)

为了保证路由拦截有效,必须在初始化路由前就通过登录人信息拿到路由白名单。
为了改善用户体验,可以预先明确哪些页面不涉及权限管控(如闪屏页,首页,登录页)。

// 完整路由表
static final routeTable = {
  loginPath: Handler(
      handlerFunc: (BuildContext context, Map<String, dynamic> params) {
    return LoginPage();
  }),
  dynamicDetailPath: Handler(
      handlerFunc: (BuildContext context, Map<String, dynamic> params) {
    return DynamicDetailPage(params['id'][0]);
  }),
  splashPath: Handler(
      handlerFunc: (BuildContext context, Map<String, dynamic> params) {
    return Splash();
  }),
  transitionPath: Handler(
      handlerFunc: (BuildContext context, Map<String, dynamic> params) {
    return TransitionPage();
  }),
  homePath: Handler(
      handlerFunc: (BuildContext context, Map<String, dynamic> params) {
    return AppHomePage();
  }),
};
// 未授权页面处理器
static final permissionDeniedHandler =
    Handler(handlerFunc: (BuildContext context, Map<String, dynamic> params) {
  return PermissionDenied();
});
// 定义路由
// 添加路由时,将路由路径与白名单进行比对,若不在白名单内,则使用未授权路由处理器。
static void defineRoutes({List<String> whiteList}) {
  routeTable.forEach((path, handler) {
    if (whiteList == null || whiteList.contains(path)) {
      router.define(path, handler: handler);
    } else {
      router.define(path,handler: permissionDeniedHandler,transitionType: TransitionType.material);
    }
  });
  router.notFoundHandler = Handler(
      handlerFunc: (BuildContext context, Map<String, dynamic> params) {
    return NotFound();
  });
}

示例(继承FluroRouter类,重写navigateTo方法拦截路由)

如果首页不涉及授权,可以在 App 启动后再获取授权白名单,而不需要在启动时获取,可以降低启动时的任务,加快启动速度和提高用户体验。

import 'package:flutter/material.dart';
import 'package:fluro/fluro.dart';
class PermissionRouter extends FluroRouter {
  List<String> _whiteList;
  set whiteList(value) => _whiteList = value;
  String _permissionDeniedPath;
  set permissionDeniedPath(value) => _permissionDeniedPath = value;
  @override
  Future navigateTo(
    BuildContext context,
    String path, {
    bool replace = false,
    bool clearStack = false,
    bool maintainState = true,
    bool rootNavigator = false,
    TransitionType transition,
    Duration transitionDuration,
    transitionBuilder,
    RouteSettings routeSettings,
  }) {
    String pathToNavigate = path;
    // 如果匹配成功,则返回匹配的路由对象AppRouteMatch,如果没有匹配到则返回 null。
    AppRouteMatch routeMatched = this.match(path);
    // 获取匹配到的路径
    String routePathMatched = routeMatched?.route?.route;
    if (routePathMatched != null) {
      // 设置了白名单且当前路由不在白名单内,更改路由路径到授权被拒绝页面
      if (_whiteList != null && !_whiteList.contains(routePathMatched)) {
        pathToNavigate = _permissionDeniedPath;
      }
    }
    return super.navigateTo(context, pathToNavigate,
        replace: replace,
        clearStack: clearStack,
        maintainState: maintainState,
        rootNavigator: rootNavigator,
        transition: transition,
        transitionDuration: transitionDuration,
        transitionBuilder: transitionBuilder,
        routeSettings: routeSettings);
  }
}
/*
match方法匹配成功返回AppRouteMatch对象(有一个AppRoute类型的route属性)
class AppRoute {
  String route;  // 匹配到的路由路径
  dynamic handler;
  TransitionType? transitionType;
  Duration? transitionDuration;
  RouteTransitionsBuilder? transitionBuilder;
  AppRoute(this.route, this.handler,
      {this.transitionType, this.transitionDuration, this.transitionBuilder});
}
*/
  1. 路由2.0
为了满足
  1. Web端复杂路由的需要
  2. 状态驱动界面的设计理念。界面与行为进行分离,通过更改状态来驱动界面完成既定行为。
因此,路由2.0最关键的地方就是之前的Navigator.push或Navigator.pop方法不见了,界面只是响应用户操作去更改数据状态,而页面路由跳转统一交给了RougterDelegate来完成。

优点
  1. 路由管理和路由解析分离,可以自己定义路由解析类和路由参数配置类,更为灵活。
  2. 路由页面可以动态生成,因此实现动态路由更为简单。
  3. 页面无需管理跳转逻辑,将页面和路由分离解耦,保持状态驱动界面的一致性。
  4. 可以引入状态管理组件来管理整个 App 的路由状态,扩展性更强。

新加入了如下内容:
1. Page
  设置Navigator导航器历史堆栈的不可变对象。
2. Router
  设置Navigator导航器要显示的页面列表。
3. RouteInformationParser
  从路由信息提供者(RouteInformationProvider)获取路由信息并将信息解析转换为用户定义的数据类型。
4. RouterDelegate
  定义路由如何获知App状态改变的行为,以及如何响应这些行为。它的职责就是监听 RouteInformationParser 和 App 状态,然后构建当前页面列表的导航器。
5. BackButtonDispatcher
  向路由通知返回按钮点击事件。

流程
  用户点击跳转,RouteInformationParser解析路径为对应的类,RouterDelegate会调用其setNewRoutePath方法(传入解析的类,设置状态,调用notifyListeners),当 notifyListeners 被调用后会通知Router重建RouterDelegate(调用build,返回一个新导航器),最新页面。
路由2.0

示例

1. 在根widget的build方法中使用MaterialApp.router构建,设置路由委托routerDelegate和路由信息解析器routeInformationParser。

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: '2.0路由',
      routerDelegate: AppRouterDelegate(),
      routeInformationParser: AppRouterInformationParser(),
      // 省略其他代码
    );
  }
}
2. 路由解析类 app_router_path.dart。

import 'package:flutter/cupertino.dart';
// 路由枚举,不同枚举对应不同页面
enum RouterPaths { splash, dynamicList, dynamicDetail, notFound }
class AppRouterConfiguration {  // 路由配置类
  final RouterPaths path;  // 当前路由path路径
  final dynamic state;  // 状态数据state(用于将数据传递到新的页面)
  AppRouterConfiguration(this.path, this.state);
}
// 路由信息解析类,继承自RouteInformationParser<AppRouterConfiguration>
// 当进行路由跳转时就会调用路由解析方法,获取对应的路由配置对象。
class AppRouterInformationParser
    extends RouteInformationParser<AppRouterConfiguration> {
  @override
  Future<AppRouterConfiguration> parseRouteInformation(
      RouteInformation routeInformation) async {
    final String routeName = routeInformation.location;
    switch (routeName) {
      case '/':
        return AppRouterConfiguration(
            RouterPaths.splash, routeInformation.state);
      case '/home':
        return AppRouterConfiguration(
            RouterPaths.dynamicList, routeInformation.state);
      case '/dynamicDetail':
        return AppRouterConfiguration(
            RouterPaths.dynamicDetail, routeInformation.state);
      default:
        return AppRouterConfiguration(
            RouterPaths.notFound, routeInformation.state);
    }
  }
  // 不同的路由枚举返回不同的路由信息对象。
  @override
  RouteInformation restoreRouteInformation(
      AppRouterConfiguration configuration) {
    switch (configuration.path) {
      case RouterPaths.splash:
        return RouteInformation(location: '/', state: configuration.state);
      case RouterPaths.dynamicList:
        return RouteInformation(location: '/home', state: configuration.state);
      case RouterPaths.dynamicDetail:
        return RouteInformation(
            location: '/dynamicDetail', state: configuration.state);
      default:
        return RouteInformation(location: '/404', state: configuration.state);
    }
  }
}
3. 路由委托实现类 router_delegate.dart:

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:home_framework/dynamic_detail.dart';
import 'package:home_framework/models/dynamic_entity.dart';
import 'package:home_framework/not_found.dart';
import 'package:home_framework/routers/app_router_path.dart';
import 'package:home_framework/splash.dart';
import '../dynamic.dart';
// AppRouterDelegate继承自RouterDelegate<AppRouterConfiguration>,实现了ChangeNotifier、PopNavigatorRouterDelegateMixin。
// ChangeNotifier用于增加状态更改监听对象(由底层完成)和通知监听对象进行动作,当有状态改变时应当调用 notifyListeners方法通知所有监听者。
// PopNavigatorRouterDelegateMixin用于管理返回事件,只有一个方法,可以覆盖来自定义返回事件。
class AppRouterDelegate extends RouterDelegate<AppRouterConfiguration>
    with
        ChangeNotifier,
        PopNavigatorRouterDelegateMixin<AppRouterConfiguration> {
  // 用于存储导航器状态的GlobalKey,可以全局获知导航器的当前状态。
  @override
  final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
  // 存储当前路由,发生改变后会触发通知进行路由跳转。
  RouterPaths _routerPath;
  get routerPath => _routerPath;
  set routerPath(RouterPaths value) {
    if (_routerPath == value) return;
    _routerPath = value;
    notifyListeners();
  }
  // 路由状态对象(即路由参数)。
  dynamic _state;
  get state => _state;
  // 启动页是否完成,有启动页时首页是启动页,用于在启动完成后将启动页移除路由表,以便显示实际的首页。
  bool _splashFinished = false;
  get splashFinished => _splashFinished;
  set splashFinished(bool value) {
    if (_splashFinished == value) return;
    _splashFinished = value;
    notifyListeners();
  }
  // 构建路由,通过一个 Navigator 包裹全部路由页面。
  @override
  Widget build(BuildContext context) {
    return Navigator(
      key: navigatorKey,
      pages: _buildPages(),
      onPopPage: _handlePopPage,
    );
  }
  // 用于返回 build 方法所需要的pages参数。
  List<Page<void>> _buildPages() {
    if (_splashFinished) {  // 启动页加载完成
      return [
        MaterialPage(
            // 根据路由枚举匹配对应页面,同时指定自定义处理方法(该页面有几种跳转就需要几个)。
            key: ValueKey('home'),  // 第一个路由是首页。
            child: DynamicPage(_handleDynamicItemChanged)),  // 对应的跳转页面
        if (_routerPath == RouterPaths.splash)  
          MaterialPage(
              key: ValueKey('splash'), child: Splash(_handleSplashFinished)),
        if (_routerPath == RouterPaths.dynamicDetail)
          MaterialPage(
              key: ValueKey('dynamicDetail'), child: DynamicDetail(state)),
        if (_routerPath == RouterPaths.notFound)
          MaterialPage(key: ValueKey('notFound'), child: NotFound()),
      ];
    } else {  //
      return [
        MaterialPage(
            key: ValueKey('splash'), child: Splash(_handleSplashFinished)),
      ];
    }
  }
  // 一些自定义处理方法
  void _handleSplashFinished() {
    _routerPath = RouterPaths.dynamicList;
    _splashFinished = true;
    notifyListeners();
  }
  void _handleDynamicItemChanged(DynamicEntity dynamicEntity) {
    _routerPath = RouterPaths.dynamicDetail;
    _state = dynamicEntity;
    notifyListeners();
  }
  // 覆写了PopNavigatorRouterDelegateMixin的方法
  @override
  Future<bool> popRoute() async {
    return true;
  }
  // 传入路由配置对象进行跳转。
  @override
  Future<void> setNewRoutePath(AppRouterConfiguration configuration) async {
    _routerPath = configuration.path;
    _state = configuration.state;
  }
  // 返回处理方法(build方法用到)
  bool _handlePopPage(Route<dynamic> route, dynamic result) {
    final bool success = route.didPop(result);
    return success;
  }
  // 通过_routerPath和_state构建当前的路由配置对象并返回
  @override
  AppRouterConfiguration get currentConfiguration =>
      AppRouterConfiguration(routerPath, state);
}
4. 由于代码不能再使用 push 和 pop 跳转和返回,因此涉及到这些都需要变更。

启动页
class Splash extends StatefulWidget {
  final Function onFinished;  // 具体实现在AppRouterDelegate类中
  Splash(this.onFinished, {Key key}) : super(key: key);
  @override
  _SplashState createState() => _SplashState(onFinished);
}
class _SplashState extends State<Splash> {
  final Function onFinished;
  _SplashState(this.onFinished);
  bool _initialized = false; 
  //省略其他代码
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    if (!_initialized) {
      _initialized = true;
      Timer(const Duration(milliseconds: 2000), () {
        onFinished();  // 跳转
      });
    }
  }
}
动态列表页
  需要接收一个onItemTapped方法来响应每行元素的点击事件,并把点击的元素作为参数传递。

这种方式暴露了业务的实现,破坏了封装性,而且如果父子元素嵌套过深会导致传递链路过长。
需要使用类似Redux的状态管理器来解耦。

2. 资源管理

Flutter APP安装包中会包含 代码 和 资源(assets) 两部分。

常见assets类型:
  1. 静态数据(json文件)
  2. 配置文件
  3. 图标和图片(JPEG,WebP,GIF,动画WebP / GIF,PNG,BMP和WBMP)

指定assets

和包管理一样,Flutter使用pubspec.yaml来管理资源。

1. assets指定应包含在应用程序中的文件。
2. 每个asset都通过相对于pubspec.yaml文件所在的文件系统路径来标识自身的路径。
3. asset的声明顺序是无关紧要的。
4. asset的实际目录可以是任意文件夹。
5. Asset变体(适配各机型)
  在选择匹配当前设备分辨率的图片时,Flutter会使用到asset变体。未来会将这种机制扩展到本地化、阅读提示等方面。
  构建过程支持“asset变体”:不同版本的asset可能会显示在不同的上下文中。 在pubspec.yaml的assets部分中指定asset路径时,构建过程中,会在相邻子目录中查找具有相同名称的任何文件。这些文件随后会与指定的asset一起被包含在asset bundle中。
6. 在构建期间,Flutter将asset放置到称为asset bundle中,应用程序可以在运行时读取它们(但不能修改)。

示例

flutter:
  assets:
    - assets/my_icon.png
    - assets/background.png

这里的图片文件夹名是assets,也可是imgs(imgs/my_icon.png)

示例2(Asset变体)

如果应用程序目录中有以下文件:
    …/graphics/background.png
    …/graphics/dark/background.png

pubspec.yaml文件中只需包含:
flutter:
  assets:
    - graphics/background.png

graphics/background.png和graphics/dark/background.png 都将会包含在asset bundle中。
前者被认为是main asset (主资源),后者被认为是一种变体(variant)。
  1. 文本(json文件、)
加载文本(2种方法)
  1. 通过rootBundle对象加载
    每个Flutter应用程序都有一个rootBundle对象, 通过它可以轻松访问主资源包,直接使用package:flutter/services.dart中全局静态的rootBundle对象来加载asset。
  2. 通过DefaultAssetBundle加载(建议)
    使用DefaultAssetBundle.of(context) 来获取当前BuildContext的AssetBundle。 
    获取的不是应用程序构建的默认asset bundle,而是使父级widget在运行时动态替换的不同的AssetBundle,这对于本地化或测试场景很有用。

示例(通过rootBundle对象加载)

通常使用DefaultAssetBundle方式来加载,但在widget上下文之外或其它AssetBundle句柄不可用时,则使用rootBundle加载:

import 'dart:async' show Future;
import 'package:flutter/services.dart' show rootBundle;
Future<String> loadAsset() async {
  return await rootBundle.loadString('assets/config.json');
}
  1. 图片
1. 在pubspec.yaml文件指定asset

AssetImage可以将asset的请求逻辑映射到最接近当前设备像素比例(dpi)的asset,内部会自动处理分辨率。为了使这种映射起作用,必须根据特定的目录结构来保存asset:
    …/image.png
    …/Mx/image.png
    …/Nx/image.png
    …etc.
其中M和N是数字标识符,对应于其中包含的图像的分辨率,也就是说,它们指定不同设备像素比例的图片。主资源默认对应于1.0倍的分辨率图片。

示例:
    …/my_icon.png
    …/2.0x/my_icon.png
    …/3.0x/my_icon.png
在设备像素比率为1.8的设备上,.../2.0x/my_icon.png 将被选择。对于2.7的设备像素比率,.../3.0x/my_icon.png将被选择。

/*
如果未在Image widget上指定渲染图像的宽度和高度,那么Image widget将占用与主资源相同的屏幕空间大小。即,如果.../my_icon.png是72px乘72px,那么.../3.0x/my_icon.png应该是216px乘216px; 但如果未指定宽度和高度,它们都将渲染为72像素×72像素(以逻辑像素为单位)。

pubspec.yaml中asset部分中的每一项都应与实际文件相对应,但主资源项除外。当主资源缺少某个资源时,会按分辨率从低到高的顺序去选择 ,也就是说1x中没有的话会在2x中找,2x中还没有的话就在3x中找。
*/
2. 使用AssetImage类加载图片
Widget build(BuildContext context) {
  return new DecoratedBox(
    decoration: new BoxDecoration(
      image: new DecorationImage(
        image: new AssetImage('graphics/background.png'),
      ),
    ),
  );
}

AssetImage 并非是一个widget, 它实际上是一个ImageProvider,有些时候可能期望直接得到一个显示图片的widget,那么可以使用Image.asset()方法,如:
Widget build(BuildContext context) {
  return Image.asset('graphics/background.png');
}
3. 如果要加载依赖包中的图像,必须给AssetImage提供package参数。

示例:
假设应用程序依赖于一个名为“my_icons”的包,它具有如下目录结构:
    …/pubspec.yaml
    …/icons/heart.png
    …/icons/1.5x/heart.png
    …/icons/2.0x/heart.png
    …etc.
然后加载图像,使用:
 new AssetImage('icons/heart.png', package: 'my_icons')
或
new Image.asset('icons/heart.png', package: 'my_icons')

包在使用本身的资源时也必须在pubspec.yaml文件中加上package参数来获取。
包也可以选择在其lib/文件夹中包含未在其pubspec.yaml文件中声明的资源。例如,一个名为“fancy_backgrounds”的包,可能包含以下文件:
    …/lib/backgrounds/background1.png
    …/lib/backgrounds/background2.png
    …/lib/backgrounds/background3.png
如果要包含第一张图像,则必须在pubspec.yaml的assets部分中声明它:
flutter:
  assets:
    - packages/fancy_backgrounds/backgrounds/background1.png
lib/是隐含的,所以它不应该包含在资产路径中。

上面的资源都是flutter应用中的,这些资源只有在Flutter框架运行之后才能使用。
如果要给应用设置APP图标或者添加启动图,那必须使用特定平台的assets。

特定平台 assets
  iOS
    Assets.xcassets中设置App图标、启动图。直接拖入图片。
  Android
    App图标:.../android/app/src/main/res目录,里面包含了各种资源文件夹,替换。
    启动图:.../android/app/src/main目录。也可以在res/drawable/launch_background.xml,通过自定义drawable来实现自定义启动界面。
    注意:如果重命名了.png文件,则必须在AndroidManifest.xml的<application>标签的android:icon属性中更新名称。
在Flutter框架加载时,Flutter会使用本地平台机制绘制启动页。此启动页将持续到Flutter渲染应用程序的第一帧时。这意味着如果不在应用程序的main()方法中调用runApp 函数 (更具体地说,如果不调用window.render去响应window.onDrawFrame)的话, 启动屏幕将永远持续显示。
iOS

iOS

Android
  1. 字体

默认字体

在iOS上:
  中文字体:PingFang SC
  英文字体:.SF UI Text 、.SF UI Display

在Android 上:
  中文字体:Source Han Sans / Noto
  英文字体:Roboto

/*
iOS使用.SF的好处:
  SF Text 的字距及字母的半封闭空间,比如 "a"! 上半部分会更大,因其可读性更好,适用于更小的字体; 
  SF Display 则适用于偏大的字体。
字体小于 20pt 时用 Text ,大于等于 20pt 时用 Display 。
由于 SF 属于动态字体,系统自动根据字体的大小匹配这两种显示模式。
*/

自定义字体

1. 在pubspec.yaml中声明(以确保字体会打包到应用程序中)。
flutter:
  fonts:
    - family: Raleway  // family设置字体名, 用于TextStyle的fontFamily属性中使用。
      fonts:
        - asset: assets/fonts/Raleway-Regular.ttf  // asset 是相对于 pubspec.yaml 文件的字体路径
        - asset: assets/fonts/Raleway-Medium.ttf
          weight: 500  // 指定字体的粗细,取值范围是100到900之间的整百数(100的倍数). 对应TextStyle的FontWeight属性
          style: italic  // 指定字体是倾斜还是正常,对应的值为italic和 normal. 对应TextStyle的 fontStyle TextStyle 属性
        - asset: assets/fonts/Raleway-SemiBold.ttf
          weight: 600
    - family: AbrilFatface
      fonts:
        - asset: assets/fonts/abrilfatface/AbrilFatface-Regular.ttf

2. 通过style(TextStyle)使用字体。
// 声明文本样式
const textStyle = const TextStyle(
  fontFamily: 'Raleway',
);
// 使用文本样式
var buttonText = const Text(
  "Use the font for this text",
  style: textStyle,
);
Package中的字体

1. 要使用Package中定义的字体,必须提供package参数。
如果在package包内部使用它自己定义的字体,也应该在创建文本样式时指定package参数。
const textStyle = const TextStyle(
  fontFamily: 'Raleway',
  package: 'my_package', // 指定包名
);

2. 一个包也可以只提供字体文件而不需要在pubspec.yaml中声明。 这些文件应该存放在包的lib/文件夹中。字体文件不会自动绑定到应用程序中,应用程序可以在声明字体时有选择地使用这些字体。假设一个名为my_package的包中有一个字体文件:lib/fonts/Raleway-Medium.ttf
然后,应用程序可以声明一个字体,如下面的示例所示(lib/是隐含的,所以它不应该包含在asset路径中。):
 flutter:
   fonts:
     - family: Raleway
       fonts:
         - asset: assets/fonts/Raleway-Regular.ttf
         - asset: packages/my_package/fonts/Raleway-Medium.ttf
           weight: 500
在这种情况下,由于应用程序本地定义了字体,所以在创建TextStyle时可以不指定package参数:
const textStyle = const TextStyle(
  fontFamily: 'Raleway',
);

示例(自定义字体)

1. 在pubsec.yaml中声明字体

name: my_application
description: A new Flutter project.
dependencies:
  flutter:
    sdk: flutter
flutter:
  # Include the Material Design fonts.
  uses-material-design: true
  fonts:
    - family: Rock Salt
      fonts:
        # https://fonts.google.com/specimen/Rock+Salt
        - asset: fonts/RockSalt-Regular.ttf
    - family: VT323
      fonts:
        # https://fonts.google.com/specimen/VT323
        - asset: fonts/VT323-Regular.ttf
    - family: Ewert
      fonts:
        # https://fonts.google.com/specimen/Ewert
        - asset: fonts/Ewert-Regular.ttf

2. 使用字体
import 'package:flutter/material.dart';
const String words1 = "Almost before we knew it, we had left the ground.";
const String words2 = "A shining crescent far beneath the flying vessel.";
const String words3 = "A red flair silhouetted the jagged edge of a wing.";
const String words4 = "Mist enveloped the ship three hours out from port.";
void main() {
  runApp(new MyApp());
}
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Fonts',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new FontsPage(),
    );
  }
}
class FontsPage extends StatefulWidget {
  @override
  _FontsPageState createState() => new _FontsPageState();
}
class _FontsPageState extends State<FontsPage> {
  @override
  Widget build(BuildContext context) {
    // Rock Salt - https://fonts.google.com/specimen/Rock+Salt
    var rockSaltContainer = new Container(
      child: new Column(
        children: <Widget>[
          new Text(
            "Rock Salt",
          ),
          new Text(
            words2,
            textAlign: TextAlign.center,
            style: new TextStyle(
              fontFamily: "Rock Salt",
              fontSize: 17.0,
            ),
          ),
        ],
      ),
      margin: const EdgeInsets.all(10.0),
      padding: const EdgeInsets.all(10.0),
      decoration: new BoxDecoration(
        color: Colors.grey.shade200,
        borderRadius: new BorderRadius.all(new Radius.circular(5.0)),
      ),
    );
    // VT323 - https://fonts.google.com/specimen/VT323
    var v2t323Container = new Container(
      child: new Column(
        children: <Widget>[
          new Text(
            "VT323",
          ),
          new Text(
            words3,
            textAlign: TextAlign.center,
            style: new TextStyle(
              fontFamily: "VT323",
              fontSize: 25.0,
            ),
          ),
        ],
      ),
      margin: const EdgeInsets.all(10.0),
      padding: const EdgeInsets.all(10.0),
      decoration: new BoxDecoration(
        color: Colors.grey.shade200,
        borderRadius: new BorderRadius.all(new Radius.circular(5.0)),
      ),
    );
    // https://fonts.google.com/specimen/Ewert
    var ewertContainer = new Container(
      child: new Column(
        children: <Widget>[
          new Text(
            "Ewert",
          ),
          new Text(
            words4,
            textAlign: TextAlign.center,
            style: new TextStyle(
              fontFamily: "Ewert",
              fontSize: 25.0,
            ),
          ),
        ],
      ),
      margin: const EdgeInsets.all(10.0),
      padding: const EdgeInsets.all(10.0),
      decoration: new BoxDecoration(
        color: Colors.grey.shade200,
        borderRadius: new BorderRadius.all(new Radius.circular(5.0)),
      ),
    );

    // Material Icons font - included with Material Design
    String icons = "";
    // https://material.io/icons/#ic_accessible
    // accessible: &#xE914; or 0xE914 or E914
    icons += "\u{E914}";
    // https://material.io/icons/#ic_error
    // error: &#xE000; or 0xE000 or E000
    icons += "\u{E000}";
    // https://material.io/icons/#ic_fingerprint
    // fingerprint: &#xE90D; or 0xE90D or E90D
    icons += "\u{E90D}";
    // https://material.io/icons/#ic_camera
    // camera: &#xE3AF; or 0xE3AF or E3AF
    icons += "\u{E3AF}";
    // https://material.io/icons/#ic_palette
    // palette: &#xE40A; or 0xE40A or E40A
    icons += "\u{E40A}";
    // https://material.io/icons/#ic_tag_faces
    // tag faces: &#xE420; or 0xE420 or E420
    icons += "\u{E420}";
    // https://material.io/icons/#ic_directions_bike
    // directions bike: &#xE52F; or 0xE52F or E52F
    icons += "\u{E52F}";
    // https://material.io/icons/#ic_airline_seat_recline_extra
    // airline seat recline extra: &#xE636; or 0xE636 or E636
    icons += "\u{E636}";
    // https://material.io/icons/#ic_beach_access
    // beach access: &#xEB3E; or 0xEB3E or EB3E
    icons += "\u{EB3E}";
    // https://material.io/icons/#ic_public
    // public: &#xE80B; or 0xE80B or E80B
    icons += "\u{E80B}";
    // https://material.io/icons/#ic_star
    // star: &#xE838; or 0xE838 or E838
    icons += "\u{E838}";
    var materialIconsContainer = new Container(
      child: new Column(
        children: <Widget>[
          new Text(
            "Material Icons",
          ),
          new Text(
            icons,
            textAlign: TextAlign.center,
            style: new TextStyle(
              inherit: false,
              fontFamily: "MaterialIcons",
              color: Colors.black,
              fontStyle: FontStyle.normal,
              fontSize: 25.0,
            ),
          ),
        ],
      ),
      margin: const EdgeInsets.all(10.0),
      padding: const EdgeInsets.all(10.0),
      decoration: new BoxDecoration(
        color: Colors.grey.shade200,
        borderRadius: new BorderRadius.all(new Radius.circular(5.0)),
      ),
    );
    return new Scaffold(
      appBar: new AppBar(
        title: new Text("Fonts"),
      ),
      body: new ListView(
        children: <Widget>[
          rockSaltContainer,
          v2t323Container,
          ewertContainer,
          materialIconsContainer,
        ],
      ),
    );
  }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,099评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,828评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,540评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,848评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,971评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,132评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,193评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,934评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,376评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,687评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,846评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,537评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,175评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,887评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,134评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,674评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,741评论 2 351

推荐阅读更多精彩内容