Flutter 组件抽取:通用页面 State 抽取(BasePageState)
通用页面 State 抽取,统一了 AppBar 的样式及基本交互,可对用 StatefulWidget / State 类型实现的页面的其他通用交互进行统一定义及维护。
·
简介
通用页面 State 抽取,统一了 AppBar 的样式及基本交互,可对用 StatefulWidget / State 类型实现的页面的其他通用交互进行统一定义及维护
效果

范例
class _TestPageState extends BasePageState<TestPage> {
PageConfig pageConfigBuilder() {
return PageConfig(
title: '测试一下下',
moreActions: [
MoreActionWidget(
child: const Icon(Icons.menu, color: Colors.black),
onClick: () => pageConfig.updateTitle('???'),
),
],
);
}
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
/// do点啥 ... ...
/// do点啥 ... ...
/// do点啥 ... ...
});
}
Widget buildPageBody(BuildContext context) {
return Center(
child: GestureDetector(
child: const Text('点我试试'),
onTap: () {
pageConfig.updateTitle('点我干嘛');
pageConfig.updateInvalidActions(true);
pageConfig.updateBackWidget(const Icon(Icons.close, color: Colors.black));
},
),
);
}
}
说明
1、务必保持页面基类的简单性、独立性、专注性
2、能用系统 API 实现的功能,不要使用第三方
3、StatefulWidget / StatelessWidget 只有一个状态,不用基类,可以考虑采用 mixin
4、使用 ChangeNotifier+AnimatedBuilder 的监听构建形式,实现 AppBar 的局部刷新
代码
import 'package:flutter/material.dart';
export 'package:flutter/material.dart';
/// 页面基类
/// 1、务必保持页面基类的简单性、独立性、专注性
/// 2、能用系统 API 实现的功能,不要使用第三方
/// 3、StatefulWidget/StatelessWidget 只有一个状态,不用基类,可以考虑采用 mixin
abstract class BasePageState<T extends StatefulWidget> extends State<T> {
late final PageConfig pageConfig = pageConfigBuilder();
/// 页面参数构建
PageConfig pageConfigBuilder();
void setState(VoidCallback fn) {
// 用于"异步回调刷新时,状态已失效"的适配
if (!mounted) return;
super.setState(fn);
}
void initState() {
super.initState();
}
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: pageConfig.onPageWillPop,
child: Scaffold(
appBar: buildNavigationBar(context),
body: SizedBox(
width: double.maxFinite,
height: double.maxFinite,
child: buildPageBody(context),
),
backgroundColor: pageConfig.backgroundColor ?? const Color(0xF9F9F9F9),
resizeToAvoidBottomInset: pageConfig.resizeToAvoidBottomInset,
),
);
}
/// 实现局部刷新的AppBar
PreferredSizeWidget buildNavigationBar(BuildContext context) {
return _ListenableAppBar(
toolbarHeight: BaseAppBar.sToolbarHeight,
child: AnimatedBuilder(
animation: pageConfig,
builder: (BuildContext context, Widget? child) {
return pageConfig.hideAppBar
? const SizedBox()
: BaseAppBar(
context,
titleStr: pageConfig.title,
hideBackWidget: pageConfig.hideBackWidget ?? false,
backWidget: pageConfig.backWidget,
backCallback: pageConfig.backCallback,
moreActions: pageConfig.moreActions,
invalidActions: pageConfig.invalidActions ?? false,
elevation: pageConfig.elevation,
shadowColor: pageConfig.shadowColor,
);
},
),
);
}
Widget buildPageBody(BuildContext context);
}
/// 页面配置类
class PageConfig extends ChangeNotifier {
final bool hideAppBar;
/// 页面背景色
/// [backgroundColor] 初始化后不允许修改
final Color? backgroundColor;
/// bottom自适应
/// If true the [body] and the scaffold's floating widgets should size
/// themselves to avoid the onscreen keyboard whose height is defined by the
/// ambient [MediaQuery]'s [MediaQueryData.viewInsets] `bottom` property.
/// 针对有输入框的页面,若设置为false,可以解决pixel冲突的问题,当然前提是使用了scaffold容器
/// [resizeToAvoidBottomInset] 初始化后不允许修改
final bool resizeToAvoidBottomInset;
/// 不为空时,iOS滑动返回(Swipe Back)效果会失效
final Future<bool> Function()? onPageWillPop;
/// 导航栏title
String? _title;
/// 如果想拓展在initState时设置title,可使用以下方式:
/// 但是,很不建议这样做,在[BasePageState.pageConfigBuilder]中初始化才是最佳方式!!!
/// ```
/// set title(String? title) => _title = title;
/// ```
String? get title => _title;
/// 「不需要」也「不要」调用setState(() {}),避免页面重绘!!!
/// ```
/// set title(String? title) {
/// _title = title;
/// notifyListeners();
/// }
/// ```
void updateTitle(String? title) {
_title = title;
notifyListeners();
}
/// 是否隐藏导航栏左侧回退按钮,默认展示
final bool? hideBackWidget;
/// 导航栏左侧回退按钮
Widget? _backWidget;
Widget? get backWidget => _backWidget;
/// 「不需要」也「不要」调用 setState(() {}),避免页面重绘!!!
/// [backWidget] 为 null 时使用默认样式
void updateBackWidget(Widget? backWidget, {EdgeInsetsGeometry? padding}) {
_backWidget = ((backWidget == null)
? null
: Padding(
padding: padding ?? const EdgeInsets.fromLTRB(5, 10, 10, 10),
child: backWidget,
));
notifyListeners();
}
/// 导航栏左侧回退按钮监听,为空时点击默认退出当前页
VoidCallback? _backCallback;
VoidCallback? get backCallback => _backCallback;
/// 「不需要」也「不要」调用 setState(() {}),避免页面重绘!!!
/// [backCallback] 为 null 时使用默认行为
void updateBackCallback(VoidCallback? backCallback) {
_backCallback = backCallback;
notifyListeners();
}
/// 导航栏右侧更多按钮
List<Widget>? _moreActions;
List<Widget>? get moreActions => _moreActions;
/// 「不需要」也「不要」调用 setState(() {}),避免页面重绘!!!
void updateMoreActions(List<Widget>? moreActions) {
_moreActions = moreActions;
notifyListeners();
}
/// [_moreActions] 置灰且点击无效化
bool? _invalidActions;
bool? get invalidActions => _invalidActions;
/// 「不需要」也「不要」调用 setState(() {}),避免页面重绘!!!
void updateInvalidActions(bool? invalidActions) {
_invalidActions = invalidActions;
notifyListeners();
}
/// 导航栏底部阴影
final double? elevation;
/// 导航栏投影颜色
final Color? shadowColor;
PageConfig({
this.hideAppBar = false,
this.backgroundColor,
this.resizeToAvoidBottomInset = false,
this.onPageWillPop,
String? title,
this.hideBackWidget,
Widget? backWidget,
VoidCallback? backCallback,
List<Widget>? moreActions,
bool? invalidActions,
this.elevation,
this.shadowColor,
}) {
_title = title;
_backWidget = backWidget;
_backCallback = backCallback;
_moreActions = moreActions;
_invalidActions = invalidActions;
}
}
class _ListenableAppBar extends PreferredSize {
_ListenableAppBar({
super.key,
required Widget child,
double? toolbarHeight = BaseAppBar.sToolbarHeight,
PreferredSizeWidget? bottom,
}) : super(
child: child,
preferredSize:
_PreferredAppBarSize(toolbarHeight, bottom?.preferredSize.height),
);
}
/// flutter/packages/flutter/lib/src/material/app_bar.dart:57【app_bar._PreferredAppBarSize】
class _PreferredAppBarSize extends Size {
_PreferredAppBarSize(this.toolbarHeight, this.bottomHeight)
: super.fromHeight(
(toolbarHeight ?? kToolbarHeight) + (bottomHeight ?? 0));
final double? toolbarHeight;
final double? bottomHeight;
}
/// AppBar基类/默认UI
class BaseAppBar extends AppBar {
static const double sToolbarHeight = 45;
BaseAppBar(
BuildContext context, {
Key? key,
String? titleStr,
double? elevation,
Color? shadowColor,
bool hideBackWidget = false,
Widget? backWidget,
VoidCallback? backCallback,
List<Widget>? moreActions,
bool invalidActions = false,
double? toolbarHeight = sToolbarHeight,
PreferredSizeWidget? bottom,
}) : super(
key: key,
title: (titleStr?.isNotEmpty == true)
? Text(
titleStr!,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.black,
),
)
: null,
centerTitle: true,
backgroundColor: Colors.white,
toolbarHeight: toolbarHeight,
bottom: bottom,
elevation: elevation ?? 0,
shadowColor: shadowColor,
actions: (invalidActions && (moreActions != null))
? (moreActions
.map((child) =>
AbsorbPointer(child: Opacity(opacity: 0.3, child: child)))
.toList())
: moreActions,
leading: hideBackWidget
? null
: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: backCallback ?? () => Navigator.pop(context),
child: AbsorbPointer(
child: backWidget ??
const Icon(
Icons.arrow_back_ios,
color: Colors.black,
size: 21,
),
),
),
);
}
/// 用于构建固定样式的 moreActions 按钮
class MoreActionWidget extends StatelessWidget {
final Widget child;
final Function()? onClick;
const MoreActionWidget({Key? key, required this.child, this.onClick})
: super(key: key);
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onClick,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
child,
const SizedBox(width: 16),
],
),
);
}
}
更多推荐



所有评论(0)