内容简介:大家好,经过几个月的潜水,Flutter出乎意料的火热,抱歉一直没有更新,由于加入了创业团队,经历了几波大起大落,现在终于腾出时间搞搞技术,现在和成都的几位技术极客合作推出门路网,正在用flutter实践开发APP,也算是对flutter商业化的小试牛刀,本篇将门路网APP用到的flutter技术进行简单分享。之前的新闻APP的实践项目中,用到了首先,我们自己写一个TabBar玩玩,为什么呢?因为这样可以实现控件的高度自定义,顺便学一学新的组件用法:
大家好,经过几个月的潜水,Flutter出乎意料的火热,抱歉一直没有更新,由于加入了创业团队,经历了几波大起大落,现在终于腾出时间搞搞技术,现在和成都的几位技术极客合作推出门路网,正在用flutter实践开发APP,也算是对flutter商业化的小试牛刀,本篇将门路网APP用到的flutter技术进行简单分享。
之前的新闻APP的实践项目中,用到了 Tab+TabBarView+Tabcontroller 的用法,实现了基于 scaffold 下顶部标签页的页面切换,但是大家都会遇到来回切换页面导致 TabBarView 自动重绘的问题,页面无法停留到切换前的状态,这个问题也是困扰了我很久,用PageStorageKey搭配 Stack + Offstage 解决这个问题。
首先,我们自己写一个TabBar玩玩,为什么呢?因为这样可以实现控件的高度自定义,顺便学一学新的组件用法:
class NewsTab {
String text;
String tab;
NewsTab(this.text,this.tab);
}
//定义tab页基本数据结构
final List<NewsTab> NewsTabs = <NewsTab>[
new NewsTab('金融','financial'),
new NewsTab('科技','technology'),
new NewsTab('医疗','medical'),
];
class TabNavigation extends StatelessWidget {
TabNavigation({this.currentTab, this.onSelectTab});
final NewsTab currentTab;
final ValueChanged<NewsTab> onSelectTab; //这个参数比较关键,仔细理解下,省了setState()调用的环节
@override
Widget build(BuildContext context) {
return Row(
children: NewsTabs.map((item){
return GestureDetector( //手势监听控件,用于监听各种手势
child: Container(
padding: EdgeInsets.fromLTRB(24.0, 0.0, 24.0, 0.0),
child: Text(item.text,style: TextStyle(color: _colorTabMatching(item: item)),),
),
onTap: ()=>onSelectTab(item,)
//onSelectTab函数的使用非常巧妙,
//相当于定义了一个接口,可操控当前控件以外的数据
);
}).toList()
);
}
//定义tab被选中和没被选中的颜色样式
Color _colorTabMatching({NewsTab item}) {
return currentTab == item ? Colors.black : Colors.grey;
}
}
复制代码
为什么要这么做呢?因为我们可以通过 onSelectTab 函数对外部数据进行控制,主页面调用 TabNavigation :
class _MainListState extends State<MainList> {
NewsTab _currenttab = NewsTabs[0]; //定义默认打开的Tab页
void _selectTab(NewsTab tab){ //修改状态值
setState(() {
_currenttab = tab;
});
}
TabNavigation(
currentTab: _currenttab,
onSelectTab: _selectTab,
),
....
}
复制代码
当使用 TabNavigation 时,向其传入定义好的 _selectTab 函数,即可完成状态值修改的任务,这也是子控件向父控件传递参数的一种方式,特别适用于子控件修改父控件状态值时的场景。
以上是 Tab 标签和主页面的定义,接下来看 Tab 页的定义:
class NewsList extends StatefulWidget{
@override
NewsList({this.newsType,this.pageKey});
final PageStorageKey<NewsTab> pageKey; //当前控件唯一标识Key
final String newsType;
NewsListState createState() => new NewsListState();
}
class NewsListState extends State<NewsList>{
....
}
复制代码
注意看控件唯一标识Key的定义,有关 PageStorageKey 的说明请参考官方阅读理解,看不懂可以用谷歌翻译过一遍,这里不做赘述了,关键在 PageStorageKey<NewsTab> 中的 <NewsTab> 。 PageStorageKey 是局部Key,在父控件中定义时不要重复即可,所以我用了 NewsTab 类型,当然小伙伴也可以定义其他不会重复的值作为标识,不过可能会比我这个麻烦一点,想知道为啥,因为在主页面下是这样定义和使用 Key 的:
class MainList extends StatefulWidget {
const MainList({ Key key }) : super(key: key);
@override
_MainListState createState() => new _MainListState();
}
class _MainListState extends State<MainList> {
//定义Key值,类型名即是构造函数,需要传入匹配类型的参数
Map<NewsTab, PageStorageKey<NewsTab>> pageKeys = {
NewsTabs[0]: PageStorageKey<NewsTab>(NewsTabs[0]),
NewsTabs[1]: PageStorageKey<NewsTab>(NewsTabs[1]),
NewsTabs[2]: PageStorageKey<NewsTab>(NewsTabs[2]),
};
...
Widget build(BuildContext context){
return Scaffold(
...
body: Stack( //Stack在初始化时,会将子控件全部渲染,而TabBarView则仅渲染默认子控件
children: NewsTabs.map((item) {
return Offstage( //使用Offstage,把不需要显示的子控件隐藏起来
offstage: _currenttab != item,
child: NewsList(
pageKey: pageKeys[item], //传入Key值
newsType: item.tab),
);
}).toList(),
)
}
}
}
复制代码
这里用到了 Stack + Offstage 的组合,特性在注释中可了解,由于这两个控件可以保留子控件的特性,再加上 PageStorageKey<NewsTab> 标识,即可以保证 NewsList 在控件树中的位置保持不变,从而避免了 NewsList 被切换后重复渲染的问题。
为了方便理解,我把 pageKeys 的定义和使用分开进行,也就是在列表控件 NewsList 初始化的时候,即为其分配了一个 PageStorageKey<NewsTab> 类型的key值,保证它需要重复使用的时候不被flutter认为是新控件,也就不会触发重绘了。当然你也可以这么写:
NewsList( pageKey: PageStorageKey<NewsTab>(item), newsType: item.tab), ) 复制代码
以上两种方式,不管怎么写,都会通过遍历 NewsTabs 获取 NewsTab ,这样创建 PageStorageKey 方便不少。
为什么没有用 GlobalKey ?因为用不上,一方面 GlobalKey 比较耗费资源,存在于APP的整个生命周期,如同全局变量,另一方全局不允许重复定义,万一在别的地方需要重建相同控件,还得费脑子想办法避开相同的 GlobalKey ,免得捅出其他篓子。另外补充一点,只有有状态控件才能使用 GlobalKey ,一看 GlobalKey 的定义你就明白了: GlobalKey<State<StatefulWidget>> ,是给 StatefulWidget 下的 State 类使用的。
动图对比一下 Tab+TabBarView+Tabcontroller 和 PageStorageKey+Stack+Offstage :
可以看到,在页面初始渲染和切换时,两者的区别,前者初始化时仅渲染了一个Tab页,页面切换时每个Tab页都会自动 dispose 掉,并且新页面要重新 initState ,而后者则在初始化时即渲染了所有子Tab页,页面切换时没有 dispose ,而是仅调用了有状态控件的 didChangeDependencies 事件。
源码地址请 点击此处 ,本次分享仅做解决方案上的思考,也许还有更好的方案,欢迎大家分享。
此处感谢JarvanMo的分享才有了以上的解决方案
另外再总结几个小问题
1.当项目打包APK后,再次修改代码运行,有一定几率遇到新代码不生效解决办法: 1). 打开flutter下的这个目录:[你的地址]\flutter\bin 2). 删除cache文件夹 3). 命令行中输入:flutter doctor 4). 等待处理结束后,再次flutter run
我以为直接用命令: flutter clean 删除项目目录下的build文件夹,重新运行一下就可以解决问题,没想到运行后报错:
flutter clean
会直接删除整个build文件夹,都不带放回收站的,然后就悲剧了,整个项目没法运行,这时候你需要一句命令满血复活:
flutter create -i objc。
- -i 是表示iOS项目开发语言,objc和swift两个选项,其中objc是默认的。
- -a 是表示Android项目开发语言,java和kotlin两个选项,其中 java 是默认的 此处感谢JarvanMo的 分享
2.使用 Navigator 做页面跳转时,记得在其使用它的父控件构造函数或函数中添加 BuildContext 属性
BuildContext 属性在flutter中的意义是控件在控件树中的锚点,也可以理解为索引,当需要跳转页面时,需要告诉 Navigator 当前控件的锚点,以便于在新页面中点击返回键时,可以回退到原来的页面,英文好的同学可以查看原阅读理解。实际上 Navigator 也是基于此锚点创建页面锚点堆栈,所以当你需要对一个写的很深的子控件触发页面跳转时,需要把 context 参数从顶层父控件一层一层往下传。 控件函数中加入 BuildContext context 参数的意义是让控件明白:我是谁,我从哪里来,要到哪里去 ,比如:
//这里加入了BuildContext context,是为了把获取到的context传递到子控件,以用于Navigator做页面跳转
_list(BuildContext context, List dataList){
....
return ListView.builder(
// padding: const EdgeInsets.all(16.0),
itemCount: dataList.length,
itemBuilder: (context, i) {
//context参数相当于当前控件在控件树中的锚点,
//缺少这个参数会导致列表中的项目无法通过MaterialPageRoute进入下一个页面
return _newsRow(dataList[i],context);
}
);
}
//这里又需要定义context,是从上面的_list传下来的
_newsRow(Map newsInfo,BuildContext context){
return ListTile(
...
onTap: (){
Navigator.of(context).push( //直到被Navigator.of(context)用到
MaterialPageRoute(
builder: (BuildContext context) => NewsDetail(
id:newsInfo["id"].toString()
)
)
);
},
);
}
复制代码
那么 控件树 是啥呢?相信大家在写页面布局的时候应该感受到了什么叫父子控件,整个flutter项目就是N个父子控件串起来的控件树。
3. 从网络获取的 json 数据内包含数组,无法直接被 List.add() 或 List.addAll() 这个问题需要处理两个问题:
- 用于保存数据的
List对象,必须要进行初始化,否则直接调用list.add()会报null错误:
List
-
获取到的json数据键值对有数据的情况下,无法直接赋值到定义好的
List<Map> list,需要重新组装数据,由于获取到的json键值对中有这样格式的数据://获取到的json数据 data:{items:[{'k1':'v1'},{'k2':'v2'},{'k3':'v3'},{'k4':'v4'},...]} 复制代码
我便直接赋值给了上面定义的 list 变量:
List<Map> list = new List();
list = request['data']['items'];
复制代码
结果就悲剧了,一直报这个错:
所以,从网络请求获取到的 json 数据默认是 Iterable<Map<dynamic,dynamic>> 格式,无法直接赋值给 List 对象,因此需要做一下处理:
List<Map> a = new List(); //这句new很重要,数组对象实例化,否则无法运行a.add()
if (request['success']==true){
for(int i=0;i<request['data']['items'].length;i++){
a.add(request['data']['items'][i]); //
}
return a; //此处的意义便是把网络请求获取到的数据标准化,否则无法直接赋值给dataList
}else return null;
复制代码
这样就好多了,当然,如果你做了json序列化,请无视这个问题。
篇幅较长见谅,在此感谢大家的支持,想继续了解更多Flutter技巧,请关注Flutter圈子,欢迎大牛向这里投稿布道,也可以加入flutter 中文社区(官方QQ群:338252156)共同成长,谢谢大家~
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- RocketMQ 平滑升级到主从切换(实战篇)
- # 前端每日实战 168# 视频演示如何利用 Web Animation API 制作一个切换英语单词的交互动画
- Egret场景切换管理类切换和单例使用方法
- Spring项目中使用两种方法动态切换数据源,多数据源切换
- Pear Admin Ant 1.1.0.Release 正式发布,新增布局切换、主题切换、工作空间
- MySQL -- 主从切换
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
解放战争(上)(1945年8月—1948年9月)
王树增 / 人民文学出版社 / 2009-8 / 60.00
《解放战争》为王树增非虚构文学著述中规模最大的作品。武器简陋、兵力不足的军队对抗拥有现代武器装备的兵力庞大的军队,数量不多、面积有限的解放区最终扩展成为九百六十万平方公里的共和国,解放战争在短短四年时间里演绎的是人类历史上的战争传奇。国际风云,政治智慧,时事洞察,军事谋略,军队意志,作战才能,作品具有宏阔的视角和入微的体察,包含着惊心动魄的人生沉浮和变幻莫测的战场胜负,尽展中国历史上规模最大的一场......一起来看看 《解放战争(上)(1945年8月—1948年9月)》 这本书的介绍吧!
JSON 在线解析
在线 JSON 格式化工具
HEX CMYK 转换工具
HEX CMYK 互转工具