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

如何适配Flutter TabBarView子组件的高度?

动态调整TabBarView高度适配异步加载组件的问题

我的TabBarView包含3个高度不同的子组件,其中第二个是需要异步获取数据的Query组件(加载时显示CircularPercentIndicator,加载完成后渲染HospitalsByTreatments)。尝试使用AutoScaleTabBarView后发现,它仅在切换标签时调整高度,导致Query组件数据加载完成后无法正常显示内容。相关代码如下:

TabBarView(
  controller: tabController,
  children: [
    MarkdownBody(
      data: currentTreatment.treatmentDescription,
      styleSheet: MarkdownStyleSheet(
        p: TextStyle(fontSize: 16 * w),
        h4: TextStyle(
            fontSize: 19 * w,
            fontWeight: FontWeight.w800,
            color: Colors.grey.shade700),
      ),
    ),
    Query(
        options: QueryOptions(
            document: gql(hospitalsGraphQL)),
        builder: (QueryResult result,
            {fetchMore, refetch}) {
          if (result.hasException) {
            return Text(
                result.exception.toString());
          }
          if (result.isLoading) {
            return Center(
              child: CircularPercentIndicator(
                curve: Curves.decelerate,
                lineWidth: 12 * h,
                progressColor:
                    Colors.green.shade400,
                backgroundColor:
                    Colors.green.shade200,
                radius: 50 * h,
                percent: 1,
                circularStrokeCap:
                    CircularStrokeCap.round,
                animation: true,
                center: Image.asset(
                  "lib/images/Medicte_cropped.png",
                  height: 60 * h,
                ),
                animationDuration: 1000,
              ),
            );
          }
          final hospitalList =
              result.data?['hospitals'];
          
          return HospitalsByTreatments(
            currentTreatment: currentTreatment,
            hospitalList: hospitalList,
          );
        }),
    SizedBox(
      height: heightContainer,
      child: Column(
        children: [
          Expanded(
            child: PageView.builder(
                controller: pageController,
                itemCount: listLengthCalculator(
                    currentTreatment
                        .medicte_hotels.length),
                itemBuilder: (BuildContext context,
                    int index) {
                  return Padding(
                    padding: EdgeInsets.symmetric(
                        horizontal: 6 * w),
                    child: Column(
                      children: [
                        ClipRRect(
                          borderRadius:
                              BorderRadius.circular(
                                  15),
                          child: Container(
                            padding:
                                EdgeInsets.only(
                                    right: 12 * w,
                                    left: 12 * w,
                                    top: 12 * h),
                            color: Colors.orange,
                            width: 400 * w,
                            height:
                                (heightContainer -
                                        35) /
                                    2,
                            child: Column(
                              children: [
                                CircleAvatar(
                                  radius: 65 * h,
                                  backgroundImage:
                                      NetworkImage(
                                    "https://admin.medicte.ca${currentTreatment.medicte_hotels[index * 2]["hotelPicture"]["formats"]["small"]["url"]}",
                                  ),
                                ),
                                SizedBox(
                                  height: 10 * h,
                                ),
                                Expanded(
                                  child: Column(
                                    crossAxisAlignment:
                                        CrossAxisAlignment
                                            .center,
                                    children: [
                                      Text(
                                          currentTreatment.medicte_hotels[index *
                                                  2]
                                              [
                                              "hotelTitle"],
                                          style: TextStyle(
                                              fontWeight: FontWeight
                                                  .bold,
                                              fontSize:
                                                  18 * w)),
                                      SizedBox(
                                          height:
                                              10 *
                                                  h),
                                      Text(
                                        currentTreatment
                                                .medicte_hotels[
                                            index *
                                                2]["hotelDescription"],
                                        style: TextStyle(
                                            fontSize:
                                                13 *
                                                    w),
                                      ),
                                    ],
                                  ),
                                ),
                              ],
                            ),
                          ),
                        ),
                        SizedBox(
                          height: 10 * h,
                        ),
                        if (index * 2 + 1 + 1 <
                            currentTreatment
                                .medicte_hotels
                                .length)
                          ClipRRect(
                            borderRadius:
                                BorderRadius
                                    .circular(15),
                            child: Container(
                              padding:
                                  EdgeInsets.all(
                                      12 * h),
                              color: Colors.orange,
                              width: 400 * w,
                              height:
                                  (heightContainer -
                                          35) /
                                      2,
                              child: Column(
                                children: [
                                  CircleAvatar(
                                    radius: 65 * h,
                                    backgroundImage:
                                        NetworkImage(
                                      "https://admin.medicte.ca${currentTreatment.medicte_hotels[index * 2 + 1]["hotelPicture"]["formats"]["small"]["url"]}",
                                    ),
                                  ),
                                  SizedBox(
                                    height: 10 * h,
                                  ),
                                  Expanded(
                                    child: Column(
                                      crossAxisAlignment:
                                          CrossAxisAlignment
                                              .center,
                                      children: [
                                        Text(
                                            currentTreatment.medicte_hotels[index * 2 +
                                                    1]
                                                [
                                                "hotelTitle"],
                                            style: TextStyle(
                                                fontWeight:
                                                    FontWeight.bold,
                                                fontSize: 18 * w)),
                                        SizedBox(
                                            height:
                                                10 *
                                                    h),
                                        Text(
                                          currentTreatment
                                              .medicte_hotels[index *
                                                  2 +
                                              1]["hotelDescription"],
                                          overflow:
                                              TextOverflow
                                                  .fade,
                                          style: TextStyle(
                                              fontSize:
                                                  13 * w),
                                        ),
                                      ],
                                    ),
                                  ),
                                ],
                              ),
                            ),
                          ),
                      ],
                    ),
                  );
                }),
          ),
          SmoothPageIndicator(
              controller: pageController,
              count: listLengthCalculator(
                  currentTreatment
                      .medicte_hotels.length),
              effect: ScrollingDotsEffect(
                activeStrokeWidth: 2.6,
                activeDotScale: 1.3,
                maxVisibleDots: 5,
                radius: 8 * h,
                spacing: 10 * w,
                dotHeight: 12 * w,
                dotWidth: 12 * w,
              )),
        ],
      ),
    ),
  ],
),

解决方案

1. 手动监听Tab切换+异步组件高度更新

给每个Tab子组件绑定GlobalKey,监听Tab切换事件计算当前页面高度,同时在Query组件数据加载完成后主动触发高度更新:

// 定义全局Key和高度变量
final GlobalKey _tab1Key = GlobalKey();
final GlobalKey _tab2Key = GlobalKey();
final GlobalKey _tab3Key = GlobalKey();
double _tabBarViewHeight = 0;

@override
void initState() {
  super.initState();
  // 监听Tab切换事件
  tabController.addListener(_updateTabBarViewHeight);
}

// 计算当前活跃Tab的高度
void _updateTabBarViewHeight() {
  WidgetsBinding.instance.addPostFrameCallback((_) {
    RenderBox? renderBox;
    switch(tabController.index) {
      case 0:
        renderBox = _tab1Key.currentContext?.findRenderObject() as RenderBox?;
        break;
      case 1:
        renderBox = _tab2Key.currentContext?.findRenderObject() as RenderBox?;
        break;
      case 2:
        renderBox = _tab3Key.currentContext?.findRenderObject() as RenderBox?;
        break;
    }
    if(renderBox != null) {
      setState(() {
        _tabBarViewHeight = renderBox.size.height;
      });
    }
  });
}

// 改造Query组件的builder
builder: (QueryResult result, {fetchMore, refetch}) {
  if (result.hasException) {
    return Text(result.exception.toString());
  }
  if (result.isLoading) {
    return Center(/* 原有加载指示器 */);
  }
  // 数据加载完成后,延迟触发高度更新
  WidgetsBinding.instance.addPostFrameCallback((_) {
    _updateTabBarViewHeight();
  });
  final hospitalList = result.data?['hospitals'];
  return HospitalsByTreatments(
    key: _tab2Key, // 绑定Key
    currentTreatment: currentTreatment,
    hospitalList: hospitalList,
  );
}

// 用SizedBox包裹TabBarView
SizedBox(
  height: _tabBarViewHeight,
  child: TabBarView(
    controller: tabController,
    children: [
      MarkdownBody(key: _tab1Key, /* 原有配置 */),
      Query(/* 原有配置 */),
      SizedBox(key: _tab3Key, /* 原有配置 */),
    ],
  ),
)

2. 封装自适应TabBarView组件

封装一个能自动监听子组件高度变化的TabBarView,适配异步组件的重绘:

class AdaptiveTabBarView extends StatefulWidget {
  final TabController controller;
  final List<Widget> children;

  const AdaptiveTabBarView({
    super.key,
    required this.controller,
    required this.children,
  });

  @override
  State<AdaptiveTabBarView> createState() => _AdaptiveTabBarViewState();
}

class _AdaptiveTabBarViewState extends State<AdaptiveTabBarView> {
  final List<GlobalKey> _keys = [];
  double _currentHeight = 0;

  @override
  void initState() {
    super.initState();
    _keys.addAll(List.generate(widget.children.length, (_) => GlobalKey()));
    widget.controller.addListener(_updateHeight);
  }

  void _updateHeight() {
    WidgetsBinding.instance.addPostFrameCallback((_) {
      final renderBox = _keys[widget.controller.index].currentContext?.findRenderObject() as RenderBox?;
      if (renderBox != null) {
        setState(() {
          _currentHeight = renderBox.size.height;
        });
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: _currentHeight,
      child: TabBarView(
        controller: widget.controller,
        children: widget.children.asMap().entries.map((entry) {
          return KeyedSubtree(
            key: _keys[entry.key],
            child: entry.value,
          );
        }).toList(),
      ),
    );
  }
}

// 使用方式
AdaptiveTabBarView(
  controller: tabController,
  children: [/* 你的三个子组件 */],
)

3. 给Query组件设置最小高度兜底

如果不需要高度完全贴合,给Query的加载状态和内容状态设置合理的最小高度,避免高度突变:

Query(
  options: QueryOptions(document: gql(hospitalsGraphQL)),
  builder: (result, {fetchMore, refetch}) {
    if (result.hasException) return Text(result.exception.toString());
    if (result.isLoading) {
      return SizedBox(
        height: 300, // 设置加载状态的固定高度
        child: Center(/* 原有加载指示器 */),
      );
    }
    return SizedBox(
      minHeight: 300, // 给内容设置最小高度
      child: HospitalsByTreatments(/* ... */),
    );
  },
)

内容的提问来源于stack exchange,提问作者mctracy

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.25 04:24:14