Flutter项目(4)之仿微信聊天界面
1 创建网络仓库和假数据接口
-
创建接口:
- 返回值是array,生成规则可以为50,表示一次返回50条数据;
- 点击前面的+号,可以返回每条数据的具体值,比如image_url;网络头像链接,每个头像的结尾数字值不一样,代表不同的图片,可以使用生成随机数字的语法来生成不同的图片,根据mock.js语法,加入@natural(10,70),代表生成10-70之间的随机数;
- 需要返回user_name,即用户名,初始值可以输入 @cname,返回的是中文名称;
- 还需要返回聊天的内容message,初始值可以写@cparagraph,返回的是中文的随机内容;
-
返回接口内容示例:
image.png
2 导航栏的创建
image.png聊天界面导航栏右上角有一个+更多按钮,点击这个按钮的时候弹出一个弹窗:
appBar: AppBar(
centerTitle: true,
title: Text('微信'),
actions: <Widget>[
Container(
margin: EdgeInsets.only(right: 15),
child: PopupMenuButton(
offset: Offset(0, 60),
child: Image(image: AssetImage('images/圆加.png'),width: 25,),
itemBuilder: _PopMenuItemBuild,
),
)
],
),
//itemBuilder单独写的方法,返回值是List <PopupMenuItem <String>>
List <PopupMenuItem <String>> _PopMenuItemBuild(BuildContext context){
return <PopupMenuItem<String>>[
_CreatPopMenuBuildItem('images/发起群聊.png', '发起群聊'),
_CreatPopMenuBuildItem('images/添加朋友.png', '添加朋友'),
_CreatPopMenuBuildItem('images/扫一扫1.png', '扫一扫'),
_CreatPopMenuBuildItem('images/收付款.png', '收付款'),
];
}
//创建每一个item分出来的公共方法,传入一个imageName和一个title返回一个PopupMenuItem
PopupMenuItem<String> _CreatPopMenuBuildItem(String imageName, String title){
return PopupMenuItem<String>(child: Row(
children: <Widget>[
Image(image: AssetImage(imageName),width: 25,),
SizedBox(width: 20),
Text(title,style: TextStyle(color: Colors.white),),
],
),);
}
在flutter中,对于右上角的按钮,以及点击弹出的弹窗,系统提供了一个按钮:
/// See also:
///
/// * [PopupMenuItem], a popup menu entry for a single value.
/// * [PopupMenuDivider], a popup menu entry that is just a horizontal line.
/// * [CheckedPopupMenuItem], a popup menu item with a checkmark.
/// * [showMenu], a method to dynamically show a popup menu at a given location.
class PopupMenuButton<T> extends StatefulWidget {
/// Creates a button that shows a popup menu.
///
/// The [itemBuilder] argument must not be null.
const PopupMenuButton({
Key key,
@required this.itemBuilder,
this.initialValue,
this.onSelected,
this.onCanceled,
this.tooltip,
this.elevation,
this.padding = const EdgeInsets.all(8.0),
this.child,
this.icon,
this.offset = Offset.zero,
this.enabled = true,
this.shape,
this.color,
this.captureInheritedThemes = true,
}) : assert(itemBuilder != null),
- 对于PopupMenuButton, itemBuilder是required,不能为空,返回的是一个List<PopupMenuItem>,点击的时候直接创建一个菜单view;
- offset: Offset(0, 60),menuView相对于+按钮的偏移量,xy方向,默认是0,紧挨着+号按钮;
3 网络数据的请求
通过创建的网络假数据接口可以请求到网络数据,网络请求到的数据的可是是Json格式的,需要转换成Map,然后转换成对应的模型.用于ListView的创建;
- 请求网络数据需要去官网导入支持http的package,导入http的步骤
- 通过http.get方法直接请求链接url,使用async和await配合使用异步请求数据;
- 数组的遍历直接使用.map((item){}�),强转成List可以使用toList()方法;
- 需要添加一个判断response.stateCode,如果不是请求成功的code,则抛出一个异常;��
- 添加一个bool值_cancelConnect来对超时,多次重复请求容错;
- 网络请求之后使用then用来处理将来Future异步网络请求回来的数据,后面跟上catchError会回调失败的原因;
- 加载完毕回调whenComplete(成功和失败都会调这个回调);
- 网络请求超时回调timeout,后面跟上catchError是返回超时的错误原因
class _WeChatPageState extends State<WeChatPage> with AutomaticKeepAliveClientMixin<WeChatPage> {
List<Chat> _datas = [];
bool _cancelConnect = false;
@override
// TODO: implement wantKeepAlive
bool get wantKeepAlive => true;
@override
void initState() {
// TODO: implement initState
super.initState();
getDatas().then((List<Chat> datas) {
if(!_cancelConnect){
setState(() {
_datas = datas;
});
}
}).catchError((error) {
print(error);
}).whenComplete(() {
print("完毕");
}).timeout(Duration(seconds: 10)).catchError(
(timeOutError){
_cancelConnect = true;
print('${timeOutError}');
}
);
// print('来了');
}
Future<List<Chat>> getDatas() async {
_cancelConnect = false;
final response = await http
.get('http://rap2.taobao.org:38080/app/mock/245766/api/chat/list');
if (response.statusCode == 200) {
var respones_Body = json.decode(response.body);
List<Chat> chat_list = respones_Body['chat_list']
.map<Chat>((item) => Chat.formJson(item))
.toList();
return chat_list;
} else {
throw Exception('stateCode=${response.statusCode}');
}
print(response.statusCode);
}
@override
Widget build(BuildContext context) {
super.build(context);
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text('微信'),
actions: <Widget>[
Container(
margin: EdgeInsets.only(right: 15),
child: PopupMenuButton(
offset: Offset(0, 60),
child: Image(
image: AssetImage('images/圆加.png'),
width: 25,
),
itemBuilder: _PopMenuItemBuild,
),
)
],
),
body: Container(
child: _datas.length == 0
? Center(
child: Text("loading"),
)
: ListView.builder(
itemCount: _datas.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: Container(
height: 45,
width: 45,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
image: DecorationImage(
fit: BoxFit.fitHeight,
image: NetworkImage(_datas[index].imageUrl))),
),
title: Text(_datas[index].name),
subtitle: Container(child: Text(_datas[index].message),height: 20,),
);
}),
));
}
}
3.1 Chat模型类中的实现代码:
里面有用到一个factory工厂方法来实现jsonMap转模型,在外部可以直接调用这个工厂方法传入有一个Map返回一个Chat模型.
class Chat {
final String name;
final String message;
final String imageUrl;
Chat({this.name, this.message,this.imageUrl});
factory Chat.formJson(Map json) {
return Chat(
name: json['user_name'],
message: json['message'],
imageUrl: json['image_url'],
);
}
}
3.2 Dart中Json和Map的互转
首先需要导入头文件;
示例代码如下:
- 将Map转换为Json使用json.encode();
- 将Json转换为Map使用json.decode();
import 'dart:convert';
_MapAndJson() {
var TestMap = {
'name': 'Jack',
'message': 'today is nice',
};
//map转json使用encode
var mapJson = json.encode(TestMap);
print(mapJson);
//json转map使用decode
var jsonMap = json.decode(mapJson);
print(jsonMap);
}
4 保持部件的状态MixIn和PageViewController
为了不每次进这个页面都刷新页面的状态,需要保持这个页面的状态,这时候需要用到dart中的混合MixIn,类似于iOS中的分类,就是可以用其他class的方法,需要添加如下操作:
- 使用MixIn,加上with AutomaticKeepAliveClientMixin<WeChatPage>
- 需要重写wantKeepAlive方法
- 需要在Widget build方法中加入super.build(context)
class _WeChatPageState extends State<WeChatPage> with AutomaticKeepAliveClientMixin<WeChatPage> {
@override
// TODO: implement wantKeepAlive
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context);
4.1 PageViewController
但是在page.dart中需改了以后,发现点击还是会去跟新,原因是在rootpage中,body返回的是每个创建的page,其他的page会被销毁,会重新渲染,所以保持部件的状态没有效果,所以在rootPage中需要更改控制body的方式,使用PageViewController专门来控制tabbarpage的切换.
class PageController extends ScrollController {
/// Creates a page controller.
///
/// The [initialPage], [keepPage], and [viewportFraction] arguments must not be null.
PageController({
this.initialPage = 0,
this.keepPage = true,
this.viewportFraction = 1.0,
}) : assert(initialPage != null),
assert(keepPage != null),
assert(viewportFraction != null),
assert(viewportFraction > 0.0);
/// The page to show when first creating the [PageView].
final int initialPage;
/// Save the current [page] with [PageStorage] and restore it if
/// this controller's scrollable is recreated.
///
/// If this property is set to false, the current [page] is never saved
/// and [initialPage] is always used to initialize the scroll offset.
/// If true (the default), the initial page is used the first time the
/// controller's scrollable is created, since there's isn't a page to
/// restore yet. Subsequently the saved page is restored and
/// [initialPage] is ignored.
///
/// See also:
///
/// * [PageStorageKey], which should be used when more than one
/// scrollable appears in the same route, to distinguish the [PageStorage]
/// locations used to save scroll offsets.
final bool keepPage;
PageViewController的使用需要先创建一个实例对象,initialPage=0是选中的第0个page;然后在root_Page中body传入PageView,PageView的controller传入之前创建好的PageViewController对象;
class _RootPageState extends State<RootPage> {
int _cuttentIndex = 0;
final PageController _pageController = PageController(
initialPage: 0,
);
@override
Widget build(BuildContext context) {
return Container(
child: Scaffold(
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
fixedColor: Colors.greenAccent,
currentIndex: _cuttentIndex,
selectedFontSize: 12.0,
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Image(
height: 20,
width: 20,
image: AssetImage('images/tabbar_chat.png')),
activeIcon: Image(
height: 20,
width: 20,
image: AssetImage('images/tabbar_chat_hl.png')),
title: Text('微信')),
BottomNavigationBarItem(
icon: Image(
height: 20,
width: 20,
image: AssetImage('images/tabbar_friends.png')),
activeIcon: Image(
height: 20,
width: 20,
image: AssetImage('images/tabbar_friends_hl.png')),
title: Text('通讯录')),
BottomNavigationBarItem(
icon: Image(
height: 20,
width: 20,
image: AssetImage('images/tabbar_discover.png')),
activeIcon: Image(
height: 20,
width: 20,
image: AssetImage('images/tabbar_discover_hl.png')),
title: Text('发现')),
BottomNavigationBarItem(
icon: Image(
height: 20,
width: 20,
image: AssetImage('images/tabbar_mine.png')),
activeIcon: Image(
height: 20,
width: 20,
image: AssetImage('images/tabbar_mine_hl.png')),
title: Text('我的')),
],
onTap: (int index) {
_cuttentIndex = index;
setState(() {});
_pageController.jumpToPage(index);
},
),
body: PageView(
controller: _pageController,
onPageChanged: (int index){//tabbar页面滚动回调,更改index刷新tabbar选中的item
_cuttentIndex = index;
setState(() {});
},
// physics: NeverScrollableScrollPhysics(),//禁止tabbar页面滚动
children: <Widget>[
WeChatPage(),
FriendsPage(),
FindPage(),
MinePage()
],
),
),
);
}
}
和PageViewController配合使用的有一个PageView,
- controller:_pageController,使用创建好的PageViewController来控制tabbar的四个控制器
- onPageChanged回调,选中某个page时的回调,需要更改_currentIndex(当前选中的page),用于刷新tabbar上选中的item;
- physics: NeverScrollableScrollPhysics(),禁止tabbar手势左右滑动切换页面
- children:传入子page.
5 总结
效果:这次的界面搭建出来的实现效果比较粗糙,主要是为了学习网络请求的逻辑流程,Json和模型的转换,理解弄清楚了这些界面的搭建细节可以修改.
网络请求是学会Flutter很关键的一点,熟练的运用网络请求和处理数据,那么项目困难已经解决了的一大半.