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

学习 Flutter(五):玩安卓项目实战 - 下

学习 Flutter(五):玩安卓项目实战 - 下

在上一章节,我们完成主界面的首页、体系、导航、项目、界面的编码,这一章节我们将剩下的内容包括我的、我的收藏列表、退出登录、文章详情、搜索界面给完成。

一、我的

我的界面算是叫个人中心界面吧,我这里就显示用户昵称和收藏列表以及退出登录,用户头像的话接口没有,所以自己用本地图片来显示

services/api_service.dart

/// 退出登录static Future<Map<String, dynamic>> logout() async {final url = Uri.parse("${ApiConstants.baseUrl}${ApiConstants.logout}");print('退出登录 url 为: $url');final headers = <String, String>{};if (_cookie != null) {headers['Cookie'] = _cookie!;}final response = await http.get(url, headers: headers);_cookie = null;return _handleResponse(response);}

personal/personal_page_view_model.dart

class PersonalPageViewModel extends ChangeNotifier {// 调用登录接口,发送用户名和密码,等待响应并转换为模型对象Future<bool> logout() async {final response = await ApiService.logout();final result = EmptyResponse.fromJson(response);return result.success;}
}

personal/personal_page.dart

class PersonalPage extends StatelessWidget {const PersonalPage();Widget build(BuildContext context) {return ChangeNotifierProvider<PersonalPageViewModel>(create: (_) => PersonalPageViewModel(),child: _PersonalPageBody(),);}
}class _PersonalPageBody extends StatefulWidget {const _PersonalPageBody();_PersonalPageState createState() => _PersonalPageState();
}class _PersonalPageState extends State<_PersonalPageBody> {void initState() {super.initState();}Widget build(BuildContext context) {return Scaffold(body: Column(children: [const SizedBox(height: 30),// 头像 + 用户名Center(child: Column(children: [CircleAvatar(radius: 50,backgroundImage: AssetImage('assets/images/npc_face.png'),),SizedBox(height: 12),Text("用户名: ${context.read<LoginResponseProvider>().user?.data?.nickname}",style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),)],),),const SizedBox(height: 40),// 列表项Expanded(child: ListView(children: [ListTile(leading: const Icon(Icons.favorite_border),title: const Text('我的收藏'),trailing: const Icon(Icons.chevron_right),onTap: () {// 跳转到收藏页面Navigator.pushNamed(context, RoutesConstants.collectList);},),ListTile(leading: const Icon(Icons.info_outline),title: const Text('关于我'),trailing: const Icon(Icons.chevron_right),onTap: () {ToastUtils.showToast(context, "记录学习的一个打工牛马!");},),],),),// 退出登录按钮Padding(padding: const EdgeInsets.all(20.0),child: ElevatedButton.icon(icon: const Icon(Icons.logout),label: const Text('退出登录'),style: ElevatedButton.styleFrom(backgroundColor: Colors.red,foregroundColor: Colors.white,minimumSize: const Size.fromHeight(50),),onPressed: () {// 执行退出逻辑_onLogoutClick();},),),],),);}_onLogoutClick() async {bool isSuccess = await (context.read<PersonalPageViewModel>().logout());if (isSuccess) {ToastUtils.showToast(context, "退出登录成功!");Navigator.pushNamed(context, RoutesConstants.login);}}
}

作者实现的效果很简单,退出登录我们就将保存在内部的cookie给重置,并且退出到登录界面,这里作者没有实现持久化登录,读者们可以自己去实现一下,原理也很简单,将登录成功返回的cookie保存在本地,在下次登录的时候查看是否有cookie,有的话就直接登录,没有的话就要求用户进行登录操作。至此我们我的界面就设计完成了,如下所示

在这里插入图片描述

二、我的收藏界面

接下来我们来实现我的收藏界面

services/api_service.dart

/// 我的收藏页面static Future<Map<String, dynamic>> collectList(int page) async {final url = Uri.parse("${ApiConstants.baseUrl}${ApiConstants.collectList}$page/json");print('我的收藏页面 url 为: $url');final headers = <String, String>{};if (_cookie != null) {headers['Cookie'] = _cookie!;}final response = await http.get(url, headers: headers);return _handleResponse(response);}

models/collect_list_response.dart

class CollectResponse {final Collect? data;final int errorCode;final String errorMsg;CollectResponse({required this.data,required this.errorCode,required this.errorMsg,});factory CollectResponse.fromJson(Map<String, dynamic> json) {return CollectResponse(data: json['data'] != null ? Collect.fromJson(json['data']) : null,errorCode: json['errorCode'],errorMsg: json['errorMsg'],);}Map<String, dynamic> toJson() {return {'data': data?.toJson(),'errorCode': errorCode,'errorMsg': errorMsg,};}
}
class Collect {final int curPage;final List<CollectDetail> datas;final int offset;final bool over;final int pageCount;final int size;final int total;Collect({required this.curPage,required this.datas,required this.offset,required this.over,required this.pageCount,required this.size,required this.total,});factory Collect.fromJson(Map<String, dynamic> json) {return Collect(curPage: json['curPage'],datas: (json['datas'] as List).map((e) => CollectDetail.fromJson(e)).toList(),offset: json['offset'],over: json['over'],pageCount: json['pageCount'],size: json['size'],total: json['total'],);}Map<String, dynamic> toJson() {return {'curPage': curPage,'datas': datas.map((e) => e.toJson()).toList(),'offset': offset,'over': over,'pageCount': pageCount,'size': size,'total': total,};}
}class CollectDetail {final String author;final int chapterId;final String chapterName;final int courseId;final String desc;final String envelopePic;final int id;final String link;final String niceDate;final String origin;final int originId;final int publishTime;final String title;final int userId;final int visible;final int zan;CollectDetail({required this.author,required this.chapterId,required this.chapterName,required this.courseId,required this.desc,required this.envelopePic,required this.id,required this.link,required this.niceDate,required this.origin,required this.originId,required this.publishTime,required this.title,required this.userId,required this.visible,required this.zan,});factory CollectDetail.fromJson(Map<String, dynamic> json) {return CollectDetail(author: json['author'] ?? '',chapterId: json['chapterId'],chapterName: json['chapterName'] ?? '',courseId: json['courseId'],desc: json['desc'] ?? '',envelopePic: json['envelopePic'] ?? '',id: json['id'],link: json['link'] ?? '',niceDate: json['niceDate'] ?? '',origin: json['origin'] ?? '',originId: json['originId'],publishTime: json['publishTime'],title: json['title'] ?? '',userId: json['userId'],visible: json['visible'],zan: json['zan'],);}Map<String, dynamic> toJson() {return {'author': author,'chapterId': chapterId,'chapterName': chapterName,'courseId': courseId,'desc': desc,'envelopePic': envelopePic,'id': id,'link': link,'niceDate': niceDate,'origin': origin,'originId': originId,'publishTime': publishTime,'title': title,'userId': userId,'visible': visible,'zan': zan,};}
}

pages/collect/collect_list_page_view_model.dart

class CollectListPageViewModel extends ChangeNotifier {// 文章列表数据(私有)List<CollectDetail> _collectDetailList = [];// 对外暴露文章列表List<CollectDetail> get collectDetailList => _collectDetailList;// 加载文章数据,page 为页码(0 表示首页刷新)Future<void> loadCollectList(int page) async {// 调用 API 获取文章列表数据final response = await ApiService.collectList(page);// 解析响应数据中的文章列表final newList = CollectResponse.fromJson(response).data!.datas;if (page == 0) {// 如果是第一页,重置整个文章列表_collectDetailList = newList;} else {// 否则追加到原有列表后_collectDetailList.addAll(newList);}// 通知 UI 更新notifyListeners();}
}

pages/collect/collect_list_page.dart

class CollectListPage extends StatelessWidget {const CollectListPage({super.key});Widget build(BuildContext context) {// 使用ChangeNotifierProvider提供视图模型return ChangeNotifierProvider(// 创建视图模型实例并立即加载第一页数据create: (_) => CollectListPageViewModel()..loadCollectList(0),// 页面主体内容child: const _CollectListPageBody(),);}
}// 收藏列表页面主体内容(私有组件)
class _CollectListPageBody extends StatefulWidget {const _CollectListPageBody();State<_CollectListPageBody> createState() => _CollectListPageState();
}// 收藏列表页面状态类,处理滚动和加载更多逻辑
class _CollectListPageState extends State<_CollectListPageBody> {late final ScrollController _scrollController;  // 滚动控制器int _currentPage = 0;  // 当前页码bool _isLoadingMore = false;  // 是否正在加载更多void initState() {super.initState();// 初始化滚动控制器并添加滚动监听_scrollController = ScrollController()..addListener(_onScroll);}void dispose() {// 销毁时清理滚动控制器_scrollController.dispose();super.dispose();}// 滚动事件处理函数,实现无限滚动void _onScroll() {final maxScroll = _scrollController.position.maxScrollExtent;  // 最大滚动距离final currentScroll = _scrollController.position.pixels;  // 当前滚动位置// 当滚动到底部附近(50px缓冲)且没有正在加载时,触发加载更多if (currentScroll >= maxScroll - 50 && !_isLoadingMore) {_loadMore();}}// 加载更多数据的异步函数Future<void> _loadMore() async {setState(() {_isLoadingMore = true;  // 设置加载状态_currentPage++;  // 页码增加});// 通过视图模型加载下一页数据await context.read<CollectListPageViewModel>().loadCollectList(_currentPage);setState(() {_isLoadingMore = false;  // 重置加载状态});}Widget build(BuildContext context) {return Scaffold(appBar: AppBar(title: const Text("我的收藏")),  // 页面标题body: Consumer<CollectListPageViewModel>(builder: (context, vm, _) {final collectList = vm.collectDetailList;  // 从视图模型获取收藏列表// 使用CustomScrollView实现复杂滚动布局return CustomScrollView(controller: _scrollController,  // 绑定滚动控制器slivers: [// 如果列表为空且不在加载中,显示加载指示器if (collectList.isEmpty && !_isLoadingMore)SliverFillRemaining(child: const Center(child: CircularProgressIndicator()),)else ...[// 列表顶部添加8像素间距SliverToBoxAdapter(child: const SizedBox(height: 8)),// 收藏项列表主体SliverList(delegate: SliverChildBuilderDelegate((context, index) {// 使用CollectItemLayout组件渲染每个收藏项return CollectItemLayout(collectDetail: collectList[index],);},childCount: collectList.length,  // 列表项数量),),// 底部加载指示器(加载时显示)SliverToBoxAdapter(child: _isLoadingMore? const Padding(padding: EdgeInsets.all(16.0),child: Center(child: CircularProgressIndicator()),): const SizedBox.shrink(),  // 不加载时隐藏),]],);},),);}
}

记得要实现 routes 中我的收藏列表界面的配置,至此我们完成了我的收藏列表界面的设计,有些地方还是值得优化的,例如列表为空的时候,这里作者不进行详细的设计了,大致实现ui如下图所示

在这里插入图片描述

三、文章详情界面

接下来我们要实现文字详情界面,包括我们首页、体系、项目、导航、我的收藏列表、搜索,这些界面都需要进行查看文章详情,实现起来非常简单,我们使用 webview 来直接打开文章详情就行了

pubspec.yaml

webview_flutter: ^3.0.0

pages/detail/article_detail_page.dart

// 文章详情页面(直接使用WebView展示)
class ArticleDetailPage extends StatefulWidget {final String link; // 接收外部传入的文章链接final String title;const ArticleDetailPage({super.key, required this.link, required this.title});State<ArticleDetailPage> createState() => _ArticleDetailPageState();
}class _ArticleDetailPageState extends State<ArticleDetailPage> {void initState() {super.initState();if (Platform.isAndroid) WebView.platform = AndroidWebView();}Widget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text(widget.title)),body: WebView(initialUrl: widget.link,),);}
}

app/routes.dart

/// 路由表及跳转管理
class AppRoutes {static final routes = <String, WidgetBuilder>{RoutesConstants.login: (_) => LoginRegisterPage(),RoutesConstants.home: (_) => HomePage(),RoutesConstants.treeChild: (context) {// 从ModalRoute获取参数final args =ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;return TreePageChild(chapterId: args['chapterId'],childId: args['childId'],);},RoutesConstants.collectList: (_) => CollectListPage(),RoutesConstants.linkDetail: (context) {final args =ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;return ArticleDetailPage(link: args['link'], title: args['title'],);}};
}

接着我们给我们对应列表添加点击事件,我这里就只添加一次就行了,其余列表添加点击事件希望读者自己能处理哈,点击事件如下所示

Navigator.pushNamed(context, RoutesConstants.linkDetail,arguments: {'link': collectList[index].link,'title': collectList[index].title,});

至此,文章详情界面就完成了

四、搜索界面

接下来我们要实现最后一个功能,搜索界面

services/api_service.dart

static Future<Map<String, dynamic>> search({required int page,required int key,}) async {final url = Uri.parse("${ApiConstants.baseUrl}${ApiConstants.searchForKeyword}$page/json");final response = await http.post(url,headers: {'Content-Type': 'application/x-www-form-urlencoded'},body: {'k': key,},);return _handleResponse(response);}

pages/search/search_page_view_mode.dart

class SearchPageViewModel extends ChangeNotifier {final userSearchController = TextEditingController();// 文章列表数据(私有)List<Article> _articleList = [];// 对外暴露文章列表List<Article> get articleList => _articleList;Future<void> search(int page) async {// 调用 API 获取文章列表数据final response = await ApiService.search(page: page,key: userSearchController.text.trim(),);// 解析响应数据中的文章列表final newList = ArticleListResponse.fromJson(response).data.datas;if (page == 0) {// 如果是第一页,重置整个文章列表_articleList = newList;} else {// 否则追加到原有列表后_articleList.addAll(newList);}// 通知 UI 更新notifyListeners();}// 收藏文章(传入文章 ID),返回是否成功Future<bool> collect(int id) async {final response = await ApiService.collect(id);final result = EmptyResponse.fromJson(response);return result.success;}// 取消收藏文章(传入文章 ID),返回是否成功Future<bool> uncollect(int id) async {final response = await ApiService.uncollect(id);final result = EmptyResponse.fromJson(response);return result.success;}
}

pages/search/search_page.dart


// 搜索页面外壳组件(Stateless),用于提供 ViewModel 注入
class SearchPage extends StatelessWidget {const SearchPage({super.key});Widget build(BuildContext context) {// 使用 Provider 注入 SearchPageViewModelreturn ChangeNotifierProvider<SearchPageViewModel>(create: (_) => SearchPageViewModel(),child: _SearchPageBody(), // 主页面主体);}
}// 搜索页面的具体 UI 和交互逻辑
class _SearchPageBody extends StatefulWidget {_SearchPageState createState() => _SearchPageState();
}class _SearchPageState extends State<_SearchPageBody>with BasePage<_SearchPageBody> {final ScrollController _scrollController = ScrollController(); // 控制列表滚动int _page = 0;               // 当前分页页码bool _isLoadingMore = false; // 是否正在加载更多数据,避免重复加载void initState() {// 注册滑动监听器,用于实现滑动到底部自动加载更多_scrollController.addListener(_onScroll);}void dispose() {// 页面销毁时释放滚动控制器资源_scrollController.dispose();super.dispose();}Widget build(BuildContext context) {// 获取 ViewModel 实例,监听数据变化以触发重建final vm = context.watch<SearchPageViewModel>();return Scaffold(appBar: AppBar(title: const Text('搜索界面')),body: Column(children: [// 搜索栏区域Padding(padding: const EdgeInsets.all(12),child: Row(children: [// 输入框Expanded(child: TextField(controller: vm.userSearchController, // 输入控制器decoration: const InputDecoration(hintText: '请输入搜索内容',border: OutlineInputBorder(),),),),const SizedBox(width: 10),// 搜索按钮ElevatedButton(onPressed: () {vm.search(_page); // 点击搜索,调用 ViewModel 的搜索方法},child: const Text('搜索'),),],),),// 搜索结果列表Expanded(child: ListView.separated(controller: _scrollController, // 绑定滚动控制器itemCount: vm.articleList.length, // 数据项数量separatorBuilder: (_, __) => const Divider(height: 1), // 分隔线itemBuilder: (_, i) {return ArticleItemLayout(article: vm.articleList[i], // 单篇文章数据onCollectTap: () {_onCollectClick(vm.articleList[i]); // 收藏按钮点击事件},onTap: () {// 点击跳转详情页Navigator.pushNamed(context, RoutesConstants.linkDetail,arguments: {'link': vm.articleList[i].link,'title': vm.articleList[i].title,});},showCollectBtn: true, // 是否显示收藏按钮);},),)],),);}// 处理文章的收藏/取消收藏点击事件_onCollectClick(Article article) async {bool collected = article.collect;// 根据当前收藏状态调用不同接口bool isSuccess = await (!collected? context.read<SearchPageViewModel>().collect(article.id) // 收藏: context.read<SearchPageViewModel>().uncollect(article.id)); // 取消收藏if (isSuccess) {// 接口调用成功,更新状态并提示用户ToastUtils.showToast(context, collected ? "取消收藏!" : "收藏成功!");article.collect = !article.collect; // 本地切换收藏状态} else {// 接口调用失败,弹出提示ToastUtils.showToast(context, collected ? "取消收藏失败 -- " : "收藏失败 -- ");}}// 滚动监听器,用于检测是否接近底部void _onScroll() {final maxScroll = _scrollController.position.maxScrollExtent; // 最大滚动位置final currentScroll = _scrollController.position.pixels;      // 当前滚动位置// 若已接近底部,且未处于加载中状态,则加载更多数据if (currentScroll >= maxScroll - 50 && !_isLoadingMore) {_loadMore();}}/// 加载更多文章数据(分页)Future<void> _loadMore() async {_isLoadingMore = true; // 标记为加载中_page++; // 页码 +1await context.read<SearchPageViewModel>().search(_page); // 调用搜索_isLoadingMore = false; // 重置加载状态}
}

至此我们完成了搜索界面的设计,读者们记得在route表中添加搜索界面,以及在首页搜索按钮中添加点击跳转搜索界面。

在这里插入图片描述

至此Flutter版的玩安卓项目就完成了,还有很多细节没处理好,希望读者能够自行去优化哈~~,最后再次感谢大佬提供的API 玩Android 开放API-玩Android - wanandroid.com

项目地址:zengjinghong/wananadroid_flutter

http://www.lryc.cn/news/597322.html

相关文章:

  • 2025年7月一区SCI-投影迭代优化算法Projection Iterative Methods-附Matlab免费代码
  • Flutter学习笔记(四)---基础Widget
  • 如何解决pip安装报错ModuleNotFoundError: No module named ‘jupyter’问题
  • OSPF路由协议——上
  • 2025.7.15vlan作业
  • vscode怎么安装MINGW
  • Linux下SVN常用指令
  • VRRP虚拟路由器冗余协议
  • 民营医院如何突破技术与模式创新,迎来发展机遇?
  • 14.10 《24小时单卡训练!LoRA微调LLaMA2-7B全攻略,RTX 3090轻松跑》
  • Async/Await
  • translateZ数值大小变化
  • Python 程序设计讲义(7):Python 的基本数据类型——整数类型
  • SpringMVC快速入门之请求与响应
  • JavaScript事件循环机制
  • 免费下载入户申请书,轻松办理登记手续——“文件扫描助手”网站介绍
  • 使用 piano_transcription_inference将钢琴录音转换为 MIDI
  • 开闭原则在C++中的实现
  • 基于Tornado的WebSocket实时聊天系统:从零到一构建与解析
  • 【js(5)原型与原型链】
  • 自由学习记录(72)
  • JavaEE Spring框架的概述与对比无框架下的优势
  • 大模型开发
  • 【Ansible】Ansible 管理 Elasticsearch 集群启停
  • NAPI node-addon-api 编译报错 error C1083: “napi.h”: No such file or directory
  • 【esp32s3】GPIO 寄存器 开发解析
  • MACOS安装配置Gradle
  • 垃圾回收介绍
  • static 关键字的 特殊性
  • 双流join 、 Paimon Partial Update 和 动态schema