플러터의 메인 레이아웃이라고 할 수 있는
StatelessWidget, StatefulWidget을 보면 다음과 같은 키(Key)가 존재합니다.
해당 Key의 의미는 뭐고 왜 존재하는걸까요?
Key의 역할 확인을 위한 예시 프로젝트 만들기
다음과 같은 UI를 가진 프로젝트를 생성합니다.
위 프로젝트의 동작은 다음과 같습니다.
- Sort Descending 버튼을 클릭하면 해당 리스트의 이름을 기준으로 정렬을 실시한다.
- 정렬 방법은 a -> z 또는 z -> a 이렇게 두 가지의 케이스가 존재한다.
(프로젝트 전체 코드는 제일 아래에 게시하겠습니다. 또는 그냥 직접 만드셔도 상관 없습니다.)
Sort Descending 버튼을 누를 경우 발생하는 일 살펴보기
버튼을 클릭할 경우 다음과 같이 리스트가 재정렬됩니다.
리스트 아이템은 모두 같은 Widget을 사용한다는 것을 기억해두시기 바랍니다.
이 때 각 Widget Tree는 다음과 같이 동작합니다.
위 그림과 같이 Widget Tree 부분에서 리스트의 아이템은 모두 같은 Wdiget을 사용하기 때문에
정렬이 발생하더라도 Widget을 재생성 하는 것이 아닌 "재활용"합니다.
하지만 Element Tree에서는 교체가 일어나지 않습니다.
Widget Tree가 뭔지 모르겠다면 아래의 포스팅을 확인해주시길 바랍니다.
플러터에서 Widget 재활용 하면 State 또한 바뀌는지 확인
그렇다면 Element Tree는 바뀌지 않는 걸까요?
아이템에 다음과 같이 CheckBox를 추가해줍니다.
그리고 CheckBox에 체크를 한 다음 정렬을 해보겠습니다.
CheckBox의 Check는 그 자리에 그대로 있는 것을 알 수 있습니다.
그 이유는 각각의 Widget에 대한 State는 Widget Tree에서 관리하고 있지 않기 때문입니다.
그렇기 때문에 모든 StateWidget에서 다음과 같이 Key를 갖게 하는 이유입니다.
플러터 UI에서 Key 관리하기
그럼 Key가 올바른 대상의 WidgetTree의 요소를 가리키도록 해보겠습니다.
CheckableTodoItem이라는 Item의 StatefulWidget은 다음과 같습니다.
class CheckableTodoItem extends StatefulWidget {
// 여기에 Key가 있음
const CheckableTodoItem(this.text, this.priority, {super.key});
final String text;
final Priority priority;
@override
State<CheckableTodoItem> createState() => _CheckableTodoItemState();
}
이제 CheckableTodoItem에 Key를 넣겠습니다.
CheckableTodoItem은 다음과 같은 위치에서 정의되며 Key는 다음과 같이 정의할 수 있습니다.
Column(
children: [
// for (final todo in _orderedTodos) TodoItem(todo.text, todo.priority),
for (final todo in _orderedTodos)
CheckableTodoItem(
// Key를 셋팅
key: ValueKey(todo.text),
todo.text,
todo.priority,
),
],
)
Key에는 해당 아이템의 고유한 값(ID 같은 것들)을 넣어야합니다.
Key를 만드는 방법에는 다음과 같이 두 가지의 방법이 있습니다.
- ValueKey()를 사용
- ObjectKey()를 사용
ValueKey의 경우
ValueKey를 사용할 경우 해당 요소의 고유 값을 지정해줘야합니다.
만약 위 요소(todo)에서 고유한 id가 있었을 경우 todo.id를 지정해주는 것이 좋습니다.
또한 ValueKey의 경우 해당 Value로만 고유한 값인지 비교를 하면되기 때문에 ObjectKey보다 가볍습니다.
ObjectKey의 경우
ObjectKey의 경우 고유한 값이 존재하지 않고 해당 object나 instance 만으로 값을 판별해야할 경우 사용합니다.
그렇기 때문에 ValueKey보다 비교해야할 데이터가 더 많아서 더 무겁습니다.
위와 같이 설정한 후 다시 테스트 해보면 다음과 같이 됩니다.
체크박스까지 제대로 정렬되는 것을 알 수 있습니다.
전체 코드
Key.dart 파일을 Cloumn 등의 Widget에서 표시하도록 하면 정상적으로 출력됩니다.
Key.dart
class Todo {
const Todo(this.text, this.priority);
final String text;
final Priority priority;
}
class Keys extends StatefulWidget {
const Keys({super.key});
@override
State<Keys> createState() {
return _KeysState();
}
}
class _KeysState extends State<Keys> {
var _order = 'asc';
// List에서 사용할 item 셋팅
final _todos = [
const Todo(
'Learn Flutter',
Priority.urgent,
),
const Todo(
'Practice Flutter',
Priority.normal,
),
const Todo(
'Explore other courses',
Priority.low,
),
];
// 정렬된 List item 반환
List<Todo> get _orderedTodos {
final sortedTodos = List.of(_todos);
sortedTodos.sort((a, b) {
final bComesAfterA = a.text.compareTo(b.text);
return _order == 'asc' ? bComesAfterA : -bComesAfterA;
});
return sortedTodos;
}
void _changeOrder() {
setState(() {
_order = _order == 'asc' ? 'desc' : 'asc';
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Align(
alignment: Alignment.centerRight,
child: TextButton.icon(
onPressed: _changeOrder,
icon: Icon(
_order == 'asc' ? Icons.arrow_downward : Icons.arrow_upward,
),
label: Text('Sort ${_order == 'asc' ? 'Descending' : 'Ascending'}'),
),
),
Expanded(
child: Column(
children: [
// for (final todo in _orderedTodos) TodoItem(todo.text, todo.priority),
for (final todo in _orderedTodos)
CheckableTodoItem(
key: ValueKey(todo.text),
todo.text,
todo.priority,
),
],
),
),
],
);
}
}
Checkable_todo_item.dart
enum Priority { urgent, normal, low }
/// Check 할 수 있는 TODO 리스트 아이템
class CheckableTodoItem extends StatefulWidget {
const CheckableTodoItem(this.text, this.priority, {super.key});
final String text;
final Priority priority;
@override
State<CheckableTodoItem> createState() => _CheckableTodoItemState();
}
class _CheckableTodoItemState extends State<CheckableTodoItem> {
var _done = false;
void _setDone(bool? isChecked) {
setState(() {
_done = isChecked ?? false;
});
}
@override
Widget build(BuildContext context) {
var icon = Icons.low_priority;
if (widget.priority == Priority.urgent) {
icon = Icons.notifications_active;
}
if (widget.priority == Priority.normal) {
icon = Icons.list;
}
return Padding(
padding: const EdgeInsets.all(8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Checkbox(value: _done, onChanged: _setDone),
const SizedBox(width: 6),
Icon(icon),
const SizedBox(width: 12),
Text(widget.text),
],
),
);
}
}
todo_item.dart
enum Priority { urgent, normal, low }
/// Check 기능은 없는 TODO 리스트 아이템
class TodoItem extends StatelessWidget {
const TodoItem(this.text, this.priority, {super.key});
final String text;
final Priority priority;
@override
Widget build(BuildContext context) {
var icon = Icons.low_priority;
if (priority == Priority.urgent) {
icon = Icons.notifications_active;
}
if (priority == Priority.normal) {
icon = Icons.list;
}
return Padding(
padding: const EdgeInsets.all(8),
child: Row(
children: [
Icon(icon),
const SizedBox(width: 12),
Text(text),
],
),
);
}
}
'플러터(flutter)' 카테고리의 다른 글
플러터의 Riverpod 사용 방법 기초부터 자세히 설명 - 2 (0) | 2023.12.07 |
---|---|
플러터의 Riverpod 사용 방법 기초부터 자세히 설명 - 1 (2) | 2023.12.06 |
플러터 - UI 업데이트를 최적화 하는 방법, UI Tree란? (0) | 2023.09.22 |
flutter에서 화면 회전 하지 못하게 막는 방법 (0) | 2023.08.29 |
flutter_dotenv을 사용해서 local에 API Key 보관하기 (0) | 2023.08.11 |
"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."
댓글