这是我参与11月更文挑战的第17天,活动详情查看:2021最后一次更文挑战
首先做的事右上角的按钮,右上角要弄一个menu,那么这里可以在AppBar的actions添加Flutter封装好的控件PopupMenuButton,用一个container包着调整位置,然后用offset调整自身的位置。
actions: [
Container(child:
PopupMenuButton(
itemBuilder: (BuildContext context) {
return <PopupMenuItem<String>>[
PopupMenuItem(child: Text("发起群聊")),
PopupMenuItem(child: Text("发起群聊")),
PopupMenuItem(child: Text("发起群聊")),
];
},
child: Image(
image: AssetImage('images/圆加.png'),
width: 25,
),
offset: Offset(0,60)
),
margin: EdgeInsets.only(right: 10),),
color: Color.fromRGBO(1, 1, 1, 0.65),),
],
创建一个_buildPopupMenuItem来快速创建PopupMenuItem.
Widget _buildPopupMenuItem(String imgAsset, String title) {
return Row(
children: [
Image(
image: AssetImage(imgAsset),
width: 20,
),
SizedBox(width: 20,),
Text(title,style: TextStyle(color:Colors.white),),
],
);
}
在PopupMenuButton中使用。
child: PopupMenuButton(
itemBuilder: (BuildContext context) {
return <PopupMenuItem<String>>[
PopupMenuItem(
child: _buildPopupMenuItem('images/发起群聊.png', '发起群聊')),
PopupMenuItem(
child: _buildPopupMenuItem('images/添加朋友.png', '添加朋友'),
),
PopupMenuItem(
child: _buildPopupMenuItem('images/扫一扫1.png', '扫一扫'),
),
PopupMenuItem(
child: _buildPopupMenuItem('images/收付款.png', '/收付款')),
];
},
接下来要模拟网络请求。去rap2.taobao.org/创建一个仓库。
之后点击新建接口。
输入相应的内容
创建好之后点击编辑
随机生成响应内容图片,名字以及内容,这里使用randomuser.me/来生成随机头像。
这样网络数据就准备好了。 将Scaffold中的body改成下面的构造。
body: Container(
child: ListView.builder(
itemBuilder: (BuildContext context, int index) {
return Text('hello');
},
itemCount: 10,
),
),
接下来要添加库,一般Flutter使用的网络请求库有两个,一个是dio,一个是官方的http。这里先使用http,在pubspec添加http: ^0.13.4。
引用http之后调用一个新创建的方法getDatas,在getDatas里面使用http请求数据。这里getDatas使用async异步执行。异步不代表多线程,多线程可以异步,但是异步不代表多线程。一个线程可以在多个方法中来回切换来实现异步。
import 'package:http/http.dart' as http;
@override
void initState() {
// TODO: implement initState
super.initState();
getDatas();
}
void getDatas() async {
Uri url = Uri.parse("http://rap2api.taobao.org/app/mock/293458/api/chat/list");
final response = await http.get(url);
}
这里模拟Json 转map的过程,先创建一个map,然后转为json,然后在转为map,打印出来看到两者区别就是有没有“ ”。
//Json 转map
final chat = {
'name':'张三',
'message':'吃了吗'
};
final chatJson = json.encode(chat);
print(chatJson);
final newChat = json.decode(chatJson);
print(newChat);
print(newChat['name']);
print(newChat['message']);
接下来还要map转模型。 这里先创建一个Chat Class,然后创建一个工厂构造方法来返回模型。
class Chat {
final String? name;
final String? message;
final String? imageUrl;
Chat({this.name,this.message,this.imageUrl});
factory Chat.fromMap(Map map){
return Chat(
name:map['name'],
message:map['message'],
imageUrl:map['imageUrl'],
);
}
}
然后在newChat下面添加
final chatModel = Chat.fromMap(newChat);
print('name:${chatModel.name}, message:${chatModel.message}, ');
运行后发现转成功了,成功打印出来了。
接下来要从网络中拉取数据,然后将其转换为模型,然后放入模型数组里面。 getDatas方法一般是要有返回值的,而在Flutter中,如果一个异步方法有返回值,则需要使用Future.这里改写getDatas方法,然后将获取的数据转换为List返回出去。
Future<List<Chat>> getDatas() async {
final Uri url = Uri.parse(
"http://rap2api.taobao.org/app/mock/293458/api/chat/list");
final response = await http.get(url);
if (response.statusCode == 200) {
// 获取响应数据,转成map
final responseBody = json.decode(response.body);
// map 作为List的遍历方法。
final chatList = responseBody['chat_list'].map<Chat>((item) => Chat.fromMap(item)).toList();
print(chatList);
return chatList;
} else {
throw Exception('Error with statusCode ${response.statusCode}');
}
}
Future可以搭配then使用,这里就相当于回调。Future代表着是未来的数据,then相当于把里面的代码保存起来,等数据来的时候再调用。
getDatas().then((value) {
});
很多时候我们会依赖一些异步数据来动态更新UI,比如在打开一个页面时我们需要先从互联网上获取数据,在获取数据的过程中我们显示一个加载框,等获取到数据时我们再渲染页面。当然,通过 StatefulWidget 完全可以实现上述这些功能。但由于在实际开发中依赖异步数据更新UI的这种场景非常常见,因此Flutter专门提供了FutureBuilder 组件来快速实现这种功能。FutureBuilder会依赖一个Future,通常是一个异步耗时任务,它会根据所依赖的Future的状态来动态构建自身。这个时候,getDatas返回的数据就在snapshot里面。
child:FutureBuilder(
future: getDatas(),
builder:(BuildContext context, AsyncSnapshot snapshot) {
return Container(
print('Data: ${snapshot.data}');
);
} ,
),
运行后发现这里没有数据的时候也运行了一次,这是因为这里不能卡住UI,没有数据的时候渲染占位的东西,有数据的时候重新渲染更新页面。这里就是异步渲染。
这里也可以使用snapshot.的connectionState来返回不同的界面。
if(snapshot.connectionState == ConnectionState.waiting) {
return Center(child: Text( "Loading"),);
}
return ListView(
children: snapshot.data.map<Widget>((Chat item){
return ListTile(
title: Text(item.name ?? ""),
subtitle: Container(
height: 20,
child: Text(item.message ?? ""),
),
leading: CircleAvatar(
backgroundImage: NetworkImage(item.imageUrl ?? ""),
),
);
}
).toList(),
);
FutureBuilder比较适合在数据不多的情况下使用,在数据很多的情况下,用FutureBuilder并不好,因为我们需要保存数据。 声明一个空数组,然后在initState的时候使用then在请求数据后进行保存。
// 模型数据
List<Chat> _datas = [];
@override
void initState() {
// TODO: implement initState
super.initState();
getDatas().then((List<Chat> datas) {
setState(() {
_datas = datas;
});
});
}
然后在使用ListView进行渲染。
body: Container(
child: Container(
child: _datas.length == 0
? Center(
child: Text('Loading'),
)
: ListView.builder(
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(_datas[index].name ?? ""),
subtitle: Container(
alignment: Alignment.bottomCenter,
padding: EdgeInsets.only(right: 10),
height: 25,
child: Text(
_datas[index].message ?? "",
overflow: TextOverflow.ellipsis,
),
),
leading: ClipRRect(
//剪裁为圆角矩形
borderRadius: BorderRadius.circular(5.0),
child: Image(
image:
NetworkImage(_datas[index].imageUrl ?? "")),
),
);
},
itemCount: _datas.length,
))),
这里单纯的使用_datas的长度判断并不是很好,那么可以使用catchError,whenComplete,timeout来进行处理,例如timeout不进行加载_datas。
bool _cancelConnect = false;
class _ChatPageState extends State<ChatPage> {
bool _cancelConnect = false;
// 模型数据
List<Chat> _datas = [];
@override
void initState() {
// TODO: implement initState
super.initState();
getDatas().then((List<Chat> datas) {
if (!_cancelConnect){
setState(() {
_datas = datas;
});
}
}).catchError((e) {
print(e);
}).whenComplete(() {
}).timeout(Duration(milliseconds: 100)).catchError((timeoutError){
print('超时了!$timeoutError');
_cancelConnect = true;
});
}
现在每次进来依然会调用initState进行请求数据,Flutter中如果想要保留数据,那么就需要混入AutomaticKeepAliveClientMixin类。
class _ChatPageState extends State<ChatPage> with AutomaticKeepAliveClientMixin
然后重写wantKeepAlive为true。
bool get wantKeepAlive => true;
然后需要在build方法里面调用 super.build(context)。
Widget build(BuildContext context) {
super.build(context);
.......
}
现在这个类可以保住状态了,但是在root页面里面只是显示某一个页面,也就是切出去的时候Widget树就没有chatpage这个对象了,这个时候chatpage就会被移除掉。也就是说,如果想要保存下来,那么就要将页面保存在Widget树里面,只是显不显示的问题。那么就到HomePage修改。这里声明一个PageController,然后Body使用PageView,然后在bottomNavigationBar的onTap中使用jumpToPage切换页面。
使用PageView之后可以左右拖拽切换页面的,这个时候如果想要切换下面的bottomNavigationBar的选中,那么就可以使用onPageChanged。
onPageChanged:(int index){
setState(() {
_currentIndex = index;
});
} ,
否则就将physics设为NeverScrollableScrollPhysics()。
physics: NeverScrollableScrollPhysics(),