Future 데이터들
다루기 너무나도 까다로운 Future
Future 변수들을 사용하여 비동기 작업을 하다보면 가장 힘든 부분이 화면에 그려주는 부분이다.
데이터가 바로 들어온다고 가정하고 화면을 그리게 되면 빨간색 에러 화면을 마주하기 쉽상이다.
데이터가 완성되는게 아무리 빠르다고 해도 화면을 그리는 부분이 별도의 장치가 없다면 선행되기 때문에 에러가 쉽게 발생한다.
데이터가 들어오는걸 기다리는 동안에…
상태관리로 전부 처리하자
그렇기에 데이터가 들어오는것을 기다리며 그 동안에는 로딩 화면을 보여주거나 빈 화면을 보여줘야 하는데, Stateful Widget
이나 GetX
를 사용하여 상태관리를 하게 되면 이 부분이 상당히 귀찮아 질 수 있다.
필자의 경우에는 아래와 같은 로직으로 페이지를 로딩하게 했었다. ▼
일반적인 상태관리 로직과 같은데, 여기에 비동기적인 데이터 처리가 들어간 것 뿐이다.
하지만 이렇게 관리하면 Obx위젯(GetX 기준)이 들어가게 되면서 코드의 들여쓰기가 깊어지게 된다.
상태관리를 하면서 예외처리도 해야하기 때문에 코드가 조금 더 복잡해진다.
- 데이터가 로딩 되었을 때
- 데이터가 로딩 되지 않았을 때
- 데이터 로딩이 실패했을 때
- 등등…
여러가지 상황에 대해 대비를 해야하는데, 이를 View에 작성하기에는 상당히 코드가 지저분해져 Controller에
if else
나 별도의 다른 enum
을 설정하여 예외를 처리해야하는데, 이러면 오버헤드가 커지게 된다.
FutureBuilder를 사용하면 모든게 해결
이런 오버헤드들을 전부 처리하여 클래스화 해 놓으면 편하겠다는 생각이 든다.
그런데 이걸 개발자가 직접 할 필요는 없다.
이미 FutureBuilder 클래스로 만들어져있기 때문이다. ▼
FutureBuilder 클래스를 통해 비동기적으로 데이터를 불러오고 화면을 그릴 수 있게 된다.
이렇게 Switch문으로 더 간결하게 어떤 상황에서 어떤 위젯을 그릴 지 짤 수 있게 된다. ▼
FutureBuilder
주의사항
FutureBuilder의 공식문서를 보면 몇 가지 주의점을 이야기하고 있다.
Managing Future
Future는 State.initState
, State.didUpdateWidget
또는 State.didChangeDependencies
중 하나에서 미리 얻어져야 한다.
FutureBuilder를 구성하는 동안 State.build
또는 StatelessWidget.build
메서드 호출 중에 생성되어서는 안된다.
만약 Future가 FutureBuilder와 동시에 생성되었다면, FutureBuilder의 부모가 다시 빌드될 때마다 비동기 작업이 다시 시작된다.
Timing
State.setState
를 통해 future가 완성이 될 때 위젯이 다시 빌드되게 예정되어있지만, 그렇지 않은 경우 빌드되는 시점은 미래의 시점과 독립적이게 된다.
빌더 콜백은 Flutter 파이프라인의 재량에 따라 호출되며, 미래와의 상호 작용을 나타내는 스냅숏의 시간 종속 하위 시퀀스 중 일부만 수신한다.
부작용은 새로운 미래를 제공하더라도 이미 완료된 미래를 FutureBuilder에 동기적으로 확인할 수 없기 때문에 ConnectionState.waiting 상태에서 단일 프레임이 발생한다는 것이다.
사용 예제
간단한 예제와 함께 GetX에서 어떻게 사용하는 지 확인해보자.
마이페이지에서 정보를 가져오는 예제 전체 코드다.
실제 마이페이지에서는 하나의 데이터만 비동기적으로 불러오는데, 기본적인 로직은 같기에 같은 데이터를 여러개 불러오는 방식으로 바꾼 뒤 예제로 사용했다. ▼
class MyPageController {
final mainFuture = Future.wait([]).obs;
final myPageInfo1 = MyPageInfo().obs;
final myPageInfo2 = MyPageInfo().obs;
final myPageInfoFuture1 = Future.value(MyPageInfo()).obs;
final myPageInfoFuture2 = Future.value(MyPageInfo()).obs;
@override
onInit() {
super.onInit();
updateMainFuture();
}
updateMainFuture() {
getMyPageInfo();
mainFuture.value = Future.wait([myPageInfoFuture1.value,
myPageInfoFuture2.value,]);
}
assignFutures(List data) {
final datas = [myPageInfo1, myPageInfo2];
for (var element in data) {
datas[data.indexOf(element)].value = element;
}
}
getMyPageInfo() {
myPageInfoFuture1.value = UserService(dio).getMyPageInfo());
myPageInfoFuture2.value = UserService(dio).getMyPageInfo());
}
}
데이터를 받아오고, 이를 할당해주는 서비스 코드는 프로젝트에서 사용하는, 외부에 공개되면 안되는 API 주소가 담겨있어서 제공할수 없다.
간단하게 테스트를 해보고 싶다면 아래의 API를 받아서 사용해보는 것을 추천한다. ▼
그럼 이제 위의 코드를 하나씩 뜯어보자.
필드
총 3가지 필드가 존재하는데, 각 필드는 다음과 같다.
- mainFuture
Future.wait를 통해 여러 개의 Future 데이터들을 비동기적으로 처리할 수 있다. - myPageInfoFuture1, 2
Future 데이터로, 데이터를 비동기적으로 받아오는데 사용한다. - myPageInfo1, 2
myPageInfoFutre1, 2가 성공적으로 불러와지면, 이 데이터들을 myPageInfo1, 2에 옮긴다.
이후 View에서는 해당 데이터들을 사용하여 위젯에 보여주거나 데이터를 처리한다.
이렇게 데이터를 받아오는 Future 필드와 직접 사용하는 필드를 구분하여 사용한다.
데이터를 받아오는 Future 필드는 mainFuture를 이용하여 전부 불러와지기 전에는 사용을 못하게 설정했다.
getMyPageInfo()
필드에 대해 알았다면, 다음은 Future 필드들에 값을 채우는 메소드를 볼 차례다. ▼
getMyPageInfo() {
myPageInfoFuture1.value = UserService(dio).getMyPageInfo();
myPageInfoFuture2.value = UserService(dio).getMyPageInfo();
}
getMyPageInfo()
메소드는 프로젝트의 API 서비스를 통해 데이터를 받아오는 메소드다.
(해당 코드의 UserService(dio).getMyPageInfo();
는 프로젝트의 서비스 코드이니 본인의 코드로 수정을 해야한다.)
updateMainFuture()
getMyPageInfo()
메소드를 통해 데이터를 받아왔다면, mainFuture에 할당할 차례다. ▼
updateMainFuture() {
getMyPageInfo();
mainFuture.value = Future.wait([myPageInfoFuture1.value,
myPageInfoFuture2.value,]);
}
데이터가 성공적으로 다 불러와졌는지 mainFuture에 데이터를 할당함으로 판단할 수 있다.
assignFutures(List data)
이후 데이터를 다 받아왔다면 assignFutures() 메소드를 통해 직접 사용하는 필드들에 Future 데이터들을 할당해준다. ▼
assignFutures(List data) {
final datas = [myPageInfo1, myPageInfo2];
for (var element in data) {
datas[data.indexOf(element)].value = element;
}
}
해당 메소드는 MyPage View에서 사용이 된다.
여기까지 메소드는 준비가 끝났다.
이제 View에 가서 이를 사용하여 FutureBuilder위젯에 넣어주면 된다.
MyPage View
아래는 조금은 많이 생략되었지만, 프로젝트에서 사용한 MyPage View의 Body
부분이다. ▼
FutureBuilder(
future: controller.mainFuture.value,
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.active:
case ConnectionState.waiting:
return const Loading();
case ConnectionState.done:
if (snapshot.hasError) return Container();
controller.assignFutures((snapshot.data! as List));
return Column(
children: [
//Widgets ...
],
);
case ConnectionState.none:
return const Loading();
}
}
);
FutureBuilder의 각 필드 부분을 보자.
가장 먼저 보이는 부분인 future 매개변수를 우리가 사용하는 future 필드로 채워줘야 한다. ▼
future: controller.mainFuture.value,
우리는 여러 개의 Future 데이터들을 다룰 것이기에, mainFuture를 넣어준다.
이제 FutureBuilder는 mainFuture.value 값을 보고 위젯을 그릴 지 말 지를 결정한다.
다음은 builder 매개변수다. ▼
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.active:
case ConnectionState.waiting:
return const Loading();
case ConnectionState.done:
if (snapshot.hasError) return Container();
controller.assignFutures((snapshot.data! as List));
return Column(
children: [
//Widgets ...
],
);
case ConnectionState.none:
return const Loading();
}
}
builder의 함수에는 AsyncSnapshot<dynamic> snapshot
이라는 평소에 잘 보지 못했던 매개변수가 들어있다.
우리는 저 snapshot의 필드인 connectionState를 통해 연결 상태를 파악할 수 있게 된다.
ConnectionState에는 4가지 상태가 있다.
첫번째가 active
상태로, 어떤한 값도 받아오지 않았을 때를 말한다.
사실상 통신이 이제 막 시작되었음을 알려주는 상태이다. ▼
case ConnectionState.active:
현재로서 통신은 무조건 이루어지기에 해당 case는 바로 넘어가게 설정해두었다.
두번째는 waiting
상태로, 데이터를 받아오고 있는 상태를 말한다. ▼
case ConnectionState.waiting:
return const Loading();
데이터를 받아오고 있는 중에는 진행이 되고 있다고 알려줘야하기 때문에 Loading() 위젯으로 현재 데이터가 로딩중임을 알려준다.
세번째는 done
상태로, 데이터가 전부 받아와졌음을 말한다. ▼
case ConnectionState.done:
if (snapshot.hasError) return Container();
controller.assignFutures((snapshot.data! as List));
return Column(
children: [
//Widgets ...
],
);
이 때부터 데이터를 처리하기 시작하면 되는데, 만약에 데이터가 잘못 받아와졌을 수 있으니 snopshot.hasError를 통해 에러를 확인한다.
만약에 데이터에 이상이 있다면 아무화면을 안보여주게 Container()를 반환하게 했다.
그게 아니라면 데이터를 이용해도 되는 것이니, 이전에 작성해두었던 Future데이터들을 실제 사용 필드에 할당해주는 assignFutures메소드를 수행한다. ▼
controller.assignFutures((snapshot.data! as List));
전부 할당이 되었다면 마지막으로 사용자에게 보여줄 위젯을 반환한다. ▼
return Column(
children: [
//Widgets ...
],
);
마지막으로 none
상태로, 받아온 데이터가 null일 때 이다. ▼
case ConnectionState.none:
return Container();
null 데이터를 처리하면 안되기 때문에 아무 화면도 안보여주게 Container()
를 반환하게 했다.
이렇게 각 상태에 맞게 데이터를 처리하고 보여주면 여러 개의 비동기 데이터를 쉽게 보여줄 수 있다. ▼
마치며
이전에는 상태관리를 전부 직접해줬는데, FutureBuilder로 상태관리를 해주니 관리하기가 더 편해졌다.
지금의 마이페이지의 경우에는 받아오는 데이터가 하나지만, 위의 템플릿대로 하니 조금 더 파악하기가 쉬워져서 데이터가 하나일 때도 위의 방식대로 데이터를 받아오게 했다.
많이 알 수록 더 효율적으로 코드를 짤 수 있게 되는 거 같다.
'Develop > Flutter' 카테고리의 다른 글
[Flutter][Widget] FloatingActionButton 위젯 (0) | 2024.02.12 |
---|---|
[Flutter][Widget][Package] WebView Widget(4.2.x) (0) | 2024.02.12 |
[Flutter] Dart의 Single Quote와 Double Quote (0) | 2024.01.31 |
[Flutter] 파이어베이스 이메일 인증에서 생긴 문제 (0) | 2024.01.23 |
[Flutter] Dart 비동기 프로그래밍 찍먹 (0) | 2024.01.23 |