flutter实战6:TAB页面切换免重绘

大家好,经过几个月的潜水,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+TabcontrollerPageStorageKey+Stack+Offstage

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文件夹,重新运行一下就可以解决问题,没想到运行后报错:

缺少build文件夹报错

flutter clean会直接删除整个build文件夹,都不带放回收站的,然后就悲剧了,整个项目没法运行,这时候你需要一句命令满血复活:

flutter create -i objc。

  • -i 是表示iOS项目开发语言,objc和swift两个选项,其中objc是默认的。
  • -a 是表示Android项目开发语言,java和kotlin两个选项,其中java是默认的
    此处感谢JarvanMo分享

2.使用Navigator做页面跳转时,记得在其使用它的父控件构造函数或函数中添加BuildContext属性

缺少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<Map> list = new 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)共同成长,谢谢大家~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,568评论 25 707
  • 祝澜 焦点网络中8 分享153天 约练第7次 2018-5-28 下午3点,和另外两位老师在6房间约练。期待什...
    祝澜阅读 117评论 0 0
  • 信任是一种宝贵的,但又非常脆弱的人际关系。建立和维持信任关系很难,摧毁它却很容易。也许不经意间的一句话,一个眼神,...
    大伟传说阅读 506评论 0 4
  • 懒懒散散的度过了一天,到是给自己做了一顿饭,一个人的日子,煮饭是很难得的。也是无聊,想着,给自己做饭也是爱自己的方...
    再见Sarah1992阅读 111评论 0 1
  • 想拜名师,起码自己也得是高徒,前段时间老跑医院,见到有位病人拿着图片让大夫看,大夫看看不慌不忙说“拿着片子去...
    拉起手阅读 253评论 0 2