当前位置: 首页 > news >正文

flutter学习-day13-功能型组件和状态共享

📚 目录

  1. 导航返回拦截
  2. InheritedWidget数据共享
  3. 跨组件状态共享
    1. 事件总线EventBus
    2. 依赖注入Provider
  4. 颜色和主题
    1. 颜色字符串转成color对象
    2. 颜色亮度
    3. MaterialColor类
    4. 主体
  5. 异步UI更新
    1. FutureBuilder
    2. StreamBuilder
  6. 对话框

本文学习和引用自《Flutter实战·第二版》:作者:杜文

1. 导航返回拦截

为了避免用户误触返回按钮而导致 App 退出,在很多 App 中都拦截了用户点击返回键的按钮,然后进行一些防误触判断,比如当用户在某一个时间段内点击两次时,才会认为用户是要退出。Flutter中可以通过WillPopScope来实现返回按钮拦截。

  • 示例如下:
import 'package:flutter/material.dart';class HomePage extends StatefulWidget {const HomePage({super.key});State<HomePage> createState() => HomePageState();
}class HomePageState extends State<HomePage> {/// 时间DateTime? times;Widget build(BuildContext context) {return WillPopScope(onWillPop: () async {const oneSecond = Duration(seconds: 1);if (times == null || DateTime.now().difference(times!) > oneSecond) {// 两次点击间隔超过1秒则重新计时times = DateTime.now();return false;}return true;},child: Scaffold(appBar: AppBar(title: const Text('Flutter Home'),),body: Center(child: TextButton(child: const Text('点击我'),onPressed: () {},),),),);}
}

2. InheritedWidget数据共享

InheritedWidget提供了一种在 widget 树中从上到下共享数据的方式,比如我们在应用的根 widget 中通过InheritedWidget共享了一个数据,那么我们便可以在任意子widget 中来获取该共享的数据。如Flutter SDK中正是通过 InheritedWidget 来共享应用主题和Locale信息的。如下例子:

  • 计数器使用共享数据
import 'package:flutter/material.dart';class ShareDataWidget extends InheritedWidget {const ShareDataWidget({super.key, required this.data, required Widget child}): super(child: child);final int data;static ShareDataWidget? of(BuildContext context) {return context.dependOnInheritedWidgetOfExactType<ShareDataWidget>();}bool updateShouldNotify(ShareDataWidget old) {return old.data != data;}
}class MyTextWidget extends StatefulWidget {const MyTextWidget({super.key});MyTextWidgetState createState() => MyTextWidgetState();
}class MyTextWidgetState extends State<MyTextWidget> {Widget build(BuildContext context) {/// 使用InheritedWidget中的共享数据var txt = ShareDataWidget.of(context)!.data.toString();return Text(txt);}/// 父或祖先widget中的InheritedWidget改变(updateShouldNotify返回true)时会被调用。如果build中没有依赖InheritedWidget,则此回调不会被调用。void didChangeDependencies() {super.didChangeDependencies();debugPrint("count修改了");}
}class HomePage extends StatefulWidget {const HomePage({super.key});State<HomePage> createState() => HomePageState();
}class HomePageState extends State<HomePage> {int count = 0;Widget build(BuildContext context) {return Scaffold(appBar: AppBar(title: const Text('Flutter Home'),),body: Center(child: ShareDataWidget(/// 使用ShareDataWidgetdata: count,child: Column(mainAxisAlignment: MainAxisAlignment.center,children: <Widget>[const Padding(padding: EdgeInsets.only(bottom: 20.0),/// 子widget中依赖ShareDataWidgetchild: MyTextWidget(),),ElevatedButton(child: const Text("点击增加"),/// 每点击一次,将count自增,然后重新build,ShareDataWidget的data将被更新onPressed: () => setState(() => ++count),)],),),),);}
}

2-1. 只引用数据不调用didChangeDependencies

在上面的例子中,如果我们只想在MyTextWidgetState中引用ShareDataWidget数据,但却不希望在ShareDataWidget发生变化时调用MyTextWidgetState的didChangeDependencies()方法,只需要将ShareDataWidget.of()的实现改一下即可。唯一的改动就是获取ShareDataWidget对象的方式,把dependOnInheritedWidgetOfExactType()方法换成了context.getElementForInheritedWidgetOfExactType<ShareDataWidget>().widget。他们的区别就是前者会注册依赖关系,而后者不会。

class ShareDataWidget extends InheritedWidget {const ShareDataWidget({super.key, required this.data, required Widget child}): super(child: child);final int data;static ShareDataWidget? of(BuildContext context) {// return context.dependOnInheritedWidgetOfExactType<ShareDataWidget>();return context.getElementForInheritedWidgetOfExactType<ShareDataWidget>()!.widget as ShareDataWidget;}bool updateShouldNotify(ShareDataWidget old) {return old.data != data;}
}

3. 跨组件状态共享

状态管理是一个永恒的话题。如果状态是组件私有的,则应该由组件自己管理;如果状态要跨组件共享,则该状态应该由各个组件共同的父元素来管理。常用的有事件总线EventBus(订阅者模式)和依赖注入Provider(观察者模式)。

3-1. 事件总线EventBus

//订阅者回调签名
typedef void EventCallback(arg);class EventBus {//私有构造函数EventBus._internal();//保存单例static EventBus _singleton = EventBus._internal();//工厂构造函数factory EventBus()=> _singleton;//保存事件订阅者队列,key:事件名(id),value: 对应事件的订阅者队列final _emap = Map<Object, List<EventCallback>?>();//添加订阅者void on(eventName, EventCallback f) {_emap[eventName] ??=  <EventCallback>[];_emap[eventName]!.add(f);}//移除订阅者void off(eventName, [EventCallback? f]) {var list = _emap[eventName];if (eventName == null || list == null) return;if (f == null) {_emap[eventName] = null;} else {list.remove(f);}}//触发事件,事件触发后该事件所有订阅者会被调用void emit(eventName, [arg]) {var list = _emap[eventName];if (list == null) return;int len = list.length - 1;//反向遍历,防止订阅者在回调中移除自身带来的下标错位for (var i = len; i > -1; --i) {list[i](arg);}}
}//定义一个top-level(全局)变量,页面引入该文件后可以直接使用bus
var bus = EventBus();

使用如下:

/// 页面A//监听登录事件
bus.on("login", (arg) {// ......
});/// 页面B// 登录成功后触发登录事件,页面A中订阅者会被调用
bus.emit("login", userInfo);

3-2. 依赖注入Provider

现在Flutter社区已经有很多专门用于状态管理的包了,在此列出几个相对评分比较高的:

名称描述
Provider & Scoped Model两个包都是基于InheritedWidget的,原理相似
ReduxReact生态链中Redux包的Flutter实现
MobXReact生态链中MobX包的Flutter实现
BLoCBLoC模式的Flutter实现

4. 颜色和主题

Flutter里有一个Colors类,里面定义了100多个颜色常量,颜色都是以一个int值保存。而Theme组件可以为Material APP定义主题数据。

4-1. 颜色字符串转成Color对象

  • 颜色固定
/// 直接使用整数值
Color(0xffdc380d)
  • 字符串变量
var c = "dc380d";/// 通过位运算符将Alpha设置为FF
Color(int.parse(c,radix:16)|0xFF000000)/// 通过方法将Alpha设置为FF
Color(int.parse(c,radix:16)).withAlpha(255)

4-2. 颜色亮度

假如要实现一个背景颜色和Title可以自定义的导航栏,并且背景色为深色时我们应该让Title显示为浅色;背景色为浅色时,Title 显示为深色。要实现这个功能,我们就需要来计算背景色的亮度,然后动态来确定Title的颜色。Color 类中提供了一个computeLuminance()方法,它可以返回一个[0-1]的一个值,数字越大颜色就越浅,我们可以根据它来动态确定Title的颜色。

import 'package:flutter/material.dart';/// 定义
class HomePage extends StatefulWidget {const HomePage({super.key});State<HomePage> createState() => HomePageState();
}/// 实现
class HomePageState extends State<HomePage> {Widget build(BuildContext context) {return Scaffold(appBar: AppBar(title: const Text('Flutter Home'),),body: const Center(child: Column(children: [NavBar(color: Colors.blue, title: '标题1'),NavBar(color: Colors.white, title: '标题2')],),),);}
}/// 定义组件
class NavBar extends StatelessWidget {final String title;final Color color;const NavBar({super.key,required this.color,required this.title,});Widget build(BuildContext context) {return Container(constraints: const BoxConstraints(minHeight: 52,minWidth: double.infinity,),decoration: BoxDecoration(color: color,boxShadow: const [// 阴影BoxShadow(color: Colors.black26,offset: Offset(0, 3),blurRadius: 3,),],),alignment: Alignment.center,child: Text(title,style: TextStyle(fontWeight: FontWeight.bold,// 根据背景色亮度来确定Title颜色color: color.computeLuminance() < 0.5 ? Colors.white : Colors.black,),));}
}

4-3. MaterialColor类

MaterialColor是实现Material Design中的颜色的类,它包含一种颜色的10个级别的渐变色。它通过"[]"运算符的索引值来代表颜色的深度,有效的索引有:50,100,200,…,900,数字越大,颜色越深。默认值是500。使用方式为Colors.blue.shade50。

  • 例子:
class HomePageState extends State<HomePage> {Widget build(BuildContext context) {return Scaffold(appBar: AppBar(title: const Text('Flutter Home'),),body: Center(child: Column(children: [NavBar(color: Colors.blue.shade50, title: '标题1'),NavBar(color: Colors.blue.shade100, title: '标题2'),NavBar(color: Colors.blue.shade200, title: '标题2'),NavBar(color: Colors.blue.shade300, title: '标题2'),NavBar(color: Colors.blue.shade400, title: '标题2'),NavBar(color: Colors.blue.shade500, title: '标题2'),NavBar(color: Colors.blue.shade600, title: '标题2'),NavBar(color: Colors.blue.shade700, title: '标题2'),NavBar(color: Colors.blue.shade800, title: '标题2'),NavBar(color: Colors.blue.shade900, title: '标题2')],),),);}
}

4-4. 主题

ThemeData用于保存是Material 组件库的主题数据,Material组件需要遵守相应的设计规范,而这些规范可自定义部分都定义在ThemeData中了,所以我们可以通过ThemeData来自定义应用主题。在子组件中,我们可以通过Theme.of方法来获取当前的ThemeData。(有些是不能自定义的,比如导航栏高度)。

  • 定义:
ThemeData({Brightness? brightness, // 深色还是浅色MaterialColor? primarySwatch, // 主题颜色样本,见下面介绍Color? primaryColor, // 主色,决定导航栏颜色Color? cardColor, // 卡片颜色Color? dividerColor, // 分割线颜色ButtonThemeData buttonTheme, // 按钮主题Color dialogBackgroundColor,// 对话框背景颜色String fontFamily, // 文字字体TextTheme textTheme,// 字体主题,包括标题、body等文字样式IconThemeData iconTheme, // Icon的默认样式TargetPlatform platform, // 指定平台,应用特定平台控件风格ColorScheme? colorScheme,/// ......
})

下面是一个单个页面换肤的例子,如果想要对整个应用换肤,则可以去修改MaterialApp的theme属性,例子如下:

import 'package:flutter/material.dart';/// 定义
class HomePage extends StatefulWidget {const HomePage({super.key});State<HomePage> createState() => HomePageState();
}/// 实现
class HomePageState extends State<HomePage> {var myThemeColor = Colors.teal;Widget build(BuildContext context) {ThemeData themeData = Theme.of(context);return Theme(data: ThemeData(// 用于导航栏、FloatingActionButton的背景色等primarySwatch: myThemeColor,// 用于Icon颜色iconTheme: IconThemeData(color: myThemeColor),),child: Scaffold(appBar: AppBar(title: const Text('Flutter Home'),),body: Center(child: Column(children: [const Row(mainAxisAlignment: MainAxisAlignment.center,children: [Icon(Icons.favorite),Text(" 颜色跟随主题"),],),Theme(data: themeData.copyWith(iconTheme:themeData.iconTheme.copyWith(color: Colors.black),),child: const Row(mainAxisAlignment: MainAxisAlignment.center,children: <Widget>[Icon(Icons.favorite),Text(" 颜色固定黑色")]),)],),),floatingActionButton: FloatingActionButton(// 切换主题onPressed: () {setState(() => myThemeColor = myThemeColor == Colors.teal? Colors.blue: Colors.teal);},child: const Icon(Icons.palette))));}
}

5. 异步UI更新

很多时候我们会依赖一些异步数据来动态更新UI,比如在打开一个页面时我们需要先从互联网上获取数据,在获取数据的过程中我们显示一个加载框,等获取到数据时我们再渲染页面;又比如我们想展示文件流、互联网数据接收流的进度。Flutter专门提供了FutureBuilder和StreamBuilder两个组件来快速实现这种功能。

5-1. FutureBuilder

FutureBuilder是Flutter中专门用于异步UI更新的组件,它会依赖一个Future,它会根据所依赖的Future的状态来动态构建自身。

属性描述
futureFutureBuilder依赖的Future,通常是一个异步耗时任务
initialData初始数据,用户设置默认数据
builderWidget构建器,该构建器会在Future执行的不同阶段被多次调用,第一个参数是context
builder.snapshotbuilder的参数,包含当前异步任务的状态信息及结果信息
import 'package:flutter/material.dart';/// 定义
class HomePage extends StatefulWidget {const HomePage({super.key});State<HomePage> createState() => HomePageState();
}/// 实现
class HomePageState extends State<HomePage> {Future<String> mockNetworkData() async {return Future.delayed(const Duration(seconds: 2), () => "我是从互联网上获取的数据");}Widget build(BuildContext context) {return Scaffold(appBar: AppBar(title: const Text('Flutter Home'),),body: Center(child: FutureBuilder(future: mockNetworkData(),builder: (BuildContext context, AsyncSnapshot snapshot) {if (snapshot.connectionState == ConnectionState.done) {if (snapshot.hasError) {// 请求失败,显示错误return Text("Error: ${snapshot.error}");} else {// 请求成功,显示数据return Text("Contents: ${snapshot.data}");}} else {// 请求未结束,显示loadingreturn const CircularProgressIndicator();}},),));}
}

5-2. StreamBuilder

StreamBuilder是用于配合Stream来展示流上事件(数据)变化的UI组件。

例子如下:

import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart';/// 定义
class HomePage extends StatefulWidget {const HomePage({super.key});State<HomePage> createState() => HomePageState();
}/// 实现
class HomePageState extends State<HomePage> {/// 任务流Stream<int> myCounter() {return Stream.periodic(const Duration(seconds: 1), (i) {return i;});}/// 状态文案String str = '';Widget build(BuildContext context) {return Scaffold(appBar: AppBar(title: const Text('Flutter Home'),),body: Center(child: StreamBuilder(stream: myCounter(),builder: (BuildContext context, AsyncSnapshot<int> snapshot) {if (snapshot.hasError) {str = snapshot.error.toString();}switch (snapshot.connectionState) {case ConnectionState.none:str = '没有Stream';case ConnectionState.waiting:str = '等待数据';case ConnectionState.active:str = snapshot.data.toString();case ConnectionState.done:str = '已关闭';}return Text('状态:$str');},),),floatingActionButton: FloatingActionButton(onPressed: () {}, child: const Icon(Icons.palette)));}
}

6. 对话框

对话框本质上也是UI布局,通常一个对话框会包含标题、内容,以及一些操作按钮,为此,Material库中提供了一些现成的对话框组件来用于快速的构建出一个完整的对话框。下面写一个AlertDialog做例子。

构造函数如下:

const AlertDialog({Key? key,this.title, // 对话框标题组件this.titlePadding, // 标题填充this.titleTextStyle, // 标题文本样式this.content, // 对话框内容组件this.contentPadding = const EdgeInsets.fromLTRB(24.0, 20.0, 24.0, 24.0), // 内容的填充this.contentTextStyle,// 内容文本样式this.actions, // 对话框操作按钮组this.backgroundColor, // 对话框背景色this.elevation,// 对话框的阴影this.semanticLabel, // 对话框语义化标签(用于读屏软件)this.shape, // 对话框外形
})

使用例子如下:

import 'package:flutter/material.dart';/// 定义
class HomePage extends StatefulWidget {const HomePage({super.key});State<HomePage> createState() => HomePageState();
}/// 实现
class HomePageState extends State<HomePage> {/// 定义对话框Future<bool?> showDeleteConfirmDialog() {return showDialog<bool>(context: context,builder: (context) {return AlertDialog(title: const Text("提示"),content: const Text("您确定要删除当前文件吗?"),actions: <Widget>[TextButton(child: const Text("取消"),onPressed: () => Navigator.of(context).pop(), // 关闭对话框),TextButton(child: const Text("删除"),onPressed: () {//关闭对话框并返回trueNavigator.of(context).pop(true);},),],);},);}Widget build(BuildContext context) {return Scaffold(appBar: AppBar(title: const Text('Flutter Home'),),body: const Center(child: Text('点击此处'),),floatingActionButton: FloatingActionButton(onPressed: () async {bool? deleteDialog = await showDeleteConfirmDialog();if (deleteDialog == null) {debugPrint("取消");} else {debugPrint("确认");}}, child: const Icon(Icons.palette)));}
}

本次分享就到这儿啦,我是鹏多多,如果您看了觉得有帮助,欢迎评论,关注,点赞,转发,我们下次见~

往期文章

  • 手把手教你搭建规范的团队vue项目,包含commitlint,eslint,prettier,husky,commitizen等等
  • Web Woeker和Shared Worker的使用以及案例
  • Vue2全家桶+Element搭建的PC端在线音乐网站
  • vue3+element-plus配置cdn
  • 助你上手Vue3全家桶之Vue3教程
  • 助你上手Vue3全家桶之VueX4教程
  • 助你上手Vue3全家桶之Vue-Router4教程
  • 超详细!Vue的九种通信方式
  • 超详细!Vuex手把手教程
  • 使用nvm管理node.js版本以及更换npm淘宝镜像源
  • vue中利用.env文件存储全局环境变量,以及配置vue启动和打包命令
  • 超详细!Vue-Router手把手教程

个人主页

  • CSDN
  • GitHub
  • 简书
  • 博客园
  • 掘金

相关文章:

  • ImageNet 数据集介绍
  • LLM Agent发展演进历史(观看metagpt视频笔记)
  • python读取excel数据 附实战代码
  • 剑指offer 背包问题求具体方案
  • python接口自动化测试(单元测试方法)
  • 【UE5.1 MetaHuman】使用mixamo_converter把Mixamo的动画重定向给MetaHuman使用
  • Android多进程和跨进程通讯方式
  • 频谱论文:面向频谱地图构建的频谱态势生成技术研究
  • oracle aq java jms使用(数据类型为XMLTYPE)
  • 使用AppleScript自动滚动预览
  • 关于“Python”的核心知识点整理大全26
  • 【数据结构】八大排序之直接插入排序算法
  • 正则表达式入门与实践
  • C 库函数 - time()
  • 06 Rust 枚举类
  • SegmentFault for Android 3.0 发布
  • $translatePartialLoader加载失败及解决方式
  • 2018以太坊智能合约编程语言solidity的最佳IDEs
  • Android Studio:GIT提交项目到远程仓库
  • angular学习第一篇-----环境搭建
  • IDEA 插件开发入门教程
  • iOS编译提示和导航提示
  • PHP的Ev教程三(Periodic watcher)
  • React as a UI Runtime(五、列表)
  • React-生命周期杂记
  • Vue 动态创建 component
  • vue-loader 源码解析系列之 selector
  • 案例分享〡三拾众筹持续交付开发流程支撑创新业务
  • 彻底搞懂浏览器Event-loop
  • 聊聊sentinel的DegradeSlot
  • 前端设计模式
  • 小程序01:wepy框架整合iview webapp UI
  • ​力扣解法汇总1802. 有界数组中指定下标处的最大值
  • !! 2.对十份论文和报告中的关于OpenCV和Android NDK开发的总结
  • #QT(串口助手-界面)
  • %@ page import=%的用法
  • (6)设计一个TimeMap
  • (机器学习的矩阵)(向量、矩阵与多元线性回归)
  • (太强大了) - Linux 性能监控、测试、优化工具
  • (一)C语言之入门:使用Visual Studio Community 2022运行hello world
  • (一)SpringBoot3---尚硅谷总结
  • (一)基于IDEA的JAVA基础1
  • (幽默漫画)有个程序员老公,是怎样的体验?
  • (转)关于如何学好游戏3D引擎编程的一些经验
  • (转)项目管理杂谈-我所期望的新人
  • .bat批处理(八):各种形式的变量%0、%i、%%i、var、%var%、!var!的含义和区别
  • .FileZilla的使用和主动模式被动模式介绍
  • .net core 3.0 linux,.NET Core 3.0 的新增功能
  • .NET 将多个程序集合并成单一程序集的 4+3 种方法
  • .Net 知识杂记
  • .NET/C# 编译期间能确定的相同字符串,在运行期间是相同的实例
  • .NET/C# 解压 Zip 文件时出现异常:System.IO.InvalidDataException: 找不到中央目录结尾记录。
  • .Net6 Api Swagger配置
  • /*在DataTable中更新、删除数据*/
  • @GetMapping和@RequestMapping的区别