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

Flutter Bloc屏幕旋转时LinkedScrollPosition控制器变更报错求助

解决Flutter横竖联动滚动组件屏幕旋转报错问题

问题场景

实现了横竖联动的时间表组件:横向表头为日期(Day),纵向表头为时段(Period),使用linked_scroll_controller实现滚动联动,Flutter Bloc做状态管理。组件正常运行,但屏幕旋转时抛出如下错误:

======== Exception caught by widgets library =======================================================
The following assertion was thrown building SingleChildScrollView(dependencies: [Directionality]):
_LinkedScrollPosition cannot change controllers once created.
'package:linked_scroll_controller/linked_scroll_controller.dart':
Failed assertion: line 142 pos 12: 'linkedPosition.owner == this'

The relevant error-causing widget was: 
SingleChildScrollView SingleChildScrollView:
When the exception was thrown, this was the stack: 
#2      _LinkedScrollController.attach (package:linked_scroll_controller/linked_scroll_controller.dart:142:12)
#3      ScrollableState.didUpdateWidget (package:flutter/src/widgets/scrollable.dart:561:34)
#4      StatefulElement.update (package:flutter/src/widgets/framework.dart:5142:55)
#5      Element.updateChild (package:flutter/src/widgets/framework.dart:3660:15)

问题根源

  1. 控制器重复创建:在build方法中每次都会新建LinkedScrollControllerGroup和对应的ScrollController实例。屏幕旋转触发Widget重建时,旧控制器仍与SingleChildScrollView绑定,新控制器尝试关联同一滚动位置时触发断言冲突。
  2. 重复发起Bloc事件:每次build都调用context.read<ClassBloc>().add(FetchPeriodTimeTable(widget.classId)),导致不必要的重复请求。

修复步骤

  1. 在initState中初始化滚动控制器,确保只创建一次
  2. 在dispose中销毁控制器,释放资源
  3. 将Bloc事件移至initState,仅在组件初始化时发起请求;若需响应widget.selClass变化,在didUpdateWidget中处理
  4. 重建Widget时复用已有的控制器实例

修改后的代码

MyTimeTable组件代码

import 'package:linked_scroll_controller/linked_scroll_controller.dart';
import '../../time_table.dart';

class MyTimeTable extends StatefulWidget {
  final SchoolClass selClass;

  MyTimeTable({Key? key, required this.selClass}) : super(key: key);

  @override
  State<MyTimeTable> createState() => _MyTimeTableState();
}

class _MyTimeTableState extends State<MyTimeTable> {
  late LinkedScrollControllerGroup _horizontalControllersGroup;
  late ScrollController _horizontalBody;
  late ScrollController _horizontalHeader;
  late LinkedScrollControllerGroup _verticalControllersGroup;
  late ScrollController _verticalBody;
  late ScrollController _verticalHeader;

  List<dynamic>? periodList;
  List<dynamic>? timeTableList;
  final List<String> rowEntries = ["Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday",];

  @override
  void initState() {
    super.initState();
    // 初始化滚动控制器,仅执行一次
    _horizontalControllersGroup = LinkedScrollControllerGroup();
    _horizontalBody = _horizontalControllersGroup.addAndGet();
    _horizontalHeader = _horizontalControllersGroup.addAndGet();
    _verticalControllersGroup = LinkedScrollControllerGroup();
    _verticalBody = _verticalControllersGroup.addAndGet();
    _verticalHeader = _verticalControllersGroup.addAndGet();
    
    // 仅初始化时发起一次数据请求
    context.read<ClassBloc>().add(FetchPeriodTimeTable(widget.selClass.classId));
  }

  @override
  void didUpdateWidget(covariant MyTimeTable oldWidget) {
    super.didUpdateWidget(oldWidget);
    // 当选中的班级变化时,重新请求数据
    if (oldWidget.selClass.classId != widget.selClass.classId) {
      context.read<ClassBloc>().add(FetchPeriodTimeTable(widget.selClass.classId));
    }
  }

  @override
  void dispose() {
    // 销毁控制器,避免内存泄漏
    _horizontalBody.dispose();
    _horizontalHeader.dispose();
    _verticalBody.dispose();
    _verticalHeader.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        resizeToAvoidBottomInset: false,
        appBar: sizedAppBar,
        body: SafeArea(
          child: Container(
            child: Column(
              children: [
                Row(
                  children: <Widget>[
                    SizedBox(height: cellHeight,width: cellWidth,...),
                    Expanded(
                      child: Container(// horizontal day header
                        height: 65,
                        width: 400,
                        child: SingleChildScrollView(
                          scrollDirection: Axis.horizontal,
                          controller: _horizontalHeader,
                          child: HeaderContainer(rowEntries: rowEntries),
                        ),
                      ),
                    )
                  ],
                ),
                Expanded(
                  child: Container(
                    child: Row(
                      children: <Widget>[
                        Container(// vertical period header
                          width: 100,
                          child: SingleChildScrollView(
                            controller: _verticalHeader,
                            child: getColumnContainer(),
                          ),
                        ),
                        Expanded( // data grid
                          child: SingleChildScrollView(
                            controller: _verticalBody,
                            child: SingleChildScrollView(
                              scrollDirection: Axis.horizontal,
                              controller: _horizontalBody,
                              child: getBodyContainer(),
                            ),
                          ),
                        )
                      ],
                    ),
                  ),
                ),
              ],
            ),

          ),
        ),
      ),
    );
  }
  

getColumnContainer() { // vertical period header
    return BlocBuilder<ClassBloc, ClassState>(
    builder: (context, state) {
        if(state is PeriodTimeTableList) {
        periodList = state.item1MultiList;
        }
        if(periodList != null) {
        int numberOfRows = periodList!.length;
        return ColumnContainer(
            colEntries: periodList!,
        );
        }
        return const SizedBox();
    },
    );
}

getBodyContainer() {
    return BlocBuilder<ClassBloc, ClassState>(
    builder: (context, state) {
        if(state is PeriodTimeTableList) {
        periodList = state.item1MultiList;
        timeTableList = state.item2MultiList;
        }
        if(periodList != null && timeTableList != null) {
        int _numberOfColumns = rowEntries.length;
        int _numberOfLines = periodList!.length;
        int itemCounter = 0;
        return BodyContainer(
            rowEntries: rowEntries,
            colEntries: periodList!,
            dataList: timeTableList!,
        );
        }
        return SizedBox();
    },
    );
}
    
}

ClassBloc代码(无需修改,保留原逻辑)

class ClassBloc extends Bloc<ClassEvent, ClassState> {
    final MyApi myApi;

    ClassBloc({required this.myApi}) : super(ClassInitial()) {
        
        on<FetchPeriodTimeTable>((event, emit) async {
        var result = await myApi.getPeriodTimeTable(event.classId);
        List<dynamic> periodList = [];
        periodList = result['periodList'];
        List<dynamic> timeTableList = [];
        timeTableList = result['timeTableList'];
        emit(PeriodTimeTableList(item1MultiList: periodList,item2MultiList: timeTableList));
        });
    }

    Future<void> updateTimeTable(TimeTable timeTable) async {
        try {   // to save add/edit function and update the list
        var result = await myApi.updateTimeTable(timeTable);
        if(result['status'] == 1) {
            List<dynamic> periodList = [];
            periodList = result['periodList'];
            List<dynamic> timeTableList = [];
            timeTableList = result['timeTableList'];
            emit(const TimeTableCreated());
            emit(PeriodTimeTableList(item1MultiList: periodList,item2MultiList: timeTableList));
        } else if(result['index'] == 2) {
            emit(const TimeTableNotCreated());
        } else {
            emit(const ErrorDialog(title: 'Sorry',message: 'Something went wrong'));
        }
        } catch (error) {
        emit(const ErrorDialog(title: 'Sorry',message: 'Something went wrong'));
        }
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.30 21:24:28