这节课中我将尝试在点击按钮时,跳转到一个新的界面上去。
1. 代码实现界面的更换
位置:quiz.dart----> _QuizState类中
a.一种尝试和犯错
I.就在 _QuizState类下方,第一行添加如下代码
var activeScreen = const StartScreen();
效果:我们将StartScreen()类赋值给activeScreen变量
II.就在 I 的代码之后,添加如下代码
void switchScreen() {
setState(() {
activeScreen = const QuestionsScreen();
});
}
这段代码是会出错的,因为根据var的自动推断,var会认为该变量为StartScreen()类型的,而StartScreen继承至StatelessWidget,但是QuestionsScreen()继承至StatefulWidget,所以会发生冲突。
b.a的纠错
I. 与其用var自动推测,不如强制设定用Widget类,StatelessWidget和StatefulWidget都为Widget的子类
修改a ---> I 中的代码为如下
Widget activeScreen = const StartScreen();
这样就解决了这个问题!
c.既然我们已经用变量来修改页面,那么就要舍弃之前的硬编码了
位置:quiz.dart ----> _QuizState类 ----> build()方法 ---> child属性
修改为如下:
child: activeScreen,
该Quiz.dart代码如下
import 'package:adv_basics/questions.screen.dart';
import 'package:flutter/material.dart';
import 'package:adv_basics/start_screen.dart';
class Quiz extends StatefulWidget {
const Quiz({super.key});
@override
State<Quiz> createState() {
return _QuizState();
}
}
class _QuizState extends State<Quiz> {
Widget activeScreen = const StartScreen();
void switchScreen() {
setState(() {
activeScreen = const QuestionsScreen();
});
}
@override
Widget build(Object context) {
return MaterialApp(
home: Scaffold(
body: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [
Color.fromARGB(255, 132, 84, 214),
Color.fromARGB(255, 67, 17, 99)
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
)),
child: activeScreen,
),
),
);
}
}
接下来,我们分析下,它会怎么运行:
①在启动app时,它找到activeScreen变量,里面存着StartScreen(),于是加载了StartScreen()
②如果switchScreen()方法被执行了,activeScreen变为了QuestionsScreen(),setState方法会触发flutter重新执行该类下的Widget build(Object context)方法
③flutter会对比旧的widget树和新的widget树的区别,并只更新发生了变化的地方,在这个例子中是child属性。
通过这样,flutter可以高效地更新UI,并且不浪费机能,因为它只更新变了的地方。
2.在按下按钮时,我们要能转换到一个新的界面
为了达到这个目的,引入一个概念叫做Lifting Up State(状态提升),这是一种设计模式。
大概意思就是将多个子组件的状态控制代码提升到同一个父组件上,在父组件上能更容易地控制子组件的状态。
那么其实在上面的1.里面我们已经实现了状态提升,我们已经将如下,状态和状态控制代码放在了Quiz.dart中了
Widget activeScreen = const StartScreen();
void switchScreen() {
setState(() {
activeScreen = const QuestionsScreen();
});
}
问题在于,控制状态的switchScreen()代码放在Quiz.dart中,我们怎么在它的子部件中调用父部件的方法呢?
解决办法:在dart中,方法是可以当作参数来传的,我们可以将一个方法名作为参数传入另一个方法里。像这种可以传入一个函数作为参数的函数也叫做高阶函数。
a.在StartScreen()的构造器中添加函数参数
位置:start_screen.dart ---> StartScreen构造器
既然按钮在StartScreen中,我们就得把switchScreen()传入其中。
I.首先添加参数的名字,名字随便取,我取startQuiz,如下
const StartScreen(startQuiz,{super.key});
II.在该名称前面添加Function,表明这是方法或函数
const StartScreen(Function startQuiz,{super.key});
III. 再添加void,表明这是个不返回任何类型的方法,在Function后面添加一个括号
const StartScreen(void Function() startQuiz,{super.key});
这就是dart中,高阶函数的写法。
b.接下来,我们需要在StartScreen类中使用该函数
I.构造器中的void Function() startQuiz参数是不能直接使用的,所以需要把它赋值给本地变量
位置:start_screen.dart中,构造器下面
我们新增一个final修饰的Function变量,final表示在运行时才被赋值,且只能赋值一次
final void Function() startQuiz;
既然final保证了startQuiz必须被初始化,那么在构造器中startQuiz就可以去掉前面的头衔了。改用this.startQuiz了,它指向了下面这个startQuiz变量。
const StartScreen(this.startQuiz,{super.key});
final void Function() startQuiz;
II.在按钮中的触发方法调用startQuiz()方法
位置:start_screen.dart中 --> build()方法 ----> OutlinedButton.icon()方法下的onPressed属性
有二种方法:
第一种为用匿名函数调用startQuiz()方法
OutlinedButton.icon(
onPressed: () {
startQuiz();
},
第二种为直接用startQuiz指针作为onPressed属性的参数
OutlinedButton.icon(
onPressed: startQuiz,
3.引入initState()方法
注意到switchScreen作为参数放入也是报错的,为什么呢?
因为在这个_QuizState类中,在类被实例化的过程中,
Widget activeScreen = StartScreen(switchScreen);
void switchScreen() {
setState(() {
activeScreen = const QuestionsScreen();
});
}
上述两块代码几乎就是同时生成的,编译器不能保证switchScreen()方法先于StartScreen()生成出来,并且能作为参数嵌入,所以就报警了。
解决办法就是initState()方法
此方法为flutter生命周期中的第一个方法,它会确保在变量和方法被创造之后,才调用此initState()方法并只调用一次,所以成为了常规初始化各种变量的绝佳地方,也保证了变量和方法在这个方法内是可用的。
稍微修改下代码如下:
Widget? activeScreen;
@override
void initState() {
super.initState();
activeScreen = StartScreen(switchScreen);
}
来试试把
在点击了Start Quiz,之后跳转到了questions_screen.dart文件中,在最左上角显示了QuestionScreen的文本控件。