Keys In Flutter - UniqueKey, ValueKey, ObjectKey, PageStorageKey, GlobalKey
<aside> 💡 Key란 위젯, 엘리멘트, 노드들의 식별자다.
</aside>
파라미터가 없는 기본 생성자만 존재한다.
앱의 모든 위젯들을 각각 식별할 수 있도록 해주는 키의 종류다.
위젯트리를 이동할 때 상태를 보존한다.
위젯들을 재배치하거나 추가하거나 삭제할 때 사용한다.
같은 값과 같은 타입을 가진 위젯들이 위젯 트리에 존재할 때 그 위젯들을 개별로 구분하고자 할 때 유용하다.
또한 DB 컬렉션에서 모든 위젯들을 고유하게 관리하고 있지 않을 때 UniqueKey를 특정 위젯에 할당하는 식으로 사용할 수도 있다.
예를 들어, 버튼을 누르면 이모지의 위치를 교체해주는 프로그램이 있다고 하자.
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
List<Widget> emojis = [
GetEmoji(emoji: "😎"),
GetEmoji(emoji: "🤠")
];
swapEmoji() {
setState(() {
emojis.insert(1, emojis.removeAt(0));
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SizedBox.expand(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: emojis,
),
SizedBox(
height: 20,
),
ElevatedButton(
onPressed: swapEmoji,
child: Text("Swap"),
)
],
),
));
}
}
class GetEmoji extends StatelessWidget {
GetEmoji({required this.emoji});
String emoji;
@override
Widget build(BuildContext context) {
return Text(
emoji,
style: TextStyle(
fontSize: 100,
),
);
}
}
하지만 GetEmoji
StatelessWidget을 StatefulWidget 로 변경하고 상태값을 저장하고자 할 때 문제가 발생한다.
class GetEmoji extends StatefulWidget {
GetEmoji({required this.emg});
String emg;
@override
_GetEmojiState createState() => _GetEmojiState();
}
class _GetEmojiState extends State<GetEmoji> {
late String emoji;
@override
void initState() {
super.initState();
emoji = widget.emg;
}
@override
Widget build(BuildContext context) {
return Text(
emoji,
style: TextStyle(
fontSize: 100,
),
);
}
}
위 코드에서는버튼을 눌러도 동작하지 않는다.
왜냐하면 플러터는 runtimeType
에서 반환되는 타입과 key
로 위젯을 구분하기 때문이다.
위의 위젯 리스트에서 두 개의 GetEmoji 위젯(😎과 🤠)을 가지고 있다. 이 때 swap 버튼을 누르면, 플러터는 ElementTree
를 확인한다.
여기서 엘리먼트 트리는 각 위젯의 타입과 자식 요소의 참조만을 가지고 있다는 점을 알아야 한다.
위젯 트리 상에서 두 이모지의 위치는 변경 되지만, 엘리먼트 트리 상에서는 똑같은 Row Widget에, 그 자식으로 똑같은 Text Widget을 가지고 있는 셈이다. 따라서 위젯과 트리 간의 참조만 업데이트하고 UI를 변경할 이유가 없는 것이다.
이런 경우를 위해, 두 개의 Text Widget은 각각 다른 고유한 ID를 가지고 있음을 알려서 같은 타입이여도 플러터가 이를 구분할 수 있도록 존재하는게 UniqueKey
이다.
class GetEmoji extends StatefulWidget {
GetEmoji({required this.emg, required Key key}) : super(key: key);
String emg;
@override
_GetEmojiState createState() => _GetEmojiState();
}
List<Widget> emojis = [
GetEmoji(
emg: "😎",
key: **UniqueKey()**,
),
GetEmoji(
emg: "🤠",
key: **UniqueKey()**,
),
];
여기서 주의해야 할 점이 하나 있다. 그것은 만약에 key를 이용해 위젯을 구분하고자 할 경우, 반드시 위젯 서브트리의 최상단에 키를 넣어줘야 한다는 점이다.
만약 그렇지 않을 경우의 예시를 들어 보면, 상단 코드에 Container 위젯을 감싸고, 랜덤하게 컬러를 설정한다고 생각해보자. 그 뒤에 이해를 돕기 위해 Padding 위젯으로 한번 더 감싸준다.
이 상황에서 Container에 키를 넣어주고 Swap 버튼을 눌러보자.
예상 대로라면 키를 넣어줬으니 한번 정해진 배경색이 변하지 않고 서로 바뀌기만 해야할 것이다. 하지만 결과는
이렇게 매번 배경색이 바뀐다. 엄밀히 따지면 2가지 이모지만 사용해서 이모지끼리는 잘 바뀌는 것 같아 보일 뿐 완전히 새로운 위젯으로 갈아끼워지는 작업이 계속 일어나는 중이다.
그 이유에 대해 말하자면, 우선 버튼을 누르기 전 위젯 트리는 다음과 같다.
이후 버튼을 누른 다음 트리는 다음과 같을 것이다.
우선 플러터는 최상단 트리인 Row 위젯을 본다. 별도로 키를 넣어준적도 없고 실제로 변경되지도 않았으니 일치할 것이다.
다음으로 1단계 내려간 서브트리인 Padding 위젯 2개를 살펴본다. 변경되지 않았으니 일치할 것이다.
다음으로 1단계 더 내려가 Container 위젯을 본다.