作業(yè)做哪些類型的網(wǎng)站查權(quán)重網(wǎng)站
Flutter 的自定義組件
一、添加 UI 組件
在進(jìn)行自定義組件的封裝之前,應(yīng)該先掌握如何在 Flutter 應(yīng)用頁(yè)面中添加內(nèi)置組件,如按鈕和文本等,以下面的頁(yè)面定義為例:
import 'package:flutter/material.dart';class SecondPage extends StatelessWidget { Widget build(BuildContext context) {return Text("Hello Word!");}
}
由于 dart 語(yǔ)法規(guī)定了方法返回值只能一個(gè),所以無(wú)法通過(guò)簡(jiǎn)單地返回多個(gè) Text 組件,去完成頁(yè)面內(nèi)容的增加;而作為 UI 組件,也不支持像字符串那樣通過(guò)操作符 + 進(jìn)行內(nèi)容的拼接,因此,想要上述代碼對(duì)應(yīng)的頁(yè)面具有更多的內(nèi)容,可行的方法就是使用容器類組件作為最終的返回值,例如下面:
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("文本二"),],);}
}
然而,即便是容器組件,也不意味著就能夠一次添加多個(gè)UI元素,因?yàn)橄?Container 這種容器組件,只能聲明定義一個(gè)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"),],),);}
}
仔細(xì)對(duì)比 Container 組件和 Column 組件與 Row 組件的屬性,就不難發(fā)現(xiàn)可以通過(guò)屬性名稱,去判斷一個(gè)容器組件是支持多元素定義、還是單一元素定義。
對(duì)于像 Column、Row 和 ListView 等支持多元素定義的容器組件,都是用 children 作為屬性名,而對(duì)于像 Container、Padding 和 Center 等支持單一元素定義的容器組件,則是用 child 作為屬性名。
二、定義布局約束
光知道用容器組件去豐富頁(yè)面內(nèi)容,是不足以形成自定義組件封裝能力的,因?yàn)楹每吹膽?yīng)用頁(yè)面,還必須具有位置合理、排列整齊等特點(diǎn),而這些就需要布局約束
去實(shí)現(xiàn)。
同其他UI框架一樣,Flutter中的布局約束,也是分為相對(duì)布局和絕對(duì)布局。
1、絕對(duì)布局
Flutter 中,使用 絕對(duì)布局可以通過(guò)結(jié)合 Stack
組件和 Positioned
組件來(lái)實(shí)現(xiàn),例如下面:
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')),),),],),);}
}
子所以如此,是因?yàn)?Flutter 基礎(chǔ)組件中并沒(méi)有提供 position 屬性去設(shè)置組件的位置。Positioned 組件可以通過(guò)設(shè)置四個(gè)方向上與父組件邊界的距離,完成對(duì)自身位置的絕對(duì)定位。
2、相對(duì)布局
在 flutter 提供的內(nèi)置組件中,除了 Positioned 組件外,都是采用相對(duì)布局的,即通過(guò)設(shè)置子組件對(duì)齊方式、或內(nèi)外邊距,實(shí)現(xiàn)對(duì)子組件的位置約束,例如下面:
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 屬性設(shè)置外邊距、padding 屬性設(shè)置內(nèi)邊距,從而避免頁(yè)面上的按鈕或文本的位置,太靠邊;而 Column 和 Row 等容器組件,都能夠通過(guò) mainAxisAlignment 屬性和 crossAxisAlignment 屬性,分別設(shè)置主軸方向的對(duì)齊方式和交叉軸方向的對(duì)齊方式,對(duì)于 Column 來(lái)說(shuō),主軸方向就是垂直方向,而交叉軸方向就是水平方向,而對(duì)齊方式主要有如下幾種:
1)start:對(duì)于 Column 就是居上對(duì)齊,從頂部開(kāi)始往下排列子組件;對(duì)于 Row 組件來(lái)說(shuō),就是居左對(duì)齊,從左邊開(kāi)始往右邊排列子組件;
2)center:居中對(duì)齊,子組件會(huì)從中間往兩邊排列
3)end:對(duì)于 Column 就是從下往上排列子組件,對(duì)于 Row 就是從右往左排列子組件
4)spaceBetween:兩端對(duì)齊,此方式會(huì)根據(jù)子組件的數(shù)量形成不同的布局結(jié)果,但子組件為單個(gè)時(shí),會(huì)將子組件放在開(kāi)始處,即頂部或右邊;當(dāng)子組件為兩個(gè),則分別放在兩端;當(dāng)子組件為三個(gè),則分別放在兩邊和中間……,總之,會(huì)隨著子組件數(shù)量的增加,進(jìn)行組件間距的調(diào)整,保證平均整個(gè)排列方向。
5)spaceEvenly:也是兩端對(duì)齊,但與 spaceBetween 的區(qū)別在于,子組件為一是居中放置,子組件為二則兩端放置,并且與左右都與邊界保持距離,該距離與組件間距是一樣的。
6)spaceAround:兩端對(duì)齊,與 spaceEvenly 類似,但兩端與邊界的距離,是組件間距的一半。
在 flutter 中使用相對(duì)布局時(shí),需要注意一點(diǎn),就是并非所有的容器組件,都支持設(shè)置外邊距、內(nèi)邊距,例如上面的 Column 組件。而對(duì)于 Text 組件,是不能像 HTML 或其他UI框架中那樣,設(shè)置自身的內(nèi)外邊距。
3、SizedBox 填充
flutter 的容器組件,沒(méi)有提供 space 屬性去設(shè)置子組件間的間距,因此,在不使用兩端對(duì)齊的布局方式,且不想用 Container 組件去設(shè)置內(nèi)外邊距時(shí),就需要使用 SizeBox 進(jìn)行空白填充,例如下面:
Text("Hello World",style: style,
),
SizedBox(height: 20.0,
),
GestureDetector(onTap: () {print("文本被點(diǎn)擊了!");_showDialog(context);},child: Text("可點(diǎn)擊文本",style: style,),
),
在 Column 中,SizedBox 通常只需設(shè)置height屬性即可,同理,在 Row 中只需設(shè)置 width 屬性。
合理利用 SizedBox 也能夠完成對(duì)子組件的排列,并且相對(duì)兩端對(duì)齊來(lái)說(shuō),這種方式的自由度會(huì)更大一些,因?yàn)槟切┙M件間的間隔大一些、那些組件間的間隔小一些,都能夠進(jìn)行控制了。
三、定義組件樣式
布局只是起到讓頁(yè)面內(nèi)容結(jié)構(gòu)化,秩序井然,然而,這還不足以編寫(xiě)出好看的應(yīng)用頁(yè)面,因?yàn)镕lutter組件的默認(rèn)樣式,往往都比較簡(jiǎn)陋,要么是尺寸大小不合適,要么就是顏色對(duì)比不協(xié)調(diào),這些顯然都需要進(jìn)行調(diào)整。
在組件的諸多樣式屬性中,最基本的就是尺寸和顏色,而顏色又可以細(xì)分為背景色和前景色,而這兩種樣式是 flutter 內(nèi)置組件都有的,屬于公共樣式屬性;而一些組件,除了公共樣式屬性外,還具有一些自己才有的特殊樣式屬性,例如 Text 組件的下劃線樣式、字間距等。那么,這些組件樣式如何定義呢?
1、定義尺寸
Flutter 中,不是所有的組件都支持尺寸的設(shè)置,而要看一個(gè)組件是否支持尺寸設(shè)置,最簡(jiǎn)單的方式,就是看起屬性列表中是否存在 width 和 height 這對(duì)屬性,例如 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,})
因?yàn)橛羞@對(duì)屬性,所以可以無(wú)關(guān)子組件地設(shè)置自身尺寸,而對(duì)于 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,);
}
由于沒(méi)有width屬性和height屬性,所以無(wú)法定義自身的尺寸,只能根據(jù)子組件內(nèi)容來(lái)確定大小。同時(shí),也不難發(fā)現(xiàn),用于定義邊框樣式的 decoration 也不是所有組件都支持的。
一個(gè)寬度為屏幕寬度的99%、高度與屏幕高度相等,且?guī)в羞吙蝾伾?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());
對(duì)于像按鈕、文本這類組件,它們的尺寸是通過(guò) style
屬性進(jìn)行定義的,定義的時(shí)候有兩種方式,一種是內(nèi)聯(lián)方式,一種是變量傳入方式;對(duì)于內(nèi)聯(lián)方式,通常如下:
Text("Hello World",style: TextStyle(color: Colors.blue,fontSize: 25.0,fontWeight: FontWeight.w100,letterSpacing: 2.5),
),
而對(duì)于變量傳入方式,則是先 build 函數(shù)的 return 語(yǔ)句之前,定義一個(gè) final 變量,例如下面的黑色按鈕的定義:
final btnStyle = ButtonStyle(fixedSize: WidgetStateProperty.all(Size(100, 40)),backgroundColor: WidgetStateProperty.all(Colors.black),
);
然后,在需要使用該樣式的按鈕中,用 btnStyle 作為 style 屬性的值即可:
ElevatedButton(onPressed: () {print("按鈕被點(diǎn)擊");},style: btnStyle,child: Text("按鈕", style: TextStyle(color: Colors.white)),
)
2、定義邊框
使用 BoxDecoration 對(duì)象定義邊框顏色的時(shí)候,需要注意的是,只有當(dāng)邊框顏色統(tǒng)一時(shí),才能進(jìn)行邊框圓角即 borderRadius 屬性的設(shè)置,對(duì)于邊框顏色不一致的,設(shè)置 borderRadius 將會(huì)導(dǎo)致編譯失敗。當(dāng)需要設(shè)置不同的邊框顏色時(shí),可以用如下代碼實(shí)現(xiàn):
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),
),
另外,還需要注意的地方,就是當(dāng) Container 組件使用 decoration 屬性后,color 屬性就不要使用,否則會(huì)編譯失敗。
3、定義顏色
比起定義邊框顏色,背景色這種顏色定義,在 UI 實(shí)現(xiàn)過(guò)程中,反而更為常見(jiàn)。在 Flutter 中,容器組件的背景色,通常用 color 屬性進(jìn)行定義。
由于容器組件本身沒(méi)有實(shí)際內(nèi)容,所以就不存在背景色和前景色的嚴(yán)格區(qū)分,使用 color 作為背景色的屬性名,也是能夠理解的;而像按鈕這種存在實(shí)際內(nèi)容的,就需要用 backgroundColor 屬性和 foregroundColor 屬性進(jìn)行嚴(yán)格區(qū)分
然而,就像尺寸一樣,flutter 并不支持所有的容器類組件進(jìn)行背景色的定義。
4、定義 Style
非容器類的文本、按鈕等組件,它們的顏色定義同尺寸定義一起放在 Style 對(duì)象中。創(chuàng)建 Style 對(duì)象的時(shí)候,既可以用 ButtonStyle、TextStyle 這種具體的 Style 類去創(chuàng)建,還可以從應(yīng)用主題中進(jìn)行復(fù)制:
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,
);
主題中的樣式信息,來(lái)自于根組件 MaterialApp 中的 theme 屬性值 ThemeData 的定義情況,Flutter 提供的默認(rèn)主題中,并不包含 elevatedButtonTheme,因此,如果像用類似上面的代碼,從應(yīng)用上下文的主題數(shù)據(jù)中復(fù)制 elevatedButtonTheme 中的按鈕樣式,那么就應(yīng)該在最開(kāi)始的根組件中定義好 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(),},),);}
}
之后如下的代碼才不會(huì)導(dǎo)致運(yùn)行時(shí)錯(cuò)誤:
final btnTheme = theme.elevatedButtonTheme.style!.copyWith(fixedSize: WidgetStateProperty.all(Size(200, 50))
);
這種從主題中復(fù)制樣式的做法,通常適用于需要基于公共樣式進(jìn)行微調(diào)的場(chǎng)景,例如這里的調(diào)整按鈕的大小。
四、定義組件交互
一個(gè)應(yīng)用,如果只是好看,卻無(wú)法對(duì)用戶的操作進(jìn)行響應(yīng),那么就跟PPT沒(méi)有什么區(qū)別,所以,定義組件交互式封裝自定義組件,所不能或缺的能力,而在Flutter中,也是用事件處理函數(shù)來(lái)實(shí)現(xiàn)交互的,例如,每種按鈕組件都必須傳入一個(gè) onPressed
事件處理函數(shù),去完成用戶按下按鈕后的處理,再比如可選中文本可以傳入一個(gè) onTap
函數(shù),再比如 TextField 組件可以用 onChange
函數(shù)去讀取用戶的輸入數(shù)據(jù),以及各種對(duì)話框的彈出與關(guān)閉。
那么現(xiàn)在,從簡(jiǎn)單到復(fù)雜,看看如何實(shí)現(xiàn)這些交互處理函數(shù)。
1、按鈕響應(yīng)
對(duì)于按鈕的響應(yīng)處理函數(shù) onPressed 的實(shí)現(xiàn),可以根據(jù)處理流程的多少,以匿名函數(shù)
或普通函數(shù)
的形式實(shí)現(xiàn)。
普通函數(shù)分為類成員函數(shù)和公共方法兩種,大多時(shí)候,都采用類成員函數(shù)的形式,因?yàn)檫@樣可以輕松地進(jìn)行狀態(tài)聯(lián)動(dòng);當(dāng)按鈕觸發(fā)的功能不涉及UI狀態(tài),只是存儲(chǔ)的數(shù)據(jù)獲取和寫(xiě)入時(shí),使用公共方法更為恰當(dāng),比如從互聯(lián)網(wǎng)上請(qǐng)求數(shù)據(jù)的功能。而匿名函數(shù),也分為兩種,一種是箭頭函數(shù)
,另一種則是只有函數(shù)體的形式。
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("對(duì)話框"),
),
ElevatedButton(onPressed: _goBack,style: btnTheme,child: Text("返回"),
)
如上所示,用普通函數(shù)和箭頭函數(shù)的方式,可以讓代碼更為簡(jiǎn)潔,所以,強(qiáng)烈建議當(dāng)按鈕的功能處理,只需一句代碼就能實(shí)現(xiàn)時(shí),應(yīng)該采用箭頭函數(shù)的形式。
2、文本響應(yīng)
在 Flutter 中,文本組件 Text 是不具有響應(yīng)能力的,因?yàn)樗鼪](méi)有提供任何事件處理函數(shù):
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, }
)
但在應(yīng)用使用場(chǎng)景中,點(diǎn)擊文本的需求又是存在的,如此一來(lái),也必然需要實(shí)現(xiàn)這種交互,那么基于 Flutter 提供的能力,該如何實(shí)現(xiàn)這種通過(guò)文本觸發(fā)的交互呢?
對(duì)于點(diǎn)擊事件來(lái)說(shuō),例如一個(gè)顯示http連接的文本,要讓它可以響應(yīng)用戶的點(diǎn)擊,一種方式就是使用TextButton
來(lái)顯示文本,如下:
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 事件進(jìn)行交互響應(yīng);而另一種方式,就是在 Text 組件的外表包裹一個(gè) GestureDetector
組件,例如下面:
GestureDetector(onTap: (){String msg = "點(diǎn)擊了文本 ‘Hello World’";_showDialog(context, msg);},child: Text("Hello World",style: TextStyle(color: Colors.blue,fontSize: 25.0,fontWeight: FontWeight.w100,letterSpacing: 2.5),),
),
從而借助手勢(shì)捕獲處理函數(shù) onTap 去實(shí)現(xiàn)交互響應(yīng)。而對(duì)于選擇文本這種交互,可以使用 Flutter 提供的現(xiàn)成組件 SelectableText
:
SelectableText("可選擇文本",style: style,onTap: () {print("SelectableText被點(diǎn)擊");_controller.text = "可選擇文本";},
),
這個(gè)組件自帶有彈出菜單,即選中文本后可以進(jìn)行復(fù)制等操作。
3、輸入響應(yīng)
而文本響應(yīng)中,最為關(guān)鍵的恐怕還要數(shù)文本輸入框的響應(yīng),因?yàn)楹芏鄶?shù)據(jù)都是依賴用戶在文本輸入框進(jìn)行輸入才能獲取到,比如用戶的姓名等身份信息。在 Flutter 中,無(wú)論是多行文本的輸入,還是單行文本的輸入,通通用 TextField
實(shí)現(xiàn),并沒(méi)有像其他 UI 框架那樣,同時(shí)提供針對(duì)單行文本輸入的 TextInput、和多行文本輸入的 TextArea,也與其他 UI 框架有所區(qū)別的是,TextField 不能直接放置在容器組件中,必須包裹一層 Material 組件,如下:
Material(child: TextField(controller: _controller,//maxLines: 5,decoration: InputDecoration(border: OutlineInputBorder(), labelText: "Input"),onChanged: (value){if(value.isNotEmpty){setState(() {_text = value;});}},),
),
TextField 默認(rèn)樣式是單行文本,想要支持多行輸入,可以為 maxLine 屬性賦值。此外,Flutter 還提供了一個(gè) TextFormField
用于構(gòu)建登錄界面等輸入場(chǎng)景:
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 相對(duì)于 TextField 來(lái)說(shuō),多了一個(gè)用于數(shù)據(jù)校驗(yàn)的 validator
函數(shù)。
五、定義數(shù)據(jù)流
幾乎所有的手機(jī)應(yīng)用,頁(yè)面上的內(nèi)容,除了一些提示文本外,全都上由數(shù)據(jù)流注入的,換句話說(shuō),在實(shí)際開(kāi)發(fā)中,頁(yè)面內(nèi)容往往不是硬編碼實(shí)現(xiàn)的,而是動(dòng)態(tài)生成的,這也意味著,封裝自定義組件,就必須知道如何給組件定義數(shù)據(jù)流。
1、借助頁(yè)面級(jí)狀態(tài)構(gòu)建數(shù)據(jù)流
應(yīng)該感到輕松的是,Flutter 不僅是事件驅(qū)動(dòng)的,同時(shí)也是數(shù)據(jù)驅(qū)動(dòng)的,利用 Flutter 提供的狀態(tài)機(jī)制,我們可以很輕松地為組件集成數(shù)據(jù)流。換句話說(shuō),只要我們?cè)诼暶鞫x組件的時(shí)候,記得將其聲明為帶狀態(tài)的組件,那么就很容易借助狀態(tài)機(jī)制去從父組件獲取數(shù)據(jù),例如下面
class TestPage extends StatefulWidget {final String data;const TestPage({super.key, required this.data}) ; State<TestPage> createState() => _TestPageState();
}
TestPage 中定義的 data,可以在 _TestPageState 中的 build 方法中,使用 widget.data
去獲取,而當(dāng)我們需要對(duì)外部傳入的數(shù)據(jù)進(jìn)行非全局的更新,那么可以在 _TestPageState 中定義一個(gè)字段去轉(zhuǎn)存,例如下面:
class _TestPageState extends State<TestPage> {String _tempText = ""; Widget build(BuildContext context) {_tempText = widget.data;// ....Text("外部傳入的數(shù)據(jù):${_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、借助應(yīng)用級(jí)狀態(tài)構(gòu)建數(shù)據(jù)流
而如果想數(shù)據(jù)更新后,影響的不止當(dāng)前頁(yè)面,而是App內(nèi)使用到同一項(xiàng)數(shù)據(jù)的所有 APP 頁(yè)面,那么就要使用全局狀態(tài)來(lái)構(gòu)建數(shù)據(jù)流,例如在如下的一個(gè)全局狀態(tài)類中定義一個(gè) globalText 和相應(yīng)的更新方法:
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 = "全局狀態(tài)中的text";void updateGlobalText(String text){globalText = text;notifyListeners();}}
那么,其他頁(yè)面中,只需在 build 函數(shù)中使用語(yǔ)句 var appState = context.watch<MyAppState>();
,去持有全局狀態(tài)句柄,就很容易在后續(xù)代碼中,去訪問(wèn)宿主在全局狀態(tài)中的相應(yīng)數(shù)據(jù)流:
class _TestPageState extends State<TestPage> { Widget build(BuildContext context) {var appState = context.watch<MyAppState>();// ....Text("來(lái)自全局狀態(tài)的數(shù)據(jù):${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),});}},),),}}
六、封裝自定義組件
當(dāng)懂得如何進(jìn)行組件的增加、布局約束、樣式定義和響應(yīng)交互,以及能夠?yàn)榻M件定義數(shù)據(jù)流,那么便能夠在 flutter 應(yīng)用開(kāi)發(fā)中,實(shí)現(xiàn)自定義組件的封裝了。
以展示用戶信息的頁(yè)面為例,封裝成可復(fù)用的自定義組件,具體要如何編寫(xiě)呢?答案是,可以按照如下的思路進(jìn)行頁(yè)面設(shè)計(jì)。
1、確定布局約束
用戶信息,通常不會(huì)只有一條,比較常見(jiàn)的就有姓名、性別、年齡和住址等,所以,在布局方面必然要以支持多子組件的Column容器或ListView為主,考慮到要進(jìn)行頁(yè)面背景色和內(nèi)容邊距的調(diào)整,可以用 Container 作為最外層的容器,而具體的用戶信息展示,肯定是以信息說(shuō)明、信息內(nèi)容共處一行的形式進(jìn)行展示,所以可以用一個(gè) Row 容器進(jìn)行容納,為了讓信息說(shuō)明文本和信息內(nèi)容文本,不會(huì)挨得很近、影響閱讀,每個(gè) Row 容器中間都需要放置一個(gè)寬度合適的 SizedBox,當(dāng)然了,用于展示用戶信息的每個(gè) Row 容器之間,也應(yīng)該存在高度合適的 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、了解生命周期
由于,數(shù)據(jù)的獲取,常常借助組件的聲明周期函數(shù)來(lái)完成,因此,需要了解 flutter 組件的生命周期函數(shù),都有哪些。
對(duì)于無(wú)狀態(tài)的組件(繼承StatelessWidget)來(lái)說(shuō),它的生命周期函數(shù)就只有 build 函數(shù),而對(duì)于繼承自 StatefulWidget 的有狀態(tài)的組件來(lái)說(shuō),生命周期函數(shù)有如下:
createState
:用于創(chuàng)建State對(duì)象。initState
:初始化State對(duì)象。didChangeDependencies
:當(dāng)依賴項(xiàng)發(fā)生變化時(shí)調(diào)用。build
:構(gòu)建組件。deactivate
:組件被移除屏幕但未銷(xiāo)毀時(shí)調(diào)用。dispose
:組件被銷(xiāo)毀時(shí)調(diào)用?。
而對(duì)于應(yīng)用級(jí)的狀態(tài),主要有如下幾個(gè):
- ?Resumed?:應(yīng)用進(jìn)入前臺(tái)?。
- ?Paused?:應(yīng)用進(jìn)入后臺(tái)?。
- ?Inactive?:應(yīng)用進(jìn)入非活動(dòng)狀態(tài)?。
- ?Detached?:應(yīng)用在運(yùn)行但與組件分離?。
要監(jiān)聽(tīng)?wèi)?yīng)用生命周期狀態(tài)的變化,可以在繼承 WidgetsBindingObserver
后,用如下代碼進(jìn)行監(jiān)聽(tīng):
class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver {void initState() {super.initState();WidgetsBinding.instance.addObserver(this);}void didChangeAppLifecycleState(AppLifecycleState state) {super.didChangeAppLifecycleState(state);print("當(dāng)前的應(yīng)用生命周期狀態(tài):$state");if(state == AppLifecycleState.paused) {print("應(yīng)用進(jìn)入后臺(tái)");} else if(state == AppLifecycleState.resumed) {print("應(yīng)用進(jìn)入前臺(tái)");} else if(state == AppLifecycleState.inactive) {print("應(yīng)用處于非活躍狀態(tài)");} else if(state == AppLifecycleState.detached) {print("應(yīng)用處于分離狀態(tài)");}}void dispose() {super.dispose();WidgetsBinding.instance.removeObserver(this);}
}
一般來(lái)說(shuō),只有全局使用的數(shù)據(jù),才需要結(jié)合應(yīng)用生命周期狀態(tài)監(jiān)聽(tīng)去操作,而對(duì)于普通的非全局?jǐn)?shù)據(jù),通常只需在頁(yè)面級(jí)的生命周期函數(shù)中進(jìn)行操作即可,例如,在 initState 中對(duì)某個(gè)私有字段進(jìn)行初始化:
void initState() {super.initState();_data = widget.personInfo;
}
3、構(gòu)造數(shù)據(jù)流
頁(yè)面展示的內(nèi)容數(shù)據(jù),有的是在 initState 函數(shù)中,從服務(wù)端拉取的;有的只是對(duì)來(lái)自父組件的數(shù)據(jù)進(jìn)行解析轉(zhuǎn)存。這里以父組件透?jìng)鳛槔?#xff0c;進(jìn)行數(shù)據(jù)流的構(gòu)造。
作為當(dāng)前頁(yè)面的數(shù)據(jù)流源頭,需要在繼承了 StatefulWidget 的組件類中,定義一個(gè)數(shù)據(jù)字段,并在構(gòu)造方法中聲明為必傳參數(shù):
class PersonInfoPage extends StatefulWidget {final PersonInfoModel personInfo;const PersonInfoPage({super.key, required this.personInfo}) ;// 創(chuàng)建 State 對(duì)象 State<PersonInfoPage> createState() => _PersonInfoPageState();}
當(dāng)上層頁(yè)面通過(guò)語(yǔ)句 PersonInfoPage(personInfo: PersonInfoModel("張三", "18", "男", "張家村七巷102號(hào)"),)
,去創(chuàng)建 PersonInfoPage 的時(shí)候,PersonInfoPage 中的 personInfo 會(huì)被自然而然進(jìn)行賦值,并藉由 State<PersonInfoPage> createState() => _PersonInfoPageState()
的執(zhí)行,自發(fā)地流入到 _PersonInfoPageState
,而 _PersonInfoPageState 中就能夠在生命周期函數(shù)中,通過(guò) widget 實(shí)例訪問(wèn) personInfo,也就能夠借助 initState 函數(shù)對(duì)數(shù)據(jù)進(jìn)行轉(zhuǎn)存,從而能夠在當(dāng)前頁(yè)面對(duì)數(shù)據(jù)進(jìn)行更新。而后續(xù)的數(shù)據(jù)展示,則直接從轉(zhuǎn)存后的字段中進(jìn)行讀取:
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、實(shí)現(xiàn)對(duì)話框
為了方便用戶進(jìn)行信息更新,應(yīng)該提供一個(gè)包含輸入組件的對(duì)話框,這可以借用 flutter 的 AlertDialog 組件進(jìn)行實(shí)現(xiàn):
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,)),]);});}
同樣的,為了方便復(fù)用該對(duì)話框,將其封裝在一個(gè)私有方法中,方法參數(shù)除了必須的上下文外,還包括一個(gè)輸入提示和確認(rèn)按鈕的處理函數(shù),沒(méi)錯(cuò),flutter 所基于的 dart 語(yǔ)言,支持函數(shù)式編程
,能夠?qū)⒑瘮?shù)作為參數(shù)傳入其他函數(shù)中。
經(jīng)過(guò)這四個(gè)步驟,用于展示用戶信息的 PersonPage 便完成了,而其中所涉及的開(kāi)發(fā)經(jīng)驗(yàn),足以復(fù)用來(lái)開(kāi)發(fā)許多比較基礎(chǔ)的 Flutter 應(yīng)用頁(yè)面。