Repository: flutter
Issue: State-based animations
Labels: c: new feature,framework,a: animation,c: proposal,P3,team-framework,triaged-framework

## Use case

Flutter's animation framework is very capable, but more verbose and complex to use when compared to SwiftUI. I have the sense that many developers new to Flutter struggle a bit with it. 

SwiftUI has a powerful, yet intuitive and terse, as well as transparent [animation framework](https://developer.apple.com/documentation/swiftui/animations) ([Introduction](https://developer.apple.com/tutorials/swiftui/animating-views-and-transitions)).

In SwiftUI, instead of explicitly specifying what values to animate and how, a state change can be associated with a general description (`Animation`) of how to animate changes (e.g. curve, duration, delay) and any animatable value that changes as a result of the state change is animated based on this `Animation`.

```swift
withAnimation {
    showDetail.toggle()
}
```

The snippet above uses the default  `Animation` and animates all changes of animatable values that are a result of toggling `showDetail`.

The Flutter equivalent could look like this:

```dart
withAnimation(AnimationSpec(), () {
  setState(() {
    showDetail = !showDetail;
  });
});
```

There is also a declarative way of defining animations, which supports targeting parts of the UI.

```swift
Label("Graph")
  .scaleEffect(showDetail ? 1.5 : 1)
  .animation(.easeInOut, value: showDetail)
```

Here, the `easeIntOut` `Animation` is used to animate changes that coincide with a change in `showDetail`. `.animation` is a modifier for a part of the UI and restricts animations to it.

The Flutter equivalent could look like this:

```dart
Animated(
  animation: AnimationSpec.easeInOut,
  value: showDetail,
  child: Transform.scale(
    scale: showDetail ? 1.5 : 1,
    child: Text('Graph'),
  ),
);
```

Obviously, Flutter would not have to adopt SwiftUI's approach one-to-one.

## Proposal

The abstraction which is central to the SwiftUI animation framework is the [`Transaction`](https://developer.apple.com/documentation/swiftui/transaction).

Introducing a similar concept to Flutter would allow implementing state-based animations on top of it. There might be use cases other than animations.

`InheritedWidget` provides context to widgets based on their location. Transactions provide context to widgets based on when they are being built. The context value of the current transaction is available in a global variable. If a widget is built outside of a transaction, this variable contains `null`.

The code below illustrates how state-based animations could use transactions:

```dart
import 'package:flutter/material.dart';

abstract class AnimationSpec {}

abstract class Transaction {
  static Transaction? current;

  AnimationSpec? get animation;
}

class AnimatedValue<T> {
  AnimatedValue(T value, this.tween, {required this.owner})
      : _value = value,
        _animatedValue = value;

  final TickerProviderStateMixin owner;
  final Tween<T> tween;

  T get value => _value;
  T _value;

  set value(T value) {
    _value = value;

    final animation = Transaction.current?.animation;
    if (animation == null) {
      _animatedValue = value;
    } else {
      _animate(animation);
    }
  }

  T get animatedValue => _animatedValue;
  T _animatedValue;

  void _animate(AnimationSpec animation) {
    // Uses [owner] to create [Ticker] or [AnimationController]
    // and rebuild owner widget.
    // TODO: animate from [animatedValue] to [value]
  }

  void dispose() {
    // TODO: dispose running animation
  }
}

class BlueSquare extends StatefulWidget {
  const BlueSquare({super.key, required this.size});

  final double size;

  @override
  State<BlueSquare> createState() => _BlueSquareState();
}

class _BlueSquareState extends State<BlueSquare> with TickerProviderStateMixin {
  late final size = AnimatedValue(widget.size, Tween<double>(), owner: this);

  @override
  void dispose() {
    size.dispose();
    super.dispose();
  }

  @override
  void didUpdateWidget(covariant BlueSquare oldWidget) {
    super.didUpdateWidget(oldWidget);
    size.value = widget.size;
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox.square(
      dimension: size.animatedValue,
      child: const ColoredBox(color: Colors.blue),
    );
  }
}
```

`withTransaction` allows users to associate state changes with transactions:

```dart
T withTransaction(Transaction transaction, T Function() block);
```

At a high level, the steps to execute a transaction are :

1. Build all `Element`s that are marked as needing to rebuild.
2. Run the `block` closure.
3. Set the global transaction context value to `transaction`.
4. Build all `Element`s that are marked as needing to rebuild.
5. Set the global transaction context value to `null`.

Transactions should support nesting, which would complicate the algorithm for executing a transaction, so I omitted it here.

`ScopedTransaction` is a widget that makes the provided transaction the current one while building its child:

```dart
class ScopedTransaction extends Widget {
  const ScopedTransaction({super.key, this.transaction, this.child});

  final Transaction? transaction;
  final Widget? child;
}
```

Again, nesting should be supported. The `Animated` widget from earlier could be built with `ScopedTransaction`.