DartSDK向下兼容问题
dart_sdk
2.12.0版本以上会存在空安全
的问题,例如声明属性final String? imageUrl;
需要添加?
。
// 查看dart_sdk版本
$ dart --version
Dart SDK version: 2.13.4 (stable) (Wed Jun 23 13:08:41 2021 +0200) on "macos_x64"
如果声明属性final String imageUrl;
报错,说明pubspec.yaml
文件配置的dart_sdk
版本是>=2.12.0
,不兼容2.12.0
以下的版本。可以更改dart_sdk
兼容版本为>=2.7.0 <3.0.0
,同时点击Pub get
进行配置就能解决报错。
配置dart_sdk
的两种方式
- 点击
Pub get
进行配置 -
Terminal
终端执行$ flutter pub get
命令进行配置
推荐dart_sdk
兼容版本是>=2.12.0
,该版本以上是flutter添加空安全
的一次变革。
之前我们在开发friends_page.dart
页面时,自定义了_FriendCell
类,通讯录页面顶部四个栏目用的是本地图片资源
,底部列表排序用的是网络图片资源
,代码如下
// dart_sdk 2.7.0写法
class _FriendCell extends StatelessWidget {
......
Container(
margin: EdgeInsets.all(10),
width: 34,
height: 34,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6.0),
image: DecorationImage(
image: imageUrl != null
? NetworkImage(imageUrl)
: AssetImage(imageAssets),
)
),
), //图片
// dart_sdk 2.12.0以上写法
Container(
margin: EdgeInsets.all(10),
width: 34,
height: 34,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6.0),
// 装饰器image
image: DecorationImage(
image: imageUrl != null
? NetworkImage(imageUrl!) as ImageProvider
: AssetImage(imageAssets!),
),
),
), //图片
滚动ListView
上篇文章通讯录与索引条中,我们实现了拖动索引条可以定位到索引条的具体标识
,下面我们需要把索引条标识
回调出去给到ListView
,让ListView
能够准确滚动到具体分组。
-
index_bar.dart
文件添加回调
class IndexBar extends StatefulWidget {
// 对外提供回调,告诉friends_page当前选中的是标识
final void Function(String str) indexBarCallBack;
IndexBar({this.indexBarCallBack});
@override
State<StatefulWidget> createState() => _IndexBarState();
}
- 拖动
IndexBar
把具体标识回调出去
// 添加手势
child: GestureDetector(
// 拖拽手势
onVerticalDragUpdate: (DragUpdateDetails details) {
// String str = getIndex(context, details.globalPosition);
// print('选中的是$str');
// 把具体索引标识回调出去
widget.indexBarCallBack(getIndex(context, details.globalPosition));
},
// 点击索引
onVerticalDragDown:(DragDownDetails details){
// 把具体索引标识回调出去
widget.indexBarCallBack(getIndex(context, details.globalPosition));
setState(() {
_bkColor = Color.fromRGBO(1, 1, 1, 0.5);
_textColor = Colors.white;
});
},
......
- 悬浮索引条获取回调标识
// 悬浮索引条
IndexBar(
indexBarCallBack: (String str){
// 获取索引标识
},
)
-
ListView
在构建的时候可以指定ScrollController
,这里实例化_scrollController
用于ListView
的滚动偏移
class _FriendsPageState extends State<FriendsPage> {
ScrollController _scrollController;
@override
// _FriendsPageState页面载入的时候会执行,热重载是不会执行的
void initState() {
// TODO: implement initState
super.initState();
_scrollController = ScrollController();
......
body: Stack(
children: [
Container(
color: WeChatThemeColor,
child: ListView.builder(
// ListView指定_scrollController
controller: _scrollController,
itemBuilder: _itemForRow,
itemCount: _listDatas.length + _headerData.length,
),
),
......
- 构造数据源
计算出索引条标识具体偏移量
_groupOffsetMap
字典用于存储索引条标识
与偏移量
class _FriendsPageState extends State<FriendsPage> {
double _cellHeight = 54.5;
double _groupHeight = 30.0;
// 字典,里面放item和高度对应的数据
final Map _groupOffsetMap = {
INDEX_WORDS[0]: 0.0,
INDEX_WORDS[1]: 0.0,
};
final List<Friends> _headerData = [
Friends(imageAssets: 'images/新的朋友.png', name: '新的朋友'),
Friends(imageAssets: 'images/群聊.png', name: '群聊'),
Friends(imageAssets: 'images/标签.png', name: '标签'),
Friends(imageAssets: 'images/公众号.png', name: '公众号'),
];
final List<Friends> _listDatas = [];
ScrollController _scrollController;
@override
// _FriendsPageState页面载入的时候会执行,热重载是不会执行的
void initState() {
// TODO: implement initState
super.initState();
_scrollController = ScrollController();
// 创建数据, 链式表达
_listDatas..addAll(datas)..addAll(datas); // 等价于 _listDatas.addAll(datas);_listDatas.addAll(datas);
// 数据排序
_listDatas.sort((Friends a, Friends b) {
return a.indexLetter.compareTo(b.indexLetter);
});
// 注意⚠️⚠️⚠️ 这里是计算具体标识偏移量
var _groupOffset = _cellHeight * _headerData.length;
//进过循环计算,将每一个头的位置算出来。放入字典
for (int i = 0; i < _listDatas.length; i++) {
if (i < 1) {
//第一个cell一定有头!
_groupOffsetMap.addAll({_listDatas[i].indexLetter: _groupOffset});
//保存完了再加_groupOffset
_groupOffset += _cellHeight + _groupHeight;
} else if (_listDatas[i].indexLetter == _listDatas[i - 1].indexLetter) {
//相同分组,只需要加Cell的高度
_groupOffset += _cellHeight;
} else {
_groupOffsetMap.addAll({_listDatas[i].indexLetter: _groupOffset});
//保存完了再加_groupOffset
_groupOffset += _cellHeight + _groupHeight;
}
}
}
......
- 获取
索引条回调标识
让ListView
滚动到指定偏移位置
// 悬浮索引条
IndexBar(
indexBarCallBack: (String str){
// 选中右侧索引,listView滚动到固定位置
if (_groupOffsetMap[str] != null) {
_scrollController.animateTo(_groupOffsetMap[str],
duration: Duration(microseconds: 100),
curve: Curves.easeIn);
}
},
)
显示指示器
问题:上面指示器在滚动的时候,底部会有空白内容滑上去
解决思路:判断当前索引字符是否在屏幕内,如果在屏幕内就不用添加,下面滚动的时候也就不会偏移
var _groupOffset = _cellHeight * _headerData.length;
//进过循环计算,将每一个头的位置算出来。放入字典
for (int i = 0; i < _listDatas.length; i++) {
if (i < 1) {
......
} else if (_listDatas[i].indexLetter == _listDatas[i - 1].indexLetter) {
......
} else {
// 判断当前索引字符有没有超出屏幕,如果没有超出屏幕就不用添加,下面滚动的时候也就不会偏移
_groupOffsetMap.addAll({_listDatas[i].indexLetter: _groupOffset});
//保存完了再加_groupOffset
_groupOffset += _cellHeight + _groupHeight;
}
}
......
// 上面判断索引字符在屏幕内,就不会添加到_groupOffsetMap,这里就不会滚动偏移
// 选中右侧索引,listView滚动到固定位置
if (_groupOffsetMap[str] != null) {
_scrollController.animateTo(_groupOffsetMap[str],
duration: Duration(microseconds: 100),
curve: Curves.easeIn);
}
下面给IndexBar
添加选中索引的图片指示器
-
IndexBar
添加指示器Y值
、显示索引标识
、是否显示
等属性
class _IndexBarState extends State<IndexBar> {
// 索引条选中 背景色与文字颜色
Color _bkColor = Color.fromRGBO(1, 1, 1, 0.0);
Color _textColor = Colors.black;
// 添加图片指示器Y值,显示索引标识,是否显示等属性
double _indicatorY = 0.0;
String _indicatorText = 'A';
bool _indicatorHidden = true;
......
- 修改
getIndex
方法返回类型为索引index
// 获取选中的item的字符!!
// 修改为返回索引下标
int getIndex(BuildContext context, Offset globalPosition) {
// 获取当前小部件的盒子
RenderBox? box = context.findRenderObject() as RenderBox?;
// 获取y值 globalToLocald当前位置距离部件原点(左上角)的位置
double y = box!.globalToLocal(globalPosition).dy;
// 算出字符高度
var itemHeight = screenHeigth(context) / 2 / INDEX_WORDS.length;
// 算出第几个item, clamp约束index范围值
int index = (y ~/ itemHeight).clamp(0, INDEX_WORDS.length - 1);
return index;
}
-
IndexBar
添加图片指示器
,右侧索引条拖动时更新属性值
// TODO: implement build
return Positioned(
right: 0.0,
top: screenHeigth(context) / 8,
height: screenHeigth(context) / 2,
width: 120,
// 添加手势
child: Row(
children: [
Container(
alignment: Alignment(0, _indicatorY),
width: 100,
// color: Colors.red,
child: _indicatorHidden
? null
: Stack(
// 让气泡居中
alignment: Alignment(-0.2, 0),
children: [
Image(image: AssetImage('images/气泡.png'), width: 60,),
Text(
_indicatorText,
style: TextStyle(fontSize: 35, color: Colors.white),
)
],
),
), //指示器
GestureDetector(
// 拖拽手势
onVerticalDragUpdate: (DragUpdateDetails details) {
// String str = getIndex(context, details.globalPosition);
// print('选中的是$str');
int index = getIndex(context, details.globalPosition);
widget.indexBarCallBack(INDEX_WORDS[index]);
setState(() {
_indicatorY = 2.2 / INDEX_WORDS.length * index - 1.1;
_indicatorText = INDEX_WORDS[index];
_indicatorHidden = false;
});
},
// 点击索引
onVerticalDragDown:(DragDownDetails details){
int index = getIndex(context, details.globalPosition);
widget.indexBarCallBack(INDEX_WORDS[index]);
setState(() {
_indicatorY = 2.2 / INDEX_WORDS.length * index - 1.1;
_indicatorText = INDEX_WORDS[index];
_indicatorHidden = false;
_bkColor = Color.fromRGBO(1, 1, 1, 0.5);
_textColor = Colors.white;
});
},
// 点击结束,颜色值还原
onVerticalDragEnd:(DragEndDetails details){
setState(() {
_indicatorHidden = true;
_bkColor = Color.fromRGBO(1, 1, 1, 0.0);
_textColor = Colors.black;
});
},
child: Container(
// 设置宽度,让右侧索引居中
width: 20,
color: _bkColor,
child: Column(
children: words,
),
),
), //索引条
],
),
);
聊天页面导航条
AppBar
里面有个属性actions
,可以设置导航栏上面的一些按钮。我们现在要实现的是聊天界面,导航栏右边的按钮被点击,会弹出菜单列表
。系统就提供了一个PopupMenuButton
,我们可以拿来直接使用。
PopupMenuButton
的一些属性
-
color
: 下拉框的背景颜色 -
offset
:下拉框距离导航栏的偏移量 -
child
:是指导航栏上按钮,就是你要点击的那个按钮 -
temBuilder
:这个是下拉框里面的内容了 -
PopupMenuItem
:这个是下拉框里面的每个item
,是一个Widget
class _ChatPageState extends State<ChatPage> {
// PopupMenuItem自定义item
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),
),
],
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: WeChatThemeColor,
title: const Text('微信页面'),
// 让AppBar的title居中显示
centerTitle: true,
actions: [
Container(
margin: EdgeInsets.only(right: 10),
// PopupMenuButton自定义菜单栏
child: PopupMenuButton(
color: Color.fromRGBO(1, 1, 1, 0.65),
// 让PopupMenuButton菜单栏向下偏移60
offset: Offset(0, 60.0),
child: Image(
image: AssetImage('images/圆加.png'),
width: 25,
),
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', '收付款')),
];
},
),
)
],
),
body: const Center(
child: Text('微信页面'),
),
);
}
}
准备网络数据
RAP网站搭建服务器,准备网络数据
- 注册好账号,
仓库
->新建仓库
->填写仓库信息
- 可以使用
示例接口
- 点击
删除模版
删除示例接口,新建接口
-
编辑接口
->保存
这时候浏览器查看接口地址
,就能看到接口数据
编辑接口数据
Mock自定义接口数据规则 -> 示例
生成假用户信息 -> 我们这里只需要生成用户头像即可
头像地址
只需要更改数字即可改变不同头像
,example:1.jpg
2.jpg
发送网络请求
Dart三方库,网络请求库dio
、http
引入三方库
-
pubspec.yaml
文件配置网络请求库
-> 点击Pub get
载入三方库
- 导入网络请求头文件
import 'package:http/http.dart' as http;
- 网络请求获取数据
class _ChatPageState extends State<ChatPage> {
@override
void initState() {
super.initState();
//获取网络数据
getDatas();
}
// 异步方法,但是不一定切换线程
getDatas() async {
final url =
Uri.parse('http://rap2api.taobao.org/app/mock/256798/api/chat/list');
// 等待网络请求返回结果
final response = await http.get(url);
print(response.statusCode);
print(response.body);
}
......
多线程可以是异步的
,但异步不代表多线程