#flutter
#трепетание
Вопрос:
У нас есть пользовательский интерфейс, который показывает список со многими элементами, и мы хотим иметь возможность легко сортировать их. Мы уже используем ReorderableListView
, но поскольку для этого требуется перетаскивание пользователем, это не очень хорошо работает для списков со многими элементами. Мы пришли с идеей использовать поиск для него следующим образом:
- На первом шаге пользователю разрешается изменять порядок элементов путем перетаскивания, но он также может щелкнуть начало
IconButton
, чтобы выбрать элемент «источник» для перемещения. Чтобы помочь быстро выбрать правильный элемент «source», пользователю разрешено выполнять поиск / фильтрацию отображаемых элементов по запросу. - Выбор элемента «источник» на шаге 1 запускает второй шаг: отображается список с теми же данными (с отключенным элементом, выбранным на шаге 1), и пользователь может щелкнуть по началу
IconButton
, чтобы выбрать элемент «цель». Чтобы помочь быстро выбрать правильный «целевой» элемент, пользователю разрешено искать / фильтровать отображаемые элементы по запросу. - Выбор «целевого» элемента запускает шаг 3: отображается небольшое диалоговое окно, в котором пользователь может выбрать, следует ли поместить «исходный» элемент с шага 1 до или после «целевого» элемента с шага 2.
- После выбора срабатывает логика (в моем случае используется блок).
Шаги 1 и 2 реализованы с использованием showSearch
функций Flutters с делегатами, т. Е. Выбор элемента в поиске на шаге 1 запускает новый «дочерний» поиск с новым делегатом. Пожалуйста, взгляните на следующий GIF, где я сначала выбираю «исходный» «Элемент 0» на шаге 1, затем «целевой» «Элемент 4» на шаге 2, а затем «После» на шаге 3:
В общем, это работает так, как мы хотим, если мы передаем экземпляр блока и не ищем его с помощью контекста. Тем не менее, мы хотим использовать контекст, поскольку реальное приложение намного сложнее, и передача блока по кругу нежизнеспособна. Однако использование 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
интересна, остальное — Блок и т. Д. что работает и, вероятно, не имеет значения, но кто знает. Код должен выполняться без проблем с 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
main.dart:
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 amp; 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();
}
Ответ №1:
Оказывается, причина в том, что после выполнения второго showSearch
вызова все виджеты, созданные для отображения пользовательского интерфейса первого, удаляются (что связано с тем, что для внутреннего используемого маршрута maintainState
установлено значение false
). Это означает, что как только возвращается результат второго поиска, и я пытаюсь получить блок из контекста, контекст устаревает, отсюда и ошибка.
Мы пробовали различные решения, чтобы заставить Flutter сохранять состояние, в том числе AutomaticKeepAliveClientMixin
, но ничего не сработало.
В конце концов, жизнеспособным решением в нашем случае было немного реструктурировать код и выполнить поиск по блокам глубже, во втором поиске, вместо того, чтобы возвращать результат и выполнять логику перемещения в первом. В нашем реальном приложении это имеет тот недостаток, что нам нужно переносить некоторые данные с собой во второй поиск, но у нас нет ничего лучше этого…
Вот полный измененный и рабочий код на случай, если кому-то интересно (сравните с первой версией, чтобы точно увидеть, как именно мы исправили проблему):
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 amp; 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();
}