变量名必须由数字、字母、下划线、美元符号$组成,且不能以数字开头,且不能是关键字。变量名区分大小写。
注释的写法:
//注释
/*注释*/基本数据类型:
// 自动类型推断
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);
});运算符:
+、减-、乘*、除/、取余%、取整~/
等运算符进行计算==、!=、>、<、>=、<=!、并$$、或||=、为空才赋值??=(比如b??=23,表示如果b为空则赋值为23)+=、-=、*=、/=、%=、~/=c=flag?"a":"b",表示如果flag是true则把a赋值给c,否则把b赋值给c??运算符:c=a ?? b,表示如果a为空则将b赋值给c,否则把a的值赋值给ca++或++a,自减a--或--a如果是int? i,这里的?表示空安全,默认情况下变量不能为null,必须赋予初始值,否则编译时就会报错,加了?后表示变量可以为null或它的类型的任意值,该符号可以用在声明变量、函数参数、返回值,范型等地方。
相应的,有i!,这里的!表示非空断言,比如,空安全类型不能传入非空类型参数的函数里,这时就需要使用非空断言进行强制转换,使用非空断言后,如果变量为空,则会抛出异常。
Dart同样有if...else、switch...case、try...catch、for、for...in、while、do...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();//3Dart支持运算符重载,其中只有部分运算符可被重载,以及->只能被类的成员函数重载:
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;
}继承,对象运算符(?、is、as、..):
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即混入,就是在类中混入其他功能。在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.63、然后命令行进入项目所在文件夹,运行pub get下载第三方库;
4、项目中导入库,比如import 'package:http/http.dart' as http;,具体看文档。
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'))导入。
在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(),
),
);
}
}Text(
"xxx",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16.0),
maxLines: 3,
overflow: TextOverflow.ellipsis,
);// 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)
),
],
),
);模型类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(
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为准,超出部分不显示
),
],
),
);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在主轴方向拉伸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可以将多个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的宽度的;若宽高都不写,就会占满父部件
),
);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: '发现'),
],
),
),
);
}
}前面一直没有重写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树:
Widget是最轻量的,只存储组件的配置信息,它不可修改,要更改只能重建;Element是真正使用的对象,它可以被修改;RenderObject才是真正负责UI渲染的;因此Element和RenderObject的重建非常消耗资源,只会在有必要的情况下才会重建。
当我们调用setState时,Flutter就会根据各个Widget节点是否有更改,去决定是否需要重建对应的Element节点,这个判断的函数就是canUpdate,它主要判断新Widget的runTimeType以及key是否和旧Widget相同:
runTimeType不同,就说明组件的类型发生了改变,Element也就无法复用,会调用Widget的build方法重建Element;key没有设置或相同的情况下,若runTimeType相同,说明只是组件的配置发生了改变,这时可以复用Element,只需修改Element对应RenderObject中的配置信息即可;key是用来唯一标识Widget的,若key不同,说明新Widget和旧Widget不一致,此时对应位置的Element不会立刻被销毁,而是会根据由它创建的RenderObject中的旧Widget,在同级目录下逐个查找:
runTimeType和key都要一样),则会重新建立Element和新Widget的对应关系;build方法重新创建Element,而Element一旦被销毁,那么其中保存的state也会丢失,对应的旧的RenderObject也会被销毁;不难看出,key只在StatefulWidget的情况下有用,而StatelessWidget是不需要key的。
解析:
在Stateless的情况下,我们互换的是Widget,而color写在了Widget里,因此MyStatefulContainer所渲染的颜色也会发生改变。
在Stateful的情况下,由于state是由Element管理的,而我们把color放在了State中,我们互换的是Widget,此时调用setState,会发现两个MyStatefulContainer的类型是一致的,且没有设置key,因此会复用Element,而Element没有发生互换,则其中存储color的state也就没有发生互换,因此颜色没有改变。
解决方法:
如果想在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分为两种:
ValueKey('xxx'),使用任何东西做key都可以,如果使用自定义的对象,且该对象重写了operator ==方法以及hashCode方法,则会按照重写后的结果判断key是否一致;ObjectKey(Object),无论是否重写==运算符,只要是两个不同的对象(内存地址不一样),就会被视为不同的Key;UniqueKey(),它每次调用都会生成一个新的值,因此每次刷新界面的时候,旧Widget的key总是无法找到新Widget的key与之对应,因此状态会一直丢失,这个多用于要强制刷新的动画,比如AnimatedSwitcher;GlobalObjectKey(Object),和ObjectKey一样;GlobalKey(),是全局唯一的键,用途有:
final GlobalKey<_CounterState> _globalKey = GlobalKey(),且某个组件设置了key: _globalKey,则_globalKey.currentWidget可以获取当前组件的配置信息;