Appearance
14红点组件:如何将红点设计做成Flutter组件
上一课时我们完善了首页推荐功能,本课时将完善个人页面。个人页面涉及红点组件的知识点,因此本课时在完善个人页面的同时,会着重介绍下该功能的实现。
实现效果
我们先来看下本课时要完成的一个界面效果,如图 1 动画所示。
图 1 本课时目标效果
首先在最底部导航栏增加了消息未读数提示,当有新的未读消息时候,会有红点提示。个人界面展示了个人信息以及个人相关的操作栏(我的好友、我的消息和系统设置)。
接下来我们来看下实现该效果需要做哪些前期准备工作。
前期准备
基于图 1 的效果,我们首先要实现个人页面。个人页面是一个新页面,对于新页面我们按照第 7 课时的知识去设计页面。由于新增了红点功能,首先在 App 启动时 ,需要一个新的接口拉取服务器未读消息数,然后新增 API 来拉取该数据。其次这个数据状态,需要在多个组件中共享,因此要新增 Model 来管理该状态 。
组件设计
根据图 2 的界面效果,我们将页面拆分图 3 组件树。
图 2 个人页面效果
图 3 个人页面组件树设计
图 3 组件树中, 左侧为最上面的头像和昵称,右侧为功能列表 。由于是一个有限的列表,因此可以使用 ListView 来封装组件。具体代码编写部分和之前所介绍的没有太大区别,详细代码可前往 github 参考。
个人页面开发完成后,我们再来看下红点功能所涉及的 API 和 Model 功能部分。由于用户信息和红点未读消息,都需要状态共享,因此需要创建两个 Model 类 。 这两个 Model 类的代码逻辑基本一致,下面只介绍与红点未读消息有关的部分。
API
在 App 启动时就需要拉取未读消息数,因此需要一个接口来获取未读消息内容 。在 api/user_info 目录下创建 message.dart 来管理消息接口 ,实现该 ApiUserInfoMessage 类,代码如下:
dart
/// 获取用户消息相关
class ApiUserInfoMessage {
/// 获取自己的个人信息
static int getUnReadMessageNum() {
return 18;
}
}
上面就是这个 API 的功能,里面包含一个 getUnReadMessageNum 方法,这里模拟返回一个假数据 18 个未读消息。
Model
由于未读消息会被应用在底部导航栏和个人页面两个组件页面,因此需要使用 Provider 来做状态管理,在 model 下创建 new_message_model.dart ,并实现下面代码:
dart
import 'package:flutter/material.dart';
/// name状态管理模块
class NewMessageModel with ChangeNotifier {
/// 系统未读新消息数
int newMessageNum;
/// 构造函数
NewMessageModel({this.newMessageNum});
/// 获取未读消息
int get value => newMessageNum;
/// 设置已经阅读消息
void readNewMessage() {
if(newMessageNum == 0) {
return;
}
newMessageNum = 0;
notifyListeners();
}
}
第 6 行是声明一个未读消息字段,保存未读消息数量。第 9 行是构造函数,第 12 行是设置一个 get 方法,第 14 行是设置已读状态,也可以在此调用服务端,将服务端未读状态清零,同时将本地的未读消息数清零。在写 Model 代码要特别注意,比如上面的第 16 行,目的就是避免没必要的 rebuild,当已经没有未读消息,则不需要处理任何行为。
在完成 API 和 Model 部分代码后,接下来修改入口文件 main.dart,在该入口文件中要多增加一个状态管理模块。
Main.dart
由于需求的改变,现在需要多个共享状态类,课时之前只有一个状态 like_num_model,现在需要新增一个 new_message_model 状态,这里就需要使用到 MultiProvider,避免嵌套多层。需要将 main.dart build 方法修改为下面逻辑:
dart
@override
Widget build(BuildContext context) {
return _getProviders(
context,
MaterialApp(
title: 'Two You', // APP 名字
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue, // APP 主题
),
routes: Router().registerRouter(),
home: Entrance()),
);
}
为了保持代码的整洁,新增了一个 _getProviders 方法,然后将状态管理相关的逻辑放入 _getProviders 中,其他组件相关的逻辑还是在 build 中,具体在看下 _getProviders 中的代码逻辑:
dart
/// 部分数据需要获取初始值
Widget _getProviders(BuildContext context, Widget child) {
StructUserInfo myUserInfo = ApiUserInfoIndex.getSelfUserInfo();
if(myUserInfo == null){
return CommonError();
}
int unReadMessageNum = ApiUserInfoMessage.getUnReadMessageNum();
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => LikeNumModel()),
ChangeNotifierProvider(
create: (context) => UserInfoModel(
myUserInfo: myUserInfo
)
),
ChangeNotifierProvider(
create: (context) => NewMessageModel(
newMessageNum: unReadMessageNum
)
),
],
child: child,
);
}
代码第 3 到 7 行是从服务端调用未读消息接口,并获得用户信息和用户未读消息数 。第 9 行使用 MultiProvider 来封装所有需要状态管理的代码,其中每一个状态管理的格式按照下面的方式:
dart
ChangeNotifierProvider(create: (context) => LikeNumModel()),
LikeNumModel 为 Model 类,可以为类增加初始赋值,比如上面的 UserInfoModel 和 NewMessageModel。
通过以上方法,我们就将用户信息和未读消息两个状态进行了组件共享,接下来我们看下如何设计红点组件。
红点组件
在 App 中红点和消息提醒是非常常见的应用,因此需要将该功能,设计为一个基础通用组件。在 Flutter 中是提供了一个比较通用的库 badges。如果你觉得不太适用也可以自己来封装,本课时主要是基于这个组件库实现一个二次封装应用,先来具体看下二次封装的红点组件实现部分。
组件实现
根据自身的业务,我们可以设计为两种, 一个是只显示红点,另一个是显示具体未读消息数的,先看下只显示红点的部分。
dart
import 'package:flutter/material.dart';
import 'package:badges/badges.dart';
/// 通用红点逻辑
class CommonRedMessage {
/// 只展示红点,不展示具体消息数
static Widget showRedWidget(Widget needRedWidget, int newMessageNum) {
if(newMessageNum < 1) { // 小于 1 的消息则无须设置
return needRedWidget;
}
return _getBadge(needRedWidget, '');
}
/// 获取 badge 组件
static Widget _getBadge(Widget needRedWidget, String msgTips) {
return Badge(
alignment: Alignment.bottomLeft,
position: BadgePosition.topRight(),
toAnimate: false,
badgeContent: Text(
'$msgTips',
style: TextStyle(
color: Colors.white,
fontSize: 10.0,
letterSpacing: 1,
wordSpacing: 2,
height: 1,
),
),
child: needRedWidget,
);
}
}
代码第 7 行的方法 showRedWidget 就是只显示红点提醒,其调用的是 _getBadge 方法,该方法主要是应用 Badge 第三方组件,上面代码中的五个参数的作用分别是:
alignment,child 组件的展示方式,这里是底部靠左;
position,红点或者未读消息数的位置,这里是右上角;
toAnimate,表示动画,这里直接去掉,感觉效果不太好,也没有必要;
badgeContent,则是红点的样式内容,需要 Text 组件;
child,就是需要展示红点的组件。
未读消息也是使用到 _getBadge 方法,但这里传入的是具体的消息数,而不是一个空字符,具体代码如下:
dart
/// 展示消息提醒
static Widget showRedNumWidget(Widget needRedWidget, int newMessageNum) {
if(newMessageNum < 1) { // 小于1的消息则无须设置
return needRedWidget;
}
// 消息数大于99时,则只显示一个红点即可
String msgTips = newMessageNum > 99 ? '99+' : '$newMessageNum';
return _getBadge(needRedWidget, msgTips);
}
考虑到未读消息数小于 1 不用展示,其次为了避免消息未读数量过大导致 UI 问题,这里在第 7 行代码也加了判断,具体还是根据业务场景来配置。完成基础组件后,我们再来看下该组件的应用部分。
组件应用
组件的应用包含在两部分,一部分是底部导航栏,另外一部分是个人页的"我的消息"按钮。我们先来看下底部导航栏部分,这部分代码在 pages/entrance.dart 中,我们只看修改的部分。
dart
/// 获取页面内容部分
Widget _getScaffold(BuildContext context) {
final newMessageModel = Provider.of<NewMessageModel>(context);
首先需要通过 Provider 获得 NewMessageModel 的操作句柄。
dart
BottomNavigationBarItem(
icon: CommonRedMessage.showRedWidget(
Icon(Icons.person),
newMessageModel.value
),
title: Text('我'),
activeIcon: CommonRedMessage.showRedWidget(
Icon(Icons.person_outline),
newMessageModel.value
),
),
修改底部导航栏的"我",将其中的 icon 使用红点组件封装,代码在第 2 到 5 行,这里封装在 icon 上界面效果是最好的 , 这样就在底部导航栏增加了未读消息红点提醒 。
为了演示红点和消息数两种场景,在导航上使用红点来演示效果,个人页面则演示展示具体未读消息数量。再来看下个人页面的代码部分,这部分逻辑在 widgets/user_page/button_list.dart 中。
dart
/// 个人页面的功能列表
class UserPageButtonList extends StatelessWidget {
@override
Widget build(BuildContext context) {
final newMessageModel = Provider.of<NewMessageModel>(context);
return ListView(
children: <Widget>[
ListTile(
leading: Icon(Icons.person_pin),
title: Text('我的好友'),
onTap: () {},
),
ListTile(
leading: CommonRedMessage.showRedNumWidget(
Icon(Icons.email),
newMessageModel.value
),
title: Text('我的消息'),
onTap: () {},
),
ListTile(
leading: Icon(Icons.settings),
title: Text('系统设置'),
onTap: () {},
)
],
);
}
}
以上代码中的第 18 行到 21 行,就是将 icon 封装在了红点组件内。未读消息展示已经介绍了,那么我们再来看下如何消除消息红点和未读消息。
消除红点
在 new_messsage_model 状态管理类中,有一个 readNewMessage 方法,该方法就是将未读消息设置为 0 , 然后通知数据监听方,这里我们将点击行为在个人页面的"我的消息"来触发,将 widgets/user_page/button_list.dart 修改为下面的部分:
dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:two_you_friend/model/new_message_model.dart';
import 'package:two_you_friend/widgets/common/red_message.dart';
/// 个人页面的功能列表
class UserPageButtonList extends StatelessWidget {
@override
Widget build(BuildContext context) {
final newMessageModel = Provider.of<NewMessageModel>(context);
return ListView(
children: <Widget>[
ListTile(
leading: Icon(Icons.person_pin),
title: Text('我的好友'),
onTap: () {},
),
ListTile(
leading: CommonRedMessage.showRedNumWidget(
Icon(Icons.email),
newMessageModel.value
),
title: Text('我的消息'),
onTap: () {
newMessageModel.readNewMessage();
},
),
ListTile(
leading: Icon(Icons.settings),
title: Text('系统设置'),
onTap: () {},
)
],
);
}
}
上面代码中的第 28 行就是点击触发消息消除,接下来我们运行看下效果,如图 4 的动效所示。
图 4 红点效果图
以上就实现了红点组件的设计,并应用红点组件完善了 Two You Friend 的个人页面功能。
总结
本课时在实现 App 个人页面的过程中,着重介绍了红点组件的设计和应用,同时介绍到了 Provider 多状态管理的方法。学习完本课时后,你要熟练应用红点组件,并且掌握其业务组件设计的方法,其次需要掌握 Provider 的多状态管理方法 。
在本课时之前,所有的 API 接口都是一个假接口数据,下一课时我们将介绍如何进行网络请求,来完善 API 部分功能。谢谢大家。