Flutter之基类逻辑 2025-04-21 周一
简介
-
GetX的最大价值是简化了页面导航的写法,去掉了耦合的context,使用起来方便很多。
-
其次,GetBuilder和update()的组合,可以将页面和逻辑分开,这也在一定程度上简化了业务。
-
BasePage基于GetxController,复用的内容还是太少,所以考虑增加一个BaseLogic与之对应,进一步增加基类内容,增加代码复用。
上拉下拉
-
将下拉刷新插件改为pull_to_refresh之后,代码简化了不少。
-
如果想基类中整合下拉刷新组件,那么BaseLogic可以继承自BaseRefreshLogic,因为后者就是继承自GetxController
-
对于不需要下拉刷新功能的页面,只要不激活这部分代码,不展示这部分页面就可以了。基类中多一些冗余代码,问题不大。
页面分类
-
根据工程中出现过的页面进行分类,划分页面类型;
-
把一些共性代码放到基类中,增加复用;
-
根据页面类型参数PageType pageType,调用不同的_buildXXX方法
-
将一些自定义的部分,一占位函数的方法buildXXX()供子类重写
-
最外层套一个手势,加上一个去焦点收键盘的方法
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
FocusScope.of(context).requestFocus(FocusNode());
},
child: GetBuilder<T>(
tag: tag,
builder: (_) {
switch (pageType) {
case PageType.scaffold:
return _buildScaffold();
case PageType.custom:
return _buildCustom();
case PageType.gradientNavigationBar:
return _buildGradientNavigationBar();
case PageType.refresh:
return _buildRefresh();
}
},
),
);
}
1. 普通页面
- 就是通常所说的Scaffold页面,当然实际使用时,会在Scaffold外面套一层,提供一个XXScaffold的组件,将一些背景色之类的统一写了。
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
FocusScope.of(Get.context!).requestFocus(FocusNode());
},
child: RepaintBoundary(
child: Scaffold(
backgroundColor: widget.backgroundColor ?? ColorUtils.c_f8f8f8,
// backgroundColor: Colors.white,
appBar: AppBar(
flexibleSpace: Container(
color: widget.flexibleSpaceColor ?? ColorUtils.c_ffffff,
),
centerTitle: true,
elevation: 0,
backgroundColor: widget.backgroundColor ?? ColorUtils.c_ffffff,
// backgroundColor: Colors.white,
title: widget.title != null ? Text(widget.title!, style: StyleUtils.ts_1f_17_600) : widget.titleWidget,
iconTheme: IconThemeData(color: ColorUtils.c_1f1f1f),
leading: widget.leading,
leadingWidth: widget.leadingWidth,
actions: widget.actions,
bottom: widget.bottom,
),
body: widget.dataReady ? widget.body : buildEmptyWidget(),
floatingActionButton: widget.floatingActionButton,
floatingActionButtonLocation: widget.floatingActionButtonLocation,
),
),
);
}
- 这是用得最多的页面,大多数时候只要改一下title和body就可以了。少部分时候,action和bottom也会用到。至于leading和floatingButton之类的用的时候很少,还不如直接隐藏掉。
abstract class BasePage<T extends BaseLogic> extends StatelessWidget {
const BasePage({super.key, this.tag});
final String? tag;
T get logic => GetInstance().find<T>(tag: tag);
@override
Widget build(BuildContext context) {
return GetBuilder<T>(
tag: tag,
builder: (_) {
return _buildScaffold();
},
);
}
/// 重写这个切换页面类型
final PageType pageType = PageType.scaffold;
Widget _buildScaffold() {
return GBScaffold(
title: title,
body: buildBody(),
actions: buildActions(),
bottom: buildBottom(),
);
}
final String title = "";
Widget buildBody() {
return Container();
}
List<Widget>? buildActions() {
return null;
}
PreferredSizeWidget? buildBottom() {
return null;
}
}
enum PageType {
scaffold,
custom,
}
Scaffold页面
- 使用的时候,只需要重写title,body等变量和方法就可以了。
class CartPage extends BasePage<CartLogic> {
CartPage({super.key, super.tag}) {
Get.put(CartLogic(), tag: tag);
}
@override
String get title => "tab_cart".tr;
@override
Widget buildBody() {
return Column(
children: [
messageRow(),
logic.dataList.isNotEmpty
? Expanded(child: listWidget())
: Expanded(
child: Kong(
img: R.assetsImgKongNocontent,
title: logic.dataReady ? 'cart_empty_tip'.tr : "",
)),
operatorWidget(),
],
);
}
@override
PreferredSizeWidget? buildBottom() {
return CartActionWidget(
onEditClick: logic.onEditClick,
onCouponClick: logic.onCouponClick,
number: logic.productNumber,
onFinishClick: logic.onFinishClick,
showEdit: logic.showEdit,
);
}
2. 自定义页面
-
leading和floatingButton这些虽然用的不多,但是有时候也需要用到的
-
这里干脆就不限制,基类只提供一个空白函数,让使用者完全自定义。
abstract class BasePage<T extends BaseLogic> extends StatelessWidget {
const BasePage({super.key, this.tag});
final String? tag;
T get logic => GetInstance().find<T>(tag: tag);
@override
Widget build(BuildContext context) {
return GetBuilder<T>(
tag: tag,
builder: (_) {
switch (pageType) {
case PageType.scaffold:
return _buildScaffold();
case PageType.custom:
return buildCustom();
}
},
);
}
/// 重写这个切换页面类型
final PageType pageType = PageType.scaffold;
/// 自定义页面
Widget buildCustom() {
return Container();
}
}
enum PageType {
scaffold,
custom,
}
3. 渐变导航栏
-
默认是透明的导航栏,随着上划,导航栏变成不透明的灰色。
透明导航栏
灰色导航栏
-
SliverAppBar感觉很接近这种效果,但是实践下来总感觉力不从心,效果总是差那么一点。
-
所以准备采用stack,分成背景,内容,头部三层视图,并且通过滑动监听来达到效果。
/// 渐变导航栏
Widget _buildGradientNavigationBar() {
return Scaffold(
body: Stack(
children: [
buildBackground(),
buildContent(),
buildNavigationBar(),
],
),
);
}
/// 第1层:背景
Widget buildBackground() {
return Image.asset(
backgroundImageName,
width: ScreenUtil().screenWidth,
fit: BoxFit.fitWidth,
);
}
/// 第2层内容
Widget buildContent() {
return NotificationListener(
onNotification: (notification) {
if (notification is ScrollUpdateNotification) {
/// 监听滚动位置,导航栏渐变
double scrollPosition = notification.metrics.pixels;
logic.onScrollChange(scrollPosition);
}
return true;
},
child: CustomScrollView(
slivers: buildSlivers(),
),
);
}
/// 第3层:背景色渐变的导航栏
Widget buildNavigationBar() {
return Container(
color: ColorUtils.c_f8f8f8.withValues(alpha: logic.alpha),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(height: GBUtils.getTopSafeHeight()),
SizedBox(
height: 44.r,
child: Stack(
children: [
Container(
padding: EdgeInsets.symmetric(horizontal: 40.r),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
title,
style: StyleUtils.ts_1f_17_600,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
Get.back();
},
child: Row(
children: [
SizedBox(width: 22.r),
Icon(
Icons.arrow_back_ios,
size: 22.r,
color: ColorUtils.c_000000,
),
SizedBox(width: 22.r),
],
),
),
],
),
],
),
),
],
),
);
}
4. 下拉刷新的滑动视图
-
下拉刷新包一个SmartRefresh插件
-
滑动组件固定使用CustomScrollView,只要重写buildSlivers()就好了。
-
空视图,网络错误视图也给出默认实现,也可以重写
/// 刷新视图
Widget _buildRefresh() {
return GBScaffold(
title: title,
actions: buildActions(),
bottom: buildBottom(),
body: Container(
margin: margin,
padding: padding,
child: RefreshWidget(
controller: logic.refreshController,
onRefresh: logic.onRefresh,
onLoad: logic.onLoad,
isEmpty: logic.isEmpty,
emptyWidget: buildEmpty(),
isNetworkError: logic.isNetworkError,
networkErrorWidget: buildNetworkError(),
slivers: buildSlivers(),
),
),
);
}
/// sliver类型的数组
List<Widget> buildSlivers() {
return [];
}
/// 空视图
Widget? buildEmpty() {
return null;
}
/// 网络错误
Widget? buildNetworkError() {
return null;
}
5. 普通滑动视图
-
如果没有超过一屏,不能滑动
-
如果超出一屏,可以滑动
-
固定采用SingleChildScrollView,内部套一个Column,只要重写buildChildren()就可以了。
/// 普通滑动:超出一屏滑动,否则不滑动
Widget _buildSingleScroll() {
return GBScaffold(
title: title,
actions: buildActions(),
bottom: buildBottom(),
body: Container(
margin: margin,
padding: padding,
child: SingleChildScrollView(
child: Column(
children: buildChildren(),
),
),
),
);
}
/// children数组
List<Widget> buildChildren() {
return [];
}
6. 嵌套滑动
-
顶部部分可以收缩的,能省空间
展开状态
-
收缩状态
-
固定采用NestedScrollView;头部可收缩部分重写buildSlivers();内容部分重写buildBody()
枚举类型
- 采用枚举类型区分各种页面模板
enum PageType {
scaffold,
custom,
gradientNavigationBar,
refresh,
singleScroll,
nestedScroll,
}
- 使用一个变量确定类型
/// 重写这个切换页面类型
final PageType pageType = PageType.scaffold;
- 根据类型调用不同的build函数,外面套一个收键盘的手势,方便使用
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
FocusScope.of(context).requestFocus(FocusNode());
},
child: GetBuilder<T>(
tag: tag,
builder: (_) {
switch (pageType) {
case PageType.scaffold:
return _buildScaffold();
case PageType.custom:
return buildCustom();
case PageType.gradientNavigationBar:
return _buildGradientNavigationBar();
case PageType.refresh:
return _buildRefresh();
case PageType.singleScroll:
return _buildSingleScroll();
case PageType.nestedScroll:
return _buildNestedScroll();
}
},
),
);
}