Dart官网

Dart和Flutter安装包

repository

Dart

数据类型

变量名必须由数字、字母、下划线、美元符号$组成,且不能以数字开头,且不能是关键字。变量名区分大小写。

注释的写法:

//注释
/*注释*/

基本数据类型:

// 自动类型推断
var str1='string1';

// 字符串用单引号或双引号定义都可以
String str2="string2";
String str3="""换

字符串""";
// 字符串拼接与打印
print(str2 + " " + str3);
print("$str2 $str3");

int num1=123;
double num2=3.14;

bool b1=true;
bool b2=false;


// 类型判断
print(str2 is String);
// 注意dart是强类型的
print(123!="123");

类型转换:

String str1="123";
String str2="12.3";
int num1=123;
String str3="";
var num2=0/0;

int i1=int.parse(str1);
double d1=double.parse(str2);
String s1=num1.toString();
bool b1=str3.isEmpty;
bool b2=num2.isNaN;

常量:

// const只能修饰编译时常量,且一开始就要赋值
const num1=100;
const int num2=200;

// final既可以修饰编译时常量也可以修饰运行时(比如对象),它可以一开始不赋值,但只能赋值一次;且final使用懒加载策略,修饰的东西在第一次使用时才会初始化
final name1;
name1="Bob";
final String name2="Bobby";
final time=new DateTime.now();

List、Set和Map:

// List的几种定义方式
var l1=["name1",18,true];
var l2=<String>["str1","str2"];
var l3=List.filled(2,"");// 该方法可定义固定长度的List,如果尝试修改长度则会报错,这里2表示长度,空字符串表示各元素的初始值
var l4=List<String>.filled(2,"");
var l5=new List<String>.filled(2,"");// new可写可不写

l1[0]="name2";
l1.add("company");
print(l1.length);
print(l1[0]);
l1.length=0;//可以这样改变List的长度
l1=l1.reversed.toList();
print(l1.indexOf('name1'));
l1.removeAt(2);
print(l1.join(','));// List转字符串,元素之间用逗号分隔
l1="1,2,3".split(',');// 字符串转List,以逗号分隔


// Set和List的区别在于,Set中的元素不可重复
var s1=new Set();
s1.add('name1');
s1.add('name1');
print(s1);
s1.addAll(l1);


// Map的几种定义方式(key必须是字符串类型)
var m1={
  "name":"zhangsan",
  "age":18
};
var m2=new Map();
m2["name"]="zhangsan";
m2["age"]=18;
m2["company"]=["c1","c2"];

print(m2);
print(m2["name"]);
print(m2.keys.toList());
print(m2.values.toList());

一些方便的遍历方法:

List l1=["a",2,true];
l1.forEach( (value)=>print("$value") );

List l2=[1,2,3,4,5,6];
List l3=l2.map((value){
    return value*2;
}).toList();

// where返回符合条件的值
List l4=l2.where((value){
    return value>4;
}).toList();

// any判断有没有符合条件的值,如果有则返回true,没有就返回false
bool b1=l2.any((value){
    return value>6;
});

// every要求集合里的每个元素都符合条件才会返回true,否则返回false
bool b2=l2.every((value){
    return value>0;
});


var m1={
  "name":"zhangsan",
  "age":18
};
m1.forEach((key,value){
    print(key+" "+value);
});

运算符

运算符:

如果是int? i,这里的?表示空安全,默认情况下变量不能为null,必须赋予初始值,否则编译时就会报错,加了?后表示变量可以为null或它的类型的任意值,该符号可以用在声明变量、函数参数、返回值,范型等地方。

相应的,有i!,这里的!表示非空断言,比如,空安全类型不能传入非空类型参数的函数里,这时就需要使用非空断言进行强制转换,使用非空断言后,如果变量为空,则会抛出异常。

程序结构

Dart同样有if...elseswitch...casetry...catchforfor...inwhiledo...while

if(flag=="flag1"){
    print("1");
}
else if(flag=="flag2"){
    print("2");
}
else{
    print("3");
}

switch(flag){
    case "flag1":
        print("1");
        break;
    case "flag2":
        print("2");
        break;
    default:
        print("3");
        break;
}

// 抛出异常 throw Exception("网络错误")
try{
    var num=double.parse('');
    print(num);
}catch(err){
    print(err);
}

// 循环可以通过break和continue控制
for(int i=0; i<10; i++){
    print(i);
}

List l1=[1,2,3,4];
for(var item in l1){
    print(item);
}

int i=0;
while(i<10){
    i++;
}

do{
    i--;
}while(i>=0);

函数

函数:

// main是入口函数,void可以不写
void main(){
    print(getNum1(1));
}

int getNum1(i1){
    i1++;
    int getNum2(i2){// 函数可以嵌套,但只能在当前作用域内调用
        return i2+1;
    }
    return getNum2(i1);
}

// 可选位置参数(即行参)、默认参数
void fun1(String a, int b, [bool c=true, double d]){ }

// 命名参数,可以加required关键字表示必传参数,也可以指定默认值
void fun2(String name, {required int age, String gender='male', String job}){ }
// 使用的时候要指定名字
fun2('zhangsan',age:18);

// 可以把函数当作参数
void fun3(funName){
    funName();
}

// 匿名函数
var fun4=(String str){
    print(str+"Hello world");
}

// 箭头函数,只能写一行代码
mylist.forEach((value)=>print(value));
// 还可以写成(注意里面不要写分号,也是只能写一行代码):
mylist.forEach((value)=>{
    print(value)
});

// 自执行函数
((int n){
    print(n);
    print("Hello world");
})(10);

Dart支持闭包:

void printNum(){
    int a=1;
    // 返回一个闭包,这样a是局部变量,不会污染全局变量名,也可以常驻内存
    return (){
        a++;
        print(a);
    };
}

var b=printNum();
b();//2
b();//3

Dart支持运算符重载,其中只有部分运算符可被重载,以及->只能被类的成员函数重载:

class Person {
    int age;
    public Person(this.age);

    bool operator+(Person p){
        return this.age + p.age;
    }
}
 
main() {
    Person p1 = new Person(10);
    Person p2 = new Person(10);
    print(p1 == p2);
}

可以重载全局的运算符,但是符号左边的类型需要在参数中指定:

bool operator-(Person p1 ,Person p2){
    return p1.age - p2.age;
}

构造函数命名构造函数访问控制

// 要引入其他文件中的类,使用import
import 'lib/Person2.dart';

class Person{
    String name;
    // 要把变量或函数变为静态,使用static修饰,静态方法只能访问静态成员;静态成员可以通过像Person.age来访问
    static int age;
    // Dart没有访问修饰符,要把变量或函数改为私有,可以在名字前面加一个下划线,并且要单独把该类放到一个文件里才能生效
    String _gender;

    // 默认构造函数,只能有一个
    Person(this.name, {required this.age, this._gender});
    /* 上面的构造函数相当于
    Person(String name, int age, String gender){
        this.name=name;
        this.age=age;
        this._gender=gender;
    }*/

    // 命名构造函数,可以有多个
    Perosn.initAge(int age){
        this.age=age;
    }

    void getInfo(){
        print("${this.name} ${this.age}");
    }
}

void main(){
    // 从Dart 2.x 开始,new关键字可以省略不写
    Person p1=new Person("zhangsan",18);
    print(p1.name);
    p1.getInfo();

    print(Person.age);

    Person p2=new Person.initAge(18);
}

初始化列表

class Rect{
    int width;
    int height;
    // 在构造的时候赋予初始值
    Rect():width=5,height=10{
    }
}

空安全

class Person{
    // Dart要求每个变量都要初始化,否则无法通过静态检查,加了late关键字就可以通过静态检查,但若忘记赋值,该变量的值是随机的,会带来风险
    late int _age;

    void young() { _age = '18'; }
    void old() { _age = '48'; }
    int readAge(){ return 20; }
}
 
main() {
    var p = Person();
    p.young();
    p.old();
}

延迟初始化:若上面的例子改成late int _age = readAge();,则只要_age没有被使用,则永远不会调用readAge方法,相当于懒加载。

常量构造函数

首先要知道const修饰实例化构造函数的作用:

import 'dart:core';

void main(){
    var o1 = Object();
    // identical函数检查两个引用是否指向同一对象
    var b1 = identical(o1, Object());// false
    var b2 = identical(o1, o1);// true
    var b3 = identical([1], [1]);// false
    var b4 = identical(const Object(), const Object());// true,因为const关键字在多个地方创建相同的对象时,会共享内存
    var b5 = identical(const [1], const [2]);// false,因为构造时传的参不一样,不是“相同的对象”
    var b6 = identical(const [1], const [1]);// true
}

使用常量构造函数的类可以节省内存空间:

class Rect{
    final int width;
    final int height;
    // 构造函数用const修饰表示常量构造函数,只有当所有成员变量都是final时,才能使用常量构造函数
    // 实例化常量构造函数时,也需要用const修饰(否则即使调用的是常量构造函数,参数一样也会占用多份内存空间)
    const Rect({required this.width, required this.height});
}

void main(){
    var r1 = const Rect(width: 10, height: 10);// 只有定义了常量构造函数的类,才能用const修饰构造函数
    var r2 = const Rect(width: 10, height: 10);
    var r3 = Rect(width: 10, height: 10);
    
    var b1 = identical(r1, r2);// true
    var b2 = identical(r1, r3);// false
}

get、set修饰符

class Rect{
    num width;
    num height;
    Rect(this.width, this,height);
    get area{
        return this.width*this,height;
    }
    set setHeight(value){
        this.height=value;
    }
}

void main(){
    Rect r=new Rect(5,10);
    //使用时可以通过属性的方式直接访问get/set修饰的函数
    print(r.area);
    r.setHeight=5;
}

继承对象运算符?isas..):

class Perosn{
    String name;
    int age;
    Person(this.name,this.age);
    void printInfo(){
        print("name:${this.name}, age:${this.age}");
    }
    void fun1(){}
}

// Dart只能单继承
// 使用extends继承,可以继承父类中可见的属性和方法,但不会继承构造函数
class Child extends Person{
    String gender;

    // 给父类的构造函数传参
    Child(String name, int age, String gender) : super(name, age){
        this.gender=gender;
    }
    /* 如果要给父类的命名构造函数传参:
    Child(String name, int age, String gender) : super.xxx(name, age){ ... } */
    
    // 子类可以重写父类的方法,重写时最好加上override的注解
    @override
    void fun1(){
        super.printInfo();
        print(this.name);
    }
}

void main(){
    Person p;
    p?.printInfo();// 加的问号表示如果对象为null则不执行后面的方法调用

    print(p is Object);// is 表示类型判断,所有类的父类都是Object

    var c=new Child("zhangsan",18, "male");
    (c as Person).printInfo();// as 表示类型转换,这里不用as也可以,只是为了演示

    // .. 表示级联(即连缀)
    c..name="lisi"
     ..age=20
     ..printInfo();
}

抽象类(接口)

Dart中没有接口的关键字,接口可以用类或者抽象类间接实现,一般都用抽象类:

// 抽象类使用abstract修饰,抽象类不能被实例化,只有继承/实现它的子类才可以实例化
abstract class Animal{
    // 子类如果要继承(extends)/实现(implements)抽象类,就必须实现所有的抽象属性和抽象方法
    String name;// 抽象属性
    eat();// 抽象方法,它没有方法体

    // 也可以有普通的方法
    run(){
        print("running");
    }
}

abstract class B{
    b();
}

// 可以实现多个接口,用逗号分隔
class Dog implements Animal,B{
    @override
    String name;
    
    Dog(this.name);

    @override
    eat() => print("a dog is eating");

    @override
    b() => print("b");
}

// 只能继承一个类,抽象类也可以被继承
class Cat extends Animal{
    @override
    String name;
    
    Cat(this.name);

    @override
    eat() => print("a cat is eating");
}

main(){
    Dog d = new Dog("xx");
    d.eat();
    d.run();

    //多态:将子类类型赋值给父类类型,同一函数调用会有不同的效果
    Animal a = new Cat("xxx");
    a.eat();
}

关于extends和implements的使用:如果要重写抽象方法,则用extends;如果要把抽象类当作标准,则用implements。

Mixins

mixins即混入,就是在类中混入其他功能。在Dart中可以使用mixins实现类似多继承的功能:

class A{
    void a() => print('a');
}

class B{
    void b() => print('b');
}

// 可以Mixins多个
// 如果要使用Mixins,则这里的A、B都只能继承自Object,且A、B无法定义构造函数
class C with A,B{}

更一般地,会使用mixin修饰:

// 比如我们定义了一个Person,然后不同的人有不同的技能,比如Cure、Teaching、Coding,以此成为不同的职业
class Person {
    /*
    // Perosn可以有构造函数
    String name;
    Person(this.name);
    */
    study() => print('study');
}

// 同样地,Mixins的类只能继承自Object,且无法定义构造函数
mixin Cure {
    cure() => print('cure');
}

mixin Teaching {
    teaching() => print('teaching');
}

// 添加限制条件,只有Person才有Coding这个技能,别的类不能混入Coding
mixin Coding on Person{
    coding() => print('coding');

    // Mixins虽然只能继承自Object,但里面也可以重写方法
    @override
    study() {
        super.study();// 调用父类的study()
        print('children');
    }
}

// 使用with关键字来给Person添加Mixins
// 比如Doctor,它的类型既是Person,也是Cure
class Doctor extends Person with Cure {}
class Teacher extends Person with Teaching {}
class Programmer extends Person with Coding {}

要注意混入的顺序:

mixin A {
    f() => print('A');
}

mixin B {
    f() => print('B');
}

// 这时,B中的f函数会覆盖掉A中的f函数
class AB with A,B{
    // 要是这里也有f函数,则会覆盖掉A和B中的f函数
}

泛型

泛型函数:

// 普通的方法:传入int,返回int
int getData1(int value){
    return value;
}

// 泛型方法:传入任意类型,返回同样的类型
T getData2<T>(T value){
    return value;
}

void main(){
    print(getData2<int>(123));
}

泛型类(泛型接口定义方法一样):

class MyArray<T>{
    List list = <T>[];
    void add(T data){
        this.list.add(date);
    }
    List getList() => return this.list;
}

void main(){
    MyArray a1=new MyArray<int>();
    a1.add(1);
    a1.add(2);

    // 比如List也可以指定泛型
    List list1 = <String>["str1","str2"];
    List list2 = new List.filled<String>(2,"");
}

异步

常规写法:

Future<String> getNetworkData(){
    return new Future<String>((){
        sleep(Duration(seconds: 2));
        return 'xxx';
    });
}

void main() async{
    print("start");
    Future<String> f = getNetworkData();
    f.then((result)=>{ print(result) }).catchError((err)=>{ print(err) });
    print("end");
}

使用async和await关键字,可以实现用同步的写法写异步:

// await只能用在async方法里,用了await的方法都要加async;别的函数要调用异步函数拿返回值时,也要使用await关键字
Future<String> getNetworkData() async{
    sleep(Duration(seconds: 2));
    return 'xxx';// 虽然返回的是String,但由于函数加了async,返回值会自动包装进Future中
}

void main() async{
    print("start");
    var result = await testAsync();// 加了await会等待异步方法执行完成,且await会自动解包Future
    print(result);
    print("end");
}

库的重命名、部分导入:

// 如果import的时候类名冲突,可以使用as来指定命名空间,使用时:a.Abc a=new a.Abc();
import '/lib/abc1.dart';
import '/lib/abc2.dart' as a;

// 导入时可以指定只能访问某函数,或者不能访问某函数
import 'lib/ab1.dart' show fun1;
import 'lib/ab2.dart' hide fun2;

库的种类

第一种:自定义的库:

import 'myLib/xxx.dart';

第二种:系统内置库:

import 'dart:io';
import 'dart:convert';

void main() async{
    var result = await _getData();
}
_getData() async{
    var httpClient = new HttpClient();
    var uri = Uri.http('new-at.zhihu.com','/api/3/stories/latest');
    var request = await httpClient.getUrl(uri);
    var response = await request.close();
    return await response.transform(utf8.decoder).join();
}

第三种:pub包管理系统中的库:https://pub.dev、https://pub.flutter-io.cn/packages

如何使用第三方库:

1、在项目根目录创建一个叫pubspec.yaml的文件;

2、在该文件中配置名称、描述、依赖等信息,比如:

name: 项目名
description: 项目描述
version: 1.0.0
dependencies:
  http: ^0.12.0+2
  data_format: ^1.0.6

3、然后命令行进入项目所在文件夹,运行pub get下载第三方库;

4、项目中导入库,比如import 'package:http/http.dart' as http;,具体看文档。

Flutter

Flutter是通过在Android或IOS或其他平台上安装一个渲染引擎,来实现跨平台的,该引擎在不同的平台使用不同的代码编写,但实现的功能是一样的,因此,Flutter的UI不受系统UI库的影响。

Flutter的代码放在项目的lib文件夹下;图片则一般是放在项目的images文件夹下,且在pubspec.yaml中配置要用到的图片:

flutter:
  assets:
    - images/1.jpeg
    - images/2.jpeg
    - images/ #如果文件夹下的所有图片都要用

最后使用Image(height: 20, width: 20, image: AssetImage('images/ic_background.png'))导入。

自定义Widget

在Flutter中,一切显示的部件皆为Widget,比如下面就使用了两个Widget,分别是Center和Text:

import 'package:flutter/material.dart';

void main(){
    // 要显示内容,使用runApp;要显示不变的内容,最好用const修饰
    runApp(const Center(
        child: Text(
            "Hello World",
            textDirection: textDirection.ltr,
        ),
    ));
}

上面的Center(xxx)、Text(xxx)是构造函数,像textDirection是命名参数,具体有什么参数点进源码就能看到。

看源码可以发现,像Text中的属性都是final修饰的,因此只能赋值一次,而且大多都有默认值,相当于Widget一创建就被渲染了,无法更改,要改变只能新建一个Text然后替换掉原来的。Flutter底层是树的结构,要改变UI,只要改变树中的某个节点就可以了,这就是Flutter的增量渲染。

自定义Widget

import 'package:flutter/material.dart';

// Widget分为两种,一种是有状态(stateful),一种是无状态(stateless);区别就是能否更改内容
class MyWidget extends StatelessWidget{
    @override
    Widget build(BuildContext context){
        return xxx;
    }
}

要使用Material Design,可以:

import 'package:flutter/material.dart';

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

class App extends StatelessWidget{
    @override
    Widget build(BuildContext context){
        return MaterialApp(
            home: Scaffold(
                appBar: AppBar(
                    tittle: const Text('Demo'),
                ),
                body: MyWidget(),
            ),
        );
    }
}

常用Widget

Text

Text(
    "xxx",
    textAlign: TextAlign.center,
    style: TextStyle(fontSize: 16.0),
    maxLines: 3,
    overflow: TextOverflow.ellipsis,
);

RichText

// RichText可以实现一段文字里有不同的样式
RichText(
    text: const TextSpan(
        children: <TextSpan>[
            TextSpan(
                text: '123',
                style: TextStyle(fontSize: 16.0, color: Colors.black),
            ),
            TextSpan(
                text: '321',
                style: TextStyle(color: Colors.red)
            ),
        ],
    ),
);

ListView

模型类lib/model/car.dart,属性一般都写成final:

class Car{
    const Car({this.name, this.imageUrl});
    final String? name;
    final String? imageUrl;
}

lib/main.dart

import 'package:flutter/material.dart';
import 'model/car.dart';

// 假设获取了一堆数据
final List<Car> datas = [const Car(name: "123", imageUrl: "xxx"),];

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

class App extends StatelessWidget{
    @override
    Widget build(BuildContext context){
        return MaterialApp(home: Home());
    }
}

class Home extends StatelessWidget{
    @override
    Widget build(BuildContext context){
        return Scaffold(
            appBar: AppBar(tittle: const Text('Demo')),
            body: ListView.builder(
                // 这里指定要传入的函数
                itemBuilder: _itemForRow,
                itemCounts: datas.length,
            ),
        );
    }

    //返回每一行要显示的Widget
    Widget _itemForRow(BuildContext context, int index){
        //return Text(datas[index].name!);
        // 一般会用Container
        return Container(
            color: Colors.White,
            margin: const EdgeInsets.all(10),
            child: Column(
                children: <Widget>[
                    Image.network(datas[index].imageUrl!),
                    Container(height: 10,),/*Sizebox 也可以用来占位*/
                    Text(datas[index].name!),
                ],
            ),
        );
    }
}

Container

Container(
    color: Colors.yellow,
    child: Row(
        children: <Widget>[
            Container(
                color: Colors.red,
                child: Icon(
                    Icons.add,
                    size: 15
                ),
                padding: EdgeInsets.all(10),// 内边距
                margin: EdgeInsets.all(5),// 外边距
                height: 20,// 如果padding后的大小超过height,以height为准,超出部分不显示
            ),
        ],
    ),
);

布局:Row和Column

Container(
    color: Colors.yellow,
    alignment: Alignment(0, 0),// 使用百分比坐标指定位置,参数取值范围-1.0至1.0;(0,0)即该Container的正中心
    child: Text('layout'),
);

用来指定布局排列方式的Widget有:Row(主轴方向从左到右)、Column(主轴方向从上到下)、Center,以Row为例:

Container(
    color: Colors.yellow,
    // 由于Row是占满一行的,这样写会把该行竖向居中,但是该行会从最左边开始;对Column依此类推
    alignment: Alignment(0, 0),
    child: Row(
        children: [
            Icon(Icons.add, size: 60),
            Icon(Icons.ac_unit, size: 60),
            Icon(Icons.access_alarm, size: 60),
        ],
    ),
);

对于Row和Column,如果要对齐右/下,可以设置mainAxisAlignment: MainAxisAlignment.end,但是内部Widget的方向不会变;主轴还可以设置成其他值,比如居中、两边对齐、环绕等分…除此以外,还有交叉轴,即垂直于主轴的轴,交叉轴对于Row是从上往下的,对Column是从左到右的,要对齐下/右,可以设置crossAxisAlignment: CrossAxisAlignment.end

要改变Row内部Widget的排列顺序同时改变主轴方向,可以用textDirection,比如textDirection: textDirection.rtl,Widget从右往左排列,且对齐右边,但文字方向依然不会改变。Column没有类似的属性。

Container(
    color: Colors.yellow,
    alignment: Alignment(0, 0),
    child: Row(
        textDirection: textDirection.rtl,
        mainAxisAlignment: MainAxisAlignment.start,
        // 如果要使用CrossAxisAlignment.baseline,就必须结合textBaseline使用
        crossAxisAlignment: CrossAxisAlignment.baseline,
        //textBaseline: TextBaseline.alphabetic,// 英文字符对齐
        textBaseline: TextBaseline.ideographic,// 中文字符对齐
        children: [
            Container(child: Text('你好', style: TextStyle(fontSize: 12)), color: Colors.red),
            Container(child: Text('啊啊', style: TextStyle(fontSize: 24)), color: Colors.blue),
            Container(child: Text('呃呃', style: TextStyle(fontSize: 36)), color: Colors.white),
        ],
    ),
)

布局:Expanded

Expanded在主轴方向拉伸widget,Widget之间不会留下缝隙;主轴横向width设置没有意义,主轴纵向height设置没有意义;如果Text被Expanded包装,会自动换行。

Container(
    color: Colors.yellow,
    alignment: Alignment(0, 0),
    child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        crossAxisAlignment: CrossAxisAlignment.baseline,
        textBaseline: TextBaseline.ideographic,
        // 对于Row,几个children拉伸后占满一行(字体不拉伸,只拉伸背景),即便设置了width也会被拉伸,且即使设置了spaceEvenly,中间也没有空隙,且文字会自动换行
        children: [
            Expanded(child: Container(width: 80, child: Text('你好', style: TextStyle(fontSize: 12)), color: Colors.red)),
            Expanded(child: Container(child: Text('啊啊', style: TextStyle(fontSize: 24)), color: Colors.blue)),
            Expanded(child: Container(child: Text('呃呃', style: TextStyle(fontSize: 36)), color: Colors.white)),
        ],
    ),
)

对于交叉轴,可以使用crossAxisAlignment: CrossAxisAlignment.strech来填充,比如对于column,就是横向填满。

布局:Stack

Stack可以将多个Widget堆叠起来,堆叠时默认按代码顺序,最后写的在最上面:

Container(
    color: Colors.yellow,
    alignment: Alignment(0, 0),
    // Stack会放在外部的Container的中心,它的大小是children中最大的大小,内部对齐左上角
    child: Stack(
        children: [
            Container(child: Icon(Icons.add, size: 60), color: Colors.red),
            Container(child: Icon(Icons.ac_unit, size: 40), color: Colors.blue),
            Container(child: Icon(Icons.access_alarm, size: 20), color: Colors.white),
        ],
    ),
);

也可以指定位置:

Container(
    color: Colors.yellow,
    alignment: Alignment(0, 0),
    child: Stack(
        children: [
            Container(child: Icon(Icons.add, size: 80),color: Colors.red),
            Positioned(
                // 如果Positioned的left和right都设置了,那么child的宽也就定下来的,child内就不用再设置width
                child: Container(child: Icon(Icons.ac_unit, size: 40),color: Colors.blue),
                right: 5,
            ),
            Positioned(
                child: Container(child: Icon(Icons.access_alarm, size: 20),color: Colors.white),
                left: 10,
            ),
        ],
    ),
);

布局:宽高比

Container(
    color: Colors.yellow,
    alignment: Alignment(0, 0),
    child: Container(
        color: Colors.blue,
        height: 150,// 同时设置宽和高,则AspectRatio无效,只需设置其中一个就行
        child: const AspectRatio(aspectRatio: 1/2),// 它虽然写在child里,但它设置的是父Widget的宽高比
        // 注意,AspectRatio虽然可以写child,但是child的宽、高设置都无效,即使设置了也会占满父Widget,一般不用
    ),
);

关于默认宽高:

Container(
    color: Colors.yellow,
    width: 300,
    height: 300,
    alignment: Alignment(0, 0),
    child: Container(
        color: Colors.blue,
        height: 150,// 这里只写高度,宽度默认是被拉伸成父Widget的宽度的;若宽高都不写,就会占满父部件
    ),
);

Widget状态管理

StatefulWidget的思路是渲染逻辑和数据逻辑分开:

// StatefulWidget就是重写函数返回状态管理者
class StateManagerDemo extends StatefulWidget{
    @override
    State<StatefulWidget> createState() => _MyState();
}

// 要使用有状态的Widget,需要一个状态管理者,该类用来在更新数据后刷新界面
// 在State中,要访问Widget的属性,可以用widget.xxx
class _MyState extends State<StateManagerDemo>{
    int count = 0;
    _currentIndex = 0;

    @override
    Widget build(BuildContext context){
        return MaterialApp(
            debugShowCheckedModeBanner: false,
            home: Scaffold(
                backgroundColor: Colors.grey[100],
                appBar: AppBar(tittle: const Text('Demo')),
                body: Center(child: Chip(label: Text("count: $count"))),
                floatingActionButton: FloatingActionButton(
                    child: const Icon(Icons.add),
                    onPressed: (){// 匿名函数写法
                        // setState传入一个函数,用来改变数据,这样界面才能随数据改变实时刷新
                        // 每调一次setState相当于build了一次,但由于Flutter是增量渲染的,只会重新创建改了数据的部分
                        setState((){
                            count += 1;
                        });
                        print("count = $count");
                    },
                ),
                bottomNavigationBar: BottomNavigationBar(
                    onTap: (index){
                        setState((){
                            _currentIndex = index;
                        });
                    },
                    type: BottomNavigationBarType.fixed,
                    fixedColor: Colors.green,
                    currentIndex: _currentIndex,
                    item: const [
                        BottomNavigationBarItem(icon: Icon(AssetImage('images/1.png')), label: '首页'),
                        BottomNavigationBarItem(icon: Icon(AssetImage('images/2.png')), label: '发现'),
                    ],
                ),
            ),
        );
    }
}

Key的作用

前面一直没有重写Widget的构造函数,实际上Widget的构造函数是可以传入key的,关于Widget的key的作用:

我们定义一个Stateless的Widget,指定宽高并赋予随机颜色,

class MyStatelessContainer extends StatelessWidget {
    final Color color = RandomColor().randomColor();

    @override
    Widget build(BuildContext context) {
        return Container(width: 100, height: 100, color: color);
    }
}

然后在界面上显示两个上述定义的Widget,并在点击按钮时交换两者的位置,

class MyScreen extends StatefulWidget {
    @override
    _MyScreenState createState() => _MyScreenState();
}

class _MyScreenState extends State<Screen> {
    List<Widget> widgets = [MyStatelessContainer(), MyStatelessContainer()];

    @override
    Widget build(BuildContext context) {
        return Scaffold(
            body: Center(
                child: Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: widgets
                )
            ),
            floatingActionButton: FloatingActionButton(
                onPressed: switchWidget,
                child: Icon(Icons.undo),
            ),
        );
    }

    switchWidget(){
        setState(() {
            widgets.insert(0, widgets.removeAt(1));
        });
    }
}

目前程序可以正常运行,但是如果把MyStatelessContainer换成Stateful

class MyStatefulContainer extends StatefulWidget {
    MyStatefulContainer({Key key}) : super(key: key);

    @override
    _MyStatefulContainerState createState() => _MyStatefulContainerState();
}

class _MyStatefulContainerState extends State<MyStatefulContainer> {
    final Color color = RandomColor().randomColor();

    @override
    Widget build(BuildContext context) {
        return Container(width: 100, height: 100, color: color);
    }
}

这时无论怎么点击,都无法交换两者的位置了。但是如果把color放在StatefulWidget中,又变得可以正常交换位置了。

原理

Flutter渲染底层是树形结构,其中Element树是根据Widget树自动生成的,它俩是一一对应的,此外还有Render树

因此ElementRenderObject的重建非常消耗资源,只会在有必要的情况下才会重建。

当我们调用setState时,Flutter就会根据各个Widget节点是否有更改,去决定是否需要重建对应的Element节点,这个判断的函数就是canUpdate,它主要判断新Widget的runTimeType以及key是否和旧Widget相同:

不难看出,key只在StatefulWidget的情况下有用,而StatelessWidget是不需要key的。

解析

Stateless的情况下,我们互换的是Widget,而color写在了Widget里,因此MyStatefulContainer所渲染的颜色也会发生改变。

Stateful的情况下,由于state是由Element管理的,而我们把color放在了State中,我们互换的是Widget,此时调用setState,会发现两个MyStatefulContainer的类型是一致的,且没有设置key,因此会复用Element,而Element没有发生互换,则其中存储colorstate也就没有发生互换,因此颜色没有改变。

解决方法

如果想在State中定义变量,且希望仍能正常刷新,只需要在构造的时候加上key:

class _MyScreenState extends State<Screen> {
  List<Widget> widgets = [
    StatefulContainer(key: ValueKey('111')),
    StatefulContainer(key: ValueKey('222')),
  ];
...

加上key之后,Widget和Element就有了对应关系,如果一方的key没有对应就会重新在同层级下寻找,如果找不到,最终这个Widget或Element就会被删除。

key分为两种: