下拉菜單欄實現(xiàn)

Widget _buildPopupMenuItem(String imgAss, String title) {
return Row(
children: [
Image(image: AssetImage(imgAss), width: 20,),
SizedBox(width: 20,),
Text(title, style: TextStyle(color: Colors.white),),
],
);
}
Container(
margin: EdgeInsets.only(right: 10),
child: PopupMenuButton(
offset: Offset(0, 50.0),
color: Color.fromRGBO(1, 1, 1, 0.65),
child: Image(image: AssetImage('images/圓加.png'), width: 25,),
itemBuilder: (BuildContext context) {
return <PopupMenuItem<String>>[
PopupMenuItem(child: _buildPopupMenuItem('images/發(fā)起群聊.png', '發(fā)起群聊')),
PopupMenuItem(child: _buildPopupMenuItem('images/添加朋友.png', '添加朋友')),
PopupMenuItem(child: _buildPopupMenuItem('images/掃一掃1.png', '掃一掃')),
PopupMenuItem(child: _buildPopupMenuItem('images/收付款.png', '收付款')),
];
},
),
)
如圖所示,實現(xiàn)這種菜單欄我們可以使用 Flutter 提供的部件 PopupMenuButton 來實現(xiàn),itemBuilder 屬性是一個 PopupMenuItem 類型的數(shù)組,這里我們抽取了 _buildPopupMenuItem 方法來創(chuàng)建 item,最后用 PopupMenuItem 部件包裝 _buildPopupMenuItem 方法的返回值。
json 轉(zhuǎn)模型
class ChatModel {
final String? name;
final String? message;
final String? imageUrl;
ChatModel({this.name, this.message, this.imageUrl});
//工廠構(gòu)造方法
factory ChatModel.fromMap(Map map) {
return ChatModel(
name: map['name'],
message: map['message'],
imageUrl: map['imageUrl'],
);
}
}
final chatMap = {
'name' : 'ChenXi',
'message' : 'Hello!',
};
//Map 轉(zhuǎn) json
final chatJson = json.encode(chatMap);
print(chatJson);
// json 轉(zhuǎn) Map
final newChatMap = json.decode(chatJson);
print(chatJson);
final chatModel = ChatModel.fromMap(newChatMap as Map);
print(chatModel);
這里我們簡單定義了一個 map 對象,代碼示例中給出里 json 與 Map 的相互轉(zhuǎn)換,及 Map 轉(zhuǎn)模型。我們定義了一個 ChatModel 的模型,添加了 fromMap 方法,由外部傳入一個 Map 類型的對象。開源的也有一些轉(zhuǎn)模型的框架,這里我們先自己實現(xiàn)。
Future 使用
void initState() {
super.initState();
//獲取網(wǎng)絡(luò)數(shù)據(jù)
_getDatas().then((value) {
print('$value');
});
}
Future<List<ChatModel>> _getDatas() async {
//url 鏈接
final url = Uri.parse('http://rap2api.taobao.org/app/mock/294394/api/chat/list');
//發(fā)送請求
final response = await http.get(url);
if (response.statusCode == 200) {
//獲取響應(yīng)數(shù)據(jù),并且把 json 轉(zhuǎn)成 Map
final bodyMap = json.decode(response.body);
// 取出 bodyMap 中的 chat_list 數(shù)組,通過 map 方法進行遍歷并轉(zhuǎn)為模型,通過 toList 返回一個模型數(shù)組
final chatList = (bodyMap['chat_list'] as List).map((item) => ChatModel.fromMap(item)).toList();
return chatList;
} else {
throw Exception('statusCode:${response.statusCode}');
}
這里 Future 代表未來的數(shù)據(jù),我們對返回的數(shù)據(jù)通過 Future 包裝成 Future<List<ChatModel>> ,Future 有一個 then 方法,then 有一個外部傳入一個閉包屬性,當數(shù)據(jù)請求完成會調(diào)用閉包,這里我們可以拿到 value 的值,也就是模型數(shù)組。
FutureBuilder 異步渲染

body: Container(
child: FutureBuilder(
future: _getDatas(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
//正在加載
if (snapshot.connectionState == ConnectionState.waiting) {
return Container(
child: Text('正在加載!'),
);
}
//加載完成
return ListView(
children: snapshot.data.map<Widget>((ChatModel item) {
return ListTile(
title: Text(item.name as String),
subtitle: Container(
alignment: Alignment.bottomCenter,
height: 25,
child: Text(item.message as String, overflow: TextOverflow.ellipsis,),
),
leading: Container(
width: 44,
height: 44,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6.0),
image: DecorationImage(image: NetworkImage(item.imageUrl as String)),
),
),
);
}).toList(),
);
},
)
)
這里我們通過 FutureBuilder 部件實現(xiàn)網(wǎng)絡(luò)數(shù)據(jù)的加載,FutureBuilder 部件支持異步渲染,future 屬性是 Future 類型的數(shù)據(jù)。在每次進入微信頁面的時候 builder 方法最少會被調(diào)用兩次,沒有數(shù)據(jù)的時候會調(diào)用一次,數(shù)據(jù)來了之后又會調(diào)用一次。ConnectionState.waiting 代表數(shù)據(jù)正在加載,在這里我們可以做一些空頁面展示的處理。ConnectionState.done 代表數(shù)據(jù)加載完成,snapshot.data 就是 _getDatas 方法返回的列表數(shù)據(jù),這里可以進行相關(guān)邏輯的處理, 這里我們展示聊天列表數(shù)據(jù)。ListView 中我們用 ListTile 部件來作為 cell,ListTile 包含主標題 title、副標題 subtitle、頭像 leading 等屬性,用起來很方便。
網(wǎng)絡(luò)請求數(shù)據(jù)處理
//模型數(shù)組
List<ChatModel> _datas = [];
void initState() {
super.initState();
//獲取網(wǎng)絡(luò)數(shù)據(jù)
_getDatas().then((value) {
if (!_cancelConnect) {
setState(() {
_datas = value;
});
}
}).catchError((e) {
_cancelConnect = true;
//獲取數(shù)據(jù)失敗
print(e);
}).whenComplete(() {
print('數(shù)據(jù)請求結(jié)束');
}).timeout(Duration(seconds: 5)).catchError((timeout) {
_cancelConnect = true;
print('請求超時 ! $timeout');
});
}
這里我們創(chuàng)建了一個外部成員變量 _datas,用來保存網(wǎng)絡(luò)請求的數(shù)據(jù),網(wǎng)絡(luò)請求成功之后調(diào)用 setState 方法,定義了一個屬性 _cancelConnect 標識網(wǎng)絡(luò)請求是否取消。catchError 代表請求失敗,whenComplete 代表請求結(jié)束,timeout 可以設(shè)置超時時間,這里我們可以用來做一些 loading 頁面的展示及錯誤頁面的展示。
Container(
child: _datas.length == 0 ? Center(child: Text('Loading...')) :
ListView.builder(itemCount: _datas.length ,itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(_datas[index].name as String),
subtitle: Container(
alignment: Alignment.bottomCenter,
height: 25,
child: Text(_datas[index].message as String, overflow: TextOverflow.ellipsis,),
),
leading: Container(
width: 44,
height: 44,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6.0),
image: DecorationImage(image: NetworkImage(_datas[index].imageUrl as String)),
),
),
);
}),
)
對于列表的展示我們這里換回了 ListView,使用 FutureBuilder 的話,當數(shù)據(jù)很多的話需要進行數(shù)據(jù)的保存,會專門放入一個數(shù)組,例如數(shù)據(jù)的分頁加載等,這時候使用 FutureBuilder 就不太好,但是數(shù)據(jù)量不大的話用 FutureBuilder 就會很方便。
頁面保持狀態(tài)
class _ChatPageState extends State<ChatPage> with AutomaticKeepAliveClientMixin<ChatPage>
Widget build(BuildContext context) {
super.build(context);
}
class _RootPageState extends State<RootPage> {
int _currentIndex = 0;
List <Widget>_pages = [ChatPage(), FriendsPage(), DiscoverPage(), MinePage()];
final PageController _controller = PageController();
@override
Widget build(BuildContext context) {
return Container(
child: Scaffold(
body: PageView(
//禁止頁面拖拽
physics: NeverScrollableScrollPhysics(),
onPageChanged: (int index) {
setState(() {
_currentIndex = index;
});
},
controller: _controller,
children: _pages,
),
當我們切換底部 tabBar 的時候,每次進入頁面都會重新加載,這里我們采用 AutomaticKeepAliveClientMixin 來保持狀態(tài),讓頁面只會被加載一次,以聊天頁面為例,_ChatPageState 后面加上 with AutomaticKeepAliveClientMixin<ChatPage>,并在 build 方法中調(diào)用 super.build(context)。在 RootPage 中,用 _pages 數(shù)組來保存底部子頁面,body 使用 PageView 部件,controller 賦值為我們定義的 _controller,children 賦值為 _pages。
Dart 中的異步編程

通過這個案例我們可以看到 任務(wù) 3 會被 for 循環(huán)給阻塞,那么我們怎么能把循環(huán)給放到異步而不影響其他任務(wù)繼續(xù)執(zhí)行呢?

這里我們可以通過 Future 進行包裝,把耗時任務(wù)放到 Future 中,而不會阻塞 任務(wù) 3 的執(zhí)行。

如果 任務(wù) 4 需要依賴異步耗時任務(wù)完成后再執(zhí)行的話,可以使用 async 加 await 結(jié)合的方式。

當我們需要在耗時任務(wù)之后需要執(zhí)行 任務(wù) 4,并且耗時操作不阻塞 任務(wù) 4 的執(zhí)行,這里我們可以定義一個變量 future,調(diào)用 then 方法,then 中的閉包會在耗時操作完成之后執(zhí)行。