学习封装Flutter组件,看这篇就够了
Flutter 的自定义组件
一、添加 UI 组件
在进行自定义组件的封装之前,应该先掌握如何在 Flutter 应用页面中添加内置组件,如按钮和文本等,以下面的页面定义为例:
import 'package:flutter/material.dart';class SecondPage extends StatelessWidget {Widget build(BuildContext context) {return Text("Hello Word!");}
}
由于 dart 语法规定了方法返回值只能一个,所以无法通过简单地返回多个 Text 组件,去完成页面内容的增加;而作为 UI 组件,也不支持像字符串那样通过操作符 + 进行内容的拼接,因此,想要上述代码对应的页面具有更多的内容,可行的方法就是使用容器类组件作为最终的返回值,例如下面:
import 'package:flutter/material.dart';class TestPage extends StatelessWidget {Widget build(BuildContext context) {return Column(children: [Text("文本一"),Text("文本二"),],);}
}
又或者:
import 'package:flutter/material.dart';class TestPage extends StatelessWidget {Widget build(BuildContext context) {return Row(children: [Text("文本一"),Text("文本二"),],);}
}
然而,即便是容器组件,也不意味着就能够一次添加多个UI元素,因为像 Container 这种容器组件,只能声明定义一个UI元素:
class TestPage extends StatelessWidget {Widget build(BuildContext context) {return Container(width: (MediaQuery.of(context).size.width*0.95),padding: const EdgeInsets.all(8.0),child: Column(children: [Text("Hello World"),Text("Hello World"),Text("Hello World"),],),);}
}
仔细对比 Container 组件和 Column 组件与 Row 组件的属性,就不难发现可以通过属性名称,去判断一个容器组件是支持多元素定义、还是单一元素定义。
对于像 Column、Row 和 ListView 等支持多元素定义的容器组件,都是用 children 作为属性名,而对于像 Container、Padding 和 Center 等支持单一元素定义的容器组件,则是用 child 作为属性名。
二、定义布局约束
光知道用容器组件去丰富页面内容,是不足以形成自定义组件封装能力的,因为好看的应用页面,还必须具有位置合理、排列整齐等特点,而这些就需要布局约束去实现。
同其他UI框架一样,Flutter中的布局约束,也是分为相对布局和绝对布局。
1、绝对布局
Flutter 中,使用 绝对布局可以通过结合 Stack 组件和 Positioned 组件来实现,例如下面:
class TestPage extends StatelessWidget {Widget build(BuildContext context) {return Container(width: (MediaQuery.of(context).size.width*0.95),padding: const EdgeInsets.all(8.0),child: Stack(children: [Positioned(top: 50.0,left: 50.0,child: Container(width: 100.0,height: 100.0,color: Colors.red,child: Center(child: Text('Box 1')),),),Positioned(top: 100.0,right: 50.0,child: Container(width: 100.0,height: 100.0,color: Colors.blue,child: Center(child: Text('Box 2')),),),],),);}
}
子所以如此,是因为 Flutter 基础组件中并没有提供 position 属性去设置组件的位置。Positioned 组件可以通过设置四个方向上与父组件边界的距离,完成对自身位置的绝对定位。
2、相对布局
在 flutter 提供的内置组件中,除了 Positioned 组件外,都是采用相对布局的,即通过设置子组件对齐方式、或内外边距,实现对子组件的位置约束,例如下面:
class TestPage extends StatelessWidget {Widget build(BuildContext context) {return Container(width: (MediaQuery.of(context).size.width*0.95),color: Colors.white,margin: const EdgeInsets.all(4.0),padding: const EdgeInsets.all(8.0),child: Column(mainAxisAlignment: MainAxisAlignment.spaceEvenly,crossAxisAlignment: CrossAxisAlignment.center,children: [Text("Hello World"),Text("Hello World"),Text("Hello World"),],),);}
}
使用 Container 组件的 margin 属性设置外边距、padding 属性设置内边距,从而避免页面上的按钮或文本的位置,太靠边;而 Column 和 Row 等容器组件,都能够通过 mainAxisAlignment 属性和 crossAxisAlignment 属性,分别设置主轴方向的对齐方式和交叉轴方向的对齐方式,对于 Column 来说,主轴方向就是垂直方向,而交叉轴方向就是水平方向,而对齐方式主要有如下几种:
1)start:对于 Column 就是居上对齐,从顶部开始往下排列子组件;对于 Row 组件来说,就是居左对齐,从左边开始往右边排列子组件;
2)center:居中对齐,子组件会从中间往两边排列
3)end:对于 Column 就是从下往上排列子组件,对于 Row 就是从右往左排列子组件
4)spaceBetween:两端对齐,此方式会根据子组件的数量形成不同的布局结果,但子组件为单个时,会将子组件放在开始处,即顶部或右边;当子组件为两个,则分别放在两端;当子组件为三个,则分别放在两边和中间……,总之,会随着子组件数量的增加,进行组件间距的调整,保证平均整个排列方向。
5)spaceEvenly:也是两端对齐,但与 spaceBetween 的区别在于,子组件为一是居中放置,子组件为二则两端放置,并且与左右都与边界保持距离,该距离与组件间距是一样的。
6)spaceAround:两端对齐,与 spaceEvenly 类似,但两端与边界的距离,是组件间距的一半。
在 flutter 中使用相对布局时,需要注意一点,就是并非所有的容器组件,都支持设置外边距、内边距,例如上面的 Column 组件。而对于 Text 组件,是不能像 HTML 或其他UI框架中那样,设置自身的内外边距。
3、SizedBox 填充
flutter 的容器组件,没有提供 space 属性去设置子组件间的间距,因此,在不使用两端对齐的布局方式,且不想用 Container 组件去设置内外边距时,就需要使用 SizeBox 进行空白填充,例如下面:
Text("Hello World",style: style,
),
SizedBox(height: 20.0,
),
GestureDetector(onTap: () {print("文本被点击了!");_showDialog(context);},child: Text("可点击文本",style: style,),
),
在 Column 中,SizedBox 通常只需设置height属性即可,同理,在 Row 中只需设置 width 属性。
合理利用 SizedBox 也能够完成对子组件的排列,并且相对两端对齐来说,这种方式的自由度会更大一些,因为那些组件间的间隔大一些、那些组件间的间隔小一些,都能够进行控制了。
三、定义组件样式
布局只是起到让页面内容结构化,秩序井然,然而,这还不足以编写出好看的应用页面,因为Flutter组件的默认样式,往往都比较简陋,要么是尺寸大小不合适,要么就是颜色对比不协调,这些显然都需要进行调整。
在组件的诸多样式属性中,最基本的就是尺寸和颜色,而颜色又可以细分为背景色和前景色,而这两种样式是 flutter 内置组件都有的,属于公共样式属性;而一些组件,除了公共样式属性外,还具有一些自己才有的特殊样式属性,例如 Text 组件的下划线样式、字间距等。那么,这些组件样式如何定义呢?
1、定义尺寸
Flutter 中,不是所有的组件都支持尺寸的设置,而要看一个组件是否支持尺寸设置,最简单的方式,就是看起属性列表中是否存在 width 和 height 这对属性,例如 Container 组件:
Container({super.key,this.alignment,this.padding,this.color,this.decoration,this.foregroundDecoration,double? width,double? height,BoxConstraints? constraints,this.margin,this.transform,this.transformAlignment,this.child,this.clipBehavior = Clip.none,})
因为有这对属性,所以可以无关子组件地设置自身尺寸,而对于 Column 组件:
class Column extends Flex {const Column({super.key,super.mainAxisAlignment,super.mainAxisSize,super.crossAxisAlignment,super.textDirection,super.verticalDirection,super.textBaseline,super.children,}) : super(direction: Axis.vertical,);
}
由于没有width属性和height属性,所以无法定义自身的尺寸,只能根据子组件内容来确定大小。同时,也不难发现,用于定义边框样式的 decoration 也不是所有组件都支持的。
一个宽度为屏幕宽度的99%、高度与屏幕高度相等,且带有边框颜色的 Container 组件,可以用如下代码定义:
return Container(decoration: BoxDecoration(border: Border.all(color: Colors.green,width: 2.0,style: BorderStyle.solid,),color: Colors.white,borderRadius: BorderRadius.circular(10.0),),width: (MediaQuery.of(context).size.width*0.99),height: MediaQuery.of(context).size.height,//color: Colors.white,margin: const EdgeInsets.all(4.0),padding: const EdgeInsets.all(8.0),child: ChildComponent());
对于像按钮、文本这类组件,它们的尺寸是通过 style 属性进行定义的,定义的时候有两种方式,一种是内联方式,一种是变量传入方式;对于内联方式,通常如下:
Text("Hello World",style: TextStyle(color: Colors.blue,fontSize: 25.0,fontWeight: FontWeight.w100,letterSpacing: 2.5),
),
而对于变量传入方式,则是先 build 函数的 return 语句之前,定义一个 final 变量,例如下面的黑色按钮的定义:
final btnStyle = ButtonStyle(fixedSize: WidgetStateProperty.all(Size(100, 40)),backgroundColor: WidgetStateProperty.all(Colors.black),
);
然后,在需要使用该样式的按钮中,用 btnStyle 作为 style 属性的值即可:
ElevatedButton(onPressed: () {print("按钮被点击");},style: btnStyle,child: Text("按钮", style: TextStyle(color: Colors.white)),
)
2、定义边框
使用 BoxDecoration 对象定义边框颜色的时候,需要注意的是,只有当边框颜色统一时,才能进行边框圆角即 borderRadius 属性的设置,对于边框颜色不一致的,设置 borderRadius 将会导致编译失败。当需要设置不同的边框颜色时,可以用如下代码实现:
decoration: BoxDecoration(/*border: Border.all(color: Colors.green,width: 2.0,style: BorderStyle.solid,),*/border: Border(top: BorderSide(color: Colors.red, width: 2.0),left: BorderSide(color: Colors.green, width: 2.0),bottom: BorderSide(color: Colors.blue, width: 2.0),right: BorderSide(color: Colors.orange, width: 2.0),),color: Colors.white,//borderRadius: BorderRadius.circular(10.0),
),
另外,还需要注意的地方,就是当 Container 组件使用 decoration 属性后,color 属性就不要使用,否则会编译失败。
3、定义颜色
比起定义边框颜色,背景色这种颜色定义,在 UI 实现过程中,反而更为常见。在 Flutter 中,容器组件的背景色,通常用 color 属性进行定义。
由于容器组件本身没有实际内容,所以就不存在背景色和前景色的严格区分,使用 color 作为背景色的属性名,也是能够理解的;而像按钮这种存在实际内容的,就需要用 backgroundColor 属性和 foregroundColor 属性进行严格区分
然而,就像尺寸一样,flutter 并不支持所有的容器类组件进行背景色的定义。
4、定义 Style
非容器类的文本、按钮等组件,它们的颜色定义同尺寸定义一起放在 Style 对象中。创建 Style 对象的时候,既可以用 ButtonStyle、TextStyle 这种具体的 Style 类去创建,还可以从应用主题中进行复制:
final style = theme.textTheme.displayMedium!.copyWith(color: Colors.black,fontSize: 20.0,textBaseline: TextBaseline.ideographic,decoration: TextDecoration.underline,decorationColor: Colors.blue,decorationStyle: TextDecorationStyle.dashed,fontWeight: FontWeight.bold,letterSpacing: 2.0,
);
主题中的样式信息,来自于根组件 MaterialApp 中的 theme 属性值 ThemeData 的定义情况,Flutter 提供的默认主题中,并不包含 elevatedButtonTheme,因此,如果像用类似上面的代码,从应用上下文的主题数据中复制 elevatedButtonTheme 中的按钮样式,那么就应该在最开始的根组件中定义好 elevatedButtonTheme:
class MyApp extends StatelessWidget {const MyApp({super.key});Widget build(BuildContext context) {return ChangeNotifierProvider(create: (context) => MyAppState(),child: MaterialApp(title: 'FlutterDemo',theme: ThemeData(useMaterial3: true,colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),elevatedButtonTheme: ElevatedButtonThemeData(style: ButtonStyle(backgroundColor: WidgetStateProperty.all(Colors.grey),foregroundColor: WidgetStateProperty.all(Colors.black),))),//home: MyHomePage(),initialRoute: '/',routes: {'/': (context) => MyHomePage(),'/second': (context) => SecondPage(),'/list': (context) => ListWidget(),'/test': (context) => TestPage(),},),);}
}
之后如下的代码才不会导致运行时错误:
final btnTheme = theme.elevatedButtonTheme.style!.copyWith(fixedSize: WidgetStateProperty.all(Size(200, 50))
);
这种从主题中复制样式的做法,通常适用于需要基于公共样式进行微调的场景,例如这里的调整按钮的大小。
四、定义组件交互
一个应用,如果只是好看,却无法对用户的操作进行响应,那么就跟PPT没有什么区别,所以,定义组件交互式封装自定义组件,所不能或缺的能力,而在Flutter中,也是用事件处理函数来实现交互的,例如,每种按钮组件都必须传入一个 onPressed 事件处理函数,去完成用户按下按钮后的处理,再比如可选中文本可以传入一个 onTap 函数,再比如 TextField 组件可以用 onChange 函数去读取用户的输入数据,以及各种对话框的弹出与关闭。
那么现在,从简单到复杂,看看如何实现这些交互处理函数。
1、按钮响应
对于按钮的响应处理函数 onPressed 的实现,可以根据处理流程的多少,以匿名函数或普通函数的形式实现。
普通函数分为类成员函数和公共方法两种,大多时候,都采用类成员函数的形式,因为这样可以轻松地进行状态联动;当按钮触发的功能不涉及UI状态,只是存储的数据获取和写入时,使用公共方法更为恰当,比如从互联网上请求数据的功能。而匿名函数,也分为两种,一种是箭头函数,另一种则是只有函数体的形式。
ElevatedButton(onPressed: () => _controller.clear(),style: ButtonStyle(fixedSize: WidgetStateProperty.all(Size(150, 40)),),child: Text("清空输入框"),
),
ElevatedButton(onPressed: () {if(_text != "Hello World" || _text != "") {_showDialog(context, _text);} else {_showDialog(context);}},style: btnStyle,child: Text("对话框"),
),
ElevatedButton(onPressed: _goBack,style: btnTheme,child: Text("返回"),
)
如上所示,用普通函数和箭头函数的方式,可以让代码更为简洁,所以,强烈建议当按钮的功能处理,只需一句代码就能实现时,应该采用箭头函数的形式。
2、文本响应
在 Flutter 中,文本组件 Text 是不具有响应能力的,因为它没有提供任何事件处理函数:
Text Text( String data, { Key? key, TextStyle? style, StrutStyle? strutStyle, TextAlign? textAlign, TextDirection? textDirection, Locale? locale, bool? softWrap, TextOverflow? overflow, double? textScaleFactor, TextScaler? textScaler, int? maxLines, String? semanticsLabel, TextWidthBasis? textWidthBasis, TextHeightBehavior? textHeightBehavior, Color? selectionColor, }
)
但在应用使用场景中,点击文本的需求又是存在的,如此一来,也必然需要实现这种交互,那么基于 Flutter 提供的能力,该如何实现这种通过文本触发的交互呢?
对于点击事件来说,例如一个显示http连接的文本,要让它可以响应用户的点击,一种方式就是使用TextButton来显示文本,如下:
TextButton(onPressed: () {Uri url = Uri.parse("https://www.baidu.com/");_launchUrl(url);},child: Text("https://www.baidu.com/",style: TextStyle(color: Colors.blue,textBaseline: TextBaseline.alphabetic,decoration: TextDecoration.underline,decorationStyle: TextDecorationStyle.solid,decorationColor: Colors.green,fontSize: 20.0,),),
),
从而能够借用 TextButton 的 onPressed 事件进行交互响应;而另一种方式,就是在 Text 组件的外表包裹一个 GestureDetector 组件,例如下面:
GestureDetector(onTap: (){String msg = "点击了文本 ‘Hello World’";_showDialog(context, msg);},child: Text("Hello World",style: TextStyle(color: Colors.blue,fontSize: 25.0,fontWeight: FontWeight.w100,letterSpacing: 2.5),),
),
从而借助手势捕获处理函数 onTap 去实现交互响应。而对于选择文本这种交互,可以使用 Flutter 提供的现成组件 SelectableText:
SelectableText("可选择文本",style: style,onTap: () {print("SelectableText被点击");_controller.text = "可选择文本";},
),
这个组件自带有弹出菜单,即选中文本后可以进行复制等操作。
3、输入响应
而文本响应中,最为关键的恐怕还要数文本输入框的响应,因为很多数据都是依赖用户在文本输入框进行输入才能获取到,比如用户的姓名等身份信息。在 Flutter 中,无论是多行文本的输入,还是单行文本的输入,通通用 TextField 实现,并没有像其他 UI 框架那样,同时提供针对单行文本输入的 TextInput、和多行文本输入的 TextArea,也与其他 UI 框架有所区别的是,TextField 不能直接放置在容器组件中,必须包裹一层 Material 组件,如下:
Material(child: TextField(controller: _controller,//maxLines: 5,decoration: InputDecoration(border: OutlineInputBorder(), labelText: "Input"),onChanged: (value){if(value.isNotEmpty){setState(() {_text = value;});}},),
),
TextField 默认样式是单行文本,想要支持多行输入,可以为 maxLine 属性赋值。此外,Flutter 还提供了一个 TextFormField 用于构建登录界面等输入场景:
Form(key: _formKey,child: Column(children: [Text(_tip,style: TextStyle(color: Colors.red, fontSize: 18.0),),SizedBox(height: 5,),Material(child: TextFormField(decoration: InputDecoration(border: OutlineInputBorder(), labelText: "E-mail"),validator: (String? value) {if (value == null || value.isEmpty) {return "邮箱不能为空";} else if (!RegExp(r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+").hasMatch(value)) {return "邮箱格式不正确";}return null;},),),SizedBox(height: 10,),Material(child: TextFormField(decoration: InputDecoration(border: OutlineInputBorder(),labelText: "密码",suffixIcon: IconButton(onPressed: () {setState(() {_isPasswordVisible = !_isPasswordVisible;});},icon: Icon(_isPasswordVisible? Icons.visibility: Icons.visibility_off,),)),keyboardType: TextInputType.visiblePassword,obscureText: !_isPasswordVisible,obscuringCharacter: "*",validator: (String? value) {if (value == null || value.isEmpty) {return "密码不能为空";}return null;},),),SizedBox(height: 5,),ElevatedButton(onPressed: () {if (_formKey.currentState!.validate()) {setState(() {_tip = "提交成功";});} else {setState(() {_tip = "提交失败";});}},child: Text("Submit"))],),
),
如上所示,TextFormField 相对于 TextField 来说,多了一个用于数据校验的 validator函数。
五、定义数据流
几乎所有的手机应用,页面上的内容,除了一些提示文本外,全都上由数据流注入的,换句话说,在实际开发中,页面内容往往不是硬编码实现的,而是动态生成的,这也意味着,封装自定义组件,就必须知道如何给组件定义数据流。
1、借助页面级状态构建数据流
应该感到轻松的是,Flutter 不仅是事件驱动的,同时也是数据驱动的,利用 Flutter 提供的状态机制,我们可以很轻松地为组件集成数据流。换句话说,只要我们在声明定义组件的时候,记得将其声明为带状态的组件,那么就很容易借助状态机制去从父组件获取数据,例如下面
class TestPage extends StatefulWidget {final String data;const TestPage({super.key, required this.data}) ;State<TestPage> createState() => _TestPageState();
}
TestPage 中定义的 data,可以在 _TestPageState 中的 build 方法中,使用 widget.data 去获取,而当我们需要对外部传入的数据进行非全局的更新,那么可以在 _TestPageState 中定义一个字段去转存,例如下面:
class _TestPageState extends State<TestPage> {String _tempText = "";Widget build(BuildContext context) {_tempText = widget.data;// ....Text("外部传入的数据:${_tempText}", style: style, ),Material(child: TextField(controller: _controller,//maxLines: 5,decoration: InputDecoration(border: OutlineInputBorder(), labelText: "Input"),onChanged: (value) {if (value.isNotEmpty) {setState(() {_tempText = value;});}},),),}}
2、借助应用级状态构建数据流
而如果想数据更新后,影响的不止当前页面,而是App内使用到同一项数据的所有 APP 页面,那么就要使用全局状态来构建数据流,例如在如下的一个全局状态类中定义一个 globalText 和相应的更新方法:
class MyAppState extends ChangeNotifier {var current = WordPair.random();void getNext() {current = WordPair.random();notifyListeners();}var favorites = <WordPair>[];void toggleFavorite() {if (favorites.contains(current)) {favorites.remove(current);} else {favorites.add(current);}notifyListeners();}var globalText = "全局状态中的text";void updateGlobalText(String text){globalText = text;notifyListeners();}}
那么,其他页面中,只需在 build 函数中使用语句 var appState = context.watch<MyAppState>();,去持有全局状态句柄,就很容易在后续代码中,去访问宿主在全局状态中的相应数据流:
class _TestPageState extends State<TestPage> {Widget build(BuildContext context) {var appState = context.watch<MyAppState>();// ....Text("来自全局状态的数据:${appState.globalText}", style: style, ),Material(child: TextField(controller: _controller,//maxLines: 5,decoration: InputDecoration(border: OutlineInputBorder(), labelText: "Input"),onChanged: (value) {if (value.isNotEmpty) {setState(() {appState.updateGlobalText(value),});}},),),}}
六、封装自定义组件
当懂得如何进行组件的增加、布局约束、样式定义和响应交互,以及能够为组件定义数据流,那么便能够在 flutter 应用开发中,实现自定义组件的封装了。
以展示用户信息的页面为例,封装成可复用的自定义组件,具体要如何编写呢?答案是,可以按照如下的思路进行页面设计。
1、确定布局约束
用户信息,通常不会只有一条,比较常见的就有姓名、性别、年龄和住址等,所以,在布局方面必然要以支持多子组件的Column容器或ListView为主,考虑到要进行页面背景色和内容边距的调整,可以用 Container 作为最外层的容器,而具体的用户信息展示,肯定是以信息说明、信息内容共处一行的形式进行展示,所以可以用一个 Row 容器进行容纳,为了让信息说明文本和信息内容文本,不会挨得很近、影响阅读,每个 Row 容器中间都需要放置一个宽度合适的 SizedBox,当然了,用于展示用户信息的每个 Row 容器之间,也应该存在高度合适的 SizedBox。
Container(width: MediaQuery.of(context).size.width,height: MediaQuery.of(context).size.height,color: Colors.white,padding: const EdgeInsets.all(16),child: Column(mainAxisAlignment: MainAxisAlignment.center,crossAxisAlignment: CrossAxisAlignment.center,children: [Row(mainAxisAlignment: MainAxisAlignment.start,crossAxisAlignment: CrossAxisAlignment.center,children: [Text("姓名:", style: TextStyle(fontSize: 20, color: Colors.black, fontWeight: FontWeight.bold),),SizedBox(width: 20,),Text(_data.name),],),SizedBox(height: 15,),Row(mainAxisAlignment: MainAxisAlignment.start,crossAxisAlignment: CrossAxisAlignment.center,children: [Text("性别:", style: TextStyle(fontSize: 20, color: Colors.black, fontWeight: FontWeight.bold),),SizedBox(width: 20,),Text(_data.gender),],),SizedBox(height: 15,),Row(mainAxisAlignment: MainAxisAlignment.start,crossAxisAlignment: CrossAxisAlignment.center,children: [Text("年龄:", style: TextStyle(fontSize: 20, color: Colors.black, fontWeight: FontWeight.bold),),SizedBox(width: 20,),Text(_data.age),],),SizedBox(height: 15,),GestureDetector(onTap: (){_infoUpdateDialog(context, "输入地址", (value) => setState(() {_data.address = value;widget.personInfo.address = value;}));},child: Row(mainAxisAlignment: MainAxisAlignment.start,crossAxisAlignment: CrossAxisAlignment.center,children: [Text("住址:", style: TextStyle(fontSize: 20, color: Colors.black, fontWeight: FontWeight.bold),),SizedBox(width: 20,),Text(_data.address),],),),SizedBox(height: 15,),Text("widget.personInfo.address=${widget.personInfo.address}"),]),
);
2、了解生命周期
由于,数据的获取,常常借助组件的声明周期函数来完成,因此,需要了解 flutter 组件的生命周期函数,都有哪些。
对于无状态的组件(继承StatelessWidget)来说,它的生命周期函数就只有 build 函数,而对于继承自 StatefulWidget 的有状态的组件来说,生命周期函数有如下:
createState:用于创建State对象。initState:初始化State对象。didChangeDependencies:当依赖项发生变化时调用。build:构建组件。deactivate:组件被移除屏幕但未销毁时调用。dispose:组件被销毁时调用。
而对于应用级的状态,主要有如下几个:
- Resumed:应用进入前台。
- Paused:应用进入后台。
- Inactive:应用进入非活动状态。
- Detached:应用在运行但与组件分离。
要监听应用生命周期状态的变化,可以在继承 WidgetsBindingObserver后,用如下代码进行监听:
class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver {void initState() {super.initState();WidgetsBinding.instance.addObserver(this);}void didChangeAppLifecycleState(AppLifecycleState state) {super.didChangeAppLifecycleState(state);print("当前的应用生命周期状态:$state");if(state == AppLifecycleState.paused) {print("应用进入后台");} else if(state == AppLifecycleState.resumed) {print("应用进入前台");} else if(state == AppLifecycleState.inactive) {print("应用处于非活跃状态");} else if(state == AppLifecycleState.detached) {print("应用处于分离状态");}}void dispose() {super.dispose();WidgetsBinding.instance.removeObserver(this);}
}
一般来说,只有全局使用的数据,才需要结合应用生命周期状态监听去操作,而对于普通的非全局数据,通常只需在页面级的生命周期函数中进行操作即可,例如,在 initState 中对某个私有字段进行初始化:
void initState() {super.initState();_data = widget.personInfo;
}
3、构造数据流
页面展示的内容数据,有的是在 initState 函数中,从服务端拉取的;有的只是对来自父组件的数据进行解析转存。这里以父组件透传为例,进行数据流的构造。
作为当前页面的数据流源头,需要在继承了 StatefulWidget 的组件类中,定义一个数据字段,并在构造方法中声明为必传参数:
class PersonInfoPage extends StatefulWidget {final PersonInfoModel personInfo;const PersonInfoPage({super.key, required this.personInfo}) ;// 创建 State 对象State<PersonInfoPage> createState() => _PersonInfoPageState();}
当上层页面通过语句 PersonInfoPage(personInfo: PersonInfoModel("张三", "18", "男", "张家村七巷102号"),),去创建 PersonInfoPage 的时候,PersonInfoPage 中的 personInfo 会被自然而然进行赋值,并藉由 State<PersonInfoPage> createState() => _PersonInfoPageState() 的执行,自发地流入到 _PersonInfoPageState,而 _PersonInfoPageState 中就能够在生命周期函数中,通过 widget 实例访问 personInfo,也就能够借助 initState 函数对数据进行转存,从而能够在当前页面对数据进行更新。而后续的数据展示,则直接从转存后的字段中进行读取:
GestureDetector(onTap: (){_addressUpdateDialog(context, "输入地址", (value) => setState(() {_data.address = value;}));},child: Row(mainAxisAlignment: MainAxisAlignment.start,crossAxisAlignment: CrossAxisAlignment.center,children: [Text("住址:", style: TextStyle(fontSize: 20, color: Colors.black, fontWeight: FontWeight.bold),),SizedBox(width: 20,),Text(_data.address),],),
),
4、实现对话框
为了方便用户进行信息更新,应该提供一个包含输入组件的对话框,这可以借用 flutter 的 AlertDialog 组件进行实现:
void _infoUpdateDialog(BuildContext context, String label, void Function(String value) func) {final theme = Theme.of(context);final style = theme.textTheme.displayMedium!.copyWith(color: Colors.black, fontSize: 15.0);showDialog(context: context,builder: (context) {var newValue = "";return AlertDialog(backgroundColor: Colors.grey,title: Text("提示"),content: Material(child: TextField(decoration: InputDecoration(border: OutlineInputBorder(), labelText: label),onChanged: (value){newValue = value;},),),actions: [TextButton(onPressed: () {Navigator.of(context).pop();},child: Text("取消",style: style,)),TextButton(onPressed: () {func(newValue);Navigator.of(context).pop();},child: Text("确定",style: style,)),]);});}
同样的,为了方便复用该对话框,将其封装在一个私有方法中,方法参数除了必须的上下文外,还包括一个输入提示和确认按钮的处理函数,没错,flutter 所基于的 dart 语言,支持函数式编程,能够将函数作为参数传入其他函数中。
经过这四个步骤,用于展示用户信息的 PersonPage 便完成了,而其中所涉及的开发经验,足以复用来开发许多比较基础的 Flutter 应用页面。
相关文章:
学习封装Flutter组件,看这篇就够了
Flutter 的自定义组件 一、添加 UI 组件 在进行自定义组件的封装之前,应该先掌握如何在 Flutter 应用页面中添加内置组件,如按钮和文本等,以下面的页面定义为例: import package:flutter/material.dart;class SecondPage exten…...
无线麦克风方案芯片DSH32F3024
直播跑道狂飙后,与其相配套的产品链也逐渐成形。其中麦克风的发展更是随着直播的火热而直线上升。无线麦克风以其便捷性、灵活性和高质量的音频传输能力,更受大家的青睐。今天我们就来说一下无线麦克风及对它起着至关重要的主控芯片的技术特点和性能解析…...
谷粒商城の秒杀服务
文章目录 前言一、秒杀系统的设计二、缓存预热1.缓存结构设计2、上架 三、秒杀业务实现 前言 本篇基于谷粒商城的秒杀服务,介绍设计一个秒杀系统的要素,包括缓存预热、商品随机码、动静分离、消息队列削峰等。对应视频P311-P325(只介绍系统设…...
庆祝程序员节:聊一聊编程语言的演变
人不走空 🌈个人主页:人不走空 💖系列专栏:算法专题 ⏰诗词歌赋:斯是陋室,惟吾德馨 目录 🌈个人主页:人不走空 💖系列专栏:算法专题 ⏰诗词歌…...
大模型技术在网络安全领域的应用与发展
一、概述 大模型技术,尤其是深度学习和自然语言处理领域的大型预训练模型,近年来在网络安全领域得到了广泛应用。这些模型通过其强大的数据处理能力和泛化能力,为网络安全带来了新的机遇和挑战。本文将对大模型技术在网络安全领域的应用进行…...
基于vite和vue3、 eslint、prettier、stylelint、husky规范
前言 在现代的前端开发中,代码规范非常重要。它可以提高团队的协作效率,减少代码错误,使代码更易于维护。为了实现代码规范化,我们可以使用一些工具来辅助我们的开发流程,包括eslint、prettier、stylelint、husky&am…...
git push到远程怎么回退
git push到远程服务器想继续修改,你必须要回退然后在此提交。而且需要保留本地的修改文件。 下面给你一些git命令,回退很简单。 按照下面的流程操作就行: 1.查看提交历史 首先,使用git log命令查看提交历史。可以使用以下命令显…...
Web保存状态的手段(Application的使用)
Application 在Java Web开发中,ServletContext(通常称为application)是一个非常重要的接口,它代表了Web应用程序的上下文。每个Web应用都有其自己的ServletContext,当Web应用被加载到Servlet容器时创建,并…...
高翔【自动驾驶与机器人中的SLAM技术】学习笔记(十二)拓展图优化库g2o(一)框架
【转载】理解图优化,一步步带你看懂g2o框架 文章来源:理解图优化,一步步带你看懂g2o框架 小白:师兄师兄,最近我在看SLAM的优化算法,有种方法叫“图优化”,以前学习算法的时候还有一个优化方法…...
Flutter Row组件实战案例
In this section, we’ll continue our exploration by combining the Row and Container widgets to create more complex layouts. Let’s dive in! 在本节中,我们将继续探索,结合“Row”和“Container”小部件来创建更复杂的布局。让我们开始吧! Sc…...
【ubuntu20.04】【ROS Noetic】【ROS安装】【Website may be down.】【gpg: 找不到有效的 OpenPGP 数据。】
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言一、登入www.ros.org1.Setup your sources.list2.Set up your keys中间出了点问题 gpg: 找不到有效的 OpenPGP 数据。4.Installation下载安装ros5.环境参数的配…...
Python开发必备,这些黑科技库你get到了吗
大家好,今天我要为大家推荐一些非常强大和实用的Python库,相信无论是Python新手还是老司机,都能从中受益,提升你的Python开发技能。那就一起来看看吧! 1.Requests: 简单好用的HTTP请求库 第一个要介绍的是Requests库。它是Python中最流行的HTTP客户端库之一,大大简化了网络请…...
sublime text 常用快捷键
sublimetext常用快捷键 CtrlShiftP:打开命令面板 CtrlP:搜索项目中的文件 CtrlG:跳转到第几行 CtrlW:关闭当前打开文件 CtrlShiftW:关闭所有打开文件 CtrlShiftV:粘贴并格式化 CtrlD:选择单词&a…...
Kubernetes(K8S) + Harbor + Ingress 部署 SpringBoot + Vue 前后端分离项目
文章目录 1、环境准备2、搭建 K8S3、搭建 Harbor4、搭建 MySQL5、构建 SpringBoot 项目镜像6、构建 Vue.js 项目镜像7、部署项目7.1、配置 NameSpace7.2、配置 Deployment、Service7.3、配置 Ingress-Nginx7.4、访问测试 1、环境准备 本次整体项目部署使用的是阿里云ECS服务器…...
【iOS】知乎日报第一周总结
知乎日报第一周总结 文章目录 知乎日报第一周总结前言网络异步导致视图无法加载加载网络上的图片实现一个上拉刷新的效果左上角的时间初步实现了点击cell进入网页小结 前言 笔者在本周算是正式开始写项目了,本周主要是大致完成了主页的内容,大致完成了主…...
Springboot整合spring-boot-starter-data-elasticsearch
前言 <font style"color:rgb(36, 41, 47);">spring-boot-starter-data-elasticsearch</font> 是 Spring Boot 提供的一个起始依赖,旨在简化与 Elasticsearch 交互的开发过程。它集成了 Spring Data Elasticsearch,提供了一套完整…...
【大模型系列】mPLUG-Owl3(2024.08)
Paper: https://arxiv.org/pdf/2408.04840Github: https://github.com/X-PLUG/mPLUG-OwlHuggingFace:https://huggingface.co/mPLUG/mPLUG-Owl3-7B-240728Author: Jiabo Ye et al. 阿里巴巴 文章目录 0 总结(省流版)1 模型结构1.1 Cross-attention Based Achitectur…...
从0到1学习node.js(express模块)
文章目录 Express框架1、初体验express2、什么是路由3、路由的使用3、获取请求参数4、电商项目商品详情场景配置路由占位符规则5、小练习,根据id参数返回对应歌手信息6、express和原生http模块设置响应体的一些方法7、其他响应设置8、express中间件8.1、什么是中间件…...
MambaVision
核心速览 研究背景 研究问题 :这篇文章提出了一种新的混合Mamba-Transformer骨干网络,称为MambaVision,专为视 觉应用量身定制。研究的核心问题是如何有效地结合Mamba的状态空间模型(SSM)和Transf ormer的自注意力机制…...
MySQLDBA修炼之道-开发篇(二)
四、开发进阶 1. 范式和反范式 范式是数据库规范化的一个手段,是数据库设计中的一系列原理和技术,用于减少数据库中的数据冗余,并增进数据的一致性。 范式 1.1 第一范式 第一范式是指数据库表的每一列(属性)都是不可…...
多模态2025:技术路线“神仙打架”,视频生成冲上云霄
文|魏琳华 编|王一粟 一场大会,聚集了中国多模态大模型的“半壁江山”。 智源大会2025为期两天的论坛中,汇集了学界、创业公司和大厂等三方的热门选手,关于多模态的集中讨论达到了前所未有的热度。其中,…...
利用ngx_stream_return_module构建简易 TCP/UDP 响应网关
一、模块概述 ngx_stream_return_module 提供了一个极简的指令: return <value>;在收到客户端连接后,立即将 <value> 写回并关闭连接。<value> 支持内嵌文本和内置变量(如 $time_iso8601、$remote_addr 等)&a…...
Qwen3-Embedding-0.6B深度解析:多语言语义检索的轻量级利器
第一章 引言:语义表示的新时代挑战与Qwen3的破局之路 1.1 文本嵌入的核心价值与技术演进 在人工智能领域,文本嵌入技术如同连接自然语言与机器理解的“神经突触”——它将人类语言转化为计算机可计算的语义向量,支撑着搜索引擎、推荐系统、…...
《通信之道——从微积分到 5G》读书总结
第1章 绪 论 1.1 这是一本什么样的书 通信技术,说到底就是数学。 那些最基础、最本质的部分。 1.2 什么是通信 通信 发送方 接收方 承载信息的信号 解调出其中承载的信息 信息在发送方那里被加工成信号(调制) 把信息从信号中抽取出来&am…...
解决本地部署 SmolVLM2 大语言模型运行 flash-attn 报错
出现的问题 安装 flash-attn 会一直卡在 build 那一步或者运行报错 解决办法 是因为你安装的 flash-attn 版本没有对应上,所以报错,到 https://github.com/Dao-AILab/flash-attention/releases 下载对应版本,cu、torch、cp 的版本一定要对…...
python执行测试用例,allure报乱码且未成功生成报告
allure执行测试用例时显示乱码:‘allure’ �����ڲ����ⲿ���Ҳ���ǿ�&am…...
AGain DB和倍数增益的关系
我在设置一款索尼CMOS芯片时,Again增益0db变化为6DB,画面的变化只有2倍DN的增益,比如10变为20。 这与dB和线性增益的关系以及传感器处理流程有关。以下是具体原因分析: 1. dB与线性增益的换算关系 6dB对应的理论线性增益应为&…...
CSS | transition 和 transform的用处和区别
省流总结: transform用于变换/变形,transition是动画控制器 transform 用来对元素进行变形,常见的操作如下,它是立即生效的样式变形属性。 旋转 rotate(角度deg)、平移 translateX(像素px)、缩放 scale(倍数)、倾斜 skewX(角度…...
【Linux】Linux安装并配置RabbitMQ
目录 1. 安装 Erlang 2. 安装 RabbitMQ 2.1.添加 RabbitMQ 仓库 2.2.安装 RabbitMQ 3.配置 3.1.启动和管理服务 4. 访问管理界面 5.安装问题 6.修改密码 7.修改端口 7.1.找到文件 7.2.修改文件 1. 安装 Erlang 由于 RabbitMQ 是用 Erlang 编写的,需要先安…...
DAY 26 函数专题1
函数定义与参数知识点回顾:1. 函数的定义2. 变量作用域:局部变量和全局变量3. 函数的参数类型:位置参数、默认参数、不定参数4. 传递参数的手段:关键词参数5 题目1:计算圆的面积 任务: 编写一…...
