Flutter 新手入门 - For 前端开发者

144 阅读9分钟

安装

按照官网安装指引,下载 Flutter_SDK,解压到任意目录,(安装这步 Flutter官方就没想过要优化一下,自己解压,自己配置 PATH,但是如果不会配置 PATH,还是要看下 Linux下怎么配置 PATH的)

记录下解压的目录,假设是 ~/flutter

$ sudo vim ./.zshrc

vim 中输入 
export FLUTTER_HOME=~/flutter
# flutter 可执行文件地址
export PATH=$FLUTTER_HOME/bin:$PATH
# flutter pub 缓存地址
export PATH=$FLUTTER_HOME/.pub-cache/bin:$PATH
export PATH=$FLUTTER_HOME/bin/cache/dart-sdk/bin:$PATH


重启控制台或者执行 source ./.zshrc
$ flutter doctor

默认来说,页端开发 flutter 直接使用 VsCode + VsCode Flutter 插件就好

需要安卓环境的请自行安装 AS

需要 IOS环境请自行安装 XCode

初始化一个应用

按照官网的指引,初始化一个应用

初始化完应用根目录下有个文件 pubspec.yaml,标明了项目的一些基础信息,和依赖的包,作用和页端的 package.json一致

# 安装依赖和 npm install 效果一致
$ flutter pub get

组件

Flutter使用 dart 作为编码语言,它是一门强类型面向对象的编程语言,基本语言概述可以从 Dart中文网 找到,dart 发展到这个阶段,语言特性基本和 TS Kotlin很相似,不能说全部一样,但是很多都有类似的影子,比起两年前差别真的很大

设计

Flutter 设计参考了 React 的思想,组件编写和组件更新都有 React 的影子,都是由一个一个组件堆砌成一个应用,Flutter中组件称为 Widget,同样的,在 React中称为 Component

无状态组件

无状态组件类似 React 的非钩子函数组件,组件无状态,组件更新依赖外部属性

import 'package:flutter/material.dart';

class StatelessChild extends StatelessWidget {
  final String text;
  StatelessChild({@required this.text});

  @override
  Widget build(BuildContext context) {
    return Text(text);
  }
}

万物皆类:从最简单的文本组件开始

仔细看代码,一个简单的文本,既然用了一个 Text 组件包裹,在前端展示这种文本直接返回一个字符串就好,在 Flutter 中并不可以。

  • 从代码可以看出 build 方法必须返回一个 Widget类型的值,上面返回的是 TextText 的继承链是 Text -> StatelessWidget -> Widget,所以并没有问题。
  • Dart编写代码的时候,实例化类,不需要 new 操作符(开始我还很抗拒,后面真香系列),当然,你坚持写 new 也是没任何问题的。
  @override
  Widget build(BuildContext context) {
    return new Text(text);
  }
  • 除了组件是一个类外,不少组件属性也是一个类,比如我需要我的字体颜色是红色,字体大小是 20,行高是 1.5,字重是 700,字体有下划线,就需要通过 TextStyle类修改字体的样式,同时会用到 Color ,FontWidget``TextDecoration 的类,
  @override
  Widget build(BuildContext context) {
    return Text(text,
        style: TextStyle(
            color: Colors.red,
            height: 1.5,
            fontSize: 20,
            fontWeight: FontWeight.w700,
            decoration: TextDecoration.underline));
  }

有状态组件

有状态组件,顾名思义就是,带有状态的组件,通过调用组件内部的方法 setState,可以让组件重新渲染,更新过程和老版 React差不多,从调用setState的组件开始,一直遍历子组件,查看需要更新的组件,需要更新的组件,会重新调用组件的 build方法,和 React 组件中的 render一致

class Parent extends StatefulWidget {
  @override
  State<Parent> createState() => _ParentState();
}

class _ParentState extends State<Parent> {
  String text = 'init';

  void changeText() {
    setState(() {
      text = 'text changed';
    });
  }

  @override
  Widget build(BuildContext context) {
    return StatelessChild(text: text);
  }
}

组件间传值

组件间传值方式,和 React 完全一致,不过,在 React中使用的是 JSX 语法编写,在 Flutter中,组件就是类,所以使用组件,就是实例化这个类,通过给类定义属性,实例化类之后,实例化的类就能拿到从父组件传过来的属性了

子组件修改

让我们修改一下上面的子组件,不仅要传一个 text 字符串过来,还需要传一个修改 text 的方法过来,然后我们子组件改成这样,onClick属性不是必传的,但是如果不传,那按钮点击没任何响应

class StatelessChild extends StatelessWidget {
  final String text;
  final VoidCallback onClick;
  StatelessChild({@required this.text, this.onClick});

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text(text,
            style: TextStyle(
                color: Colors.red,
                height: 1.5,
                fontSize: 20,
                fontWeight: FontWeight.w700,
                decoration: TextDecoration.underline)),
        IconButton(onPressed: onClick, icon: Icon(Icons.ac_unit))
      ],
    );
  }
}

父组件修改

父组件改动不多,多传一个属性,当然,不传 onClick也不会有任何报错的,因为 onClick 没有标明 @required

class Parent extends StatefulWidget {
  @override
  State<Parent> createState() => _ParentState();
}

class _ParentState extends State<Parent> {
  String text = 'init';

  void changeText() {
    setState(() {
      text = 'text changed';
    });
  }

  @override
  Widget build(BuildContext context) {
    return StatelessChild(text: text, onClick: changeText);
  }
}

示例等效 React代码

子组件

import { FC } from 'react';

interface StalessChildProps {
  text: string;
  onClick?: () => void;
}

const StatelessChild: FC<StalessChildProps> = ({ text, onClick }) => {
  return (
    <div>
      <div>{text}</div>
      <div onClick={onClick}>按钮</div>
    </div>
  );
};

export default StatelessChild;

父组件

import { Component } from 'react';
import StatelessChild from './child';

interface ParentState {
  text: string;
}

interface ParentProps {}

class Parent extends Component<ParentProps, ParentState> {
  constructor(props: ParentProps) {
    super(props);
    this.state = { text: 'init' };
  }

  changeText = () => {
    this.setState({ text: 'text changed' });
  };

  render() {
    return <StatelessChild text={this.state.text} onClick={this.changeText} />;
  }
}

export default Parent;

布局

Flutter 里面最长见的布局,就是 Flex 布局,页端开发熟悉 Flex 布局的,可以快速上手

容器

Flutter最常见的容器是 Container,可以设置 width、height、padding、margins、borders,同时也可以修改背景,盒子的呈现方式和 HTMLbox-sizing: border-box 一致。

除了 Container 外,还有一些别的容器,要仔细了解这些容器和它们的约束,需要认真看一下 Flutter 布局约束,很多新人会遇到各种容器溢出的问题,仔细看些这里会有不少收获

多个元素

处理多个元素在同一排或者同一列,在 HTML 中,实现方式很多,但是最简单的,可以通过设置 display: flex; 将容器设置成弹性布局,处理起来非常方便。

Flutter 里面,也是这样,在同一行的元素,我们使用 Row 组件,在同一列的元素,我们使用 Column组件,其表现和 display: flex; 完全一致,甚至属性都长的一样

布局拆解

image.png

image.png

// 伪代码
Container
  Column
    Row
      Image
      Expanded
        Column
          Text
          Text
      Icon
    Row
      Expanded
        Text
      Container
      Expanded
        Text

布局总结

属性以下几种布局不成问题

  • 常规布局,Container + Column + Row
  • 列表,ListView
  • 层叠布局,Stack

生命周期

Flutter生命周期和 React Class Component类型

React Class Component 生命周期

class ExampleComponent extends React.Component {
// 用于初始化 state
constructor() {}
// 用于替换 `componentWillReceiveProps` ,该函数会在初始化和 `update` 时被调用
// 因为该函数是静态函数,所以取不到 `this`
// 如果需要对比 `prevProps` 需要单独在 `state` 中维护
static getDerivedStateFromProps(nextProps, prevState) {}
// 判断是否需要更新组件,多用于组件性能优化 常用
shouldComponentUpdate(nextProps, nextState) {}
// 组件挂载后调用
// 可以在该函数中进行请求或者订阅 常用
componentDidMount() {}
// 用于获得最新的 DOM 数据
getSnapshotBeforeUpdate() {}
// 组件即将销毁
// 可以在此处移除订阅,定时器等等 常用
componentWillUnmount() {} 
// 组件销毁后调用
componentDidUnMount() {}
// 组件更新后调用 常用
componentDidUpdate() {}
// 渲染组件函数
render() {}
// 以下函数不建议使用
UNSAFE_componentWillMount() {}
UNSAFE_componentWillUpdate(nextProps, nextState) {}
UNSAFE_componentWillReceiveProps(nextProps) {}
}

Flutter StatefulWidget 生命周期

class Parent extends StatefulWidget {
  @override
  State<Parent> createState() => _ParentState();
}

class _ParentState extends State<Parent> {
  @override
  void initState() {
    // State 初始化,这个周期,可以获取到父组件传过来的属性
    // 和 React Class Component 构造函数一致
    super.initState();
  }

  @override
  void didUpdateWidget(covariant Parent oldWidget) {
  // 组件更新完成后会调用,和 React componentDidMount 一致
  super.didUpdateWidget(oldWidget);
	}

  @override
  void dispose() {
  // 组件被销毁后调用,和 React componentWillUnmount 基本一致
  super.dispose();
	}
}

技巧

快速生成有状态组件

一个有状态组件初始化长这样,鬼才记得哦,两个类太繁琐了

class Widget extends StatefulWidget {
  @override
  State<Widget> createState() => _WidgetState();
}

class _WidgetState extends State<Widget> {
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    throw UnimplementedError();
  }
}

VsCode 中可以快速将一个无状态组件切换成有状态组件

// 一个初始化的无状态组件
class Widget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    throw UnimplementedError();
  }
}

VsCode中生成一个无状态组件,光标选中这个类声明的这一行,会看到一个小灯泡按钮,点击这个小灯泡,就可以触发转化成有状态组件的选项,非常实用,官方生成的代码,也比自己写的 State 组件更加严谨

快速组件包裹和删除某个组件

同样也是基于 VsCode,选中某个组件,可以进行很多操作

常规组件

  • 删除这个组件
  • 交换父子组件(ClipRect + Container互换位置很常见)
  • Builder 包裹组件,最常见的是 StoreConnector 这种类 builder 组件
  • Center Padding Container SizedBox 包裹组件,修改布局
  • widget 包裹组件,最常见的是组件外面要加一个 GestureDetector

在 children 中的组件

  • 支持把这个组件和兄弟组件交换位置

局限和总结

不支持生成任意子组件是 children 的组件,也即是 Row Column ListView这些组件的生成,不过现有的功能已经很给开发提效了

编码

  • 文件名用蛇形,格式用 dart,例如 novel_header.dart image_cover.dart
  • 类用大驼峰,变量和函数用小驼峰,不要蛇形和匈牙利命名法
  • var声明可变化的变量,用 final声明不可再赋值的变量(这两个完全和 JS letconst一致)
  • 可以显式声明变量类型,显式申明类型的时候,不能用 var,但是可以搭配 final
var now = DateTime.now();
DateTime now = DateTime.now();
now = DateTime.fromMillisecondsSinceEpoch(930989389432);
// 不可再赋值
final DateTime now = DateTime.now();
  • 复杂类型请不要使用 var,必须显式声明类型,能够避免很多问题
Map<String, dynamic> map = {};
// 不建议
var map = {};
  • const声明常量,和不能再赋值是有差别的,通常用于绝对不会变的变量,变量也用全大写声明(不会用不要用这个)
const String PLATFORM_ANDROID = 'android';
const Map<String, String> map = {};
// 常量不能修改,编译器不会报错,但是运行会炸,并且报错还不好定位,不要做这种事情
map['a'] = '1';
  • 使用 _ 开头的属性和函数或者类,是私有的,作用域在 类里面或者文件内
  • 函数的参数,包裹在 {} 内的参数,是可选的,没有 {} 包裹,是必填参数,规则也适用于构造函数,可选参数可以设定默认值,如果是 bool类型的参数,请务必设定默认值,默认值必须是常量
void func(int p1, int p2, { bool p3 = true, String p4, List<int> p5 = const [] }) {}
  • 组件的参数,建议不要定义必填属性,除非目的很明确的组件 例如 Text,必填属性就一个 String text,使用 @requied 标明可选属性里面必填属性,这两者作用一样,但是使用有差别,但是配合 VsCode,会自动提示函数形参,使用起来也没多大差别
Text('123')
// VsCode 默认提示,必填参数是 text
Text(text) 
StatelessChild({@required this.text, this.onClick});
StatelessChild(text: 'text 1', onClick: () {});
// VsCode 默认提示,必填参数是 text
StatelessChild(text: text);