Flutter - Introduction to widgets

63 阅读5分钟

Flutter widget

Flutter widgets 是从React框架中获得的灵感, 核心思想是build your UI out of widgets, widgets根据当前的配置和state描述了它应该是什么样子的, 当widget的state改变之后, widget就会重构这个描述, 并且会根据diffs算法, 根据前一个描述来计算出最小的更改, 把当前的widge切换到下一个state.

import 'package:flutter/material.dart';

void main() {
  runApp(
    const Center(
      child: Text(
        'Hello, world!',
        textDirection: TextDirection.ltr,
      ),
    ),
  );

runApp函数会把给定的widget作为根, 就比如上面的Center和Text, 会强制的把Center铺满全屏, 如果要写App的话, 一般是要继承自StatelessWidgetStatefulWidget, 继承自哪个是要取决这个widget会不会有变化, 每个widget都要重写 build()函数, 就是这个函数用来描述widget树来着

基础的Widgets

  • Text 展示文本
  • Row, Column 横向/纵向的一个容器 是以web的flexbox为基础
  • Stack 线性的一个布局容器, 是以web的绝对布局为基础
  • Container 就是一个容器, 可以设置很多东西: border, shadow
import 'package:flutter/material.dart';

void main() {
  runApp(
    const MaterialApp(
      title: 'Flutter Tutorial',
      home: TutorialHome(),
    ),
  );
}

class TutorialHome extends StatelessWidget {
  const TutorialHome({super.key});

  @override
  Widget build(BuildContext context) {
    // Scaffold is a layout for
    // the major Material Components.
    return Scaffold( // scaffold 的框架
      appBar: AppBar(
        leading: const IconButton(
          icon: Icon(Icons.menu),
          tooltip: 'Navigation menu',
          onPressed: null,
        ),// 设置左上角的按钮
        title: const Text('Example title'), // 导航栏中间的title
        actions: const [
          IconButton(
            icon: Icon(Icons.search),
            tooltip: 'Search',
            onPressed: null,
          ), // 导航栏右侧的按钮
        ],
      ),
      // body is the majority of the screen.
      body: const Center(
        child: Text('Hello, world!'), // 中间显示的文本
      ),
      floatingActionButton: const FloatingActionButton(
        tooltip: 'Add', // used by assistive technologies
        onPressed: null,
        child: Icon(Icons.add),
      ),  // 底部右下角的浮动按钮
    );
  }
}

flutter里最神奇的是手势动画什么的也是widget, 本身GestureDetector并不是可见的, 但是用户是可以触碰到的, 当用户点击了Container就会触发onTap()callback, 许多的系统控件都是使用这种方式, 比如IconButton, FloatingActionButton用的是onPressed()

import 'package:flutter/material.dart';

class MyButton extends StatelessWidget {
  const MyButton({super.key});

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        print('MyButton was tapped!');
      },
      child: Container(
        height: 50.0,
        padding: const EdgeInsets.all(8.0),
        margin: const EdgeInsets.symmetric(horizontal: 8.0),
        decoration: BoxDecoration(
          borderRadius: BorderRadius.circular(5.0),
          color: Colors.lightGreen[500],
        ),
        child: const Center(
          child: Text('Engage'),
        ),
      ),
    );
  }
}

void main() {
  runApp(
    const MaterialApp(
      home: Scaffold(
        body: Center(
          child: MyButton(),
        ),
      ),
    ),
  );
}

一个可变的实例, 这里需要注意一个问题StatefulWidgetState是两个对象, 并不是同一个, 因为这两个对象具有不同的生命周期, widget是一个临时的对象, 因为这货会根据state的变化而重组, 而state是一直存在的

import 'package:flutter/material.dart';

class Counter extends StatefulWidget {
  const Counter({super.key});

  @override
  State<Counter> createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  int _counter = 0;

  void _increment() {
    setState(() {
      // 数据变化必须要在setState这个函数里, 这个函数可以触发rebuild, 如果不通过这个函数
      // 数据变化则不会体现到UI上
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    // 每次setState被调用的时候, 这个方法都会被调用, flutter底层做过优化
    // 所以... just do it
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        ElevatedButton(
          onPressed: _increment,
          child: const Text('Increment'),
        ),
        const SizedBox(width: 16),
        Text('Count: $_counter'),
      ],
    );
  }
}

void main() {
  runApp(
    const MaterialApp(
      home: Scaffold(
        body: Center(
          child: Counter(),
        ),
      ),
    ),
  );
}

👆🏻这个例子比较简单粗暴, widget的修改直接就放到build里就完事儿了, 真实的情况, 可能不一个widget的不同部分要处理的情况不同, 所以, 有必要把他们给分开, 这样看着会复杂, 但项目的复杂度上来之后, 就会很有必要

import 'package:flutter/material.dart';

class CounterDisplay extends StatelessWidget {
  const CounterDisplay({required this.count, super.key});

  final int count;

  @override
  Widget build(BuildContext context) {
    return Text('Count: $count');
  }
}

class CounterIncrementor extends StatelessWidget {
  const CounterIncrementor({required this.onPressed, super.key});

  final VoidCallback onPressed;

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: onPressed,
      child: const Text('Increment'),
    );
  }
}

class Counter extends StatefulWidget {
  const Counter({super.key});

  @override
  State<Counter> createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  int _counter = 0;

  void _increment() {
    setState(() {
      ++_counter;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        CounterIncrementor(onPressed: _increment),
        const SizedBox(width: 16),
        CounterDisplay(count: _counter),
      ],
    );
  }
}

void main() {
  runApp(
    const MaterialApp(
      home: Scaffold(
        body: Center(
          child: Counter(),
        ),
      ),
    ),
  );
}

image.png

最外围的Row是一个可变组件(红色部分), 需要计数以及state的改变都是他在维护, 而里面的蓝色框框部分, 被分成了两部分都是不可变组件, 左侧的可点击按钮, 是通过callback回调出来, 去调用_increment(), 而右侧的展示部分, 则是通过传参方式进行传入

tips: _CounterState 里的build每次state变化的时候都会调用

结合一下, 自定义一个ShopListItem

import 'package:flutter/material.dart';

// 产品类
class Product {
  const Product({required this.name});
  final String name;
}

// 回调的callback
typedef CartChangedCallback = Function(Product pro, bool inCart);

// item
class ShoppingListItem extends StatelessWidget {
    ShoppingListItem({
      required this.product,
      required this.inCart,
      required this.onCartChanged
    }) : super(key: ObjectKey(product));

    final Product product;
    final bool inCart;
    final CartChangedCallback onCartChanged;

// 根据是否 inCart 改变颜色
    Color _getColor(BuildContext context) {
      return inCart ? Colors.black54 : Theme.of(context).primaryColor;
    }

// 根据是否 inCart 该干文本style, 中线
    TextStyle? _getTextStyle(BuildContext context) {
      if(!inCart) return null;
      return const TextStyle(
        color: Colors.black54,
        decoration: TextDecoration.lineThrough,
      );
    }

    @override
    Widget build(BuildContext context) {
      return ListTile(
          onTap: () {onCartChanged(product, inCart);},
          leading: CircleAvatar(backgroundColor: _getColor(context),
          child: Text(product.name[0]),),
          title: Text(product.name, style: _getTextStyle(context),),
      );
    }
}

void main() {
  runApp(
    MaterialApp(
      home: Scaffold(
        body: Center(child: 
        ShoppingListItem(
          product: const Product(name:"Map"),
          inCart: true,
          onCartChanged: (product, inCart) {},
        ),
        ),
      ),
    ),
  );
}

动态的改变item的状态, 第一次执行的时候会创建一个shoppingList的widget tree对象, 因为是继承自StatefulWidget, 同时会创建State, 但是当有改动需要重组的时候(这个widget会被标记为dirty然后会被安排进行重组), widget tree会创建一个新的, 但是会重用State对象. 如果想监听widget的改变可以重写didUpdateWidget()函数, 这个会把oldWidget和当前的widget进行对比

import 'package:flutter/material.dart';

class Product {
  const Product({required this.name});
  final String name;
}

typedef CartChangedCallback = Function(Product product, bool inCart);

class ShoppingListItem extends StatelessWidget {
  ShoppingListItem({
    required this.product,
    required this.inCart,
    required this.onCartChanged,
  }) : super(key: ObjectKey(product));

  final Product product;
  final bool inCart;
  final CartChangedCallback onCartChanged;

  Color _getColor(BuildContext context) {
    return inCart //
        ? Colors.black54
        : Theme.of(context).primaryColor;
  }

  TextStyle? _getTextStyle(BuildContext context) {
    if (!inCart) return null;

    return const TextStyle(
      color: Colors.black54,
      decoration: TextDecoration.lineThrough,
    );
  }

  @override
  Widget build(BuildContext context) {
    return ListTile(
      onTap: () {
        onCartChanged(product, inCart);
      },
      leading: CircleAvatar(
        backgroundColor: _getColor(context),
        child: Text(product.name[0]),
      ),
      title: Text(
        product.name,
        style: _getTextStyle(context),
      ),
    );
  }
}

class ShoppingList extends StatefulWidget {
  const ShoppingList({required this.products, super.key});

  final List<Product> products;

  @override
  State<ShoppingList> createState() => _ShoppingListState();
}

class _ShoppingListState extends State<ShoppingList> {
// 用set包含选中的item, 无序且不重复的集合
  final _shoppingCart = <Product>{};

// 回调触发build执行, 刷新UI
  void _handleCartChanged(Product product, bool inCart) {
    setState(() {
      if (!inCart) {
        _shoppingCart.add(product);
      } else {
        _shoppingCart.remove(product);
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Shopping List'),
      ),
      body: ListView(
        padding: const EdgeInsets.symmetric(vertical: 8.0),
        children: widget.products.map((product) {
          return ShoppingListItem(
            product: product,
            inCart: _shoppingCart.contains(product),
            onCartChanged: _handleCartChanged, // 通过listitem 的callback 回调
          );
        }).toList(),
      ),
    );
  }
}

void main() {
  runApp(const MaterialApp(
    title: 'Shopping App',
    home: ShoppingList(
      products: [
        Product(name: 'Eggs'),
        Product(name: 'Flour'),
        Product(name: 'Chocolate chips'),
      ],
    ),
  ));
}

widget 的生命周期事件

在StatefulWidget调用`createState()`后, framework会在widget树中插入一个新的state对象, 然后会调用State对象的`initState()`, 这个函数只会执行一次, 如果子类重写该方法, 那么就需要调用`super.initState`.

当一个state对象不再需要的时候, framework会调用state对象的`dispose()`函数进行清理工作, 跟`initState()`一样, 如果重写必须调用super