新收藏flutterflutter

Flutter 改善套娃地狱问题(仿喜马拉雅PC页面举例)

2021-03-15  本文已影响0人  小呆呆666

前言

这篇文章是我一直以来很想写的一篇文章,终于下定决心动笔了。

写Flutter的小伙伴可能都感受到了:掘金的一些热门的Flutter文章下,知乎的一些Flutter的话题下或者一些论坛里面,喷Flutter套娃地狱总是永不过时的一个话题。

如果你不服气,上去辩驳俩下:“嵌套是你代码习惯问题,你看我,抬手一个Row,反手一个Column,在children中把widget一提,层次分明,年轻人望你耗子尾汁,莫要瞎带节奏”;然后你可能就被一群人喷成狗,大意了,这帖子没同一阵营的小伙伴,喷不过,闪了闪了;一般被喷后,不是身经百被喷,都需要一段时间来平复心情。。。

所以,终于我下定决心把这篇文章肝出来,如果你认真看完,你可能会发现:嵌套什么的都是浮云,从此你的页面代码将变的超级好维护,交互逻辑入口,也变得层次分明。

全篇文章,绝无教大家做事之意,这是在项目中摸爬滚打,被坑出的不得不如此规范的一种行为。

准备

改善

这篇文章能帮你改善什么问题?

上面关于页面层的这些问题,如果多人协同开发一个大型项目,代码不规范的话,大概率都是会遇到的(改别人写的模块...);后期改需求 ,真的是一种折磨,有种码海找针的感觉。

如果改你自己写的模块,那可能还会好点,毕竟你还有点印象,整个模块的大概思路,还知道怎么改。如果是改别人写的模块,你就需要在大量widget海中,去揣摩别人写这些widget的意图,结构一下子也不能理清,十分痛苦,有可能边改边骂骂咧咧的。。。

Demo效果

在构思文章的时候,就在想演示的Demo页面必定不能过于简单,一个简单的Demo页面,怎么能演示出套娃地狱的改善效果呢?思考了很久,想寻找一个合适demo页面,周末时在听喜马拉雅里面的盗墓小说,看了看发现页面,发现整体样式不错,咱就仿一个吧!而且整体的页面复杂度,也足够来演示了!

喜马拉雅的这个PC页面Demo,写起来真的花费了不少时间,希望能对大家有所帮助吧。

地址

说明

代码已经发布到Github上,web端也已经部署好了,因为使用的CanvasKit模式打包的,首次加载可能比较慢,多等一会,因为Web端部署在Github上,访问的话,要确保你的网络能访问Github。

效果对比

来对比下仿制的效果吧,有个六七成相似,很多Icon和图片实在找不到相似,,,这里demo只提供一个样式演示,功能别想了,这不是一朝一夕,一个人能搞出的。。。

照片都是从喜马拉雅web端上搞下来的,数据一直在变,相应栏目的数据有对不上,但是整体样式大致还是差不多。

其中Banner模块是区别最大的一块,用的三方库只能支持搞成这样,各位靓仔将就着看看吧。

image-20210314165954339 image

总结

上面俩组图片,细节方面对比基本惨不忍睹,但是整体架构上还是比较相似。

建议各位彦祖,下载下window安装包,安装体验下;MacOS的于晏们,你们可以看看web展示效果。

咱们马上来看看怎么搞规范代码吧!复杂的模块,让你的代码也能高度可维护!

开搞

分析

结合上面的业务View和一切皆Widget的思路,我们可以得出一个结论:搞业务Widget,然后再进行组合!

当然,咱们在这里得出了一个不是结论的结论,一般来说,这种操作是咱们基本素养,但是具体的操作细节上,还是有很多需要注意的:

主模块封装

上面咱们一通分析猛如虎后,得出一个结论:搞业务Widget!

关于业务Widget的封装细节,这里说明下:

主模块的结构

这里使用了一点Getx知识,如果你不了解,可参考:Flutter GetX使用---简洁的魅力!

class HimalayaPage extends StatelessWidget {
  final logic = Get.put(HimalayaLogic());
  final state = Get.find<HimalayaLogic>().state;

  @override
  Widget build(BuildContext context) {
    return himalayaBuildBg(children: [
      //顶部:左边侧边导航栏 + 右边信息流
      himalayaBuildTopBg(children: [
        //左边导航栏
        HimalayaLeftNavigation(
          data: state,
          //导航栏item回调
          onTap: (HimalayaSubItemInfo item) => logic.navigationItem(item),
        ),

        //右边信息流
        himalayaBuildInfoListBg(children: [
          //顶部搜索框及其一些个人信息设置按钮
          HimalayaPersonalInfo(
            //搜索框输入监听
            onChanged: (String msg) => logic.onSearch(msg),
            //左箭头
            onLeftArrow: () => logic.dealLeftArrow(),
            //右箭头
            onRightArrow: () => logic.dealRightArrow(),
            //刷新按钮
            onRefresh: () => logic.onRefreshData(),
            //皮肤按钮
            onSkin: () => logic.switchSkin(),
            //设置按钮
            onSetting: () => logic.onSetting(),
          ),

          //右侧信息流 - 可滑动部分
          himalayaBuildScrollInfoListBg(children: [
            //轮播图
            HimalayaBanner(
              data: state.bannerList,
              //具体banner的监听
              onTap: (int index) => logic.clickBanner(index),
            ),

            //猜你喜欢
            HimalayaGuess(
              data: state.guessList,
              //换一批
              onChange: () => logic.guessChange(),
              //猜你喜欢具体卡片
              onGuess: (HimalayaSubItemInfo item) => logic.guessDetail(item),
            ),

            //最新精选
            HimalayaNewest(
              data: state,
              //分类标题
              onSortTitle: (item) => logic.sortTitle(item),
              //具体精选卡片
              onNewest: (HimalayaSubItemInfo item) => logic.onNewest(item),
            ),

            //热门主播
            HimalayaAnchor(
              data: state.anchorList,
              onAnchor: (HimalayaSubItemInfo item) => logic.hotAnchor(item),
            ),

            //各类榜单
            HimalayaRankList(
              data: state.rankList,
              //标题
              onTitle: (String title) => logic.rankTitle(title),
              //榜单上具体item
              onItem: (HimalayaSubItemInfo item) => logic.rankItem(item),
            ),
          ]),
        ]),
      ]),

      //底部:音频播放控制台
      HimalayaAudioConsole(
        data: state.audioPlayInfo,
        //左切换
        onLeftArrow: () => logic.onLeftArrow(),
        //播放
        onPlay: () => logic.onPlay(),
        //右切换
        onRightArrow: () => logic.onRightArrow(),
        //喜欢
        onLove: () => logic.onLove(),
        //播放模式
        onPlayModel: () => logic.onPlayModel(),
        //封面
        onCover: () => logic.onCover(),
        //进度
        onProgress: () => logic.onProgress(),
        //音量
        onVolume: () => logic.onVolume(),
        //标题
        onSubtitle: () => logic.onSubtitle(),
        //倍速
        onSpeed: () => logic.onSpeed(),
        //定时
        onTiming: () => logic.onTiming(),
        //目录
        onCatalog: () => logic.onCatalog(),
      ),
    ]);
  }
}

经过上面的一通封装组合后,大家摸着良心说说:

别喷套娃了,外观模式的思想稍稍这么一用,套娃直接GG

设计模式,yyds!

细节分析

一般来说,一个页面整体基本上是横向(Row)或者纵向(Column)的结构

咱们仿造的喜马拉雅模块也是属于纵向结构:上下俩大模块

通过上面的说明,很明显,Row和Column中children属性才是我们所关注的,其它的细节描述封装起来即可

主体细节封装

主模块的很多主体细节,是完全可以封装起来的,新建一个(模块名_function)文件

///喜马拉雅整体外层布局设置
Widget himalayaBuildBg({required List<Widget> children}) {
  return Scaffold(
    backgroundColor: Colors.white,
    body: Column(children: children),
  );
}

///播放控制栏上面的外层布局设置
Widget himalayaBuildTopBg({required List<Widget> children}) {
  return Expanded(
    child: Row(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: children,
    ),
  );
}

///顶部右侧信息流外层布局设置
Widget himalayaBuildInfoListBg({required List<Widget> children}) {
  return Expanded(
    child: Column(children: children),
  );
}

///顶部右侧信息流外层布局设置 - 可滑动部分
Widget himalayaBuildScrollInfoListBg({required List<Widget> children}) {
  return Expanded(
    child: CustomSingleChildScrollView(
      child: Container(
        width: 860.dp,
        child: Column(children: children),
      ),
    ),
  );
}

业务Widget封装

关于业务Widget封装,是核心所在,这个非常重要

几个要点

children中封装

先来看看第一种情况,最常见的情况,children的widget,从上到下排列下来,非列表类数据

image-20210314212412718
///搜索框 个人信息 设置等按钮
class HimalayaPersonalInfo extends StatelessWidget {
  HimalayaPersonalInfo({
    Key? key,
    required this.onRefresh,
    required this.onLeftArrow,
    required this.onRightArrow,
    required this.onSetting,
    required this.onSkin,
    required this.onChanged,
  }) : super(key: key);

  .............

  @override
  Widget build(BuildContext context) {
    return _buildBg(children: [
      //左图标
      _buildLeftArrow(),

      //右图标
      _buildRightArrow(),

      //刷新图标
      _buildRefresh(),

      //搜索框
      _buildSearch(),

      //头像
      _buildHeadImg(),

      //皮肤
      _buildSkin(),

      //设置
      _buildSetting(),
    ]);
  }

  ..........
}
///搜索框 个人信息 设置等按钮
class HimalayaPersonalInfo extends StatelessWidget {
  ........

  Widget _buildBg({required List<Widget> children}) {
    return Container(
      margin: EdgeInsets.symmetric(vertical: 10.dp, horizontal: 18.dp),
      width: 800.dp,
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: children,
      ),
    );
  }
}
image-20210314214406466 image-20210314214520198

单层列表样式封装

类列表样式的封装也是比较关键的,直接从头莽尾式的提取是不行,这边有一丝调整

这里就以猜你喜欢模块举例

image-20210314220037075
///猜你喜欢
class HimalayaGuess extends StatelessWidget {
  HimalayaGuess({
    Key? key,
    required this.data,
    required this.onChange,
    required this.onGuess,
  }) : super(key: key);

  ..........

  @override
  Widget build(BuildContext context) {
    return _buildBg(children: [
      //标题 + 换一批
      Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
        //标题
        _buildTitle(),

        //换一批
        _buildGuessChange()
      ]),

      //显示具体信息流
      _buildItemBg(itemBuilder: (item) {
        return [
          //图片卡片
          _buildPicCard(item),

          //文字描述
          Text(item.title, style: TextStyle(fontSize: 15.sp)),

          //子标题
          _buildSubTitle(item),
        ];
      })
    ]);
  }

  ..........
}
///猜你喜欢
class HimalayaGuess extends StatelessWidget {
  
  ...............

  Widget _buildItemBg({
    required List<Widget> Function(HimalayaSubItemInfo item) itemBuilder,
  }) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: List.generate(data.length, (index) {
        return Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: itemBuilder(data[index]),
        );
      }),
    );
  }
}

双层列表样式封装

关于双层列表数据源(List的每个具体数据源,又含有List)又该怎么封装呢?

image-20210314221811228
///数据源:侧边导航栏目初始数据,简化了下,数据源太长了
///该数据源都放在state层维护,此处放在这里,让大家有个对比
leftItemList = [
    HimalayaItemInfo(title: '推荐', subItemList: [
        HimalayaSubItemInfo(
            title: '发现',
            icon: CupertinoIcons.compass,
            tag: TagHimalayaConfig.find,
            isSelected: true,
        ),
        ..............
    ]),
    HimalayaItemInfo(title: '我听', subItemList: [
        HimalayaSubItemInfo(
            title: '我的订阅',
            icon: Icons.star_border,
            tag: TagHimalayaConfig.subscription,
        ),
        .........
    ]),
    HimalayaItemInfo(title: '我创建的听单', subItemList: [
        HimalayaSubItemInfo(
            title: '我喜欢的声音',
            icon: Icons.favorite_border,
            tag: TagHimalayaConfig.sound,
        ),
        ............
    ]),
];

///左边导航栏
class HimalayaLeftNavigation extends StatelessWidget {
  HimalayaLeftNavigation({
    Key? key,
    required this.data,
    required this.onTap,
  }) : super(key: key);

  ........

  @override
  Widget build(BuildContext context) {
    return _buildBg(children: [
      //喜马拉雅logo图标
      _buildLogo(),

      //遍历俩层循环:不同item栏目 - 可点击,可滑动
      //第一层:标题 + 子item列表
      //第二层:子item详细布局
      _buildItemListBg(itemBuilder: (item) {
        return [
          //最外层item - 大标题
          _buildTitle(item.title),

          //子栏目 - 列表
          _buildSubItemListBg(item, subBuilder: (subItem) {
            return [
              //选中红色长方形条块
              _buildRedTag(subItem),

              //图标
              _buildItemIcon(subItem),

              //描述
              _buildItemDesc(subItem),
            ];
          })
        ];
      }),
    ]);
  }
    
  ..........
}
class HimalayaLeftNavigation extends StatelessWidget {
  ..........

  Widget _buildItemListBg({
    required List<Widget> Function(HimalayaItemInfo item) itemBuilder,
  }) {
    return Expanded(
      child: Scrollbar(
        child: CustomSingleChildScrollView(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: List.generate(data.leftItemList.length, (index) {
              return Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: itemBuilder(data.leftItemList[index]),
              );
            }),
          ),
        ),
      ),
    );
  }
}
class HimalayaLeftNavigation extends StatelessWidget {
  ..........

  Widget _buildSubItemListBg(
    HimalayaItemInfo item, {
    required List<Widget> Function(HimalayaSubItemInfo item) subBuilder,
  }) {
    return Column(
      children: List.generate(item.subItemList.length, (index) {
        return InkWell(
          onTap: () => onTap(item.subItemList[index]),
          child: Container(
            padding: EdgeInsets.symmetric(vertical: 9.dp),
            child: Row(children: subBuilder(item.subItemList[index])),
          ),
        );
      }),
    );
  }
}

总结

经过上面的一通操作,业务Widget立马变的清爽N倍

大家在写Flutter的时候,应该能明显的感觉到,写页面拥有高度的自由,样式、页面结构及其逻辑全都能耦合在一起。

既然我们还达不到,无招胜有招的水平;那么下笔之前还是要有点章法的好,所以在实际开发中,要注意自己代码规范啊。。。

假设一种情况

题外话

说一点题外话

实际上写html也是无限套娃,不同的是,它从根本上做到的样式结构分离,控件的细节描述,全部交给了css去做,所以页面整体看上去还是满清爽的:

Flutter直接从根本上样式结构不分离,结构上直接从上往上下一套到底

所以,哪里有十全十美的框架,总是有舍有得。。。

新的事物发展,必然会迎来相应的阻力

这里假设一种场景:

角色互换

其实,对于很多言论,我们没必要在意;角色互换,说不定,对方此刻的行为,就是我们自己以后可能会做的事。

小丑竟是我自己是什么梗小丑竟是我自己是什么意思出处在哪-站长之家

其实,我们都是打工人,又何必撕来撕去呢?

最后

文中DEMO地址:flutter_use

系列文章

通过上面一些代码规范操作后,再配合上GetX的状态管理,相信一般的项目,你都能hold的住了

加油,我们都是这条街,最靓的仔

上一篇下一篇

猜你喜欢

热点阅读