学习 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
文件并声明网络请求权限,如果是要有ios
、linux
、windows
(作者不是很懂),也都声明一下对应包下的权限,并且我们要在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?.username
、errorMsg
等)。
-
-
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, // 整体背景色);} }
至此,我们首页已经搭建完成,我们自定义了一个标题栏组件,当在 ’我的‘ 界面时,搜索按钮不进行显示,页面效果如下所示