Flutter实现基于REST API的动态TabBar与TabBarView方案咨询
解决方案
1. 定义数据模型
先为API返回的主题和列表数据创建模型类,方便JSON解析:
// 主题模型 class Topic { final int id; final String name; Topic({required this.id, required this.name}); factory Topic.fromJson(Map<String, dynamic> json) => Topic( id: json['id'], name: json['name'], ); } // 列表项模型 class ListItem { final String title; final String content; ListItem({required this.title, required this.content}); factory ListItem.fromJson(Map<String, dynamic> json) => ListItem( title: json['title'], content: json['content'], ); }
2. 封装API请求
把API请求逻辑抽成工具类,方便复用和维护:
import 'dart:convert'; import 'package:http/http.dart' as http; class ApiClient { // 获取主题列表接口 static Future<List<Topic>> fetchTopics() async { final response = await http.get(Uri.parse('你的主题API地址')); if (response.statusCode == 200) { return (json.decode(response.body) as List) .map((item) => Topic.fromJson(item)) .toList(); } else { throw Exception('加载主题失败'); } } // 根据主题ID获取对应列表数据接口 static Future<List<ListItem>> fetchItemsByTopic(int topicId) async { final response = await http.get(Uri.parse('你的列表API地址?topicId=$topicId')); if (response.statusCode == 200) { return (json.decode(response.body) as List) .map((item) => ListItem.fromJson(item)) .toList(); } else { throw Exception('加载列表数据失败'); } } }
3. 实现主页面逻辑
核心思路:先加载主题数据,完成后渲染TabBar;Tab切换时懒加载对应数据,用状态保持避免重复请求。
import 'package:flutter/material.dart'; class DynamicTabPage extends StatefulWidget { const DynamicTabPage({super.key}); @override State<DynamicTabPage> createState() => _DynamicTabPageState(); } class _DynamicTabPageState extends State<DynamicTabPage> with SingleTickerProviderStateMixin { late TabController _tabController; List<Topic> _topics = []; bool _isLoadingTopics = true; String? _topicLoadError; // 存储每个Tab的加载状态、数据和错误信息 final Map<int, (bool isLoading, List<ListItem> items, String? error)> _tabStates = {}; @override void initState() { super.initState(); _loadTopics(); } // 预加载主题数据 void _loadTopics() async { try { _topics = await ApiClient.fetchTopics(); _tabController = TabController(length: _topics.length, vsync: this); // 监听Tab切换,触发对应Tab的数据加载 _tabController.addListener(() { if (!_tabController.indexIsChanging) { _loadTabData(_tabController.index); } }); // 初始化第一个Tab的数据 if (_topics.isNotEmpty) _loadTabData(0); } catch (e) { setState(() => _topicLoadError = e.toString()); } finally { setState(() => _isLoadingTopics = false); } } // 懒加载指定Tab的数据 void _loadTabData(int tabIndex) async { // 已加载过则直接返回 if (_tabStates.containsKey(tabIndex) && !_tabStates[tabIndex]!.isLoading && _tabStates[tabIndex]!.items.isNotEmpty) { return; } setState(() => _tabStates[tabIndex] = (true, [], null)); try { final items = await ApiClient.fetchItemsByTopic(_topics[tabIndex].id); setState(() => _tabStates[tabIndex] = (false, items, null)); } catch (e) { setState(() => _tabStates[tabIndex] = (false, [], e.toString())); } } @override void dispose() { _tabController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('动态Tab页面'), bottom: _buildTabBar(), ), body: _buildTabBarView(), ); } // 构建TabBar(主题加载完成后才显示) PreferredSizeWidget? _buildTabBar() { if (_isLoadingTopics) { return const PreferredSize( preferredSize: Size.fromHeight(48), child: Center(child: CircularProgressIndicator()), ); } if (_topicLoadError != null) { return PreferredSize( preferredSize: const Size.fromHeight(48), child: Center(child: Text('主题加载失败:$_topicLoadError')), ); } return TabBar( controller: _tabController, isScrollable: true, tabs: _topics.map((topic) => Tab(text: topic.name)).toList(), ); } // 构建TabBarView Widget _buildTabBarView() { if (_isLoadingTopics) return const Center(child: CircularProgressIndicator()); if (_topicLoadError != null) return Center(child: Text('主题加载失败:$_topicLoadError')); return TabBarView( controller: _tabController, children: List.generate(_topics.length, (index) { final state = _tabStates[index] ?? (true, [], null); return _TabContent( state: state, onRetry: () => _loadTabData(index), ); }), ); } } // 单个Tab的内容页面,保持状态避免重复加载 class _TabContent extends StatefulWidget { final (bool isLoading, List<ListItem> items, String? error) state; final VoidCallback onRetry; const _TabContent({required this.state, required this.onRetry}); @override State<_TabContent> createState() => _TabContentState(); } class _TabContentState extends State<_TabContent> with AutomaticKeepAliveClientMixin { @override bool get wantKeepAlive => true; @override Widget build(BuildContext context) { super.build(context); // 必须调用以保持状态 final (isLoading, items, error) = widget.state; if (isLoading) return const Center(child: CircularProgressIndicator()); if (error != null) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text('加载失败:$error'), const SizedBox(height: 16), ElevatedButton(onPressed: widget.onRetry, child: const Text('重试')), ], ), ); } return Container( color: Colors.blueGrey[50], // 蓝灰色背景 child: ListView.builder( itemCount: items.length, itemBuilder: (context, index) { return ListTile( title: Text(items[index].title), subtitle: Text(items[index].content), ); }, ), ); } }
关键实现说明
- 主题预加载:在页面初始化时先请求主题数据,加载完成后才渲染TabBar,确保标签数量准确。
- 懒加载机制:监听Tab切换事件,仅在切换到未加载数据的Tab时才发起API请求,避免不必要的网络开销。
- 状态保持:使用
AutomaticKeepAliveClientMixin让Tab页面切换时保留状态,不会重新初始化和加载数据。 - 错误处理:分别处理主题加载失败和单个Tab数据加载失败的场景,提供重试选项优化用户体验。
内容的提问来源于stack exchange,提问作者ayush
相关产品推荐
相关产品推荐

