Flutter for OpenHarmony 个人理财管理App实战 - 主框架与底部导航
本文介绍了理财类应用的主框架和底部导航实现方案。选用convex_bottom_bar库实现底部Tab导航,支持中间凸起按钮效果,包含首页、统计、预算和我的四个主要模块。采用IndexedStack管理页面切换,保持各页面状态不重建。通过GetX状态管理控制导航切换,使用FloatingActionButton作为快速记账入口。整体设计遵循用户心智模型,优化了导航体验和性能表现。
上一篇完成了项目的基础架构搭建,这篇来实现应用的主框架和底部导航。一个好的导航设计能让用户快速找到想要的功能,理财类应用通常采用底部 Tab 导航,把最常用的功能放在一级入口。
导航方案选型
Flutter 自带的 BottomNavigationBar 功能够用,但样式比较普通。这次选用 convex_bottom_bar 这个库,它支持中间凸起的按钮效果,视觉上更有层次感,也更符合现代应用的设计趋势。
选择第三方库时要考虑几个因素:功能是否满足需求、维护是否活跃、文档是否完善、社区评价如何。convex_bottom_bar 在这几个方面都表现不错,是个可靠的选择。
底部导航需要承载四个主要模块:首页、统计、预算、我的。中间再放一个悬浮的添加按钮,方便用户快速记账。这种布局在记账类应用中很常见,用户已经形成了使用习惯。
导航设计要遵循用户的心智模型。用户对记账应用有一定的预期,比如首页看总览、统计看分析、预算看控制、我的看设置。按照这个预期来设计,用户上手会更快。
四个 Tab 的选择是经过深思熟虑的。首页是用户最常访问的页面,放在第一位。统计和预算是核心功能,放在中间。我的页面访问频率较低,放在最后。
主框架页面结构
MainPage 是整个应用的容器,负责管理底部导航和页面切换。先看整体结构:
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:convex_bottom_bar/convex_bottom_bar.dart';
import '../home/home_page.dart';
import '../statistics/statistics_page.dart';
import '../budget/budget_page.dart';
import '../profile/profile_page.dart';
import '../../routes/app_pages.dart';
import 'main_controller.dart';
const _primaryColor = Color(0xFF2E7D32);
导入部分包含了 Flutter 核心库、GetX、convex_bottom_bar,以及四个子页面和路由配置。导入语句的组织要有规律,通常按照:Dart 核心库、Flutter 库、第三方库、项目内部文件的顺序排列。
颜色定义放在文件顶部用 const 声明,这样做有两个好处:一是避免在 build 方法中重复创建 Color 对象,二是方便统一管理和修改。_primaryColor 用下划线前缀表示私有,只在当前文件使用。
常量的使用是性能优化的基础。const 对象在编译时就确定了,运行时不需要重新创建。在 Flutter 中,尽可能使用 const 可以减少不必要的对象创建和垃圾回收。
页面类的定义:
class MainPage extends StatelessWidget {
const MainPage({super.key});
Widget build(BuildContext context) {
final controller = Get.put(MainController());
final pages = [
const HomePage(),
const StatisticsPage(),
const BudgetPage(),
const ProfilePage()
];
MainPage 继承自 StatelessWidget,因为状态由 GetX Controller 管理,页面本身不需要持有状态。这是 GetX 推荐的做法,让 Widget 保持纯净。
Get.put 在 build 方法中调用,GetX 会自动处理重复注册的情况。如果 Controller 已存在就返回现有实例,不存在才创建新的。这种写法简洁,不需要在 Binding 中提前注册。
pages 数组定义了四个子页面,都用 const 修饰。这些页面在整个应用生命周期内只创建一次,切换 Tab 时不会重新创建。
页面数组的顺序要和底部导航的顺序一致。索引 0 对应首页,索引 1 对应统计,以此类推。如果顺序不一致,会导致点击 Tab 时显示错误的页面。
Scaffold 结构
Scaffold 是 Flutter 页面的基础结构:
return Scaffold(
body: Obx(() => IndexedStack(
index: controller.currentIndex.value,
children: pages
)),
bottomNavigationBar: Obx(() => ConvexAppBar(
style: TabStyle.react,
backgroundColor: _primaryColor,
activeColor: Colors.white,
color: Colors.white70,
items: const [
TabItem(icon: Icons.home, title: '首页'),
TabItem(icon: Icons.pie_chart, title: '统计'),
TabItem(icon: Icons.account_balance_wallet, title: '预算'),
TabItem(icon: Icons.person, title: '我的'),
],
initialActiveIndex: controller.currentIndex.value,
onTap: controller.changePage,
)),
Scaffold 提供了应用页面的基本结构,包括 body、appBar、bottomNavigationBar、floatingActionButton 等。这里只用了 body、bottomNavigationBar 和 floatingActionButton。
body 部分用 IndexedStack 包裹四个子页面,通过 index 控制显示哪个页面。Obx 包裹 IndexedStack,当 currentIndex 变化时自动更新显示。
bottomNavigationBar 使用 ConvexAppBar,配置了样式、颜色、图标等属性。同样用 Obx 包裹,保证状态同步。
悬浮按钮的配置:
floatingActionButton: FloatingActionButton(
onPressed: () => Get.toNamed(Routes.addTransaction),
backgroundColor: _primaryColor,
child: const Icon(Icons.add, color: Colors.white),
),
);
}
}
FloatingActionButton 是 Material Design 的标志性组件,用于最重要的操作。在理财应用中,添加交易是最高频的操作,放在悬浮按钮上非常合适。
点击悬浮按钮后,通过 Get.toNamed 跳转到添加交易页面。命名路由的好处是解耦,页面之间不需要直接引用,只需要知道路由名称。
悬浮按钮的位置默认在右下角,正好在底部导航栏上方。这个位置用户容易触及,也不会遮挡重要内容。
IndexedStack 的妙用
页面切换用 IndexedStack 而不是 PageView,这是个重要的选择:
body: Obx(() => IndexedStack(
index: controller.currentIndex.value,
children: pages
)),
IndexedStack 会同时保持所有子页面的状态,切换时不会重建页面。对于理财应用来说,用户可能在首页看了数据,切到统计页面分析,再切回首页,数据应该还在,不需要重新加载。
这种行为和 PageView 不同。PageView 默认只保持相邻页面的状态,其他页面会被销毁。虽然可以通过 AutomaticKeepAliveClientMixin 来保持状态,但 IndexedStack 更简单直接。
IndexedStack 的缺点是所有子页面都会被创建,即使用户从未访问过。对于只有四个页面的应用,这个开销可以接受。如果页面数量很多,需要考虑懒加载策略。
Obx 包裹 IndexedStack,当 currentIndex 变化时只重建这一小块,而不是整个 Scaffold。这是 GetX 响应式更新的精髓,只更新需要更新的部分。
pages 数组在 build 方法开头定义,每次 build 都会创建新的数组,但里面的 Widget 是 const 的,实际上不会重复创建。这是 Dart 的常量优化机制。
如果页面数量很多或者某些页面初始化开销大,可以考虑懒加载:
final pages = [
const HomePage(),
controller.currentIndex.value >= 1
? const StatisticsPage()
: const SizedBox(),
controller.currentIndex.value >= 2
? const BudgetPage()
: const SizedBox(),
controller.currentIndex.value >= 3
? const ProfilePage()
: const SizedBox(),
];
这种方式只有在用户访问过某个 Tab 后才会创建对应的页面。SizedBox 是一个轻量级的占位组件,几乎没有开销。
不过对于只有四个页面的应用,这种优化意义不大,反而增加了复杂度。过早优化是万恶之源,应该在确实遇到性能问题时再考虑优化。
ConvexAppBar 配置详解
convex_bottom_bar 提供了多种样式,这里选用 TabStyle.react:
bottomNavigationBar: Obx(() => ConvexAppBar(
style: TabStyle.react,
backgroundColor: _primaryColor,
activeColor: Colors.white,
color: Colors.white70,
items: const [
TabItem(icon: Icons.home, title: '首页'),
TabItem(icon: Icons.pie_chart, title: '统计'),
TabItem(icon: Icons.account_balance_wallet, title: '预算'),
TabItem(icon: Icons.person, title: '我的'),
],
initialActiveIndex: controller.currentIndex.value,
onTap: controller.changePage,
)),
TabStyle.react 是一种响应式风格,点击时有轻微的缩放动画,交互感比较好。动画让界面更生动,也给用户即时的反馈。
其他可选的样式包括:TabStyle.fixed 是固定样式,没有动画;TabStyle.fixedCircle 中间按钮是圆形凸起;TabStyle.flip 是翻转动画效果;TabStyle.textIn 文字从下方滑入。
backgroundColor 设置导航栏背景色,和 AppBar 保持一致,形成统一的视觉风格。activeColor 是选中状态的颜色,color 是未选中状态的颜色,用 white70 稍微透明一点,形成对比。
颜色的对比度很重要。选中和未选中状态要有明显区分,让用户一眼就能看出当前在哪个 Tab。但对比也不能太强烈,否则会显得刺眼。
items 数组定义每个 Tab 的图标和文字:
items: const [
TabItem(icon: Icons.home, title: '首页'),
TabItem(icon: Icons.pie_chart, title: '统计'),
TabItem(icon: Icons.account_balance_wallet, title: '预算'),
TabItem(icon: Icons.person, title: '我的'),
],
图标选择要有辨识度:home 代表首页,pie_chart 代表统计图表,account_balance_wallet 代表钱包/预算,person 代表个人中心。这些都是用户熟悉的隐喻,不需要额外解释。
图标和文字要配合使用。图标提供快速识别,文字提供明确含义。只有图标可能会让用户困惑,只有文字又不够直观。
initialActiveIndex 设置初始选中的 Tab,这里绑定到 controller 的状态。onTap 是点击回调,调用 controller.changePage 切换页面。
回调函数的设计要简洁。onTap 只负责通知 Controller,具体的逻辑在 Controller 中处理。这种分离让代码更清晰,也更容易测试。
悬浮添加按钮
记账是高频操作,把添加按钮放在显眼的位置很重要:
floatingActionButton: FloatingActionButton(
onPressed: () => Get.toNamed(Routes.addTransaction),
backgroundColor: _primaryColor,
child: const Icon(Icons.add, color: Colors.white),
),
FloatingActionButton 默认会悬浮在右下角,正好在底部导航栏上方。这个位置符合人体工程学,用户的拇指容易触及。
点击后跳转到添加交易页面,用 Get.toNamed 进行命名路由导航。导航完成后,用户可以通过返回按钮回到主页面。
有些应用会把添加按钮放在底部导航栏中间,做成凸起的效果。convex_bottom_bar 也支持这种布局,但需要处理点击事件的特殊逻辑。这里选择分开放置,实现更简单,功能上也没有区别。
悬浮按钮的设计要考虑可见性和可触及性。按钮要足够大,颜色要醒目,位置要方便点击。同时也要注意不要遮挡重要内容。
控制器实现
MainController 非常简单,只需要管理当前选中的 Tab 索引:
import 'package:get/get.dart';
class MainController extends GetxController {
final RxInt currentIndex = 0.obs;
void changePage(int index) {
currentIndex.value = index;
}
}
currentIndex 用 RxInt 包装,初始值为 0 表示默认显示首页。RxInt 是 GetX 的响应式整数类型,值变化时会自动通知监听者。
changePage 方法在点击 Tab 时调用,更新索引值。方法实现非常简单,就是赋值操作。但通过方法封装,可以在后续添加额外逻辑,比如埋点统计。
这个 Controller 看起来简单到没必要单独抽出来,但这样做有几个好处:保持 View 层的纯净,所有状态都在 Controller 中;方便后续扩展,比如添加页面切换的埋点统计;可以在其他地方通过 Get.find 获取并控制页面切换。
简单的 Controller 也是 Controller。不要因为逻辑简单就把状态放在 Widget 中,保持架构的一致性更重要。
比如在添加交易成功后,可能想自动切换到首页查看最新记录:
final mainController = Get.find<MainController>();
mainController.changePage(0);
这种跨页面的状态控制,只有把状态放在 Controller 中才能实现。如果状态在 Widget 中,其他页面无法访问。
Binding 配置
MainBinding 用于注册 MainController:
import 'package:get/get.dart';
import 'main_controller.dart';
class MainBinding extends Bindings {
void dependencies() {
Get.lazyPut<MainController>(() => MainController());
}
}
lazyPut 是懒加载注册,只有在第一次 Get.find 时才会创建实例。对于主框架这种应用启动就需要的页面,用 put 或 lazyPut 区别不大。
但养成用 lazyPut 的习惯是好的,可以减少启动时的初始化开销。特别是对于那些可能不会被访问的页面,懒加载可以显著提升启动速度。
在路由配置中关联 Binding:
GetPage(
name: Routes.main,
page: () => const MainPage(),
binding: MainBinding()
),
这样当导航到 /main 路由时,GetX 会自动执行 MainBinding 的 dependencies 方法,注册所需的依赖。这种声明式的依赖注入让代码更清晰。
Binding 和路由的关联是 GetX 的特色功能。它让依赖的生命周期和页面的生命周期绑定,页面销毁时依赖也会被清理,避免内存泄漏。
页面切换动画
默认情况下 IndexedStack 切换是没有动画的,页面直接替换。如果想要添加淡入淡出效果,可以用 AnimatedSwitcher:
body: Obx(() => AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: pages[controller.currentIndex.value],
)),
AnimatedSwitcher 会在子组件变化时自动播放动画。duration 设置动画时长,200 毫秒是比较舒适的时长,不会太快也不会太慢。
但这样做会失去 IndexedStack 保持状态的优势,每次切换都会重建页面。这是一个权衡:要动画效果还是要状态保持。
折中的方案是给每个页面加上 key:
body: Obx(() => AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: KeyedSubtree(
key: ValueKey(controller.currentIndex.value),
child: pages[controller.currentIndex.value],
),
)),
KeyedSubtree 可以给子组件指定 key,AnimatedSwitcher 根据 key 的变化来判断是否需要播放动画。但这仍然不能保持页面状态。
不过对于底部导航这种高频切换的场景,我倾向于不加动画,响应更快,用户体验更好。动画虽然好看,但如果影响了响应速度,得不偿失。
处理返回键
在 Android 上,用户按返回键时的预期行为是:如果不在首页,先切回首页;如果已经在首页,再按一次退出应用。实现这个逻辑需要用 WillPopScope:
return WillPopScope(
onWillPop: () async {
if (controller.currentIndex.value != 0) {
controller.changePage(0);
return false;
}
return true;
},
child: Scaffold(
// ...
),
);
onWillPop 返回 false 表示拦截返回事件,返回 true 表示允许返回(退出应用)。这种处理方式符合用户的直觉,避免误触退出。
WillPopScope 是 Flutter 提供的返回键拦截组件。它包裹在需要拦截返回键的组件外面,当用户按返回键时会调用 onWillPop 回调。
更友好的做法是双击退出,给用户一个确认的机会:
DateTime? _lastPressedAt;
onWillPop: () async {
if (controller.currentIndex.value != 0) {
controller.changePage(0);
return false;
}
if (_lastPressedAt == null ||
DateTime.now().difference(_lastPressedAt!) >
const Duration(seconds: 2)) {
_lastPressedAt = DateTime.now();
Get.snackbar('提示', '再按一次退出应用');
return false;
}
return true;
},
这段代码的逻辑是:如果两次按返回键的间隔超过 2 秒,就显示提示并重置计时;如果间隔在 2 秒内,就允许退出。这种设计可以有效防止误触退出。
Get.snackbar 是 GetX 提供的便捷方法,可以快速显示一个 Snackbar 提示。不需要 BuildContext,在任何地方都可以调用。
底部导航栏高度适配
在有底部安全区域的设备上(比如 iPhone X 及以后的机型),需要注意导航栏的高度适配:
bottomNavigationBar: SafeArea(
child: ConvexAppBar(
height: 50,
// ...
),
),
SafeArea 会自动处理安全区域的内边距,避免内容被刘海或底部横条遮挡。convex_bottom_bar 默认会处理这个问题,但如果你自定义了高度,要记得加上安全区域。
或者使用 MediaQuery 获取底部安全区域高度:
final bottomPadding = MediaQuery.of(context).padding.bottom;
MediaQuery 提供了设备的各种信息,包括屏幕尺寸、像素密度、安全区域等。在需要精确控制布局时,这些信息非常有用。
安全区域的处理是跨平台开发的重要课题。不同设备的安全区域大小不同,有的设备有刘海,有的有底部横条,有的两者都有。正确处理安全区域可以让应用在各种设备上都有良好的显示效果。
导航栏显示隐藏
某些页面可能需要隐藏底部导航栏,比如全屏查看图表。可以在 Controller 中添加一个控制变量:
class MainController extends GetxController {
final RxInt currentIndex = 0.obs;
final RxBool showBottomNav = true.obs;
void changePage(int index) {
currentIndex.value = index;
}
void hideBottomNav() => showBottomNav.value = false;
void showBottomNavBar() => showBottomNav.value = true;
}
showBottomNav 控制导航栏的显示状态。hideBottomNav 和 showBottomNavBar 方法分别用于隐藏和显示导航栏。
然后在 UI 中根据这个变量决定是否显示:
bottomNavigationBar: Obx(() => controller.showBottomNav.value
? ConvexAppBar(...)
: const SizedBox.shrink()
),
SizedBox.shrink 是一个零尺寸的组件,用于占位但不显示任何内容。当 showBottomNav 为 false 时,导航栏会消失,页面内容会占据整个屏幕。
这种设计让导航栏的显示隐藏变得可控。子页面可以通过 Get.find 获取 MainController,然后调用相应的方法来控制导航栏。
徽章显示
有时候需要在 Tab 上显示徽章,比如未读消息数量:
items: [
TabItem(icon: Icons.home, title: '首页'),
TabItem(icon: Icons.pie_chart, title: '统计'),
TabItem(icon: Icons.account_balance_wallet, title: '预算'),
TabItem(
icon: Stack(
children: [
const Icon(Icons.person),
Positioned(
right: 0,
top: 0,
child: Container(
padding: const EdgeInsets.all(2),
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
child: const Text('3',
style: TextStyle(fontSize: 10, color: Colors.white)),
),
),
],
),
title: '我的'
),
],
使用 Stack 在图标上叠加一个红色圆点,显示数字。这种徽章设计在很多应用中都能看到,用户已经习惯了这种提示方式。
徽章的数字应该来自 Controller 的状态,当有新消息时更新数字,消息已读后清除徽章。这需要和后端或本地存储配合实现。
键盘弹出时的处理
当键盘弹出时,底部导航栏默认会被顶上去。如果不想要这个行为,可以设置 Scaffold 的 resizeToAvoidBottomInset:
return Scaffold(
resizeToAvoidBottomInset: false,
body: ...,
bottomNavigationBar: ...,
);
设为 false 后,键盘弹出时页面不会调整大小,底部导航栏会被键盘遮挡。这在某些场景下是需要的,比如聊天页面。
但对于理财应用,通常希望键盘弹出时能看到输入框,所以保持默认值 true 就好。
键盘处理是移动端开发的常见问题。不同的页面可能需要不同的处理方式,要根据具体场景来决定。
页面生命周期
GetX Controller 提供了完整的生命周期回调:
class MainController extends GetxController {
void onInit() {
super.onInit();
// 初始化逻辑
}
void onReady() {
super.onReady();
// 页面渲染完成后的逻辑
}
void onClose() {
super.onClose();
// 清理逻辑
}
}
onInit 在 Controller 创建后立即调用,适合做初始化工作。onReady 在页面渲染完成后调用,适合做需要 BuildContext 的操作。onClose 在 Controller 销毁前调用,适合做清理工作。
生命周期的正确使用对于资源管理很重要。在 onInit 中创建的资源,应该在 onClose 中释放。比如定时器、流订阅等,不释放会导致内存泄漏。
状态持久化
如果希望用户下次打开应用时还在上次的 Tab,可以把 currentIndex 持久化:
class MainController extends GetxController {
final RxInt currentIndex = 0.obs;
final _storage = Get.find<StorageService>();
void onInit() {
super.onInit();
currentIndex.value = _storage.lastTabIndex;
}
void changePage(int index) {
currentIndex.value = index;
_storage.saveLastTabIndex(index);
}
}
在 onInit 中读取上次保存的索引,在 changePage 中保存当前索引。这样用户的使用习惯会被记住。
状态持久化是提升用户体验的小细节。用户不需要每次都从首页开始,可以直接回到上次离开的地方。
深色模式适配
底部导航栏需要适配深色模式:
bottomNavigationBar: Obx(() => ConvexAppBar(
style: TabStyle.react,
backgroundColor: Theme.of(context).brightness == Brightness.dark
? const Color(0xFF1E1E1E)
: _primaryColor,
activeColor: Colors.white,
color: Colors.white70,
// ...
)),
通过 Theme.of(context).brightness 判断当前是否为深色模式,然后使用不同的背景色。深色模式下使用深灰色背景,浅色模式下使用主题色背景。
深色模式的适配要全面。不仅是导航栏,所有组件都需要考虑深色模式下的显示效果。使用 Theme 可以让适配更加系统化。
无障碍支持
为了让视障用户也能使用应用,需要添加无障碍支持:
TabItem(
icon: Icons.home,
title: '首页',
),
convex_bottom_bar 的 TabItem 会自动使用 title 作为无障碍标签。屏幕阅读器会朗读"首页",让视障用户知道这是什么功能。
如果需要更详细的描述,可以使用 Semantics 组件包裹:
Semantics(
label: '首页,查看资产总览和最近交易',
child: TabItem(icon: Icons.home, title: '首页'),
)
无障碍支持是应用质量的重要指标。让更多人能够使用应用,是开发者的社会责任。
测试考虑
主框架的测试主要关注页面切换逻辑:
void main() {
test('changePage should update currentIndex', () {
final controller = MainController();
controller.changePage(2);
expect(controller.currentIndex.value, 2);
});
}
这个测试验证 changePage 方法能正确更新 currentIndex。测试要覆盖正常情况和边界情况,比如索引为 0 或最大值时的行为。
Widget 测试可以验证 UI 的正确性:
testWidgets('MainPage should show correct tab', (tester) async {
await tester.pumpWidget(const MaterialApp(home: MainPage()));
expect(find.text('首页'), findsOneWidget);
await tester.tap(find.text('统计'));
await tester.pump();
// 验证页面切换
});
测试是保证代码质量的重要手段。主框架作为应用的核心组件,更需要充分的测试覆盖。
性能优化
主框架的性能优化主要关注以下几点:
- 使用 const 构造函数减少对象创建
- 使用 IndexedStack 保持页面状态,避免重复创建
- Obx 精确包裹需要更新的组件,减少重建范围
- 避免在 build 方法中做耗时操作
// 好的做法
final pages = const [
HomePage(),
StatisticsPage(),
BudgetPage(),
ProfilePage()
];
// 不好的做法
final pages = [
HomePage(), // 每次 build 都会创建新实例
StatisticsPage(),
BudgetPage(),
ProfilePage()
];
const 的使用看起来是小事,但积少成多,对性能的影响是显著的。养成使用 const 的习惯,是 Flutter 开发的基本功。
性能优化要有数据支撑。使用 Flutter DevTools 可以分析应用的性能,找出瓶颈所在。不要凭感觉优化,要用数据说话。
小结
主框架的实现看起来简单,但细节不少。核心要点包括:
用 IndexedStack 保持页面状态,避免切换时重建。这是底部导航的最佳实践,用户体验更好。
ConvexAppBar 提供美观的底部导航效果。选择合适的样式和颜色,让导航栏和整体设计风格一致。
悬浮按钮放在显眼位置,方便快速记账。添加交易是最高频的操作,要让用户触手可及。
Controller 管理页面索引,保持 View 层纯净。状态和 UI 分离,代码更清晰,也更容易测试。
处理返回键逻辑,提升用户体验。符合用户预期的行为,让应用更易用。
这套框架搭好之后,后续只需要专注于各个子页面的实现。下一篇会介绍首页的设计和实现,包括资产总览、月度统计、快捷功能入口等内容。
主框架是应用的骨架,它的设计决定了整个应用的结构。花时间把主框架做好,后续的开发会更加顺畅。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
多语言适配
底部导航的文字需要支持多语言:
items: [
TabItem(icon: Icons.home, title: 'home'.tr),
TabItem(icon: Icons.pie_chart, title: 'statistics'.tr),
TabItem(icon: Icons.account_balance_wallet, title: 'budget'.tr),
TabItem(icon: Icons.person, title: 'profile'.tr),
],
使用 GetX 的 .tr 扩展方法获取翻译文本。当语言切换时,导航栏的文字会自动更新。
多语言支持是应用国际化的基础。即使当前只支持中文,也建议从一开始就做好多语言架构,后续添加其他语言会更容易。
翻译文本要简洁。导航栏空间有限,文字太长会显示不全或换行,影响美观。
横屏适配
在平板或横屏模式下,底部导航可能需要调整:
Widget build(BuildContext context) {
final isLandscape = MediaQuery.of(context).orientation == Orientation.landscape;
if (isLandscape) {
return Row(
children: [
NavigationRail(
selectedIndex: controller.currentIndex.value,
onDestinationSelected: controller.changePage,
destinations: const [
NavigationRailDestination(icon: Icon(Icons.home), label: Text('首页')),
NavigationRailDestination(icon: Icon(Icons.pie_chart), label: Text('统计')),
NavigationRailDestination(icon: Icon(Icons.account_balance_wallet), label: Text('预算')),
NavigationRailDestination(icon: Icon(Icons.person), label: Text('我的')),
],
),
Expanded(child: pages[controller.currentIndex.value]),
],
);
}
return Scaffold(...);
}
横屏时使用 NavigationRail 侧边导航,更好地利用屏幕空间。NavigationRail 是 Material Design 推荐的横屏导航方案。
响应式布局是跨平台开发的重要课题。同一套代码要在手机、平板、桌面等不同设备上都有良好的体验。
MediaQuery 提供了设备的各种信息,可以根据这些信息调整布局。除了方向,还可以根据屏幕宽度来决定使用哪种布局。
更多推荐
所有评论(0)