首页
苏兮影视
随笔记
壁纸
更多
直播
时光轴
友联
关于
统计
Search
1
v2ray节点搭建
1,074 阅读
2
软件添加id功能按钮
1,029 阅读
3
QQ扫码无法登录的解决方案
990 阅读
4
网易云音乐歌单ID获取教程
921 阅读
5
js逆向xhs
792 阅读
谈天说地
建站源码
经验教程
资源分享
动漫美图
登录
Search
标签搜索
java
rust
flutter
esp32c3
springboot
安卓
linux
vue
dart
设计模式
docker
ssh
joe
快捷键
git
fish shell
maven
redis
netty
groovy
尽意
累计撰写
116
篇文章
累计收到
41
条评论
首页
栏目
谈天说地
建站源码
经验教程
资源分享
动漫美图
页面
苏兮影视
随笔记
壁纸
直播
时光轴
友联
关于
统计
搜索到
12
篇与
的结果
2026-06-18
记录几个flutter自定义组件
波浪效果 class SineWaveWidget extends StatefulWidget { final double amplitude; // 振幅(波浪的高度) final double frequency; // 频率(波浪的密集程度) final Color color; // 波浪颜色 final Duration duration; // 流动一圈的周期 const SineWaveWidget({ super.key, this.amplitude = 20.0, this.frequency = 2.0, this.color = Colors.blueAccent, this.duration = const Duration(seconds: 1), }); @override State<SineWaveWidget> createState() => _SineWaveWidgetState(); } class _SineWaveWidgetState extends State<SineWaveWidget> with SingleTickerProviderStateMixin { late AnimationController _controller; bool _isForward = true; @override void initState() { super.initState(); // 创建一个无限循环的动画控制器,用于驱动波浪的相位变化 _controller = AnimationController( vsync: this, duration: widget.duration, lowerBound: 0.0, upperBound: 2 * pi, )..repeat(); // 自动无限循环 } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return GestureDetector( onTap: () { _isForward = !_isForward; }, child: AnimatedBuilder( animation: _controller /*.drive(Tween(begin: 0, end: 2 * pi))*/, // 将 0~1 的进度映射为 0~2π 的相位 builder: (context, child) { return CustomPaint( size: Size.infinite, // 填满父容器 painter: SineWavePainter( amplitude: widget.amplitude, frequency: widget.frequency, color: widget.color, phase: _controller.value * (_isForward ? 1 : -1), ), ); }, ), ); } } class SineWavePainter extends CustomPainter { final double amplitude; final double frequency; final Color color; final double phase; SineWavePainter({ required this.amplitude, required this.frequency, required this.color, required this.phase, }); @override void paint(Canvas canvas, Size size) { // final paint = Paint() // ..color = color // ..style = PaintingStyle.fill // ..blendMode = BlendMode.xor; // // final path = Path(); // // // 将波浪的基准线设置在垂直中心 // final centerY = size.height / 2; // // // 1. 起点:从左下角开始 // path.moveTo(0, size.height); // // // 2. 绘制正弦曲线 // // 遍历画布的每一个像素宽度 // for (double x = 0; x <= size.width; x++) { // // 核心公式:y = A * sin(ω * x + φ) // // 乘以 2π 是为了让 frequency 代表“画布宽度内包含的完整波浪数量” // final y = centerY + // amplitude * sin((x / size.width) * frequency * 2 * pi + phase); // path.lineTo(x, y); // } // // // 3. 闭合路径:连接到右下角,再回到左下角,形成封闭的填充区域 // path.lineTo(size.width, size.height); // path.close(); // // final path2 = Path(); // path2.moveTo(0, size.height); // for (double x = 0; x <= size.width; x++) { // final y = centerY + // amplitude * sin((x / size.width) * frequency * 2 * pi + (phase + pi)); // path2.lineTo(x, y); // } // path2.lineTo(size.width, size.height); // path2.close(); // // // 4. 绘制到画布上 // canvas.drawPath(path, paint); // canvas.drawPath(path2, paint); final paint = Paint() ..color = color ..style = PaintingStyle.fill ..blendMode = BlendMode.multiply; final centerY = size.height / 2; final omega = frequency * 2 * pi / size.width; // 将波浪划分为固定的段数(例如 16 段),确保动画循环时首尾完美衔接 final segments = (frequency * 4).toInt(); final step = size.width / segments; for (int wave = 0; wave < 2; wave++) { final currentPhase = phase + (wave * pi); final path = Path(); // 起点:左下角 path.moveTo(0, size.height); // 遍历每个分段点 for (int i = 0; i <= segments; i++) { final x = i * step; final y = centerY + amplitude * sin(omega * x + currentPhase); if (i == 0) { path.lineTo(x, y); } else { // 前一个点的坐标和 Y 值 final prevX = (i - 1) * step; final prevY = centerY + amplitude * sin(omega * prevX + currentPhase); // 计算前一个点的切线斜率(导数) final prevSlope = amplitude * omega * cos(omega * prevX + currentPhase); // 计算控制点(基于前一点的切线推算) final controlX = prevX + step / 2; final controlY = prevY + (prevSlope * step) / 2; // 使用二次贝塞尔曲线连接 path.quadraticBezierTo(controlX, controlY, x, y); } } // 严格闭合路径:确保右下角和左下角完美贴合 path.lineTo(size.width, size.height); path.close(); canvas.drawPath(path, paint); } } @override bool shouldRepaint(covariant SineWavePainter oldDelegate) { // 只要相位(phase)发生变化,就重绘(用于实现流动动画) return oldDelegate.phase != phase; } }QQ消息拖拽效果 class QqDragBubble extends StatefulWidget { final double initialRadius; // 初始圆半径 final double maxDragDistance; // 最大拖拽断裂距离 final Color color; const QqDragBubble({ super.key, this.initialRadius = 15.0, this.maxDragDistance = 100.0, this.color = Colors.red, }); @override State<QqDragBubble> createState() => _QqDragBubbleState(); } class _QqDragBubbleState extends State<QqDragBubble> with SingleTickerProviderStateMixin { Offset _dragOffset = Offset.zero; // 手指拖拽位置 bool _isDragging = false; bool _isBurst = false; // 是否已断裂 late AnimationController _burstController; late Animation<double> _burstAnimation; @override void initState() { super.initState(); _burstController = AnimationController( vsync: this, duration: const Duration(milliseconds: 200), ); _burstAnimation = Tween<double>(begin: 1.0, end: 0.0).animate( CurvedAnimation(parent: _burstController, curve: Curves.easeOut), ); } @override void dispose() { _burstController.dispose(); super.dispose(); } void _onPanUpdate(DragUpdateDetails details) { if (_isBurst) return; setState(() { _isDragging = true; _dragOffset += details.delta; }); } void _onPanEnd(DragEndDetails details) { if (_isBurst) return; final distance = _dragOffset.distance; if (distance > widget.maxDragDistance) { // 超过临界值,触发断裂动画 setState(() => _isBurst = true); _burstController.forward(from: 0.0); } else { // 未超过临界值,回弹 setState(() { _isDragging = false; _dragOffset = Offset.zero; }); } } @override Widget build(BuildContext context) { return GestureDetector( onPanUpdate: _onPanUpdate, onPanEnd: _onPanEnd, child: AnimatedBuilder( animation: _burstAnimation, builder: (context, child) { return CustomPaint( size: Size(300, 300), // 给足绘制空间 painter: QqBubblePainter( color: widget.color, initialRadius: widget.initialRadius, dragOffset: _isDragging ? _dragOffset : Offset.zero, maxDragDistance: widget.maxDragDistance, burstScale: _isBurst ? _burstAnimation.value : 1.0, isDragging: _isDragging && !_isBurst, ), ); }, ), ); } } class QqBubblePainter extends CustomPainter { final Color color; final double initialRadius; final Offset dragOffset; final double maxDragDistance; final double burstScale; final bool isDragging; QqBubblePainter({ required this.color, required this.initialRadius, required this.dragOffset, required this.maxDragDistance, required this.burstScale, required this.isDragging, }); @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = color ..style = PaintingStyle.fill; // 起点圆心(屏幕中心) final startCenter = Offset(size.width / 2, size.height / 2); // 拖拽圆(终点)圆心 final endCenter = startCenter + dragOffset; // 计算两点间的距离和角度 final distance = (endCenter - startCenter).distance; final angle = atan2(endCenter.dy - startCenter.dy, endCenter.dx - startCenter.dx); // 1. 动态计算半径:拖拽越远,起点圆越小;终点圆保持原大小 double startRadius = initialRadius; double endRadius = initialRadius; if (isDragging && distance < maxDragDistance) { final ratio = distance / maxDragDistance; startRadius = initialRadius * (1 - ratio * 0.7); // 起点圆最多缩小 70% } // 2. 绘制起点圆(断裂后消失) if (!isDragging || distance <= maxDragDistance) { if (isDragging) { // 拖拽过程中起点圆随距离缩小 } else { startRadius = initialRadius; } canvas.drawCircle(startCenter, startRadius, paint); } // 3. 绘制终点圆(拖拽时跟随手指,断裂时播放缩放消失动画) if (isDragging) { canvas.drawCircle(endCenter, endRadius, paint); } else if (burstScale < 1.0) { // 断裂爆炸动画 canvas.drawCircle(endCenter, endRadius * burstScale, paint); } else { // 初始状态 canvas.drawCircle(startCenter, initialRadius, paint); } // 4. 绘制贝塞尔连接带 if (isDragging && distance < maxDragDistance && distance > 0) { final path = Path(); // 计算连接点坐标(基于三角函数) // 起点圆上的两个切点 final startX1 = startCenter.dx + startRadius * cos(angle + pi / 2); final startY1 = startCenter.dy + startRadius * sin(angle + pi / 2); final startX2 = startCenter.dx + startRadius * cos(angle - pi / 2); final startY2 = startCenter.dy + startRadius * sin(angle - pi / 2); // 终点圆上的两个切点 final endX1 = endCenter.dx + endRadius * cos(angle + pi / 2); final endY1 = endCenter.dy + endRadius * sin(angle + pi / 2); final endX2 = endCenter.dx + endRadius * cos(angle - pi / 2); final endY2 = endCenter.dy + endRadius * sin(angle - pi / 2); // 贝塞尔控制点(取两点连线的中点) final controlX = (startCenter.dx + endCenter.dx) / 2; final controlY = (startCenter.dy + endCenter.dy) / 2; // 绘制上半部分曲线 path.moveTo(startX1, startY1); path.quadraticBezierTo(controlX, controlY, endX1, endY1); // 绘制下半部分曲线(闭合路径) path.lineTo(endX2, endY2); path.quadraticBezierTo(controlX, controlY, startX2, startY2); path.close(); canvas.drawPath(path, paint); } } @override bool shouldRepaint(covariant QqBubblePainter oldDelegate) { return oldDelegate.dragOffset != dragOffset || oldDelegate.isDragging != isDragging || oldDelegate.burstScale != burstScale; } } 手写板带播放效果import 'package:flutter/material.dart'; // 1. 笔画数据模型(包含时间戳) class Stroke { final List<Offset> points; final DateTime startTime; final DateTime endTime; final Color color; final double strokeWidth; Stroke({ required this.points, required this.startTime, required this.endTime, this.color = Colors.black, this.strokeWidth = 5.0, }); int get duration => endTime.difference(startTime).inMilliseconds; } class HandwritingCanvas extends StatefulWidget { const HandwritingCanvas({super.key}); @override State<HandwritingCanvas> createState() => _HandwritingCanvasState(); } class _HandwritingCanvasState extends State<HandwritingCanvas> with SingleTickerProviderStateMixin { // 历史笔画 final List<Stroke> _strokes = []; // 正在绘制的笔画(使用 ValueNotifier 保证书写绝对流畅) final ValueNotifier<List<Offset>> _currentPointsNotifier = ValueNotifier([]); DateTime? _strokeStartTime; // 播放控制 late AnimationController _playbackController; late Animation<double> _playbackAnimation; bool _isPlaying = false; @override void initState() { super.initState(); _playbackController = AnimationController( vsync: this, duration: const Duration(seconds: 5), ); _playbackAnimation = Tween<double>(begin: 0.0, end: 1.0).animate( CurvedAnimation(parent: _playbackController, curve: Curves.linear), ); // 监听播放动画,触发画笔重绘 _playbackAnimation.addListener(() { if (_isPlaying) setState(() {}); }); } @override void dispose() { _playbackController.dispose(); _currentPointsNotifier.dispose(); super.dispose(); } // --- 录制逻辑 --- void _onPanStart(DragStartDetails details) { if (_isPlaying) return; // 播放时禁止书写 _strokeStartTime = DateTime.now(); _currentPointsNotifier.value = [details.localPosition]; } void _onPanUpdate(DragUpdateDetails details) { if (_isPlaying) return; _currentPointsNotifier.value = [ ..._currentPointsNotifier.value, details.localPosition ]; } void _onPanEnd(DragEndDetails details) { if (_isPlaying) return; if (_currentPointsNotifier.value.isNotEmpty && _strokeStartTime != null) { setState(() { _strokes.add(Stroke( points: List.from(_currentPointsNotifier.value), startTime: _strokeStartTime!, endTime: DateTime.now(), )); _currentPointsNotifier.value = []; }); } } // --- 播放逻辑 --- void _startPlayback() { if (_strokes.isEmpty || _isPlaying) return; setState(() => _isPlaying = true); final totalDuration = _strokes.last.endTime.difference(_strokes.first.startTime); _playbackController.duration = totalDuration; _playbackController.forward(from: 0.0).then((_) { setState(() => _isPlaying = false); }); } // --- 清除逻辑 --- void _clearCanvas() { if (_isPlaying) return; setState(() { _strokes.clear(); _currentPointsNotifier.value = []; }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('完美手写板'), actions: [ IconButton( icon: const Icon(Icons.play_arrow), onPressed: _isPlaying ? null : _startPlayback, tooltip: '播放笔迹', ), IconButton( icon: const Icon(Icons.delete_outline), onPressed: _isPlaying ? null : _clearCanvas, tooltip: '清除画板', ), ], ), body: GestureDetector( onPanStart: _onPanStart, onPanUpdate: _onPanUpdate, onPanEnd: _onPanEnd, child: RepaintBoundary( child: CustomPaint( size: Size.infinite, painter: HandwritingPainter( strokes: _strokes, currentPoints: _currentPointsNotifier, isPlaying: _isPlaying, playbackProgress: _playbackAnimation.value, totalDurationMs: _strokes.isNotEmpty ? _strokes.last.endTime .difference(_strokes.first.startTime) .inMilliseconds : 0, ), ), ), ), ); } } class HandwritingPainter extends CustomPainter { final List<Stroke> strokes; final ValueNotifier<List<Offset>> currentPoints; final bool isPlaying; final double playbackProgress; final int totalDurationMs; HandwritingPainter({ required this.strokes, required this.currentPoints, required this.isPlaying, required this.playbackProgress, required this.totalDurationMs, }) : super(repaint: currentPoints); // 依然保留 ValueNotifier 驱动书写 @override void paint(Canvas canvas, Size size) { if (isPlaying) { // ================= 播放模式 ================= final currentTimeMs = (playbackProgress * totalDurationMs).toInt(); for (final stroke in strokes) { final elapsed = currentTimeMs - stroke.startTime.difference(strokes.first.startTime).inMilliseconds; if (elapsed <= 0) continue; // 还没到开始时间 final paint = Paint() ..color = stroke.color ..strokeWidth = stroke.strokeWidth ..strokeCap = StrokeCap.round ..strokeJoin = StrokeJoin.round ..style = PaintingStyle.stroke; final path = Path(); path.moveTo(stroke.points.first.dx, stroke.points.first.dy); for (int i = 1; i < stroke.points.length; i++) { path.lineTo(stroke.points[i].dx, stroke.points[i].dy); } if (elapsed < stroke.duration) { // 正在播放中的笔画:按比例截取 final progress = elapsed / stroke.duration; final metric = path.computeMetrics().first; canvas.drawPath( metric.extractPath(0, metric.length * progress), paint); } else { // 已经播放完的笔画:完整绘制 canvas.drawPath(path, paint); } } } else { // ================= 书写模式 ================= // 绘制历史笔画 for (final stroke in strokes) { final paint = Paint() ..color = stroke.color ..strokeWidth = stroke.strokeWidth ..strokeCap = StrokeCap.round ..strokeJoin = StrokeJoin.round ..style = PaintingStyle.stroke; final path = Path(); path.moveTo(stroke.points.first.dx, stroke.points.first.dy); for (int i = 1; i < stroke.points.length; i++) { path.lineTo(stroke.points[i].dx, stroke.points[i].dy); } canvas.drawPath(path, paint); } // 绘制正在写的笔画(通过 ValueNotifier 局部重绘,绝对流畅) final points = currentPoints.value; if (points.isNotEmpty) { final paint = Paint() ..color = Colors.black ..strokeWidth = 5.0 ..strokeCap = StrokeCap.round ..strokeJoin = StrokeJoin.round ..style = PaintingStyle.stroke; final path = Path(); path.moveTo(points.first.dx, points.first.dy); for (int i = 1; i < points.length; i++) { path.lineTo(points[i].dx, points[i].dy); } canvas.drawPath(path, paint); } } } @override bool shouldRepaint(covariant HandwritingPainter oldDelegate) { // 播放时或者历史笔画变化时重绘 return isPlaying || oldDelegate.strokes != strokes; } } 抛物线动画效果
2026年06月18日
15 阅读
0 评论
0 点赞
Flutter模板方法模式在多Tab列表中的应用
项目背景与需求在Flutter开发中,我们经常遇到需要实现多Tab栏列表的场景,每个Tab具有相似的结构(下拉刷新、上拉加载、列表展示),但数据类型和UI展示各不相同。本文将介绍如何使用模板方法模式来优雅地解决这个问题。模板方法模式简介模板方法模式是一种行为设计模式,它在超类中定义了一个算法的框架,允许子类在不修改结构的情况下重写特定步骤。在我们的场景中,每个Tab的加载流程(初始化→刷新→加载更多)是相同的,但具体的数据加载和UI展示是不同的。代码实现解析1. 抽象基类定义const int defaultPageSize = 10; abstract class TabData<T> { final String name; // tab名称 int total; // 数据数量 bool isSelected; // 选中状态 bool hasMore; // 是否有更多数据 List<T> items; // 数据列表 bool _isInited = false; // 是否初始化完成 final RefreshController controller; final Widget Function(BuildContext context,T item) itemBuilder; TabData({ required this.name, required this.itemBuilder, this.total = 0, this.isSelected = false, this.hasMore = true, this.items = const [] }) : controller = RefreshController(); int get pageSize => defaultPageSize; Future<List<T>> loadData(int page); void updateItems(List<T> newItems, bool isRefresh); Future<bool> init() async{ if (_isInited) { return true; } _isInited = true; await onRefresh(); return false; } Future<void> onRefresh() async { try { final data = await loadData(1); updateItems(data, true); hasMore = data.length >= pageSize; controller.refreshCompleted(); } catch (e) { controller.refreshFailed(); rethrow; } } Future<void> onLoadMore() async { if (!hasMore) { controller.loadNoData(); return; } try { final currentPage = (items.length / pageSize).ceil() + 1; final data = await loadData(currentPage); updateItems(data, false); hasMore = data.length >= pageSize; controller.loadComplete(); } catch (e) { controller.refreshFailed(); rethrow; } } void dispose() { controller.dispose(); } }设计要点:定义了完整的生命周期方法(init、onRefresh、onLoadMore)使用泛型T支持不同的数据类型封装了刷新控制器和分页逻辑提供itemBuilder回调实现UI定制化2. 具体子类实现点赞Tab实现:class LikeTabData extends TabData<LikeTabDataEntity>{ LikeTabData():super(name: "点赞",itemBuilder: (context,item){ return Text(item.name); }); @override Future<List<LikeTabDataEntity>> loadData(int page) async{ await Future.delayed(const Duration(seconds: 1)); return List.generate(pageSize, (index)=> LikeTabDataEntity(name: "$name 数据 ${page * pageSize + index}")); } @override void updateItems(List<LikeTabDataEntity> newItems, bool isRefresh) { items = isRefresh ? newItems : [...items, ...newItems]; total = items.length; } }评论Tab实现:class CommentTabData extends TabData<CommentTabDataEntity>{ CommentTabData():super(name: "评论",itemBuilder: (context,item) { Widget text () => Text(item.text,style: const TextStyle(color: Colors.white)); return Container( margin: const EdgeInsets.all(8), padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: Colors.blueAccent, borderRadius: BorderRadius.circular(8), ), child: text(), ); }); @override Future<List<CommentTabDataEntity>> loadData(int page) async{ await Future.delayed(const Duration(seconds: 1)); return List.generate(pageSize, (index)=> CommentTabDataEntity(text: "$name 数据 ${page * pageSize + index}")); } @override void updateItems(List<CommentTabDataEntity> newItems, bool isRefresh) { items = isRefresh ? newItems : [...items, ...newItems]; total = items.length; } }子类只需实现:loadData:具体的数据加载逻辑updateItems:数据合并策略itemBuilder:列表项UI构建3. 状态管理class DialogState extends BaseViewState { List<TabData> tabDataList = [ LikeTabData(), CommentTabData(), LikeTabData(), CommentTabData(), ]; }4. 逻辑控制器class DialogLogic extends BaseGetXLifeCycleController{ late final PageController tabController; final state = DialogState(); @override void onInit() { super.onInit(); tabController = PageController(); state.viewState = TYPE_LOAD; update(); state.tabDataList.first.init().then((_)=> update()); Future.delayed(const Duration(seconds: 1),(){ state.tabDataList.first.isSelected = true; state.viewState = TYPE_NORMAL; update(); }); } @override void onClose() { tabController.dispose(); for(final item in state.tabDataList) { item.dispose(); } super.onClose(); } void onTabChange(int index) async{ for(int i = 0; i < state.tabDataList.length; i++) { state.tabDataList[i].isSelected = index == i; } update(); final isInited = await state.tabDataList[index].init(); if(!isInited) update(); } void changeTab(int index,BuildContext context){ final screenWidth = MediaQuery.of(context).size.width; tabController.animateTo(index * screenWidth,duration: const Duration(milliseconds: 300), curve: Curves.easeInOut); } void onRefresh(Future<void> Function() refresh) async{ await refresh(); update(); } void onLoadMore(Future<void> Function() loadMore) async{ await loadMore(); update(); } }5. UI界面实现class Dialog extends BaseTitleBottomSheet with GetXControllerMixin<DialogLogic>{ Dialog({super.key}); final logic = Get.put(DialogLogic()); final state = Get.find<DialogLogic>().state; @override String getTitle() => "Dialog"; @override Widget buildContent() { return buildPartBaseView( state.viewState,context, _buildContentWidget() ); } Widget _buildContentWidget() { return Expanded( child: Column( children: [ SingleChildScrollView( child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ for(int i = 0;i<state.tabDataList.length;i++) GestureDetector(onTap: ()=> logic.changeTab(i,context),child: _buildTabWidget(state.tabDataList[i])) ], ), ), Expanded( child: PageView.builder( controller: logic.tabController, onPageChanged: logic.onTabChange, itemCount: state.tabDataList.length, itemBuilder: (_,index){ final data = state.tabDataList[index]; if(data.items.isEmpty){ return const Center(child: Text("无数据"),); } return _KeepAliveList( data, keepAlive: true, onLoading: ()=> logic.onLoadMore(data.onLoadMore), onRefresh: ()=> logic.onRefresh(data.onRefresh), controller: data.controller ); }, ), ), ], ), ); } Widget _buildTabWidget(TabData entity){ final noSelectedStyle = TextStyle(color: Colors.black,fontSize: 23.w); final selectedStyle = TextStyle(color: Colors.blueAccent,fontSize: 23.w); final style = entity.isSelected ? selectedStyle : noSelectedStyle; return Row( mainAxisSize: MainAxisSize.min, children: [ Text(entity.name,style: style), Text("(${entity.total})",style: style), ], ); } } class _KeepAliveList extends StatefulWidget { final TabData tabData; final bool keepAlive; final VoidCallback? onRefresh; final VoidCallback? onLoading; final RefreshController controller; const _KeepAliveList(this.tabData,{super.key,this.keepAlive = true,required this.controller,this.onRefresh,this.onLoading}); @override State<_KeepAliveList> createState() => _KeepAliveListState(); } class _KeepAliveListState extends State<_KeepAliveList> with AutomaticKeepAliveClientMixin{ @override Widget build(BuildContext context) { super.build(context); return SmartRefresher( controller: widget.controller, enablePullDown: true, enablePullUp: true, header: const ClassicHeader(), onRefresh: widget.onRefresh, onLoading: widget.onLoading, child: ListView.builder( itemCount: widget.tabData.items.length, itemBuilder: (context,index){ // 新版本dart可以使用switch匹配模式,可以穷举完全部密封类的子类 // 为了兼容只能使用if了 final tabData = widget.tabData; if(tabData is LikeTabData){ return tabData.itemBuilder(context,tabData.items[index]); }else if(tabData is CommentTabData){ return tabData.itemBuilder(context,tabData.items[index]); } throw "未知的TabData类型"; }, ), ); } @override bool get wantKeepAlive => widget.keepAlive; }设计优势1. 代码复用性公共的刷新、加载、分页逻辑在基类中实现新增Tab只需继承TabData并实现三个抽象方法2. 可维护性算法框架稳定,修改基类即可影响所有子类业务逻辑与UI展示分离3. 扩展性支持任意类型的列表数据可轻松添加新的Tab类型4. 一致性所有Tab保持相同的交互逻辑统一的错误处理和加载状态使用建议添加新Tab:只需创建新的TabData子类,实现loadData、updateItems和itemBuilder修改公共逻辑:在TabData基类中修改,所有子类自动生效自定义分页大小:在子类中重写pageSize getter特殊处理:子类可重写onRefresh或onLoadMore方法进行个性化处理总结通过模板方法模式,我们成功地将多Tab列表中的公共逻辑提取到基类中,同时保留了子类的灵活性。这种设计模式特别适用于具有相似流程但不同实现的场景,能够显著减少代码重复,提高项目的可维护性和扩展性。
2026年01月16日
38 阅读
0 评论
2 点赞
Flutter 实现流式消息接收
我们通过 Dio 发起一个 ResponseType.stream 请求,消费后端逐字符返回的 HTTP 流。核心代码如下:void sendMessage() => Dio().get( "http://192.168.149.178:8080/stream/covert?url=${state.text}", options: Options(responseType: ResponseType.stream), cancelToken: CancelToken(), ).then((response) { final data = response.data; if (data is! ResponseBody) return; state.data = ""; update(); data.stream.listen((chunk) { final res = utf8.decoder.convert(chunk); state.data += res; update(); }); });UI 部分使用 Expanded + SingleChildScrollView 展示实时追加的文本:Column( children: [ TextField(controller: logic.text), TextButton(onPressed: logic.sendMessage, child: Text("发送消息")), Expanded( child: SelectionArea( child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [Text(state.data)], ), ), ), ), ], )后端使用 Spring WebFlux 将目标网页内容逐字符延迟返回:@GetMapping("/covert") public Flux<String> covert(@RequestParam String url) { return Mono.fromCallable(() -> { // 同步抓取网页内容(在非 Netty 线程执行) var request = new Request.Builder().url(url).build(); try (var resp = client.newCall(request).execute()) { return resp.body().string(); } }) .flux() .flatMap(text -> Flux.fromStream(text.chars().mapToObj(c -> String.valueOf((char) c))) .delayElements(Duration.ofMillis(10)) // 每字符间隔 10ms ); }该方案无需 SSE 或 WebSocket,仅依赖标准 HTTP 分块传输,即可实现“打字机式”流式响应,适用于 AI 对话、实时日志等场景。
2026年01月07日
42 阅读
0 评论
1 点赞
Flutter LayoutBuilder 原理深度解析
引言在 Flutter 响应式布局体系中,LayoutBuilder 是一个强大而特殊的组件。与大多数在构建阶段(build phase)确定其子树的组件不同,LayoutBuilder 将子组件的构建推迟到布局阶段(layout phase),从而能够根据父级传递的实际布局约束动态构建 UI。本文将深入源码层面,全面剖析 LayoutBuilder 的工作原理。核心架构概览类继承层次Widget ├── RenderObjectWidget │ └── ConstrainedLayoutBuilder<BoxConstraints> │ └── LayoutBuilder (具体实现)主要角色分工ConstrainedLayoutBuilder<T> - 抽象基类,定义约束构建器的通用协议LayoutBuilder - 面向 BoxConstraints 的具体实现_LayoutBuilderElement - 连接 Widget 与 RenderObject 的桥梁_RenderLayoutBuilder - 实际执行布局的 RenderObject源码深度解析1. Element 的初始化与绑定// _LayoutBuilderElement.mount 方法 @override void mount(Element? parent, Object? newSlot) { super.mount(parent, newSlot); // 1. 创建对应的 RenderObject renderObject.updateCallback(_rebuildWithConstraints); // 2. 绑定重建回调 }关键点分析:super.mount() 会创建 _RenderLayoutBuilder 实例updateCallback 将 _rebuildWithConstraints 方法注册到 RenderObject此时回调并未立即执行,只是建立了调用关系2. 布局阶段的核心调用链// _RenderLayoutBuilder.performLayout 方法 @override void performLayout() { final BoxConstraints constraints = this.constraints; // 1. 获取父级约束 rebuildIfNecessary(); // 2. 触发约束重建 if (child != null) { child!.layout(constraints, parentUsesSize: true); // 3. 布局子节点 size = constraints.constrain(child!.size); // 4. 确定自身大小 } else { size = constraints.biggest; } }2.1 约束的来源追踪约束的传递遵循 Flutter 渲染管线的标准流程:// RenderObject 层级传递 void layout(Constraints constraints, { bool parentUsesSize = false }) { this.constraints = constraints; // 父组件传递约束 // ... 后续布局计算 }this.constraints 在 performLayout 中通过 this.constraints 访问,这是 RenderObject 的标准属性。2.2 重建触发机制// RenderConstrainedLayoutBuilder 混入类 void rebuildIfNecessary() { assert(_callback != null); invokeLayoutCallback(_callback!); // 调用注册的回调 } // RenderObject 中的回调执行 void invokeLayoutCallback<T extends Constraints>(LayoutCallback<T> callback) { owner!._enableMutationsToDirtySubtrees(() { callback(constraints as T); // 关键:传递当前约束 }); }3. 安全的回调执行环境_enableMutationsToDirtySubtrees 是保证布局阶段安全修改组件树的关键:void _enableMutationsToDirtySubtrees(VoidCallback callback) { assert(_debugDoingLayout); bool? oldState; assert(() { oldState = _debugAllowMutationsToDirtySubtrees; _debugAllowMutationsToDirtySubtrees = true; // 临时允许修改 return true; }()); try { callback(); // 执行 LayoutBuilder 的 builder } finally { _shouldMergeDirtyNodes = true; assert(() { _debugAllowMutationsToDirtySubtrees = oldState!; // 恢复原状态 return true; }()); } }安全机制解析:断言保护:仅在布局阶段允许此操作状态切换:临时开启脏子树修改权限异常安全:使用 try-finally 确保状态恢复操作合并:设置 _shouldMergeDirtyNodes 以优化更新4. 约束重建的实际执行void _rebuildWithConstraints(ConstraintType constraints) { // 构建子组件的闭包 void updateChildCallback() { Widget built = widget.builder(this, constraints); // 调用用户定义的builder _child = updateChild(_child, built, null); // 更新子Element _needsBuild = false; _previousConstraints = constraints; // 缓存约束 } // 条件性执行重建 final VoidCallback? callback = _needsBuild || (constraints != _previousConstraints) ? updateChildCallback : null; owner!.buildScope(this, callback); }智能优化机制:_previousConstraints 缓存上次的约束仅当约束变化或标记需要重建时才执行避免不必要的重建,提升性能5. 更新与重建调度5.1 组件更新处理@override void update(ConstrainedLayoutBuilder<ConstraintType> newWidget) { super.update(newWidget); renderObject.updateCallback(_rebuildWithConstraints); // 重新绑定 if (newWidget.updateShouldRebuild(oldWidget)) { _needsBuild = true; // 标记需要重建 renderObject.markNeedsLayout(); // 触发重新布局 } }5.2 智能调度策略void _scheduleRebuild() { // 根据调度阶段决定立即标记还是延迟 final bool deferMarkNeedsLayout = switch (SchedulerBinding.instance.schedulerPhase) { SchedulerPhase.idle || SchedulerPhase.postFrameCallbacks => true, _ => false, }; if (!deferMarkNeedsLayout) { renderObject.markNeedsLayout(); } else { // 在稳定阶段延迟调度 SchedulerBinding.instance.scheduleFrameCallback(_frameCallback); } }调度策略优势:立即响应:在布局/绘制阶段立即触发更新延迟优化:在空闲阶段延迟到下一帧,避免打断当前操作设计模式分析1. 延迟决策模式LayoutBuilder 实现了典型的延迟决策模式:构建阶段:只建立结构和回调关系布局阶段:根据实际约束做出具体构建决策优势:适应动态布局需求,支持响应式设计2. 回调注册模式通过 updateCallback 机制实现松耦合:Element 向 RenderObject 注册回调RenderObject 在适当时机触发回调分离了决策逻辑与执行时机3. 安全沙箱模式_enableMutationsToDirtySubtrees 提供安全执行环境:临时放宽框架限制执行用户代码后立即恢复平衡灵活性与安全性实战应用示例1. 经典响应式布局Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { // 根据宽度决定布局方式 if (constraints.maxWidth > 600) { return Row(children: [Sidebar(), ContentArea()]); } else { return Column(children: [AppBar(), ContentArea()]); } }, ); }2. 动态尺寸计算Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { final itemWidth = (constraints.maxWidth - 32) / 3; return GridView.builder( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, childAspectRatio: itemWidth / 100, ), itemBuilder: (context, index) => ItemWidget(), ); }, ); }源码设计亮点1. 性能优化约束缓存:通过 _previousConstraints 避免重复构建条件更新:仅在实际变化时触发重建智能调度:根据框架状态选择最佳更新时机2. 错误边界void _rebuildWithConstraints(ConstraintType constraints) { try { built = widget.builder(this, constraints); } catch (e, stack) { built = ErrorWidget.builder(...); // 优雅降级 } // ... 错误处理继续 }3. 调试支持详细的断言检查调试信息收集异常报告机制总结与启示LayoutBuilder 的设计体现了 Flutter 框架的几个核心理念:关注点分离:将构建决策延迟到拥有完整布局信息时性能优先:通过多种优化避免不必要的计算安全第一:在灵活性与框架稳定性间取得平衡声明式表达:通过简单的回调 API 提供强大功能通过深入分析 LayoutBuilder 的源码,我们不仅理解了其工作原理,更能学习到如何设计既灵活又稳定的框架级组件。这种在布局阶段获取约束并动态构建的能力,为 Flutter 的响应式设计提供了坚实的基础设施支持。源码文件:packages/flutter/lib/src/widgets/layout_builder.dart 设计模式:延迟决策、回调注册、安全沙箱 适用场景:响应式布局、动态 UI、约束依赖型组件
2025年12月25日
38 阅读
0 评论
1 点赞
Flutter 状态管理之 Provider
在 Flutter 开发中,状态管理是一个非常重要的话题。而 Provider 作为 Flutter 社区推荐的状态管理解决方案之一,因其轻量、简单、灵活而备受欢迎。本文将系统地解析 Provider 的基本用法和高级用法,包括 read、watch、consume 和 selector,并通过单个 Model 和多个 Model 的管理案例详细说明。什么是 Provider?Provider 是一个基于 InheritedWidget 的封装,用于高效地管理 Flutter 应用中的状态。它的主要优势包括:简单易用,符合 Dart 语言的惯用写法。支持多层 Widget 树的状态共享。高性能,只会更新需要重建的 Widget。通过 Provider,我们可以方便地实现依赖注入和响应式编程。Provider 的基本用法在使用 Provider 之前,需要先在 pubspec.yaml 文件中添加依赖:dependencies: provider: ^lates以下通过一个简单的计数器示例说明 Provider 的基本用法。单个 Model 的使用创建 Model 类import 'package:flutter/foundation.dart'; class CounterModel with ChangeNotifier { int _count = 0; int get count => _count; void increment() { _count++; notifyListeners(); } }在应用中注册 Providerimport 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'counter_model.dart'; void main() { runApp( ChangeNotifierProvider( create: (_) => CounterModel(), child: MyApp(), ), ); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: CounterScreen(), ); } }获取状态和更新 UIclass CounterScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Provider 示例')), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('当前计数:'), Text( context.watch<CounterModel>().count.toString(), style: Theme.of(context).textTheme.headline4, ), ], ), ), floatingActionButton: FloatingActionButton( onPressed: () => context.read<CounterModel>().increment(), child: Icon(Icons.add), ), ); } }多个 Model 的管理在大型应用中,可能需要管理多个 Model。在这种情况下,可以使用 MultiProvider。定义多个 Modelclass CounterModel with ChangeNotifier { int _count = 0; int get count => _count; void increment() { _count++; notifyListeners(); } } class ThemeModel with ChangeNotifier { bool _isDark = false; bool get isDark => _isDark; void toggleTheme() { _isDark = !_isDark; notifyListeners(); } }注册多个 Providervoid main() { runApp( MultiProvider( providers: [ ChangeNotifierProvider(create: (_) => CounterModel()), ChangeNotifierProvider(create: (_) => ThemeModel()), ], child: MyApp(), ), ); }在不同的 Widget 中使用 Modelclass CounterScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Provider 示例'), actions: [ IconButton( icon: Icon(Icons.brightness_6), onPressed: () => context.read<ThemeModel>().toggleTheme(), ), ], ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('当前计数:'), Text( context.watch<CounterModel>().count.toString(), style: Theme.of(context).textTheme.headline4, ), ], ), ), floatingActionButton: FloatingActionButton( onPressed: () => context.read<CounterModel>().increment(), child: Icon(Icons.add), ), ); } }Provider 中的核心方法1. readread 方法用于访问状态,但不会订阅状态的变化。这意味着调用 read 的 Widget 在状态更新时不会重建。适用场景:需要触发一次性的操作,例如调用方法或事件处理器。FloatingActionButton( onPressed: () => context.read<CounterModel>().increment(), child: Icon(Icons.add), )2. watchwatch 方法会订阅状态的变化。当状态发生变化时,使用 watch 的 Widget 会重新构建。适用场景:需要根据状态动态更新 UI。Text( context.watch<CounterModel>().count.toString(), style: Theme.of(context).textTheme.headline4, )3. consumeConsumer 是 Provider 提供的一个 Widget,能够订阅状态并重建自身及其子树。适用场景:需要精确控制重建的范围,避免不必要的 Widget 重建。Consumer<CounterModel>( builder: (context, counter, child) { return Text( counter.count.toString(), style: Theme.of(context).textTheme.headline4, ); }, )4. selectorSelector 提供了更高效的订阅方式,可以从状态中选择特定字段进行订阅,避免因其他字段变化导致的无效重建。适用场景:状态对象较大且只需要监听其中部分字段时。Selector<CounterModel, int>( selector: (_, counter) => counter.count, builder: (context, count, child) { return Text( count.toString(), style: Theme.of(context).textTheme.headline4, ); }, )性能优化建议使用 Consumer 或 Selector 控制重建范围:避免不必要的父级 Widget 重建。分离逻辑和 UI:将业务逻辑放在模型中,UI 层只负责展示。尽量减少依赖注入的深度:如果可能,使用多层 Provider。
2024年12月06日
202 阅读
0 评论
1 点赞
1
2
3