liuyulin
发布于 2024-02-19 / 68 阅读
0
0

Flutter

Flutter

Hello World

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}
​
class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;
​
  @override
  _MyHomePageState createState() => _MyHomePageState();
}
​
class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
​
  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }
​
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('You have pushed the button this many times:'),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

应用入口:

void main() => runApp(MyApp());

runApp接受一个Widget参数

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
​
  // This widget is the root of your application.
​
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        //platform: TargetPlatform.android,
      ),
      home: const MyHomePage(),
    );
  }
}

在Flutter中,大多数东西都是widget(叫“组件”或“部件”),包括对齐(Align)、填充(Padding)等都是以Widget形式提供的

Flutter在构建页面时,会调用build方法,widget的主要工作时提供一个build()方法来描述如何构建UI界面(通常是通过组合、拼装其他基础widget)

MaterialApp 是Material 库中提供的 Flutter APP 框架,通过它可以设置应用的名称、主题、语言、首页及路由列表等。MaterialApp也是一个 widget。

home为Flutter应用的首页,也是一个widget

初识Widget

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;
  
  @override
  _MyHomePageState createState() => _MyHomePageState();
}
​
class _MyHomePageState extends State<MyHomePage> {
 ...
}

MyHomePage是应用的首页,继承自StatefulWidget类,表示一个有状态的组件。组件有有状态(Stateful)和无状态(Stateless)。Stateful widget可以拥有状态,这些状态在widget生命周期中是可变的,而Statless widget是不可变的。

Stateful widget至少由两个类组成:一个StatefulWidget类,一个State类;StatefulWidget类本身是不变的,但是State类中持有的状态在Widget生命周期中可能会发生变化。

MyHomePageState类是MyHomePage类对应的状态类,MyHomePage中并没有build方法,而是将build方法挪到了MyHomePageState方法中,这个后面解答。

State类

counter.dart文件中_MyHomePageState类解析:

import 'package:flutter/material.dart';
​
typedef CounterRoute = MyApp;
​
class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
​
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}
​
class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;
​
  @override
  _MyHomePageState createState() => _MyHomePageState();
}
​
class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
​
  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }
​
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text('You have pushed the button this many times:'),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

组件的状态,由于我们只需要维护一个点击次数计算机,所有定义了一个_counter状态

状态自增函数_incrementCounter,当调用该函数时,先自增_counter,然后再调用setState方法,告知Flutter框架,状态发生了改变,Flutter框架收到通知后,会执行build方法,来根据新的状态重新构建页面,Flutter对此方法进行了优化,执行很快,所以可以重新构建任何需要更新的东西,而无需分别去修改各个widget

构建UI的逻辑在build方法中,当MyHomePage第一次被创建时,_MyHomePageState类会被创建,当初始化完成后,Flutter框架会调用widget的build方法构建widget数,最终渲染到屏幕上。

  • Scaffold是Material库中提供的页面脚手架,提供了默认的导航栏,标题和包含主频木widget树的body属性,组件树可以很复杂,此后默认路由都是通过Scaffold创建的

  • body的组件树中包含了一个Center组件,可以将其子组件树对其到屏幕中心

  • Column的作用是将其所有子组件沿屏幕垂直方向依次排列,第一个子组件Text,固定显示文本,第二个Text显示_counter状态的数值

  • floatingActionButton是页面右下角带+的悬浮按钮,他的onPressed属性接受一个回调函数,代表点击之后的处理器。

为什么将build方法放到State中,而不是StatefulWidget中?

这样做提高了开发的灵活性,如果将build方法放在StatefulWidget中,会出现下面两个问题:

  1. 状态访问不便

如果build方法在StatefulWidget中,由于构建界面的时候需要依赖State,所以build方法必须加一个State参数

Widget build(BuildContext context, State state){
      //state.counter
      ...
  }

如果这样的话只能将State的所有状态声明为公开的状态,这样才能在State类外部访问,但是将状态设置为公开后,状态将不再具有私密性,会导致对状态的修改不可控。但是如果将build方法放在State中的话,构建过程不仅可以直接访问状态,而且不需要公开私有状态。

  1. 继承StatefulWidget不变

例如,Flutter 中有一个动画 widget 的基类AnimatedWidget,它继承自StatefulWidget类。AnimatedWidget中引入了一个抽象方法build(BuildContext context),继承自AnimatedWidget的动画 widget 都要实现这个build方法。现在设想一下,如果StatefulWidget 类中已经有了一个build方法,正如上面所述,此时build方法需要接收一个 State 对象,这就意味着AnimatedWidget必须将自己的 State 对象(记为_animatedWidgetState)提供给其子类,因为子类需要在其build方法中调用父类的build方法,代码可能如下:

class MyAnimationWidget extends AnimatedWidget{
    @override
    Widget build(BuildContext context, State state){
      //由于子类要用到AnimatedWidget的状态对象_animatedWidgetState,
      //所以AnimatedWidget必须通过某种方式将其状态对象_animatedWidgetState
      //暴露给其子类   
      super.build(context, _animatedWidgetState)
    }
}
​

这样很显然是不合理的,因为

  • AnimatedWidget的状态对象是AnimatedWidget内部实现细节,不应该暴露给外部。

  • 如果要将父类状态暴露给子类,那么必须得有一种传递机制,而做这一套传递机制是无意义的,因为父子类之间状态的传递和子类本身逻辑是无关的。

综上所述,可以发现,对于StatefulWidget,将build方法放在 State 中,可以给开发带来很大的灵活性。

Widget

Widget比原生种的控件更广泛,Flutter中万物皆为Widget,通过Widget嵌套的方法构建UI和进行事件处理

在Flutter中,widget的功能是“描述一个UI元素的配置信息”,他就是说,Widget其实并不是表示最终绘制在设备屏幕上的显示元素,所谓的配置信息就是Widget接收的参数。

@immutable // 不可变的
abstract class Widget extends DiagnosticableTree {
  const Widget({ this.key });
​
  final Key? key;
​
  @protected
  @factory
  Element createElement();
​
  @override
  String toStringShort() {
    final String type = objectRuntimeType(this, 'Widget');
    return key == null ? type : '$type-$key';
  }
​
  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.dense;
  }
​
  @override
  @nonVirtual
  bool operator ==(Object other) => super == other;
​
  @override
  @nonVirtual
  int get hashCode => super.hashCode;
​
  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }
  ...
}
  • @immutable代表Widget是不可变的,这会限制Widget中定义的属性必须是不可变的,Flutter中如果属性发生变化则会重新构建Widget树,所以允许Widget属性变化是没有意义的,因为一旦Widget自己的属性发生了变化,自己就会被替换

  • Widget继承自DiagnosticableTree,即诊断树,主要作用是提供调试信息

  • Key类似于Vue中的Key,主要作用是决定是否在下一次build时复用就的Widget,决定的条件在canUpdate方法中

  • createElement,正如前文所述,一个widget对应多个Element,Flutter框架在构建UI树时,会先调用此方法生成对应节点的Element对象,此方法时隐式调用的,在开发过程中用不到

  • debugFillProperties复写父类的方法,主要是设置诊断树的一些特性

  • canUpdate是一个静态方法,主要用于在Widget树重新build时复用旧的widget,具体来说是,是否用新的widget对象去更新旧的UI树上对应的Element对象的配置,通过其源码可以看到,只要newWidget与oldWidget的runtimeType和key同时相等时就会用new widget去更新Element对象的配置,否则就会创建新的Element

Widget本身是一个抽象类,其最核心的就是定义了createElement接口,在开发中一般使用StatefulWidget和StatelessWidget间接继承Widget。

Flutter中的四棵树

Flutter框架的布局,绘制流程是这样的:

  • 根据Widget树生成一个Element树,Element树中节点都继承自Element类

  • 根据Element树生成Render树(渲染树),渲染树中的节点都继承自RenderObject类

  • 根据渲染树生成Layer树,然后上屏显示,Layer树中的节点都继承自Layer类

真正的布局和渲染逻辑在Render树中,Element是Widget和RenderObject的粘合剂,三棵树中,Widget和Element时一一对应的,但并不和RenderObject一一对应

StatelessWidget

StatelessWidget继承自Widget类,重写了createElement()方法

@override
StatelessElement createElement() => StatelessElement(this);

StatelessElement简介继承Element类,与StatelessWidget相对应。StatelessWidget用于不需要维护状态的场景,它通常在build方法中嵌套其他widget来构建UI,在构建过程中会递归的构建其嵌套的widget

import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
​
class Echo extends StatelessWidget {
  const Echo({Key? key, required this.text, this.backgroundColor = Colors.grey})
      : super(key: key);
​
  final String text;
  final Color backgroundColor;
​
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        color: backgroundColor,
        child: Text(text),
      ),
    );
  }
}

上面的代码实现了一个回显字符串的Echo widget

按照惯例,widget的构造函数参数应使用命名参数,命名参数中的必须参数要添加required关键字,这样有利于静态代码分析器检查,在继承Widget时,第一个参数通常是Key,另外,如果widget需要接收子widget,child或者children参数通常被放在参数列表最后,同样是按照惯例,widget的属性应尽可能的被声明为final,防止被意外改变

Context

build方法有一个context参数,它是一个BuildContext类的一个实例,表示当前widget在widget树中的上下文,每一个widget都会对应一个context对象(因为每一个widget都是widget树上的一个节点)。实际上,context是当前widget在widget树中位置中执行相关操作的一个句柄(handle),比如,它提供了从当前widget开始向上遍历widget树以及按照widget类型查找父级widget的方法。

class ContextRoute extends StatelessWidget  {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Context测试"),
      ),
      body: Container(
        child: Builder(builder: (context) {
          // 在 widget 树中向上查找最近的父级`Scaffold`  widget 
          Scaffold scaffold = context.findAncestorWidgetOfExactType<Scaffold>();
          // 直接返回 AppBar的title, 此处实际上是Text("Context测试")
          return (scaffold.appBar as AppBar).title;
        }),
      ),
    );
  }
}

StatefulWidget

和StatelessWidget一样,也是继承自Widget类,重写了createElement方法,不同的是,StatefulWidget中添加了一个createState接口

abstract class StatefulWidget extends Widget {
  const StatefulWidget({ Key key }) : super(key: key);
    
  @override
  StatefulElement createElement() => StatefulElement(this);
    
  @protected
  State createState();
}
  • StatefulElement间接继承Element类,与StatefulWidget相对应,StatefulElement中可能多次调用createElement来创建State对象

  • createState方法用于创建和Stateful相关的状态,在StatefulWidget的生命周期中可能会被多次调用

State

一个StatefulWidget类会对应一个State类,State是要维护的状态,State中保存的状态信息可以在widget构建时可以被同步读取,在widget生命周期中可以被改变,当State被改变时,可以手动调用其setState方法通知Flutter框架状态发生改变,Flutter框架在收到消息后,会重新调用其build方法重新构建widget树,从而达到更新UI的目的

State中有两个常用属性:

  1. widget,表示与该State实例关联的widget实例,由Flutter框架动态设置。注意,这种关联并非永久的,因为在应用生命周期中,UI树上的某一个节点的widget实例在重新构建时可能会变化,但是State实例只会在第一次插入到树中时被创建,当在重新构建时,如果widget被修改了,Flutter框架会动态设置State,widget为新的widget实例。

  2. context,StatefulWidget对应的BuildContext,作用同StatelessWidget的BuildContext

State生命周期

理解State的生命周期对flutter开发非常重要。

仍然以计数器为例,要保存计数状态,所以继承StatefulWidget

class CounterWidget extends StatefulWidget {
  const CounterWidget({Key? key, this.initValue = 0});
​
  final int initValue;
​
  @override
  _CounterWidgetState createState() => _CounterWidgetState();
}

ConterWidget接收一个initValue整形参数,表示计算器初始值,State代码:

class _CounterWidgetState extends State<CounterWidget> {
  int _counter = 0;
​
  @override
  void initState() {
    super.initState();
    //初始化状态
    _counter = widget.initValue;
    print("initState");
  }
​
  @override
  Widget build(BuildContext context) {
    print("build");
    return Scaffold(
      body: Center(
        child: TextButton(
          child: Text('$_counter'),
          //点击后计数器自增
          onPressed: () => setState(
            () => ++_counter,
          ),
        ),
      ),
    );
  }
​
  @override
  void didUpdateWidget(CounterWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    print("didUpdateWidget ");
  }
​
  @override
  void deactivate() {
    super.deactivate();
    print("deactivate");
  }
​
  @override
  void dispose() {
    super.dispose();
    print("dispose");
  }
​
  @override
  void reassemble() {
    super.reassemble();
    print("reassemble");
  }
​
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    print("didChangeDependencies");
  }
}

接下来创建一个新的路由,在路由中,只显示一个CounterWidget

class StateLifecycleTest extends StatelessWidget {
  const StateLifecycleTest({Key? key}) : super(key: key);
​
  @override
  Widget build(BuildContext context) {
    return const CounterWidget();
  }
}

打开路由界面,屏幕中会出现一个数字0,控制台输出

I/flutter (23216): initState
I/flutter (23216): didChangeDependencies
I/flutter (23216): build

可以看到,在StatefulWidget插入到widget树时,首先会调用initState方法

然后点击热加载,控制台输出

I/flutter (23216): reassemble
I/flutter (23216): didUpdateWidget 
I/flutter (23216): build

可以看到,initState和didChangeDependencies都没有被调用,而didUpdateWidget被调用

我们在路由中移除CounterWidget,随便添加一个widget

  @override
  Widget build(BuildContext context) {
    // return const CounterWidget();
    return Text("text");
  }

控制台输出:

I/flutter (23216): reassemble
I/flutter (23216): deactivate
I/flutter (23216): dispose

可以看到,deactivate和dispose被依次调用

由上可以得出widget各个回调函数的作用:

  • initState:当widget第一次插入到widget树时会被调用,对于每一个State对象,Flutter只会调用一次该回调,通常在该回调中做一些一次性操作,如状态初始化,订阅子树的事件通知等。【不能在该回调中调用BuildContext.dependOnInheritedWidgetOfExactType(该方法用于在 widget 树上获取离当前 widget 最近的一个父级InheritedWidget,关于InheritedWidget我们将在后面章节介绍),原因是在初始化完成后, widget 树中的InheritFrom widget也可能会发生变化,所以正确的做法应该在在build()方法或didChangeDependencies()中调用它。】

  • didChangeDependencies:当State对象的依赖发生变化时会被调用,典型的场景是当系统语言Locale或应用主题改变时,Flutter框架会通知widget调用此回调。注意,组件第一次被创建后挂载的时候(包括重创建)对应的didChangeDependencies也会被调用

  • build:用于构建widget,会在initState、didUpdateWidget、setState、didChangeDependencies、State对象从树中一个位置移除后(会调用deactivate)又重新插入到树的其他位置之后被调用

  • reassemble:专门为开发调试而使用的,在热重载的时候会被调用

  • didUpdateWidget:在widget重新构建时,Flutter框架会调用canUpdate来检测widget树中同一位置的新旧节点,然后决定是否更新,如果canUpdate返回true则调用此回调,canUpdate会比较新旧widget的key和runtimeType,相同时返回true

  • deactivate:当State对象从树中移除时,会调用此回调。在一些场景下,Flutter框架会将State重新插入树中,例如包含此State对象的子树在树的一个位置移动到另一个位置时,如果移除后没有重新插入,则紧接着调用dispose方法

  • dispose:当State对象从树种被永久移除时调用,通常在此回调中释放资源

图2-5

注意:在继承StatefulWidget重写方法时,对于包含@mustCallSuper标注的父类方法,都要在子类方法中调用父类方法。



评论