0%

Learn Flutter

笔记结合了Flutter官方文档,Dart官方文档,B站这个视频,《Flutter实战第二版》这本书

Introduction

  1. Flutter 跨平台、高保真、高性能,使用Dart语言开发,Impeller将会取代Skia作为Flutter主渲染方案(和React Native需要native components和javascript通信,Dart可以直接操作UI层,不需要两层通信)
  2. 配环境
    • Flutter SDK + 配置bin到环境变量
    • Android Studio 配置Flutter,Dart插件, 配置模拟器; VS Code 配置Flutter, Dart插件; Idea 配置Flutter, Dart插件
    • flutter doctor检查flutter环境和需要的依赖是否完整
      • 如果是customized Android SDK path: flutter config --android-sdk <path>
      • Android toolchain报错: 再Android Studio中下载Android SDK Command-line Tools (latest)
      • 需要同意Android许可: flutter doctor --android-licenses
    • 升级Flutter: 运行flutter upgrade
    • Android Studio 修改已安装模拟器的路径:
      • 添加环境变量: ANDROID_SDK_HOME, 值是<path>
      • C:\Users\<User>\.android中的avd文件夹复制到<path>\.android
      • 修改模拟器对应的<emulator_name>.ini文件,把path改为<path>\.android\avd
    • Dart SDK

Dart Language

  1. General concepts
    • Everything in a variable is an object, every object is an instance of a class
    • Dart is a strong-typed language, but it can do type inference when using var
    • Dart introduced null safety in 2.12, if a variable can be of type x or null, it should be declared as x?, if a variable can be of type x but Dart disagrees you cau use x! to force it to be of type x
    • When any type is allowed, use Object?, Object or dynamic
    • Dart supports generic types
    • Dart supports top-level functions/variables, as well as functions/variables tied to a class or object
    • Dart does not have accessibility keywords, for private items, use _ as the first character
    • Dart has both expressions (which have runtime values, like ternary expressions) and statements (which don’t, like if else statements)
    • Dart tools can report two kinds of problems: warnings and errors
  2. Variables
    • You can declare a variable with its type, or use type inference
    • Uninitialized variables that have a nullable type have an initial value of null
    • You can use late modifier to defer initialization until the first time the variable is used
    • A final variable can be set only once; a const variable is a compile-time constant(you must initialize a constant variable with a value, otherwise there will be a compile error)
  3. Built-int types
    • Numbers: int, double; Strings: String; Booleans: bool; Lists: List; Sets: Set; Maps: Map; Runes: Runes; Symbols: Symbol, the value null: Null. Other types include: Object, Enum, Future and Stream, Iterable, Never, dynamic, void
    • 字符串转换成数字: int.parse('1'), double.parse('1.1')
    • 用三引号创建多行字符串: var a = """ a\n b\n c\n""";, 用r创建原始字符串
    • 整形转换成字符串: 1.toString()
    • 浮点型转换成字符串并且小数点后保留n位: 1.1.toStringAsFixed(n)
    • 布尔类型: truefalse
    • 列表初始化: 空列表var l0 = <int>[];, 指定列表大小和初始值var l1 = List.filled(3, 0);, 指定列表大小和赋值方法var l2 = List.generate(3, (i) => i);, 创建constant列表: var l3 = const [1, 2, 3];
    • 列表展开: var l1 = [1, 2, 3]; var l2 = [0, ...l1];(使用...), 对于可能是null的列表进行展开: var l1 = [1, 2, 3]; var l2 = [0, ...?l1]; (使用...?)
    • 带if和for的列表:
      1
      2
      3
      4
      List intList = List.generate(3, (index) => index);
      // 下面不要再函数体加括号,想要什么格式直接写什么格式
      List strList1 = ["#0", for (var item in intList) "#${item}"];
      List strList2 = [...strList1, if (3 > 2) "#4"];
    • 反转列表: var l1 = [1, 2, 3]; var l2 = new List.from(l1.reversed);
    • 集合初始化: 空列表var s1 = <int>{};, 字面量var s2 = {1, 2, 3};, 从Iterable创建集合var s3 = Set.from([1,2,3])
    • 一些集合操作: s1.add(1), s1.remove(1), s1.clear()
    • 字典初始化: 空字典Map m1 = <String, int>{}; 字面量Map m = {"a": 0, "b": 1};, 构造器Map m = new Map(); m["a"] = 0; m["b"] = 1;
    • 字典一些操作: m.containsKey("a"), m.remove("a"), m.clear(), 还包括使用if和for, 以及......?
  4. Functions
    • functions are objects and have a type, Function
    • Every app must have a top-level main() function, which serves as the entrypoint to the app. The main() function returns void and has an optional List<String> parameter for arguments
    • 函数返回类型也可以进行类型推断,但是不推荐
    • A function can have any number of required positional parameters. These can be followed either by named parameters or by optional positional parameters (but not both)
    • Use curly braces {} for named parameters. If no default value is provided and marked as required (use required keyword), the parameter type must be nullable and its default value should be null
      1
      2
      3
      4
      5
      6
      7
      8
      9
      int addAge({int num1 = 0, int num2 = 0}) {
      return num1 + num2;
      }
      void main() {
      print(addAge()); // 0
      print(addAge(num1: 1)); // 1
      print(addAge(num2: 2)); // 2
      print(addAge(num1: 1, num2: 2)); // 3
      }
    • Use bracket [] for optional positional parameters. If no default value is provided, the parameter type must be nullable and its default value should be null
      1
      2
      3
      4
      5
      6
      7
      int addAge({int num1, [int? num2]) {
      return num1 + (num2 == null ? 0 : num2);
      }
      void main() {
      print(addAge(1)); // 1
      print(addAge(1, 2)); // 3
      }
    • Functions as first-class objects: you can pass a function as a parameter to another function
    • Anonymous functions/lambda/closure
  5. Operators
    • Some operators: / division, ~/ integer division, ?? if null operator (var a = b?? c; if variable b is null, a is assigned to the value in variable c), type1 as type2 cast type1 to type2, type1 is type2 check if type1 is type2, type1 is! type2 check if type1 is not type2
    • Cascade notation: .. and ?.., usage: object..method1()..method2()..method3(), the ?.. guarantees that non of the methods will be called if the object is null. Cascade can be nested, you need to use () to separate them
  • Control Flow statements
    • In and else: if(condition) {} else if (condition) {} else {}
    • For loops: for (initial; condition; increment) {}, for (var i in iterable), forEach on iterable iterable.forEach((ele) => callback(ele));, iterable forEach with condition iterable.where((ele) => condition(ele)).forEach((ele) => callback(ele));
    • While and do-while loops: while (condition) {}, do {} while (condition);
    • Break and continue: break and continue are used to control the flow of the loop
    • Switch and case: switch (expression) { case value1: break; case value2: break; default: break; }, expression can be integers, strings, or compile-time constants. If a case is empty and you want fall-through, you can omit break; for a non-empty case, if break is omitted, there will be a compile error; if you want fall-through with a non empty case, you need to use and labels
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      void main() {
      print("Enter an integer");
      String? command = stdin.readLineSync();
      int input = int.parse(command!);
      int res = 0;
      switch (input) {
      case 0:
      print("${res}");
      break;
      case 1:
      case 2:
      print("positive ${res + input}");
      continue label;
      label:
      case 3:
      print("done!");
      break;
      default:
      print("done!");
      }
      }
      // for input 0, output is 0
      // for input 1, output is positive 1 \n done!
      // for input 2, output is positive 2 \n done!
      // for input 3, output is done!
      // for other inputs, output is done!
  1. Assert: assert(condition, message), if condition is false, an AssertionError is thrown and the message is printed

  2. Exceptions:

    • Throw exception: throw Object;, throw Exception(meg);
    • Catch exceptions
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      try {
      breedMoreLlamas();
      } on OutOfLlamasException {
      // A specific exception
      buyMoreLlamas();
      } on Exception catch (e) {
      // Anything else that is an exception
      print('Unknown exception: $e');
      } catch (e, s) {
      // No specified type, handles alls
      print('Something really unknown: $e');
      print('Stack trace:\n $s');
      }
    • Finally
      1
      2
      3
      4
      5
      6
      7
      try {
      breedMoreLlamas();
      } catch (e) {
      print('Error: $e'); // Handle the exception first.
      } finally {
      cleanLlamaStalls(); // Then clean up.
      }
  3. Classes

    • All classes except Null descend from Object
    • Mixin-based inheritance means that although every class (except for the Object class) has exactly one superclass, a class body can be reused in multiple class hierarchies
    • Use . to refer to an instance variable or method, use ?. to avoid exception when the leftmost operand is null
    • Use constructor to, the new keyword is optional, constructors can be either ClassName(can have one unnamed constructor) or ClassName.identifier (can have multiple named constructors), use const to create a compile-time constant
    • Use object.runtimeType to get the type of an object
    • All instance variables generate an implicit getter method, non-final instance variables and late final instance variables without initializers also generate an implicit setter method
    • Constructors
      • A syntactic sugar
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        class A {
        // non-null variables must be initialized
        String name = "";
        int age = 0;

        A (String name, int age) {
        this.name = name;
        this.age = age;
        }
        }

        // recommended syntax in dart
        class B {
        // non-null variables and final variables must be initialized
        String name;
        final int age;

        // the instance variables are initialized before the constructor body is executed
        A (this.name, this.age);
        }
      • Each class has a default unnamed constructor with no parameters, if you define your own unnamed constructor, the default constructor is not generated
      • Named parameters, the order does not matter for named parameters
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        class A {
        final String name;
        final String email;

        // the user should input email, although it can be null
        A (this.name, {required this.email});
        }

        void main() {
        A a = A("name", email: "email");
        }
      • Factory constructor can be used to create instances of subclasses, to prepare calculated values to forward them as parameters to a normal constructor so that final fields can be initialized with them
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        class A {
        final String? name;
        final String? email;

        A._internal(this.name, this.email);

        factory A(String name) {
        var email = name + "@gmail.com";
        return A._internal(name, email);
        }
        }
      • Constructors cannot be inherited
      • Order of execution in subclass constructor
        1. Initializer list
        2. Superclass's no-arg constructor (if it does not exist, you must manually call a superclass constructor after colon before the constructor body)
        3. Current class's no-arg constructor
    • Abstract classes: use the abstract class keyword
    • Use @override to override a method in a subclass
    • If you want to down cast a class, use the covariant keyword
    • There is not interface keyword in dart, you need to implement all the instance members in the interface. Explicit interface: use abstract class, implicit interface: every class implicitly defines an interface containing all the instance members of the class and of any interfaces it implements
    • Mixins: mixins are a way of reusing a class’s code in multiple class hierarchies
      • Use the mixin keyword to define a mixin
      • Use the with keyword to apply a mixin to a class, the order matters, in A extends B with C, D, the top of the inheritance hierarchy is B, then B with C, then B with C and D, then A
      • Example
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        class Performer {
        void perform() => print("Performs!");
        }

        mixin Guitarist {
        void playGuitar() => print("Play guitar");
        void perform() => playGuitar();
        }

        mixin Drummer {
        void playDrums() => print("Play drums");
        void perform() => playDrums();
        }

        class Musician extends Performer with Guitarist, Drummer {}

        void main() {
        Musician musician = new Musician();
        // in Performer with Guitarist, Drummer, the perform method in mixin Drummer is called
        musician.perform();
        }
      • Use the on keyword to specify which class the mixin can be applied to, mixin A on B means that mixin A can only be applied to a class that extends B
    • Use extension to extend built-in classes
      1
      2
      3
      4
      5
      6
      7
      8
      9
      extension NumberParsing on String {
      int parseInt() {
      return int.parse(this);
      }
      }

      void main() {
      print("42".parseInt());
      }
  4. Generics

    • Generics are often required for type safety, but it can also result in better generated code and reduce code duplication
    • You can use generics with collection literals: var names = <String>['Seth', 'Kathy', 'Lars'];
    • You can use parameterized types with constructors: var nameSet = Set<String>.from(names);
    • You can use generics to restrict the parameterized type: class Foo<T extends SomeBaseClass>
    • You can use generics in methods: T first <T>(List<T> ts) {return ts[0];};
  5. Libraries and visibility

    • Use import to specify how a namespace from one library is used in the scope of another library
    • Dart built-in libraries, the URI has the special dart: scheme
    • You can specify a library prefix when there is a conflict between two libraries
      1
      2
      3
      4
      5
      6
      7
      import "package1:/lib1/lib1.dart";
      import "package2:/lib2/lib2.dart" as lib2;

      // use lib1
      Element element1 = Element();
      // use lib2
      lib2.Element element2 = lib2.Element();
    • Use part of the library only: import 'package:lib1/lib1.dart' show foo; import only foo, import 'package:lib1/lib1.dart' hide foo; import everything except foo
    • Lazily loading a library: only load the library when it is needed, import 'package:greetings/hello.dart' deferred as hello;
  6. Asynchrony support

    • Some functions return Future or Stream objects
    • Use async and await to write asynchronous code (await can only be used in async functions), try, catch, finally can also help
      1
      2
      3
      4
      5
      6
      void main() async {
      print("Before!");
      await Future.delayed(
      new Duration(seconds: 5), () => print("Delayed 5 seconds"));
      print("After");
      }
  7. Generators

    • Used to lazily produce a sequence of values
    • Synchronous generator returns an Iterable object
    • Asynchronous generator returns a Stream object

Flutter Introduction

  1. 跨平台技术简介
    • H5 + 原生 (Cordova、Ionic、微信小程序): 将APP中需要动态变化的内容通过HTML5实现,通过原生网页加载控件来加载
    • JavaScript开发 + 原生渲染 (React Native, Weex): 状态转移之后更新UI(RN中虚拟DOM会通过JavaScriptCore映射为原生控件)
    • 自绘UI + 原生 (Qt for mobile, Flutter): 不同平台实现一个同意接口的渲染引擎来绘制UI,不依赖系统原生控件
  2. Flutter 高性能
    • Flutter使用Dart,在JIT(Just-In-Time,实时编译)和JS差不多快,在AOT(Ahead-Of-Time, 预先编译)比JS快
    • Flutter使用自绘引擎来绘制UI,不需要Javascript和Native之间通信
  3. Flutter架构
    • Flutter framework: a modern, reactive framework written in the Dart language
      • 底层UI库: Foundation, Animation, Painting, Gestures
      • 抽象布局层: Rendering
      • 基础组件: Widgets, 在其之上还有Material和Cupertino风格组件库,分别实现了Material和iOS设计规范
    • Flutter Engine: mostly written in C++ and supports the primitives necessary to support all Flutter applications
    • Embedders:the embedder is written in a language that is appropriate for the platform
  4. First app
    • lib/main.dart主要代码
    • 如果要使用Material风格,在pubspec.yamlflutter部分加入uses-material-design: true,在主文件中加入import 'package:flutter/material.dart';
    • In flutter, almost everything is a widget, your class should extend StatelessWidget or StatefulWidget
    • A widget’s main job is to provide a build() method that describes how to display the widget in terms of other, lower level widget
    • 简单来说, Stateful widget有状态,这些状态在widget的生命周期中是可以变化的。一个stateful widget至少由两个类组成,一个是StatefulWidget类,另一个是State类。StatefulWidget类本身是不变的,但是State类中的状态时可能变化的。 一般来说Stateful widget中的build方法写在State类中,用来解耦,否则需要把State类传到build方法中
    • Scaffold是Material库中的页面脚手架,它包括了导航栏,标题,包含主屏幕的widget树的body属性
  5. Widget
    • Flutter中万物都是widget
    • Widget用来描述UI元素的配置信息,配置信息就是widget接收的参数
    • Widget属性应该是final的,因为如果属性变化Flutter就会重新构建widget
    • widget类继承自DiagnosticableTree,DiagnosticableTree主要作用是提供调试信息
    • Flutter框架处理流程
      1. 根据Widget树生成Element树,Element树中的节点继承自Element类。Widget树和Element树是一一对应的
      2. Element树生成Render树,Render树中的节点继承自RenderObject类。Render树包含了真正的布局和渲染逻辑。Element树和Render树不是一一对应的
      3. Render树生成Layer树,Layer树中的节点继承自Layer类
    • 按照惯例widget的constructor使用named parameters
    • StatelessWidget
      • 用于不需要维护状态的场景,通常在build方法中嵌套其他widget来构建UI
      • BuildContext表示当前widget在widget树中的上下文,每一个widget对应一个context对象,可以遍历widget树查找其他widget
    • StatefulWidget
      • createState()用来创建和StatefulWidget相关的状态
    • State
      • 常用属性: widget, context
      • 生命周期
        • initState(): widget第一次插入到widget树的时候调用,只调用一次
        • didChangeDependencies(): 当State对象的一来发生变化的时候调用
        • build(): 用于构建widget子树,返回一个widget
        • reassemble"(): 在开发模式下热重载时会调用,该回调用于重新构建widget
        • didUpdateWidget(): 当widget重新构建时,Flutter framework会调用Widget.canUpdate来检测Widget树中同一位置的新旧节点,然后决定是否需要更新,如果需要更新则会调用此回调
        • deactivate(): 当State对象从树中被移除时,会调用此回调
        • dispose(): 当State对象从树中被永久移除时调用,通常在此回调中释放资源
    • 在widget中获取state对象:
      • 私有state: Type _state = context.findAncestorStateOfType<Type>()
      • 非私有state: Type _state = Type.of(context), 这里需要Type提供一个of静态方法
      • 给StatefulWidget添加一个GlobalKey: static GlobalKey<Type> _globalKey = GlobalKey();, 通过_globalKey.currentState获取state对象
    • 通过RenderObject自定义widget
      • Stateful和Stateless widget都是用于组合其他组件的,它们本身没有对应的RenderObject
      • Flutter组件库中的很多基础组件不是通过Stateful/Stateless widget实现的,而是通过RenderObject实现的
      • 如果组件不包含子组件,则可以继承LeafRenderObjectWidget,如果组件包含子组件,则可以继承RenderObjectWidget, 再通过重写方法就可以串键对应的组件
    • 常用基础组件
      • 用法: import 'package:flutter/widgets.dart';
      • Text: 带格式的文本
      • Row/ Column: 类似flexbox
      • Stack: 取消线性布局,使用Positioned来定位与相对于Stack的上下左右四条边的位置
      • Container: 创建矩形视觉元素, 有margins, padding等
    • Material组件
      • 用法: import 'package:flutter/material.dart';
      • Scaffold, AppBar, TextButton
    • Cupertino组件
      • 用法: import 'package:flutter/cupertino.dart';
      • CupertinoApp, CupertinoButton, CupertinoNavigationBar
  6. 状态管理
    • 如果状态是用户数据,如复选框的选中状态、滑块的位置,则该状态最好由父widget管理
    • 如果状态是有关界面外观效果的,如动画,那么状态最好由widget本身管理
    • 如果状态是有关界面布局的,如文本框的宽度,则该状态最好由父widget管理
  7. 路由管理
    • 路由(Route)在移动开发中通常指页面(Page),Route 在 Android中 通常指一个 Activity,在 iOS 中指一个 ViewController
    • 路由管理,就是管理页面之间如何跳转,通常也可被称为导航管理
    • Flutter中的路由管理和原生开发类似,会维护一个路由栈,路由入栈(push)操作对应打开一个新页面,路由出栈(pop)操作对应页面关闭操作,而路由管理主要是指如何来管理路由栈
    • 一个例子
      1
      2
      3
      4
      5
      6
      7
      8
      TextButton(
      child: const Text("Navigation."),
      onPressed: () {
      Navigator.push(context, MaterialPageRoute(builder: (context) {
      return NewPage();
      }));
      },
      ),
    • MaterialPageRoute
      • class MaterialPageRoute<T> extends PageRoute<T> with MaterialRouteTransitionMixin<T>, 这个类继承了PageRoute,PageRoute是一个抽象类,它定义了路由构建及切换时过渡动画的相关接口和属性。MaterialPageRoute的页面切换动画和原生平台风格一致
      • MaterialPageRoute({ WidgetBuilder builder, RouteSettings settings, bool maintainState = true, bool fullscreenDialog = false})
        • builder是一个回调函数,作用是构建路由页面的具体内容,返回值是一个widget
        • settings包含路由的配置信息,如路由名称、是否初始路由(首页)
        • maintainState默认情况下,当入栈一个新路由时,原来的路由仍然会被保存在内存中
        • fullscreenDialog表示新的路由页面是否是一个全屏的模态对话框
      • 如果需要自定义路由切换动画,可以自己继承PageRoute来实现
    • Navigator
      • Navigator是一个路由器管理的组件,它提供了打开和退出路由页面的方法
      • Navigator.push(BuildContext context, Route route):打开route对应的路由页面,返回一个Future对象,当路由出栈关闭时,Future才会结束
      • Navigator.pop(BuildContext context, [ result ]):关闭当前路由页面,返回上一个路由页面,result是关闭时传递给上一个路由页面的数据
      • Navigator类中第一个参数为context的静态方法都对应一个Navigator的实例方法, 比如Navigator.push(BuildContext context, Route route)等价于Navigator.of(context).push(Route route)
    • 路由器传值
      • Navigator.pop()方法可以传一个参数给上一个路由页面,这个参数可以是任意类型,如String、int、bool、Map等。这时候需要上一个路由页面使用async方法和await来接受Navigator.push()方法Future的返回值
      • 注意直接点击导航栏是不会触发Navigator.pop()方法的,所以需要在onWillPop回调中处理
    • 命名路由
      • 对于据大多数应用来说,不推荐使用命名路由
      • 给路由器起一个名字,然后通过名字直接打开新的路由, 需要一个Map<String, WidgetBuilder> routes的路由表
      • 在App中配置routes属性, 比如
        1
        2
        3
        4
        5
        6
        routes: {
        // new_page
        "new_page": (context) => NewPage(),
        // HomePage
        "/": (context) => MyHomePage(title: 'Flutter Demo Home Page')
        },
      • 使用Navigator.pushNamed(BuildContext context, String routeName,{Object arguments})打开新的路由
      • 传递参数
        • pushNamed中传递参数
        • 在路由页面通过var args = ModalRoute.of(context)!.settings.arguments获取参数
    • 路由器生成钩子
      • onGenerateRoute在打开命名路由的时候可能调用:如果路由表中没有注册,才会调用,如果注册了,就会直接调用路由的builder方法
      • 在进入页面前需要进行权限控制的时候(比如判断是否登录), 可以不使用路由表,而是在APP中重写这个方法
        1
        2
        3
        4
        5
        6
        7
        onGenerateRoute:(RouteSettings settings){
        return MaterialPageRoute(builder: (context){
        String routeName = settings.name;
        // 如果访问的路由页需要登录,但当前未登录,则直接返回登录页路由,
        // 引导用户登录;其他情况则正常打开路由。
        });
        }
  8. 包管理
    • Flutter通过配置pubspec.yaml文件来管理依赖包
    • dependencies下面添加依赖包
    • dev_dependencies下面添加开发依赖包
    • Pub(https://pub.dev/ )是 Google 官方的 Dart Packages 仓库
    • 还可以使用本地包,和Git仓库中的包
      1
      2
      3
      4
      5
      6
      7
      8
      9
      dependencies:
      pkg1:
      # path of package
      path: ../../code/pkg1
      pkg2:
      # git url of package
      git:
      # if the package is not in the root path, you can use the path attribute
      url: git://github.com/xxx/pkg1.git
  9. 资源管理
    • Flutter APP 安装包中会包含代码和 assets(资源)两部分。Assets 是会打包到程序安装包中的,可在运行时访问。常见类型的 assets 包括静态数据(例如JSON文件)、配置文件、图标和图片等
    • pubspec.yaml文件中配置资源文件
      1
      2
      3
      assets:
      - assets/images/
      - assets/config.json
    • 在代码中加载资源文件
      • 加载文本文件
        1
        2
        3
        Future<String> loadAsset() async {
        return await rootBundle.loadString('assets/config.json');
        }
      • 加载图片
        1
        2
        3
        4
        5
        6
        7
        8
        9
        Widget build(BuildContext context) {
        return DecoratedBox(
        decoration: BoxDecoration(
        image: DecorationImage(
        image: AssetImage('graphics/background.png'),
        ),
        ),
        );
        }
    • 设置APP图标
      • Android中在.../android/app/src/main/res修改
      • iOS中在.../ios/Runner/Assets.xcassets/AppIcon.appiconset中修改
    • 修改启动页图片
      • Android中在.../android/app/src/main/res/drawable/launch_background.xml中修改
      • iOS中在.../ios/Runner/Assets.xcassets/LaunchImage.imageset中修改

Basic Flutter Widgets

  1. Text
    • Text用于显示简单样式文本,包含一些控制文本样式的一些属性
    • Text("<textContent>", textAlign: <align>, maxLines: n, overflow: <overflow>, textScaleFactor: <scale>)
      • textAlign: TextAlign.left, TextAlign.right, TextAlign.center, etc. 对齐参考的是Text的宽度,如果只有一行,那么就是整个textContent的宽度
      • maxLines: 最大行数, 默认自动换行,指定这个参数之后,文本不会超过n行
      • overflow: 超出显示方式,TextOverflow.clip裁剪,TextOverflow.fade渐隐,TextOverflow.ellipsis省略号
      • textScaleFactor: 字体缩放因子,相当于style中的fontSize
    • TextStyle
      • 用于指定文本显示的样式
      • 用法:Text(..., style: TextStyle(key: val,...))
      • 属性: color, fontSize, height (是个倍数因子,相当于行高 = fontSize * height), fontFamily, background
    • TextSpan
      • 对于Text的内容的不同部分按照不同的样式显示
      • 用法:Text.rich(TextSpan(children: [TextSpan(text: "<content1>", style: TextStyle(...)), TextSpan(text: "<content2>", style: TextStyle(...))]))
    • DefaultTextStyle
      • 注意到父级别的设置会被子级别继承,因此可以用DefaultTextStyle作为父级别,给它设置一个默认样式,然后所有的子级别Text都会继承这个样式
      • 用法:DefaultTextStyle(style: TextStyle(...), child: Text(...)),这里child可以是多个Text的列表之类的,他们都会继承在style中定义的样式,如果子级别不想继承父级别的样式,可以在它自己的TextStyle中加入属性inherit: false
    • Font
      • 如果需要外部字体,首先需要修改pubspec.yaml文件,添加字体文件的路径
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        flutter:
        fonts:
        - family: <fontFamily>
        fonts:
        # 下面代表了不同的写法
        - asset: <fontPath>
        - asset: <fontPath>
        weight: <weight>
        - asset: <fontPath>
        weight: <weight>
        style: <style>
      • 在代码中使用字体
        1
        2
        3
        4
        5
        6
        7
        Text(
        "Hello World",
        style: TextStyle(
        fontFamily: <fontFamily>,
        fontSize: 18,
        ),
        )
      • 使用package中的字体
        1
        2
        3
        4
        5
        6
        7
        Text(
        "Hello World",
        style: TextStyle(
        fontFamily: <fontFamily>,
        package: <package>,
        ),
        )
  2. Button
    • Material组件库中有不同的按钮,比如ElevatedButton, TextButton, OutlineButton
    • Material Button的相同点
      • 按下按钮都会有水波动画
      • 按钮都有一个onPressed属性,用于设置点击事件,没设置这个callback不影响点击
    • ElevatedButton
      • 默认带有阴影和灰色背景,按下按钮之后,阴影会变大
      • 用法:ElevatedButton(child: <child>, onPressed: <callback>)
    • TextButton
      • 默认背景透明不带阴影,按下之后会有背景色
      • 用法:TextButton(child: <child>, onPressed: <callback>)
    • OutlineButton
      • 默认有一个边框,不带阴影并且背景透明,按下后,边框颜色会变亮、同时出现背景和阴影(较弱)
      • 用法:OutlineButton(child: <child>, onPressed: <callback>)
    • IconButton
      • 是一个可点击的Icon,不包括文字,默认没有背景,点击后会出现背景
      • 用法:IconButton(icon: <icon>, onPressed: <callback>)
      • icon举例:Icon(Icons.add), Icon(Icons.send), Icon(Icons.thumb_up), Icon(Icons.info), Icon(Icons.favorite)
    • ElevatedButton, TextButton, OutlineButton都有一个icon构造函数,通过它门可以轻松创建带图标的按钮
      • 用法:XXXXXButton.icon(onPressed: <callback>, icon: <icon>, label: <label>)
  3. Image and ICON
    • 通过Image组件加载并且显示图片,数据源可以是asset,文件,内存和网路
    • ImageProvider是一个抽象类,主要定义了图片数据获取的接口load(),从不同数据源获取图片需要不同的ImageProvider,比如AssetImage, NetworkImage
    • Image
      • 必选属性image,对应了一个ImageProvider
      • 在asset中加载图片
        • 图片放到/images/下面
        • pubsepc.yaml中添加图片路径
        • Imageimage属性中加入AssetImage("<assetPath>"), 或者使用Image.asset("<assetPath>", ...)
      • 在网路加载图片:在Image的image属性中加入NetworkImage("<URL>"),或者使用Image.network("<URL>", ...)
      • 其他参数 width, height (如果不指定长宽则会根据parent大小限制尽可能显示图片原始大小),fit(值fill, cover, contain, fitWidth, fitHeight, none,在不指定长宽的时候大小适应模式), color, colorBlendMode(混合模式,值是BlendMode.<attr>), repeat(值是ImageRepeat.<attr>, 图片重复方式)
    • Flutter对加载过的图片会缓存在内存中
    • ICON
      • 默认有Material Design的图标,需要在pubsepc.yaml中配置
        1
        2
        flutter:
        uses-material-design: true
      • 使用Icon组件加载图标: Icon(Icons.<attr>, color: Colors.<attr>)
  4. Switch/Checkbox/Radio
    • SwitchCheckbox都是StatefulWidget,但是他们本身不保存当前选中的状态,选中状态由parent管理。当点击的时候会触发onChanged回调改变逻辑
    • Switch和Checkbox都有value属性,代表当前状态,value初始值可以自选true和false, 在onChanged回调中通过setState改变状态
    • Switch和Checkbox都有activeColor属性,代表选中的时候的状态
    • Radio是互斥多选框中的一个,有value属性和groupValue属性,如果这两个相同就回被选中,在onChanged回调中通过setState把groupValue修改成当前value。一般多个多选框可以用ColumnListTile(一行中有Text和ICON)完成
  5. TextField and Form
    • TextField用于文本输入
      • controller属性用来获取/编辑文本内容,监听时间等,一般是显示提供,否则会自动重建
      • inputDecoration属性用来控制显示外观
      • keyboardType属性用书设置输入的键盘类型,比如TextInputType.numberTextInputType.emailAddressTextInputType.textTextInputType.multiline
      • style控制文本样式,textAlign控制文本水平对齐,autofocus是否自动获取焦点, obscureText控制是否隐藏文本,适用于密码等, maxLines最大行数,默认1,如果是null就是没有行数限制,maxLengthmaxLengthEnforcement控制最大长度和截断方式, toolbarOptions控制长按或者右键出现的菜单, onChange内容改变时的回调,可以通过controller完成,onEditingCompleteonSubmitted输入完毕时的回调
      • 一个例子
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        Column(
        children: const <Widget>[
        TextField(
        // 自动获取焦点,会弹出键盘
        autofocus: true,
        // 输入文本的样式
        decoration: InputDecoration(
        labelText: "Username",
        hintText: "Username or email",
        // 前置图标
        prefixIcon: Icon(Icons.person),
        ),
        ),
        TextField(
        decoration: InputDecoration(
        labelText: "Password",
        hintText: "Password",
        prefixIcon: Icon(Icons.lock),
        ),
        obscureText: true,
        ),
        ],
        )
      • 使用controller获取输入内容
        1
        2
        3
        4
        5
        6
        TextEditingController _controller = TextEditingController();
        TextField(
        controller: _controller,
        )
        // Get TextField content
        print(_controller.text);
      • 监听文本变化
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        // Approach 1
        TextField(
        onChanged: (value) {
        print("onChanged: ${value}");
        }
        )
        // Approach 2
        TextEditingController _controller = TextEditingController();
        @override
        void initState() {
        super.initState();
        _controller.addListener(() {
        print("addListener: ${_controller.text}");
        });
        }
      • 控制焦点变化: FocusNode管理焦点, FocusScopeNode在输入框之间移动焦点,设置默认焦点
        • Step1: 创建不同的FocusNode实例,并且关联给不同输入框的focusNode属性
        • Step2: FocusScope.of(context).requestFocus(<FocusNode实例>), 其中FocusScope.of(context)获取了FocusScopeNode实例
        • 让输入框失去焦点: focusNode实例.unfocus()
        • 获取当前节点是否有焦点: focusNode实例.hasFocus
        • 获取有焦点的节点: FocusScope.of(context).focusedChild
    • Form
      • autovalidate: 对于表单每一个子内容输入是否校验, onWillPop决定Form所在的路由是否直接返回,这个回调返回一个Future,通常用来拦截返回按钮, onChanged如果任意一个表单子内容改变会触发这个回调
      • Form的子元素必须是FormField类型,这是一个抽象类,它的子类有TextFormFieldDropdownButtonFormField
      • FormState是Form的State类,可以通过Form.of(context)或者GlobalKey获得
        • FormState.validate() 用来调用每一个FormFieldvalidate方法,如果有一个返回false,那么整个表单就会返回false
        • FormState.save() 用来调用每一个FormFieldsave方法,用来保存表单内容
        • FormState.reset() 用来调用每一个FormFieldreset方法,用来重置表单内容
      • 对于每一个Form添加验证,可以通过validator属性,这个属性是一个回调函数,返回一个字符串,如果为空则表示验证通过,否则验证失败
      • 对于表达提交验证
        1
        2
        3
        4
        // globalKey.currentState 拿到FormState,调用validate方法会调用每一个FormField的validate方法
        if ((globalKey.currentState as FormState).validate()) {
        // Submit form
        }
  6. ProgressIndicator
    • Material组件库中有LinearProgressIndicatorCircularProgressIndicator两种进度指示器, 他们都有精确进度指示和模糊进度指示
    • LinearProgressIndicator
      • value属性在[0,1] 之间,如果是null则表示模糊进度(一个循环动画)
      • backgroundColor属性表示进度条背景色
      • valueColor属性表示进度条颜色,它是一个Animation<Color>类型,可以通过AlwaysStoppedAnimation(Colors.<颜色>)来指定一个固定的颜色
    • CircularProgressIndicator
      • value, backgroundColor, valueColor属性与LinearProgressIndicator一致, strokeWidth属性表示进度条的宽度,默认是4.0
    • LinearProgressIndicator和CircularProgressIndicator都是用父容器的尺寸作为边界的,因此可以使用SizedBox当作parent,从而可以指定进度条大小的尺寸

Layout Widgets

  1. 布局原理和约束
    • Flutter有两种布局模型: RenderBoxRenderSiler
      • 布局流程: 上层组件向下传递约束条件,下层组件确认自己的大小并告诉上层组件,上层组件确认下层组件相对于自身的偏移并确认自身的大小
      • RenderBox模型中,Widget对应的渲染对象继承于RenderBox类,父组件传递给子组件的约束信息通过BoxConstraints描述
    • BoxConstraints
      • 默认属性: minWidth =0.0, maxWidth = double.infinity, minHeight = 0.0, maxHeight = double.infinity
      • 其他方法: BoxConstraints.tight(Size size)生成固定宽高的限制,BoxConstraints.expand()可以生成一个尽可能大的用以填充另一个容器的BoxConstraints
    • ConstrainedBox: 用来对于子组件添加额外的约束。用法ConstrainedBox(constraints: BoxConstraints(<>), child:Container(<>))
    • SizedBox(width:<>, height: <>, child: <>)
    • UnconstrainedBox: 一般A包含B,B包含C,会导致B把A的约束传给C,如果不想让A影响到C,那么需要B是UnconstrainedBox,注意这样A仍然会影响B
  2. 线性布局(Row, Column)
    • Row水平是主轴,Column垂直是主轴
    • MainAxisAlignment是主轴对齐,CrossAxisAlignment是交叉轴对齐
    • RowColumn的属性
      • textDirection = TextDirection.ltr/rtl表示文字方向
      • mainAxisSize = MainAxisSize.max/min表示主轴占用的空间, 默认是max,也就是占满主轴空间
      • MainAxisAlignment.start/center/end 具体取决于textDirection的方向
      • CrossAxisAlignment.start/center/end/stretch表示交叉轴对齐方式, 取决于verticalDirection.up/down的方向
      • 对于交叉轴,长度取决于最大子元素的长度
    • 嵌套Row/Column: 对于嵌套的情况,只有最外层默认主轴占满,内层占用实际大小空间,如需要内层占满外层主轴,需要使用Expanded包裹内层
  3. 弹性布局(Flex)
    • Row和Column都继承了Flex
    • Flex可以和Expanded配合实现弹性布局。Expanded只能作为Flex的孩子组件
    • 用法: Flex(direction: Axis.horizontal/vertical, children: <Widget>[Expanded(flex: <int数字>, child: <Widget实现类>), ...])
    • Spacer(flex: <int数字>)是Expanded的简化版,作用是占据空间,但是不显示任何内容
  4. 流式布局(Wrap, Flow)
    • 对于线性布局如果超出屏幕范围会报错,对于流式布局超出屏幕范围会自动换行。WrapFlow都是流式布局
    • Wrap
      • direction = Axis.horizontal/vertical表示主轴方向
      • alignment = WrapAlignment.start/center/end/spaceAround/spaceBetween/spaceEvenly表示主轴对齐方式
      • runAlignment = WrapAlignment.start/center/end/spaceAround/spaceBetween/spaceEvenly表示交叉轴对齐方式'
      • spacing = <double>表示主轴方向子Widget的间距
      • runSpacing = <double>表示交叉轴方向子Widget的间距
    • Flow
      • 一般首先考虑Wrap是否能满足对象,Flow很少使用,因为需要自己实现Widget的位置转换
      • Flow主要用于一些需要自定义布局策略或者性能要求比较高的场景
        • Flow性能好,对于子组件的尺寸和位置调整效率非常高
        • Flow比较灵活,需要自己计算每一个组建的位置,可以自定义布局策略
        • Flow使用比较复杂
        • Flow不能自动适应子Widget的大小,需要通过指定父容器大小或者实现TestFlowDelegategetSize(BoxConstraints constraints)来指定大小
    • 注意ListTile(title, leading)本身和Chip(avatar, label)差不多,但是ListTile自己独占一行,Chip可以一行多个
  5. 堆叠布局(Stack, Positioned)
    • 层叠布局中子组件可以通过父容器四个角的位置来确定自身位置,这和Web中的绝对定位,Android中的Frame布局是类似的
    • Flutter中使用StackPositioned来实现绝对定位。Stack允许子组件堆叠,Positioned用于根据Stack的四个角来确定子组件的位置
    • Stack
      • alignment = AlignmentDirectional.top/left/right/bottom/Start 决定如何对齐没有使用Positioned或者部分定位的子组件
      • textDirection = TextDirection.ltr/rtl 决定alignment对齐的参考系
      • fit = StackFit.loose/expand 决定没有定位的子组件如何去适应Stack的大小,loose表示使用子组件的大小,expand表示扩伸到Stack的大小
      • clipBehavior = Clip.hardEdge等,决定对超出Stack显示空间的部分如何裁剪,hardEdge表示直接裁剪
    • Positioned
      • left/right/width三选二,top/bottom/height三选二
  6. 对齐和相对位置(Align)
    • Align
      • Align可以简单的调整一个元素在父元素中的位置
      • alignment = Alignment(x, y)表示相对于父元素的位置, Alignment(0, 0)是中心,Alignment(-1, -1)是左上角,Alignment(1, 1)是右下角
      • widthFactor/heightFactor表示相对于父元素的宽高比例
    • Center
      • Center继承自Align, 比Align少了一个alignment参数
  7. LayoutBuilder, AfterLayout
    • 通过LayoutBuilder可以在布局过程中拿到父组件传递的约束信息,然后根据约束信息动态的构建不同的布局
    • 比如可以使用 LayoutBuilder 来根据设备的尺寸来实现响应式布局
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      @override
      Widget build(BuildContext context) {
      return LayoutBuilder(
      builder: (BuildContext context, BoxConstraints constraints) {
      if (constrains.maxWidth < <constraint>) {
      // do something;
      } else {
      // do something else;
      }
      },
      );
      }
    • AfterLayout可以在布局完成后执行回调,比如可以获取组件大小和相对于屏幕/父组件的位置坐标
    • Flutter的build和layout是可以交错执行的,并不是严格的先执行build再执行layout

Container Widget

  1. 填充(Padding)
    • Padding可以给其他子节点填充(空白),和边距效果类似
    • 用法:Padding(EdgeInsetsGeometry padding, Widget child), 一般来说使用EdgeInsets, 因为他提供了一些设置填充的便捷方法
    • EdgeInset便捷方法
      • fromLTRB(double left, double top, double right, double bottom):分别指定四个方向的填充
      • all(double value): 所有方向均使用相同数值的填充
      • only({key: value}): 指定某个方向的填充,可以有多个方向,key是LTRB中的一个
      • symmetric({vertical, horizontal}): 分别指定垂直方向和水平方向的填充
  2. 装饰容器(DecoratedBox)
    • DecoratedBox可以在组件上绘制一些装饰,比如背景,边框,渐变等等
    • 属性
      • decoration = BoxDecoration():装饰的样式,可以通过BoxDecoration来指定
      • position = DecorationPosition.background/foreground:指定装饰的绘制位置,bg是背景装饰,fg是前景装饰
    • BoxDecoration是Decoration的子类,一般decoration属性都是用BoxDecoration。可以定义color, image, border(对边用BorderSide来描述), borderRadius(这个只能用在统一border上), gradient(渐变), backgroundBlendMode, shape等属性
  3. 变换(Transform)
    • Transform可以在其子组件绘制时对其应用一些矩阵变换来实现一些特效
    • Transform.translate(offset: Offset(x, y), child: child) 平移
    • Transform.rotate(angle: f(math.pi), child: child) 旋转, 注意需要导入import 'dart:math' as math;
    • Transform.scale(scale: 1.5, child: child) 缩放
    • 注意的是Transform实在绘制阶段而不是布局(layout)阶段,所以无论子组件如何变化,其占用的空间大小和在屏幕中的位置是固定不变的,因为这些是在布局阶段确定的。比如Row的一个子组件经过了Transform变化,那么其他子组件的位置是不会变的,因为这些是在布局阶段确定的
    • RotatedBoxTransform.rotate功能类似,但是他是在布局阶段就确定了,所以会影响其他子组件的位置
  4. 容器组件(Container)
    • Container是一个组合类容器,本身不对应具体的RenderObject,它是多个组件组合的多功能容器,只通过一个Container组件可以同时实现装饰,变换,限制的场景。
    • height, width优先于constraints
    • colordecoration不能同时存在
    • marginpadding实际上都是通过padding来实现的,只不过margin是在父容器中,而padding是在自身中
  5. 裁剪(Clip)
    • 裁剪类组件: ClipOval, ClipRRect, ClipRect, ClipPath
    • 通过继承CustomClipper并且重写getClip()方法和shouldReclip()方法
  6. 页面骨架(Scaffold)
    • AppBar导航栏骨架
      • title导航栏标题
      • leading导航栏左侧按钮,通常是抽屉按钮。如果设置了drawer属性,则此属性默认为抽屉按钮
      • actions导航栏右侧菜单,比如分享链接
      • bottom导航栏底部菜单,通常是Tab按钮组
      • elevation导航栏阴影
      • backgroundColor导航栏背景颜色
      • centerTitle 标题是否居中
      • drawer左侧抽屉菜单,endDrawer右侧抽屉菜单
    • MyDrawer抽屉菜单
    • BottomNavigationBar 底部导航栏, 它的items属性是一个List<BottomNavigationBarItem>,每个BottomNavigationBarItem都有icontitle属性
    • FloatingActionButton 浮动按钮, 通过floatingActionButtonLocation指定悬浮按钮在页面中的位置
    • 可以配合使用BottomAppBarFloatingActionButton来实现一个打洞的导航栏。bottomNavigationBar: BottomAppBar,在BottomAppBar的shape属性设定打洞的形状,比如CircularNotchedRectangle(), 在child属性中放一个空的SizedBox()FloatingActionButton的floatingActionButtonLocation属性设置在这个洞的位置即可
    • body页面内容,是一个Widget

Slidable Widgets

  1. RenderSilver按需求加载列表布局
    • 通常可滚动组建的子组件可能非常多,占用的总高度非常大,因为一般子组件不会一次性构建出来
    • RenderSilver模型使用了基于Sliver的列表按需加载模型,只有当Silver出现在视觉中才会去构建它
    • Flutter中的可滚动组件主要有三个角色构成: Scrollable, Viewport, Silver
      • Scrollable用户处理滑动手势,确定滑动便宜,滑动偏移变化时构建Viewport
      • Viewport显示视窗,也就是列表的可视区域
      • Silver视窗中显示的元素
    • Scrollable
      • axisDirection = AxisDirection.down/right滑动方向
      • physics接受ScrollPhysics对象,Flutter默认有ClampingScrollPhysics滑到边界的时候不能继续滑动,BouncingScrollPhysicsiOS的弹性效果
      • controller接受ScrollController对象,控制滚动位置和监听滚动事件,默认是PrimaryScrollController
      • viewportBuilder用来构建Viewport的回调
    • Viewport
      • ViewportOffset offset描述了窗口的偏移量,也就是应该显示的内容
      • cacheExtentcacheExtentStyle用来控制渲染长度,对于Viewport最终渲染区域长度是cacheExtent * viewport
    • Silver: Silver对应的渲染对象是RenderSilver,它的约束是SilverConstraints
    • 可滚动组件通用配置: scrollDirection(主轴), reverse(是否在scrollDirection上面反向滑动), controller, physics, cacheExtent
    • Scrollbar
      • child属性对应可滚动组件
      • 这是Material风格的滚动条,对于iOS上面的Cupertino风格会自动替换成CupertinoScrollbar
  2. SingleChildScrollView
    • 一般在期望内容不超过屏幕太多的时候使用,否则性能很差
  3. ListView
    • 最常用可滚动组件
    • 常用参数
      • itemExtent:子组件在滚动方向上的长度,如果为null则子组件自己决定长度,但是一般指定一个值
      • prototypeItem: 用来计算子组件长度,和itemExtent不能同时存在
      • shrinkWrap: 是否根据子组件的总长度来设置ListView的长度,默认为false,如果容器是无边界的话,那么必须是true
      • padding: 内边距
      • children: 子组件列表,适合子组件不多的情况,反之(子组件很多或者不确定数量)使用ListView.builder
      • itemBuilder: 用来生成子组件
      • separatorBuilder: 用来生成分割线
    • ListView.builder(itemCount: <cnt>, itemExtent: <len>, itemBuilder: (context, index) =><Widget>) 用来生成子组件
    • ListView.separated(itemCount: <cnt>, itemExtent: <len>, itemBuilder: (context, index) =><Widget>, separatorBuilder: (context, index) =><Widget>) 用来生成子组件和分割线、】
    • 一个无限列表加载的思路
      • 定义一个loading提示和一个加载完毕的提示
      • itemBuilder中最后一个元素的时候判断是否需要加载更多
        • 如果需要加载更多数据,调用函数加载数据,并且最后一个元素显示loading提示
        • 如果不需要加载更多,显示加载完毕的提示
  4. 滚动监听和控制
    • 使用ScrollController来控制可滚动组建的滚动位置
    • ScrollController(initialScrollOffset:<offset>, keepScrollOffset:<true/false>)设定初始滚动位置和是否保存滚动位置
    • 滚动方法
      • jumpTo(double offset): 跳转到指定位置
      • animateTo(double offset, ...): 动画的方式跳转到指定位置
    • 滚动监听,因为ScrollController继承了Listenable,所以可以加一个监听controller.addListener(() => {})
    • 一个显示返回按钮的滚动控制组件写法
      • 定义显示按钮状态,初始为false
      • iniState方法中添加监听,判断offset和显示状态,从而更改显示状态。 先super.initState(),然后controller.addListener(() => {})
      • dispose方法中移除监听,避免内存泄漏。先controller.dispose(),然后super.dispose()
      • build方法中根据显示状态显示按钮,按钮使用floatingActionButton并且在点击的时候重置offset
    • 保存滚动位置
      • PageStorage可以用来存储滚动的位置
      • 对于不同的滚动组件,需要指定不同的PageStorageKey
    • 使用ScrollBarNotificationListener来进行滚动监听
      • NotificationListener可以在不同地方监听滚动事件,而ScrollController只能在和具体的滚动组件关联之后才可以
      • NotificationListener可以收到更多的滚动事件(ScrollMetrics metrics属性中包含了pixels当前滚动位置,maxScrollExtent最大滚动长度,atEdge是否到底等),而ScrollController只能获取当前滚动位置
  5. AnimatedList
    • 和ListView相似,但是可以再插入和删除节点时执行一个动画
    • 添加和删除方法insertItem(int index, {Duration duration}), removeItem(int index, AnimatedListRemovedItemBuilder builder, { Duration duration})
    • itemBuilder: (context, index, animation) {} 用来在创建item的时候执行的动画,会被insertItem调用,removeItem里面会传入一个删除item的时候执行的动画的builder
    • 一个例子: 点击按钮添加新的item,点击item的删除icon删除这个icon
      • 用一个counter记录当前数据的数量,用一个list保存初始数据,用一个GlobalKey用来在AnimatedList之外获取state然后调用插入和修改方法
      • 插入: 修改counter, 修改数据list,globalKey.currentState!.insertItem(data.length - 1)
      • 删除: 修改counter, 修改数据list,globalKey.currentState!.removeItem(index, builder: (context, animation) {return <Transition>} ...), 如果什么效果都不想要,直接return一个空的widget即可
      • 重置list:首先清空数据列表,在循环中调用removeItem(<args>),然后初始化数据列表,初始化counter,在循环中调用insertItem(<args>)(相当于initState中循环多了insertItem的调用)
  6. GridView
    • 常见布局类型,是实现了网格布局的组件。和ListView很多属性相同, gridDelegate用来控制子组件如何排列
    • SilverGridDelegate是一个抽象类,定义了GridView布局的相关接口,子类需要通过实现它们来实现具体的布局算法。Flutter中提供了两个子类SliverGridDelegateWithFixedCrossAxisCount, SliverGridDelegateWithMaxCrossAxisExtent
  7. PageView与页面缓存
    • 如果需要实现页面切换和Tab布局(Tab换页,图片轮动,上下滑动页面切换等),可以使用PageView组件
    • PageView的children里面放的是不同的Widget(不同页面),设置不同滑动方向来进行页面切换
  8. 可滚动组件子项缓存
    • ListView中addAutomaticKeepAlives: true会为每一个表项添加AutomaticKeepAlive父组件,从而将对应的RenderObject的keepAlive标记成true和false,从而决定是否缓存
    • 当keepAlive是false的时候,如果列表项滑出加载区域的时候,列表组件将会被销毁, 当是true的时候会缓存,列表进入加载区域的时候会首先检查缓存
    • keepAlive从true修改成false的时候需要释放缓存
    • AutomaticKeepAlive
      • Flutter中AutomaticKeepAlive设置组件keepAlive的时机由开发者决定。AutomaticKeepAlive相当于一个server,子组件相当于client,AutomaticKeepAlive收到子组件的统治之后会修改子组件的keepAlive状态,同时进行一些必要的资源清理工作
      • 需要类混入AutomaticKeepAliveClientMixin,在build方法中调用super.build(context)(这里调用了AutomaticKeepAliveClientMixin的方法,根据wantKeepAlive的值给AutomaticKeepAlive发送信息让其工作),并且在wantKeepAlive返回true
  9. TabBarView
    • TabBarView提供了Tab布局组件,通常和TabBar配合使用
    • TarBarView封装了PageView(可以有页面缓存),TabController用来监听和控制TabBarView的页面切换
    • TabBar一般在AppBar最底部(AppBar的bottom属性),和TabBarView的controller共同使用的话,使用同一个TabController
    • 两种写法
      • initState中初始化TabController,dispose中释放资源,然后在build中对应到TabBar和TabBarView
      • 直接在build中创建一个DefaultTabController,它的child是一个页面,此时TabBar和TabBarView的controller不需要显示指定,它们会自动在组件树上向上查找饼使用最近的一个DefaultTabController
  1. 导航栏返回拦截(WillPopScope)
    • 为了防止用户误触返回按钮导致APP退出可以设置返回按钮拦截,WillPopScope可以用来实现这一点
    • WillPopScope( WillPopCallback onWillPop, Widget child), 其中onWillPop回调在点击返回按钮和物理返回按钮的时候触发,返回Future对象,只有Future最终是true的时候路由才会返回
    • 一个onWillPop的例子
      1
      2
      3
      4
      5
      6
      7
      8
      onWillPop: () async {
      if (_lastPressedAt == null || DateTime.now().difference(_lastPressedAt) > Duration(seconds: 1)) {
      // Prevent user from leaving the page by accident
      _lastPressedAt = DateTime.now();
      return false;
      }
      return true;
      }
    • 使用的时候WillPopScope的child设置成Scaffold即可
  2. InheritedWidget
    • Inherited可以在Widget树中共享数据,比如Flutter SDK中的Theme和Locale就是通过InheritedWidget共享的
    • 使用InheritedWidget可以直接在两个Widget之间共享数据,不需要在多层Widget的每一层之间都传递同一个数据
    • 使用方法
      • 自定义class继承InheritedWidget
      • 重写of, maybeOf方法(返回值是否允许为null)
      • 重写updateShouldNotify方法,当数据发生变化的时候,返回true,此时子类会重新渲染。这个方法的参数类型改成自定义的class类型
      • 在子类中使用class.of(context).field来获取继承的数据
  3. 跨组件状态共享
    • 对于跨足剑状态共享,一般通过各个组件共同的父元素来管理
    • EventBus
      • 这是Flutter的全局事件总线
      • 它是一个观察者模式:状态持有方更新,发布状态,状态使用方监听状态变化
      • 缺点是需要手动定义每一个事件,然后再持有方和使用方手动注册和注销事件
    • Provider
      • 思路是InheritedWidget保存跨组件共享的状态,子孙组件引用InheritedWidget来获取状态即可
      • 保存数据: class InheritedProvider<T> extends InheritedWidget
      • 数据发生变化时通过ChangeNotifier来通知子孙组件:class Model extends ChangeNotifier, 状态变化的时候调用notifyListeners()
      • 结合: class ChangeNotifierProvider<T extends ChangeNotifier> extends StatefulWidget
  4. 颜色和主题
    • 颜色
      • 将颜色字符串转换成Color对象,16进制字符串比如0xffdc380d,直接Color(0xffdc380d)即可,普通字符串比如var c = "dc380d",使用Color(int.parse(c,radix:16)|0xFF000000)
      • 颜色亮度,通过color.computeLuminance()可以获取颜色的亮度,返回值是0-1之间的值,0是黑色,1是白色
      • MaterialColor通过[]表示颜色深度,index从50, 100到900,每100表示一种颜色深度,可以使用color.shade<index>来获取具体颜色,比如Colors.blue.shade900
    • 主题
      • Theme可以为MaterialApp定义主题数据,Theme内部使用InheritedWidget来共享样式数据
      • 主题数据在ThemeData中,按照Material Design规范,一些是固定的,一些是可以自定义的
      • ThemeDataprimarySwatch是主题颜色的样本色,可以生成一些其他的属性
      • 使用主题的方法 Theme(data: ThemeData(<args>), child: <Widget>)
      • 复制属性,并且修改一部分: Theme.of(context).copyWith(<args>)在copyWith中传入需要修改的属性即可
  5. 异步UI更新
    • FutureBuilderStreamBuilder可以在异步任务完成时更新UI
    • FutureBuilder
      • FutureBuilder(future:<Future>, initialData: <data>, builder: <builder>)
      • builder是Function (BuildContext context, AsyncSnapshot snapshot),它会在Future构建的不同阶段被多次调用。snapshot含有异步任务的状态信息和结果信息
      • 应用场景:网路请求数据的时候显示加载框,请求完成后显示数据
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        // Return a Future<String> that will resolve after 2 seconds
        Future<String> mockNetworkData() async {
        return Future.delayed(Duration(seconds: 2), () => "Data from network");
        }
        Widget build(BuildContext context) {
        return Center(
        child: FutureBuilder<String>(
        future: mockNetworkData(),
        builder: (BuildContext context, AsyncSnapshot snapshot) {
        // If network request is done
        if (snapshot.connectionState == ConnectionState.done) {
        // If request filed, show error
        if (snapshot.hasError) {
        return Text("Error: ${snapshot.error}");
        } else {
        // If request succeeded, show data
        return Text("Contents: ${snapshot.data}");
        }
        } else {
        // If network request is still ongoing, show loading
        return CircularProgressIndicator();
        }
        },
        ),
        );
        }
    • StreamBuilder
      • 构造函数,builder和FutureBuilder的格式是一样的
      • 应用场景:可以接收多个异步操作的结果,常用于会多次读取数据的异步任务场景,如网络内容下载、文件读写等
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        Stream<int> getStreamData() {
        return Stream.periodic(const Duration(seconds: 4), (i) => (i)).take(5);
        }
        Widget build(BuildContext context) {
        return StreamBuilder<int>(
        stream: getStreamData(),
        builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
        if (snapshot.hasError)
        return Text('Error: ${snapshot.error}');
        switch (snapshot.connectionState) {
        case ConnectionState.none:
        return Text('Empty Stream');
        case ConnectionState.waiting:
        return Text('Awaiting for data...');
        case ConnectionState.active:
        return Text('active: ${snapshot.data}');
        case ConnectionState.done:
        return Text('Stream closed');
        }
        },
        );
        }
  6. 对话框
    • Material库中有不同的对话框,比如AlertDialogSimpleDialog等,它们都使用了Dialog
    • AlertDialog
      • 属性 title, titlePadding, titleTextStyle, content, contentPadding, contentTextStyle, actions(按钮), backgroundColor, shape
      • 关闭对话框: Navigator.of(context).pop(<args>)
    • showDialog()
      • 用法: Future<T?> showDialog(BuildContext context, WidgetBuilder builder, bool barrierDismissible = true)。barrierDismissible表示点击对话框外部是否关闭对话框。如果点击对话框是点击外部关闭的话,返回值是null,否则返回的是Navigator.of(context).pop(<args>)中的args
      • 用法: 自定义函数,返回Future<T?>类型,在函数中调用showDialog<T>(),在showDialog<T>()的builder中返回对话框,对话框中不同按钮的Navigator.of(context).pop(<args>)返回不同T类型值,或者null。在需要的Widget中调用自定义函数,使用await关键字获取返回值
    • SimpleDialog
      • 这个对话框会展示一个列表,用来进行不同选择。具体选项是SimpleDialogOption,选项们组成了children属性
      • 在自定义函数里面可以用switch case来处理不同选项的返回值: switch(await SimpleDialog()) { case 1: ... }
    • 日历选择器
      • Material风格的日历选择器: 自定义函数,在其中返回showDatePicker()。这里涉及到DateTime的格式问题,如果只要日期的话用date.toString().split(' ')[0]
      • iOS风格的日历选择器: 自定义函数,在其中返回showCupertinoModalPopup(),在showCupertinoModalPopup()的builder中返回CupertinoDatePicker

Event and Notification

  1. Pointer Event
    • 完整的指针事件分成三个阶段:按下、移动、抬起,其他事件都是基于这些原始事件的
    • Listener
      • 可以用Listener监听原始触摸事件
      • Listener有onPointerDown, onPointerMove, onPointerUp回调,监听不同阶段的事件
      • PointerEvent的属性position(对全局坐标偏移), localPosition(对本身布局坐标的偏移), delta(对上一次事件的偏移), pressure(压力值), orientation(角度方向)
    • 忽略指针事件: IgnorePointer, AbsorbPointer这两个都会阻止子组件接收指针事件。IgnorePointer使得本组件不接受指针事件,AbsorbPointer允许本组件接受指针事件
  2. Gesture Detector
    • GestureDetector是一个用于手势识别的功能性组件,可以通过它来识别各种手势。它内部封装了Listener
    • 点击事件: onTap点击,onDoubleTap双击,onLongPress长按。注意如果同时监听点击和双击,点击事件会有一个小延迟,因为要判断是否是双击。只监听点击事件的话没有延迟
    • 拖动,滑动事件:
      • onPanDown手指按下触发此事件,onPanUpdate手指滑动触发此事件,onPanEnd手指抬起触发此事件
      • 单一方向滑动可以用onVerticalDragUpdateonHorizontalDragUpdate
      • onScaleUpdate(ScaleUpdateDetails details)缩放事件 details.scale.clamp(minScale, maxScale)缩放倍数
  3. Flutter事件机制
    • 事件处理流程
      1. 命中测试:事件发生的时候,对渲染树进行深度优先遍历,对每一个渲染对象进行命中测试,测试通过的组件会被添加到HitTestResult
      2. 事件分发:命中测试完成后遍历HitTestResult列表,调用每一个对象的事件处理方法,这个过程叫做事件分发
      3. 时间清理:当事件结束或者取消的时候,会首先对时间进行分发,然后清空HitTestResult列表
    • 如果父子组件都监听了同一个事件,子组件会先响应,因为是DFS遍历的
    • 同一个组建的多个GestureDetector是竞争关系的。解决冲突方法:使用Listener,自定义手势识别器(Recognizer)
  4. 事件总线
    • 事件总线通过订阅者模式可以用来进行跨页面广播
  5. 通知Notification
    • Widget树中每一个节点都可以分发通知,通知沿着当前节点向上传递。所有父节点都可以通过NotificationListener来监听通知。这叫做通知冒泡。
    • 通知冒泡可以终止,但是用户触摸事件冒泡不可以终止
    • 自定义通知:自定义类继承NOtification类,使用dispatch(context)分发通知,使用NotificationListener监听通知, NotificationListeneronNotification方法返回值为bool,如果返回true则阻止冒泡,返回false则继续冒泡

Animation

  1. 动画简介
    • 动画原理: 一段时间内多次更改UI外观,从而达到动画效果。流畅度由FPS(Frame Per Second)决定,一般32FPS以上就可以了
    • Flutter中动画抽象
      • Flutter中动画主要涉及Animation, Curve, Controller, Tween
      • Animation
        • 主要是用来保存动画的插值和状态,本身和UI渲染没有关系,在一段时间内依次生成一个区间
        • 常用的是Animation<double>
        • 通过Animation对象的value获取动画当前的状态值
        • 通过Animation监听动画每一帧的执行状态变化addListener(), addStatusListener()
      • Curve
        • 用来描述动画过程,比如线性和非线性
        • 状态Curve.<value>, value可以是linear, decelerate(匀减速), ease(先加速后减速), easeln(先慢后快), easeOut(先快后慢), easeLnOut(先慢后加速再减速)
      • AnimationController
        • 用来控制动画播放,比如forward()开始,stop()reverse()反向播放等
        • AnimationController(duration: Duration(<args>), vsync: this)
        • 默认在duration内从0线性到1,可以用lowerBound, upperBound来控制
        • vsync是一个TickerProvider对象,可以创建Ticker
      • Tween
        • 配合AnimationController使用,可以把其对象值从0.0到1.0修改成执行范围
        • 用法
          1
          2
          3
          4
          5
          6
          final AnimationController controller = AnimationController(
          duration: const Duration(milliseconds: 500),
          vsync: this,
          );
          // 在500毫秒内生成0到255的整数值
          Animation<int> alpha = IntTween(begin: 0, end: 255).animate(controller);

File Processing and Network Request

  1. 文件操作
    • Dart IO库把敖汉了文件读写的相关类,它属于Dart语法标准的一部分。Dart VM下的脚本和Flutter都是通过Dart IO库来操作文件的
    • APP目录: Android和iOS的存储目录不同,可以使用插件PathProvider插件提供了一种平台透明的方式来访问文件系统上的常用位置(在pubspec.yaml中声明path_provider:^<version>)
      • 临时目录: getTemporaryDirectory()
      • 文档目录: getApplicationDocumentsDirectory()
      • 外部存储目录: getExternalStorageDirectory()
    • File, Directory等类都是源于dart:io库,并且都是异步操作
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      Future<File> getFile() async {
      // getApplicationDocumentsDirectory返回了一个Directory对象
      String path = (await getApplicationDocumentsDirectory()).path;
      return File('$path/counter.txt');
      }

      Future<int> readCounter() async {
      // 因为File可能不存在所以需要try catch
      try {
      File file = await getFile();
      // 读取文件内容
      String contents = await file.readAsString();
      return int.parse(contents);
      } on FileSystemException {
      return 0;
      }
      }

      Future<void> setCounter() async {
      setState(() {
      _counter += 1;
      });
      await (await getFile()).writeAsString('$_counter');
      }

      @override
      void initState() {
      super.initState();
      setState(() {
      getCounter().then((value) {
      setState(() {
      _counter = value;
      });
      });
      });
      }