《Flutter实战》第三章
- 本篇参考资料《Flutter实战》
- 本篇文章只是本人看书的理解和整理的笔记,更完整的内容还在书上!
- 电子书链接:https://book.flutterchina.club/
- Flutter中文社区链接:https://flutterchina.club/
- 尊重原作者,能支持购买实体书当然最好
3.1~3.2
一.Flutter项目的基本结构和文件命名方式
我们来看一个例子:github上5k星的flutter项目:在flutter上实现豆瓣APP
https://github.com/kaina404/FlutterDouBan
这个是一个值得学习的大型Flutter项目结构,有很多代码d本人没有看懂,以后再补充,我们可以看到,这个项目结构是根据代码的不同功能区分的很清楚
同时补充一下dart文件和class命名:我们打开pages包
image.png
那么在我们的dart文件是按不用的功能放在不同的包里,那android studio怎么新建dart包呢?如下图: image.png
这里我们新建了一个error_dart,显示错误信息界面、
image.png
在main使用需引入:为引入就使用时会报错直接alt+enter就可以看到IDE提供的解决方案
import 'package:tomatoflutterapp/pages/error_page.dart';
二 StatelessWidget和StatefulWidget
之前已经提到过,书上也讲的很清楚,复制黏贴没有意义,直接跳转
https://book.flutterchina.club/chapter3/flutter_widget_intro.html
StatelessWidget
:是没有状态,不可改变的界面
StatefulWidget
:是有状态的,界面内容可以根据状态进行改变
其中无状态界面StatelessWidget
是直接在其内部重写build方法构建出界面,
但是有状态界面内通常是有两个类组成StatefulWidget
和State
,build在State
中,同时State
中还可以调用setState
进行刷新
2.1 State
一个StatefulWidget类会对应一个State类,State表示与其对应的StatefulWidget要维护的状态,State中的保存的状态信息可以:
- 在widget 构建时可以被同步读取。
- 在widget生命周期中可以被改变,当State被改变时,可以手动调用其setState()方法通知Flutter framework状态发生改变,Flutter framework在收到消息后,会重新调用其build方法重新构建widget树,从而达到更新UI的目的。
State中有两个常用属性:
-
widget,它表示与该State实例关联的widget实例,由Flutter framework动态设置。注意,这种关联并非永久的,因为在应用生命周期中,UI树上的某一个节点的widget实例在重新构建时可能会变化,但State实例只会在第一次插入到树中时被创建,当在重新构建时,如果widget被修改了,Flutter framework会动态设置State.widget为新的widget实例。
通过weiget可以获取到StatefulWidget的一些属比如weiget.title -
context。StatefulWidget对应的BuildContext,作用同StatelessWidget的BuildContext。
State的生命周期
为了更好的理解我们在计数器里重写生命中周期函数
//生命周期
@override
void initState() {
super.initState();
_counter = widget.initValue; //使用widget属性 获得与之关联的widget实例初始值
print("initState");
}
@override
Widget build(BuildContext context) {
print("build");
...
}
@override
void didUpdateWidget(MyStatefulWidgetPage oldWidget) {
super.didUpdateWidget(oldWidget);
print("didUpdateWidget");
}
@override
void deactivate() {
super.deactivate();
print("deactive");
}
@override
void dispose() {
super.dispose();
print("dispose");
}
@override
void reassemble() {
super.reassemble();
print("reassemble");
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
print("didChangeDependencies");
}
运行程序进入计数器界面我们可以看到生命周期initState-->didChangeDependencies-->build
I/flutter ( 5042): initState
I/flutter ( 5042): didChangeDependencies
I/flutter ( 5042): build
点击“+”我们可以看到:build被重复调用 界面重构了
D/SettingsInterface( 5042): from settings cache , name = sound_effects_enabled , value = 0
I/flutter ( 5042): build
D/SettingsInterface( 5042): from settings cache , name = sound_effects_enabled , value = 0
I/flutter ( 5042): build
D/SettingsInterface( 5042): from settings cache , name = sound_effects_enabled , value = 0
I/flutter ( 5042): build
我们点击热加载时:
Performing hot reload...
|I/flutter ( 5042): reassemble
/I/flutter ( 5042): didUpdateWidget
I/flutter ( 5042): build
当我们退出界面:
I/flutter ( 5042): deactive
I/flutter ( 5042): dispose
下边是书上对各个生命周期的解释:
- initState:当Widget第一次插入到Widget树时会被调用,对于每一个State对象,Flutter framework只会调用一次该回调,所以,通常在该回调中做一些一次性的操作,如状态初始化、订阅子树的事件通知等。不能在该回调中调用BuildContext.dependOnInheritedWidgetOfExactType(该方法用于在Widget树上获取离当前widget最近的一个父级InheritFromWidget,关于InheritedWidget我们将在后面章节介绍),原因是在初始化完成后,Widget树中的InheritFromWidget也可能会发生变化,所以正确的做法应该在在build()方法或didChangeDependencies()中调用它。
- didChangeDependencies():当State对象的依赖发生变化时会被调用;例如:在之前build() 中包含了一个InheritedWidget,然后在之后的build() 中InheritedWidget发生了变化,那么此时InheritedWidget的子widget的didChangeDependencies()回调都会被调用。典型的场景是当系统语言Locale或应用主题改变时,Flutter framework会通知widget调用此回调。
- build():此回调读者现在应该已经相当熟悉了,它主要是用于构建Widget子树的,会在如下场景被调用:
在调用initState()之后。
在调用didUpdateWidget()之后。
在调用setState()之后。
在调用didChangeDependencies()之后。
在State对象从树中一个位置移除后(会调用deactivate)又重新插入到树的其它位置之后。
- reassemble():此回调是专门为了开发调试而提供的,在热重载(hot reload)时会被调用,此回调在Release模式下永远不会被调用。
- didUpdateWidget():在widget重新构建时,Flutter framework会调用Widget.canUpdate来检测Widget树中同一位置的新旧节点,然后决定是否需要更新,如果Widget.canUpdate返回true则会调用此回调。正如之前所述,Widget.canUpdate会在新旧widget的key和runtimeType同时相等时会返回true,也就是说在在新旧widget的key和runtimeType同时相等时didUpdateWidget()就会被调用。
- deactivate():当State对象从树中被移除时,会调用此回调。在一些场景下,Flutter framework会将State对象重新插到树中,如包含此State对象的子树在树的一个位置移动到另一个位置时(可以通过GlobalKey来实现)。如果移除后没有重新插入到树中则紧接着会调用dispose()方法。
- dispose():当State对象从树中被永久移除时调用;通常在此回调中释放资源。
3.2 暂时跳过
3.3~3.8是关于每个单独控件的使用,书上是说明很清楚,例子也很丰富,建议先全部浏览完毕后,对于某个控件想深入了解再去搜索
补充点:
3.6:单选框和复选框
注意每个单选框/复选框 都需要有一个value 布尔变量对应,首先给初始值,然后点击的时候需要
onChanged
中调用setState
方法来刷新界面才能看到点击效果
class _MySwitchPageState extends State<MySwitchPage> {
bool _switchSelected = true; //维护单选开关状态
.....
Switch(
value: _switchSelected, //给予一个初始值
onChanged: (value) {
//在点击后会调用onChanged 传入改变后的值
//重新构建页面
setState(() {
//修改状态值
_switchSelected = value;
});
},
)
.....}
单独使用Switch和Checkbox就只是一个开关
image.png
那我们想要有一些文字说明怎么做呢?
image.png
这时候要用到Switch和Checkbox的上层封装类 SwitchListTile / CheckboxListTile
SwitchListTile(
secondary: Icon(
Icons.block,
color: Colors.pink[200],
),
title: Text("关闭通知"),
value: _switchSelected, //给予一个初始值
onChanged: (value) {
//在点击后会调用onChanged 传入改变后的值
//重新构建页面
setState(() {
//修改状态值
_switchSelected = value;
});
},
),
CheckboxListTile(
title: Text("硬件加速"),
secondary: Icon(
Icons.access_alarm,
color: Colors.pink[200],
),
value: _checkboxSelected,
onChanged: (value) {
setState(() {
//修改状态值
_checkboxSelected = value;
});
},
)
3.7:输入框和表单
书上的不是很好理解
参考链接:https://www.jianshu.com/p/54419a143d70
3.7.1 controller
的一般用法:
class _MyInputPageState extends State<MyInputPage> {
//定义一个Controller
TextEditingController _myDemoController = TextEditingController();
void initDemoController() {
//设置默认值
_myDemoController.text = "hello world!";
//第三个字符开始选中后面的字符
_myDemoController.selection = TextSelection(
baseOffset: 2, extentOffset: _myDemoController.text.length);
//监听输入改变
_myDemoController.addListener(() {
//这里是只要输入框的内容变了都可以监听到
print(_myDemoController.text);
});
}
@override
void initState() {
super.initState();
initDemoController();
}
.....
}
在TextField中设置controller
:
TextField(
//设置controller,可以通过controller完成很多很多工作
controller: _myDemoController,
textInputAction: TextInputAction.search,
)
3.7.2 onEditingComplete和onSubmitted
onEditingComplete和onSubmitted:这两个回调都是在输入框输入完成时触发,比如按了键盘的完成键(对号图标)或搜索键(🔍图标)。不同的是两个回调签名不同,onSubmitted回调是ValueChanged<String>类型,它接收当前输入内容做为参数,而onEditingComplete不接收参数。
TextField(
//设置controller,可以通过controller完成很多很多工作
controller: _myDemoController,
textInputAction: TextInputAction.search,
//输入完成时调用,比如点击了搜索,但是无参数
//可以使用controller获取到输入内容
onEditingComplete: (){
print("onEditingComplete: ${_myDemoController.text}");
},
//输入完成时调用,这里有参数是输入的内容input
onSubmitted: (input){
print("onSubmitted:$input");
},
)
image.png
我突然发现这种decoration设计属性可以简单封装一下,每次调用就会节省很多时间,风格也统一了
3.7.3 用很笨的一个方式来实现一个错误输入提醒
效果(主要学习整理一个思想,慢慢改进,记录一个代码优化的思路)
错误提示
我们知道error信息是在InputDecoration中显示的,其实如果在InputDecoration构造函数中设置了errorText,并传入null,那么和不设置其实是一个效果,所以我们设置一个errorMsg变量,让他随输入的内容变化:
//错误信息
String errorMsg;
void initDemoController() {
//设置默认值
_myDemoController.text = "hello world!";
//监听输入改变
_myDemoController.addListener(() {
//这里是只要输入框的内容变了都可以监听到
print(_myDemoController.text);
setState(() {
//每次输入字符则改变界面状态
if (_myDemoController.text.length > 18) {
//如果长度大于18就显示
errorMsg = "输入内容太长";
} else {
errorMsg = null;
}
});
});
}
在TextField中设置
TextField(
//设置controller,可以通过controller完成很多很多工作
controller: _myDemoController,
textInputAction: TextInputAction.search,
decoration: InputDecoration(
labelText: "搜索",
hintText: "输入搜索内容",
prefixIcon: Icon(Icons.search),
fillColor: Colors.blue[200],
filled: true,
errorText: errorMsg),
)
但是我们发现这个错误提示显示了还是可以继续输入,不许用户输入怎么操作?
maxLength: 18,//设置最长输入
maxLengthEnforced: true,//true的话就阻止超过,false就会错误提示