Flutter 入门与实战(五十三):仿掘金个人主页,学习 FutureProvider 状态管理

·  阅读 1316
Flutter 入门与实战(五十三):仿掘金个人主页,学习 FutureProvider 状态管理

这是我参与8月更文挑战的第16天,活动详情查看:8月更文挑战

前言

好久没有讲界面的内容了,本篇借仿掘金个人主页的头部区域,一方面是讲一下 Flutter 的 Stack 层叠组件的用法,另一方面是 Provider 的 FutureProvider 的使用。声明一下,仿的掘金手机端个人主页头部区域如下图,实际还有底部的滑动后悬停部分没有完成,后续有时间的时候再完善整个界面,来个“假冒”掘金个人主页。

image.png

界面分析

拿到界面,我们首先分析一下界面的结构,头部这部分从下到上由如下内容组成:

  • 底图和头像
    • 底部 Banner 图:在最底部;
    • 头像:包括头像图片下的背景圆(或边框),叠加在底部 Banner 上;
  • 用户名称、级别、工作信息(头衔及公司)及个人介绍等个人信息:在底图和头像下方。
  • 关注、关注者和掘力值等统计数据:在用户信息下方。
  • 返回按钮:返回按钮在整个页面的顶层,以便可以随时点击返回。

得到我们页面的布局结构如下图所示。其中头像和 Banner 因为重叠了,需要使用一个 Stack 组件包裹,即蓝色的区域。 个人主页.png

组件结构

确定好了布局,我们再来确定使用什么样的组件,各个布局区域对应的组件如下:

  • 返回按钮:IconButton,使用一个返回图标按钮,点击后返回上一页。
  • Banner:使用 CachedImageNetWork,以便可以加载网络图片。
  • 头像:使用一个 Stack,底部是实现边框的圆形 Container,上层是圆形头像,使用 Container+CachedImageNetwork 实现。 同时整个头像区域使用 Positioned 绝对位置布局,保持左侧和返回按钮对齐,然后由有一半区域叠加在Banner上。
  • 个人信息区域:使用列布局 Column排布各项信息,然后昵称和等级使用行布局 Row
  • 统计信息区域:使用行布局 Row 排布各个统计数据,统计数据本身使用 Column 布局数字和数据项名称。

整个页面使用的是 CustomScrollView,然后再用 Stack包裹整个页面和返回按钮,并将返回按钮使用 Positioned 绝对定位保持在左上角。最后的组件树如下(省略了Container组件)。

个人主页组件树.png

重点介绍一下 StackPostioned 组件。Stack 组件分为StackIndexedStack。其中IndexedStack 我们在Flutter入门与实战(三):构建一个常用的页面框架有介绍过,其实就是一个有序的 Stack,可以通过控制当前序号显示第几层的界面。而 Stack 的 children 是一组组件,使用的是堆叠排序,次序在后面的层级越高,显示层级也更靠前(后进先出原则)。通过这种方式可以达到多个界面层叠显示的目的。

掘金个人主页-堆叠次序.png

Positioned 组件是专门用于 Stack 的子组件,用于控制 Stack 的子组件相对于 Stack 的位置,可以通过 lefttoprightbottomheight属性来控制子组件位置。

const Positioned({
  Key? key,
  this.left,
  this.top,
  this.right,
  this.bottom,
  this.width,
  this.height,
  required Widget child,
}) : assert(left == null || right == null || width == null),
     assert(top == null || bottom == null || height == null),
     super(key: key, child: child);
复制代码

具体规则如下:

  • 如果 top 属性不为空,就会将组件的顶部定位到 Stack 组件顶部距离 top单位的位置。其他如 leftrightbottom 的机制类似。
  • 如果 topbottom 都不为空,那么该组件就会按照约束条件限制在 Stack 中布局的高度(固定距离上下边界的位置)。leftright 不为空的时候就会限制宽度约束。
  • 如果 topbottom 只有一个不为空,那么就可以指定高度。如果 leftright 只有一个不为空,那么就可以指定宽度。
  • leftrightwidth 三个属性至少有一个不为空;topbottomheight 也一样。

例如我们要定位头像组件的位置,我们可以设置左边距离为20,然后垂直方向距离为 Banner 图片的高度减去头像组件自身高度的一半(等于头像半径加上边框尺寸)使得头像与 Banner 图片重叠。

Positioned(
    left: 20,
    top: imageHeight - avatarRadius - avatarBorderSize,
    child: _getAvatar(
      personalProfile.avatar,
      avatarRadius * 2,
      avatarBorderSize,
    ),
  ),
复制代码

接下来就是其他代码实现了,这里就只贴一下顶层组件的代码,具体实现细节可以去这里下载代码(界面文件为:personal_homepage.dart):状态管理代码

Stack(
  children: [
    CustomScrollView(
      slivers: [
        _getBannerWithAvatar(context, personalProfile),
        _getPersonalProfile(personalProfile),
        _getPersonalStatistic(personalProfile),
      ],
    ),
    Positioned(
      top: 40,
      left: 10,
      child: IconButton(
        onPressed: () {
          Navigator.of(context).pop();
        },
        icon: Icon(
          Icons.arrow_back,
          color: Colors.white,
        ),
      ),
    ),
  ],
);
复制代码

接口数据获取

从浏览器开发者工具抓出掘金的个人主页接口为:https://api.juejin.cn/user_api/v1/user/get?user_id={user_id},接口返回的数据项很多,摘抄我们需要的数据格式如下:

{
    "err_no": 0,
    "err_msg": "success",
    "data": {
        "user_id": "70787819648695",
        "user_name": "岛上码农",
        "company": "岛上码农",
        "job_title": "公众号",
        "avatar_large": "https://sf3-ttcdn-tos.pstatp.com/img/user-avatar/097899bbe5d10bceb750d5c69415518a~300x300.image",
        "level": 3,
        "power": 2193,
        "description": "从南飘到北,从北游到南的业余码农",
        "github_verified": 1,
        "followee_count": 148,
        "follower_count": 440,
    }
}
复制代码

然后就是基于这个数据构建实体类了,即源码里的personal_entity.dart 文件。对应的接口请求服务如下:

class JuejinService {
  static Future<PersonalEntity?> getPersonalProfile(String userId) async {
    var response = await HttpUtil.getDioInstance()
        .get('https://api.juejin.cn/user_api/v1/user/get?user_id=$userId');
    if (response.statusCode == 200) {
      if (response.data['err_no'] == 0) {
        return PersonalEntity.fromJson(response.data['data']);
      }
    }

    return null;
  }
}
复制代码

Future 状态管理

Provider 的状态管理为异步操作 Future 对象提供了一种更为快捷简便的方式,那就是 FutureProvider。还记得我们的动态详情页面,因为需要先请求数据才能刷新界面,我们将详情页面改成了 StatefulWidget,以便在 initState 中请求数据。

@override
void initState() {
  super.initState();
  context.read<DynamicModel>().getDynamic(widget.id).then((success) {
    if (success) {
      context.read<DynamicModel>().updateViewCount(widget.id);
    }
  });
}
复制代码

而使用了 FutureProvider 后,可以将请求放到 FutureProviderFutureProvider 会发起该异步操作,并且在 Future异步操作完成后会自动通知下级组件,可以不需要使用 StatefulWidget也能完成网络请求。FutureProvider 的使用方法和 ChangeNotiferProvider 类似,如下所示,其中 initialValue 是初始数据,可以是 null

// create 方式
FutureProvider<T?>(
  initialValue: null,
  create: (context) => Future,
  child: MyApp(),
)

// value 方式
FutureProvider<T?>.value(
  value: Future, 
	initialData: null,
	child: MyApp(),
}
复制代码

在这里我们就可以使用 FutureProvider 来完成个人信息请求后自动刷新界面,相关代码如下所示:

class PersonalHomePageWrapper extends StatelessWidget {
  const PersonalHomePageWrapper({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return FutureProvider<PersonalEntity?>(
      create: (context) => JuejinService.getPersonalProfile('70787819648695'),
      initialData: null,
      child: _PersonalHomePage(),
    );
  }
}

class _PersonalHomePage extends StatelessWidget {
  const _PersonalHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    PersonalEntity? personalProfile = context.watch<PersonalEntity?>();
    if (personalProfile == null) {
      return Center(
        child: Text('加载中...'),
      );
    }
    return Stack(
  		// ...省略界面代码
  	);
	}
	// ...省略界面代码
}
复制代码

运行结果

运行效果如下图所示,是不是感觉和掘金的个人主页很像?

屏幕录制2021-08-15 下午2.32.12.gif

总结

本篇仿了掘金的个人主页顶部部分界面,通过界面我们分析了布局、组件层级,重点介绍了 Stack 组件和 Positioned 组件的使用,以及使用FutureProvider 自动完成异步操作后通知界面刷新,从而简化我们的页面代码,比如无需使用 initState 和编写状态管理类。每个人实现界面的方式不同,但思路都是一致的:

  • 分析UI设计稿布局;
  • 划分代码层级大的布局块。
  • 细化布局块,构建 UI 组件树。
  • 抽取界面中可能共用的部分,提高复用性,例如本篇中的统计数据,这块就具有一定的通用性,是可以单独抽出来组件的。

我是岛上码农,微信公众号同名,这是Flutter 入门与实战的专栏文章,对应源码请看这里:Flutter 入门与实战专栏源码

👍🏻:觉得有收获请点个赞鼓励一下!

🌟:收藏文章,方便回看哦!

💬:评论交流,互相进步!

分类:
Android