flutter / flutter

Flutter makes it easy and fast to build beautiful apps for mobile and beyond
https://flutter.dev
BSD 3-Clause "New" or "Revised" License
165.09k stars 27.21k forks source link

Animated List rebuilds Stateful Widget State on Animation #101551

Closed Gastrolize closed 2 years ago

Gastrolize commented 2 years ago

Hello,

I tried to create two AnimatedList with Stateful Widgets to interact with. When interacting at example with the first 3 widgets of in the first list (clicking on the item so background gets blue) and removing the first item to animate out, all items or most the item above in the first list gets rebuild to its init state. The items on the first List should have its state saved not reset to initState.

Here is the whole code to run. Also I tried with value keys, but do not work. It only works when not adding any animation.

code ``` import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:merchant/components/animated_list/scale_then_slide_transition.dart'; import '../../components/animated_list/autolist.dart'; class TestList extends StatefulWidget { @override _TestList createState() => _TestList(); } class _TestList extends State { List one = ["1","2","3"]; List two = []; GlobalKey first = GlobalKey(); GlobalKey second = GlobalKey(); putOver(int index,int list) async { if(list == 0){ var item = one[index]; two.add(one[index]); first.currentState?.removeItem(index, (context, animation) => ScaleThenSlideTransitionContinue(animation: animation, child: Card(index: index,name: item,putOver: (_) => putOver(index, 0)))); second.currentState?.insertItem(0,duration: const Duration(milliseconds: 800)); one.removeAt(index); } else if(list == 1){ var item = two[index]; one.add(two[index]); second.currentState?.removeItem(index, (context, animation) => ScaleThenSlideTransitionContinue(animation: animation,child: Card(index: index,name: item,putOver: (_) => putOver(index, 1)))); first.currentState?.insertItem(0,duration: const Duration(milliseconds: 800)); two.removeAt(index); } setState(() { }); } @override Widget build(BuildContext context) { // TODO: implement build return Scaffold( backgroundColor: Colors.black, body: Row( children: [ Flexible(flex: 1, child: Container( height: MediaQuery.of(context).size.height, decoration: BoxDecoration( color: Colors.white, ), clipBehavior: Clip.antiAliasWithSaveLayer, child: AnimatedList( key: first, initialItemCount: one.length, itemBuilder: (context,index,animation){ return ScaleThenSlideTransition(key: Key(one[index]),child: Card(index: index,name: one[index],putOver: (_) => putOver(index, 0)),animation: animation,); }, ), )), SizedBox(width: 10,), Flexible(flex: 1, child: Container( height: MediaQuery.of(context).size.height, decoration: BoxDecoration( color: Colors.white, ), clipBehavior: Clip.antiAliasWithSaveLayer, child: AnimatedList( key: second, initialItemCount: two.length, itemBuilder: (context,index,animation){ return ScaleThenSlideTransition(child: Card(index: index,name: two[index],putOver: (_) => putOver(index, 1)),animation: animation,); }, ), )), ], ), ); } } class Card extends StatefulWidget { final int index; final Function(int) putOver; final String name; const Card({Key? key, required this.index,required this.putOver,required this.name}) : super(key: key); @override _Card createState() => _Card(); } class _Card extends State with SingleTickerProviderStateMixin, KeepAliveParentDataMixin{ bool wasSet = false; getColor(){ return wasSet ? Colors.blue : Colors.green; } @override void initState() { super.initState(); } void putOver() async { setState(() { wasSet = true; }); } @override void didUpdateWidget(covariant Card oldWidget) { if(widget.name != oldWidget.name){ super.didUpdateWidget(oldWidget); } } void putOverr() async { widget.putOver(widget.index); } @override Widget build(BuildContext context) { // TODO: implement build return GestureDetector( onTap: () => putOver(), onDoubleTap: () => putOverr(), child: Container( height: 100, color: getColor(), child: Center( child: Text(widget.name.toString(),style: TextStyle(fontSize: 30,color: Colors.white),), ), ), ); } @override void detach() { // TODO: implement detach } @override // TODO: implement keptAlive bool get keptAlive => true; } class ScaleThenSlideTransition extends StatelessWidget { final Animation animation; final Widget child; const ScaleThenSlideTransition({ Key? key, required this.animation, required this.child, }) : super(key: key); @override Widget build(BuildContext context) { return SlideTransition( key: key, position: animation .drive(CurveTween(curve: const Interval(0.5, 1))) .drive(CurveTween(curve: Curves.easeOutQuint)) .drive(Tween( begin: const Offset(-1.0, 0.0), end: const Offset(0.0,0.0), )), child: SizeNoClipTransition( axis: Axis.vertical, sizeFactor: animation .drive(CurveTween(curve: const Interval(0, 0.5))) .drive(CurveTween(curve: Curves.easeInOut)), child: child, ), ); } } class ScaleThenSlideTransitionContinue extends StatelessWidget { final Animation animation; final Widget child; const ScaleThenSlideTransitionContinue({ Key? key, required this.animation, required this.child, }) : super(key: key); @override Widget build(BuildContext context) { return SlideTransition( key: key, position: animation .drive(CurveTween(curve: Interval(0.5, 1))) .drive(CurveTween(curve: Curves.easeOutQuint)) .drive(Tween( begin: Offset(1.0, 0.0), end: Offset(0.0,0.0), )), child: SizeNoClipTransition( axis: Axis.vertical, sizeFactor: animation .drive(CurveTween(curve: const Interval(0, 0.5))) .drive(CurveTween(curve: Curves.easeInOut)), child: child, ), ); } } ```

Flutter Doctor:

[✓] Flutter (Channel stable, 2.10.3, on macOS 11.6 20G165 darwin-x64, locale
    de-DE)
    • Flutter version 2.10.3 at /Users/sobhihammoud/flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 7e9793dee1 (5 weeks ago), 2022-03-02 11:23:12 -0600
    • Engine revision bd539267b4
    • Dart version 2.16.1
    • DevTools version 2.9.2

[!] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
    • Android SDK at /Users/sobhihammoud/Library/Android/sdk
    ✗ cmdline-tools component is missing
      Run `path/to/sdkmanager --install "cmdline-tools;latest"`
      See https://developer.android.com/studio/command-line for more details.
    ✗ Android license status unknown.
      Run `flutter doctor --android-licenses` to accept the SDK licenses.
      See https://flutter.dev/docs/get-started/install/macos#android-setup for
      more details.

[✓] Xcode - develop for iOS and macOS (Xcode 13.0)
    • Xcode at /Applications/Xcode-beta.app/Contents/Developer
    • CocoaPods version 1.11.2

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 4.0)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin version 49.0.2
    • Dart plugin version 193.7547
    • Java version OpenJDK Runtime Environment (build
      1.8.0_242-release-1644-b3-6222593)

[✓] IntelliJ IDEA Ultimate Edition (version 2021.3)
    • IntelliJ at /Applications/IntelliJ IDEA.app
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart

[✓] VS Code (version 1.46.1)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.12.2

[✓] Connected device (2 available)
    • iPad Pro (12.9-inch) (5th generation) (mobile) •
      51398D89-6496-4B5B-B07E-1F0951EA4D0D • ios            •
      com.apple.CoreSimulator.SimRuntime.iOS-15-0 (simulator)
    • Chrome (web)                                   • chrome
      • web-javascript • Google Chrome 100.0.4896.75
    ! Error: iPad von Golden is not connected. Xcode will continue when iPad von
      Golden is connected. (code -13)

[✓] HTTP Host Availability
    • All required HTTP hosts are available

! Doctor found issues in 1 category.
darshankawar commented 2 years ago

@Gastrolize The code you shared isn't runnable because it contains custom code and imports. In order to address this issue properly, please provide a runnable but complete minimal reproducible code sample that we can directly use and verify the behavior you are reporting. Also provide actual vs expected result upon running your code sample.

Thanks.

Gastrolize commented 2 years ago

Hello yes sorry. Here a provided code without custom imports:

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

class TestList extends StatefulWidget {
  @override
  _TestList createState() => _TestList();
}

class Cart {

  final String index;
  Cart({Key? key,required this.index});
}

class _TestList extends State<TestList> {

  List<Cart> one = [
    Cart(index: "1",key: UniqueKey()),
    Cart(index: "2",key: UniqueKey()),
    Cart(index: "3",key: UniqueKey()),
  ];

  List<Cart> two = [];

  GlobalKey<AnimatedListState> first = GlobalKey<AnimatedListState>();
  GlobalKey<AnimatedListState> second = GlobalKey<AnimatedListState>();

  putOver(int index,int list) async {

    if(list == 0){
      var item = one[index];
      two.add(one[index]);
      first.currentState?.removeItem(index, (context, animation) => ScaleThenSlideTransitionContinue(key:ObjectKey(item),animation: animation,
          child: Card(key: ObjectKey(item),index: index,name: item.index,putOver: (_) => putOver(index, 0))));
      second.currentState?.insertItem(0,duration: const Duration(milliseconds: 800));

      one.removeAt(index);

    } else if(list == 1){
      var item = two[index];
      one.add(two[index]);
      second.currentState?.removeItem(index, (context, animation) => ScaleThenSlideTransitionContinue(key:ObjectKey(item),animation: animation,child: Card(key:ObjectKey(item),index: index,name: item.index,putOver: (_) => putOver(index, 1))));
      first.currentState?.insertItem(0,duration: const Duration(milliseconds: 800));

      two.removeAt(index);

    }
    setState(() {

    });
  }

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return Scaffold(
      backgroundColor: Colors.black,
      body: Row(
        children: [
          Flexible(flex: 1, child: Container(
            height: MediaQuery.of(context).size.height,

            decoration: BoxDecoration(
              color: Colors.white,
            ),

            clipBehavior: Clip.antiAliasWithSaveLayer,
            child: AnimatedList(
              key: first,
              initialItemCount: one.length,
              itemBuilder: (context,index,animation){
                return   ScaleThenSlideTransition(key:ObjectKey(one[index]),child: Card(key:ObjectKey(one[index]),index: index,name: one[index].index,putOver: (_) => putOver(index, 0)),animation: animation,);
              },
            ),
          )),
          SizedBox(width: 10,),
          Flexible(flex: 1, child: Container(

            height: MediaQuery.of(context).size.height,
            decoration: BoxDecoration(
              color: Colors.white,
            ),
            clipBehavior: Clip.antiAliasWithSaveLayer,
            child:  AnimatedList(
              key: second,
              initialItemCount: two.length,
              itemBuilder: (context,index,animation){
                return   ScaleThenSlideTransition(key:ObjectKey(two[index]),child: Card(key:ObjectKey(two[index]),index: index,name: two[index].index,putOver: (_) => putOver(index, 1)),animation: animation,);
              },
            ),
          )),

        ],
      ),
    );
  }
}

class Card extends StatefulWidget {

  final int index;
  final Function(int) putOver;
  final String name;

  const Card({Key? key, required this.index,required this.putOver,required this.name}) : super(key: key);

  @override
  _Card createState() => _Card();

}

class _Card extends State<Card> with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin{

  bool wasSet = false;

  getColor(){
    return wasSet ? Colors.blue : Colors.green;
  }

  @override
  void initState() {

    super.initState();
  }

  void putOver() async {
    setState(() {
      wasSet = true;
    });

  }

  @override
  void didUpdateWidget(covariant Card oldWidget) {
    if(widget.name != oldWidget.name){
      super.didUpdateWidget(oldWidget);
    }

  }

  void putOverr() async {

    widget.putOver(widget.index);
  }

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return GestureDetector(
      onTap: () => putOver(),
      onDoubleTap: () => putOverr(),
      child:  Container(
        height: 100,
        color: getColor(),
        child: Center(
          child: Text(widget.name.toString(),style: TextStyle(fontSize: 30,color: Colors.white),),
        ),
      ),
    );
  }

  @override
  bool get wantKeepAlive => true;

}

@immutable
class ScaleThenSlideTransition extends StatelessWidget {
  final Animation<double> animation;

  final Widget child;

  const ScaleThenSlideTransition({
    Key? key,
    required this.animation,
    required this.child,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return SlideTransition(
      position: animation
          .drive(CurveTween(curve: const Interval(0.5, 1)))
          .drive(CurveTween(curve: Curves.easeOutQuint))
          .drive(Tween<Offset>(
        begin: const Offset(-1.0, 0.0),
        end: const Offset(0.0,0.0),
      )),
      child: SizeNoClipTransition(
        axis: Axis.vertical,
        sizeFactor: animation
            .drive(CurveTween(curve: const Interval(0, 0.5)))
            .drive(CurveTween(curve: Curves.easeInOut)),
        child: child,
      ),
    );
  }
}

@immutable
class ScaleThenSlideTransitionContinue extends StatelessWidget {
  final Animation<double> animation;

  final Widget child;

  const ScaleThenSlideTransitionContinue({
    Key? key,
    required this.animation,
    required this.child,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return SlideTransition(
      position: animation
          .drive(CurveTween(curve: Interval(0.5, 1)))
          .drive(CurveTween(curve: Curves.easeOutQuint))
          .drive(Tween<Offset>(
        begin: Offset(1.0, 0.0),
        end: Offset(0.0,0.0),
      )),
      child: SizeNoClipTransition(
        axis: Axis.vertical,
        sizeFactor: animation
            .drive(CurveTween(curve: const Interval(0, 0.5)))
            .drive(CurveTween(curve: Curves.easeInOut)),
        child: child,
      ),
    );
  }
}

@immutable
class SizeNoClipTransition extends AnimatedWidget {
  /// Creates a size transition.
  ///
  /// The [axis], [sizeFactor], and [axisAlignment] arguments must not be null.
  /// The [axis] argument defaults to [Axis.vertical]. The [axisAlignment]
  /// defaults to 0.0, which centers the child along the main axis during the
  /// transition.
  const SizeNoClipTransition({
    Key? key,
    this.axis = Axis.vertical,
    required Animation<double> sizeFactor,
    this.axisAlignment = 0.0,
    this.child,
  }) : super(key: key, listenable: sizeFactor);

  /// [Axis.horizontal] if [sizeFactor] modifies the width, otherwise
  /// [Axis.vertical].
  final Axis axis;

  /// The animation that controls the (clipped) size of the child.
  ///
  /// The width or height (depending on the [axis] value) of this widget will be
  /// its intrinsic width or height multiplied by [sizeFactor]'s value at the
  /// current point in the animation.
  ///
  /// If the value of [sizeFactor] is less than one, the child will be clipped
  /// in the appropriate axis.
  Animation<double> get sizeFactor => listenable as Animation<double>;

  /// Describes how to align the child along the axis that [sizeFactor] is
  /// modifying.
  ///
  /// A value of -1.0 indicates the top when [axis] is [Axis.vertical], and the
  /// start when [axis] is [Axis.horizontal]. The start is on the left when the
  /// text direction in effect is [TextDirection.ltr] and on the right when it
  /// is [TextDirection.rtl].
  ///
  /// A value of 1.0 indicates the bottom or end, depending upon the [axis].
  ///
  /// A value of 0.0 (the default) indicates the center for either [axis] value.
  final double axisAlignment;

  /// The widget below this widget in the tree.
  ///
  /// {@macro flutter.widgets.child}
  final Widget? child;

  @override
  Widget build(BuildContext context) {
    if (sizeFactor.value == 1) {
      return child!;
    }

    AlignmentDirectional alignment;
    if (axis == Axis.vertical)
      // ignore: curly_braces_in_flow_control_structures
      alignment = AlignmentDirectional(-1.0, axisAlignment);
    else
      // ignore: curly_braces_in_flow_control_structures
      alignment = AlignmentDirectional(axisAlignment, -1.0);
    return ClipRect(
      child: Align(
        alignment: alignment,
        heightFactor:
        axis == Axis.vertical ? math.max(sizeFactor.value, 0.0) : null,
        widthFactor:
        axis == Axis.horizontal ? math.max(sizeFactor.value, 0.0) : null,
        child: child,
      ),
    );
  }
}

Also I attached the images of what actual happens:

Three items in first List

Bildschirmfoto 2022-04-09 um 02 36 39

Then all items get clicked so they get blue

Bildschirmfoto 2022-04-09 um 02 36 45

Then when removing first item, all items above gets rebuild its state ( they get green again but should stay blue

Bildschirmfoto 2022-04-09 um 02 36 49

Final Result should be

Bildschirmfoto 2022-04-09 um 02 42 42
darshankawar commented 2 years ago

Thanks for the update. Can you try to use https://api.flutter.dev/flutter/widgets/AutomaticKeepAliveClientMixin-mixin.html which helps to keep elements in a list alive, ie, not re-render when you perform any action on them and see if it helps ?

Gastrolize commented 2 years ago

Hello, no does not work at all.

maheshj01 commented 2 years ago

Hi @Gastrolize, This seems to be working as intended. Animated Widgets are expected to rebuild more often than normal widgets.

looks similar to https://github.com/flutter/flutter/issues/72915 (closed)

Gastrolize commented 2 years ago

Hi @maheshmnj, but these are StatefulWidgets which runs the remove Animation and Insert Animation. I fixed it by using GlobalKeys with State in List.

darshankawar commented 2 years ago

@Gastrolize Thanks for the update. Since you are calling setState() inside Stateful widget, it in turn calls build() method which ideally also rebuilds the descendant widgets and hence you are seeing the behavior of rebuilding all elements and plays the animation as applicable.

I fixed it by using GlobalKeys with State in List.

Does this mean we can close as resolved ?

Gastrolize commented 2 years ago

@darshankawar Hi,

I know, the problem is not that the widget calls build multiple times. The problem is, that the animated list rebuilds the state to init state, when one item gets removed from list. SetState is whole different then resets the whole state.

The only way to resolve this was by setting a List with Statefulwidgets and give them initially a GlobalKey and using AutoList which includes the DifUtil.

Animated List needs to be fixed a little bit on the framework side.

Gastrolize commented 2 years ago

You can close this issue sure, but i guess we need to improve the animated list for those issues

darshankawar commented 2 years ago

but i guess we need to improve the animated list for those issues

This sounds as an enhancement for which I suggest you to open a new issue for better tracking. I will close this issue as resolved and per your comment above.

github-actions[bot] commented 2 years ago

This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of flutter doctor -v and a minimal reproduction of the issue.