Flutter ListView в чём разница между key: UniqueKey() и GlobalKey()
В ListView передаётся список. Необходимо по тапу удалять элемент из этого ListView. Когда в параметрах буилдера ListView не указан key: или указан но является GlobalKey() то при удалении элемента списка слетает нумерация элементов и список визуально отображается правильно, но если попробывать редактировать какой нибудь элемент, то у такого элемента будут не верные данные. Если же в случае когда в билдере ListView указан какой нибудь уникальный идентификатор, например key: UniqueKey() то поведение элементов ListView становится корректным и предсказуемым, но после удаления любого элемента, список автоматически скроллится в самое начало, до первого его элемента. Хотелось бы удалять элемент из ListView так, что бы его поведение было после корректным, а сам ListView не скролился в самое начало.
Вот тут у меня хранится список который я перадаю в ListView
List<Channel> _editorChannels;
Так я удаляю элемент. Ищу его в списке по уникальному Id и удаляю.
onDelete(Channel c) {
final f = _editorChannels.firstWhere((element) => element.Id == c.Id,
orElse: () => null);
if (f != null) setState(() => _editorChannels.remove(f));
}
Вот так выглядит ListView , он внутри одной из вкладок DefaultTabController, я сокращенно только покажу код одной вкладки, что бы не так много кода было.
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Scaffold(
body: ListView.builder(
key: UniqueKey(),
itemCount: _editorChannels == null
? 0
: _editorChannels.length,
itemBuilder: (context, index) {
final item = _editorChannels[index];
return Card(
shadowColor: Colors.black26,
margin: EdgeInsets.all(3.0),
clipBehavior: Clip.antiAlias,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(2),
),
child: ListTile(
title: Container(
child: Text(
item.Name != null ? item.Name : '',
style: new TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16.0),
)),
subtitle: Text(item.Url),
onTap: () => {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
ChewieDemo(channel: item)),
)
},
isThreeLine: false,
leading: getIconbyId(item.Status),
trailing: PopMenuWidget(
channel: item,
onDelete: () => onDelete(item),
onUpdate: (item) => onUpdate(item),
)),
);
}),
),
)
],
),
Ответы (1 шт):
Использование параметра key: UniqueKey() приводит к пересозданию виджета при каждой перерисовке экрана, т.к. созданное ранее представление виджета сопоставляется с новым по данному ключу, а этот ключ всегда уникален. Что касается ключа GlobalKey, то он особых изменений в механизм сопоставления виджета с отрисованным объектом не вносит, т.к. по сути является глобальной ссылкой на состояние виджета.
На вашем примере использование UniqueKey приводит к полному пересозданию ListView в его первоначальном состоянии, что помимо проблемы с сохранением текущей позиции, ещё и снижает производительность.
Кстати о производительности: созданный с помощью ListView.builder виджет использует оптимизацию и отрисовывает только видимые элементы списка и при прокрутке удаляет далеко ушедшие элементы. И второй момент, вероятно в вашем случае вызывающий "рассогласование" представления с моделью, это то, что при удалении элемента из модели старый элемент списка из представления не удаляется, а используется повторно с новыми аргументами. Если в элементе списка нет элементов со своим состоянием, то проблем это не вызывает. В противном случае stateful-виджет будет по прежнему использовать свое старое состояние, хотя через аргументы ему были переданы данные уже совсем другого элемента списка. В этом можно убедиться на простом примере:
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(home: ListViewPitfall()));
}
class ListViewPitfall extends StatefulWidget {
@override
_ListViewPitfallState createState() => _ListViewPitfallState();
}
class _ListViewPitfallState extends State<ListViewPitfall> {
final _content =
List<String>.generate(64, (index) => "List item №${index + 1}");
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
itemCount: _content.length,
itemBuilder: (context, index) => ListTile(
title: Row(
children: [
Expanded(child: Text(_content[index])),
ClumsyWidget(
// this widget will be recreated on each render
// if you uncomment next line
// key: UniqueKey(),
title: _content[index],
),
SizedBox(width: 6.0),
RaisedButton(
child: Text("delete"),
onPressed: () => setState(() {
_content.removeAt(index);
}),
)
],
),
onTap: () => print(_content[index]),
),
),
);
}
}
class ClumsyWidget extends StatefulWidget {
final String title;
const ClumsyWidget({Key key, this.title}) : super(key: key);
@override
_ClumsyWidgetState createState() => _ClumsyWidgetState();
}
class _ClumsyWidgetState extends State<ClumsyWidget> {
String statefulTitle;
@override
void initState() {
super.initState();
statefulTitle = widget.title;
}
@override
Widget build(BuildContext context) {
return OutlineButton(
onPressed: () {
setState(() {
statefulTitle = widget.title;
});
},
child: Text(statefulTitle),
);
}
}
В левой части строк использован stateless виджет, а в правой, перед кнопкой удаления, stateful. При удалении строки наблюдается рассогласование левой и правой части у расположенных ниже элементов. Если прокрутить список до конца и обратно, то рассогласование исчезает. Stateful виджет сделан кнопкой, при клике которой состояние обновляется из параметров виджета. Кроме того, в коде есть закоментированный фрагмент с использованием UniqueKey в качестве "костыля".