Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: ReorderableListView Control #4865

Merged
merged 6 commits into from
Feb 12, 2025

Conversation

ndonkoHenri
Copy link
Contributor

@ndonkoHenri ndonkoHenri commented Feb 10, 2025

Test Code

import flet as ft


def main(page: ft.Page):
    page.title = "ReorderableListView Demo"
    
    # the primary color is the color of the reorder handle
    page.theme = page.dark_theme = ft.Theme(color_scheme=ft.ColorScheme(primary=ft.Colors.BLUE))

    def handle_reorder(e: ft.OnReorderEvent):
        print(f"Reordered from {e.old_index} to {e.new_index}")

    get_color = lambda i: ft.Colors.ERROR if i % 2 == 0 else ft.Colors.ON_ERROR_CONTAINER

    h = ft.ReorderableListView(
        expand=True,
        horizontal=True,
        on_reorder=handle_reorder,
        controls=[
            ft.Container(
                content=ft.Text(f"Item {i}", color=ft.Colors.BLACK),
                bgcolor=get_color(i),
                margin=ft.margin.symmetric(horizontal=5, vertical=10),
                width=100,
                alignment=ft.alignment.center,
            )
            for i in range(10)
        ],
    )

    v = ft.ReorderableListView(
        expand=True,
        on_reorder=handle_reorder,
        controls=[
            ft.ListTile(
                title=ft.Text(f"Item {i}", color=ft.Colors.BLACK),
                leading=ft.Icon(ft.Icons.CHECK, color=ft.Colors.RED),
                bgcolor=get_color(i),
            )
            for i in range(10)
        ],
    )

    page.add(h, v)


ft.app(main)

Screenshot

image

Note

Custom handles could be implemented with the use of ReorderableDragStartListener. We could discuss on the best approach for it's addition.

Summary by Sourcery

New Features:

  • Add ReorderableListView to enable reordering of list items.

@FeodorFitsner FeodorFitsner added this to the Flet v0.27.0 milestone Feb 10, 2025
@FeodorFitsner FeodorFitsner merged commit 2569df3 into main Feb 12, 2025
2 checks passed
@FeodorFitsner FeodorFitsner deleted the ndonkoHenri/reorderable-list-view branch February 12, 2025 19:50
@ndonkoHenri
Copy link
Contributor Author

To test this new feature/control, do one of the following:

  • from Flet version: pip install 'flet[all]' --upgrade (if Flet v0.27.0 has been released at the time you are reading this)
  • from latest Flet pre-release: pip install 'flet[all]' --pre --upgrade

You can use the code from my comment above as starting point.

@Wanna-Pizza
Copy link

FREAKING AWESOME

@Wanna-Pizza
Copy link

@ndonkoHenri I think there is need "Spacing". Just like in row or column. Is it possible to add?

@Wanna-Pizza
Copy link

I think the best option is to give the user the ability to use their own widget that will allow them to move elements. As a result, the design developer will be able to make a widget that will move when dragging. Like when you hover over the mouse, an icon appears smoothly, or the entire element begins to change color. That is, the widget is created inside the flet, and is transferred to the dart itself to control the movement of the element. Such customization ppl would love.

@ndonkoHenri
Copy link
Contributor Author

I think there is need "Spacing". Just like in row or column. Is it possible to add?

Yeah, will be nice. But it seems like it isnt yet officially possible in Flutter. The workarounds from the Flutter community doesnt help either. (1, 2, 3)

I think the best option is to give the user the ability to use their own widget that will allow them to move elements.

In 0.28 it will be possible to define a custom drag handle, which I guess will solve this concern.

@Wanna-Pizza
Copy link

Issue: The order is reset after the window is refreshed.

2025-03-08.05-10-41.mp4

@Wanna-Pizza
Copy link

I didnt test it. Must to work. We are saving orders after reorder and taking orders from list before rebuilding interface.

import 'dart:convert';

import 'package:flutter/material.dart';

import '../flet_control_backend.dart';
import '../models/control.dart';
import '../utils/edge_insets.dart';
import '../utils/others.dart';
import 'create_control.dart';
import 'scroll_notification_control.dart';
import 'scrollable_control.dart';

class ReorderableListViewControl extends StatefulWidget {
  final Control? parent;
  final Control control;
  final bool parentDisabled;
  final List<Control> children;
  final bool? parentAdaptive;
  final FletControlBackend backend;

  const ReorderableListViewControl(
      {super.key,
      this.parent,
      required this.control,
      required this.children,
      required this.parentDisabled,
      required this.parentAdaptive,
      required this.backend});

  @override
  State<ReorderableListViewControl> createState() => _ListViewControlState();
}

class _ListViewControlState extends State<ReorderableListViewControl> {
  late final ScrollController _controller;
  List<Control> orderedChildren = []; // Saved order of children

  @override
  void initState() {
    super.initState();
    _controller = ScrollController();
    orderedChildren = List.from(widget.children); // Init orders
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  void onReorder(int oldIndex, int newIndex) {
    debugPrint("onReorder: $oldIndex -> $newIndex");
    if (newIndex > oldIndex) {
      newIndex -= 1;
    }
    setState(() {
      // Update order in  orderedChildren
      final Control movedControl = orderedChildren.removeAt(oldIndex);
      orderedChildren.insert(newIndex, movedControl);
    });
    widget.backend.triggerControlEvent(widget.control.id, "reorder",
        jsonEncode({"old": oldIndex, "new": newIndex}));
  }

  @override
  Widget build(BuildContext context) {
    debugPrint("ListViewControl build: ${widget.control.id}");

    bool disabled = widget.control.isDisabled || widget.parentDisabled;
    bool? adaptive =
        widget.control.attrBool("adaptive") ?? widget.parentAdaptive;

    var horizontal = widget.control.attrBool("horizontal", false)!;
    var buildControlsOnDemand =
        widget.control.attrBool("buildControlsOnDemand", true)!;
    var itemExtent = widget.control.attrDouble("itemExtent");
    var cacheExtent = widget.control.attrDouble("cacheExtent");
    var firstItemPrototype =
        widget.control.attrBool("firstItemPrototype", false)!;
    var padding = parseEdgeInsets(widget.control, "padding");
    var reverse = widget.control.attrBool("reverse", false)!;
    var anchor = widget.control.attrDouble("anchor", 0.0)!;
    var clipBehavior =
        parseClip(widget.control.attrString("clipBehavior"), Clip.hardEdge)!;
    var scrollDirection = horizontal ? Axis.horizontal : Axis.vertical;
    var headerCtrls =
        widget.children.where((c) => c.name == "header" && c.isVisible);
    var header = headerCtrls.isNotEmpty
        ? createControl(widget.control, headerCtrls.first.id, disabled,
            parentAdaptive: adaptive)
        : null;
    var footerCtrls =
        widget.children.where((c) => c.name == "footer" && c.isVisible);
    var footer = footerCtrls.isNotEmpty
        ? createControl(widget.control, footerCtrls.first.id, disabled,
            parentAdaptive: adaptive)
        : null;
    var prototypeItem = firstItemPrototype && widget.children.isNotEmpty
        ? createControl(widget.control, orderedChildren[0].id, disabled,
            parentAdaptive: adaptive)
        : null;
    var autoScrollerVelocityScalar =
        widget.control.attrDouble("autoScrollerVelocityScalar", 1.0);

    Widget listView = LayoutBuilder(
      builder: (BuildContext context, BoxConstraints constraints) {
        var shrinkWrap =
            (!horizontal && constraints.maxHeight == double.infinity) ||
                (horizontal && constraints.maxWidth == double.infinity);

        Widget child = buildControlsOnDemand
            ? ReorderableListView.builder(
                scrollController: _controller,
                clipBehavior: clipBehavior,
                reverse: reverse,
                cacheExtent: cacheExtent,
                scrollDirection: scrollDirection,
                shrinkWrap: shrinkWrap,
                padding: padding,
                itemCount: orderedChildren.length,
                itemExtent: itemExtent,
                anchor: anchor,
                header: header,
                footer: footer,
                prototypeItem: prototypeItem,
                autoScrollerVelocityScalar: autoScrollerVelocityScalar,
                onReorder: onReorder,
                itemBuilder: (context, index) {
                  return createControl(
                      widget.control, orderedChildren[index].id, disabled,
                      parentAdaptive: adaptive);
                },
              )
            : ReorderableListView(
                scrollController: _controller,
                cacheExtent: cacheExtent,
                reverse: reverse,
                clipBehavior: clipBehavior,
                scrollDirection: scrollDirection,
                shrinkWrap: shrinkWrap,
                padding: padding,
                anchor: anchor,
                header: header,
                footer: footer,
                itemExtent: itemExtent,
                prototypeItem: prototypeItem,
                autoScrollerVelocityScalar: autoScrollerVelocityScalar,
                onReorder: onReorder,
                children: orderedChildren.map((c) {
                  return createControl(widget.control, c.id, disabled,
                      parentAdaptive: adaptive);
                }).toList(),
              );

        child = ScrollableControl(
            control: widget.control,
            scrollDirection: scrollDirection,
            scrollController: _controller,
            backend: widget.backend,
            parentAdaptive: adaptive,
            child: child);

        return child;
      },
    );

    return constrainedControl(context, listView, widget.parent, widget.control);
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants