接上篇:11、Flutter - 项目实战 - 仿微信(五)通讯录
详细代码参见Demo
Demo地址 -> wechat_demo
其他相关联文章
7、Flutter - 项目实战 - 仿微信(一)BottomNavigationBar 4个主页面显示
8、Flutter - 项目实战 - 仿微信(二)发现页面
9、Flutter - 项目实战 - 仿微信(三)我的页面
10、Flutter - 项目实战 - 仿微信(四)数据准备
11、Flutter - 项目实战 - 仿微信(五)通讯录
效果
创建一个chat 文件夹,用来存放我们的搜索页面和 chat_page页面
这里就会用到 10、数据准备中的聊天接口。通过网络请求获取到数据,然后显示处理出来。
这时候我们需求要导入http包,类似iOS中pod
首先介绍个网址 pub.dev (https://pub.dev/packages)中搜索 http,点进去查看
在 AS(Android Studio) 中的 pubspec.yaml 中配置 http: ^0.12.1
当然了我们这里用另外一个封装好的http 库, dio,可以在pub.dev 中搜索一下看看。(代码中有http库请求的代码)
用dio 的话我们抽一个单独的文件写请求解析
创建一个文件http_manager
import 'package:dio/dio.dart';
class HttpManager {
static final Dio dio = Dio();
static Future request(String url,
{String method = 'get',
Map queryParameters,
int timeOut}) {
// 1、创建配置,什么方法请求
final options = Options(method: method, receiveTimeout: timeOut);
// 2、发网络请求
return dio.request(
url,
queryParameters: queryParameters,
options: options,
);
}
}
Future get(url,
{Map headers,
Map queryParameters,
int timeout}) {
return HttpManager.request(url,
queryParameters: queryParameters, method: 'get', timeOut: timeout);
}
外部调用的方法, get ,因为网络请求是耗时操作,所以用异步 async。 传入参数url,可选参数headers ,queryParameters(查询参数), timeout 超时时间
然后调用HttpManager 通过dio 发起网络请求
return dio.request(
url,
queryParameters: queryParameters,
options: options,
);
pub.dev 有示例代码
import 'package:dio/dio.dart'; /// More examples see https://github.com/flutterchina/dio/tree/master/example main() async { var dio = Dio(); Response response = await dio.get('https://google.com'); print(response.data); }
chat_page zhogn 还是用到了search_bar 搜索显示,在下面介绍。
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:wechat/const.dart';
import 'package:wechat/pages/chat/search_bar.dart';
import 'package:wechat/tools/http_manager.dart' as http;
class ChatPage extends StatefulWidget {
@override
_ChatPageState createState() => _ChatPageState();
}
//部件 保持状态
class _ChatPageState extends State
with AutomaticKeepAliveClientMixin {
Widget _buildPopupMenuItem(String imageAss, String title) {
return Row(
children: [
Image(
image: AssetImage(imageAss),
width: 20,
),
SizedBox(
width: 20,
),
Text(
title,
style: TextStyle(color: Colors.white),
),
],
);
}
// bool _cancleConnect = false;
Timer _timer;
// CancelToken _token = CancelToken();
List _datas = [];
@override
void initState() {
super.initState();
print('Chat的init来了!');
// int _count = 0;
// _timer = Timer.periodic(Duration(seconds: 1), (timer) {
// _count++;
// print(object)
// print('数据来了');
// if (_count == 99) {
// timer.cancel();
// }
// });
getDatas().then((List datas) {
print('数据来了');
// if (!_cancleConnect) {
print('更新数据');
setState(() {
_datas = datas;
});
// }
}).catchError((e) {
print('错误$e');
}).whenComplete(() {
print('完毕');
// })
// .timeout(Duration(seconds: 6))
// .catchError((timeout) {
// _cancleConnect = true;
// print('超时输出:$timeout');
});
}
// 网络请求耗时,定义方法的时候定义成异步的
Future> getDatas() async {
//不在是取消连接了!
// _cancleConnect = false;
final response = await http.get(
'http://rap2.taobao.org:38080/app/mock/256914/api/chat/list',
timeout: 100);
if (response.statusCode == 200) {
// JSON 转字典,字典转模型
// //获取相应数据,并转成Map类型!
// final responseBody = json.decode(response.body);
// 转模型数组 map中遍历的结果需要返回处理啊
// http请求解析
// List chatList = responseBody['chat_list'].map((item) {
List chatList = response.data['chat_list'].map((item) {
return Chat.fromJson(item);
}).toList();
return chatList;
} else {
throw Exception('statusCode:${response.statusCode}');
}
}
@override
Widget build(BuildContext context) {
super.build(context);
return Scaffold(
appBar: AppBar(
elevation: 0.0,
title: Text('微信'),
backgroundColor: WeChatThemColor,
actions: [
Container(
margin: EdgeInsets.only(right: 10),
child: PopupMenuButton(
offset: Offset(0, 60.0),
child: Image(
image: AssetImage('images/圆加.png'),
width: 25,
),
itemBuilder: (BuildContext context) {
return >[
PopupMenuItem(
child: _buildPopupMenuItem('images/发起群聊.png', '发起群聊')),
PopupMenuItem(
child: _buildPopupMenuItem('images/添加朋友.png', '添加朋友')),
PopupMenuItem(
child: _buildPopupMenuItem('images/扫一扫.png', '扫一扫')),
PopupMenuItem(
child: _buildPopupMenuItem('images/收付款.png', '收付款')),
];
},
),
) //右上角按钮
],
),
body: Container(
child: _datas.length == 0
? Center(
child: Text('Loading...'),
)
: ListView.builder(
itemCount: _datas.length + 1,
itemBuilder: _buildCellForRow,
),
));
}
Widget _buildCellForRow(BuildContext context, int index) {
if (index == 0) {
return SearchCell(
datas: _datas,
);
}
// 因为第一个位置放了搜索框,所以index要 -1,才能得到正确的数组数据下标
index--;
print('text name = ' + _datas[index].name);
print('images url = ' + _datas[index].imageUrl);
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: Container(
width: 44,
height: 44,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6.0),
image:
DecorationImage(image: NetworkImage(_datas[index].imageUrl))),
),
);
}
@override
void dispose() {
// TODO: implement dispose
// 取消我们的timer
print('走了');
if (_timer != null && _timer.isActive) {
_timer.cancel();
}
super.dispose();
}
@override
bool get wantKeepAlive => true;
}
class Chat {
final String name;
final String message;
final String imageUrl;
//直接赋值创建对象,必须创建新对象
const Chat({this.name, this.message, this.imageUrl});
//工厂构造函数
//factory 可以创建已有的对象
factory Chat.fromJson(Map json) {
return Chat(
name: json['name'],
message: json['message'],
imageUrl: json['imageUrl'],
);
}
}
//关于Map和Json
/*
*
// final chat = {
// 'name': '张三',
// 'message': '吃了吗?',
// };
//Map转Json
// final chatJson = json.encode(chat);
// print(chatJson);
//Json转Map
// final newChat = json.decode(chatJson);
// print(newChat['name']);
// print(newChat['message']);
// print(newChat is Map);
* */
由于Flutter 在切换页面的时候,不在屏幕上的会被释放。重新返回的时候又会重新请求和渲染,这并不是我们想要的效果,那么就需要保持页面状态。
需要混入 Mixins
使用with 来混入一个或者多个Mixin
StatefulWidget 保持状态的做法,3步
1、混入一个
with AutomaticKeepAliveClientMixin {
2、重写 get 方法
@override
bool get wantKeepAlive => true;
3、bulid 调用 super 的bulid 方
@override
Widget build(BuildContext context) {
super.build(context);
return Scaffold(
NavigationBar 也会遇到同样的问题,如果切换了bar 其他的就会被释放掉。我们不想让释放就需要持有这些小部件
root_page 里面,就用了PageView 然后去持有这些小部件,保证不被释放。这样就全部放到了Widget树中
List _pages = [
ChatPage(),
FriendsPage(),
DiscoverPage(),
MinePage(),
];
class _RootPageState extends State {
int _currentIndex = 0;
final PageController _controller = PageController(initialPage: 0);
@override
Widget build(BuildContext context) {
return Scaffold(
body: PageView(
//页面滚动变化时调用
// onPageChanged: (int index){
// _currentIndex = index;
// setState(() {});
// },
physics: NeverScrollableScrollPhysics(), //禁止页面滚动,tabbar页面
controller: _controller,
children: _pages,
),
菜单弹出框
chilid 设置一下显示的图片
itemBuilder 设置一下弹出框要显示的item
_buildPopupMenuItem
定义 item 的显示样式
如果没有请求到数据,页面显示 loading...
itemCount :_data.length + 1,
加的这个1是,在ListView最顶端添加了搜索框,所以加1
显示会话列表的Cell
BoxDecoration 装饰器
DecorationImage 占位图
在iOS我们的cell都要考虑到复用的问题,为了性能。
但是在 Flutter 中没有复用,因为一旦移动到屏幕之外,就被释放掉了,所以不需要考虑复用的问题(也没有提供复用的方式)。
import 'package:flutter/material.dart';
import 'package:wechat/const.dart';
import 'package:wechat/pages/chat/search_page.dart';
import 'chat_page.dart';
class SearchCell extends StatelessWidget {
final List datas;
const SearchCell({this.datas});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: (d) {
Navigator.of(context).push(MaterialPageRoute(builder: (context) {
return SearchPage(
datas: datas,
);
}));
},
child: Container(
height: 44,
color: WeChatThemColor,
padding: EdgeInsets.only(left: 5, right: 5, bottom: 5),
child: Stack(alignment: Alignment.center, children: [
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(6.0),
),
), //白色底
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image(
image: AssetImage('images/放大镜.png'),
width: 15,
color: Colors.grey,
),
Text(
'搜索',
style: TextStyle(fontSize: 15, color: Colors.grey),
)
],
)
]),
),
);
}
}
class SearchBar extends StatefulWidget {
final ValueChanged onChanged;
const SearchBar({this.onChanged});
@override
_SearchBarState createState() => _SearchBarState();
}
class _SearchBarState extends State {
final TextEditingController _controller = TextEditingController();
bool _showClear = false;
_onChange(String text) {
if (text.length > 0) {
widget.onChanged(text);
setState(() {
_showClear = true;
});
} else {
widget.onChanged('');
_showClear = false;
setState(() {});
}
}
@override
Widget build(BuildContext context) {
return Container(
height: 84,
color: WeChatThemColor,
child: Column(
children: [
SizedBox(
height: 40,
),
Container(
height: 44,
// color: Colors.red,
child: Row(
children: [
Container(
width: ScreenWidth(context) - 40,
height: 34,
margin: EdgeInsets.only(left: 5),
padding: EdgeInsets.only(left: 5, right: 5),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(6.0)),
child: Row(
children: [
Image(
image: AssetImage('images/放大镜b.png'),
width: 20,
color: Colors.grey,
),
Expanded(
flex: 1,
child: TextField(
controller: _controller,
onChanged: _onChange,
autofocus: true,
cursorColor: Colors.green,
style: TextStyle(
fontSize: 18.0,
color: Colors.black,
),
decoration: InputDecoration(
contentPadding:
EdgeInsets.only(left: 5, bottom: 10),
border: InputBorder.none,
hintText: '搜索'),
)),
_showClear
? GestureDetector(
onTap: () {
setState(() {
_controller.clear();
_onChange('');
});
},
child: Icon(
Icons.cancel,
size: 20,
color: Colors.grey,
),
)
: Container(), //取消按钮
],
),
), //左边的圆角背景
GestureDetector(
onTap: () {
Navigator.pop(context);
},
child: Text('取消'),
)
],
),
) //下面的搜索条
],
),
);
}
}
3.1、SearchCell
这个是chat_page 中 cell 第一个显示的,就是一个样式,然后点击进行跳转到search_page。并且将chat_page,中的数据传过去
onTapDown: (d) {
Navigator.of(context).push(MaterialPageRoute(builder: (context) {
return SearchPage(
datas: datas,
);
}));
},
3.2、_onChange
输入框显示内容的调用
根据输入框内是否有内容显示小叉
3.3、searchBar 是导航栏上显示的搜索
在4、search_page 中调用
body: Column(
children: [
SearchBar(
onChanged: (text) {
_searchData(text);
},
),
4、search_page.dart
import 'dart:core';
import 'package:flutter/material.dart';
import 'package:wechat/pages/chat/search_bar.dart';
import 'chat_page.dart';
class SearchPage extends StatefulWidget {
final List datas;
const SearchPage({this.datas});
@override
_SearchPageState createState() => _SearchPageState();
}
class _SearchPageState extends State {
List _models = [];
String _searchStr = '';
TextStyle _normalStyle = TextStyle(
fontSize: 16,
color: Colors.black,
);
TextStyle _highlightStyle = TextStyle(
fontSize: 16,
color: Colors.green,
);
Widget _title(String name) {
List spans = [];
List strs = name.split(_searchStr);
for (int i = 0; i < strs.length; i++) {
String str = strs[i]; //拿出字符串
if (str == '' && i < strs.length - 1) {
spans.add(TextSpan(text: _searchStr, style: _highlightStyle));
} else {
spans.add(TextSpan(text: str, style: _normalStyle));
if (i < strs.length - 1) {
spans.add(TextSpan(text: _searchStr, style: _highlightStyle));
}
}
}
return RichText(
text: TextSpan(children: spans),
);
}
void _searchData(String text) {
//每次进来都是重新搜索
_models.clear();
_searchStr = text;
if (text.length > 0) {
for (int i = 0; i < widget.datas.length; i++) {
String name = widget.datas[i].name;
if (name.contains(text)) {
_models.add(widget.datas[i]);
}
}
}
setState(() {});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
SearchBar(
onChanged: (text) {
_searchData(text);
},
),
Expanded(
flex: 1,
child: MediaQuery.removePadding(
context: context,
removeTop: true,
child: ListView.builder(
itemCount: _models.length, itemBuilder: _buildCellForRow),
))
],
),
);
}
Widget _buildCellForRow(BuildContext context, int index) {
return ListTile(
title: _title(_models[index].name),
subtitle: Container(
alignment: Alignment.bottomCenter,
padding: EdgeInsets.only(right: 10),
height: 25,
child: Text(
_models[index].message,
overflow: TextOverflow.ellipsis,
),
),
leading: Container(
width: 44,
height: 44,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6.0),
image:
DecorationImage(image: NetworkImage(_models[index].imageUrl))),
), //聊天cell
);
}
}
通过输入框的输入内容和 传过来的数据 datas,进过计算得出搜索结果
body: Column(
children: [
SearchBar(
onChanged: (text) {
_searchData(text);
},
),
遍历数据,将name 包含搜索内容的结果天交到_models 里面。切记需要先清空,_models.clear();
void _searchData(String text) {
//每次进来都是重新搜索
_models.clear();
_searchStr = text;
if (text.length > 0) {
for (int i = 0; i < widget.datas.length; i++) {
String name = widget.datas[i].name;
if (name.contains(text)) {
_models.add(widget.datas[i]);
}
}
}
setState(() {});
}