简介

通用页面 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),
        ],
      ),
    );
  }
}
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐