Skip to content

Instantly share code, notes, and snippets.

@sirpengi
Created October 19, 2023 09:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sirpengi/df56d54f40166643fca94342ab8937fd to your computer and use it in GitHub Desktop.
Save sirpengi/df56d54f40166643fca94342ab8937fd to your computer and use it in GitHub Desktop.
Implicit animation states in flutter
// Test project with a ListView.builder where the items have an
// implicit animation widget (this case an AnimatedContainer, in zulip-flutter
// for unread-markers we're using AnimatedOpacity) and the length
// of items sent into the ListView are changing. This simulates in ZF
// where we have start and end markers that are dynamically added/removed.
// The addition of a 'mark as read' marker at the beginning of the list
// cause most animations to lose their state (and thus no longer animated, but
// immediately transitioned to their new state).
//
// This test project solves that by giving the items a unique key and
// implementing `findChildIndexCallback` in `ListView.builder`.
// See https://api.flutter.dev/flutter/widgets/SliverChildBuilderDelegate/findChildIndexCallback.html
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Test',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(title: 'Scratch'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
enum DisplayItemType {
on, off, extra;
}
class _MyHomePageState extends State<MyHomePage> {
final List<bool> _fakeState = List.filled(10, false);
final List<(int, DisplayItemType)> _displayItems = [];
bool _appended = false; // tracks if the extra item should be added to the list
void _randomizeFlags() {
setState(() {
// randomize all flags
final r = Random();
for(var i = 0; i < _fakeState.length; i++) {
_fakeState[i] = r.nextBool();
}
// create a _displayItems list based on _fakeState
// but with an extra item on every other click
_displayItems.clear();
for(var i = 0; i < _fakeState.length; i++) {
_displayItems.add((i, _fakeState[i] ? DisplayItemType.on : DisplayItemType.off));
}
_appended = !_appended;
if (_appended) {
_displayItems.insert(0, (-1, DisplayItemType.extra));
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: ListView.builder(
itemCount: _displayItems.length,
findChildIndexCallback: (Key key) {
// Index is shifted by 1 if there is an extra "appended" item.
// We didn't give the "extra" item a key but that seems to
// be fine (and the callback isn't called for it).
final valueKey = key as ValueKey;
if (_appended) {
return valueKey.value + 1;
} else {
return valueKey.value;
}
},
itemBuilder: (BuildContext context, int i) {
final (key, flag) = _displayItems[i];
switch (flag) {
case DisplayItemType.extra:
return const SizedBox(height: 40, child: Text('extra'));
case DisplayItemType.on:
case DisplayItemType.off:
final color = (flag == DisplayItemType.on) ? Colors.green : Colors.red;
return SizedBox(
key: ValueKey(key),
height: 40,
child: AnimatedContainer(
duration: const Duration(seconds: 2),
color: color,
child: Text('$key $flag')));
}
},
),
floatingActionButton: FloatingActionButton(
onPressed: _randomizeFlags,
tooltip: 'Randomize',
child: const Icon(Icons.question_mark),
),
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment