flutter进阶

上一篇 Flutter开篇

widget组合与自绘

  • 组合--返回widget


    组合.png
class UpdateItem extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Center(
          child: Text("组合demo"),
        ),
      ),
      body: Column(
        children: [
          buildTop(),
          buildBottom()
        ],
      ),
    );
  }

  buildTop() {
    return Row(
      children: [
        Padding(
          padding: EdgeInsets.all(10),
          child: ClipRRect(
            borderRadius: BorderRadius.circular(8.0),
            child: Image.asset(
              "images/xiecheng.jpg",
              width: 80,
              height: 80,
              fit: BoxFit.fill,
            ),
          ),
        ),
        Expanded(
          child: Padding(
            padding: const EdgeInsets.only(top: 10.0),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center, //垂直居中
              crossAxisAlignment: CrossAxisAlignment.start, //水平居左
              children: [
                Text(
                  "携程你值的拥有携程你值的拥有值的拥有",
                  maxLines: 1,
                  overflow: TextOverflow.ellipsis,
                ),
                Padding(
                  padding: const EdgeInsets.only(top: 8.0),
                  child: Text(
                    "2019年12月16日",
                    maxLines: 1,
                    style: TextStyle(color: Colors.grey),
                  ),
                )
              ],
            ),
          ),
        ),
        Padding(
          padding: EdgeInsets.only(right: 10),
          child: RaisedButton(
            child: Text("下载"),
            shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(18))),//背景
            onPressed: onPressed, //点击回调
          ),
        )
      ],
    );
  }

  buildBottom() {
    return Padding(
      padding: EdgeInsets.fromLTRB(15, 0, 15, 0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start, //水平方向距左对齐
        children: [
          Text(
            """8.4.0新功能介绍:
           【酒店】上线低价组合推荐,智能搭配房型,为你省钱又省心
           【旅游】一年游季在于夏,快乐出发678,特惠一暑假""",
          ), //更新文案
          Padding(
            padding: EdgeInsets.only(top: 15),
            child: Text(
              "Version 8.4.0·165.3M",
              style: TextStyle(color: Colors.grey),
            ),
          )
        ],
      ),
    );
  }

  void onPressed() {}
}
组合demo.png
  • 组合--继承widget
    Scaffold
class Scaffold extends StatefulWidget {
  /// Creates a visual scaffold for material design widgets.
  const Scaffold({
    Key key,
    this.appBar,
    this.body,
    this.floatingActionButton,
    this.floatingActionButtonLocation,//位置
    this.floatingActionButtonAnimator,//动画
    this.persistentFooterButtons,//在底部呈现一组button,显示于[bottomNavigationBar]之上,[body]之下
    this.drawer,//侧边栏,默认左边
    this.endDrawer,//侧边栏 右边
    this.bottomNavigationBar,//底部导航栏
    this.bottomSheet,//底部持久化提示框
    this.backgroundColor,
    this.resizeToAvoidBottomPadding,//弃用,使用[resizeToAvoidBottomInset]
    this.resizeToAvoidBottomInset,//重新计算布局空间大小
    this.primary = true,//是否显示到底部,默认为true将显示到顶部状态栏
    this.drawerDragStartBehavior = DragStartBehavior.start,
    this.extendBody = false,
    this.extendBodyBehindAppBar = false,
    this.drawerScrimColor,
    this.drawerEdgeDragWidth,
  }) : assert(primary != null),
       assert(extendBody != null),
       assert(extendBodyBehindAppBar != null),
       assert(drawerDragStartBehavior != null),
       super(key: key);
  • 自绘
    既然是绘制,那就需要用到画布与画笔。在 Flutter 中,画布是 Canvas,画笔则是 Paint,CustomPaint 是用以承接自绘控件的容器,并不负责真正的绘制。
    对于画笔 Paint,我们可以配置它的各种属性,比如颜色、样式、粗细等;而画布 Canvas,则提供了各种常见的绘制方法,比如画线 drawLine、画矩形 drawRect、画点 DrawPoint、画路径 drawPath、画圆 drawCircle、画圆弧 drawArc 等。
class WheelPainter extends CustomPainter {
  // 设置画笔颜色 根据颜色返回不同的画笔
  Paint getColoredPaint(Color color) {
    Paint paint = Paint();
    paint.color = color; //设置画笔颜色
    return paint;
  }

  @override
  void paint(Canvas canvas, Size size) {
    //绘制逻辑
    double wheelSize = min(size.width, size.height) / 2; //饼图的尺寸
    double nbElem = 6; //分成6份
    double radius = (2 * pi) / nbElem; //1/6圆
    Rect boundingRect = Rect.fromCircle(center: Offset(wheelSize, wheelSize), radius: wheelSize); //包裹饼图这个圆形的矩形框
    // 每次画1/6个圆弧
    canvas.drawArc(boundingRect, 0, radius, true, getColoredPaint(Colors.orange));
    canvas.drawArc(boundingRect, radius, radius, true, getColoredPaint(Colors.black38));
    canvas.drawArc(boundingRect, radius * 2, radius, true, getColoredPaint(Colors.green));
    canvas.drawArc(boundingRect, radius * 3, radius, true, getColoredPaint(Colors.red));
    canvas.drawArc(boundingRect, radius * 4, radius, true, getColoredPaint(Colors.blue));
    canvas.drawArc(boundingRect, radius * 5, radius, true, getColoredPaint(Colors.pink));
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    // 判断是否需要重绘
    return oldDelegate != this;
  }
}
自绘.png

动画

为了实现动画,需要做三件事:
1.确定画面变化的规律;
2.根据这个规律,设定动画周期,启动动画;
3.定期获取当前动画的值,不断地微调、重绘画面。
这三件事情对应到 Flutter 中,就是 Animation、AnimationController 与 Listener:
1.Animation 是 Flutter 动画库中的核心类。Animation 仅仅是用来提供动画数据,而不负责动画的渲染。
2.AnimationController 用于管理 Animation,可以用来设置动画的时长、启动动画、暂停动画、反转动画等。
3.Listener 是 Animation 的回调函数,用来监听动画的进度变化,我们需要在这个回调函数中,根据动画的当前值重新渲染组件,实现动画的渲染。

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State with SingleTickerProviderStateMixin {
  AnimationController controller;
  Animation animation;

  @override
  void initState() {
    //创建动画周期为1秒的AnimationController对象
    controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 1000));
    // 创建从50到200线性变化的Animation对象
    animation = Tween(begin: 50.0, end: 200.0).animate(controller)
      ..addListener(() {
        setState(() {}); //刷新界面
      });
    controller.forward(); //启动动画
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: Text("Animaiton demo")),
        body: Center(
          child: Container(
              width: animation.value, // 将动画的值赋给widget的宽高
              height: animation.value,
              child: FlutterLogo()),
        ));
  }

  @override
  void dispose() {
    controller.dispose(); // 释放资源
    super.dispose();
  }
}
线性动画.gif

Tween 默认是线性变化的,可以创建 CurvedAnimation 来实现非线性曲线动画。CurvedAnimation 提供了很多常用的曲线,比如震荡曲线 elasticOut:

    //创建动画周期为1秒的AnimationController对象
    controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 1000));
    //创建一条震荡曲线
    final CurvedAnimation curve = CurvedAnimation(parent: controller, curve: Curves.elasticOut);
    // 创建从50到200跟随振荡曲线变化的Animation对象
    animation = Tween(begin: 50.0, end: 200.0).animate(curve)
      ..addListener(() {
        setState(() {}); //刷新界面
      });
弹性动画.gif
  • 怎么重复执行动画?
controller.repeat(reverse: true);//让动画重复执行
AnimatedWidget 与 AnimatedBuilder
  • AnimatedWidget
    封装了animation.addListener
class AnimatedLogo extends AnimatedWidget {
  //AnimatedWidget需要在初始化时传入animation对象
  AnimatedLogo({Key key, Animation animation}) : super(key: key, listenable: animation);

  Widget build(BuildContext context) {
    //取出动画对象
    final Animation animation = listenable;
    return Center(
        child: Container(
      height: animation.value, //根据动画对象的当前状态更新宽高
      width: animation.value, child: FlutterLogo(),
    ));
  }
}
  • AnimatedBuilder
    将动画和渲染职责分离
     body: Center(
          child: AnimatedBuilder(
              animation: animation, //传入动画对象
              child: FlutterLogo(),
              builder: (context, child) => Container(
                    width: animation.value, //使用动画的当前状态更新UI
                    height: animation.value, child: child, //child参数即FlutterLogo()
                  ))),
    );
Animated执行状态
    animation.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        //动画执行完成
        print("completed");
        controller.reverse();//动画结束时反向执行
      }else if(status == AnimationStatus.forward) {
        //动画开始执行
        print("forward");
      }else if(status == AnimationStatus.reverse){
        //动画重复执行
        print("reverse");
      }
      else if (status == AnimationStatus.dismissed) {
        //动画执行一个循环
        print("dismissed");
//        controller.forward();//动画反向执行完毕时,重新执行
      }
    });
hero 动画

实现在两个页面之间切换的过渡动画
跨页面共享的控件动画效果有一个专门的名词,即“共享元素变换”(Shared Element Transition)。
通过 Hero,我们可以在两个页面的共享元素之间,做出流畅的页面切换效果。

class Page1 extends StatelessWidget {
  Widget build(BuildContext context) {
    return  Scaffold(
        appBar: AppBar(title: Text("Page1"),),
        body: GestureDetector(//手势监听点击
          child: Hero(
              tag: 'hero',//设置共享tag
              child: Center(
                 child:Container(
                      width: 100, height: 100,
                      child: FlutterLogo())),
              ),
          onTap: () {
            Navigator.of(context).push(MaterialPageRoute(builder: (_)=>Page2()));//点击后打开第二个页面
          },
        )
    );
  }
}

class Page2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return  Scaffold(
      appBar: AppBar(title: Text("Page2"),),
        body: Hero(
            tag: 'hero',//设置共享tag
            child: Center(
              child: Container(
                  width: 300, height: 300,
                  child: FlutterLogo()
              ),
            ),
    )
    );
  }
}
hero动画.gif

手势操作

  • 第一类是原始的指针事件(Pointer Event),即原生开发中常见的触摸事件,表示屏幕上触摸(或鼠标、手写笔)行为触发的位移行为;
  • 第二类则是手势识别(Gesture Detector),表示多个原始指针事件的组合操作,如点击、双击、长按等,是指针事件的语义化封装。
指针事件

在手指接触屏幕,触摸事件发起时,Flutter 会确定手指与屏幕发生接触的位置上究竟有哪些组件,并将触摸事件交给最内层的组件去响应。与浏览器中的事件冒泡机制类似,事件会从这个最内层的组件开始,沿着组件树向根节点向上冒泡分发,也就是事件分发。

Listener(
  child: Container(
    color: Colors.red,//背景色红色
    width: 300,
    height: 300,
  ),
  onPointerDown: (event) => print("down $event"),//手势按下回调
  onPointerMove:  (event) => print("move $event"),//手势移动回调
  onPointerUp:  (event) => print("up $event"),//手势抬起回调
);

子widget不冒泡

Stack(
   children: [
          Container(
            color: Colors.red, //父 背景色红色
            width: 300, 
            height: 300,
          ),
          Positioned(
            child: Listener(
             child: Container(
                color: Colors.blueAccent,//子 背景蓝色
                width: 100,
                height: 100,
            ),
            onPointerDown: (event) => print("down $event"), //手势按下回调
            onPointerMove: (event) => print("move $event"), //手势移动回调
            onPointerUp: (event) => print("up $event"), //手势抬起回调
          )),
        ],
      ),
手势识别

响应用户交互行为,会使用Gesture。如点击 onTap、双击 onDoubleTap、长按 onLongPress、拖拽 onPanUpdate、缩放 onScaleUpdate 等。
Gesture 是手势语义的抽象,而如果我们想从组件层监听手势,则需要使用 GestureDetector。GestureDetector的冒泡机制已经处理过,如果想冒泡(子widget事件父widget能收到),得引入Arena(手势竞技场)概念。

class _MyHomePageState extends State {
  //红色container坐标
  double _top = 0.0;
  double _left = 0.0;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Interaction demo"),
      ),
      body: Stack(
        //使用Stack组件去叠加视图,便于直接控制视图坐标
        children: [
          Positioned(
            top: _top,
            left: _left,
            child: GestureDetector(
              //手势识别
              child: Container(color: Colors.red, width: 50, height: 50),
              //红色子视图
              onTap: () => print("Tap"),
              //点击回调
              onDoubleTap: () => print("Double Tap"),
              //双击回调
              onLongPress: () => print("Long Press"),
              //长按回调
              onPanUpdate: (e) {
                //拖动回调
                setState(() {
                  //更新位置
                  _left += e.delta.dx;
                  _top += e.delta.dy;
                });
              },
            ),
          )
        ],
      ),
    );
  }
}
手势识别.gif

跨组件传递数据

Flutter 还提供了三种方案:InheritedWidget、Notification 和 EventBus。

InheritedWidget

InheritedWidget 是 Flutter 中的一个功能型 Widget,适用于在 Widget 树中共享数据的场景。InheritedWidget 的数据流动方式是从父 Widget 到子 Widget 逐层传递

class _MyHomePageState extends State {
  int count = 0;
  //修改计数器
  _incrementCounter() => setState(() {
        count++;
      });
  @override
  Widget build(BuildContext context) {
    return CountContainer(count: count, increment: _incrementCounter, child: Counter());
  }
}

class CountContainer extends InheritedWidget {
  //方便其子Widget在Widget树中找到它
  static CountContainer of(BuildContext context) => context.dependOnInheritedWidgetOfExactType();

  final int count;
  final Function() increment;
  CountContainer({
    Key key,
    @required this.count,
    @required this.increment,
    @required Widget child,
  }) : super(key: key, child: child);

  // 判断是否需要更新
  @override
  bool updateShouldNotify(CountContainer oldWidget) => count != oldWidget.count;
}

class Counter extends StatefulWidget {
  @override
  _CounterState createState() => _CounterState();
}

class _CounterState extends State {
  @override
  Widget build(BuildContext context) {
    //    //获取InheritedWidget节点
    CountContainer state = CountContainer.of(context);
    return Scaffold(
      appBar: AppBar(title: Text("InheritedWidget demo")),
      body: Text(
        '父widget传递的数据: ${state.count}',
      ),
      floatingActionButton: FloatingActionButton(onPressed: state.increment),
    );
  }
}
InheritedWidget.gif
Notification

Notification数据流动方式是从子 Widget 向上传递至父 Widget。

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State {
  String _msg = "通知:";
  @override
  Widget build(BuildContext context) {
    //监听通知
    return Scaffold(
      appBar: AppBar(
        title: Text("Notification demo"),
      ),
      body: NotificationListener(
          onNotification: (notification) {
            setState(() {
              _msg += notification.msg + "  ";
            }); //收到子Widget通知,更新msg
            return true;
          },
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center, 
           children: [
                    Text(_msg), 
                    CustomChild()
                  ], //将子Widget加入到视图树中
          )),
    );
  }
}

class CustomNotification extends Notification {
  CustomNotification(this.msg);

  final String msg;
}

//子Widget用来发通知
class CustomChild extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RaisedButton(
      //按钮点击时分发通知
      onPressed: () => CustomNotification("来自子widget").dispatch(context), child: Text("Fire Notification"),
    );
  }
}
Notification.gif
EventBus

无论是 InheritedWidget 还是 Notificaiton,它们的使用场景都需要依靠 Widget 树,也就意味着只能在有父子关系的 Widget 之间进行数据共享。那如果不是父子关系呢?事件总线 EventBus。
EventBus遵循发布 / 订阅模式,允许订阅者订阅事件,当发布者触发事件时,订阅者和发布者之间可以通过事件进行交互。发布者和订阅者之间无需有父子关系,甚至非 Widget 对象也可以发布 / 订阅。
EventBus 是一个第三方插件,因此我们需要在 pubspec.yaml 文件中声明它:

dependencies:  
  event_bus: 1.1.0
  • EventBus FirstPage
class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State {
  String msg = "通知:";
  StreamSubscription subscription;

  @override
  void initState() {
    //监听CustomEvent事件,刷新UI
    subscription = eventBus.on().listen((event) {
      setState(() {
        msg += event.msg;
      }); //更新msg
    });
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: Text("EventBus FirstPage")),
        body: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(msg),
            RaisedButton(
                child: Text("跳转EventBus SecondPage"),
                onPressed: () => Navigator.push(
                      context,
                      MaterialPageRoute(builder: (context) => EventBusSecond()),
                    )),
          ],
        ));
  }

  @override
  void dispose() {
    subscription.cancel(); //销毁
    super.dispose();
  }
}

class CustomEvent {
  String msg;
  CustomEvent(this.msg);
}
  • EventBus SecondPage
class _MyHomePageState extends State {

  @override
  Widget build(BuildContext context) {
    return Scaffold(appBar: AppBar(title: Text("EventBus SecondPage")),
        body:RaisedButton(onPressed: () {
          eventBus.fire(CustomEvent("来自第二个页面的数据  "));
        },child: Text("点击发送Event给FirstPage"),));
  }
}
  • 事件混乱问题
    加一层判断
subscription = eventBus.on().listen((event) {
      if(event.runtimeType == CustomEvent){
        setState(() {
          msg += event.msg;
        }); //更新msg
      }
    });

异步

Dart是单线程的,意味着Dart 代码是有序的,所以耗时加载都是靠异步。
在 Dart 中,实际上有两个队列,一个事件队列(Event Queue),另一个则是微任务队列(Microtask Queue)。在每一次事件循环中,Dart 总是先去第一个微任务队列中查询是否有可执行的任务,如果没有,才会处理后续的事件队列的流程。微任务队列优先级最高。


Event Queue 与 Microtask Queue.png

微任务顾名思义,表示一个短时间内就会完成的异步任务。
微任务是由 scheduleMicroTask 建立的。

scheduleMicrotask(() => print('This is a microtask'));//创建微任务
Future(() => null).then((_) => print('then 4'));//加入微任务

Dart 为 Event Queue 的任务建立提供了一层封装,叫作 Future。

Future(() => print('Running in Future 1'));//下一个事件循环输出字符串

Future(() => print(‘Running in Future 2'))
  .then((_) => print('and then 1'))
  .then((_) => print('and then 2’));//上一个事件循环结束后,连续输出三段字符串
输出??

Dart 会将异步任务的函数执行体放入事件队列,然后立即返回,后续的代码继续同步执行。而当同步执行的代码执行完毕后,事件队列会按照加入事件队列的顺序(即声明顺序),依次取出事件,最后同步执行 Future 的函数体及后续的 then。
所以 then 与 Future 函数体共用一个事件循环

 Future(() => print('f2')).then((_) {
    print('f3');
    scheduleMicrotask(() => print('f4'));
  }).then((_) => print('f5'));
输出??

Flutter中异步一般的写法

//声明了一个延迟3秒返回Hello的Future,并注册了一个then返回拼接后的Hello 2019
Future fetchContent() =>
    Future.delayed(Duration(seconds:3), () => "Hello")
        .then((x) => "$x 2019");

main() async{
  print(await fetchContent());//等待Hello 2019的返回
}

Dart 中的 await 并不是阻塞等待,而是异步等待。Dart 会将调用体的函数也视作异步函数,将等待语句的上下文放入 Event Queue 中,一旦有了结果,Event Loop 就会把它从 Event Queue 中取出,等待代码继续执行。

Future(() => print('f1'))
  .then((_) async => await Future(() => print('f2')))
  .then((_) => print('f3'));
Future(() => print('f4'));
输出??

数据的持久化

文件

Flutter 提供了两种文件存储的目录,即临时(Temporary)目录与文档(Documents)。

  • 临时目录是操作系统可以随时清除的目录,通常被用来存放一些不重要的临时缓存数据。这个目录在 iOS 上对应着 NSTemporaryDirectory 返回的值,而在 Android 上则对应着 getCacheDir 返回的值。
  • 文档目录则是只有在删除应用程序时才会被清除的目录,通常被用来存放应用产生的重要数据文件。在 iOS 上,这个目录对应着 NSDocumentDirectory,而在 Android 上则对应着 AppData 目录。
//先依赖
path_provider: ^1.5.1

//创建文件目录
Future get _localFile async {
  final directory = await getApplicationDocumentsDirectory();
  final path = directory.path;
  return File('$path/content.txt');
}
//将字符串写入文件
Future writeContent(String content) async {
  final file = await _localFile;
  return file.writeAsString(content);
}
//从文件读出字符串
Future readContent() async {
  try {
    final file = await _localFile;
    String contents = await file.readAsString();
    return contents;
  } catch (e) {
    return "";
  }
}

除了字符串读写之外,Flutter 还提供了二进制流的读写能力,可以支持图片、压缩包等二进制文件的读写。官方文档

SharedPreferences

缓存少量的键值对信息。在 iOS 上使用 NSUserDefaults,在 Android 使用 SharedPreferences。

  //先依赖
 shared_preferences: ^0.5.6

  setSP() async {
    SharedPreferences sp = await SharedPreferences.getInstance();
    await sp.setInt('counter', 123456);
    print("setSP");
  }

  getSP() async {
    SharedPreferences sp = await SharedPreferences.getInstance();
    counter = sp.getInt("counter") ?? 0;
    print("getSP:$counter");
    return counter;
  }

  clearSp() async {
    SharedPreferences sp = await SharedPreferences.getInstance();
    sp.clear();
  }
数据库
//先依赖
sqflite: ^1.2.0

Future create() async {
    database = openDatabase(
      join(await getDatabasesPath(), 'students_database.db'),
      onCreate: (db, version) => db.execute("CREATE TABLE students(id TEXT PRIMARY KEY, name TEXT, score INTEGER)"),
      onUpgrade: (db, oldVersion, newVersion) {
        //升级
        //dosth for migration
      },
      version: 1,
    );
  }

  //增
  Future insert() async {
    var student1 = Student(id: '123', name: '张三', score: 90);
    var student2 = Student(id: '456', name: '李四', score: 80);
    var student3 = Student(id: '789', name: '王五', score: 85);
    Future insertStudent(Student std) async {
      final Database db = await database;
      await db.insert(
        'students', std.toJson(), //插入冲突策略,新的替换旧的
        conflictAlgorithm: ConflictAlgorithm.replace,
      );
    }
    //插入3个Student对象
    await insertStudent(student1);
    await insertStudent(student2);
    await insertStudent(student3);
  }

  //删
  Future delete() async {
    Future deleteStudent(String id) async {
      final Database db = await database;
      //await db.delete("students",where: "id=?",whereArgs:[id] );
      await db.rawDelete('DELETE FROM students WHERE id = ?', ['$id']);
    }
    //删除id=123
    await deleteStudent("123");
  }

  //改
  Future update() async{
    Future updateStudent(String newName,int score,String oldName) async {
      final Database db = await database;
      await db.rawUpdate('UPDATE students SET name = ?, score = ? WHERE name = ?', ['$newName','$score','$oldName']);
    }

    await updateStudent("张xiao三",123456,"张三");
  }

  //查
  Future query() async {
    Future> students() async {
      final Database db = await database;
      final List> maps = await db.query('students');
      return List.generate(maps.length, (i) => Student.fromJson(maps[i]));
    }

    //读取出数据库中插入的Student对象集合
    //    students().then((list)=>list.forEach((s)=> print(s.name)));
    students().then((value) {
      setState(() {});
      list = value;
      value.forEach((s) {
        print(s.name);
      });
    });

  }

  @override
  void dispose() {
    db_close();
    super.dispose();
  }

  Future db_close() async {
    //释放数据库资源
    final Database db = await database;
    db.close();
  }

Isolate 多线程并发

尽管 Dart 是基于单线程模型的,但为了进一步利用多核 CPU,将 CPU 密集型运算进行隔离,Dart 也提供了多线程机制,即 Isolate。在 Isolate 中,资源隔离做得非常好,每个 Isolate 都有自己的 Event Loop 与 Queue,Isolate 之间不共享任何资源,只能依靠消息机制通信,因此也就没有资源抢占问题。

Isolate isolate;

start() async {
  ReceivePort receivePort= ReceivePort();//创建管道
  //创建并发Isolate,并传入发送管道
  isolate = await Isolate.spawn(getMsg, receivePort.sendPort);
  //监听管道消息
  receivePort.listen((data) {
    print('Data:$data');
    receivePort.close();//关闭管道
    isolate?.kill(priority: Isolate.immediate);//杀死并发Isolate
    isolate = null;
  });
}
//并发Isolate往管道发送一个字符串
getMsg(sendPort) => sendPort.send("Hello");

HTTP dio

  //创建网络调用示例
  Dio dio = Dio()
  //设置URI及请求user-agent后发起请求
  var response = await dio.get("https://flutter.dev", options:Options(headers: {"user-agent" : "Custom-UA"}));
  var postResponse = await dio.post("http://192.168.112.72:8080/apis/tm/timeline/getTimeLineListByMeetingId",data: {"meetingId":360});

  //打印请求结果
  if(response.statusCode == HttpStatus.ok) {
    print(postResponse.data.toString());
  } else {
    print("Error: ${response.statusCode}");
  }

我们的页面由多个并行的请求响应结果构成,这就需要等待这些请求都返回后才能刷新界面。

 //同时发起两个并行请求
List responseListData= await Future.wait([dio.get("https://flutter.dev",options: Options(headers: {"user-agent" : "Custom-UA"})),dio.post("http://192.168.112.72:8080/apis/tm/timeline/getTimeLineListByMeetingId",data: {"meetingId":360})]);
json解析

Flutter 不支持运行时反射,因此并没有提供像 Gson、Mantle 这样自动解析 JSON 的库来降低解析成本。

{
  "id":"123",
  "name":"张三",
  "score" : 95
}
class Student{
  //属性id,名字与成绩
  String id;
  String name;
  int score;
  //构造方法  
  Student({
    this.id,
    this.name,
    this.score
  });
  //JSON解析工厂类,使用map为对象初始化赋值
  factory Student.fromJson(Map parsedJson){
    return Student(
        id: parsedJson['id'],
        name : parsedJson['name'],
        score : parsedJson ['score']
    );
  }
}

 Map toJson() {
        final Map data = new Map();
        data['score'] = this.score;
        data['name'] = this.name;
        data['id'] = this.id;
        return data;
    }
FlutterJsonBeanFactory
FlutterJsonBeanFactory.png

image.png

image.png
  • 在线解析
  • flutter中json序列化和反序列化
//序列化
String json = JSON.encode(user);
//反序列化
Map userMap = JSON.decode(json);
var user = new User.fromJson(userMap);

Flutter与IOS/Android通信

由于 Flutter 只接管了应用渲染层,因此这些系统底层能力是无法在 Flutter 框架内提供支持的;而另一方面,Flutter 还是一个相对年轻的生态,因此原生开发中一些相对成熟的 Java、C++ 或 Objective-C 代码库,比如图片处理、音视频编解码等,可能在 Flutter 中还没有相关实现。因此,为了解决调用原生系统底层能力以及相关代码库复用问题,Flutter 为开发者提供了一个轻量级的解决方案,即逻辑层的方法通道(Method Channel)机制。基于方法通道,我们可以将原生代码所拥有的能力,以接口形式暴露给 Dart,从而实现 Dart 代码与原生代码的交互,就像调用了一个普通的 Dart API 一样。

MethodChannel
MethodChannel.png
  • Flutter
class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State {

  @override
  Widget build(BuildContext context) {
    return Scaffold(appBar: AppBar(title: Text("MethodChannel Demo"),), body: Column(children: [RaisedButton(onPressed: () {
      handleButtonClick();
    }, child: Text("MethodChannel"),),
    ],));
  }


  //处理按钮点击
  handleButtonClick() async {
    //声明MethodChannel
    const platform = MethodChannel('tami');
    var result;
    //异常捕获
    try {
      //异步等待方法通道的调用结果
      result = await platform.invokeMethod('123456');
    } catch (e) {
      result = -1;
    }
    print("Result:$result");
  }
}
  • Android
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        GeneratedPluginRegistrant.registerWith(flutterEngine)
        //创建与调用方标识符一样的方法通道
        MethodChannel(flutterEngine.dartExecutor, "tami").setMethodCallHandler { call, result ->
            //判断方法名是否支持
            if(call.method == "123456") {
                Log.e("twb","flutter --> android")
                result.success("flutter->Android,sucess")
            }else{
                result.notImplemented()
            }
        }
    }
  • IOS
EventChannel
  • Flutter
class _MyHomePageState extends State {
  //EventChannel
  static const EventChannel eventChannel = EventChannel('eventchannel');

  String _chargingStatus = 'Battery status: unknown.';

  @override
  void initState() {
    super.initState();
    eventChannel.receiveBroadcastStream().listen(_onEvent, onError: _onError);
  }

  void _onEvent(Object event) {
    setState(() {
      _chargingStatus =
      "Battery status: ${event == 'charging' ? '' : 'dis'}charging.";
    });
  }

  void _onError(Object error) {
    setState(() {
      _chargingStatus = 'Battery status: unknown.';
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("EventChannel Demo"),
        ),
        body: Center(
          child: Text(
            _chargingStatus,
          ),
        )
    );
  }
}
  • Android
public class EventChannelActivity extends FlutterActivity {
    private static final String CHARGING_CHANNEL = "eventchannel";
    @Override
    public void configureFlutterEngine(FlutterEngine flutterEngine) {
        GeneratedPluginRegistrant.registerWith(flutterEngine);
        new EventChannel(flutterEngine.getDartExecutor(), CHARGING_CHANNEL).setStreamHandler(
                new EventChannel.StreamHandler() {
                    private BroadcastReceiver chargingStateChangeReceiver;
                    @Override
                    public void onListen(Object arguments, EventChannel.EventSink events) {
                        chargingStateChangeReceiver = createChargingStateChangeReceiver(events);
                        registerReceiver(
                                chargingStateChangeReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
                    }

                    @Override
                    public void onCancel(Object arguments) {
                        unregisterReceiver(chargingStateChangeReceiver);
                        chargingStateChangeReceiver = null;
                    }
                }
        );
    }

    private BroadcastReceiver createChargingStateChangeReceiver(final EventChannel.EventSink events) {
        return new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1);

                if (status == BatteryManager.BATTERY_STATUS_UNKNOWN) {
                    events.error("UNAVAILABLE", "Charging status unavailable", null);
                } else {
                    boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
                            status == BatteryManager.BATTERY_STATUS_FULL;
                    events.success(isCharging ? "charging" : "discharging");
                }
            }
        };
    }
}
BasicMessageChannel,原生嵌套Flutter

https://www.jianshu.com/p/39575a90e820

  • Flutter
class _MyHomePageState extends State {
  static const String _channel = 'BasicMessageChannel';
  static const String _pong = 'pong';
  static const String _emptyMessage = '';
  ///详细参考 https://www.jianshu.com/p/39575a90e820 或者源码
  //StandardMessageCodec() //BasicMessageChannel的默认编解码器,其支持基础数据类型、二进制数据、列表、字典
  //BinaryCodec() //二进制格式 Android中为ByteBuffer,iOS中为NSData
  //JSONMessageCodec() //adnroid  org.json ios NSJSONSerialization
  static const BasicMessageChannel basicMessageChannel = BasicMessageChannel(_channel, StringCodec());

  int _counter = 0;
  @override
  void initState() {
    super.initState();
    basicMessageChannel.setMessageHandler(_handlePlatformIncrement);
  }

  Future _handlePlatformIncrement(String message) async {
    setState(() {
      _counter++;
    });
    return _emptyMessage;
  }

  void _sendFlutterIncrement() {
    basicMessageChannel.send(_pong);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Expanded(
            child: Center(
              child: Text(
                  'Platform button tapped $_counter time${ _counter == 1 ? '' : 's' }.',
                  style: const TextStyle(fontSize: 17.0)),
            ),
          ),
          Container(
            padding: const EdgeInsets.only(bottom: 15.0, left: 5.0),
            child: Row(
              children: [
                FlutterLogo(),
                const Text('Flutter', style: TextStyle(fontSize: 30.0)),
              ],
            ),
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _sendFlutterIncrement,
        child: const Icon(Icons.add),
      ),
    );
  }
}
  • Android
  
//注册
 flutter_view.attachToFlutterEngine(flutterEngines!!)
//销毁
override fun onDestroy() {
        flutter_view.detachFromFlutterEngine()
        super.onDestroy()
    }
 messageChannel = BasicMessageChannel(flutterEngines!!.dartExecutor, CHANNEL, StringCodec.INSTANCE)
 messageChannel?.setMessageHandler { s, reply ->
            Log.e("twb", "Flutter发送:$s")
            onFlutterIncrement()
            reply.reply("")
        }
//发送
messageChannel?.send("ping")
  • IOS
原生嵌套Flutter.gif

Flutter 调用原生view?

  • Flutter
class SampleView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //使用Android平台的AndroidView,传入唯一标识符sampleView
    if (defaultTargetPlatform == TargetPlatform.android) {
      return AndroidView(viewType: 'sampleView');
    } else {
      //使用iOS平台的UIKitView,传入唯一标识符sampleView
      return UiKitView(viewType: 'sampleView');
    }
  }
}
  • Android
        if (flutterEngine!=null){
//            flutterEngine!!.platformViewsController.registry.registerViewFactory("sampleView",object : PlatformViewFactory(StandardMessageCodec.INSTANCE){
//                override fun create(p0: Context?, p1: Int, p2: Any?): PlatformView {
//                    return SimpleViewControl(mContext)
//                }
//            })
            val shimPluginRegistry = ShimPluginRegistry(flutterEngine!!)
            shimPluginRegistry.registrarFor("samples.flutter/native_views")
                    .platformViewRegistry()
                    .registerViewFactory("sampleView",object : PlatformViewFactory(StandardMessageCodec.INSTANCE){
                        override fun create(p0: Context?, viewId: Int, p2: Any?): PlatformView {
                            return SimpleViewControl(mContext)
                        }

                    })
        }

  //原生视图封装类
  internal class SimpleViewControl(context: Context) : PlatformView {
      private val view: View = View(context)//缓存原生视图

     init {
        view.setBackgroundColor(Color.rgb(255, 0, 0))
     }

    //返回原生视图
    override fun getView(): View {
        return view
    }

    //原生视图销毁回调
    override fun dispose() {}
}
  • IOS

Flutter跳转原生

  • Flutter
  static const MethodChannel _methodChannel = MethodChannel('PlatformView');

  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }
  Future _launchPlatformCount() async {
    final int platformCounter = await _methodChannel.invokeMethod('switchView', _counter);
    setState(() {
      _counter = platformCounter;
    });
  }
  • Android
        //MethodChannel PlatformView 跳转页面
        MethodChannel(flutterEngine.dartExecutor, "PlatformView").setMethodCallHandler { call, result ->
            this.result = result
            //判断方法名是否支持
            if (call.method == "switchView") {
                val count = call.arguments as Int
                onLaunchFullScreen(count)
            } else {
                result.notImplemented()
            }
        }
    }

    //MethodChannel PlatformView 跳转页面
    private fun onLaunchFullScreen(count: Int) {
        val fullScreenIntent = Intent(this, PlatformViewActivity::class.java)
        fullScreenIntent.putExtra(PlatformViewActivity.EXTRA_COUNTER, count)
        startActivityForResult(fullScreenIntent, COUNT_REQUEST)
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
        if (requestCode == COUNT_REQUEST) {
            if (resultCode == Activity.RESULT_OK) {
                result.success(data.getIntExtra(PlatformViewActivity.EXTRA_COUNTER, 0))
            } else {
                result.error("ACTIVITY_FAILURE", "Failed while launching activity", null)
            }
        }
    }
  • IOS
Flutter跳原生界面.gif

状态管理 Provider

Provider 是一个用来提供数据的框架
提供了依赖注入的功能,允许在 Widget 树中更加灵活地处理和传递数据
什么是依赖注入?简单来说,依赖注入是一种可以让我们在需要时提取到所需资源的机制,即:预先将某种“资源”放到程序中某个我们都可以访问的位置,当需要使用这种“资源”时,直接去这个位置拿即可,而无需关心“资源”是谁放进去的。
如果我们的应用足够简单,数据流动的方向和顺序是清晰的,我们只需要将数据映射成视图就可以了。作为声明式的框架,Flutter 可以自动处理数据到渲染的全过程,通常并不需要状态管理。


image.png

但,随着产品需求迭代节奏加快,项目逐渐变得庞大时,我们往往就需要管理不同组件、不同页面之间共享的数据关系。当需要共享的数据关系达到几十上百个的时候,我们就很难保持清晰的数据流动方向和顺序了,导致应用内各种数据传递嵌套和回调满天飞。在这个时候,我们迫切需要一个解决方案,来帮助我们理清楚这些共享数据的关系,于是状态管理框架便应运而生。


image.png

因为 Provider 实际上是 InheritedWidget 的封装,所以通过 Provider 传递的数据从数据流动方向来看,是由父到子(或者反过来)。这时我们就明白了,原来需要把资源放到 FirstPage 和 SecondPage 的父 Widget,也就是应用程序的实例 MyApp 中。
//依赖
provider: ^3.2.0

//定义需要共享的数据模型,通过混入ChangeNotifier管理
class CounterModel with ChangeNotifier {
  int _count = 0;

  //读方法
  int get counter => _count;

  //写方法
  void increment() {
    _count++;
    notifyListeners(); //通知刷新
  }
}
class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    //ChangeNotifierProvider 具有读写  Provider只有读
    return ChangeNotifierProvider.value(
      value: CounterModel(),//需要共享的数据资源
      child: MaterialApp(title: 'Flutter Demo',
        theme: ThemeData(primarySwatch: Colors.blue,), home: MyHomePage(),),);
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //取出资源
    final _counter = Provider.of(context);
    return Scaffold(
        appBar: AppBar(title: Text("First Provider Screen"),),
        body:Center(child: Text('Counter: ${_counter.counter}')),
        floatingActionButton: FloatingActionButton( onPressed: () =>
            Navigator.of(context).push(MaterialPageRoute(builder: (context) => SecondProviderPage())),
        child: Icon(Icons.arrow_forward_ios),)
    );
  }
}
class SecondProviderPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    //取出资源
    final _counter = Provider.of(context);

    return Scaffold(
      appBar: AppBar(
        title: Text('Second Provider Screen'),
      ),
      body: Center(child: Text('Counter: ${_counter.counter}')),
      //用资源更新方法来设置按钮点击回调
      floatingActionButton: FloatingActionButton(
        onPressed: _counter.increment,
        child: Icon(Icons.add),
      ),
    );
  }
}
Provider共享数据.gif
Consumer

滥用 Provider.of 方法有很大的副作用,那就是当数据更新时,页面中其他的子 Widget 也会跟着一起刷新。
那么,有没有办法能够在数据资源发生变化时,只刷新对资源存在依赖关系的 Widget,而其他 Widget 保持不变呢?

class SecondProviderPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Second Provider Screen'),
      ),
      body: Consumer(
        builder: (BuildContext context, CounterModel value, Widget child) {
          return Center(child: Text('Value: ${value.counter}'));
        },
      ),
      floatingActionButton:Consumer(
        builder: (BuildContext context, CounterModel value, Widget child) {
          return FloatingActionButton(
            //用资源更新方法来设置按钮点击回调
            onPressed: value.increment,
            child: Icon(Icons.add),
          );
        },
      )
    );
  }
}
多状态的资源封装
class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    //ChangeNotifierProvider 具有读写  Provider只有读
    return MultiProvider(
      providers: [
        Provider.value(value: 30.0),//注入字体大小
        ChangeNotifierProvider.value(value: CounterModel())//注入计数器实例
      ],
      child: MaterialApp(title: 'Flutter Demo',
        theme: ThemeData(primarySwatch: Colors.blue,), home: MyHomePage(),),);
  }
}
     //取出资源
    final _counter = Provider.of(context);//获取计时器实例
    final textSize = Provider.of(context);//获取字体大小
 Scaffold(
        appBar: AppBar(
          title: Text('Second Provider Screen'),
        ),
        body: Consumer2(
          builder: (BuildContext context, double textSize,CounterModel value, Widget child) {
            return Center(child: Text('Value: ${value.counter}',style: TextStyle(fontSize: textSize,),));
          },
        ),
        floatingActionButton:Consumer2(
          builder: (BuildContext context, double textSize, CounterModel value,Widget child) {
            return FloatingActionButton(
              //用资源更新方法来设置按钮点击回调
              onPressed:  value.increment,
              child: Icon(Icons.add),
            );
          },
        )
    );

Consumer2 与 Consumer 的使用方式基本一致,只不过是在 builder 方法中多了一个数据资源参数。注意最多共享 6 个数据资源,从Consumer 到 Consumer 6。

横竖屏

@override
Widget build(BuildContext context) {
  return Scaffold(
    //使用OrientationBuilder的builder模式感知屏幕旋转
    body: OrientationBuilder(
      builder: (context, orientation) {
        //根据屏幕旋转方向返回不同布局行为
        return orientation == Orientation.portrait
            ? _buildVerticalLayout()
            : _buildHorizontalLayout();
      },
    ),
  );
}

OrientationBuilder 提供了 orientation 参数可以识别设备方向,而如果我们在 OrientationBuilder 之外,希望根据设备的旋转方向设置一些组件的初始化行为,也可以使用 MediaQueryData 提供的 orientation 方法:

if(MediaQuery.of(context).orientation == Orientation.portrait) {
  //dosth
}

Flutter 应用默认支持竖屏和横屏两种模式。如果我们的应用程序不需要提供横屏模式,也可以直接调用 SystemChrome 提供的 setPreferredOrientations 方法告诉 Flutter,这样 Flutter 就可以固定视图的布局方向了,注意setPreferredOrientations 是全局生效的。

SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
  • 问题:
    如果A页面需要横屏怎么办?
  • 解决思路:
    在A页面initState 中设置横屏,在 dispose 时设置竖屏。

判断屏宽度

if (MediaQuery.of(context).size.width > 480) {
  //大屏 或 平板
}else{
 //普通手机
}

你可能感兴趣的:(flutter进阶)