学习 Flutter(三)
在上一章节,我们对 Flutter 中常用的组件和架构有了一定的了解,那么现在我们既有轮子(基础UI),又有框架了,是时候开始造车了,那么本章将开始进行Android项目实战练习,具体实战什么看作者想要实战什么(无规划,难易不定)...遇到啥就针对的去实战,篇幅会比较长,尽量保证原生,不使用第三方插件,跟着一篇文章可以实现一个项目的完整运行!!!
在此声明,感谢大佬提供的开发API,玩Android 开放API-玩Android - wanandroid.com
一、项目准备
-
搭建项目结构
cpplib/ └─ app /// 应用配置层 └─ base /// 基础抽象层 └─ models /// 数据结构层 └─ pages /// UI 层 └─ providers /// 状态管理层 └─ services /// 业务接口层 └─ utils /// 工具类库层 └─ widgets /// 通用组件层 └─ main.dart /// 入口文件
-
创建资源文件夹
和
lib
同级目录下创建assets/images
文件夹,并且在pubspec.yaml
中进行添加yamlflutter: uses-material-design: true assets: - assets/images/
-
添加
http
相关首先在
android
包中找到AndroidManifest.xml
文件并声明网络请求权限,如果是要有ios
、linux
、windows
(作者不是很懂),也都声明一下对应包下的权限,并且我们要在pubspec.yaml
中进行添加http
相关包yamldependencies: flutter: sdk: flutter http: ^0.13.5
由于作者的 Flutter 版本只能支持到这个
http
版本,懒的去升级版本了,希望读者们能自行进行调整哈,主要是公司项目的版本就是这么低,作者也懒得去升级,还要更新一堆依赖,太麻烦了,又不是不能用! -
添加
provider
相关在
pubspec.yaml
中进行添加provider
相关包yamldependencies: flutter: sdk: flutter provider: ^6.1.1
provider
是 Flutter 官方推荐的状态管理方案之一,是基于 InheritedWidget 封装的简单、轻量级依赖注入和状态管理工具。它的设计理念是提供一种优雅且高效的手段,让 Widget 树中的各个组件能够访问和响应状态的变化,避免手动传递数据(避免"Prop Drilling"),满足中小型及大型项目的状态管理需求。简单点可以理解为 Android 中的 LiveData, 当前不是完全相同的还是有些差别的。
至此我们基础项目架构已经搭建完成了,我们接下来将逐步完成项目的实现。
二、app包
-
constant.dart
常量配置,如接口地址、主题色等
dartclass ApiConstants { static const String baseUrl = "https://www.wanandroid.com/"; /// 首页文章 static const String homePageArticle = "article/list/"; /// 置顶文章 static const String topArticle = "article/top/json"; /// 获取banner static const String banner = "banner/json"; /// 登录 static const String login = "user/login"; /// 注册 static const String register = "user/register"; /// 退出登录 static const String logout = "user/logout/json"; /// 项目分类 static const String projectCategory = "project/tree/json"; /// 项目列表 static const String projectList = "project/list/"; /// 搜索 static const String searchForKeyword = "article/query/"; /// 广场页列表 static const String plazaArticleList = "user_article/list/"; /// 点击收藏 static const String collectArticle = "lg/collect/"; /// 取消收藏 static const String uncollectArticel = "lg/uncollect_originId/"; /// 获取搜索热词 static const String hotKeywords = "hotkey/json"; /// 获取收藏文章列表 static const String collectList = "lg/collect/list/"; /// 收藏网站列表 static const String collectWebaddressList = "lg/collect/usertools/json"; /// 我的分享 static const String sharedList = "user/lg/private_articles/"; /// 分享文章 post static const String shareArticle = "lg/user_article/add/json"; /// todoList static const String todoList = "lg/todo/v2/list/"; } class RoutesConstants { /// 登录注册界面 static const String login = "/login_register"; /// 首页 static const String home = "/home"; }
-
routes.dart
路由表及跳转管理
dartclass AppRoutes{ static final routes = <String, WidgetBuilder>{ RoutesConstants.login: (_) => LoginRegisterPage(), RoutesConstants.home: (_) => HomePage(), }; }
三、base 包
-
base.dart
BasePage 是一个 Mixin,可用于所有继承 State 的类(例如 StatefulWidget 页面)
dartmixin BasePage<T extends StatefulWidget> on State<T> { /// 是否正在显示loading弹窗 bool showingLoading = false; /// 显示加载中弹窗(如果已经显示过了,就不重复显示) Future<void> showLoadingDialog() async { if (showingLoading) { return; } /// 清除焦点,隐藏键盘 FocusManager.instance.primaryFocus?.unfocus(); showingLoading = true; await showDialog<int>( context: context, barrierDismissible: true, // 允许点击背景关闭弹窗 builder: (context) { return const AlertDialog( content: Column( mainAxisSize: MainAxisSize.min, // 内容高度根据子内容压缩 children: [ CircularProgressIndicator(), // 加载进度条 Padding( padding: EdgeInsets.only(top: 24), child: Text("请稍等...."), // 加载提示文字 ) ], ), ); }); showingLoading = false; // 弹窗关闭后,恢复状态 } dismissLoading() { if (showingLoading) { /// 清除焦点,隐藏键盘 FocusManager.instance.primaryFocus?.unfocus(); showingLoading = false; Navigator.of(context).pop(); // 关闭当前弹窗 } } } /// 用于显示加载失败,并提供"点击重试"的组件 class RetryWidget extends StatelessWidget { const RetryWidget({super.key, required this.onTapRetry}); /// 点击"重试"的回调函数(由外部传入) final void Function() onTapRetry; @override Widget build(BuildContext context) { return GestureDetector( behavior: HitTestBehavior.opaque, // 允许点击透明区域 onTap: onTapRetry, // 用户点击整个区域触发重试 child: const SizedBox( width: double.infinity, height: double.infinity, child: Column( mainAxisAlignment: MainAxisAlignment.center, // 居中显示 crossAxisAlignment: CrossAxisAlignment.center, children: [ Padding( padding: EdgeInsets.only(bottom: 16), child: Icon(Icons.refresh)), // 刷新图标 Text("加载失败,点击重试") // 文本提示 ], ), )); } } /// 用于显示"无数据"的提示组件 class EmptyWidget extends StatelessWidget { const EmptyWidget({super.key}); @override Widget build(BuildContext context) { return const SizedBox( width: double.infinity, height: double.infinity, child: Column( mainAxisAlignment: MainAxisAlignment.center, // 内容垂直居中 crossAxisAlignment: CrossAxisAlignment.center, children: [ Padding( padding: EdgeInsets.only(bottom: 16), child: Icon(Icons.book)), Text("无数据") ], ), ); } }
四、services 包
-
api_service.dart
dartclass ApiService { /// 登录 static Future<Map<String, dynamic>> login({ required String username, required String password, }) async { final url = Uri.parse(ApiConstants.baseUrl + ApiConstants.login); final response = await http.post( url, headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: { 'username': username, 'password': password, }, ); return _handleResponse(response); } /// 注册 static Future<Map<String, dynamic>> register({ required String username, required String password, required String repassword, }) async { final url = Uri.parse(ApiConstants.baseUrl + ApiConstants.register); final response = await http.post( url, headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: { 'username': username, 'password': password, 'repassword': repassword, }, ); return _handleResponse(response); } /// 通用处理响应 static Map<String, dynamic> _handleResponse(http.Response response) { if (response.statusCode == 200) { print("请求结果为: ${response.body}"); return jsonDecode(response.body); } else { throw Exception('请求失败:${response.statusCode}'); } } }
五、utils 包
toast_utils.dart
工具类:用于显示原生风格的 Toast 弹窗
dart
class ToastUtils {
// 当前显示的 OverlayEntry (移除时引用)
static OverlayEntry? _overlayEntry;
// 标记是否正在显示 Toast ,避免重复弹出
static bool _isShowing = false;
/// 显示 Toast 弹窗
/// [context]: 上下文弹窗
/// [message]: 要显示的提示文字
/// [duration]: 持续显示的时间(默认为 2 秒)
static void showToast(BuildContext context, String message,
{Duration duration = const Duration(seconds: 2)}) {
// 如果已经有 Toast 正在显示, 直接返回
if (_isShowing) return;
_isShowing = true;
// 创建 OverlayEntry(悬浮层)
_overlayEntry = OverlayEntry(
builder: (context) => Positioned(
bottom: MediaQuery.of(context).size.height * 0.5, // 离底部距离
left: MediaQuery.of(context).size.width * 0.2, // 离左边距离
width: MediaQuery.of(context).size.width * 0.6, // 离右边距离
child: _ToastWidget(message: message), // 自定义 Toast 样式
),
);
// 插入到 Overlay 中显示
Overlay.of(context).insert(_overlayEntry!);
// 延时关闭 Toast
Future.delayed(duration, () {
_overlayEntry?.remove();
_overlayEntry = null;
_isShowing = false;
});
}
}
/// 私有 Toast 组件,用于实现带动画的样式
class _ToastWidget extends StatefulWidget {
final String message;
const _ToastWidget({Key? key, required this.message}) : super(key: key);
@override
State<_ToastWidget> createState() => _ToastWidgetState();
}
class _ToastWidgetState extends State<_ToastWidget>
with SingleTickerProviderStateMixin {
// 控制透明度动画的控制器
late AnimationController _controller;
// 透明度动画
late Animation<double> _opacityAnimation;
@override
void initState() {
super.initState();
// 初始化动画控制器
_controller = AnimationController(
duration: const Duration(milliseconds: 300), // 动画时长 300ms
vsync: this,
);
// 使用 CurvedAnimation 包裹,使用 easeInOut 曲线
_opacityAnimation = CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
);
// 播放动画
_controller.forward();
}
@override
Widget build(BuildContext context) {
return FadeTransition(
opacity: _opacityAnimation, // 使用透明度动画包裹整个 Toast
child: Material(
color: Colors.transparent, // 背景透明
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), // 内边距
margin: const EdgeInsets.symmetric(horizontal: 16), // 外边距
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.8), // 半透明黑背景
borderRadius: BorderRadius.circular(20), // 圆角
),
child: Text(
widget.message,
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.white, fontSize: 14),
),
),
),
);
}
@override
void dispose() {
// 释放动画资源
_controller.dispose();
super.dispose();
}
}
六、主入口
main.dart
dart
void main() {
// 保证 Flutter 与平台(Android/iOS)进行绑定初始化,确保调用平台通道、使用插件前初始化完毕
WidgetsFlutterBinding.ensureInitialized();
// 隐藏系统状态栏和底部导航栏(全屏模式)
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
// 启动应用,并注册全局的 Provider 状态管理
runApp(
MultiProvider(
providers: [
// 注册 LoginResponseProvider 到全局,可以在整个应用中访问该登录状态
ChangeNotifierProvider(create: (_) => LoginResponseProvider()),
],
child: MyApp(), // 根组件
),
);
}
/// 应用根组件
class MyApp extends StatelessWidget {
const MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '玩安卓 Flutter 版', // 应用标题
debugShowCheckedModeBanner: false, // 关闭右上角 Debug 标签
theme: ThemeData(
primarySwatch: Colors.blue, // 设置主题颜色为蓝色
),
initialRoute: RoutesConstants.login, // 应用启动时的初始路由(跳转登录页)
routes: AppRoutes.routes, // 注册路由表,定义页面跳转路径
);
}
}
七、登录界面相关
-
models/login_register_response
登录注册相关数据类,由于 API 接口登录注册数据相同,所以就共用一个dartclass LoginRegisterResponse { final int errorCode; final String errorMsg; final UserInfo? data; LoginRegisterResponse({ required this.errorCode, required this.errorMsg, required this.data, }); /// 将 JSON 转换为对象 factory LoginRegisterResponse.fromJson(Map<String, dynamic> json) { return LoginRegisterResponse( errorCode: json['errorCode'], errorMsg: json['errorMsg'] ?? '', data: json['data'] != null ? UserInfo.fromJson(json['data']) : null, ); } /// 将对象转换为 JSON Map<String, dynamic> toJson() { return { 'errorCode': errorCode, 'errorMsg': errorMsg, 'data': data?.toJson(), // 注意 data 可能为空 }; } } class UserInfo { final bool admin; final List<dynamic> chapterTops; final int coinCount; final List<int> collectIds; final String email; final String icon; final int id; final String nickname; final String password; final String publicName; final String token; final int type; final String username; UserInfo({ required this.admin, required this.chapterTops, required this.coinCount, required this.collectIds, required this.email, required this.icon, required this.id, required this.nickname, required this.password, required this.publicName, required this.token, required this.type, required this.username, }); /// 将 JSON 转换为对象 factory UserInfo.fromJson(Map<String, dynamic> json) { return UserInfo( admin: json['admin'], chapterTops: json['chapterTops'] ?? [], coinCount: json['coinCount'], collectIds: List<int>.from(json['collectIds']), email: json['email'] ?? '', icon: json['icon'] ?? '', id: json['id'], nickname: json['nickname'] ?? '', password: json['password'] ?? '', publicName: json['publicName'] ?? '', token: json['token'] ?? '', type: json['type'], username: json['username'] ?? '', ); } /// 将对象转换为 JSON Map<String, dynamic> toJson() { return { 'admin': admin, 'chapterTops': chapterTops, 'coinCount': coinCount, 'collectIds': collectIds, 'email': email, 'icon': icon, 'id': id, 'nickname': nickname, 'password': password, 'publicName': publicName, 'token': token, 'type': type, 'username': username, }; } }
-
services/api_service.dart
dart/// 登录 static Future<Map<String, dynamic>> login({ required String username, required String password, }) async { final url = Uri.parse(ApiConstants.baseUrl + ApiConstants.login); final response = await http.post( url, headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: { 'username': username, 'password': password, }, ); return _handleResponse(response); } /// 注册 static Future<Map<String, dynamic>> register({ required String username, required String password, required String repassword, }) async { final url = Uri.parse(ApiConstants.baseUrl + ApiConstants.register); final response = await http.post( url, headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: { 'username': username, 'password': password, 'repassword': repassword, }, ); return _handleResponse(response); }
-
providers/login_response_provider.dart
dartclass LoginResponseProvider extends ChangeNotifier { LoginRegisterResponse? _user; LoginRegisterResponse? get user => _user; bool get isLoggedIn => _user != null; void login(LoginRegisterResponse user) { _user = user; notifyListeners(); } void logout() { _user = null; notifyListeners(); } }
LoginResponseProvider
承自ChangeNotifier
,这使得该类可以用于Provider
框架中进行状态监听和刷新界面。-
私有属性
_user
:用于存储登录成功后的用户数据。 -
使用
LoginRegisterResponse
类型,结构清晰,可访问完整的登录响应(如user.data?.username
、errorMsg
等)。
-
-
pages/login_register/login_register_view_model.dart
dartclass LoginViewModel extends ChangeNotifier { // 用于用户名输入框的控制器,管理文本内容 final usernameController = TextEditingController(); // 用于密码输入框的控制器,管理文本内容 final passwordController = TextEditingController(); // 用于确认密码输入框的控制器,管理文本内容(注册模式时使用) final repasswordController = TextEditingController(); // 当前是否处于登录模式,true表示登录,false表示注册 bool _isLogin = true; bool get isLogin => _isLogin; // 当前是否处于加载状态(显示loading) bool _loading = false; bool get isLoading => _loading; // 切换登录/注册模式,切换时通知监听者刷新UI void toggleMode() { _isLogin = !_isLogin; notifyListeners(); } // 调用登录接口,发送用户名和密码,等待响应并转换为模型对象 Future<LoginRegisterResponse> login() async { _setLoading(true); // 设置loading状态为true final response = await ApiService.login( username: usernameController.text.trim(), password: passwordController.text.trim(), ); _setLoading(false); // 加载完成,设置loading状态为false return LoginRegisterResponse.fromJson(response); } // 调用注册接口,发送用户名、密码和确认密码,等待响应并转换为模型对象 Future<LoginRegisterResponse> register() async { _setLoading(true); // 设置loading状态为true final response = await ApiService.register( username: usernameController.text.trim(), password: passwordController.text.trim(), repassword: repasswordController.text.trim(), ); _setLoading(false); // 加载完成,设置loading状态为false return LoginRegisterResponse.fromJson(response); } // 私有方法,更新加载状态并通知监听者刷新UI void _setLoading(bool value) { _loading = value; notifyListeners(); } // 释放文本控制器资源,防止内存泄漏,建议在页面销毁时调用 void disposeControllers() { usernameController.dispose(); passwordController.dispose(); repasswordController.dispose(); } }
-
pages/login_register/login_register_page.dart
dartclass LoginRegisterPage extends StatelessWidget { const LoginRegisterPage({super.key}); @override Widget build(BuildContext context) { // 使用 ChangeNotifierProvider 提供 LoginViewModel 给子组件使用 return ChangeNotifierProvider<LoginViewModel>( create: (_) => LoginViewModel(), child: const _LoginRegisterBody(), ); } } class _LoginRegisterBody extends StatefulWidget { const _LoginRegisterBody({super.key}); @override State<_LoginRegisterBody> createState() => _LoginRegisterBodyState(); } class _LoginRegisterBodyState extends State<_LoginRegisterBody> with BasePage<_LoginRegisterBody> { @override void dispose() { // 页面销毁时释放 LoginViewModel 中的控制器资源,防止内存泄漏 context.read<LoginViewModel>().disposeControllers(); super.dispose(); } @override Widget build(BuildContext context) { // 监听 LoginViewModel 的变化,刷新UI final vm = context.watch<LoginViewModel>(); final isLogin = vm.isLogin; // 判断当前是登录模式还是注册模式 return Scaffold( appBar: AppBar( title: const Text("登录/注册"), ), body: Padding( padding: const EdgeInsets.all(16), child: Column( children: [ // 用户名输入框,绑定到 ViewModel 的控制器 TextField( controller: vm.usernameController, decoration: const InputDecoration(hintText: "用户名"), ), const SizedBox(height: 12), // 密码输入框,绑定到 ViewModel 的控制器,且密码隐藏 TextField( obscureText: true, controller: vm.passwordController, decoration: const InputDecoration(hintText: "密码"), ), // 如果是注册模式,显示确认密码输入框 if (!isLogin) ...[ /// ... 是 Dart 语言中的 扩展操作符(spread operator)。 const SizedBox(height: 12), TextField( obscureText: true, controller: vm.repasswordController, decoration: const InputDecoration(hintText: "确认密码"), ), ], const SizedBox(height: 24), // 登录或注册按钮,点击触发提交事件 ElevatedButton( onPressed: () => _onSubmit(context), child: Text(isLogin ? "登录" : "注册"), ), // 登录或注册按钮,点击触发提交事件 TextButton( onPressed: () => vm.toggleMode(), child: Text(isLogin ? "没有账号?去注册" : "已有账号?去登录"), ), ], ), ), ); } /// 点击提交按钮时的处理逻辑 Future<void> _onSubmit(BuildContext context) async { FocusScope.of(context).unfocus(); // 关闭软键盘 final vm = context.read<LoginViewModel>(); final username = vm.usernameController.text.trim(); final password = vm.passwordController.text.trim(); final repassword = vm.repasswordController.text.trim(); // 简单校验用户名和密码是否为空 if (username.isEmpty || password.isEmpty) { ToastUtils.showToast(context, '请输入用户名和密码'); return; } showLoadingDialog(); // 显示加载弹窗 // 根据当前模式调用登录或注册接口 final response = vm.isLogin ? await vm.login() : await (password == repassword ? vm.register() : Future.error("两次密码不一致")); dismissLoading(); // 关闭加载弹窗 if (response.errorCode == 0) { ToastUtils.showToast( context, vm.isLogin ? "登录成功 ${response.data?.username}" : "注册成功,请登录"); if (vm.isLogin) { // 登录成功 /// 保存用户数据 context.read<LoginResponseProvider>().login(response); /// 跳转至首页 Navigator.pushNamed(context, RoutesConstants.home); } } else { ToastUtils.showToast(context, "失败: ${response.errorMsg}"); } } }
...
是 Dart 语言中的 扩展操作符(spread operator)。它的作用是把一个集合(比如 List)中的所有元素"展开"放到另一个集合中。
在我们的代码中
-
if (!isLogin)
判断是否是注册模式(不是登录)。 -
...[someList]
就是把这个列表里的所有 widget 展开,作为父级Column
的直接子元素。
如果没有
...
,你只能写一个单独的 widget,不能写一个列表。 -
至此,我们登录注册界面和功能都已完成,如下所示
八、首页相关
首先我们要在 pages
包下创建 home、navi、project、top、tree、personal
包,分别对应的主页面、导航、项目、首页、体系、个人中心模块,考虑到篇幅问题,这里先各自创建一个 helloworld
界面,之后在逐步实现,我们先看主页面设计
-
app/constants.dart
在配置信息中
RoutesConstants
添加home_page
相关dartclass RoutesConstants { /// 登录注册界面 static const String login = "/login_register"; /// 首页 static const String home = "/home"; }
-
app/routes.dart
添加路由表及跳转管理
dartclass AppRoutes{ static final routes = <String, WidgetBuilder>{ RoutesConstants.login: (_) => LoginRegisterPage(), RoutesConstants.home: (_) => HomePage(), }; }
-
widgets/custom_appbar.dart
dart// 自定义 AppBar 组件,实现带标题和可选搜索图标的 AppBar class CustomAppBar extends StatefulWidget implements PreferredSizeWidget { final String title; // 标题文字 final bool showSearchIcon; // 是否显示右侧的搜索图标 const CustomAppBar({ Key? key, required this.title, this.showSearchIcon = true, // 默认显示搜索图标 }) : super(key: key); @override State<CustomAppBar> createState() => _CustomAppBarState(); // 指定 AppBar 的高度 @override Size get preferredSize => const Size.fromHeight(kToolbarHeight); } class _CustomAppBarState extends State<CustomAppBar> { @override Widget build(BuildContext context) { return AppBar( // 设置 AppBar 标题 title: Text(widget.title), // 关闭默认的返回按钮(返回箭头),适用于首页等不需要返回的场景 automaticallyImplyLeading: false, // 设置 AppBar 背景颜色 backgroundColor: Colors.blue, // 自定义标题文字样式 titleTextStyle: TextStyle(color: Colors.white, fontSize: 20), // 如果需要显示搜索图标,则构建 IconButton;否则不显示 actions actions: widget.showSearchIcon ? [ IconButton( icon: const Icon( Icons.search, color: Colors.white, ), onPressed: () { // 这里是搜索按钮点击后的回调,可根据需要添加跳转或弹窗逻辑 print("搜索图标点击"); }, ) ] : null, ); } }
-
page/home/home_page.dart
dartclass HomePage extends StatefulWidget { @override _HomePageState createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { // 当前底部导航选中的索引 int _currentIndex = 0; // 用于缓存页面实例,避免每次切换都重新创建 List<Widget?> _pages = List<Widget?>.filled(5, null, growable: false); static const List<String> _labels = [ "首页", "体系", "导航", "项目", "我的", ]; // 根据索引构建对应页面 Widget _buildPage(int index) { switch (index) { case 0: return const TopPage(); // 首页页面 case 1: return const TreePage(); // 体系页面 case 2: return const NaviPage(); // 导航页面 case 3: return const ProjectPage(); // 项目页面 case 4: return const PersonalPage(); // 个人中心页面 default: return const SizedBox(); // 默认空白页面 } } @override void initState() { super.initState(); // 读取登录状态Provider,获取当前用户信息 final loginProvider = context.read<LoginResponseProvider>(); final user = loginProvider.user; if (user != null) { // 打印当前用户用户名,方便调试 print('首页获取用户信息: ${user.data?.username}'); } } @override Widget build(BuildContext context) { final showSearchIcon = _currentIndex != 4; // 除了"我的"页,其他页显示搜索图标 return Scaffold( appBar: CustomAppBar( title: _labels[_currentIndex], showSearchIcon: showSearchIcon, ), // 使用 IndexedStack 保持所有页面状态,同时显示当前选中的页面 body: IndexedStack( index: _currentIndex, children: List.generate(_pages.length, (index) { // 如果缓存中该页面为空,则创建并缓存 if (_pages[index] == null) { _pages[index] = _buildPage(index); } // 返回缓存的页面实例 return _pages[index]!; }), ), // 底部导航栏,切换时更新当前索引并刷新界面 bottomNavigationBar: BottomNavigationBar( currentIndex: _currentIndex, // 当前选中索引 onTap: (index) { setState(() { _currentIndex = index; // 更新选中索引,触发重建 }); }, selectedItemColor: Colors.blue, // 选中项颜色 unselectedItemColor: Colors.grey, // 未选中项颜色 items: const [ BottomNavigationBarItem(icon: Icon(Icons.home), label: "首页"), BottomNavigationBarItem(icon: Icon(Icons.account_tree), label: "体系"), BottomNavigationBarItem(icon: Icon(Icons.navigation), label: "导航"), BottomNavigationBarItem(icon: Icon(Icons.article), label: "项目"), BottomNavigationBarItem(icon: Icon(Icons.person), label: "我的"), ], ), backgroundColor: Colors.white60, // 整体背景色 ); } }
至此,我们首页已经搭建完成,我们自定义了一个标题栏组件,当在 '我的' 界面时,搜索按钮不进行显示,页面效果如下所示