API 응답값을 UI에 출력하기 위해 FutureBuilder
, StreamBuilder
를 한 번쯤은 사용해 본 적이 있을 것이다. 오늘은 FutureBuilder
, StreamBuilder
에서 사용되는 AsyncSnapshot
의 문제점을 알아 보고 더 나은 대안인 AsyncValue
에 대해 알아 보자.
StreamBuilder<Item>(
stream: getData(),
builder: (context, snapshot) {
// 로딩의 경우
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasData) {
// 데이터를 가지고 있는 경우
final item = snapshot.data!;
return ListTile(
title: Text(item.title),
);
} else if (snapshot.hasError) {
// 에러가 발생한 경우
final error = snapshot.error!;
return Text(error.toString());
}
},
);
StreamBuilder
는 위처럼 사용해왔을 것이다.
개발자가 직접 builder
내부에서 로딩(ConnectionState.waiting
)일 때, 데이터가 있을 때(snapshot.hasData
), 에러일 때(snapshot.hasError
)를 if-else 문으로 분기처리해야 한다.
그 이유는 AsyncSnapshot
의 내부 구현을 살펴보면 알 수 있다.
class AsyncSnapshot<T> {
final ConnectionState connectionState;
final T? data;
final Object? error;
final StackTrace? stackTrace;
bool get hasData => data != null;
bool get hasError => error != null;
}
connectionState
, data
, error
, stackTrace
변수가 모두 상호 배타적이지 않고 독립적이기 때문에 개발자가 직접 if-else로 분기 처리를 해야하는 상황이 발생한 것이다.
반면 AsyncValue를 사용하는 코드를 살펴보자.
asyncValue.when(
// 데이터를 가지고 있는 경우
data: (item) => ListTile(
title: Text(item.title),
subtitle: Text(item.description),
),
// 로딩의 경우
loading: () => const Center(child: CircularProgressIndicator()),
// 에러가 발생한 경우
error: (e, st) => Center(child: Text(e.toString())),
);
코드가 훨씬 간단해지고 가독성도 좋아졌다. 또한 when
메서드의 내부 구현을 살펴보면
R when<R>({
bool skipLoadingOnReload = false,
bool skipLoadingOnRefresh = true,
bool skipError = false,
required R Function(T data) data,
required R Function(Object error, StackTrace stackTrace) error,
required R Function() loading,
}) {
if (isLoading) {
bool skip;
if (isRefreshing) {
skip = skipLoadingOnRefresh;
} else if (isReloading) {
skip = skipLoadingOnReload;
} else {
skip = false;
}
if (!skip) return loading();
}
if (hasError && (!hasValue || !skipError)) {
return error(this.error!, stackTrace!);
}
return data(requireValue);
}
AsyncSnapshot
과는 다르게 세 경우가 when 메서드에서 이미 분기 처리가 되어 있다는 것을 확인해 볼 수 있다.
따라서 개발자가 직접 if-else문으로 상태를 구분하는 것이 아니라 미리 구분되어 있는 data
, loading
, error
를 사용하기만 하면 된다.