[Easy] Reorderable List with ReorderableListView and Its Properties

Have you ever wanted to add sorting functionality while creating a list with ListView?
ReorderableListView is a Flutter widget that allows users to reorder list items by drag and drop.

This time, I will introduce ReorderableListView and its configurable properties.

Basic Usage of ReorderableListView

Below is the basic implementation code for ReorderableListView.
In the example below, you can drag and drop items in the list to change their order.

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'ReorderableListView Example',
      home: HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  final List<String> _items = List.generate(20, (index) => "Item $index");

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('ReorderableListView Example'),
      ),
      body: ReorderableListView.builder(
        itemCount: _items.length,
        itemBuilder: (context, index) {
          return ListTile(
            key: ValueKey(_items[index]),
            title: Text(_items[index]),
            trailing: const Icon(Icons.drag_handle),
          );
        },
        onReorder: (oldIndex, newIndex) {
          setState(() {
            if (newIndex > oldIndex) {
              newIndex -= 1;
            }
            final item = _items.removeAt(oldIndex);
            _items.insert(newIndex, item);
          });
        },
      ),
    );
  }
}

Using ReorderableListView.builder, 20 list items are displayed.

Properties of ReorderableListView

ReorderableListView has many properties to customize the list.
The available properties are similar to ListView, but there are slight differences.

itemBuilder (Required)

itemBuilder is a property that generates each item.
It processes the received list items one by one and returns the widget to be displayed.

itemCount: _items.length,
itemBuilder: (context, index) {
  return ListTile(
    key: ValueKey(_items[index]),
    title: Text(_items[index]),
    trailing: const Icon(Icons.drag_handle),
  );
},

itemCount (Required)

itemCount is a property that specifies the number of items in the list.
In ReorderableListView, the itemCount property is required.

itemCount: _items.length,

onReorder (Required)

It is a callback function called when the list item is rearranged.
onReorder is executed when the user drops the item.
Below is the basic processing of onReorder, but the code order is changed for easy explanation.

onReorder: (oldIndex, newIndex) {
  setState(() {
    // Remove & retrieve the item at oldIndex from the list
    final item = _items.removeAt(oldIndex);
    // Since the index after oldIndex shifts by 1,
    // if newIndex is larger than oldIndex, newIndex needs to be decremented by 1
    if (newIndex > oldIndex) {
      newIndex -= 1;
    }
    // Insert the item retrieved earlier at newIndex
    _items.insert(newIndex, item);
  });
},

itemExtent (Optional)

You can specify a fixed height (vertical extent) for each item.
When set, the height of each item is unified, improving scrolling performance. The default value is null.

itemExtent: 30.0,

prototypeItem (Optional)

The layout of the list is calculated based on the provided widget.
This reduces the overhead of layout calculations.
It is used when dealing with fixed-size (content) lists.

prototypeItem: ListTile(
  title: Text('Sample Item'),
),

proxyDecorator (Optional)

You can change the appearance of the item during drag.
Below, the item becomes semi-transparent while dragging.

proxyDecorator: (child, index, animation) {
  return Material(
    elevation: 6.0,
    color: Colors.transparent,
    shadowColor: Colors.black,
    child: child,
  );
},

buildDefaultDragHandles (Optional)

You can specify whether to use the default drag handles (on PC or smartphone).
Below, it is handled with ReorderableDragStartListener instead of the default.

buildDefaultDragHandles: false,
itemBuilder: (context, index) {
  return ListTile(
    key: ValueKey(_items[index]),
    title: Text(_items[index]),
    trailing: ReorderableDragStartListener(
      index: index,
      child: Icon(Icons.drag_handle),
    ),
  );
},

padding (Optional)

This is a familiar property in various Widgets. Padding is added to the entire list.
It is set as follows.

padding: const EdgeInsets.only(left: 15, right: 15, top: 5, bottom: 5),

header (Optional)

The widget displayed at the top of the list. It might be good for adding a list title!
Since it is an element of the list, it disappears when scrolled.
If you want to keep it always displayed, you need to add a Widget before the list or use SliverList.

header: Padding(
  padding: const EdgeInsets.all(16.0),
  child: Text(
    'List Header',
    style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
  ),
),

footer (Optional)

The widget displayed at the end of the list. Usage is the same as header.

footer: Padding(
  padding: const EdgeInsets.all(16.0),
  child: Text(
    'List Footer',
    style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
  ),
),

scrollDirection (Optional)

scrollDirection specifies the scroll direction of the list.
You can set it to vertical (Axis.vertical) or horizontal (Axis.horizontal).
The default is vertical (Axis.vertical).

scrollDirection: Axis.horizontal,

reverse (Optional)

reverse allows you to reverse the order of the list.
If set to true, the display order of the list is reversed. The default is false.

reverse: true,

scrollController (Optional)

You can finely control the scrolling of the list view from the program.

scrollController: _scrollController,

The following code has a function that moves to the top when the Top button is pressed.

class _MainState extends State<HomePage> {
  final List<String> _items = List.generate(20, (index) => "Item $index");
  final ScrollController _scrollController = ScrollController();

  @override
  void initState() {
    super.initState();
    _scrollController.addListener(() {
      print("Scroll position: ${_scrollController.position.pixels}");
    });
  }

  void _scrollToTop() {
    _scrollController.animateTo(
      0.0,
      duration: Duration(seconds: 1),
      curve: Curves.easeInOut,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Go to Top'),
        actions: [
          IconButton(
            icon: Icon(Icons.arrow_upward),
            onPressed: _scrollToTop,
          ),
        ],
      ),
      body: ReorderableListView.builder(
        itemCount: _items.length,
        itemBuilder: (context, index) {
          return ListTile(
            key: ValueKey(_items[index]),
            title: Text(_items[index]),
            trailing: ReorderableDragStartListener(
              index: index,
              child: Icon(Icons.drag_handle),
            ),
          );
        },
        scrollController: _scrollController,
        onReorder: (oldIndex, newIndex) {
          setState(() {
            if (newIndex > oldIndex) {
              newIndex -= 1;
            }
            final item = _items.removeAt(oldIndex);
            _items.insert(newIndex, item);
          });
        },
      ),
    );
  }
}

primary (Optional)

In the case of nested scroll views, setting “primary: false” to the child list prioritizes the scroll of the child list.
If not set, the scroll of the parent list is prioritized, making the child list scroll less smoothly. The default is true.

primary: false,

physics (Optional)

You can set the scroll behavior.
The standard scroll classes are as follows. The default is ClampingScrollPhysics.

  • BouncingScrollPhysics:
    Scroll rebounds when it reaches the end of the list
  • ClampingScrollPhysics:
    Scroll is fixed when it reaches the end of the list
  • FixedExtentScrollPhysics:
    Scroll moves with a fixed height position
  • NeverScrollableScrollPhysics:
    Scrolling is disabled
  • PageScrollPhysics:
    Scrolling like a page view, useful when combined with horizontal scrolling
physics: BouncingScrollPhysics(),

shrinkWrap (Optional)

shrinkWrap specifies whether the list takes up only the size it needs.
When set to true, the list determines its height based on its content.
The default is false.

shrinkWrap: true,

anchor (Optional)

You can set the display position of the list.
When set, space is created above the first item. The range is 0 to 1.
(I can’t imagine when it can be used…)

anchor: 0.1,

cacheExtent (Optional)

Controls how many widgets outside the display range the scroll view caches.
By default, it caches very little, so it may be used to improve scroll performance.
However, the default is fast enough, so it may not be necessary for small lists with a small size.

dragStartBehavior (Optional)

You can control the behavior of the touch event when starting a drag operation.

  • DragStartBehavior.start:
    Drag operation starts from the moment the user starts moving their finger
    Animation is smoother with this. This is the default.
  • DragStartBehavior.down:
    Starts from the position where the down event was first detected
    The drag operation response is slightly better
dragStartBehavior: DragStartBehavior.down,

However, the experience was the same for both. Consider setting it if you are concerned.

keyboardDismissBehavior (Optional)

You can set the behavior of closing the keyboard when the user scrolls.

  • ScrollViewKeyboardDismissBehavior.manual:
    The keyboard does not automatically close when the user scrolls
    This is set by default
  • ScrollViewKeyboardDismissBehavior.onDrag:
    The keyboard automatically closes when the user scrolls
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,

restorationId (Optional)

You can set the ID for restoring the state of the list.
This allows saving and restoring the state during the user’s session, such as scroll position and input content.
It is not set by default.
For more details, refer to the official Flutter documentation ( link ).

restorationId: 'reorderable_list', // Implement processes such as restore using this ID

clipBehavior (Optional)

Used to control how to clip the part that exceeds the boundary of the widget.
The default is Clip.hardEdge.

  • Clip.none:
    Allows drawing beyond the parent boundary
  • Clip.hardEdge:
    The part that exceeds the parent boundary is not displayed
  • Clip.antiAlias:
    Applies anti-aliasing to the part that exceeds the parent boundary (processes the overlapping part nicely)
    Smoother edges but may impact performance
  • Clip.antiAliasWithSaveLayer:
    Applies anti-aliasing to the part that exceeds the parent boundary
    Provides high-quality rendering but has high-performance costs
clipBehavior: Clip.antiAlias,

Clip.antiAlias and Clip.antiAliasWithSaveLayer may not be very effective for text lists,
but may be useful for lists with images or illustrations.

Summary

We introduced the reorderable ReorderableListView.builder.
By setting properties, it seems that the behavior can be quite customized.
Knowing what properties are available will likely expand the range of implementation.

Copied title and URL