Search code examples
flutterflutter-animation

Flutter caused ScrollController has no ScrollPosition attached


I am currently reading Flutter Cookbook - Second Edition by Simone Alessandria and encountered an exception in Chapter 6 related to the stopwatch.dart file. When I run the code, I get the following exception:

The AnimationController notifying status listeners was:
  AnimationController#a6c5f(▶ 0.000)
════════════════════════════════════════════════════════════════════════════════════════════════════
8
Another exception was thrown: The Scrollbar's ScrollController has no ScrollPosition attached.

Here is the stopwatch.dart file:

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

class StopWatch extends StatefulWidget {
  const StopWatch({super.key});
  @override
  State<StopWatch> createState() => _StopWatchState();
}

class _StopWatchState extends State<StopWatch> {
  bool isTicking = false;
  int milliseconds = 0;
  late Timer timer;
  final laps = <int>[];
  final itemHeight = 60.0;
  final scrollController = ScrollController();

  void _onTick(Timer time) {
    if (mounted) {
      setState(() {
        milliseconds += 100;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Stopwatch'),
      ),
      body: Column(
        children: [
          Expanded(child: _buildCounter(context)),
          Expanded(child: _buildLapDisplay()),
        ],
      ),
    );
  }

  Widget _buildCounter(BuildContext context) {
    return Container(
      color: Theme.of(context).primaryColor,
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Text(
            'Lap ${laps.length + 1}',
            style: Theme.of(context)
                .textTheme
                .headlineSmall!
                .copyWith(color: Colors.white),
          ),
          Text(
            _secondsText(milliseconds),
            style: Theme.of(context)
                .textTheme
                .headlineSmall!
                .copyWith(color: Colors.white),
          ),
          const SizedBox(height: 20),
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              ElevatedButton(
                style: ButtonStyle(
                  backgroundColor: WidgetStateProperty.all<Color>(Colors.green),
                  foregroundColor: WidgetStateProperty.all<Color>(Colors.white),
                ),
                onPressed: isTicking ? null : _startTimer,
                child: const Text('Start'),
              ),
              const SizedBox(width: 20),
              const SizedBox(width: 20),
              ElevatedButton(
                style: ButtonStyle(
                  backgroundColor: WidgetStateProperty.all(Colors.yellow),
                ),
                onPressed: isTicking ? _lap : null,
                child: const Text('Lap'),
              ),
              SizedBox(width: 20),
              TextButton(
                style: ButtonStyle(
                  backgroundColor: WidgetStateProperty.all<Color>(Colors.red),
                  foregroundColor: WidgetStateProperty.all<Color>(Colors.white),
                ),
                onPressed: isTicking ? _stopTimer : null,
                child: const Text('Stop'),
              ),
            ],
          )
        ],
      ),
    );
  }

  Widget _buildLapDisplay() {
    return Scrollbar(
      child: ListView.builder(
        controller: scrollController,
        itemExtent: itemHeight,
        itemCount: laps.length,
        itemBuilder: (context, index) {
          final milliseconds = laps[index];
          return ListTile(
            contentPadding: EdgeInsets.symmetric(horizontal: 50),
            title: Text('Lap ${index + 1}'),
            trailing: Text(_secondsText(milliseconds)),
          );
        },
      ),
    );
  }

  @override
  void dispose() {
    timer.cancel();
    scrollController.dispose();
    super.dispose();
  }

  void _startTimer() {
    timer = Timer.periodic(const Duration(milliseconds: 1), _onTick);
    setState(() {
      milliseconds = 0;
      laps.clear();
      isTicking = true;
    });
  }

  String _secondsText(int milliseconds) {
    final seconds = milliseconds / 1000;
    return '$seconds seconds';
  }

  void _stopTimer() {
    timer.cancel();
    setState(() {
      isTicking = false;
    });
  }

  void _lap() {
    setState(() {
      laps.add(milliseconds);
      milliseconds = 0;
    });
    scrollController.animateTo(
      itemHeight * laps.length,
      duration: const Duration(milliseconds: 500),
      curve: Curves.easeIn,
    );
  }
}

I have followed the code from the book, but I am not sure what is causing the exception. The Scrollbar's ScrollController seems to be the issue, but I cannot pinpoint the exact problem. Could someone help me understand why this exception is occurring and how to fix it?

The full code can be found on the author's GitHub


Solution

  • More Details About the Exception

    The following assertion was thrown while notifying status listeners for AnimationController: The Scrollbar's ScrollController has no ScrollPosition attached. A Scrollbar cannot be painted without a ScrollPosition. The Scrollbar attempted to use the PrimaryScrollController. This ScrollController should be associated with the ScrollView that the Scrollbar is being applied to. If a ScrollController has not been provided, the PrimaryScrollController is used by default on mobile platforms for ScrollViews with an Axis.vertical scroll direction. To use the PrimaryScrollController explicitly, set ScrollView.primary to true on the Scrollable widget.

    1. Try removing the Scrollbar widget above the ListView.builder

    The builder already renders a scrollbar, so it would be redundant to have it there. Should look like so:

    Widget _buildLapDisplay() {
      return ListView.builder(
    ...  
    

    2. Explicitly add the same scrollController to the Scrollbar widget

    If you need it for decoration or other functionality, like so:

    Widget _buildLapDisplay() {
      return Scrollbar(
        controller: scrollController,
        child: ListView.builder(
            controller: scrollController,
    ...