Flutter «Поиск предка деактивированного виджета небезопасен». при возвращении из 'showSearch'

У нас есть пользовательский интерфейс, который показывает список со многими элементами, и мы хотим иметь возможность легко их сортировать. Мы уже используем ReorderableListView, но поскольку он требует от пользователя перетаскивания, он не очень хорошо работает для списков с большим количеством элементов. Мы пришли к идее использовать поиск следующим образом:

  1. На первом этапе пользователю разрешено изменять порядок элементов путем перетаскивания, но он также может щелкнуть ведущий IconButton, чтобы выбрать «исходный» элемент для перемещения. Чтобы помочь быстро выбрать правильный «исходный» элемент, пользователю разрешено искать/фильтровать показанные элементы по запросу.
  2. Выбор «исходного» элемента на шаге 1 запускает второй шаг: отображается список с теми же данными (с отключенным элементом, выбранным на шаге 1), и пользователь может щелкнуть ведущий IconButton, чтобы выбрать «целевой» элемент. Чтобы помочь быстро выбрать правильный «целевой» элемент, пользователю разрешено искать/фильтровать отображаемые элементы по запросу.
  3. Выбор «целевого» элемента запускает шаг 3: отображается небольшое диалоговое окно, в котором пользователь может выбрать, следует ли помещать «исходный» элемент из шага 1 до или после «целевого» элемента из шага 2.
  4. После выбора срабатывает логика (в моем случае с использованием BLoC).

Шаги 1 и 2 реализованы с использованием функций Flutters showSearch с делегатами, т. е. выбор элемента в поиске на шаге 1 запускает новый «дочерний» поиск с новым делегатом. Пожалуйста, взгляните на следующий GIF, где я сначала выбираю «источник», «Элемент 0» на шаге 1, затем «целевой» «Элемент 4» на шаге 2, а затем «После» на шаге 3:

Flutter «Поиск предка деактивированного виджета небезопасен». при возвращении из 'showSearch'

В общем, это работает так, как мы хотим, если мы передаем экземпляр BLoC и не ищем его, используя контекст. Тем не менее, мы хотим использовать контекст, поскольку реальное приложение намного сложнее, и передача BLoC нецелесообразна. Однако использование context.read вызывает следующую ошибку:

[VERBOSE-2:ui_dart_state.cc(177)] Unhandled Exception: Looking up a deactivated widget's ancestor is unsafe.
At this point the state of the widget's element tree is no longer stable.
To safely refer to a widget's ancestor in its dispose() method, save a reference to the ancestor by calling dependOnInheritedWidgetOfExactType() in the widget's didChangeDependencies() method.
#0      Element._debugCheckStateIsActiveForAncestorLookup.<anonymous closure>
package:flutter/…/widgets/framework.dart:3906
#1      Element._debugCheckStateIsActiveForAncestorLookup
package:flutter/…/widgets/framework.dart:3920
#2      Element.getElementForInheritedWidgetOfExactType
package:flutter/…/widgets/framework.dart:3986
#3      Provider._inheritedElementOf
package:provider/src/provider.dart:324
#4      Provider.of
package:provider/src/provider.dart:281
#5      ReadContext.read
package:provider/src/provider.dart:614
#6      SortingTile._moveItem
package:flutter_test_app/main.dart:80
<as<…>

На всю жизнь я не могу заставить его работать. Я пытался добавить виджет Builder здесь и там, чтобы получить другой экземпляр контекста, но это просто не сработает.

Вот полный код (строка, вызывающая ошибку, помеченная <--- HERE), он немного длинный, поэтому, пожалуйста, дайте мне знать, если я должен загрузить его куда-нибудь. Я не думаю, что смогу сделать его намного короче и по-прежнему иметь возможность воспроизвести проблему (возможно, я мог бы, но я этого не понимаю, так что это так). На самом деле первая половина main.dart интересна, остальное - это BLoC и т. д., которые работают и, вероятно, не имеют значения, но кто знает. Код должен работать без проблем с Flutter 1.22.5.

pubspec.yaml:

name: flutter_sorting_app
version: 1.0.0+1

environment:
  sdk: '>=2.7.0 <3.0.0'

dependencies:
  bloc: ^6.1.0
  flutter_bloc: ^6.1.0
  flutter:
    sdk: flutter

flutter:
  uses-material-design: true

основной.дротик:

import 'dart:async';

import 'package:bloc/bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

void main() {
  runApp(
    BlocProvider(
      create: (context) => DataBloc(),
      child: MaterialApp(
        home: Scaffold(
          appBar: AppBar(
            actions: [
              Builder(
                builder: (context) => IconButton(
                  icon: Icon(Icons.search),
                  onPressed: () {
                    context.read<DataBloc>().add(DataRequested());
                    showSearch(
                      context: context,
                      delegate: SortingSearchDelegate(),
                    );
                  },
                ),
              ),
            ],
          ),
        ),
      ),
    ),
  );
}

// SearchDelegates

// Step 1 - sort by drag & drop (no step 2 necessary) or pick the item to move

class SortingSearchDelegate extends DataBlocSearchDelegate<DataBloc, void> {
  @override
  Widget buildListDataWidget(BuildContext context, List<IndexedItem> data) {
    return ReorderableListView(
      onReorder: (from, to) {
        context.read<DataBloc>().add(DataItemPositionChanged(from, to));
      },
      children: data.map((indexedItem) => SortingTile(indexedItem)).toList(),
    );
  }
}

class SortingTile extends StatelessWidget {
  final IndexedItem indexedItem;

  SortingTile(this.indexedItem) : super(key: ValueKey(indexedItem.item));

  @override
  Widget build(BuildContext context) {
    return ListTile(
      leading: IconButton(
        onPressed: () => _moveItem(context),
        icon: Icon(Icons.swap_vert),
      ),
      title: Text('${indexedItem.item}'),
      trailing: Icon(Icons.drag_handle),
    );
  }

  Future<void> _moveItem(BuildContext context) async {
    // Trigger step 2.
    // There is no need to request any data, the same BLoC is reused.
    final insertionPoint = await showSearch(
      context: context,
      delegate: InsertionPointPickerSearchDelegate(indexedItem.index),
    );
    if (insertionPoint != null) {
      var targetIndex = insertionPoint.index;
      if (insertionPoint.placement == Placement.after) {
        ++targetIndex;
      }
      context
          .read<DataBloc>() // <--- HERE
          .add(DataItemPositionChanged(indexedItem.index, targetIndex));
    }
  }
}

// Step 2 - pick placement of picked item in step 1

enum Placement { before, after }

class InsertionPoint {
  final Placement placement;
  final int index;

  InsertionPoint(this.placement, this.index);
}

class InsertionPointPickerSearchDelegate
    extends DataBlocSearchDelegate<DataBloc, InsertionPoint> {
  final int movedItemIndex;

  InsertionPointPickerSearchDelegate(this.movedItemIndex);

  @override
  Widget buildListDataWidget(BuildContext context, List<IndexedItem> listData) {
    return ListView.builder(
      itemCount: listData.length,
      itemBuilder: (context, index) {
        final indexedItem = listData[index];

        return ListTile(
          enabled: indexedItem.index != movedItemIndex,
          leading: Padding(
            padding: const EdgeInsets.symmetric(horizontal: 12),
            child: Icon(Icons.arrow_right_alt),
          ),
          title: Text(indexedItem.item),
          onTap: () => _pickPlacement(context, indexedItem.index),
        );
      },
    );
  }

  // Step 3
  Future<void> _pickPlacement(BuildContext context, int index) async {
    final placement = await showDialog<Placement>(
      context: context,
      builder: (context) => SimpleDialog(
        children: [
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              TextButton(
                child: const Text('Before'),
                onPressed: () => Navigator.of(context).pop(Placement.before),
              ),
              TextButton(
                child: const Text('After'),
                onPressed: () => Navigator.of(context).pop(Placement.after),
              ),
            ],
          ),
        ],
      ),
    );

    if (placement != null) {
      close(context, InsertionPoint(placement, index));
    }
  }
}

// DataBloc
// Events

abstract class DataEvent {}

class DataRequested extends DataEvent {}

class DataItemPositionChanged extends DataEvent {
  final int fromIndex;
  final int toIndex;

  DataItemPositionChanged(this.fromIndex, this.toIndex);
}

// States

abstract class DataState {}

class DataInitial extends DataState {}

class DataLoadingInProgress extends DataState {}

class DataLoadingSuccess extends DataState {
  final List<String> listData;

  DataLoadingSuccess(this.listData);
}

// BLoC

class DataBloc extends Bloc<DataEvent, DataState> {
  DataBloc() : super(DataInitial());

  @override
  Stream<DataState> mapEventToState(DataEvent event) async* {
    if (event is DataRequested) {
      yield DataLoadingInProgress();
      await Future.delayed(Duration(milliseconds: 500));
      yield DataLoadingSuccess(List.generate(5, (index) => 'Item $index'));
    } else if (event is DataItemPositionChanged) {
      yield* _mapPositionChanged(event);
    }
  }

  Stream<DataState> _mapPositionChanged(DataItemPositionChanged event) async* {
    final successState = state as DataLoadingSuccess;
    final listData = [...successState.listData];
    final item = listData.removeAt(event.fromIndex);

    var to = event.toIndex;
    if (event.fromIndex < to) {
      // When moving to a later index, the list has just been made smaller
      // by 1 (the removal above) so decrease the target index.
      to -= 1;
    }
    listData.insert(to, item);

    yield DataLoadingSuccess(listData);
  }
}

// DataBlocSearchDelegate

class IndexedItem {
  final String item;
  final int index;

  IndexedItem(this.item, this.index);
}

abstract class DataBlocSearchDelegate<DB extends DataBloc, R>
    extends SearchDelegate<R> {
  Widget buildListDataWidget(BuildContext context, List<IndexedItem> listData);

  @override
  Widget buildLeading(BuildContext context) {
    return IconButton(
      onPressed: () => close(context, null),
      icon: const BackButtonIcon(),
    );
  }

  @override
  List<Widget> buildActions(BuildContext context) {
    return [
      IconButton(
        onPressed: () => query = '',
        icon: Icon(Icons.clear),
      ),
    ];
  }

  @override
  Widget buildSuggestions(BuildContext context) {
    return BlocBuilder<DB, DataState>(
      builder: (context, state) {
        Widget body;
        if (state is DataLoadingSuccess) {
          body = buildListDataWidget(
            context,
            state.listData
                .asMap()
                .entries
                .map((e) => IndexedItem(e.value, e.key))
                .where((indexedItem) => indexedItem.item
                    .toLowerCase()
                    .contains(query.toLowerCase()))
                .toList(),
          );
        } else if (state is DataInitial || state is DataLoadingInProgress) {
          body = Text('Loading...');
        } else {
          body = Text('Invalid state: $state');
        }
        return body;
      },
    );
  }

  @override
  Widget buildResults(BuildContext context) => throw UnimplementedError();
}
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
0
315
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Оказывается, причина в том, что после второго вызова showSearch все виджеты, созданные для отображения пользовательского интерфейса первого, удаляются (это связано с тем, что для внутреннего маршрута maintainState установлено значение false). Это означает, что как только результат второго поиска будет возвращен, и я попытаюсь получить BLoC из контекста, контекст устарел, отсюда и ошибка.

Мы пробовали различные решения, чтобы заставить Flutter сохранять состояние, в том числе AutomaticKeepAliveClientMixin, но ничего не работало.

В конце концов, жизнеспособным решением в нашем случае было немного реструктурировать код и выполнить более глубокий поиск BLoC во втором поиске вместо того, чтобы возвращать результат и выполнять логику перемещения в первом. В нашем реальном приложении у него есть тот недостаток, что нам нужно нести некоторые данные с собой во второй поиск, но у нас нет ничего лучше, чем это...

Вот полностью измененный и работающий код, если кому-то интересно (сравните с первой версией, чтобы увидеть, как именно мы исправили проблему):

import 'dart:async';

import 'package:bloc/bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

void main() {
  runApp(
    BlocProvider(
      create: (context) => DataBloc(),
      child: MaterialApp(
        home: Scaffold(
          appBar: AppBar(
            actions: [
              Builder(
                builder: (context) => IconButton(
                  icon: Icon(Icons.search),
                  onPressed: () {
                    context.read<DataBloc>().add(DataRequested());
                    showSearch(
                      context: context,
                      delegate: SortingSearchDelegate(),
                    );
                  },
                ),
              ),
            ],
          ),
        ),
      ),
    ),
  );
}

// Delegates

// Step 1 - sort by drag & drop (no step 2 necessary) or pick the item to move

class SortingSearchDelegate extends DataBlocSearchDelegate<DataBloc, void> {
  @override
  Widget buildListDataWidget(BuildContext context, List<IndexedItem> data) {
    return ReorderableListView(
      onReorder: (from, to) {
        context.read<DataBloc>().add(DataItemPositionChanged(from, to));
      },
      children: data.map((indexedItem) => SortingTile(indexedItem)).toList(),
    );
  }
}

class SortingTile extends StatelessWidget {
  final IndexedItem indexedItem;

  SortingTile(this.indexedItem) : super(key: ValueKey(indexedItem.item));

  @override
  Widget build(BuildContext context) {
    return ListTile(
      leading: IconButton(
        onPressed: () => _moveItem(context),
        icon: Icon(Icons.swap_vert),
      ),
      title: Text('${indexedItem.item}'),
      trailing: Icon(Icons.drag_handle),
    );
  }

  Future<void> _moveItem(BuildContext context) {
    // Trigger step 2.
    // There is no need to request any data, the same BLoC is reused.
    return showSearch(
      context: context,
      delegate: InsertionPointPickerSearchDelegate(indexedItem.index),
    );
  }
}

// Step 2 - pick placement of picked item in step 1

enum Placement { before, after }

class InsertionPointPickerSearchDelegate
    extends DataBlocSearchDelegate<DataBloc, void> {
  final int movedItemIndex;

  InsertionPointPickerSearchDelegate(this.movedItemIndex);

  @override
  Widget buildListDataWidget(BuildContext context, List<IndexedItem> listData) {
    return ListView.builder(
      itemCount: listData.length,
      itemBuilder: (context, index) {
        final indexedItem = listData[index];

        return ListTile(
          enabled: indexedItem.index != movedItemIndex,
          leading: Padding(
            padding: const EdgeInsets.symmetric(horizontal: 12),
            child: Icon(Icons.arrow_right_alt),
          ),
          title: Text(indexedItem.item),
          onTap: () => _pickPlacement(context, indexedItem.index),
        );
      },
    );
  }

  // Step 3
  Future<void> _pickPlacement(BuildContext context, int index) async {
    final placement = await showDialog<Placement>(
      context: context,
      builder: (context) => SimpleDialog(
        children: [
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              TextButton(
                child: const Text('Before'),
                onPressed: () => Navigator.of(context).pop(Placement.before),
              ),
              TextButton(
                child: const Text('After'),
                onPressed: () => Navigator.of(context).pop(Placement.after),
              ),
            ],
          ),
        ],
      ),
    );

    if (placement != null) {
      if (placement == Placement.after) {
        ++index;
      }
      context
          .read<DataBloc>()
          .add(DataItemPositionChanged(movedItemIndex, index));
      close(context, null);
    }
  }
}

// DataBloc
// Events

abstract class DataEvent {}

class DataRequested extends DataEvent {}

class DataItemPositionChanged extends DataEvent {
  final int fromIndex;
  final int toIndex;

  DataItemPositionChanged(this.fromIndex, this.toIndex);
}

// States

abstract class DataState {}

class DataInitial extends DataState {}

class DataLoadingInProgress extends DataState {}

class DataLoadingSuccess extends DataState {
  final List<String> listData;

  DataLoadingSuccess(this.listData);
}

// BLoC

class DataBloc extends Bloc<DataEvent, DataState> {
  DataBloc() : super(DataInitial());

  @override
  Stream<DataState> mapEventToState(DataEvent event) async* {
    if (event is DataRequested) {
      yield DataLoadingInProgress();
      await Future.delayed(Duration(milliseconds: 500));
      yield DataLoadingSuccess(List.generate(5, (index) => 'Item $index'));
    } else if (event is DataItemPositionChanged) {
      yield* _mapPositionChanged(event);
    }
  }

  Stream<DataState> _mapPositionChanged(DataItemPositionChanged event) async* {
    final successState = state as DataLoadingSuccess;
    final listData = [...successState.listData];
    final item = listData.removeAt(event.fromIndex);

    var to = event.toIndex;
    if (event.fromIndex < to) {
      // When moving to a later index, the list has just been made smaller
      // by 1 (the removal above) so decrease the target index.
      to -= 1;
    }
    listData.insert(to, item);

    yield DataLoadingSuccess(listData);
  }
}

// DataBlocSearchDelegate

class IndexedItem {
  final String item;
  final int index;

  IndexedItem(this.item, this.index);
}

abstract class DataBlocSearchDelegate<DB extends DataBloc, R>
    extends SearchDelegate<R> {
  Widget buildListDataWidget(BuildContext context, List<IndexedItem> listData);

  @override
  Widget buildLeading(BuildContext context) {
    return IconButton(
      onPressed: () => close(context, null),
      icon: const BackButtonIcon(),
    );
  }

  @override
  List<Widget> buildActions(BuildContext context) {
    return [
      IconButton(
        onPressed: () => query = '',
        icon: Icon(Icons.clear),
      ),
    ];
  }

  @override
  Widget buildSuggestions(BuildContext context) {
    return BlocBuilder<DB, DataState>(
      builder: (context, state) {
        Widget body;
        if (state is DataLoadingSuccess) {
          body = buildListDataWidget(
            context,
            state.listData
                .asMap()
                .entries
                .map((e) => IndexedItem(e.value, e.key))
                .where((indexedItem) => indexedItem.item
                    .toLowerCase()
                    .contains(query.toLowerCase()))
                .toList(),
          );
        } else if (state is DataInitial || state is DataLoadingInProgress) {
          body = Text('Loading...');
        } else {
          body = Text('Invalid state: $state');
        }
        return body;
      },
    );
  }

  @override
  Widget buildResults(BuildContext context) => throw UnimplementedError();
}

Другие вопросы по теме