Flutter—使用網絡請求的頁面搭建流程、State生命周期、一些組件的應用
使用網絡請求的頁面搭建流程
在開發APP時,我們常常會遇到如下場景:進入一個頁面后,要先進行網絡調用,然后使用調用返回的數據進行頁面渲染。
這種頁面搭建流程大致為:調用網絡請求,獲得json格式的數據—解析獲得的數據為Dart類 — 將Dart數據傳回UI。在返回數據前,可以在頁面先放置一個加載動畫;獲得數據后,使用數據進行進行頁面重繪。
網絡請求
Flutter的網絡請求常常使用的庫有http
,dio
,其中dio
是一個強大的Dart Http請求庫,支持Restful API、FormData、攔截器、請求取消、Cookie管理、文件上傳/下載、超時、自定義適配器等。dio的詳細介紹與使用方法可以參見github-dio官方文檔。
-
在pubspec.yaml中引入包依賴
dependencies: dio: ^3.x.x // 請使用pub上3.0.0分支的最新版本
-
極簡示例
import 'package:dio/dio.dart'; void getHttp() async { try { Response response = await Dio().get("http://www.baidu.com"); print(response); } catch (e) { print(e); } }
數據解析
當調用Dio.get()
方法時,可以從網絡獲得json數據,我們可以將這些數據進行解析處理,方便UI搭建的使用。 實例如下:
//該函數從調用服務器某一個接口,獲得json數據,並將解析后的Dart model返回。
Future<Course> getCourse(String studentID) async {
Dio dio = new Dio();
Response response;
response = await dio.request(
'http://www.baidu.com',//將此替換為后端服務器地址
options: Options(method: "GET", responseType: ResponseType.plain),
);
String ss = response.data;
dynamic jsonList = json.decode(ss);
Course tempStr = new Course.fromJson(jsonList);//將json解析為Dart類,
return tempStr;
}
Course.fromJson
的實現有很多種方式,可以手寫或者自動生成。在此推薦一個json-Dart 轉換的網頁json to Dart。該網頁可以自動根據給定的json數據生成Dart類。例如對於如下格式的json數據:
{
"course_name": "計算機網絡",
"department": "計算機學院"
}
生成的Dart 類為:
class course {
String courseName;
String department;
course({this.courseName, this.department});
course.fromJson(Map<String, dynamic> json) {
courseName = json['course_name'];
department = json['department'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['course_name'] = this.courseName;
data['department'] = this.department;
return data;
}
}
FutureBuilder頁面渲染
在實現網絡請求的調用和數據解析后,我們需要把獲得的數據應用到UI的渲染中。此時就要用到``FutureBuilder`。
FutureBuilder
是一個將異步操作和異步UI更新結合在一起的類,通過它我們可以將網絡請求,數據庫讀取等的結果更新的頁面上。其構造方法如下:
FutureBuilder({
Key key, Future<T> future, //表示此構建器當前連接的異步計算。
T initialData,
@required
AsyncWidgetBuilder<T> builder //是一個基於異步交互構建widget的函數,這個函數接受兩個參數`BuildContext context`與 `AsyncSnapshot<T> snapshot`,並應該返回一個widget。其中異步計算獲得的數據就保存在`snapshot.data`中,在builder中可以通過`snapshot.data`獲得異步數據。
})
一個使用實例
//網絡調用未返回數據時,返回加載動畫;返回數據后展示返回數據的'course_name'值。
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('主題'),
),
body: FutureBuilder<CommonModel>(
future: getCourse(),//調用上面數據解析部分封裝好的網絡請求函數getCourse()為異步計算函數。
builder:
(BuildContext context, AsyncSnapshot<CommonModel> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return new Text('Input a URL to start');
case ConnectionState.waiting:
return new Center(child: new CircularProgressIndicator());
case ConnectionState.active:
return new Text('');
case ConnectionState.done:
if (snapshot.hasError) {
return new Text(
'${snapshot.error}',
style: TextStyle(color: Colors.red),
);
} else {
return Text(${snapshot.data.courese_name}$);
}
}
}),
),
);
}
State生命周期
State是構造StatefulWidget必不可少的組件。State的生命周期主要包括三個階段:創建、更新、銷毀。
-
創建
State Widget的創建流程為:
- 構造方法,接受父組件的數據,並調用createState
- initState。在這個函數中可以對State中的狀態值進行初始化。
- didChangeDependencies 。在initState后被調用,可以用來處理 State 對象依賴關系的變化。
- build。完成以上的函數后,在build方法中,將根據父 Widget 傳遞過來的初始化配置數據及 State 的當前狀態,創建一個 Widget 然后返回。
-
更新
State Widget的狀態更新可以通過三種途徑:setState、didChangeDependencies 或 didUpdateWidget 觸發。
- setState。當狀態值發生改變的時候,可以通過調用setState方法通知flutter對Widget進行重繪(build)。
- didChangeDependencies。當State依賴的數據發生改變時,Flutter 會回調該方法,隨后觸發組件構建。
- didUpdateWidget。Widget 的配置發生變化時,或熱重載時,系統會回調該方法。
-
銷毀
當 State 被永久地從視圖樹中移除時,Flutter 會調用 dispose 方法,而一旦 dispose 方法被調用,組件就要被銷毀了,因此可以在 dispose 方法中進行最終的資源釋放、移除監聽、清理環境等工作
State生命周期可以用一下圖來描述:
一些組件的應用
在出現彈窗時禁用實體返回鍵
典型場景:在進行網絡請求過程中,我們不希望用戶在中途進行其他操作。此時,我們可以使用一個過渡動畫來提示用戶目前正在進行網絡調用。
過渡動畫的實現方法有很多,最常見的是進行彈窗提醒。所以我們需要維持網絡請求過程中UI始終為彈窗頁。即要求:
- 點擊對話框以外的區域不隱藏彈窗——barrierDismissible屬性
- 點擊實體返回鍵不隱藏彈窗——在原有彈窗組件中外包裹一層WillPopScope,並設置onWillPop屬性為false。
舉例:
showDialog(
barrierDismissible: false,//barrierDismissible屬性,控制點擊對話框以外的區域是否隱藏對話框,為flase時不隱藏
context: context
builder: (context) {
return WillPopScope(//在原有的彈窗Wigdet外,嵌套WillPopScope,並設置onWillPop的返回值為false
child: Center(
Text('加載中'),
),
onWillPop: () {
return new Future.value(false);
},
);
});
點擊空白處回收鍵盤
在與鍵盤有交互的頁面,用戶常常進行的一個操作是點擊空白處回收鍵盤。實現這一功能的也很簡單,只需要在頁面的最外層包裹一層GestureDetector,並在onTap時取消鍵盤焦點。
GestureDetector用於用戶交互,進行基本的手勢控制,其基本屬性如下所示。
GestureDetector({
Key key,
this.child,
this.onTapDown,//可能導致點擊的指針已聯系到屏幕的特定位置
this.onTapUp,//觸發點的指針已停止在特定位置與屏幕聯系
this.onTap,//發生了點擊。
this.onTapCancel,//觸發onTapDown的指針取消觸發
this.onDoubleTap,//雙擊
this.onLongPress,//長按
this.onLongPressUp,//長按結束
this.onVerticalDragDown,//
this.onVerticalDragStart,//指針已經接觸到屏幕,而且可能開始垂直移動。
this.onVerticalDragUpdate,//與屏幕接觸並垂直移動的指針沿垂直方向移動
this.onVerticalDragEnd,//以前與屏幕接觸並垂直移動的指針不再與屏幕接觸,並且當其停止接觸屏幕時以特定速度移動。
this.onVerticalDragCancel,//
this.onHorizontalDragDown,//
this.onHorizontalDragStart,//
this.onHorizontalDragUpdate,//
this.onHorizontalDragEnd,//
this.onHorizontalDragCancel,//
// onPan可以取代onVerticalDrag或者onHorizontalDrag,三者不能並存
this.onPanDown,//指針已經接觸屏幕並開始移動
this.onPanStart,//與屏幕接觸並移動的指針再次移動
this.onPanUpdate,//先前與屏幕接觸並移動的指針不再與屏幕接觸,並且當它停止接觸屏幕時以特定速度移動
this.onPanEnd,//先前觸發 onPanDown 的指針未完成
this.onPanCancel,//
// onScale可以取代onVerticalDrag或者onHorizontalDrag,三者不能並存,不能與onPan並存
this.onScaleStart,//
this.onScaleUpdate,//
this.onScaleEnd,//
this.behavior,
this.excludeFromSemantics = false
})
實例:
class _CourseCommentWritePage extends State<CourseCommentWritePage> {
TextEditingController commentController = new TextEditingController();
FocusNode commentNode = new FocusNode();
Widget build(BuildContext context) {
print("build_write");
return Scaffold(
appBar: AppBar(
title: Text("標題"),
),
body: GestureDetector(
onTap: () {
commentNode.unfocus();//取消鍵盤焦點
},
child:Container(
decoration: BoxDecoration(
border: new Border.all(width: 2.0, color: Colors.black12),
borderRadius: new BorderRadius.all(new Radius.circular(10.0)),
),
child: TextField(
enabled: _enable,
maxLines: 12,
focusNode: commentNode,
controller: commentController,
decoration: InputDecoration(
hintText: '請輸入你對課程的評價',
border: InputBorder.none,
),
),
),
)
}
在彈出鍵盤時,防止頁面溢出
在我們實際的項目開發中,經常會遇到頁面UI內容過多,導致手機一屏展示不完的情況出現,在Flutter中我們通常使用SingleChildScrollView
處理滑動,即使用SingleChildScrollView
組件將普通組件包裹。
同樣,可以使用SingleChildScrollView
中將表單TextFiled
包裹,以防止溢出鍵盤彈出導致的溢出。
SingleChildScrollView
組件:
const SingleChildScrollView({
Key key,//滾動方向,默認是垂直方向
this.scrollDirection = Axis.vertical,//是否按照閱讀方向相反的方向滑動
this.reverse = false,//內容邊距
this.padding,//是否使用widget樹中默認的PrimaryScrollController
bool primary,//此屬性接受一個ScrollPhysics類型的對象,它決定可以滾動如何響應用戶操作,比如用戶滑動完抬起手指后,繼續執行動畫,或者滑動到邊界時,如何顯示。
this.controller,
this.child,
})
在特定條件下禁用RaiseButton
RaiseButton是一個按鈕組件,其一些屬性作用如下:
const RaisedButton({
Key key,
@required VoidCallback onPressed,//被點擊時的回調函數
ValueChanged<bool> onHighlightChanged,//水波紋高亮變化回調,按下返回true,抬起返回false
ButtonTextTheme textTheme,//按鈕的主題
Color textColor,//文字的顏色
Color disabledTextColor,//按鈕禁用時候文字的顏色
Color color,//按鈕的背景顏色
Color disabledColor,//按鈕被禁用的時候顯示的顏色
Color highlightColor,//點擊或者toch控件高亮的時候顯示在控件上面,水波紋下面的顏色
Color splashColor,//水波紋的顏色
Brightness colorBrightness,//按鈕主題高亮
double elevation,//按鈕下面的陰影
double highlightElevation,//高亮時候的陰影
double disabledElevation,//按下的時候的陰影
EdgeInsetsGeometry padding,
ShapeBorder shape,//設置形狀
Clip clipBehavior = Clip.none,
MaterialTapTargetSize materialTapTargetSize,
Duration animationDuration,
Widget child,
})
disabledColor
字段用於設置按鈕被禁用時顯示的顏色。但是可以發現其實並沒有將按鈕設為禁用的字段。查找資料發現,當onpress()的返回值為null時,按鈕自動被設為禁用。
例如:
class MyPage extennds StatelessWidget(){
bool _enable = true;
Widget build(BuildContext context){
return RaisedButton(
child: Text("修改"),
color: Colors.lightBlue,
disabledColor: Colors.grey,
onPressed:
_enable ? () => setTextEnable() :null,//當_enable為false時,禁用按鈕
),
}
void setTextEnable() {
setState(() {
_enable = !_enable;
});
}
}
//值得注意的是,不能將onpress的回調函數寫為以下形式,寫成這樣並不能在當_enable為false時,禁用按鈕
onpress:() {
if(_enable){
setTextEnable()
}else{
return null;
}
}