最近打算自己做一个电商 App 调研之后选择技术栈为 Flutter 因此打算梳理一下 Flutter 的所有知识点,做个预热。
- flutter 中切换组件整体上和切换图片的方式一致,就是使用一个变量存储当前要渲染的组件然后改变其值再调用 setState 重新渲染即可。
dart
Widget activeScreen = const StartScreen();
void switchScreen(){
setState((){
activeScreen = const QuestionsScreen();
})
}
- 在 flutter 中有两个比较重要的概念,其中一个是组件的条件渲染,另一个则是状态提升,所谓状态提升指的就是子组件能够通过与父组件共享状态从而影响到其他子组件。具体来说,父组件中维护状态池,然后再调用子组件的时候将父组件的 setState 函数封装成改变特定状态的函数以实参的方式传递给子组件,子组件一旦调用此形参函数就可以使父组件重新渲染并使用新的特定状态。
dart
const StartScreen(void Function() startQuiz, {super.key});
与,
dart
const StartScreen(this.startQuiz, {super.key});
final void Function() startQuiz;
在 flutter 中一定要记住,构造函数接受是接受,如果想要在组件内部使用还需要保存,所以这里有两步。
- 切换组件当然可以使用改变变量值的方式,但是这种方式有一个弊端,那就是必须考虑组件的声明周期函数的执行顺序,也就是说我们必须采用一些额外的方法对代码的执行顺序进行排序
dart
class QuizState extends State<Quiz> {
Widget? activeScreen;
@override
void initState() {
super.initState();
activeScreen = StartScreen(switchScreen);
}
void switchScreen() {
setState(() {
activeScreen = const QuestionsScreen();
});
}
}
相比较使用 initState 钩子,我们使用其他方法:
dart
...
var activeScreen = 'start-screen';
void switchScreen() {
setState(() {
activeScreen = 'questions-screen';
});
}
@override
Widget build(context) {
Widget screenWidget = StartScreen(switchScreen);
if (activeScreen == 'qustion-screen') {
screenWidget = const QuestionScreen();
}
}
...
child: activeScreen == 'start-screen' ? StartScreen(switchScreen) : const QuestionsScreen(),
我们并不是在变量中直接保存一个组件实例,而是保存要渲染的组件的令牌,这样就避免了组件初始化的时候的顺序问题。此外,这里的 if 语句和双等号判断都和 js 中是完全一致的。
- 使用 dart 文件存储数据 dart 文件中定义的变量无需使用 export 导出,它们的标识符只要不是以下划线开头被其他 dart 文件导入之后就可以直接使用,基于此 dart 文件也可以直接用来存储数据。下面的代码就是存储了组件实例数组的 dart 文件:
dart
import 'package:adv basics/models/quiz_question.dart';
const questions = [
QuizQuestion(
'What are the main building blocks of Flutter UIs?',
[
'Widgets',
'Components',
'Blocks',
'Functions',
],
)
];
- flutter 中所有的数据结构和算法统称为对象 Object, 它可以分成两个大类,分别是数据结构(包括变量和属性)和方法(函数)。
- dart 文件可以向外提供自定义的组件,也可以保存数据,还可以向外输出数据结构,也就是数据类,如下所示。
dart
class QuizQuestion {
const QuizQuestion(this.text, this.answer);
final String text;
final List<String> answers;
void PrintText() {
print(text)
}
List<String> getAnswers() {
return List.of(answers).shuffledList();
} // 定义方法无非就是在前面加上函数的返回值类型么
}
...
const questions = [
QuizQuestion(
'What are the main building blocks of Flutter UIs?',
[
'Widgets',
'Components',
'Blocks',
'Functions',
],
)
];
- 占满空间的 SizedBox 组件
dart
SizedBox(
width: double.infinity,
...,
)
以及,
dart
SizedBox(
width: double.infinity,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [],
)
)
或者,
dart
SizedBox(
width: double.infinity,
child: Container( // Column 不能直接设置外边距吗?
margin: const EdgeInsets.all(40),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [],
),
)
)
- 封装一个无状态的按钮组件
dart
class AnswerButton extends StatelessWidget {
const AnswerButton({
super.key,
required this.answerText,
required this.onTap,
});
final String answerText;
final void Function() onTap;
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: onTap,
style: ElevatedButton.styleFrom(),
child: Text(answerText),
);
}
}
注意,在这里我们使用了 required 保证通过命名传递的参数一定存在。并且使用 final void Function()
接受形参函数。
使用封装的按钮组件:
dart
AnswerButton(
answerText: 'Answer text...',
onTap: () {}
)
通过 ElevatedButton.styleFrom 方法设置按钮的:padding 背景色 前景色 形状(包括边框):
dart
ElevatedButton(
onPressed: onTap,
style: ElevatedButton.styleFrom(
padding: EdgeInsets.symmetric(vertical: 20),
backgroundColor: const Colors.fromARGB(255,33,1,95),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(40),
),
),
child: Text(answerText),
);
- dart 中数组的取元素方式和 js 中完全相同,对于下面的结构,如果我们想要取到
'Widgets'
我们应该使用questions[0].answers[0]
dart
class QuizQuestion {
const QuizQuestion(this.text, this.answer);
final String text;
final List<String> answers;
}
...
const questions = [
QuizQuestion(
'What are the main building blocks of Flutter UIs?',
[
'Widgets',
'Components',
'Blocks',
'Functions',
],
)
];
- 数组的 map 方法,dart 中的 map 方法和 js 一致,可以产生一个新的数组
dart
const numbers = [1,2,3];
const doubled = numbers.map((num){
return num * 2;
}):
- 数组的扩展操作符,可以将一个数组展开,这方面的功能和 js 也一致
dart
const numbers = [1, 2, 3];
const moreNums = [...numbers, 4];
通过 map 和 扩展运算符,我们就可以实现如同 react 中一样的列表渲染了,
dart
children: [
Text('123'),
...currentQuestion.answers.map(( answer ){
return AnswerButton( answerText: answer, onTap: (){} );
}),
]
或者,
dart
const SizedBox(height: 30), // 添加30像素的垂直间距
...currentQuestion.getShuffledAnswers().map((answer) {
return AnswerButton(
answerText: answer,
onTap: () {
answerQuestion(answer);
},
);
})
- 从文字组件内部设置文字的排布方式
dart
Text(
'??',
style: const TextStyle(
color: Colors.white,
),
textAlign: TextAlign.center,
)
- 数组乱序方法 dart 的数组提供了一个非常特殊的方法,调用之后原来的数组的顺序会被随机打乱,非常实用:
dart
const numbers = [1,2,3];
numbers.shuffle();
print(numbers);
- 数组的复制 dart 的数组构造类为 List 正如 Array 一样提供了 of 静态方法,可以将现有的数组进行备份:
dart
const numbers = [1,2,3];
final newNumbers = List.of(numbers);
print(newNumbers);
一般备份的东西,我们在其前面加 final 修饰符,表示备份不可改变。
- 变量自增 1 的做法
dart
var currentQuestionIndex = 0;
void answerQuestion() {
setState(() {
currentQuestionIndex++; // Increments the value by 1
// currentQuestionIndex += 1;
// currentQuestionIndex = currentQuestionIndex + 1;
});
}
- 引入新的字体 在 google 中搜索
flutter google fonts
进入Pub.dev
中找到google_fonts 4.0.3
按照官网的说明,依次执行如下的命令:
powershell
flutter pub add google_fonts
安装之后就可以直接使用了:
dart
style: GoogleFonts.lato(
color: const Color.fromARGB(255,201,153,251),
fontSize: 24,
fontWeight: FontWeight.bold,
),
注意这些内置的一般前面都不需要添加 const 修饰。
- 数组中增加一个元素的做法 在 dart 中,数组增加一个元素的方式不是使用 push 方法,而是使用名为 add 的方法,但是作用是一样的;
dart
const numbers = [1,2,3];
numbers.add(4);
- 以命名参数的方式向组件中传递函数参数的方式温习
dart
const QuestionsScreen({super.key, required this.onSelectAnswer});
final void Function(String answer) onSelectAnswer;
- 在可变组件类中使用外来函数的方式 在 Flutter 框架中,组件文件通常包含两个类:一个私有的
State
类负责处理业务逻辑,以及一个公有的StatefulWidget
类,后者用于接收外部参数,包括函数参数。关键点在于,虽然业务逻辑是在私有的State
类中实现的,但这个内部类可以直接访问外部StatefulWidget
类的属性和方法。
这是因为 Flutter 的State
对象在初始化时会接收到与之关联的StatefulWidget
的实例,通常通过this.widget
来引用。因此,任何通过StatefulWidget
的构造函数传入的参数,包括函数,都可以通过this.widget
在State
类中被访问和使用。这样,内部的State
类就能够获取并使用外部传入的函数参数,而无需依赖于任何全局或文件级的对象。
dart
class QuestionsScreenState extends State<QuestionsScreen> {
var currentQuestionIndex = 0;
void answerQuestion(String selectedAnswer) {
// 调用外部类(widget)的函数来处理答案选择
widget.onSelectAnswer(selectedAnswer);
// 使用setState来更新状态,因为我们需要重建UI来反映新的状态
setState(() {
currentQuestionIndex++; // Increments the value by 1
});
}
}
...
const SizedBox(height: 30), // 添加30像素的垂直间距
...currentQuestion.getShuffledAnswers().map((answer) {
return AnswerButton(
answerText: answer,
onTap: () {
answerQuestion(answer);
},
);
})
- 根据数组的长度做出 conditional render
dart
void chooseAnswer(String answer) {
selectedAnswers.add(answer);
if (selectedAnswers.length == questions.length) {
setState(() {
activeScreen = 'start-screen';
});
// 假设这里有一个方法来重置问题列表和答案列表,准备下一轮
resetQuestionsAndAnswers();
}
}
void reserQuestionsAndAnswers() {
selectedAnswers = [];
}
显然获取数组的长度我们使用 length 属性无疑。
-
在 dart 中
var activeScreen='start-screen'
和String activeScreen='start-screen'
有何区别 在 Dart 语言中,变量类型声明有两种方式:隐式类型(使用var
)和显式类型(指定具体的类型,如String
)。 -
隐式类型(使用
var
):- 使用
var
关键字声明的变量,Dart 编译器会根据变量的初始值自动推断其类型。 - 这意味着
var activeScreen = 'start-screen';
会推断activeScreen
的类型为String
,因为它被初始化为一个字符串。 - 如果之后你给
activeScreen
赋值为非字符串类型的值,Dart 编译器会报错,因为var
声明的变量类型在赋初值后是固定的。
- 使用
-
显式类型(指定具体的类型,如
String
):- 使用具体类型声明的变量,如
String activeScreen = 'start-screen';
,明确指出变量的类型是String
。 - 这种声明方式要求变量的值必须是该类型,否则编译器会报错。
- 显式类型声明可以提高代码的可读性,尤其是在复杂的项目中,它可以让其他开发者或维护者更清楚地知道变量的预期类型。
- 使用具体类型声明的变量,如
区别:
- 类型安全性 :显式类型声明(如
String
)提供了更强的类型安全性,因为它明确了变量的类型,有助于避免类型错误。 - 灵活性 :隐式类型声明(如
var
)在某些情况下提供了更大的灵活性,尤其是在变量类型可能在开发过程中变化的情况下。但是,一旦变量被初始化,其类型就固定了,这与显式类型声明的行为是一致的。 - 代码清晰性:显式类型声明可以提高代码的清晰性,尤其是在团队开发中,它有助于其他开发者更快地理解代码的意图。
在实际开发中,选择使用var
还是显式类型声明,取决于你的具体需求、项目规范以及个人或团队的偏好。对于简单的项目或快速开发,var
可能更方便;而对于需要高度类型安全性和代码清晰性的项目,显式类型声明可能更合适。
- 将其他页面上收集到的数据汇总到一个 lifting state 中之后同意传入到 summary 页面的做法
dart
if(activeScreen == 'results-screen') {
screenWidget = ResultsScreen(choseAnswers: selectedAnswers, );
}
与之对应的接受 summary 信息的无状态组件为:
dart
class ResultsScreen extends StatelessWidget {
const ResultsScreen({
super.key,
required this.chosenAnswers,
});
final List<String> chosenAnswers;
}
-
在 Dart 语言中,
final
和const
关键字都用于定义不可变的变量,但它们之间有一些关键的区别: -
final
变量:- 使用
final
关键字声明的变量,一旦被赋值后,其值就不能被改变。 final
变量可以在声明时不立即赋值,但必须在构造函数执行结束前被初始化。final
变量可以被赋予任何类型的值,包括可变类型(如列表、映射等)。- 如果
final
变量被赋予一个可变类型的值,那么这个值的内容是可以改变的,只是变量本身不能被重新赋值为另一个对象。
- 使用
-
const
变量:- 使用
const
关键字声明的变量,表示该变量的值在编译时就已经确定,并且在运行时不能被改变。 const
变量必须在声明时立即赋值,因为它们的值是编译时常量。const
变量通常用于定义不可变的字面量值或编译时常量。- 如果
const
变量被赋予一个可变类型的值,那么这个值的内容也是不能改变的,因为const
创建的是对该值的常量引用。
- 使用
区别:
- 赋值时机 :
final
变量可以在声明时不立即赋值,而const
变量必须在声明时立即赋值。 - 编译时常量 :
const
变量的值是编译时常量,这意味着它们的值在编译时就已经确定,并且可以在编译时进行优化。 - 优化 :由于
const
变量的值是编译时常量,编译器可以对它们进行更多的优化,比如内联替换。 - 类型限制 :
const
变量可以被赋予任何类型的值,包括可变类型,但与final
不同的是,const
变量的值是真正的常量,不能被修改。 - 性能 :使用
const
声明的变量和对象可以提高性能,因为它们允许编译器进行更多的优化。
在实际使用中,如果你知道一个变量的值在运行时不会改变,并且希望利用编译器的优化,那么应该使用const
。如果变量的值可能在运行时确定,但你知道它不会改变,那么可以使用final
。对于集合类型如List
,使用const
声明意味着列表及其所有元素都是不可变的。
- dart 中的对象数据结构 与 js 不同,dart 中,哈希表就是 Map 用于存储键值对,在定义的时候说清楚 key 和 value 的类型,这一点和 ts 很像。
dart
var user = {
'user name': 'Maximilian',
'password':'supersecret',
'age': 33,
}
...
user['age'] = 34;
user.containsKey('age');
- List 结构通过 for 循环生成 List
dart
List<Map<String, Object>> getSummaryData() {
final List<Map<String, Object>> summary = [];
for(var i = 0; i < chosenAnswers.length; i++) {
summary.add({
'question_index': i,
'question': questions[i].text,
'correct_answer': questions[i].answers[0],
'user_answer': chosenAnswers[i],
})
}
return summary;
}
这里只是展示 for 循环的用法,实际上我们直接使用 map 方法就可以了。但是使用 map 方法之后,记得要在后面加上 .toList()
使其变成真正的数组。
- 断言 我们可以在有些地方使用
as
对数据结构进行断言以避免不必要的报错:
dart
Text(((data['question'] as int) + 1).toString()),
number 类型转 string 类型使用 toString 即可。
- Expanded 组件 在 Flutter 中,
Expanded
是一个用来控制子 Widget 在父 Widget 中所占空间的布局 Widget。它通常用在Row
、Column
或者Flex
这样的 Flex 布局模型中,允许子 Widget 根据剩余空间自动扩展或收缩。
以下是Expanded
的一些关键点:
-
自动填充空间 :
Expanded
会尝试填充其父 Widget 中的所有可用空间。如果有多个Expanded
Widget,它们会根据flex
属性分配空间。 -
flex 属性 :
Expanded
Widget 有一个flex
属性,它是一个整数,表示这个 Widget 相对于其他Expanded
Widget 的扩展能力。flex
值越大,分配到的空间就越多。 -
单个使用 :在只有一个
Expanded
Widget 的情况下,它将占据父 Widget 中的所有可用空间。 -
多个使用 :如果有多个
Expanded
Widget,它们将根据各自的flex
值按比例分配父 Widget 的空间。 -
子 Widget :
Expanded
必须有一个子 Widget,这个子 Widget 可以是任何类型的 Widget。 -
不可收缩 :
Expanded
不允许其子 Widget 收缩到小于其最小尺寸,它总是会尝试扩展以填充可用空间。
下面是一个简单的使用Expanded
的例子:
dart
Row(
children: <Widget>[
Expanded(
flex: 2,
child: Container(
color: Colors.red,
),
),
Expanded(
flex: 1,
child: Container(
color: Colors.blue,
),
),
],
)
在这个例子中,我们创建了一个水平的Row
,其中包含两个Expanded
Widget。第一个Expanded
的flex
值为 2,第二个为 1,这意味着第一个 Widget 将占据父 Widget 空间的 2/3,而第二个 Widget 占据 1/3。
Expanded
是一个非常有用的 Widget,它使得在 Flutter 中创建灵活和响应式的布局变得更加简单。
- flutter 数组中的 filter where 相当于 filter.
dart
final numCorrectQuestions = summaryData.where((data) {
return data['user answer'] == data['correct answer'];
}).length;
- 关于 SizedBox 和 SingleScrollView
-
SizedBox :确实类似于 HTML 中的
div
,但它用于在 Flutter 中创建一个具有固定大小的盒子。它不会像 HTML 中的div
那样默认填充其内容;相反,它会根据指定的width
和height
属性来确定其大小。如果子 Widget 超出了这个大小,它会被裁剪。 -
SingleChildScrollView :这个 Widget 允许其子 Widget 在有限的空间内滚动。它类似于一个可以滚动的
div
,但通常用于创建可滚动的区域。它有一个child
属性,你可以将需要滚动的 Widget 作为子 Widget 传递给它。 -
SingleChildScrollView 的 child :在使用
SingleChildScrollView
时,你需要提供一个子 Widget,这个子 Widget 可以是任何 Widget,包括另一个布局 Widget。如果子 Widget 超出了SingleChildScrollView
的大小,它就会变得可滚动。
简而言之,SizedBox
用于创建具有固定大小的盒子,而SingleChildScrollView
用于创建可滚动的区域。
- dart 中的 getter 基本上和 js 中的一样,下面是它们的对比:
dart
List<Map<String, Object>> getSummaryData() {
final List<Map<String, Object>> summary = [];
for(var i = 0; i < chosenAnswers.length; i++) {
summary.add({
'question_index': i,
'question': questions[i].text,
'correct_answer': questions[i].answers[0],
'user_answer': chosenAnswers[i],
})
}
return summary;
}
修改成 getter:
dart
List<Map<String, Object>> get summaryData() {
final List<Map<String, Object>> summary = [];
for(var i = 0; i < chosenAnswers.length; i++) {
summary.add({
'question_index': i,
'question': questions[i].text,
'correct_answer': questions[i].answers[0],
'user_answer': chosenAnswers[i],
})
}
return summary;
}
- dart 中的箭头函数
与 JavaScript 不同,Dart 中的箭头函数更多的是匿名函数的一种简写方式。在 Dart 中,箭头函数通常用于创建匿名函数,它们可以简化函数的书写,特别是当函数体只有一行表达式时。
dart
final numCorrectQuestions = summaryData.where((data) => data['user_answer'] == data['correct_answer']).length;
-
Dart 中的箭头函数使用
=>
关键字,它表示一个函数体,其中只有单个表达式,而不需要使用return
关键字。如果函数体有多条语句或者需要使用return
关键字,就不能使用箭头函数的简写形式。 -
在上述代码中,
where
方法用于过滤summaryData
列表,只保留那些用户答案(user_answer
)与正确答案(correct_answer
)相匹配的条目。箭头函数(data) => data['user_answer'] == data['correct_answer']
作为where
方法的参数,为每个data
项提供了一个条件表达式。 -
最后,使用
.length
属性来获取过滤后的列表的长度,即正确答案的数量。
简而言之,Dart 中的箭头函数是一种简洁的匿名函数写法,适用于函数体只有单个表达式的情况。在处理集合或列表时,它们经常被用在像where
这样的方法中,以提供过滤或映射的条件。
- dart 中的两种调试模式
- view -> appearance -> Panel
- main 函数上面的 Run|Debug|Profile 中点击 Profile 打开网页调试,会自动连接模拟设备可以使用选择器选择模拟器上的元素实时修改。