第一章 Flutter环境搭建

在第一章,我们将学习如下内容

  • 配置flutter开发环境(IOS)

1.1 配置Flutter环境

1.下载Flutter SDK

打开Flutter中文官网:https://flutterchina.club/setup-macos/

选择下载SDK,我们选择稳定版:Stable channel (macOS)

下载速度比较慢,我们把链接复制到迅雷里面下载

2.配置Flutter路径:

1
export PATH=$PATH:/Library/dev/flutter/bin

3.配置Flutter国内镜像:

打开终端,输入:

1
open .bash_profile

配置镜像地址:https://flutter.dev/community/china

1
2
3
#Flutter
export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn

5.检查Flutter环境

1
flutter doctor

如果提示:Some Android licenses not accepted. To resolve this, run: flutter doctor
–android-licenses,则允许一下:

1
flutter doctor --android-licenses

启动IOS模拟器:

1
open -a Simulator

4.创建项目

1
flutter create my_app

1.2 Flutter for Desktop

官方文档:https://flutter.dev/desktop

Windows版

环境:Visual Studio 2019,添加C++支持

添加桌面支持,打开控制台:

1
2
3
flutter config --enable-windows-desktop
flutter config --enable-macos-desktop
flutter config --enable-linux-desktop

查看是否有支持的桌面:

1
2
3
4
5
6
7
flutter devices
1 connected device:

Windows (desktop) • windows • windows-x64 • Microsoft Windows [Version 10.0.18362.1082]
macOS (desktop) • macos • darwin-x64 • macOS 11.2 20D64 darwin-x64
Linux (desktop) • linux • linux-x64 • Linux

检测环境是否正常,以下是Windows

1
2
3
4
5
6
7
8
9
PS > flutter doctor                                                                     
Doctor summary (to see all details, run flutter doctor -v):
[√] Flutter (Channel beta, 1.27.0-1.0.pre, on Microsoft Windows [Version 10.0.19042.782], locale en-AU)
[√] Android toolchain - develop for Android devices (Android SDK version 30.0.3)
[√] Chrome - develop for the web
[√] Visual Studio - develop for Windows (Visual Studio Community 2019 16.7.7)
[√] Android Studio (version 4.1.0)
[√] VS Code (version 1.51.1)
[√] Connected device (3 available)

支持已有项目:

1
flutter create --platforms=windows,macos,linux .

运行:

1
2
3
flutter run -d windows
flutter run -d macos
flutter run -d linux

修改桌面窗体标题:

1
2
3
4
5
6
dependencies:
window_size:
git:
url: git://github.com/google/flutter-desktop-embedding.git
path: plugins/window_size
ref: fd519be1e8398c6b6c2062c2447bac960a71bc02

在代码中:

1
2
3
import 'package:window_size/window_size.dart';

setWindowTitle("Custom window title");

1.3 Flutter for Web

官方文档:https://flutter.dev/docs/get-started/web

检测是否有chrome环境:

1
2
3
flutter devices
1 connected device:
Chrome (web) • chrome • web-javascript • Google Chrome 88.0.4324.150

支持已有项目:

1
flutter create .

运行:

1
2
普通运行: flutter build web
指定端口和ip运行:flutter run -d chrome --web-port=8080 --web-hostname=127.0.0.1

1.4 响应式App

官网文档:https://flutter.dev/docs/development/ui/layout/adaptive-responsive

第二章 Dart基础

Dart入门参考学习资料:https://www.dartcn.com/guides/language/language-tour

1.基础数据类型

  • 数字
    • num
    • int
    • double
  • 字符串
  • 布尔
  • 集合
  • var

1.数字类型

num是数字类型的父类,有两个子类,int和double

求绝对值:abs

类型转换:toInt(),round()

示例代码:

1
2
3
4
5
6
7
8
9
10
num num1 = -1.5;
int num2 = 1;
double num3 = 1.4;

print("num1=$num1,num2=$num2,num3=$num3");
//求绝对值
print("$num1的绝对值是${num1.abs()}");
//数据类型转换
print("$num1的转为整数是${num1.toInt()}");
print("$num1四舍五入是${num1.round()}");

转String,保留小数位数

1
toStringAsFixed(位数),如:保留1位小数  如:5 -> "5.0" 

其他知识点:

  • final:被final修饰,只能赋值一次

  • const:被const修饰的是常量,不能被修改

  • 除法和整除:/表示除法,结果为小数, ~/表示整除

1
2
3
4
5
var a = 10 /5;
print(a); //2.0

var b = 9.1~/5;
print(b); //1
  • 是否是偶数:isEven(),是否是奇数:isOdd()
  • round:四舍五入 floor:向下取整 ceil:向上取整

2.字符串

使用String定义,字符串拼接类似kotlin,也可以使用Java方式拼接

1
2
3
4
5
String str1 = '字符串',str2="双引号";
String str3 = "str1:$str1 str2:$str2";
print("str1=$str1");//演示字符串拼接
print("str2=$str2");
print("str3=$str3");

可以使用单引号,双引号,三引号定义字符串,以及使用r’字符串’定义一个不转义的字符串

1
2
3
4
var str1 = "hello \n world";
var str2 = r'hello \n world';
print(str1);
print(str2); // 不转义,直接输出,结果:hello \n world

常用方法,自己尝试,类似Java的用法

1
2
3
4
5
String str = "Hello WROLD";
print(str.toUpperCase());
print(str.toLowerCase());
print(str.substring(0,5));
print(str.indexOf('e'));

字符串乘法:

1
2
var str3 = "love you " * 5;
print(str3);//love you love you love you love you love you

==比较的是内容

[]取字符串索引位置的字符

1
2
3
var str4 = "hello";
print(str4[0]);//h
print(str4.length);//5

判断内容是否为空:

1
2
String str5 = "";
print(str5.isEmpty);//只能判断内容是否为空,不能判断是否为null

split:拆分字符串,拆分结果是列表,这里和Java不太一样

1
2
3
String str6 = "this is my lover";
print(str6.split(" "));//[this, is, my, lover]
print(str6.split(" ").runtimeType);//List<String>

3.布尔

dart中定义布尔类型,使用bool

1
2
3
4
bool success = true;
bool failure = false;
print(success || failure);
print(success && failure);

4.List集合

在Dart中,list和数组是一个概念

1
2
3
4
5
创建list:var list = [1,2,3]

创建不可变lsit: var list = const[1,2,3]

构造创建:var list = new List()

使用List定义集合

1
2
3
4
List list = [1,2,3,"哈哈"];//不限制泛型,泛型就是dynamic
print(list);
List<int> list2 = [1,3,5,7,9];//限制泛型
print(list2);

注意:泛型不同,最好不要赋值,编译会报错

1
2
list = list2;//编译正常,int是dynamic的子类型
list2 = list;//编译错误,type 'List<dynamic>' is not a subtype of type 'List<int>'

集合的另一种生成方式:

1
2
List list4 = List.generate(3, (index) => index * 2);
print(list4);//[0, 2, 4]

常用操作:

length:获取长度

remove(),clear()

indexof(),lastIndexOf()

sort():排序 sublist()

shuffle():打乱 asMap():转成map forEach()

添加:add(),addAll(),insert()

1
2
3
4
List list3 = [];
list3.add("hello");
list3.add("world");
list3.addAll(list);

集合遍历:

方式一:

1
2
3
for (var value in list4) {
print("$value");
}

方式二:

1
2
3
list4.forEach((value){
print(value);
});

方式三:

1
2
3
4
for(int i=0; i<list4.length;i++){
int a = list4[i];
print("$a");
}

5.Map集合

创建map:var language = {‘first’:’Dart’,’sencond’:’Java’};

创建不可变Map:var language2 = const{‘first’:’Dart’,’sencond’:’kotlin’};

构造创建:var language3 = new Map();

定义map:

1
Map map1 = {"1":"小明","2":"小王"};

遍历map

1
2
3
map1.forEach((k,v){
print("$k -- $v");
});

常用操作:

  • [] ,length
  • isEmpty(), isNotEmpty()
  • keys() , values()
  • containsKey(),containsValue()
  • remove()
  • forEach()

6.dynamic,var,Object

Daynamic:动态数据类型,编译时不会揣测数据类型,但是运行时会推断

是这样的坏处就是会让dart的语法检查失效,所以有可能会造成混乱而不报错

所以不要直接使用dynamic

1
2
3
4
5
6
7
dynamic x = "xxx";
print(x);//xxx
print(x.runtimeType);//String

x = 1;
print(x);//1
print(x.runtimeType);//int

var:自动推断数据类型,一旦定义,不可修改数据类型

1
2
var s = "hello";
s = 1;//编译错误,不能再次修改数据类型

Object:所有dart对象的基类

区别在于:dynamic 不检查语法,如:调用不存在的方法也不会报错,运行时才报错,

Object 在编译时就会校验,如:调用不存在的方法,无法通过编译检测

1
2
3
4
5
6
7
8
9
void main(){
// var a;//无法推断出数据类型,那么类型就是dynamic,所以才能修改数据类型
// a = 10;
// a = "hello";

var a = 10;
// a = "hello";//报错,一开始自动推断出数据类型int,不能修改数据类型

}

2.运算符

算术运算符

  • 加减乘除: + , - , * , / , ~/ , %
  • 递增递减:++ , – ,

关系运算符

  • == , != , > , < , >= ,<=

注意:==是用于判断内容是否相等

逻辑运算符:

  • ! , && ,||

赋值运算符:

  • += , -= , *= , /= , ~/=
  • ??= 如果没有赋值,则赋值
1
2
3
var a;
a ??= "hello";
print("a = $a"); //a = hello

条件运算符

  • 三目运算符:同Java
  • 表达式1 ?? 表达式2 :表达式1没有值则使用表达式2的值
1
2
int gender = 1;
String str = gender == 1 ? "male" : "female";

3.语句

条件语句:

  • If
  • If else
  • If else if

循环语句:

  • for
  • while do while,break continue

switch语句同Java

4.方法

  • 方法也是对象,类型是Function

    • 方法可以作为对象赋值给其他变量

      1
      2
      3
      4
      5
      6
      ///方法可以作为参数赋值给其他变量
      printHello(){
      print("hello");
      }
      var func = printHello;
      func();//hello
    • 方法可作为参数传递给其他方法

  • 箭头语法: =>expr 是{return expr;}的缩写

1
2
3
4
5
var str = printPerson("Jack", 18);
print(str);

///定义方法,返回值类型可以省略,方法参数类型可以省略
printPerson(name,age) => "$name and $age";
  • 方法参数,可选参数可以使用{},并且可选参数必须放在最后边
1
2
3
4
5
6
7
///定义方法,两个参数都是可选参数
printPerson2({String name,int age}){
print("name = $name, age = $age");
}
///调用方法
printPerson2();//name = null, age = null
printPerson2(name: "lucy",age:18 );//name = lucy, age = 18
  • 可选参数默认值
1
2
3
4
///默认参数
printPerson3({String name = "Jack",int age = 100}){
print("name = $name, age = $age");
}

闭包

  • 闭包是一个方法
  • 闭包是定义在 其他方法内部的方法
  • 闭包会持有外部方法的局部变量(这个概念不太好理解)

面向对象

类和对象

  • 使用关键字class声明一个类
  • 使用关键字new创建一个对象,new可省略
  • 所有对象都继承Object
1.属性和方法
  • 属性默认会生成getter和setter方法
  • 使用final声明的属性只有getter方法
  • 方法不能重载,如:work() work(int n) 这样两个方法会报错
2.类和成员的可见性:
  • 使用_的类和类成员是私有的,不能被外部访问
3.计算属性

概念:计算属性的值是通过计算得来的,而不是本身存储的,举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Rectangle {
///定义两个属性
num width, height;

///定义计算属性,它的值是长和宽的乘积
num get area {
return width * height;
}
///简写形式
num get area2 => width * height;//num返回值 get表示是getter are2属性名
}

void main() {
var rect = new Rectangle();
rect.width = 20;
rect.height = 10;
print(rect.area);//200
print(rect.area2);//200
}
4.构造方法
  • 如果没有定义构造方法,则会有一个默认的构造方法
  • 如果定义了构造方法,则默认构造方法无效
  • 构造方法不能重载
1
2
3
4
5
6
7
8
9
10
11
12
13
class Person{
String name;
int age;

///类似Java的写法
// Person(String name,int age){
// this.name = name;
// this.age = age;
// }

///dart中提供的简化写法,方法体可以省略
Person(this.name, this.age){};
}

疑问:构造方法不能重载,那么如何实现多个构造方法呢?这里可以使用命名构造方法

5.命名构造方法:使用类名.方法的形式实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person{
String name;
int age;
//第一个构造方法
Person(this.name, this.age);
///命名构造方法
Person.withName(this.name);

work(){
print("$name is working");
}
}

var p = new Person.withName("lucy");
p.work();//lucy is working
6.常量构造方法
  • 如果一个类是不可变状态,可以把对象定义为编译时常量
  • 使用const声明构造方法,并且把所有变量都定义为final
1
2
3
4
5
6
7
8
9
10
11
class Person{
///使用final修饰
final String name;
final String age;
///构造方法使用const
const Person(this.name, this.age);
}

void main(){
const p = const Person("toM", "18");//const Person()的const可以省略
}
7.工厂构造方法:
  • 工厂构造方法类似于设计模式中的工厂模式
  • 在构造方法前添加关键字factory实现一个工厂构造方法
  • 在工厂构造方法中可以返回对象
8.初始化列表
  • 初始化列表会在构造方法体执行之前执行
  • 使用逗号分隔初始化表达式
  • 初始化列表常用于设置final变量的值
1
Person.withName(this.name) : gender = "male";
9.静态成员
  • 使用static关键字来实现类级别的常量和函数
  • 静态成员不能访问非静态成员,非静态成员可以访问静态成员
  • 类级别的常量需要使用static const声明
10.对象操作符:
  • 条件成员访问: ?.

    这个感觉类似kotlin中的?. 可以防止空指针报错

    1
    2
    3
    Person p;
    // p.work();//The method 'work' was called on null.
    p?.work();
  • 类型转换:as

  • 是否是指定类型:is 和 !is,并且if判断后,类型会自动转换

  • 级联操作:.. 类似于链式编程

    1
    2
    3
    4
    5
    6
    7
    ///普通赋值
    // p.name = "lucy";
    // p.age = 18;

    ///级联操作
    p..name = "lucy"
    ..age = 18;
  • 对象call方法:如果一个类实现了call方法,则该类的对象可以作为方法使用

继承

基础知识:
  • 使用关键字extends继承一个类
  • 子类会继承父类可见的属性和方法
  • 子类能够重写父类的方法,getter,setter
  • 单继承,多态

抽象类

基础知识:

  • 抽象类使用abstract关键字修饰
  • 抽象方法不需要用abstract修饰

接口:

基础知识
  • 在Dart中,类就是接口
  • 实现接口使用关键字:implements
  • 建议使用抽象类来作为接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
///定义了一个接口类
abstract class FlyAble{
///包含一个抽象方法fly
void fly()
}

///鸟实现飞的接口,可以飞
class Bird implements FlyAble{
@override
void fly() {
print("鸟可以飞");
}
}
///鸟人实现飞的接口,可以飞
class FlyMan implements FlyAble{
@override
void fly() {
print("鸟人可以飞");
}
}

void main(){
var p1 = Bird();
p1.fly();

var p2 = FlyMan();
p2.fly();
}

Mixins:

基础知识:
  • 在dart中实现多继承的方式,使用with

    1
    2
    3
    4
    ///D继承A,同时也拥有C和B的方法
    class D extends A with C,B{

    }
  • 这里的B,C就称为Mixin类

  • Mixin类不能显示声明构造方法

  • 作为Mixin的类,只能继承自Object

操作符:

基础知识:
  • 可以自定义一些操作符,举例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Person {
    int age;

    Person(this.age);

    ///操作符重载
    bool operator >(Person p) {
    return this.age > p.age;
    }
    }

    var p1 = Person(21);
    var p2 = Person(20);
    print(p1 > p2);//true

    枚举

基础知识:

  • 枚举使用enum定义
1
2
3
4
5
6
enum Season{
spring,
summer,
autumn,
winter
}
  • dart枚举有一个属性:index,从0开始
  • dart中枚举比较简单,不能定义方法

泛型:

和Java中泛型差不多的用法

编程技巧:

  • 使用?.避免空指针
  • 使用??设置默认值 如: a = str??”暂无值”

第三章 Flutter快速入门

3.1 快速入门

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
///使用import 类似Java中的导包
import 'package:flutter/material.dart';
///程序的主入口main函数,执行了runApp()方法
void main() => runApp(App());
///StatelessWidget:无状态的组件
///App是我们程序入口的第一个组件,在build方法中,返回了一个MaterialApp(材料设计)的组件
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
///theme:页面的主题
///ThemeData.dark ThemeData.light
theme: ThemeData(
///primarySwatch主色版,即设置标题栏颜色
primarySwatch: Colors.yellow
),
home: Scaffold(///Scaffold 脚手架,包含了appBar,底部导航,侧面抽屉栏等
appBar: AppBar(
title: Text('Ivan'),
elevation: 0,//设置顶部阴影
),
body: Hello(),
),
);
}
}

///这里是我们自定义的一个小组件
///在屏幕中央输出一段话:Hello World
class Hello extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Text('Hello World',
style: TextStyle(
fontSize: 40,
fontWeight: FontWeight.bold,
color: Colors.black87
),
),
);
}
}

我们再来快速实现一个列表,首先准备数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Post{
final String title;
final String author;
final String imageUrl;

const Post({
this.title,
this.author,
this.imageUrl
});
}

final List<Post> posts = [
Post(
title: 'Candy Shop',
author: 'Mohamed Chahin',
imageUrl: 'https://resources.ninghao.org/images/candy-shop.jpg',
),
Post(
title: 'Childhood in a picture',
author: 'Mohamed Chahin',
imageUrl: 'https://resources.ninghao.org/images/childhood-in-a-picture.jpg',
),
Post(
title: 'Contained',
author: 'Mohamed Chahin',
imageUrl: 'https://resources.ninghao.org/images/contained.jpg',
),
Post(
title: 'Dragon',
author: 'Mohamed Chahin',
imageUrl: 'https://resources.ninghao.org/images/dragon.jpg',
),
Post(
title: 'Free Hugs',
author: 'Mohamed Chahin',
imageUrl: 'https://resources.ninghao.org/images/free_hugs.jpg',
),
];

然后编写列表界面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import 'package:flutterdart/ninghao/model/Post.dart';
import 'package:flutter/material.dart';

///ListView的使用
///使用ListView.builder来生成每一项
///标题使用的属性:Theme.of(context).textTheme.title
///作者使用的属性:Theme.of(context).textTheme.subhead
class Home extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey[100],
appBar: AppBar(
title: Text("ListView的使用"),
),
body: ListView.builder(itemBuilder: _itemBuilder,
itemCount: posts.length,),
);
}

///生成每一个item
Widget _itemBuilder(BuildContext context,int index){
return Container(
color: Colors.white,
margin: EdgeInsets.all(8.0),
child: Column(
children: <Widget>[
Image.network(posts[index].imageUrl),
SizedBox(height: 16.0,),
Text(posts[index].title,//标题
style: Theme.of(context).textTheme.title,),
Text(posts[index].author,//作者
style: Theme.of(context).textTheme.subhead,),
SizedBox(height: 16,)
],
),
);
}
}

3.2 路由

跳转页面:

1
2
3
//跳转页面
Navigator.of(context).push(MaterialPageRoute(
builder: (BuildContext context) => Page('About')));

返回上一页:

1
Navigator.pop(context);//返回到上一页

带名字的路由:

  • 第一步:定义路由
1
2
3
4
//定义路由
routes: {
'/about': (context) => Page('About'),
},
  • 第二步:路由跳转
1
Navigator.pushNamed(context, '/about');

设置初始路由:initialRoute,注意和home不能同时出现

1
2
3
4
5
6
7
//      home: NavigatorDemo(),
initialRoute: '/',
//定义路由
routes: {
'/': (context) => NavigatorDemo(), //根路由
'/about': (context) => Page('About'),
},

3.3 输入

TextField

一:属性样式

二:监听事件

三:获取表单数据

  • 定义变量,并在触发保存的时候,存储数据

    1
    2
    3
    4
    5
    6
    String username, password;

    TextFormField(
    decoration: InputDecoration(labelText: 'Password'),
    onSaved: (value)=> password = value,//存储数据
    ),
  • 定义key,获取数据的时候,调用save()

    1
    2
    3
    4
    5
    6
    7
    8
    //定义key
    final key = GlobalKey<FormState>();
    //key设置到表单上
    Form(
    key: key,
    }
    //保存
    key.currentState.save(); //保存表单

    四:验证表单数据

  • 给TextField添加验证器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    validator: validatorPassword,

    //验证数据,如果数据通过验证,返回null
    String validatorPassword(String value) {
    if(value.isEmpty){
    return "请填写密码";
    }
    return null;
    }
  • 保存的时候,调用验证器

    1
    key.currentState.validate();//验证表单数据

    自动验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bool isAutoValidate = false;

TextFormField(
autovalidate: isAutoValidate,
),

void submitFromData() {
//验证表单数据
if (key.currentState.validate()) {
debugPrint("username = $username ,password = $password");
key.currentState.save(); //保存表单
} else {
setState(() => isAutoValidate = true);
}
}

第四章 基础控件

1.AppBar

  • Title:标题

  • Leading:标题前面的小部件,通常和抽屉栏关联,可以使用IconButton

    1
    leading: IconButton(icon: Icon(Icons.menu), onPressed: null),
  • Actions:一个widget数组,在标题栏的右边,比如我们可以放一个搜索图标:

    1
    2
    3
    actions: <Widget>[
    IconButton(icon: Icon(Icons.search), onPressed: null)
    ],

    通常使用iconButton,对于不常见的操作,使用PopupMenuButton

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    actions: <Widget>[
    IconButton(icon: Icon(Icons.search), onPressed: () {}),
    PopupMenuButton(
    onSelected: (int value){
    print(value);//获取到选中的标识
    },
    itemBuilder: (BuildContext ctx) => [
    PopupMenuItem(child: Text('A'),value: 1,), //需要使用value来作为唯一标识
    PopupMenuItem(child: Text('B'),value: 2,),
    ],
    icon: Icon(Icons.more_horiz),
    )
    ],
  • bottom:标题栏的底部,一般可以放TabBar:类似于Android中的TabLayout,必须设置控制器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    bottom: TabBar(tabs: [
    Tab(icon: Icon(Icons.local_florist),),
    Tab(icon: Icon(Icons.change_history),),
    Tab(icon: Icon(Icons.directions_bike),)
    ]),
    ///可以使用默认控制器:DefaultTabController,length必须和TabBar中的个数一致
    TabBarView:用于显示指定Tab的内容,这里我们展示到Scaffold的body里面
    body: TabBarView(children: [
    Icon(Icons.local_florist,size: 128,color: Colors.black12,),
    Icon(Icons.change_history,size: 128,color: Colors.black12,),
    Icon(Icons.directions_bike,size: 128,color: Colors.black12,),
    ]),

    2.TabBar

  • unselectedLabelColor:未选中时候标签颜色

  • indicatorColor:指示器的颜色

  • indicatorSize:指示器的样式,有两种选择:tab,label

  • indicatorWeight:指示器的高度

3.theme:主题

themeData:

  • primarySwatch标题栏的颜色
  • highlightColor按钮点击时候的高亮颜色
  • splashColor:水波纹颜色

3.drawer:属于Scaffold里面的标签

左部抽屉使用drawer,右边则使用endDrawer

4.Drawer:

一般使用listview来排版布局,

顶部可以使用DrawerHeader,下边使用ListTitle来布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
drawer: Drawer(
child: ListView(
padding: EdgeInsets.zero,//这里必须设置,顶部padding才为零,看起来正常
children: <Widget>[
DrawerHeader(
child: Text("header".toUpperCase()),
decoration: BoxDecoration(color: Colors.grey[100]),
),
ListTile(
title: Text("Message",textAlign: TextAlign.center,),
trailing: Icon(Icons.message),
),
ListTile(
title: Text("Favourite",textAlign: TextAlign.center,),
trailing: Icon(Icons.favorite),
),
ListTile(
title: Text("Settings",textAlign: TextAlign.center,),
trailing: Icon(Icons.settings),
)
],
),
),

抽屉的关闭:onTap: () => Navigator.pop(context),

使用系统提供的一个header:UserAccountsDrawerHeader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
UserAccountsDrawerHeader(
accountName: Text("Ivan"),
accountEmail: Text("loabc24@163.com"),
currentAccountPicture: CircleAvatar(
backgroundImage: NetworkImage(
"https://thirdqq.qlogo.cn/g?b=sdk&k=dcdm22zvv9c7pLsOp0LFFw&s=100&t=1556967668"),
),
decoration: BoxDecoration(
color: Colors.yellow[400],
image: DecorationImage(
fit: BoxFit.cover,
image: NetworkImage(
"https://resources.ninghao.org/images/childhood-in-a-picture.jpg"))),
),

这里使用到了BoxDecoration

BoxDecoration:一个背景装饰对象,相当于Android中的shape.xml,定制各种各样的背景(边框、圆角、阴影、形状、渐变、背景图像)

5.基础控件:

1.Text

  • textAlign 对齐方式,如:textAlign: TextAlign.center,
  • style 设置样式,属性比较多,就不细说,举例:TextStyle(fontSize: 16);
  • maxLines 最大行数
  • overflow: 超过最大行数如何处理:举例:overflow: TextOverflow.ellipsis(省略号)

RichText:富文本

RichText可以设置多种样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
RichText(text: TextSpan(
text: "我爱中国",
style: TextStyle(
color: Colors.deepPurpleAccent,
fontSize: 34,
fontStyle: FontStyle.italic,
fontWeight: FontWeight.w100
),
children: [
TextSpan(
text: 'I love China',
style: TextStyle(
fontSize: 26,
color: Colors.redAccent
)
)
]
));

RichText的children定义子类,默认继承父类的样式,在子类中再定义自己的样式,这样就能在一行里组合成富文本

2.Container

属性:

  • Child 内容
  • Color:背景色
  • Padding:内边距
  • Margin:外边距
  • Width:宽度 height:高度
  • Decoration:装饰,如可以使用boxdecoration,如果Decoration加了背景色,color属性就要去掉

3.Row 行

属性:

  • mainAxisAlignment:主轴对齐方式 如:mainAxisAlignment: MainAxisAlignment.center,
    • 行的主轴可以理解为水平线
  • crossAxisAlignment:交叉轴,和主轴对称

4.BoxDecoration:盒子装饰

  • color 颜色

  • shape: 盒子的形状,如:BoxShape.circle,如果是原型,则不能再使用borderRadius

  • gradient:渐变,可设置两种渐变

    • RadialGradient
    • LinearGradient:线性渐变
  • Image:设置盒子的背景图片

  • Border:边框 使用Border这个widget,可设置上下左右的边框,边框使用BorderSide

    Tip:可以使用Border.all一次性设置四条边框

  • borderRadius: 设置圆角

    • 同时设置4个圆角:BorderRadius.circular(8),
    • 分别设置圆角:borderRadius: BorderRadius.only()
  • boxShadow:盒子阴影

    • offset:偏移量
    • color:阴影颜色
    • blurRadius:阴影半径
    • spreadRadius:阴影扩散半径
    1
    2
    3
    4
    5
    6
    7
    8
    boxShadow: [
    BoxShadow(
    offset: Offset(0, 6.0),//阴影偏移量
    color: Color.fromRGBO(3, 54, 255, 1.0),//阴影颜色
    blurRadius: 8.0,//阴影半径
    spreadRadius: 1.0//扩散半径
    )
    ],

    5.SizedBox

具有指定大小的盒子,指定大小后,可限制子widget,如果不指定,则自适应子控件

SizedBox.expand() :创建一个父类允许最大的盒子

6.Alignment

矩形内的一个点,Alignment(x,y),其中x,y的取值范围是-1.0 - 1.0 ,表示矩形的一端到另一端

  • Alignment(0.0, 0.0)代表举行的中心
  • Alignment(-1.0, -1.0)表示左上角
  • Alignment(1.0, 1.0):表示右下角

可以使用内置的一些属性,方便定位,如:Alignment.center,Alignment.topLeft

7.stack

类似于Android布局中的帧布局Framelayout

stack可以指定alignment属性

8.Positioned

控制[Stack]的子元素放置位置的小部件,举例:

1
2
3
4
5
6
///在左上角(10,10)位置
Positioned(
child: Icon(Icons.shopping_cart),
left: 10,
top: 10,
)

9.AspectRatio

一个小部件,可以限制子widget的宽高比,宽度由父控件决定,举例:

1
2
3
4
5
6
7
8
9
Container(
width: 100,//宽度为100
child: AspectRatio(
aspectRatio: 16/9, //宽高比16:9
child: Container(
color: Colors.red,
),
),
)

10.ConstrainedBox

一个小部件,可以限制子控件的最大宽高,最小宽高,举例:

1
2
3
4
5
6
7
8
ConstrainedBox(
child: Container(
color: Colors.red,
),
constraints: BoxConstraints(
minHeight: 200,
maxWidth: 200,
))

11.PageView

类似于安卓中的ViewPager

PageView.builder:按需构造页面

常用属性:

  • scrollDirection:滑动方向

    • Axis.horizontal 水平方向,默认是水平方向

    • Axis.vertical:垂直方向

  • onPageChanged:滑动监听

  • viewportFraction:页面比例分数

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
PageView(
children: <Widget>[
_pageView_item(Colors.brown[900],'One'),
_pageView_item(Colors.grey[900],'Two'),
_pageView_item(Colors.blueGrey[900],'Three'),
],
);

///生成PageView的每一页
Widget _pageView_item(Color color,String text){
return Container(
color: color,
alignment: Alignment.center,
child: Text(text,style: TextStyle(fontSize: 32,color: Colors.white),),
);
}

12.GridView

使用GridView.count来生成

  • crossAxisSpacing:交叉轴间距
  • mainAxisSpacing:主轴间距
  • crossAxisCount:交叉轴上item的个数
  • scrollDirection:滚动方向

13.CustomScrollView

说明:类似于Android中的scrollview

参考:https://book.flutterchina.club/chapter6/custom_scrollview.html

SliverAppBar 类似于安卓中的CollapsingToolbarLayout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const SliverAppBar({
Key key,
this.leading,//左侧的图标或文字,多为返回箭头
this.automaticallyImplyLeading = true,//没有leading为true的时候,默认返回箭头,没有leading且为false,则显示title
this.title,//标题
this.actions,//标题右侧的操作
this.flexibleSpace,//可以理解为SliverAppBar的背景内容区
this.bottom,//SliverAppBar的底部区
this.elevation,//阴影
this.forceElevated = false,//是否显示阴影
this.backgroundColor,//背景颜色
this.brightness,//状态栏主题,默认Brightness.dark,可选参数light
this.iconTheme,//SliverAppBar图标主题
this.actionsIconTheme,//action图标主题
this.textTheme,//文字主题
this.primary = true,//是否显示在状态栏的下面,false就会占领状态栏的高度
this.centerTitle,//标题是否居中显示
this.titleSpacing = NavigationToolbar.kMiddleSpacing,//标题横向间距
this.expandedHeight,//合并的高度,默认是状态栏的高度加AppBar的高度
this.floating = false,//滑动时是否悬浮
this.pinned = false,//标题栏是否固定
this.snap = false,//配合floating使用
})

14.SingleChildScrollView

类似于Android中的ScrollView,主要用于一屏展示不下,需要滑动展示,如果是很长的列表,则应该使用如ListView,因为可以延迟加载,SingleChildScrollView是直接加载所有内容

15.OverflowBox

6.按钮

1.FloatingActionButton

构建方式:

  • 方式一:默认构造

  • 方式二:使用FloatingActionButton.extended构造

1
2
FloatingActionButton.extended(
onPressed: null, label: Text('add'), icon: Icon(Icons.add));

2.FlatButton

构建方式

  • 方式一:默认构建
  • 方式二:使用FlatButton.icon()构建,可以添加一个小图标

3.RaisedButton

和FlatButton的区别是,RaisedButton有一个背景和溅墨效果

构建方式同上

1
shape: StadiumBorder(),//两端半圆矩形

4.OutlineButton

描边按钮

1
2
3
4
5
6
7
8
9
OutlineButton(
onPressed: () {},
child: Text('RaisedButton'),
splashColor: Colors.grey,
borderSide: BorderSide(
color: Colors.black87,
),
shape: StadiumBorder(),
),

5.Expanded

展开,将剩余可用空间填满,如放在Row中,会填满可用空间,可设施flex,设置比例

6.PopupMenuButton

PopupMenuItem每个item项

弹出式按钮,每个item

onSelected:回调每个点击的选项,需要给每个item设置value

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
PopupMenuButton(
itemBuilder: (context) => [
PopupMenuItem(
child: Text('Home'),
value: 'Home',
),
PopupMenuItem(
child: Text('Search'),
value: 'Search',
),
PopupMenuItem(
child: Text('me'),
value: 'me',
)
],
onSelected: (value) {
setState(() => text = value);
},
)

7.checkbox

常用属性:

  • activeColor:选中时候的颜色

  • Value:当前是否选中,会在onChange()方法中回调出来,需要定义一个变量,去动态变化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var _isCheckedA = false;

    Checkbox(
    activeColor: Colors.blue,
    value: _isCheckedA,
    onChanged: (value) {
    setState(() {
    _isCheckedA = value;
    });
    })

    8.Radio

单选框

属性:

1
2
3
4
5
6
const Radio({
Key key,
@required this.value,//单选按钮表示的值
@required this.groupValue,//一组单选按钮的当前选定值,如果和value相同,则表示选中
@required this.onChanged,//变化回调

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var _groupValue = 1;

void _handleValueChange(int value) {
setState(() {
_groupValue = value;
});
}

Radio(
value: 1,
groupValue: _groupValue,
onChanged: _handleValueChange),
Radio(
value: 2,
groupValue: _groupValue,
onChanged: _handleValueChange),

9.RadioListTile

类似于Radio,但是效果更多,可以设置标题,子标题,icon

10.Switch

开关,使用比较简单

属性:

Value:布尔值

onChanged:

举例:

1
2
3
4
5
6
7
8
9
var isSwitch = false;

Switch(
value: isSwitch,
onChanged: (value) {
setState(() {
isSwitch = value;
});
})

11.Slider

滑动选择器:

1
2
3
4
5
6
7
8
9
10
11
12
13
Slider(
min: 0.0,//最小值
max: 10.0,//最大值
divisions: 10,//等分成多少份,需要和label一起使用
label: '${_sliderValue.toInt()}',//显示每份
activeColor: Theme.of(context).accentColor, //选中部分的颜色
inactiveColor:Theme.of(context).accentColor.withOpacity(0.3) ,//未选中部分颜色
value: _sliderValue,//当前的值double类型
onChanged: (value) {
setState(() {
_sliderValue = value;
});
}),

12.SimpleDialog

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void _openSimpleDialog() async {
final option = await showDialog(context: context, builder: (BuildContext context) {
return SimpleDialog(
title: Text('温馨提示'),
children: <Widget>[
SimpleDialogOption(
child: Text('确定'),
onPressed: ()=>Navigator.of(context).pop(Option.A),
),
SimpleDialogOption(
child: Text('退出'),
onPressed: ()=>Navigator.of(context).pop(Option.B),
)
],
);
});

13.AlertDialog

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void _openAlertDialog() async{
var result = await showDialog(context: context,builder: (BuildContext context){
return AlertDialog(
title: Text('warm tips'),
content: Text('Are you sure about this?'),
actions: <Widget>[
FlatButton(onPressed: ()=>Navigator.pop(context,Option.B), child: Text('Cancel',style: TextStyle(color: Colors.indigoAccent),)),
FlatButton(onPressed: ()=>Navigator.pop(context,Option.A), child: Text('Ok',style: TextStyle(color: Colors.indigoAccent)))
],
);
});

setState(() {
switch(result){
case Option.A:
_selectChoice = "ok";
break;
case Option.B:
_selectChoice = "cancel";
break;
}
});
}

14.BottomSheet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
final _key = GlobalKey<ScaffoldState>(); //key需要赋值给Scaffold的key

///打开BottomSheet
void _openBottomSheet() {
_key.currentState.showBottomSheet((BuildContext context){
return BottomAppBar(
child: Container(
color: Colors.red.withOpacity(0.2),
padding: EdgeInsets.all(16),
child: Row(
children: <Widget>[
Icon(Icons.pause_circle_outline),
SizedBox(width: 16,),
Text('01:30 / 03:30'),
Expanded(child: Text('Fix you - Coldplay',textAlign: TextAlign.right,))
],
),
),
);
});
}

15ModalBottomSheet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void _openModalBottomSheet() {
showModalBottomSheet(context: context, builder: (context){
return Container(
height: 200,
child: Column(
children: <Widget>[
ListTile(title: Text('Opton A'),),//处理点击事件onTap()
ListTile(title: Text('Opton B'),),
ListTile(title: Text('Opton C'),),
],
),
);
});
}

16.SnackBar

1
2
Scaffold.of(context).showSnackBar(SnackBar(content: Text('正在处理中...'),
action: SnackBarAction(label: 'Ok', onPressed: () {}),));

17ExpansionPanel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
bool _isExpanded = false;

ExpansionPanelList(
expansionCallback: (int panelIndex, bool isExpanded){
setState(() {
_isExpanded = !isExpanded;
});
},
children: [
ExpansionPanel(
isExpanded: _isExpanded,
headerBuilder: (BuildContext context, bool isExpanded) {
return Container(
padding: EdgeInsets.all(16.0),
child: Text(
'Panel A',
style: Theme.of(context).textTheme.title,
),
);
},
body: Container(
width: double.infinity,
child: Text('content for panel A'),
padding: EdgeInsets.all(16.0),
))
],
)

18Chip

属性:

  • deleteIconColor 删除图标颜色
  • deleteIcon 删除图标,设置onDelete,默认带删除图标,可不设置
  • onDeleted:删除动作
  • deleteButtonTooltipMessage:长按删除图标提示信息
1
2
3
4
5
6
7
8
9
10
11
Chip(label: Text('Life')),
Chip(label: Text('Sunset'),backgroundColor: Colors.orange,),//设置背景色
Chip(label: Text('Sunset'),avatar: CircleAvatar(backgroundColor: Colors.grey,child: Text('帆'),),),//设置文字头像

//设置网络图片
Chip(label: Text('Ivan'),avatar: CircleAvatar(backgroundImage: NetworkImage('https://thirdqq.qlogo.cn/g?b=sdk&k=dcdm22zvv9c7pLsOp0LFFw&s=100&t=1556967668'),),),

//设置删除图标
Chip(label: Text('删除'),onDeleted: (){},)//带删除图标
Chip(label: Text('删除'),onDeleted: (){},deleteIcon: Icon(Icons.delete_forever),),//设置删除图标

actionChip:动作碎片,类似Chip

FilterChip:过滤碎片

1
2
3
FilterChip(label: Text('过滤碎片'), onSelected: (value){setState(() {
_filterSelected = value;
});},selected: _filterSelected,)

ChoiceChip:

属性:

  • label 文本
  • selectedColor 选中的颜色
  • selected:设置是否选中
  • onSelected:选中回调
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
List<String> _tags = [
'Apple',
'Banana',
'Lemon',
];

String _choice = 'Apple';

Container(
width: double.infinity,
child: Text('ChoiceChip:$_choice'),
),
Wrap(
// mainAxisAlignment: MainAxisAlignment.spaceEvenly,
spacing: 8.0,//水平间隔
runSpacing: 8.0,//垂直间隔
children: _tags.map((tag){
return ChoiceChip(label: Text(tag), selectedColor:Colors.black,selected: _choice == tag,onSelected: (value){
setState(() {
_choice = tag;
});
},);
}).toList()
),

19.Wrap

自适应布局,就类似于流式布局那样

属性:

1
2
spacing: 8.0,//水平间隔
runSpacing: 8.0,//垂直间隔

20.Divider

分隔符

1
Divider(color: Colors.grey,)

21.DataTable

columns:定义列

DataColumn:每列的名称

rows:定义行

sortColumnIndex:排序索引位置

sortAscending:布尔值,升序还是降序

selected:选中状态

onSelectChanged:选中回调

DataRow:表示一行数据,每个单元格DataCell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
DataTable(columns: [
DataColumn(label: Text('国家')),//定义了两列数据
DataColumn(label: Text('首都')),
], rows: [
DataRow(cells: [
DataCell(Text('中国')),
DataCell(Text('北京')),
]),
DataRow(cells: [
DataCell(Text('美国')),
DataCell(Text('华盛顿')),
]),
DataRow(cells: [
DataCell(Text('俄罗斯')),
DataCell(Text('莫斯科')),
])
])

22.Card

卡片布局

  1. ClipRRect

裁剪:可以裁剪子部件的圆角等

23.Stepper

流程

主要使用两个类,Stepper,Step,每一个Step就是流程中的节点

currentStep:当前流程的位置,从零开始

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
child: Stepper(
currentStep: _currentStep, //当前打开哪个步骤
onStepTapped: (step) {
setState(() {
_currentStep = step;
});
},
steps: [
Step(
title: Text('Register'),
isActive: _currentStep == 0, //是否激活
subtitle: Text('Register First'),
content: Text(
'When toasting sliced zucchinis, be sure they are room temperature.')),
Step(
title: Text('Login'),
isActive: _currentStep == 1,
subtitle: Text('Login First'),
content: Text(
'Simmer iced eggs in a jar with whiskey for about an hour to enhance their consistency.')),
Step(
title: Text('Share'),
isActive: _currentStep == 2,
subtitle: Text('Share data'),
content: Text(
'Combine mackerel, blood oranges and peanut butter. mash up with shredded butterscotch and serve flattened with melon. Enjoy!')),
]),

7.widget

7.1 AnimatedCrossFade

作用:该组件让2个组件在切换时出现交叉渐入的效果

1
2
3
4
5
AnimatedCrossFade(
firstChild: xxx1,
secondChild: xxx2,
crossFadeState: _crossFadeState,
duration: xxx;

crossFadeState有两个状态,CrossFadeState.showSecond和CrossFadeState.showFirst

实战:在小说或者电影APP中,会有介绍,一般会有多行文字,默认展示两行,点击图标,会展示全部文字,这个时候,我们可以使用 AnimatedCrossFade来切换显示两行文字,或者多行文字,当然你也可以有其他的实现方式

1
2
3
4
5
return AnimatedCrossFade(
firstChild: _twoLineText(widget.content),
secondChild: Text(widget.content),
crossFadeState: _crossFadeState,
duration: Duration(milliseconds: 500));

7.2 transitions

RotationTransition

作用:实现动画旋转

1
2
3
4
5
6
RotationTransition({
Key key,
@required Animation<double> turns, //动画
this.alignment = Alignment.center,
this.child,
})

7.3 ValueListenableBuilder

作用:局部刷新

1
2
3
4
ValueListenableBuilder(
valueListenable: _notifier,
builder: (context, value, child){},
child: xxx,)

我们需要提供一个valueListenable:类型是 ValueNotifier

1
ValueNotifier _notifier = ValueNotifier<int>(0); //参数就是需要监听的值,这里是Int类型
1
当我们需要刷新的时候,就可以去改变ValueNotifier的value值,就会重新构建builder,而其他不变的内容,我们可以放在child中,每次重新构建的的时候,会回调给builder(builder的第三个参数child)

实战:

第五章 实战开发

5.1 打包发布

https://flutter.dev/docs/deployment/android

第一步:生成签名文件

第二步:配置key.properties

key.properties需要放到android目录下,同时添加到git忽略文件

1
2
3
4
storePassword=tingche2021
keyPassword=tingche2021
keyAlias=key
storeFile=/Users/yons/Ivan/jks/key.jks

忽略文件

1
2
# Android ignore
/android/key.properties

第三步:配置build.gradle

首页以下代码配置到android{}之前

1
2
3
4
5
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}

然后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
}
}

buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.release
}
}

第四步:发布前,配置网络权限,否则release包无法访问网络

1
2
3
4
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />

第五步:打包

1
flutter build apk --split-per-abi

5.2 冷启动优化

针对IOS和Android需要各自做冷启动优化,主要是在启动页的主题,加上过渡图片

5.3 获取屏幕宽度

使用 MediaQuery.of(context).size.width

5.4 数据加解密

使用了几个rsa加密的库,都没能加密解密,最后换成Java端去做数据加密

总结一下,数据加密方案:

  • flutter端数据加密,使用flutter端的插件来做秘钥的生成和加解密,如:rsa_util 0.1.1
  • Java端数据加密,使用methodChannel和Java端通信

以下是flutter端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class RsaPlugin {
static MethodChannel _methodChannel = MethodChannel("rsa_plugin");

///RSA加密
static Future<String> encrypt(String str) async {
return _methodChannel.invokeMethod('encrypt', str);
}

///RSA加密
static Future<String> decode(String str) async {
return _methodChannel.invokeMethod('decode', str);
}

///AES解密
static Future<String> decodeAES(String str, String aesIv) async {
return _methodChannel
.invokeMethod('decodeAes', {'data': str, 'aesIv': aesIv});
}
}

注意:如果需要传递多个参数,使用map的方式,在安卓端如何接收参数呢?

  • 一个参数:call.arguments as String
  • 两个参数:call.argument(“data”), httpKey, call.argument(“aesIv”)

以下是Java端代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class RsaPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {


private lateinit var methodChannel: MethodChannel


override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
ENCRYPT -> {
val data = RSAUtil.encrypt(call.arguments as String, RSAKey.PRIVATE_KEY_ANDROID_STR)
result.success(data)
}
DECODE -> {
val data = RSAUtil.decrypt(call.arguments as String, RSAKey.PUBLIC_KEY_PHP_STR)
result.success(data)
}
AES_DECODE -> {

val data = AESUtil.decrypt(call.argument<String>("data"), httpKey, call.argument<String>("aesIv"))
result.success(data)
}
}
}

override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
methodChannel = MethodChannel(binding.binaryMessenger, TAG)
methodChannel.setMethodCallHandler(this)
}

override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
methodChannel.setMethodCallHandler(null)
}
}

最后别忘记在FlutterActivity中注册插件

override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
//rsa加解密
flutterEngine.plugins.add(RsaPlugin())
}

5.5 单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Dart单例模式
class SingleTon {
static SingleTon _instance;

static SingleTon getInstance() {
if (_instance == null) {
_instance = new SingleTon();
}
return _instance;
}
}
//测试Dart单例模式
main(List<String> args) {
print(SingleTon.getInstance().hashCode);
print(SingleTon.getInstance().hashCode);
}

5.6 Toast

使用插件 fluttertoast

引入插件:

1
fluttertoast: ^4.0.0

代码封装:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
///toast工具类
class ToastUtil {
///弹出提示
static void showTip(String msg) {
Fluttertoast.showToast(
msg: msg,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM);
}

static void showError(String msg) {
Fluttertoast.showToast(
msg: msg,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.red,
textColor: Colors.white);
}
}

使用插件

1
ToastUtil.showTip(DString.exit_tip);

5.7 连按两次返回

使用WillPopScope这个widget

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//定义上次点击时间
DateTime _lastTime;

//使用
body: WillPopScope(child: null, onWillPop: _onWillPop)

///连按两次退出APP
Future<bool> _onWillPop() async {
if(_lastTime == null || DateTime.now().difference(_lastTime) > Duration(seconds: 2)){
_lastTime = DateTime.now();
ToastUtil.showTip(DString.exit_tip);
return false;
}else {
SystemNavigator.pop();
return true;
}
}

5.8图片处理

模糊处理 BackdropFilter和ImageFilter

使用BackdropFilter和ImageFilter可以将图片模糊处理

具体分为模糊背景和模糊前景,别搞错了,一般我们用模糊前景比较多,用一个Stack来做相对布局即可

模糊前景:

1
2
3
4
5
6
7
8
9
10
11
12
13
return Stack(
children: <Widget>[
_contentBackgroundImage(context), //这里是一张图片,需要模糊它,所以用stack
BackdropFilter(
filter: ImageFilter.blur(sigmaX: 100, sigmaY: 100),
child: Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height / 2,
color: AppColor.black.withOpacity(0.3),
),
)
],
);

https://juejin.cn/post/6844903850449584142

5.9回调

flutter中 无参数回调用voidcallback,一个参数回调用valuechanged,多个参数回调用typedef

看一下VoidCallback、ValueChanged 的源码可以知道

1
2
typedef VoidCallback = void Function()
typedef ValueSetter<T> = void Function(T value)

本质上VoidCallback、ValueChanged 就是typedef的一种写法而已,只是Flutter帮我们做了一层封装

5.10 GlobalKey

GlobalKey的文章太多了,不想看,这里只想介绍它的一个作用,局部刷新

https://www.wanjunshijie.com/note/111.html

5.11 JSON转Dart

网址:https://javiercbk.github.io/json_to_dart/

第六章 开源组件

6.1 旋转切换

toggle_rotate

该组件的封装思路:张风捷特烈:https://www.imooc.com/article/301631

第七章 Flutter杂七杂八

5.1 常用命令

命令 说明
flutter create 创建项目,如:flutter create -i swift -a kotlin xxapp
flutter pub deps 依赖关系查看

5.2 几个小技巧

赋值运算符??=

a ??= b :表示如果a为null的时候 将b赋值给a

5.3 Flutter 异步编程知识

Dart单线程模型

1
2
3
4
5
6
/// Dart如何处理IO密集型任务和CPU密集型任务
/// io密集型任务:使用 async/await,Future
/// CPU密集型任务:使用isolate,类似多进程
/// Dart单线程模型:一个消息循环机制(Event Loop),两个消息队列:微任务队列(microTask queue)和事件队列(Event Queue)
/// 在Dart的单线程模型中,入口main函数执行后,消息循环机制就开始了,首先执行微任务队列(microTask),然后执行事件队列(Event Queue),
/// 小知识插播:Future.microtask(…)方法,该方法可以向微任务队列插入一个任务

isolatecompute

1
2
3
4
5
6
///知识点:Json解析(CPU密集型任务)可能比较耗时,所以这里使用compute,什么是compute? compute是flutter中对Isolate的封装
///那什么是Isolate呢?Isolate翻译为隔离,可以理解为开启了新的线程,不阻塞UI线程
///Isolate比较重量级,因为在Isolate和UI线程之间传输数据,比较复杂,代码量多
///compute使用须知:
/// 1. compute中运行的函数,必须是顶级函数或者是static函数
/// 2. 二是compute传参,只能传递一个参数,返回值也只有一个

5.4 Flutter生命周期

第八章 常见问题

7.1 创建项目一直卡在creating Flutter Project

发现文件目录已经创建,关闭AS,重新打开AS,打开项目,编译,此时会发现存在什么问题

7.2 Flutter App Run卡在Running Gradle task ‘assembleDebug’…

用AS打开Android部分代码,编译,此时就会看到是什么问题,一般来说,我们需要配置gradle版本,kotlin版本,gradle插件版本,另外还有一些必要的第三方依赖,以上这些,下载都可能比较耗时,需要梯子、解决办法:添加阿里云仓库

7.3 android studio导入flutter项目报错:Dart SDK is not configured

1
2
3
4
5
在Android studio中导入flutter项目时报错:Dart SDK is not configured,这是因为在android studio里面没有配置Dart SDK的问题,可以通过下面步骤进行配置:

##### 1.打开File =》Setting =》Language & Frameworks => Dart

##### 2.勾选 “enable Dart support for the project”,并且选择Dart SDK path,路径为 D:\install\android\flutter\bin\cache\dart-sdk ,其中D:\install\android\flutter是flutter SDK路径

7.4 解决flutter项目在AndroidStudio4.0的logcat中出现Please configure Android SDK

https://blog.csdn.net/w815878564/article/details/106802471

解决办法:先打开一个传统的as项目,然后再新窗口打开flutter项目

7.5 报错:Waiting for another flutter command to release the startup lock…

1
2
3
4
关闭Android Studio
打开flutter安装目录/bin/cache
删除lockfile文件
此时可在命令行再执行flutter相关命令,完美解决

7.6 打开flutter项目,工具栏选择设备栏一直处于loading状态,但是flutter devices又能发现连接了设备

1
2
1. 关闭AS,删除bin/cache下的lockfile
2. 执行flutter devices,有设备后,打开as即可