纯是水字数的,要好好学的同学还是看官网的好,哈哈~~
虽然Dart是基础,但是先跟着Flutter官网做一个"第一个Flutter App"也不耽误,有些人是了再回头专门学Dart还更省时间。
变量
在类型安全的Dart代码中,要声明一个变量也只需要用var。有了类型推断,这些变量的类型会通过初始值确定下来。
dart
var name = "hello, world!";
const
可以使用var声明一个变量。也可以使用const来声明一个常量。
const
声明的常量在Flutter的widget里面更是常用。这里先简单介绍一下,后面有详细的介绍。这样的常量是编译期常量值。也就是在编译的时候就是一个常量了。这个编译期的常量怎么理解,看这个例子:
dart
const a = 5;
const b = DateTime.now(); // 报错:Const variables must be initialized with a constant value.
表达式a = 5
好说,编译的时候就知道这个值,而且不会改变。对于DateTime.now()这个表达式是无法编译通过的。因为b的值只有在运行的时候执行了DateTime.now()
才知道值是什么。
final
这个就可以弥补const
的不足。比如:
dart
final someDate = DateTime.now(); // OK
final
修饰的常量就是在这个常量初始化之后就不可变更的。
Function
一个简单的例子:
dart
int add(int a, int b) {
return a + b;
}
还可以更加简化:
dart
int add(int a, int b) => a + b;
传递参数
最简单的:
dart
void debugger(String message, int lineNum) {
print("$message, in line $lineNum");
}
// 调用一把
debugger("A bug", 55);
使用命名参数:
dart
void debugger({String? message, int? lineNum}) { print("$message, in line $lineNum");}
debugger(message: "A bug", lineNum: 5);
注意 :如果没有使用required
修饰,那么那个参数就需要定义成nullab的类型。或者,给那个参数一个默认值。
还可以定义可选位置参数,使用中括号括起来,如:
dart
void debugger(String message, int? lineNum, [String device = ""]) {
print("$message, line number: $lineNum");
}
类
在"第一个Flutter App"我们已经见识到了继承了StatelessWidget和StatefulWidget的类了。Dart的类(Null
除外)都继承了Object
类。
构造函数
定义类最关键是需要一个构造函数。一般定义的方式是用ClassName或者ClassName.identifier的方式。比如:
dart
class TodoItem {
final int id;
const TodoItem({
required this.id,
});
factory TodoItem.fromJson(Map<String, dynamic> json) {
// return TodoItem(/*... */);
}
}
final todoItem = TodoItem({id: 1});
final p = Map<String, dynamic>{"id": 1};final todoItem = TodoItem.fromJson(p);
私有成员
在Dart里,私有成员以"_"开头。
注意:这里的私有成员和常遇到的私有不是一回事。在Dart的类里的私有成员不是指类的外部不能访问这个私有成员,而是指在同一个包(Library)里可以访问,其他的包不能访问。一个Flutter的app就是一个包,虽然从来没有这么称呼过一个Flutter app。
const, final成员变量
定义类的实例常量:
dart
class A {
const a = 5; // 报错,only static fields can be declared const
}
只有静态成员可以用const
修饰!要么换成final
,要么在字段前面加static
.
只能这样用:
dart
Row(
children: [
const Image(image: NetworkImage('https://image-address')),
const Text("Image description"),
]
)
对于final
来说就没有那么多限制了。这样可以:
dart
class A {
final a = 5;
final b = DateTime.now();
}
这样也可以:
dart
class A {
static final a = 5;
static final b = DateTime.now();
}
但是,final
修饰的成员变量只能被赋值一次。这一点还是没有变。其他特殊情况且听下文分解。
late成员
late
这个关键字会经常遇到。
当你有了一个成员变量了,但是要稍微晚一点再初始化就会用导致换个关键字了。如:
dart
class CounterButton extends StatelessWidget {
String someText; // 1
CounterButton({super.key}) { //报错:Non-nullable instance field 'someText' must be initialized.
someText = "|"; // 2
}
@override
Widget build(BuildContext context) {
final counterProvider = CounterProvider.of(context);
return Text("${someText ?? "|"}"), // *
// 略
],
);
}
}
在【1】定义了一个成员变量someText
,这个变量不是可空的。在构造函数那里就会报错了。你打算在构造函数里初始化这个变量的。但是不行,报错。
那就只好late
出场了。
dart
class CounterButton extends StatelessWidget {
late String someText; // 这里加上late关键字
CounterButton({super.key}) {
someText = "|"; // 在构造函数中初始化
}
}
还有一个关于late
很有趣的例子:
dart
class AnyWidget extends StatelessWidget {
late String aString;
AnyWidget({super.key}){
print("constructor started");
aString = getAStringSomewhere(); // *
print("constructor finished");
}
String getAStringSomewhere(){
print("we're generating a string now");
return "Hello World";
}
@override
Widget build(BuildContext context) {
print("build");
return Text(aString);
}
}
来看打印的结果: constructor started we're generating a string now constructor finished build
在构造函数中使用了方法getAStringSomewhere()
给变量aString
赋值,所以在构造函数执行的过程中打印出了对应的字符串。再看一个例子:
dart
class AnyWidget extends StatelessWidget {
late String aString = getAStringSomewhere();
AnyWidget({super.key}) {
print("constructor finished");
}
String getAStringSomewhere() {
print("we're generating a string now");
return "Hello World";
}
@override
Widget build(BuildContext context) {
print("build");
return Text(aString); // *
}
}
在build
方法中使用了成员aString
, 这个时候调用了方法getAStringSomewhere()
,所以对应的字符串是在build
执行的过程打印出来的。
由此可以得出结论,late
修饰的变量在使用的时候才去调用初始化它的方法。
Mixin
定义一个mixin
现在我们有一只鸟,但是还要让它会写博客,会打游戏。
一只鸟:
dart
class Bird {
void fly() {
print("A bird is flying");
}
void chirp() {
print("A bird is chirping");
}
}
定义一个写博客的mixin
。
dart
mixin Writer {
void writ({required String text}) {
print("Writing something $text");
}
}
再来定义一个打游戏的mixin。
dart
mixin Game {
void play({required String gameName}) {
print("Play a game named $gameName");
}
}
然后把这两个功能作用到鸟身上:
dart
class SpecialBird extends Bird with Writer, Game {}
Dart是不支持多继承的,所以使用mixin可以很好的弥补这个缺陷。
使用on关键字
在定义mixin的时候使用on
可以让你的mixin只作用在特定的继承体系下。比如,要限制写博客只能是鸟,那么可以这样:
dart
mixin Writer on Bird {
void writ({required String text}) {
print("Writing something $text");
}
}
这时,你想让老虎也来写博客就不好使了:
dart
class Tiger with Writer {} // 报错
扩展类型
扩展类型就是用来给一个已经存在的类型增加方法的,而且使用扩展类型还不需要修改已经存在的类型。扩展类型不必和已有类型定义在一起,通常也不定义在一起。毕竟,有些已经存在的方法是Dart、Flutter内置的或者更多是第三方库的没法直接修改。
定义扩展类型
dart
extension ExtensionName on <Type> {
// 各种方法
}
ExtensionName
你的扩展的名字,Type
就是你要扩展的类型的名字了。 来看一个例子:
dart
extension MyExtension on String {
int get lengthPlus => this.length + 1;
String capitalize() {
return "${this[0].toUpperCase()}${this.substring(1)}";
}
}
上例中,定义了一个叫做MyExtension
的扩展。它扩展了String
类型。给String类型增加了一个capitalize
方法。
重名了怎么办
类型扩展的多了,难免会遇到重名的情况。
类型的扩展定义的多了难免出现的就是出现方法同名的情况。比如,string_apis.dart 和string_dart2.dart 都定义了一个String
的扩展叫做NumberParsing
,里面都提供了一个方法parseInt
。在调用的时候会出现错误,如:
dart
import 'package:example_app/extensions/string_apis.dart';
import 'package:example_app/extensions/string_apis2.dart';
// 略
class MyButton extends StatelessWidget {
const MyButton({super.key});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
print('${"100".parseInt()}'); // 报错
},
child: const Text('Press me'),
);
}
}
现在来处理一下这个问题:
- 使用
hide
关键字隐藏掉一个扩展,如:
dart
import 'package:flutter_todo_wasm/extensions/string_apis.dart';
import 'package:flutter_todo_wasm/extensions/string_apis2.dart' hide NumberParsing;
- 如果定义的两个字符串的扩展名字不同的话,比如一个是
NumberParsing
和NumberParsing2
。那么可以直接使用扩展名称来调用parseInt方法来避免同名冲突。
dart
import 'package:flutter_todo_wasm/extensions/string_apis.dart';
import 'package:flutter_todo_wasm/extensions/string_apis2.dart';
// 略
onPressed: () {
print('${NumberParsing("100").parseInt()}');
print('${NumberParsing2("100").parseInt()}');
},
- 给其中的一个字符串的扩展起一个别名。
dart
import 'package:flutter_todo_wasm/extensions/string_apis.dart';
import 'package:flutter_todo_wasm/extensions/string_apis2.dart' as rad;
// 略
onPressed: () {
print('${NumberParsing("100").parseInt()}');
print('${rad.NumberParsing2("100").parseInt()}');
},
异步编程
异步编程的重要性就不多说了,懂得都懂。我们来看看Flutter有哪些异步编程的知识。
Future
Flutter的异步编程是基于Future这个概念的。一个Future代表了一个不会立刻出现,在不远的未来才会出现的值或者错误。比如一个网络请求:
dart
Future fetchTodoList({}) {
return Future.delayed(Duration(seconds: 2), () => '从你的后端获取的数据');
}
async await
使用Future看起来解决了一个返回一个将来才能拿到值的问题。但是,Future有一个不大不小的问题,就是如果有多个Future有依赖关系的时候会有嵌套Future,会有很多的缩进,最后那些代码变得难以阅读。就像callback hell一样。
所以,dart引入了async-await。这样多个有依赖关系的Future看起来就像是一段顺序执行的代码:
dart
Future<void> fetchDataAndPrint() async {
var data = await fetchData();
print(data);
}
async关键字用来表示一个方法是异步的,await关键字则是用来等待Future的值的。而且还不会阻塞整个app的运行。整个app的main方法也可以是async-await的:
dart
void main() async {
print("Fetching data...");
var data = await fetchData();
print("Data received: $data");
}
在Widget里使用async await
既然是基于Future的那必定是FutureBuilder了。
dart
FutureBuilder<String>(
future: fetchData(), // 1
builder: (context, snapshot) { // 2
if (snapshot.connectionState == ConnectionState.done) {
return Text(snapshot.data ?? 'Error fetching data');
} else {
return CircularProgressIndicator();
}
},
)
在future
属性提供一个异步方法,它返回一个Future
。
在builder
中,根据snapshot
的connectionState
的值判断future返回的状态返回不同的widget。
错误处理
Flutter提供了优雅的处理异步编程中的异常的方法。那就是try, catch和finally。
dart
Future<void> fetchDataWithException() async {
try {
var data = await fetchDataWithError();
print(data);
} catch (error) {
print("Error: $error");
}
}