当前位置: 首页 > news >正文

学习 Flutter (三):玩安卓项目实战 - 上

学习 Flutter(三)

在上一章节,我们对 Flutter 中常用的组件和架构有了一定的了解,那么现在我们既有轮子(基础UI),又有框架了,是时候开始造车了,那么本章将开始进行Android项目实战练习,具体实战什么看作者想要实战什么(无规划,难易不定)…遇到啥就针对的去实战,篇幅会比较长,尽量保证原生,不使用第三方插件,跟着一篇文章可以实现一个项目的完整运行!!!

在此声明,感谢大佬提供的开发API,玩Android 开放API-玩Android - wanandroid.com

一、项目准备

  • 搭建项目结构

    lib/└─ app /// 应用配置层└─ base /// 基础抽象层└─ models /// 数据结构层└─ pages /// UI 层└─ providers /// 状态管理层└─ services /// 业务接口层└─ utils /// 工具类库层└─ widgets /// 通用组件层
    └─ main.dart /// 入口文件
    
  • 创建资源文件夹

    lib 同级目录下创建 assets/images 文件夹,并且在 pubspec.yaml 中进行添加

    flutter:uses-material-design: trueassets:- assets/images/
    
  • 添加 http 相关

    首先在 android 包中找到 AndroidManifest.xml 文件并声明网络请求权限,如果是要有 ioslinuxwindows (作者不是很懂),也都声明一下对应包下的权限,并且我们要在 pubspec.yaml 中进行添加 http 相关包

    dependencies:flutter:sdk: flutterhttp: ^0.13.5
    

    由于作者的 Flutter 版本只能支持到这个 http 版本,懒的去升级版本了,希望读者们能自行进行调整哈,主要是公司项目的版本就是这么低,作者也懒得去升级,还要更新一堆依赖,太麻烦了,又不是不能用!

  • 添加 provider 相关

    pubspec.yaml 中进行添加 provider 相关包

    dependencies:flutter:sdk: flutterprovider: ^6.1.1
    

    provider 是 Flutter 官方推荐的状态管理方案之一,是基于 InheritedWidget 封装的简单、轻量级依赖注入和状态管理工具。它的设计理念是提供一种优雅且高效的手段,让 Widget 树中的各个组件能够访问和响应状态的变化,避免手动传递数据(避免“Prop Drilling”),满足中小型及大型项目的状态管理需求。简单点可以理解为 Android 中的 LiveData, 当前不是完全相同的还是有些差别的。

至此我们基础项目架构已经搭建完成了,我们接下来将逐步完成项目的实现。

二、app包

  • constant.dart

    常量配置,如接口地址、主题色等

    class ApiConstants {static const String baseUrl = "https://www.wanandroid.com/";/// 首页文章static const String homePageArticle = "article/list/";/// 置顶文章static const String topArticle = "article/top/json";/// 获取bannerstatic 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/";/// 分享文章 poststatic const String shareArticle = "lg/user_article/add/json";/// todoListstatic const String todoList = "lg/todo/v2/list/";
    }
    class RoutesConstants {/// 登录注册界面static const String login = "/login_register";/// 首页static const String home = "/home";}
    
  • routes.dart
    路由表及跳转管理

    class AppRoutes{static final routes = <String, WidgetBuilder>{RoutesConstants.login: (_) => LoginRegisterPage(),RoutesConstants.home: (_) => HomePage(),};
    }
    

三、base 包

  • base.dart

    BasePage 是一个 Mixin,可用于所有继承 State 的类(例如 StatefulWidget 页面)

    mixin 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;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});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

    class 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 弹窗

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!);// 延时关闭 ToastFuture.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);State<_ToastWidget> createState() => _ToastWidgetState();
}class _ToastWidgetState extends State<_ToastWidget>with SingleTickerProviderStateMixin {// 控制透明度动画的控制器late AnimationController _controller;// 透明度动画late Animation<double> _opacityAnimation;void initState() {super.initState();// 初始化动画控制器_controller = AnimationController(duration: const Duration(milliseconds: 300), // 动画时长 300msvsync: this,);// 使用 CurvedAnimation 包裹,使用 easeInOut 曲线_opacityAnimation = CurvedAnimation(parent: _controller,curve: Curves.easeInOut,);// 播放动画_controller.forward();}Widget build(BuildContext context) {return FadeTransition(opacity: _opacityAnimation, // 使用透明度动画包裹整个 Toastchild: 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),),),),);}void dispose() {// 释放动画资源_controller.dispose();super.dispose();}
}

六、主入口

main.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();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 接口登录注册数据相同,所以就共用一个

    class 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,);}/// 将对象转换为 JSONMap<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'] ?? '',);}/// 将对象转换为 JSONMap<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

    /// 登录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

    class 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?.usernameerrorMsg 等)。

  • pages/login_register/login_register_view_model.dart

    class 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;// 切换登录/注册模式,切换时通知监听者刷新UIvoid toggleMode() {_isLogin = !_isLogin;notifyListeners();}// 调用登录接口,发送用户名和密码,等待响应并转换为模型对象Future<LoginRegisterResponse> login() async {_setLoading(true); // 设置loading状态为truefinal response = await ApiService.login(username: usernameController.text.trim(),password: passwordController.text.trim(),);_setLoading(false); // 加载完成,设置loading状态为falsereturn LoginRegisterResponse.fromJson(response);}// 调用注册接口,发送用户名、密码和确认密码,等待响应并转换为模型对象Future<LoginRegisterResponse> register() async {_setLoading(true); // 设置loading状态为truefinal response = await ApiService.register(username: usernameController.text.trim(),password: passwordController.text.trim(),repassword: repasswordController.text.trim(),);_setLoading(false); // 加载完成,设置loading状态为falsereturn LoginRegisterResponse.fromJson(response);}// 私有方法,更新加载状态并通知监听者刷新UIvoid _setLoading(bool value) {_loading = value;notifyListeners();}// 释放文本控制器资源,防止内存泄漏,建议在页面销毁时调用void disposeControllers() {usernameController.dispose();passwordController.dispose();repasswordController.dispose();}
    }
  • pages/login_register/login_register_page.dart

    class LoginRegisterPage extends StatelessWidget {const LoginRegisterPage({super.key});Widget build(BuildContext context) {// 使用 ChangeNotifierProvider 提供 LoginViewModel 给子组件使用return ChangeNotifierProvider<LoginViewModel>(create: (_) => LoginViewModel(),child: const _LoginRegisterBody(),);}
    }class _LoginRegisterBody extends StatefulWidget {const _LoginRegisterBody({super.key});State<_LoginRegisterBody> createState() => _LoginRegisterBodyState();
    }class _LoginRegisterBodyState extends State<_LoginRegisterBody>with BasePage<_LoginRegisterBody> {void dispose() {// 页面销毁时释放 LoginViewModel 中的控制器资源,防止内存泄漏context.read<LoginViewModel>().disposeControllers();super.dispose();}Widget build(BuildContext context) {// 监听 LoginViewModel 的变化,刷新UIfinal 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 相关

    class RoutesConstants {/// 登录注册界面static const String login = "/login_register";/// 首页static const String home = "/home";
    }
    
  • app/routes.dart

    添加路由表及跳转管理

    class AppRoutes{static final routes = <String, WidgetBuilder>{RoutesConstants.login: (_) => LoginRegisterPage(),RoutesConstants.home: (_) => HomePage(),};
    }
    
  • widgets/custom_appbar.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);State<CustomAppBar> createState() => _CustomAppBarState();// 指定 AppBar 的高度Size get preferredSize => const Size.fromHeight(kToolbarHeight);
    }class _CustomAppBarState extends State<CustomAppBar> {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;否则不显示 actionsactions: widget.showSearchIcon? [IconButton(icon: const Icon(Icons.search,color: Colors.white,),onPressed: () {// 这里是搜索按钮点击后的回调,可根据需要添加跳转或弹窗逻辑print("搜索图标点击");},)]: null,);}
    }
  • page/home/home_page.dart

    
    class HomePage extends StatefulWidget {_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(); // 默认空白页面}}void initState() {super.initState();// 读取登录状态Provider,获取当前用户信息final loginProvider = context.read<LoginResponseProvider>();final user = loginProvider.user;if (user != null) {// 打印当前用户用户名,方便调试print('首页获取用户信息: ${user.data?.username}');}}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, // 整体背景色);}
    }

至此,我们首页已经搭建完成,我们自定义了一个标题栏组件,当在 ’我的‘ 界面时,搜索按钮不进行显示,页面效果如下所示

在这里插入图片描述
在这里插入图片描述

http://www.dtcms.com/a/279337.html

相关文章:

  • 152.Vue3中使用OpenLayers+Turf实现遮罩布挖洞效果
  • MCP终极篇!MCP Web Chat项目实战分享
  • 消费 Kafka 一个TOPIC数据,插入到另一个KAFKA的TOPIC
  • c#如何将不同类型的数据存储到一起
  • 项目进度依赖纸面计划,如何提升计划动态调整能力
  • 基于FinRL深度强化学习框架的股票预测和回测交易
  • 迁移学习:知识复用的智能迁移引擎 | 从理论到实践的跨域赋能范式
  • 什么是神经网络,常用的神经网络,如何训练一个神经网络
  • python 循环遍历取出偶数
  • 「日拱一码」027 深度学习库——PyTorch Geometric(PyG)
  • MCP基础知识二(实战通信方式之Streamable HTTP)
  • 【CTF学习】PWN基础工具的使用(binwalk、foremost、Wireshark、WinHex)
  • ewdyfdfytty
  • LangChain教程——文本嵌入模型
  • 20250714让荣品RD-RK3588开发板在Android13下长按关机
  • Debezium日常分享系列之:提升Debezium性能
  • 制造业实战:数字化集采如何保障千种备件“不断供、不积压”?
  • 16.避免使用裸 except
  • MFC扩展库BCGControlBar Pro v36.2新版亮点:可视化设计器升级
  • 计算机毕业设计Java轩辕购物商城管理系统 基于 SpringBoot 的轩辕电商商城管理系统 Java 轩辕购物平台管理系统设计与实现
  • 面向对象的设计模式
  • 【数据结构】树(堆)·上
  • js的局部变量和全局变量
  • 测试驱动开发(TDD)实战:在 Spring 框架实现中践行 “红 - 绿 - 重构“ 循环
  • Bash vs PowerShell | 从 CMD 到跨平台工具:Bash 与 PowerShell 的全方位对比
  • vue3 服务端渲染时请求接口没有等到数据,但是客户端渲染是请求接口又可以得到数据
  • 7.14 map | 内存 | 二维dp | 二维前缀和
  • python+Request提取cookie
  • 电脑升级Experience
  • python transformers笔记(Trainer类)