Row, Column, 특히 ListView에서 데이터들의 순서나 위젯 순서를 바꿔야 할 때가 있다.
예를 들어보자.
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
List<Widget> _tiles = [_Tile(), _Tile()];
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: SafeArea(
child: Center(child: Row(children: _tiles)),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.error),
onPressed: () {
_tiles.insert(1, _tiles.removeAt(0));
setState(() {});
},
),
),
);
}
}
class _Tile extends StatelessWidget {
_Tile({Key? key}) : super(key: key);
final Color color = Color(Random().nextInt(0xffffffff));
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(50),
color: color,
child: FlutterLogo(),
);
}
}
_Tile
클래스를 StatefulWidget
으로 변경하면 어떤 일이 일어날까?
...
class _Tile extends StatefulWidget {
_Tile({Key? key}) : super(key: key);
State<_Tile> createState() => _TileState();
}
class _TileState extends State<_Tile> {
final Color color = Color(Random().nextInt(0xffffffff));
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(50),
color: color,
child: FlutterLogo(),
);
}
}
_Tile
클래스를 호출하는 부분에 key값을 넣어주자.
서로의 위젯을 구분하기만 하면 되는 key값이니 UniqueKey()
를 활용해보자
...
List<Widget> _tiles = [_Tile(key: UniqueKey()), _Tile(key: UniqueKey())];
...
StatefulWidget
을 상속한 하위 위젯이 있을 경우 key를 이용하여 해결하였다.
우리가 key를 사용해야 하는 상황은 이것 말고 없다고 봐도 무방하다.
만약 list의 모든 위젯이 StatelessWidget
이라면 굳이 key를 넣어주지 않아도 된다.
왜 StatefulWidget인 경우 key를 넣어주어야 할까?
플러터의 모든 위젯은 @immutable 어노테이션을 가지고 있다.
위젯은 불변하지만 UI는 유저와 수 많은 상호작용을 하며 데이터가 변경되며 업데이트된다.
이를 해결하기 위해 플러터에서는 위젯트리를 통해 UI를 관리한다.
플러터에서는 세 가지의 트리로 위젯을 관리한다
widget의 속성에 대한 정보를 관리한다.
부모자식과 관련된 정보, 즉 생명주기를 관리한다.
플러터 앱의 골격으로 볼 수 있다.
이 정보가 없다면 상위/하위 widget에 대한 정보를 얻을 수 없고, 그로 인한 widget tree를 변경해 UI를 업데이트하는 것이 힘들어진다.
widget에 대한 Size, Layout을 관리한다.
어떻게 화면에 그릴지에 대한 로직이 담겨있다.
기타 렌더링, 특히 위젯트리 변경 시 렌더링에 관한 설명은 아래 링크 참고
https://medium.com/@kimdohun0104/%ED%94%8C%EB%9F%AC%ED%84%B0%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%9C%84%EC%A0%AF%EC%9D%84-%EB%A0%8C%EB%8D%94%EB%A7%81%ED%95%A0%EA%B9%8C-%EB%B0%9C%ED%91%9C-%EC%98%81%EC%83%81-70a726f4e53e
위에서 확인한 바와 같이 플러터는 모든 widget마다 Element를 생성한다.
이 안에 대응되는 wiget의 타입 정보와 자식 element에 대한 참조를 가지고 있다.
위 예제에서 Row widget은 자식에 대한 정렬된 집합을 가지고 있다.
아래 _Tile Class의 순서를 바꾸면, 플러터는 Element 트리를 살펴보며 구조가 변경되었는지 확인한다.
Row widget부터 시작하여 자식 widget으로 이동하며 검사를 수행한다.
Element는 대응되는 widget의 Type과 Key가 이전의 widget과 동일한지 확인한다.
만약 동일하다면 widget에 대한 참조를 갱신한다.
StatelessWidget 버전에서는 key를 가지지 않으므로 플러터는 단지 Type만 확인한다.
하지만 StatefulWidget의 경우 내부적인 Element 트리 구조가 약간 다르다.
이전과 동일하게 Element와 Widget이 존재하고, 추가로 State Object가 따로 존재한다.
색상정보는 Widget트리가 아니라 State Object에 저장된다.
Stateful인 타일의 순서를 바꿀 때, 마찬가지로 플러터는 Element 트리를 검사한다.
첫째로 Row위젯의 타입을 검사하는데, 변경된 사항이 없으니 참조를 업데이트한다.
두번째로, Row의 첫번째 child인 _Tile Element의 타입을 검사한다. Type이 동일하니 Widget의 참조를 업데이트한다.
마찬가지로 Row의 두번째 child 또한 동일한 작업이 진행된다.
widget의 순서가 변경되었지만 Element 트리의 순서는 변경되지 않고 widget에 대한 참조만 업데이트되었다.
플러터는 widget을 화면에 출력하기 위해 Element 트리와 State Object의 정보를 사용한다. 따라서 위젯의 순서가 변경됨에도 출력되는 화면은 변경되지 않는다.
이때는 Stateful인 타일에 Key를 추가하면 된다.
위젯의 순서를 변경하면 위와 같은 검사를 하게 되는데, Tile Element에서는 element의 Key값과 Widget값을 서로 비교하게 되고, 서로 다르다는 것을 발견한다.
플러터는 불일치가 발견된 Tile Element를 비활성화하고 Element트리에서 삭제한다.
불일치가 발견된 Row의 child widget들을 탐색하면서 key가 일치하는 Element를 찾기 시작한다.
일치하는 것을 찾게 되면 해당 widget을 참조로 업데이트한다.
이 뿐만 아니라 List 내 요소 변경 등 Statful widget의 순서나 개수를 변경할 때 Key가 중요한 역할을 한다. 색과 같은 상태 뿐 아니라 애니메이션, 사용자Input, 스크롤 위치 등 모두 상태와 연관이 있다.
_Tile widget가 Padding Widget에 감싸져 있거나, 여러 종류의 Key를 알아보려면
https://nsinc.tistory.com/214
를 잘 살펴보자.