karvulf / flutter-reorderable-grid-view

BSD 3-Clause "New" or "Revised" License
158 stars 23 forks source link

_ReorderableBuilderState._handleDragEnd thows null check error #44

Closed meomap closed 2 years ago

meomap commented 2 years ago

Hello, thank you for your great package.

We've got this exception thrown at the production app.

flutter_reorderable_grid_view: ^3.1.2

Sentry stacktrace:

_CastError: Null check operator used on a null value
  split_config.arm64_v8a.apk0x727de7b968 _ReorderableBuilderState._handleDragEnd (reorderable_builder.dart:318)
  split_config.arm64_v8a.apk0x727de7b558 _ReorderableBuilderState._handleDragEnd (reorderable_builder.dart:315)
  split_config.arm64_v8a.apk0x727de7ca24 _ReorderableScrollingListenerState.build.<T> (reorderable_scrolling_listener.dart:82)
  split_config.arm64_v8a.apk0x727dc6e674 RenderPointerListener.handleEvent (proxy_box.dart:2916)
  split_config.arm64_v8a.apk0x727e15f3a0 GestureBinding.dispatchEvent (binding.dart:425)
  split_config.arm64_v8a.apk0x727db0df7c RendererBinding.dispatchEvent (binding.dart:329)
  split_config.arm64_v8a.apk0x727db0dec0 GestureBinding._handlePointerEventImmediately (binding.dart:380)
  split_config.arm64_v8a.apk0x727db0dac4 GestureBinding.handlePointerEvent (binding.dart:344)
  split_config.arm64_v8a.apk0x727dbd560c GestureBinding._handlePointerDataPacket (binding.dart:285)
  split_config.arm64_v8a.apk0x727dbd5510 GestureBinding._handlePointerDataPacket (binding.dart:280)
  split_config.arm64_v8a.apk0x727e1143a0 _rootRunUnary (zone.dart:1442)
  split_config.arm64_v8a.apk0x727da35d4c _rootRunUnary (zone.dart:1432)
  split_config.arm64_v8a.apk0x727e00f5f8 _CustomZone.runUnary (zone.dart:1335)
  split_config.arm64_v8a.apk0x727e23269c _CustomZone.runUnaryGuarded (zone.dart:1244)
  split_config.arm64_v8a.apk0x727da46af0 _invoke1 (hooks.dart:170)
  split_config.arm64_v8a.apk0x727da469f0 PlatformDispatcher._dispatchPointerDataPacket (platform_dispatcher.dart:331)
  split_config.arm64_v8a.apk0x727da46968 _dispatchPointerDataPacket (hooks.dart:94)
karvulf commented 2 years ago

Hi @meomap Thank you for opening the issue. I will try to fix that problem

karvulf commented 2 years ago

I am wondering why that problem is happening. Could you share the code snippet of the part where you use ReorderableBuilder? Are you changing some values of ReorderableBuilder while dragging? @meomap

meomap commented 2 years ago

Sure, we use ReorderableBuilder to render a grid view of photo collection where user can add new photo, remove existing one and dragging to change order.

Here is code snippet:

import 'package:flutter/material.dart';
import 'package:equatable/equatable.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter_reorderable_grid_view/entities/order_update_entity.dart';
import 'package:flutter_reorderable_grid_view/widgets/widgets.dart';

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

  @override
  State<PhotoCollectionModal> createState() => _PhotoCollectionModalState();
}

class _PhotoCollectionModalState extends State<PhotoCollectionModal> {
  bool draggingItem = false;

  final _gridViewKey = GlobalKey();
  final _scrollController = ScrollController();
  final List<Photo> items =
      List<Photo>.generate(10, (index) => _dummyPhoto(index));

  @override
  Widget build(BuildContext context) {
    final colorScheme = Theme.of(context).colorScheme;
    const double space = 16;
    const cols = 3;
    final generatedChildren = List<Widget>.generate(
        items.length,
        (index) => Container(
              key: ValueKey(items[index]),
              decoration: BoxDecoration(
                color: colorScheme.surface,
                borderRadius: BorderRadius.circular(4),
                border: Border.all(color: colorScheme.outline),
              ),
              child: Stack(
                children: [
                  Center(
                    child: CachedNetworkImage(
                      imageUrl: items[index].networkUrl,
                      fit: BoxFit.scaleDown,
                    ),
                  ),
                  Align(
                      alignment: Alignment.topRight,
                      child: Padding(
                        padding: const EdgeInsets.all(4),
                        child: GestureDetector(
                            onTap: () => handleRemove(index),
                            child:
                                Icon(Icons.remove_circle, color: colorScheme.error)),
                      ))
                ],
              ),
            ));

    return SafeArea(
      child: Scaffold(
          appBar: AppBar(title: const Text('Collection')),
          floatingActionButton: draggingItem
              ? null
              : FloatingActionButton(
                  child: const Icon(Icons.add), onPressed: handleAdd),
          body: Padding(
            padding: const EdgeInsets.all(space),
            child: ReorderableBuilder(
              children: generatedChildren,
              onReorder: handleReorder,
              enableLongPress: false,
              dragChildBoxDecoration: BoxDecoration(
                  color: Theme.of(context)
                      .colorScheme
                      .secondaryContainer
                      .withOpacity(0.54)),
              onDragStarted: () {
                setState(() {
                  draggingItem = true;
                });
              },
              onDragEnd: () {
                setState(() {
                  draggingItem = false;
                });
              },
              scrollController: _scrollController,
              builder: (children) {
                return GridView.builder(
                  key: _gridViewKey,
                  controller: _scrollController,
                  itemCount: children.length,
                  itemBuilder: (context, index) {
                    return children[index];
                  },
                  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                    crossAxisCount: cols,
                    mainAxisSpacing: space,
                    crossAxisSpacing: space,
                  ),
                );
              },
            ),
          )),
    );
  }

  void handleAdd() {
    setState(() {
      items.add(_dummyPhoto(items.length));
    });
  }

  void handleRemove(int index) {
    setState(() {
      items.removeAt(index);
    });
  }

  void handleReorder(List<OrderUpdateEntity> updates) {
    for (final entry in updates) {
      if (entry.oldIndex >= items.length || entry.newIndex >= items.length) {
        // Skip deleted one
        continue;
      }
      final child = items.removeAt(entry.oldIndex);
      items.insert(entry.newIndex, child);
    }
    if (!mounted) return;
    setState(() {});
  }
}

Photo _dummyPhoto(int index) => Photo(
    'https://picsum.photos/${100 + index}/${100 + index}/',
    DateTime.now().microsecondsSinceEpoch + index);

class Photo extends Equatable {
  final int id;
  final String networkUrl;

  const Photo(this.networkUrl, this.id);

  @override
  List<Object?> get props => [networkUrl, id];
}
meomap commented 2 years ago

In void handleReorder(List<OrderUpdateEntity> updates), we noticed that when user removed an item then immediately drag one at the end of list, entry.oldIndex sometimes equals current items.length. It happens occasionally and is reproducible in dev environment.

karvulf commented 2 years ago

I realized that this bug that you describe seems to happen when using GridView.builder. I also recognized that the animation of removing an item is not working. So if you don't have a special reason to use GridView.builder, you could also use GridView as workaround. I will try to fix this problem in the next days. @meomap

return  GridView(
  key: _gridViewKey,
  controller: _scrollController,
  children: children,
  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: cols,
    mainAxisSpacing: space,
    crossAxisSpacing: space,
  ),
karvulf commented 2 years ago

I think I found a solution, so you could also wait for the update in the next days 👍 @meomap

meomap commented 2 years ago

@karvulf It's ok, I think it's just a minor glitch and we don't have much users affected by it yet. I can use GridView as well. No need to hurry :)

Thank you so much for your support 👌

karvulf commented 2 years ago

The bug should be fixed and released in a few minutes. You can use version 3.1.3 for that. Also the animation for adding or removing your images should work now :) I am closing this issue, if there are some other new issues with this update, feel free to open a new one @meomap

meomap commented 2 years ago

That's great! @karvulf Thanks so much 👌