View
이번에도 30분동안 싸운 간단한 문제가 있었다. 이번에 내가 구현하려 했던 부분은 데이터 삭제 후 뷰 새로고침이였다. 되게 간단한 문제였는데, 요녀석이랑 싸우게 된 이유는 자식 위젯에서 데이터를 지우는 로직을 호출하고, 부모 위젯에서 뷰를 업데이트를 해야한다는 점이였다.
ㅤ
ㅤ
처음에는 단순하게 자식 위젯에 있는 버튼을 누르면 로직을 처리하고, 이에 따라 setState를 호출해 뷰를 업데이트해주려고 했다. 그런데, 분명 setState 함수까지 호출이 잘 되었다고 나오는데 뷰는 제대로 업데이트가 되지 않는 문제가 있었다.
// 자식 위젯 코드
onPressed() async {
await removeLogic()
.whenComplete(() {
setState(() {});
});
}
ㅤ
여기에서 갖가지 위치에 로그를 찍어보기도 하고, await then whenComplete 로 계속 실행 순서랑 비동기 제어를 바꿔보기도 했지만, 도대체 뭐가 문제인지 계속 고민만 하고 있었다.
ㅤ
그런데, setState 함수를 쓰면 단순히 호출한 위젯의 상태에 대해 뷰를 다시 그려주는건가?? 라는 생각이 들었다. 상태가 변경되었음을 알려주는 함수라고만 생각하고 여기저기에서 뷰를 새로 그려줘야하는 시점에 쓰는 함수로 써왔는데, 내 위젯에 대해서만 적용되는 함수라고는 생각을 못했던 것 같다. ㅜㅜㅜ
ㅤ
그래서, 요런 방식으로 함수 로직에 대한 노선을 변경하였다.
ㅤ
여기를 구현하기 위해서는 자식 위젯에서 부모 위젯의 state에 접근할 수 있어야 한다. 이를 위해서 2가지 방법이 있다.
- 부모 위젯이 자식 위젯에게 파라미터로 state를 전달하기
- 자식 위젯에서 context에서 부모 위젯 state를 찾기
ㅤ
여기에서 나는 2번째 방법을 선택했다. 첫 번째 방법도 물론 나쁜 구조는 아니지만, 위젯의 파라미터로 state를 넘기게 되는 상황에서의 인자가 지저분하게 보일 것 같다는 이유로 결정했다. context를 통해 객체에 접근하는 방법을 써보고 싶었던 것이 더 크다 사실 ㅎㅎ 둘 다 한 번씩 써봐야 뭐가 더 나은지 비교를 하지.
ㅤ
해당 함수의 이름은 다음과 같아!
context.findAncestorStateOfType<State_클래스명>();
context에서 부모 위젯에 접근하기 위해서는 이 함수를 사용해주면 된다. context 트리를 타고 부모 위젯 중 특정 타입을 가진 위젯을 찾아반환하는 함수로, 클래스 이름으로 타입을 찾기 때문에 바로 윗 부모 뿐만 아니라 트리 상의 조상의 상태는 모두 접근이 가능하다!!
context.findAncestorStateOfType<State_클래스명>();
ㅤ
여기에서 사용하는 State 클래스는 StatefulWidget을 만들었을 때 생성되는 그 상태를 말한다. 기본적으로는 private 으로 클래스가 숨겨져있지만, 이 경우에는 자식 위젯에서 이 클래스를 참조해야하기 때문에, State 앞에 기본적으로 붙어있는 언더스코어 _ 를 지워줘야 한다.
class SmapleWidget extends StatefulWidget {
const SmapleWidget({super.key});
@override
State<SmapleWidget> createState() => SmapleWidgetState();
}
// 요거요거!!
class SmapleWidgetState extends State<SmapleWidget> {
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}
ㅤ
이를 바탕으로 아래처럼 자식 위젯에서 버튼이 눌렸을 때, context 트리를 타고 부모 위젯 중 특정 타입을 가진 위젯을 찾아, 해당 부모 위젯에서 setState 함수를 호출하는 로직을 아래처럼 작성했다.
onPressed: () async {
if (entity.userId != null) {
if (mounted) {
await provider.removeFromFriendList(targetUid: entity.userId!);
context
.findAncestorStateOfType<FriendListScreenState>()
?.setState(() {});
} else {
print("unmounted");
}
}
},
ㅤㅤ
다만 내가 요렇게 작성한 상황에서는 async 블럭 내에 있는 await 함수 뒤에서 호출되었기 때문에, 비동기로 인한 context 참조 오류가 발생할 수 있어 위험하다는 경고가 발생한다. 요 함수는 await 함수가 실행되기 전에 호출이 되어야한다!
ㅤ
그래서 아래처럼 요렇게 실행해주었다. 우선 부모의 state 값을 받아와놓고, await 비동기 함수를 호출해 로직을 처리한 다음, 필요한 시점에 저장해둔 state에 setState를 호출하는 방식으로 코드를 작성해주면 된다!
onPressed: () async {
if (entity.userId != null) {
final parentState =
context.findAncestorStateOfType<FriendListScreenState>();
await provider.removeFromFriendList(targetUid: entity.userId!);
if (mounted) {
parentState?.setState(() {});
}
}
},
ㅤ
그러면 아래처럼 수정한 데이터에 대해 부모 위젯이 정상적으로 화면을 새로 그려야 한다는 명령을 받고 의도한대로 작동하게 된다 😊
ㅤ
+)
글을 작성하다보니, 또 다른 방법 하나가 있었던게 생각났다.
뷰에서 데이터의 상태 (state)가 변경되는 것을 감지하여 화면을 그려주도록 코드를 작성해뒀는데 (Like MVVM), 여기에서는 List 내에 있는 원소를 수정한 것이지, 리스트 데이터 자체를 수정한 것이 아니라서 state 변화가 일어났음을 감지하지 못하고 있었다. 그래서, 뷰모델 쪽에서 부모 위젯에게 변경에 대한 notification을 보내서 뷰를 업데이트시키는 방법도 있었다. 이렇게 해주면 된다.
// 데이터 변경 함수 내에서
{
state의 List에서 데이터 지우기
state = state.copyWith(newData)
}
그런데 단순히 뷰를 새로 그리기 위해서 내가 가지고 있는 많은 데이터 (사용자 데이터 리스트)를 copy paste 해야한다는게 비용적인 측면에서 부담스럽지 않을까? 생각되어서 이렇게 해주지는 않았다. 그래도 구조상으로는 가장 깔끔했을 것 같다 ㅋㅋㅋ
샤라웃
https://roseline.oopy.io/dev/flutter-tips/findancestorstateoftype
'Develop > Flutter 개발' 카테고리의 다른 글
[Flutter] Apple 로그인 창 Modal 안뜨는 경우 / AuthorizationErrorCode.unknown error 1000. (0) | 2024.06.14 |
---|---|
[Flutter] Visibility 위젯의 maintainState 프로퍼티, Offstage 위젯 (0) | 2024.06.05 |
[Flutter] Firebase에서 닉네임 문자열 필드에 대한 검색(인 척 하는) Query 기능 구현 (0) | 2024.05.22 |
[Flutter] map 함수는 단순히 리스트를 순회하는 함수가 아니다 (0) | 2024.05.18 |
[Flutter] 자연스러운 비동기 UI를 위해 FutureBuilder 사용하기 (0) | 2024.05.11 |