Dynamically creating or reordering children widgets in a PageView in Flutter

Viewed 525

Context: Imagine the Snapchat UI. You are on your friend-list screen (to the left of the Camera screen). You swipe left to right on a friend's name, and Snapchat animates a transition from the friend-list screen to the chat window for that friend as you swipe.

Desired Behavior: Swipe right to left on any tile in a vertical list to reveal a contextual screen from the right related to that tile. Within the contextual screen, swipe left to right to return to the tile-list screen.

Current Approach: Use a PageView widget with two children. The first child is the friend list widget. The second child will be the contextual screen widget for whichever tile is "swiped left" on. To accomplish this, the second child will need to be dynamically created (meaning, at runtime), perhaps by wrapping each tile in a GestureDetector widget and setting the second child of PageView in the onHorizontalDragStart callback function for a given tile.

My Question: How do you dynamically (at runtime) create, or possibly reorder, the children of a PageView widget in a Flutter application?

The flutter documentation for PageView.builder says:

PageView.builder by default does not support child reordering. If you are planning to change child order at a later time, consider using PageView or PageView.custom.

It sounds like this is possible with either PageView or PageView.custom, but how?

1 Answers

They have an pretty straight forward example in the PageView.custom about handling with reordering:

  class MyPageView extends StatefulWidget {
    const MyPageView({Key? key}) : super(key: key);
 
    @override
    State<MyPageView> createState() => _MyPageViewState();
  }
 
  class _MyPageViewState extends State<MyPageView> {
    List<String> items = <String>['1', '2', '3', '4', '5'];
 
    void _reverse() {
      setState(() {
        items = items.reversed.toList();
      });
    }
 
    @override
    Widget build(BuildContext context) {
      return Scaffold(
        body: SafeArea(
          child: PageView.custom(
            childrenDelegate: SliverChildBuilderDelegate(
              (BuildContext context, int index) {
                return KeepAlive(
                  data: items[index],
                  key: ValueKey<String>(items[index]),
                );
              },
              childCount: items.length,
              findChildIndexCallback: (Key key) {
                final ValueKey<String> valueKey = key as ValueKey<String>;
                final String data = valueKey.value;
                return items.indexOf(data);
              }
            ),
          ),
        ),
        bottomNavigationBar: BottomAppBar(
          child: Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              TextButton(
                onPressed: () => _reverse(),
                child: const Text('Reverse items'),
              ),
            ],
          ),
        ),
      );
    }
  }
 
  class KeepAlive extends StatefulWidget {
    const KeepAlive({Key? key, required this.data}) : super(key: key);
 
    final String data;
 
    @override
    State<KeepAlive> createState() => _KeepAliveState();
  }
 
  class _KeepAliveState extends State<KeepAlive> with AutomaticKeepAliveClientMixin{
    @override
    bool get wantKeepAlive => true;
 
    @override
    Widget build(BuildContext context) {
      super.build(context);
      return Text(widget.data);
    }
  }

Which could be reduced in my understandment to just:

  class MyPageView extends StatefulWidget {
    const MyPageView({Key? key}) : super(key: key);
 
    @override
    State<MyPageView> createState() => _MyPageViewState();
  }
 
  class _MyPageViewState extends State<MyPageView> {
    List<String> items = <String>['1', '2', '3', '4', '5'];
 
    void _reverse() {
      setState(() {
        items = items.reversed.toList();
      });
    }
 
    @override
    Widget build(BuildContext context) {
      return Scaffold(
        body: SafeArea(
          child: PageView.custom(
            childrenDelegate: SliverChildBuilderDelegate(
              (BuildContext context, int index) {
                return KeepAlive(
                  data: items[index],
                  key: ValueKey<String>(items[index]),
                );
              },
              childCount: items.length,
              // This is default, can be omitted.
              addAutomaticKeepAlives: true,
              findChildIndexCallback: (Key key) {
                final ValueKey<String> valueKey = key as ValueKey<String>;
                final String data = valueKey.value;
                return items.indexOf(data);
              }
            ),
          ),
        ),
        bottomNavigationBar: BottomAppBar(
          child: Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              TextButton(
                onPressed: () => _reverse(),
                child: const Text('Reverse items'),
              ),
            ],
          ),
        ),
      );
    }
  }
 
  class KeepAlive extends StatelessWidget {
    const KeepAlive({Key? key, required this.data}) : super(key: key);
 
    final String data;
 
    @override
    Widget build(BuildContext context) {
      super.build(context);
      return Text(data);
    }
  }
Related