一、准备网络数据
1.1 数据准备工作
- 来到网络数据制造的网址,注册登录后,新建仓库,名为WeChat_flutter;
- 点击进入该仓库,删掉左侧的示例接口,新建接口.
3. 接着点击右上角‘编辑’按钮,新建响应内容,类型为Array,一次生成50条
4. 点击chat_list左侧添加按钮,新建chat_list中的数据内容,此时用到一个获取随机头像的网站.到该网站中,随机复制一个图片地址,假设为:https://randomuser.me/api/portraits/women/35.jpg.将数据填上,然后保存.
5.接下来,我们想让获取的图像是个随机值,那么参考Mock.js网站中的生成规则.
6.接着回到响应内容这里,通过设置初始值规则,生成随机的图片地址.
- https://randomuser.me/api/portraits/women/@natural(10,80).jpg
- 接下来填充名称,消息的随机值.
- 综上:服务器的数据准备工作完成,请求的链接地址
二、聊天界面导航条
- 首先设置_RootPageState中进入App默认选中的NavigationBar的私有变量_currentIndex = 0,也就是默认选中微信界面.
- 然后根据微信聊天界面的UI效果,我们先实现右上角的加号.
1. AppBar的actions就是我们需要添加操作的地方.
- 按照这个思路继续,就需要自己实现一个弹出菜单的组件.然而这个时候,Flutter其实已经提供了一套成熟的控件以达到效果
2. PopupMenuButton组件
- PopupMenuButton组件用来弹出一个菜单,必传参数为itemBuilder,用来实现它需要展示的内容.PopupMenuItem就是用来展示内容的类.PopupMenuButton有个onSelected属性,这个属性是个闭包,意思是选中某个PopupMenuItem的时候,会调用这个闭包.但是有个前提就是每个PopupMenuItem的value必须不为null的时候,才会执行onSelected闭包.
- AppBar中具体内部实现为.
AppBar(
//去除导航条黑线
elevation: 0.0,
backgroundColor: WeChatThemeColor,
//设置标题默认居中、否则双端默认方式不一致
centerTitle: true,
title: const Text("微信", style: TextStyle(color: Colors.black),),
actions: [
Container(
margin: EdgeInsets.only(right: 10),
child: PopupMenuButton(
onSelected: (item){
print(item);
},
onCanceled: (){
print('onCanceled');
},
//PopupMenuButton的背景颜色
color: Colors.black,
offset: Offset(0,60),
child: Image(image: AssetImage('images/圆加.png'),width: 25,height: 25,),
itemBuilder: (BuildContext context){
return <PopupMenuItem>[
_buildMenuItem('images/发起群聊.png','发起群聊'),
_buildMenuItem('images/添加朋友.png','添加朋友'),
_buildMenuItem('images/扫一扫1.png','扫一扫'),
_buildMenuItem('images/收付款.png','收付款'),
];
},
),
)
],
),
3. 其中_buildMenuItem是我们封装的一个创建组件的方法,内部实现为
PopupMenuItem _buildMenuItem(String imageName,String title){
return PopupMenuItem(
value: {
'imageName' : imageName,
'title' : title,
},
child: Row(
children: [
Image(image: AssetImage(imageName), width: 25,),
SizedBox(width: 10,),
Text(title, style: TextStyle(color: Colors.white),),
],
)
);
}
4.关于PopupMenuButton背景颜色的设置.可以直接在其内部设置背景颜色.也可以在ThemeData中设置app的cardColor.但是优先级没有直接设置PopupMenuButton的高.如果不设置黑色背景颜色,弹出的视图显示均为白色,看不到UI效果.
三、请求网络数据
- 通过Dart packages 这个网站可以搜索flutter使用的包packages.我们使用http这个包来请求我们的网络数据.这个包是flutter官方提供的.实际项目开发的时候可能并不会使用http这个包,大部分是使用dio来请求网络数据.这里只介绍官方的http包如何使用.
- 导入http包.在名称后可以点击复制包名.
3.在项目的pubspec.yaml中粘贴复制的包名.
4. 粘贴完之后需要Pub get获取一下,获取包对应的代码.
- 也可以在终端输入flutter packages get 来获取.
5.在chat_page.dart中导入http包并取别名
import 'package:http/http.dart' as http;
6.在渲染状态组件的时候发起网络请求,也就是在initState中发起网络请求.getData后采用async表示异步执行.async需要搭配await使用,await后面跟着的是耗时的代码,所以会异步执行调用.
class _ChatPageState extends State<ChatPage> {
.....
@override
void initState() {
super.initState();
getDatas();
}
getDatas() async {
var response = await http.get(Uri.parse('http://rap2api.taobao.org/app/mock/311243/api/chat/list'),);
print(response.statusCode);//200
print(response.body);//这里就是我们自定义的网络数据了
}
.....
}
- 点击其他界面再次回到聊天界面会发现initState方法重新走了一遍,调用了网络请求,这是因为我么还没有保存住状态.后面将讲述如何保存Widget的状态.
7.处理返回数据
- 首先介绍一下,在flutter中如何将请求返回的JSON数据转为Map,在我们iOS开发中是转为字典,而flutter中没有字典这个类型,对应的类型是Map.以及如何将Map转为JSON.在iOS中我们会使用一个NSJSONSerialization的类用来处理JSON数据.同样的,在flutter中也会有一个专门的类JsonCode来处理.
JSON和Map互相转换
- 首先需要导入dart中的convert组件.
- 然后我们写点测试用例,熟悉它的使用方式.
void initState() {
super.initState();
getDatas();
final chat = {
'name': '张三',
'message': '在干嘛?',
};
//Map转JSON
final jsonChat = json.encode(chat);
print(jsonChat);
//JSON转Map
final mapChat = json.decode(jsonChat);
print(mapChat);
print(mapChat is Map);
}
- 返回结果如下:
flutter: {"name":"张三","message":"在干嘛?"}
flutter: {name: 张三, message: 在干嘛?}
flutter: true
flutter: 200
- 其中的json就是JsonCodec的实例. 'is'是用来判断是不是某个类型.
8. 新建聊天模型
- 因为网络出来的数据可能为空,那么就需要用?来修饰定义的属性;
class Chat {
final String? name;
final String? message;
final String? imageUrl;
Chat(this.name,this.message,this.imageUrl);
//工厂方法,用来初始化对象.
factory Chat.fromJson(Map json){
return Chat(json['name'],json['message'],json['imageUrl']);
}
}
- factory 关键字用来标记当前是工厂方法,是设计模式的一种,用来初始化对象.除了默认的构造方法,还可以使用这个工厂方法来实例化一个Chat对象.模型建立好了之后就可以处理响应的数据.
- 如下: 模型数据成功转换.
//将json转为Chat模型
final chatModule = Chat.fromJson(mapChat);
print('name:${chatModule.name} message:${chatModule.message}');// name:张三 message:在干嘛?
9.处理响应的数据
- 首先我们会获取到通过网络接口获取的列表数据,但是不能保证网络请求一定会发送成功.所以要处理一些错误情况.在flutter中引入Future.表示接下来请求的数据,可能有值也可能没有值,一般与网络请求配合使用.
- 因此返回值我们可以设定为.
Future<List<Chat>?> getDatas() async {}
- 对于异常情况的处理,可以通过throw Exception的形式.
Future<List<Chat>?> getDatas() async {
final response = await http.get(Uri.parse('http://rap2api.taobao.org/app/mock/311243/api/chat/list'));
print(response.statusCode);
if (response.statusCode == 200) {
} else {
throw Exception('statusCode: ${response.statusCode}');
}
}
- 接下来我们处理返回的body中的数据
- 获取响应数据,并且转换为Map类型
//获取响应数据,并且转换成Map类型
final responseBody = json.decode(response.body);
//转换模型数组
responseBody['chat_list'].map(
(item) {
print(item);
return item;
}
);
- 这样可以看到item的遍历数据.
flutter: {imageUrl: https://randomuser.mflutter: {imageUrl: https://randomuser.me/api/portraits/women/12.jpg, name: 黎超, message: 音和委起度明条部过们放省。们区以号还九保把王之候包与先件能议清。江知天能能五开比点别增石次米五平。极养提立手专把示低率号容眼组是石。离维照联子象派三热始受构参元离还。相电构次色影件力计面进东把。}
flutter: {imageUrl: https://randomuser.me/api/portraits/women/23.jpg, name: 傅秀兰, message: 但保写太满果此力少合反压色生太个图。制社并更个构北不张需国些清不。没八你或况铁员三时划志有改题头感。值年改你要变程新但八传织。进化林号中不按亲天张原美多。}e/api/portraits/women/37.jpg, name: 李丽, message: 可组品且发铁直报表状传素安小全。器音天石别数业局装共习清。加然处进派变装你农速约部族利音次层。毛得理状主质所局等工型即天研走机段。}
-
- 接下来将其返回结果直接遍历为模型返回为List
final responseBody = json.decode(response.body);
//转换模型数组
List<Chat>chatList = responseBody['chat_list'].map<Chat>(
(item) {
return Chat.fromJson(item);
}
).toList();
print(chatList);
return chatList;
- 此时我们可以看到输出均为实例对象
[Instance of 'Chat', Instance of 'Chat', Instance of 'Chat', Instance of 'Chat', Instance of 'Chat',...]
- 上述可以直接采用箭头函数.这样我们就实现了响应数据的模型转换.
List<Chat>chatList = responseBody['chat_list'].map<Chat>((item) => Chat.fromJson(item)).toList();
return chatList;
10.处理网络请求的结果
- 接下来我们要处理返回Future类型的异步网络请求结果.
- 可以采用try...catch
- 也可以采用then结合的形式
- 这里我们使用then的方式处理结果, 输出其中的value.
loadData(){
getDatas().then((value) {
print(value);
});
}
-
- 这里可以看到value的结果就是我们的chatList数据.
- 其实我们还可以采用一种更为简单的方式处理网络请求.在下一章讲解.
四、利用FutureBuilder渲染微信界面
- 在flutter中渲染网络数据专门有个控件叫做FutureBuilder.当无数据时展示默认界面,有数据时继续渲染网络请求下来的数据
- 此时可以看到返回的结果中 先是null、然后再是连续几次的数据.
- 此时我们可以通过snapshot的异步连接状态来查看.
- 也就是snapshot.connectionState
- 当处于waiting状态时,data会返回null
- 当处于done的状态时,data会返回正常解析结果.
flutter: data: null
flutter: state:ConnectionState.waiting
flutter: data: [Instance of 'Chat', Instance of 'Chat', Instance of 'Chat', ...]
flutter: state:ConnectionState.done
2. 所以这个时候我们可以借助于ConnectionState状态来确定当前要渲染的界面.
-
- 当waiting状态时,展示一个Loading...
- 当done状态时,渲染界面.
- 因此我们的FutureBuilder的渲染实现部分为
FutureBuilder(
future: getDatas(),
builder: (BuildContext context, AsyncSnapshot snapshot){
//无数据时渲染默认界面,有数据时显示网络数据
print('state:${snapshot.connectionState}');
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: Text('Loading...'),);
} else {
return ListView(
children: snapshot.data.map<Widget>((item){
return ListTile(
//右侧 标题
title: Text(item.name),
//右侧 子标题
subtitle: Container(
height: 20,width: 20,
//TextOverflow.ellipsis 展示不下的时候省略号
child: Text(item.message,overflow: TextOverflow.ellipsis,),
),
//左侧:圆型头像
leading: CircleAvatar(
backgroundImage: NetworkImage(item.imageUrl),
),
);
}).toList(),
);
}
},
)
- 展示的效果图如下:
- 但是这样的渲染方式不是最好的.因为每次进入微信界面就需要发送网络请求.每次都要重新渲染.
- 那么这种FutureBilder直接渲染布局的方式只能应用于数据相对简单的界面.
- 也可以将请求下来的数据放入一个缓存模型数组中,当builder的时候再从模型数组中拿取使用.
五、网络请求的处理
- 这里我们将网络请求的数据进行处理.首先来到 _ChatPageState中,创建一个缓存数组
List<Chat> _datas = [];
2. 在loadData时处理返回的数据.
- 通过then将正常返回的数据赋值给缓存,然后在setState方法中实现这个赋值操作,就会引起界面的渲染.
- 将错误的返回结果通过日志输出.
- 会有类型不匹配的错误: 将datas设置为可空类型,然后在赋值的时候对空的情况进行空处理就行.
loadData(){
//当数据正常返回的时候
getDatas().then((List<Chat>? datas) {
setState(() {
_datas = datas ?? [];
});
}).catchError((err){
print(err);
});
}
3. 这个时候通过缓存的数据来渲染界面
- Scaffold中body的FutureBuilder替换回Container.通过三目运算符判断当前缓存数组中是否有值,如果没有,就设置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 ?? ""),
subtitle: Container(height: 20,width: 20,child: Text(_datas[index].message ?? ""),),
leading: CircleAvatar(
backgroundImage: NetworkImage(_datas[index].imageUrl ?? ""),
),
);
}),
),
- 点击其他NavigationBar界面,再点回微信界面,会发现界面的渲染过程.
4. 关于Future请求结果的处理完善
- Future请求结果的处理方面,除了catchError之外,还有whenComplete、timeout
loadData(){
//当数据正常返回的时候
getDatas().then((List<Chat>? datas) {
setState(() {
_datas = datas ?? [];
});
}).catchError((err){
print(err);
}).whenComplete(() {
//数据处理完毕
print("完毕");
}).timeout(Duration(milliseconds: 10)).catchError((timeout){
print("加载超时 ${timeout}");//flutter: 加载超时 TimeoutException after 0:00:00.010000: Future not completed
});
}
- 在这里,超时并不意味着请求结束,当请求结果返回之后,仍然调用了请求完毕.
- 在这里,我们我们需要处理: 一旦请求发出,除非异常,否则超时后其他的数据都不应该返回去调用我们的setState渲染界面.
5. 请求超时的异常处理/多次重复刷新
- 设置一个私有bool变量_cancelConnect, 如果当前的标记不为true,那么再去setState渲染界面.避免了数据污染.
- 这个时候我们发现界面和通讯录界面都存在一种现象:当我们点击其他界面,再回到当前界面时,数据会重新加载.这个现象说明了我们需要进行状态的保存以避免重复刷新.接下来将对这个现象进行处理.
六、保存Widget的状态
关于状态的保存,这个时候我们引入另一个概念: 混入(Mixins),用来给类增加功能,是多继承模式下的一种代码复用.使用with关键字来实现混入一个或多个类.
- 因为flutter渲染效率非常高,当控件不在界面上展示的话就会被销毁,再次展示时会重新渲染.
- 如果我们的状态需要保存的话,就需要混入一个类:AutomaticKeepAliveClientMixin,也就是对ChatPage的延展.重写wantKeepAlive属性并且在build渲染方法中调用父类的渲染方法.
//AutomaticKeepAliveClientMixin让当前界面保存状态
class _ChatPageState extends State<ChatPage> with AutomaticKeepAliveClientMixin<ChatPage>{
.....
//保留setState状态
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
//6.3 重写父类渲染方法
super.build(context);
...
}
...
}
3. 同理我们去保存通讯录界面的状态.
4. 这个时候我们去检测设置的效果,发现貌似并没有设置成功.还是会重新渲染对应的界面.是否是设置失效了呢?
- 其实不是.这个时候就需要考虑我们的根视图设置的问题了.因为之前预埋了坑点.
5. 回到rootpage.dart中,我们发现在设置当前显示body的时候,采用的是从_pages数组中获取对应的界面.在iOS中,这样设置并没有什么问题.
- 但是在flutter中,这样设置,每次根据_currentIndex获取的视图显示对象并不是数组中设置的对象.
- 因为在flutter中,在数组中创建的对象对于flutter来说只是一堆数据,通过build方法渲染到界面上.
- 在build中有一个小部件树,这个小部件树是从MyApp开始build渲染,然后是它的home属性包含的RootPage对象,再通过RootPage中的设置的Container,去包含的ChatPage、FriendsPage等
MyApp => RootPage => Container => ChatPage/FriendsPage/DiscoverPage/MinePage
- 一旦当前_currentIndex改变之后,那么设置的body就从_pages中获取到对应的界面对象.然而就是这个操作导致.当前Container包含的Page从一个切换到另一个,之前的界面就不在渲染树中.也就是被销毁了.
6. 回到我们在微信界面/通讯录界面混入AutomaticKeepAliveClientMixin这个类的目的:
-
- 让小部件树之外指定的界面不要被销毁.也就是我们想要保留根视图下四个界面状态.当其中一个渲染在小部件树中的时候,另外三个不要被销毁.
- 为了解决这个问题,我们引入flutter中另外一个控件PageController.
7. PageController设置rootpage根视图界面.
- 首先创建一个私有变量PageController _controller, 并将初始界面page设置为第一个界面.
final PageController _controller = PageController(
//初始显示的界面索引
initialPage: 0,
);
- 其次将Container中Scaffold的body设置为PageView
- onPageChanged就是我们需要设置index改变的回调的地方
- NeverScrollableScrollPhysics可以去除默认的左右滚动的效果.
PageView(
onPageChanged: (int index ){
_currentIndex = index;
setState(() {
});
},
//如果不设置这个属性,那么根视图事件可以左右滚动切换.
physics: NeverScrollableScrollPhysics(),
controller: _controller,
children: [
ChatPage(),
FriendsPage(),
DiscoverPage(),
MinePage()
],
),
- 综上:做完这些操作,我们再去点击任意界面,之前的状态就会被保留下来.