You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.01 14:05:30