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

Flutter报错:ScrollController未关联滚动视图 求解决方法

Flutter中AnimatedBuilder结合ScrollController报错的解决方法

问题场景

我在Flutter项目中尝试结合AnimatedBuilder组件实现无限滚动分页功能,编写的代码如下:

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';


class Home extends StatefulWidget {
  Home({Key? key}) : super(key: key);

  @override
  State<Home> createState() => _HomeState();
}

class _HomeState extends State<Home> {
  final ScrollController _scrollController = ScrollController();
  // The item list where the data is stored.
  List<String> items = []; 
  // Flags used to check the loading status.
  bool loading = false, allLoaded = false;

  // Function that loads data in the item list.
  mockFetch() async {
    // No more data to load.
    if (allLoaded) {
      return;
    }   

    // Set the loading flag to true to prevent this function to be called several times.
    setState(() {
      loading = true;
    }); 

    // Simulates the delay that might occurs during an API call.
    await Future.delayed(Duration(milliseconds: 500));

    // If the item list is higher or equal to 60, an empty array is returned to simulate
    // the end of the data stream. If not, 20 more items are generated. 
    List<String> newData = items.length >= 60 ? [] : List.generate(20, (index) => "List Item ${index + items.length}");

    // Add the new data to the item list.
    if (newData.isNotEmpty) {
      items.addAll(newData);
    }

    // Reset the flags.
    setState(() {
      loading = false;
      // Returns false if the array is empty.
      allLoaded = newData.isEmpty;
    });
  }

  @override
  void initState() {
    super.initState();
    // Call the mockFetch() function when the state of MyHomePage is initialized for the first time.
    mockFetch();
    // Set a listener.
    _scrollController.addListener(() {

      if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent && !loading) {
        mockFetch();
      }
    });
  }

  @override
  void dispose() {
    super.dispose();
    _scrollController.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.deepOrange,
        title: Text("Home Page"),
      ),
      body: LayoutBuilder(builder: (context, constraints) {
        if (items.isNotEmpty) {
          return Stack(
            // Dispay the data.
            children: [
              ListView.separated(
                // Assign the scroll controller to the ListView.
                controller: _scrollController,
                itemBuilder: (context, index) {
                  // Display the newly loaded items.
                  if (index < items.length) {
                    return ListTile(
                      title: Text(items[index]),
                    );
                  }
                  // An extra item has been added to the index of the list meaning that
                  // all the items have been loaded.
                  else {
                    // Inform the user that there is no more data to load.
                    return Container(
                      width: constraints.maxWidth,
                      height: 50,
                      child: Center(
                        child: Text("Nothing more to load"),
                      ),
                    );
                  }
              },
              // Add a separator between each item.
              separatorBuilder: (context, index) {
                return Divider(height: 1);
              },
              // Add an extra item to the length of the list when all of the items are loaded.
              itemCount: items.length + (allLoaded ? 1 : 0)),

              if (loading)...[
                // Display a progress indicator at the bottom of the list whenever some data is loading.
                Positioned(
                  left: 0,
                  bottom: 0,
                  child: Container(
                    width: constraints.maxWidth,
                    height: 80,
                    child: Center(
                      child: CircularProgressIndicator(),
                    ),
                  )
                )
              ]
            ],
          );
        }
        else {
          return Container(
            child: Center(
              child: CircularProgressIndicator(),
            ),
          );
        }

      }
    ),

      bottomNavigationBar: AnimatedBuilder(
        animation: _scrollController,
        builder: (context, child) {
          return AnimatedContainer(
            duration: Duration(milliseconds: 300),
            height: _scrollController.position.userScrollDirection == ScrollDirection.reverse ? 0: 100,
            child: child,
          );  
        },  
        child: BottomNavigationBar(
          backgroundColor: Colors.amber[200],
          items: [
            BottomNavigationBarItem(
              icon: Icon(Icons.home),
              label: 'Home',
            ),  
            BottomNavigationBarItem(
              icon: Icon(Icons.child_friendly),
              label: 'Child',
            ),  
          ],  
        ),  
      ),  
    ); // Scaffold
  }
}

报错信息

构建AnimatedBuilder(animation: ScrollController#713ce(no clients), dirty, state: _AnimatedState#02479)时抛出断言错误:ScrollController未关联任何滚动视图。'package:flutter/src/widgets/scroll_controller.dart':第107行第12位断言失败:'_positions.isNotEmpty'

解决方案

报错根源是页面初始化阶段,ListView还未完成构建,ScrollController还未关联到任何滚动视图(即_scrollController.hasClients为false),此时直接访问_scrollController.position会触发断言失败。

你提到的_scrollController.hasClients确实是核心解决点,只需要在访问ScrollController的position属性前先判断是否有客户端关联,修改AnimatedBuilder的builder逻辑即可:

修改后的AnimatedBuilder代码

bottomNavigationBar: AnimatedBuilder(
  animation: _scrollController,
  builder: (context, child) {
    // 先判断ScrollController是否已关联滚动视图
    final scrollDirection = _scrollController.hasClients 
        ? _scrollController.position.userScrollDirection 
        : ScrollDirection.idle;
    return AnimatedContainer(
      duration: Duration(milliseconds: 300),
      height: scrollDirection == ScrollDirection.reverse ? 0 : 100,
      child: child,
    );  
  },  
  child: BottomNavigationBar(
    backgroundColor: Colors.amber[200],
    items: [
      BottomNavigationBarItem(
        icon: Icon(Icons.home),
        label: 'Home',
      ),  
      BottomNavigationBarItem(
        icon: Icon(Icons.child_friendly),
        label: 'Child',
      ),  
    ],  
  ),  
),

关键说明

  • hasClients返回布尔值,代表当前ScrollController是否已绑定至少一个滚动视图
  • 初始状态下默认设置ScrollDirection.idle,确保BottomNavigationBar默认显示(高度100),避免初始化时出现异常
  • 这种方式直接在builder中做判断,是最简洁有效的解决思路

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 04:07:06