变量名必须由数字、字母、下划线、美元符号$
组成,且不能以数字开头,且不能是关键字。变量名区分大小写。
注释的写法:
//注释
/*注释*/
基本数据类型:
// 自动类型推断
var str1='string1';
// 字符串用单引号或双引号定义都可以
String str2="string2";
String str3="""换
行
字符串""";
// 字符串拼接与打印
+ " " + str3);
print(str2 "$str2 $str3");
print(
int num1=123;
double num2=3.14;
bool b1=true;
bool b2=false;
// 类型判断
is String);
print(str2 // 注意dart是强类型的
123!="123"); print(
类型转换:
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;
="Bob";
name1final 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可写可不写
0]="name2";
l1[.add("company");
l1.length);
print(l10]);
print(l1[.length=0;//可以这样改变List的长度
l1=l1.reversed.toList();
l1.indexOf('name1'));
print(l1.removeAt(2);
l1.join(','));// List转字符串,元素之间用逗号分隔
print(l1="1,2,3".split(',');// 字符串转List,以逗号分隔
l1
// Set和List的区别在于,Set中的元素不可重复
var s1=new Set();
.add('name1');
s1.add('name1');
s1
print(s1);.addAll(l1);
s1
// Map的几种定义方式(key必须是字符串类型)
var m1={
"name":"zhangsan",
"age":18
};
var m2=new Map();
"name"]="zhangsan";
m2["age"]=18;
m2["company"]=["c1","c2"];
m2[
print(m2);"name"]);
print(m2[.keys.toList());
print(m2.values.toList()); print(m2
一些方便的遍历方法:
List l1=["a",2,true];
.forEach( (value)=>print("$value") );
l1
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
};
.forEach((key,value){
m1+" "+value);
print(key});
运算符:
+
、减-
、乘*
、除/
、取余%
、取整~/
等运算符进行计算==
、!=
、>
、<
、>=
、<=
!
、并$$
、或||
=
、为空才赋值??=
(比如b??=23
,表示如果b
为空则赋值为23
)+=
、-=
、*=
、/=
、%=
、~/=
c=flag?"a":"b"
,表示如果flag
是true
则把a
赋值给c
,否则把b
赋值给c
??
运算符:c=a ?? b
,表示如果a
为空则将b
赋值给c
,否则把a
的值赋值给c
a++
或++a
,自减a--
或--a
如果是int? i
,这里的?
表示空安全,默认情况下变量不能为null,必须赋予初始值,否则编译时就会报错,加了?
后表示变量可以为null或它的类型的任意值,该符号可以用在声明变量、函数参数、返回值,范型等地方。
相应的,有i!
,这里的!
表示非空断言,比如,空安全类型不能传入非空类型参数的函数里,这时就需要使用非空断言进行强制转换,使用非空断言后,如果变量为空,则会抛出异常。
Dart同样有if...else
、switch...case
、try...catch
、for
、for...in
、while
、do...while
if(flag=="flag1"){
"1");
print(}
else if(flag=="flag2"){
"2");
print(}
else{
"3");
print(}
switch(flag){
case "flag1":
"1");
print(break;
case "flag2":
"2");
print(break;
default:
"3");
print(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(){
1));
print(getNum1(}
int getNum1(i1){
++;
i1int 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}){ }
// 使用的时候要指定名字
'zhangsan',age:18);
fun2(
// 可以把函数当作参数
void fun3(funName){
funName();}
// 匿名函数
var fun4=(String str){
+"Hello world");
print(str}
// 箭头函数,只能写一行代码
.forEach((value)=>print(value));
mylist// 还可以写成(注意里面不要写分号,也是只能写一行代码):
.forEach((value)=>{
mylist
print(value)});
// 自执行函数
int n){
((
print(n);"Hello world");
print(})(10);
Dart支持闭包:
void printNum(){
int a=1;
// 返回一个闭包,这样a是局部变量,不会污染全局变量名,也可以常驻内存
return (){
++;
a
print(a);};
}
var b=printNum();
//2
b();//3 b();
Dart支持运算符重载,其中只有部分运算符可被重载,以及->
只能被类的成员函数重载:
class Person {
int age;
this.age);
public Person(
bool operator+(Person p){
return this.age + p.age;
}
}
{
main() = new Person(10);
Person p1 = new Person(10);
Person p2 == p2);
print(p1 }
可以重载全局的运算符,但是符号左边的类型需要在参数中指定:
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;
// 默认构造函数,只能有一个
this.name, {required this.age, this._gender});
Person(/* 上面的构造函数相当于
Person(String name, int age, String gender){
this.name=name;
this.age=age;
this._gender=gender;
}*/
// 命名构造函数,可以有多个
.initAge(int age){
Perosnthis.age=age;
}
void getInfo(){
"${this.name} ${this.age}");
print(}
}
void main(){
// 从Dart 2.x 开始,new关键字可以省略不写
=new Person("zhangsan",18);
Person p1.name);
print(p1.getInfo();
p1
.age);
print(Person
=new Person.initAge(18);
Person p2}
初始化列表:
class Rect{
int width;
int height;
// 在构造的时候赋予初始值
:width=5,height=10{
Rect()}
}
空安全:
class Person{
// Dart要求每个变量都要初始化,否则无法通过静态检查,加了late关键字就可以通过静态检查,但若忘记赋值,该变量的值是随机的,会带来风险
late int _age;
void young() { _age = '18'; }
void old() { _age = '48'; }
int readAge(){ return 20; }
}
{
main() var p = Person();
.young();
p.old();
p}
延迟初始化:若上面的例子改成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;this.width, this,height);
Rect(get area{
return this.width*this,height;
}
set setHeight(value){
this.height=value;
}
}
void main(){
=new Rect(5,10);
Rect r//使用时可以通过属性的方式直接访问get/set修饰的函数
.area);
print(r.setHeight=5;
r}
继承,对象运算符(?
、is
、as
、..
):
class Perosn{
String name;
int age;
this.name,this.age);
Person(void printInfo(){
"name:${this.name}, age:${this.age}");
print(}
void fun1(){}
}
// Dart只能单继承
// 使用extends继承,可以继承父类中可见的属性和方法,但不会继承构造函数
class Child extends Person{
String gender;
// 给父类的构造函数传参
String name, int age, String gender) : super(name, age){
Child(this.gender=gender;
}
/* 如果要给父类的命名构造函数传参:
Child(String name, int age, String gender) : super.xxx(name, age){ ... } */
// 子类可以重写父类的方法,重写时最好加上override的注解
@override
void fun1(){
super.printInfo();
this.name);
print(}
}
void main(){
Person p;?.printInfo();// 加的问号表示如果对象为null则不执行后面的方法调用
p
is Object);// is 表示类型判断,所有类的父类都是Object
print(p
var c=new Child("zhangsan",18, "male");
as Person).printInfo();// as 表示类型转换,这里不用as也可以,只是为了演示
(c
// .. 表示级联(即连缀)
..name="lisi"
c..age=20
..printInfo();
}
Dart中没有接口的关键字,接口可以用类或者抽象类间接实现,一般都用抽象类:
// 抽象类使用abstract修饰,抽象类不能被实例化,只有继承/实现它的子类才可以实例化
abstract class Animal{
// 子类如果要继承(extends)/实现(implements)抽象类,就必须实现所有的抽象属性和抽象方法
String name;// 抽象属性
// 抽象方法,它没有方法体
eat();
// 也可以有普通的方法
{
run()"running");
print(}
}
abstract class B{
b();}
// 可以实现多个接口,用逗号分隔
class Dog implements Animal,B{
@override
String name;
this.name);
Dog(
@override
=> print("a dog is eating");
eat()
@override
=> print("b");
b() }
// 只能继承一个类,抽象类也可以被继承
class Cat extends Animal{
@override
String name;
this.name);
Cat(
@override
=> print("a cat is eating");
eat() }
{
main()= new Dog("xx");
Dog d .eat();
d.run();
d
//多态:将子类类型赋值给父类类型,同一函数调用会有不同的效果
= new Cat("xxx");
Animal a .eat();
a}
关于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);
*/
=> print('study');
study() }
// 同样地,Mixins的类只能继承自Object,且无法定义构造函数
mixin Cure {
=> print('cure');
cure() }
mixin Teaching {
=> print('teaching');
teaching() }
// 添加限制条件,只有Person才有Coding这个技能,别的类不能混入Coding
mixin Coding on Person{
=> print('coding');
coding()
// Mixins虽然只能继承自Object,但里面也可以重写方法
@override
{
study() super.study();// 调用父类的study()
'children');
print(}
}
// 使用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 {
=> print('A');
f() }
mixin B {
=> print('B');
f() }
// 这时,B中的f函数会覆盖掉A中的f函数
class AB with A,B{
// 要是这里也有f函数,则会覆盖掉A和B中的f函数
}
泛型函数:
// 普通的方法:传入int,返回int
int getData1(int value){
return value;
}
// 泛型方法:传入任意类型,返回同样的类型
<T>(T value){
T getData2return value;
}
void main(){
<int>(123));
print(getData2}
泛型类(泛型接口定义方法一样):
class MyArray<T>{
List list = <T>[];
void add(T data){
this.list.add(date);
}
List getList() => return this.list;
}
void main(){
=new MyArray<int>();
MyArray a1.add(1);
a1.add(2);
a1
// 比如List也可以指定泛型
List list1 = <String>["str1","str2"];
List list2 = new List.filled<String>(2,"");
}
常规写法:
Future<String> getNetworkData(){
return new Future<String>((){
: 2));
sleep(Duration(secondsreturn 'xxx';
});
}
void main() async{
"start");
print(Future<String> f = getNetworkData();
.then((result)=>{ print(result) }).catchError((err)=>{ print(err) });
f"end");
print(}
使用async和await关键字,可以实现用同步的写法写异步:
// await只能用在async方法里,用了await的方法都要加async;别的函数要调用异步函数拿返回值时,也要使用await关键字
Future<String> getNetworkData() async{
: 2));
sleep(Duration(secondsreturn 'xxx';// 虽然返回的是String,但由于函数加了async,返回值会自动包装进Future中
}
void main() async{
"start");
print(var result = await testAsync();// 加了await会等待异步方法执行完成,且await会自动解包Future
print(result);"end");
print(}
库的重命名、部分导入:
// 如果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();
}
async{
_getData() 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是通过在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修饰
const Center(
runApp(: Text(
child"Hello World",
: textDirection.ltr,
textDirection,
)
));}
上面的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(
: Scaffold(
home: AppBar(
appBar: const Text('Demo'),
tittle,
): MyWidget(),
body,
)
);}
}
Text("xxx",
: TextAlign.center,
textAlign: TextStyle(fontSize: 16.0),
style: 3,
maxLines: TextOverflow.ellipsis,
overflow );
// RichText可以实现一段文字里有不同的样式
RichText(: const TextSpan(
text: <TextSpan>[
children
TextSpan(: '123',
text: TextStyle(fontSize: 16.0, color: Colors.black),
style,
)
TextSpan(: '321',
text: TextStyle(color: Colors.red)
style,
),
],
) );
模型类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(tittle: const Text('Demo')),
appBar: ListView.builder(
body// 这里指定要传入的函数
: _itemForRow,
itemBuilder: datas.length,
itemCounts,
)
);}
//返回每一行要显示的Widget
, int index){
Widget _itemForRow(BuildContext context//return Text(datas[index].name!);
// 一般会用Container
return Container(
: Colors.White,
color: const EdgeInsets.all(10),
margin: Column(
child: <Widget>[
children.network(datas[index].imageUrl!),
Image: 10,),/*Sizebox 也可以用来占位*/
Container(height.name!),
Text(datas[index],
],
)
);}
}
Container(: Colors.yellow,
color: Row(
child: <Widget>[
children
Container(: Colors.red,
color: Icon(
child.add,
Icons: 15
size,
): EdgeInsets.all(10),// 内边距
padding: EdgeInsets.all(5),// 外边距
margin: 20,// 如果padding后的大小超过height,以height为准,超出部分不显示
height,
),
],
) );
Container(: Colors.yellow,
color: Alignment(0, 0),// 使用百分比坐标指定位置,参数取值范围-1.0至1.0;(0,0)即该Container的正中心
alignment: Text('layout'),
child );
用来指定布局排列方式的Widget有:Row(主轴方向从左到右)、Column(主轴方向从上到下)、Center,以Row为例:
Container(: Colors.yellow,
color// 由于Row是占满一行的,这样写会把该行竖向居中,但是该行会从最左边开始;对Column依此类推
: Alignment(0, 0),
alignment: Row(
child: [
children.add, size: 60),
Icon(Icons.ac_unit, size: 60),
Icon(Icons.access_alarm, size: 60),
Icon(Icons,
],
) );
对于Row和Column,如果要对齐右/下,可以设置mainAxisAlignment: MainAxisAlignment.end
,但是内部Widget的方向不会变;主轴还可以设置成其他值,比如居中、两边对齐、环绕等分…除此以外,还有交叉轴,即垂直于主轴的轴,交叉轴对于Row是从上往下的,对Column是从左到右的,要对齐下/右,可以设置crossAxisAlignment: CrossAxisAlignment.end
。
要改变Row内部Widget的排列顺序同时改变主轴方向,可以用textDirection
,比如textDirection: textDirection.rtl
,Widget从右往左排列,且对齐右边,但文字方向依然不会改变。Column没有类似的属性。
Container(: Colors.yellow,
color: Alignment(0, 0),
alignment: Row(
child: textDirection.rtl,
textDirection: MainAxisAlignment.start,
mainAxisAlignment// 如果要使用CrossAxisAlignment.baseline,就必须结合textBaseline使用
: CrossAxisAlignment.baseline,
crossAxisAlignment//textBaseline: TextBaseline.alphabetic,// 英文字符对齐
: TextBaseline.ideographic,// 中文字符对齐
textBaseline: [
children: 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),
Container(child,
],
) )
Expanded在主轴方向拉伸widget,Widget之间不会留下缝隙;主轴横向width设置没有意义,主轴纵向height设置没有意义;如果Text被Expanded包装,会自动换行。
Container(: Colors.yellow,
color: Alignment(0, 0),
alignment: Row(
child: MainAxisAlignment.spaceEvenly,
mainAxisAlignment: CrossAxisAlignment.baseline,
crossAxisAlignment: TextBaseline.ideographic,
textBaseline// 对于Row,几个children拉伸后占满一行(字体不拉伸,只拉伸背景),即便设置了width也会被拉伸,且即使设置了spaceEvenly,中间也没有空隙,且文字会自动换行
: [
children: 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)),
Expanded(child,
],
) )
对于交叉轴,可以使用crossAxisAlignment: CrossAxisAlignment.strech
来填充,比如对于column,就是横向填满。
Stack可以将多个Widget堆叠起来,堆叠时默认按代码顺序,最后写的在最上面:
Container(: Colors.yellow,
color: Alignment(0, 0),
alignment// Stack会放在外部的Container的中心,它的大小是children中最大的大小,内部对齐左上角
: Stack(
child: [
children: 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(child,
],
) );
也可以指定位置:
Container(: Colors.yellow,
color: Alignment(0, 0),
alignment: Stack(
child: [
children: Icon(Icons.add, size: 80),color: Colors.red),
Container(child
Positioned(// 如果Positioned的left和right都设置了,那么child的宽也就定下来的,child内就不用再设置width
: Container(child: Icon(Icons.ac_unit, size: 40),color: Colors.blue),
child: 5,
right,
)
Positioned(: Container(child: Icon(Icons.access_alarm, size: 20),color: Colors.white),
child: 10,
left,
),
],
) );
Container(: Colors.yellow,
color: Alignment(0, 0),
alignment: Container(
child: Colors.blue,
color: 150,// 同时设置宽和高,则AspectRatio无效,只需设置其中一个就行
height: const AspectRatio(aspectRatio: 1/2),// 它虽然写在child里,但它设置的是父Widget的宽高比
child// 注意,AspectRatio虽然可以写child,但是child的宽、高设置都无效,即使设置了也会占满父Widget,一般不用
,
) );
关于默认宽高:
Container(: Colors.yellow,
color: 300,
width: 300,
height: Alignment(0, 0),
alignment: Container(
child: Colors.blue,
color: 150,// 这里只写高度,宽度默认是被拉伸成父Widget的宽度的;若宽高都不写,就会占满父部件
height,
) );
StatefulWidget
的思路是渲染逻辑和数据逻辑分开:
// StatefulWidget就是重写函数返回状态管理者
class StateManagerDemo extends StatefulWidget{
@override
<StatefulWidget> createState() => _MyState();
State}
// 要使用有状态的Widget,需要一个状态管理者,该类用来在更新数据后刷新界面
// 在State中,要访问Widget的属性,可以用widget.xxx
class _MyState extends State<StateManagerDemo>{
int count = 0;
= 0;
_currentIndex
@override
{
Widget build(BuildContext context)return MaterialApp(
: false,
debugShowCheckedModeBanner: Scaffold(
home: Colors.grey[100],
backgroundColor: AppBar(tittle: const Text('Demo')),
appBar: Center(child: Chip(label: Text("count: $count"))),
body: FloatingActionButton(
floatingActionButton: const Icon(Icons.add),
child: (){// 匿名函数写法
onPressed// setState传入一个函数,用来改变数据,这样界面才能随数据改变实时刷新
// 每调一次setState相当于build了一次,但由于Flutter是增量渲染的,只会重新创建改了数据的部分
{
setState(()+= 1;
count });
"count = $count");
print(},
,
): BottomNavigationBar(
bottomNavigationBar: (index){
onTap{
setState(()= index;
_currentIndex });
},
: BottomNavigationBarType.fixed,
type: Colors.green,
fixedColor: _currentIndex,
currentIndex: const [
item: Icon(AssetImage('images/1.png')), label: '首页'),
BottomNavigationBarItem(icon: Icon(AssetImage('images/2.png')), label: '发现'),
BottomNavigationBarItem(icon,
],
),
)
);}
}
前面一直没有重写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();
_MyScreenState createState() }
class _MyScreenState extends State<Screen> {
List<Widget> widgets = [MyStatelessContainer(), MyStatelessContainer()];
@override
{
Widget build(BuildContext context) return Scaffold(
: Center(
body: Row(
child: MainAxisAlignment.center,
mainAxisAlignment: widgets
children
),
): FloatingActionButton(
floatingActionButton: switchWidget,
onPressed: Icon(Icons.undo),
child,
)
);}
{
switchWidget(){
setState(() .insert(0, widgets.removeAt(1));
widgets});
}
}
目前程序可以正常运行,但是如果把MyStatelessContainer
换成Stateful
,
class MyStatefulContainer extends StatefulWidget {
{Key key}) : super(key: key);
MyStatefulContainer(
@override
=> _MyStatefulContainerState();
_MyStatefulContainerState createState() }
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 = [
: ValueKey('111')),
StatefulContainer(key: ValueKey('222')),
StatefulContainer(key
];...
加上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
可以获取当前组件的配置信息;