flutter_hooks icon indicating copy to clipboard operation
flutter_hooks copied to clipboard

A ticker created by `useSingleTickerProvider` is not muted properly

Open dev-tatsuya opened this issue 1 year ago • 0 comments
trafficstars

Describe the bug Suppose you have Screen A using useSingleTickerProvider. Screen A is rebuilt when you push from Screen A to Screen B and when you pop from Screen B to Screen A.

To Reproduce

With the code below,

  1. Verify that Screen A is rebuilt when you press the button on Screen A.
  2. Verify that Screen A is rebuilt when you press the back button on Screen B
Code example

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_hooks/flutter_hooks.dart';

void main() {
  runApp(const MaterialApp(home: AScreen()));
}

class AScreen extends HookWidget {
  const AScreen({super.key});

  @override
  Widget build(BuildContext context) {
    print('Screen A: build');

    final ticker = useSingleTickerProvider();
    final controller = useAnimationController(vsync: ticker);

    return Scaffold(
      appBar: AppBar(title: const Text('Screen A')),
      body: Center(
        child: FilledButton(
          onPressed: () {
            Navigator.of(context).push(
              MaterialPageRoute(builder: (context) => const BScreen()),
            );
          },
          child: const Text('Go to Screen B'),
        ).animate(controller: controller).shake(),
      ),
    );
  }
}

class BScreen extends StatelessWidget {
  const BScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(appBar: AppBar(title: const Text('Screen B')));
  }
}

Expected behavior It is expected that a push to Screen B and a pop from Screen B will not rebuild Screen A.

My thoughts When I didn't prepare the AnimationController myself and managed it inside Animate Widget as shown below, it worked as expected.

use ticker created by SingleTickerProviderStateMixin

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_hooks/flutter_hooks.dart';

void main() {
  runApp(const MaterialApp(home: AScreen()));
}

class AScreen extends HookWidget {
  const AScreen({super.key});

  @override
  Widget build(BuildContext context) {
    print('Screen A: build');

    return Scaffold(
      appBar: AppBar(title: const Text('Screen A')),
      body: Center(
        child: FilledButton(
          onPressed: () {
            Navigator.of(context).push(
              MaterialPageRoute(builder: (context) => const BScreen()),
            );
          },
          child: const Text('Go to Screen B'),
        ).animate().shake(),
      ),
    );
  }
}

class BScreen extends StatelessWidget {
  const BScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(appBar: AppBar(title: const Text('Screen B')));
  }
}

In other words, there seemed to be a difference in the implementation of useSingleTickerProvider and SingleTickerProviderStateMixin. As a test, I changed useSingleTickerProvider based on the implementation of SingleTickerProviderStateMixin as shown below, and it worked as expected.

changed useSingleTickerProvider

/// Creates a single usage [TickerProvider].
///
/// See also:
///  * [SingleTickerProviderStateMixin]
TickerProvider useSingleTickerProvider({List<Object?>? keys}) {
  return use(
    keys != null
        ? _SingleTickerProviderHook(keys)
        : const _SingleTickerProviderHook(),
  );
}

class _SingleTickerProviderHook extends Hook<TickerProvider> {
  const _SingleTickerProviderHook([List<Object?>? keys]) : super(keys: keys);

  @override
  _TickerProviderHookState createState() => _TickerProviderHookState();
}

class _TickerProviderHookState
    extends HookState<TickerProvider, _SingleTickerProviderHook>
    implements TickerProvider {
  Ticker? _ticker;
  ValueListenable<bool>? _tickerModeNotifier;

  @override
  Ticker createTicker(TickerCallback onTick) {
    assert(() {
      if (_ticker == null) {
        return true;
      }
      throw FlutterError(
          '${context.widget.runtimeType} attempted to use a useSingleTickerProvider multiple times.\n'
          'A SingleTickerProviderStateMixin can only be used as a TickerProvider once. '
          'If you need multiple Ticker, consider using useSingleTickerProvider multiple times '
          'to create as many Tickers as needed.');
    }(), '');
    _ticker = Ticker(onTick, debugLabel: 'created by $context');
    _updateTickerModeNotifier();
    _updateTicker(); // Sets _ticker.mute correctly.
    return _ticker!;
  }

  void _updateTicker() {
    if (_ticker != null) {
      _ticker!.muted = !_tickerModeNotifier!.value;
    }
  }

  void _updateTickerModeNotifier() {
    final ValueListenable<bool> newNotifier = TickerMode.getNotifier(context);
    if (newNotifier == _tickerModeNotifier) {
      return;
    }
    _tickerModeNotifier?.removeListener(_updateTicker);
    newNotifier.addListener(_updateTicker);
    _tickerModeNotifier = newNotifier;
  }

  @override
  void dispose() {
    assert(() {
      if (_ticker == null || !_ticker!.isActive) {
        return true;
      }
      throw FlutterError(
          'useSingleTickerProvider created a Ticker, but at the time '
          'dispose() was called on the Hook, that Ticker was still active. Tickers used '
          ' by AnimationControllers should be disposed by calling dispose() on '
          ' the AnimationController itself. Otherwise, the ticker will leak.\n');
    }(), '');
    _tickerModeNotifier?.removeListener(_updateTicker);
    _tickerModeNotifier = null;
    super.dispose();
  }

  @override
  TickerProvider build(BuildContext context) {
    _updateTickerModeNotifier();
    _updateTicker();
    return this;
  }

  @override
  String get debugLabel => 'useSingleTickerProvider';

  @override
  bool get debugSkipValue => true;
}

reference

  • https://github.com/flutter/flutter/pull/93166

Thank you.

dev-tatsuya avatar Jun 10 '24 09:06 dev-tatsuya