Flutter: запретить прослушивателям уведомлений ChangeNotifier предотвращать анимацию кнопки отправки, состояние которой зависит от ChangeNotifier?

Мое фактическое приложение намного сложнее, но я смог упростить его до этого примера, который демонстрирует проблему.

У меня есть 2 кнопки, которые должны отражать одни и те же данные. В этом примере у меня есть 2 кнопки «Мне нравится» (сердце). Они могут быть либо в любимом, либо в непохожем состоянии. Однако оба статуса должны совпадать. Я делаю это через ChangeNotifier, ChangeNotifierProvider и Consumer.

В реальном приложении в основном есть несколько экранов с кнопками, визуальное состояние которых должно соответствовать одним и тем же данным.

Все это работает нормально до сих пор.

Вот в чем проблема. Мне нужна кнопка, чтобы сделать анимацию при нажатии. Эта анимация должна происходить только в нажатой кнопке, а не в других. В этом простом примере кнопка выполняет всплывающую анимацию. Тем не менее, notifyListener ChangeNotifier предотвращает анимацию, поскольку я думаю, что он в основном перестраивает виджет и, таким образом, предотвращает анимацию. Если я закомментирую notifyListener, то анимация будет работать, но, очевидно, данные больше не будут обновляться, и, следовательно, другие кнопки не будут отражать правильный статус «нравится/не нравится».

Я пробовал назначать клавиши на кнопки, но это не помогло. Я теряюсь в том, как я могу решить это. По сути, мне нужно предотвратить влияние на нажатую кнопку Consumer и notifyListener.

Код:

var b1 = GlobalKey();
var b2 = GlobalKey();
final buttonStateProvider = ButtonStateProvider();

@override
Widget build(BuildContext context) {
  return Scaffold(
    body: Center(
      child: ChangeNotifierProvider.value(
        value: buttonStateProvider,
        child: Consumer<ButtonStateProvider>(builder: (context, provider, child) {
          return Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              PoppingButton(
                key: b1,
                liked: provider.isLiked,
                onTap: () {
                  provider.toggleLiked();
                },
              ),
              PoppingButton(
                key: b2,
                liked: provider.isLiked,
                onTap: () {
                  provider.toggleLiked();
                },
              ),
            ],
          );
        }),
      ),
    ),
  );
}

//-------------------

class ButtonStateProvider extends ChangeNotifier {
  bool isLiked = false;
  void toggleLiked() {
    isLiked = !isLiked;
    notifyListeners();
  }
}

class PoppingButton extends StatelessWidget {
  const PoppingButton({super.key, required this.liked, required this.onTap});
  final bool liked;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) {
    var keyForHeart = GlobalKey<PopWidgetState>();
    return GestureDetector(
      onTap: (() {
        keyForHeart.currentState?.pop();
        onTap.call();
      }),
      child: PopWidget(
        key: keyForHeart,
        child: Icon(
          liked ? CupertinoIcons.suit_heart_fill : CupertinoIcons.suit_heart,
          size: 100,
          color: liked ? Colors.pink : Colors.grey,
        ),
      ),
    );
  }
}

class PopWidget extends StatefulWidget {
  const PopWidget({super.key, required this.child});
  final Widget child;
  @override
  State<PopWidget> createState() => PopWidgetState();
}

class PopWidgetState extends State<PopWidget> with SingleTickerProviderStateMixin {
  late AnimationController animationController = AnimationController(duration: const Duration(milliseconds: 50), vsync: this);
  late var scaleAnimation = Tween<double>(begin: 1.0, end: 1.4).animate(CurvedAnimation(parent: animationController, curve: Curves.easeOutSine));

  void pop() {
    animationController.forward();
  }

  @override
  void initState() {
    super.initState();
    animationController.addStatusListener(animationListenerHandler);
  }

  void animationListenerHandler(status) {
    // print("Status: ${animationController.status}");
    switch (animationController.status) {
      case AnimationStatus.completed:
        animationController.reverse();
        break;
      case AnimationStatus.dismissed:
        break;
      default:
    }
  }

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

  @override
  Widget build(BuildContext context) {
    // print("scaleAnimation.value: ${scaleAnimation.value}");
    return AnimatedBuilder(
      animation: animationController,
      builder: ((context, child) {
        return Transform.scale(
          scale: scaleAnimation.value,
          child: widget.child,
        );
      }),
    );
  }
}

Состояние пользовательского интерфейса:

Итак, что вы пытаетесь заархивировать, так это то, что обе кнопки должны быть обновлены до одного и того же состояния, но только нажатая кнопка должна быть анимирована с масштабом?

Mohammed Alfateh 14.12.2022 13:22

@MohammedAlfateh да, точно!

sudoExclaimationExclaimation 14.12.2022 20:12
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
2
102
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Добавьте слушателя в свой ButtonStateProvider и проигрывайте анимацию при каждом изменении состояния. PopWidgetStateinit :

@override
void initState() {
  super.initState();
  Provider.of<ButtonStateProvider>(context, listen: false).addListener(() {
    pop();
  });
  animationController.addStatusListener(animationListenerHandler);
}

Извините, я не понимаю, куда идет этот код. Что, если PopWidgetState является частной/третьей стороной?

sudoExclaimationExclaimation 14.12.2022 12:56
PopWidgetState class initState, это из вашего кода, который вы разместили в своем вопросе. И удалите ключи, которые вы создали для своих виджетов.
Ante Bule 14.12.2022 16:36
Ответ принят как подходящий

Я немного изменил ваш код, вам не нужно GlobalKey

Я также добавил некоторые комментарии к изменениям, которые я сделал

  final buttonStateProvider = ButtonStateProvider();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: ChangeNotifierProvider.value(
          value: buttonStateProvider,
          child: Consumer<ButtonStateProvider>(
              builder: (context, provider, child) {
            return Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                for (int i = 1; i < 3; i++)
                  PoppingButton(
                    id: i,
                    liked: provider.isLiked,
                    tappedId: provider.tappedId,
                    onTap: () => provider.toggleLiked(i),
                  ),
              ],
            );
          }),
        ),
      ),
    );
  }

//-------------------
class ButtonStateProvider extends ChangeNotifier {
  bool isLiked = false;

  // used to indentfiy the widget to play the scale animation
  int tappedId = 0;

  void toggleLiked(int id) {
    isLiked = !isLiked;
    tappedId = id;
    notifyListeners();
  }
}

class PoppingButton extends StatelessWidget {
  const PoppingButton(
      {super.key,
      required this.liked,
      required this.tappedId,
      required this.onTap,
      required this.id});
  final bool liked;
  final VoidCallback onTap;
  final int id;
  final int tappedId;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: (() {
        onTap.call();
      }),
      child: PopWidget(
        key: ValueKey(id),
        id: id,
        tappedId: tappedId,
        isLiked: liked,
        child: Icon(
          liked ? CupertinoIcons.suit_heart_fill : CupertinoIcons.suit_heart,
          size: 100,
          color: liked ? Colors.pink : Colors.grey,
        ),
      ),
    );
  }
}

class PopWidget extends StatefulWidget {
  const PopWidget(
      {super.key,
      required this.child,
      required this.tappedId,
      required this.isLiked,
      required this.id});
  final Widget child;
  final bool isLiked;
  final int id;
  final int tappedId;
  @override
  State<PopWidget> createState() => PopWidgetState();
}

class PopWidgetState extends State<PopWidget>
    with SingleTickerProviderStateMixin {
  late AnimationController animationController = AnimationController(
      duration: const Duration(milliseconds: 50), vsync: this);
  late var scaleAnimation = Tween<double>(begin: 1.0, end: 1.4).animate(
      CurvedAnimation(parent: animationController, curve: Curves.easeOutSine));

  @override
  void initState() {
    super.initState();
    animationController.addStatusListener(animationListenerHandler);
  }

  void animationListenerHandler(status) {
    // print("Status: ${animationController.status}");
    switch (animationController.status) {
      case AnimationStatus.completed:
        animationController.reverse();
        break;
      case AnimationStatus.dismissed:
        break;
      default:
    }
  }

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

  // update the animation state when [isLiked] is true
  // and check the id of the tapped widget and the widget id
  @override
  void didUpdateWidget(covariant PopWidget oldWidget) {
    if (widget.isLiked && widget.id == widget.tappedId) {
      animationController.forward();
    }

    super.didUpdateWidget(oldWidget);
  }

  @override
  Widget build(BuildContext context) {
  // print("scaleAnimation.value: ${scaleAnimation.value}");
    return AnimatedBuilder(
      animation: animationController,
      builder: ((context, child) {
        return Transform.scale(
          scale: scaleAnimation.value,
          child: widget.child,
        );
      }),
    );
  }
}

Позвольте мне попробовать это. didChangeDependencies похож на didUpdateWidget(covariant PopWidget oldWidget)? Ваш ответ натолкнул меня на мысль использовать didUpdateWidget для анимации только в том случае, если статус изменился по сравнению с предыдущим состоянием.

sudoExclaimationExclaimation 14.12.2022 23:21

Вы можете использовать didUpdateWidget здесь, однако это даст тот же результат, но я думаю, что лучше использовать его здесь, чтобы вы могли использовать ValueKey вместо GlobalKey, что легче, я обновлю ответ.

Mohammed Alfateh 14.12.2022 23:58

Другие вопросы по теме