๋น๋๊ธฐ UI - FutureBuilder, StreamBuilder, AsyncValue
์์ ๊ธ์์ Future๋ก ์๋ฒ ์์ฒญ์ ํ๊ณ Stream์ผ๋ก ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ณํ๋ฅผ ๋ฐ๋ ๋ฒ์ ๋ดค๋ค. ์ด์ ๊ทธ ๊ฐ์ ํ๋ฉด์ ๋ณด์ฌ์ค ์ฐจ๋ก๋ค. Future๋ฅผ ๋๋ ค์ฃผ๋ ํจ์๋ฅผ ์์ ฏ์ ๊ทธ๋๋ก ๋ฃ์ด๋ณด์.
class UserView extends StatelessWidget {
const UserView({super.key});
@override
Widget build(BuildContext context) {
return Text(fetchUser(1).name); // ์ฌ๊ธฐ์ ์๋ฌ๋จ
}
}์๋ํฐ์ ๋นจ๊ฐ ์ค์ด ๋ฌ๋ค.
The getter 'name' isn't defined for the type 'Future<User>'.fetchUser(1)์ User๊ฐ ์๋๋ผ "๋์ค์ User๊ฐ ๋์ฐฉํ๋ค"๋ ์ฝ์(Future<User>)์ด๋ผ์ ์์ง name์ด ์๋ค. ๋น๋๊ธฐ ๊ธ์์ ๋ณธ ๊ทธ ์์๋ค. ๊ทธ๋ ๋ค๊ณ await๋ฅผ ์ธ ์๋ ์๋ค. build๋ async๊ฐ ๋ ์ ์๊ณ , ๊ธฐ๋ค๋ฆฌ๋ ๋์ ํ๋ฉด์๋ ๋ญ๊ฐ๋ฅผ ๋ณด์ฌ์ค์ผ ํ๊ธฐ ๋๋ฌธ์ด๋ค.
์์ธ: ํ๋ฉด์ ๊ธฐ๋ค๋ฆฌ๋ ๋์๊ณผ ๋์ฐฉํ ๋ค๋ฅผ ๋ชจ๋ ๊ทธ๋ ค์ผ ํ๋ค
์๋ฒ ์๋ต์ ๊ธฐ๋ค๋ฆฌ๋ ํ๋ฉด์ ์ต์ํ ์ธ ๊ฐ์ง ๋ชจ์ต์ด ํ์ํ๋ค. ๊ธฐ๋ค๋ฆฌ๋ ์ค(๋ก๋ฉ ํ์), ๋์ฐฉํจ(๋ฐ์ดํฐ), ์คํจํจ(์๋ฌ ๋ฌธ๊ตฌ). ์ง๋ ๊ธ๋ค์์ ์ด๊ฑธ isLoading, error, data ํ๋๋ก ํฉ์ด๋๋ฉด ๋ชจ์๋ ์ํ๊ฐ ๊ฐ๋ฅํ๋ค๊ณ ํ๋ค. Flutter๋ ์ด ์ธ ์ํ๋ฅผ ์์์ ๋๋ ์ฃผ๋ ์์ ฏ์ ์ค๋ค.
ํด๊ฒฐ 1: FutureBuilder - Future ํ๋๋ฅผ ํ๋ฉด์ ์ฐ๊ฒฐํ๋ค
FutureBuilder<User>(
future: fetchUser(1),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
}
if (snapshot.hasError) {
return Text('์๋ฌ: ${snapshot.error}');
}
return Text('์๋
, ${snapshot.data!.name}');
},
)future: ๊ธฐ๋ค๋ฆดFuture๋คbuilder์snapshot: ์ง๊ธFuture๊ฐ ์ด๋ค ์ํ์ธ์ง๋ฅผ ๋ด์ ๊ฐ์ฒด๋คsnapshot.connectionState: ์์ง ๊ธฐ๋ค๋ฆฌ๋ ์ค์ด๋ฉดwaiting์ด๋คsnapshot.hasError,snapshot.error: ์คํจํ๋์ง์ ๊ทธ ๋ด์ฉ์ด๋คsnapshot.data: ๋์ฐฉํ ๊ฐ์ด๋ค. ๋์ฐฉ ์ ์๋null์ด๋ผ์ ํ์ ์ดUser?์ด๊ณ , ์ ์ฝ๋๋ ์์์ ์๋ฌ๋ฅผ ๊ฑธ๋ฌ๋ธ ๋ค๋ผ!๋ก ๋จ์ธํ๋ค. ํ์คํ์ง ์์ผ๋ฉดsnapshot.hasData๋ฅผ ๋จผ์ ํ์ธํ๋ค
builder๋ ์ํ๊ฐ ๋ฐ๋ ๋๋ง๋ค ๋ค์ ์คํ๋๋ค. ์ฒ์์ waiting์ผ๋ก ๋ก๋ฉ ํ์๊ฐ ๋จ๊ณ , ์๋ต์ด ์ค๋ฉด ๋ฐ์ดํฐ ํ๋ฉด์ผ๋ก ๋ฐ๋๋ค. StatelessWidget ์์์ ์ธ ์ ์๊ณ setState๊ฐ ํ์ ์๋ค.
์ฝ์ง ํฌ์ธํธ: ํ๋ฉด์ด ๊ทธ๋ ค์ง ๋๋ง๋ค ์๋ฒ ์์ฒญ์ด ๋ฐ๋ณต๋๋ค
์ ์ฝ๋์ future: fetchUser(1)์ build๊ฐ ์คํ๋ ๋๋ง๋ค ์์ฒญ์ ์๋ก ๋ณด๋ธ๋ค. build๋ setState, ํ๋ฉด ํ์ , ํค๋ณด๋ ์ฌ๋ผ์ด ๊ฐ์ ์ผ๋ก ์์ฃผ ๋ค์ ์คํ๋๋๋ฐ, ๊ทธ๋๋ง๋ค fetchUser(1)์ด ๋ค์ ๋ถ๋ ค์ ์์ฒญ์ด ๋ฐ๋ณต๋๊ณ ๋ก๋ฉ ํ์๊ฐ ๊น๋นก์ธ๋ค.
ํด๊ฒฐ์ Future๋ฅผ ํ ๋ฒ๋ง ๋ง๋ค์ด์ State์ ๋ณด๊ดํ๋ ๊ฒ์ด๋ค. ์ฒซ ๊ธ์์ ๋ณธ late final๊ณผ initState๊ฐ ์ ํํ ์ด ์๋ฆฌ์ ์ฐ์ธ๋ค.
class UserView extends StatefulWidget {
const UserView({super.key});
@override
State<UserView> createState() => _UserViewState();
}
class _UserViewState extends State<UserView> {
late final Future<User> _future;
@override
void initState() {
super.initState();
_future = fetchUser(1); // ํ ๋ฒ๋ง ์์ฒญ
}
@override
Widget build(BuildContext context) {
return FutureBuilder<User>(
future: _future, // ๋งค๋ฒ ์๋ก ๋ง๋ค์ง ์๊ณ ๋ณด๊ดํด๋ ๊ฑธ ๋๊น
builder: (context, snapshot) {
// ์์ ๊ฐ์ ๋ถ๊ธฐ
},
);
}
}initState๋ build๋ณด๋ค ๋จผ์ , ๊ทธ๋ฆฌ๊ณ ํ ๋ฒ๋ง ์คํ๋๋, ์ฑ์ฐ๋ ์ฝ๋๊ฐ ์ฝ๋ ์ฝ๋๋ณด๋ค ๋จผ์ ์คํ๋๋ ์์๊ฐ ๋ณด์ฅ๋๋ค. FutureBuilder๋ฅผ ์ธ ๋๋ Future๋ฅผ ๋ง๋๋ ์ฝ๋๋ฅผ build ์์ ๋์ง ์๋๋ค๋ ๊ท์น์ ๊ธฐ์ตํด ๋๋ค.
ํด๊ฒฐ 2: StreamBuilder - ๊ฐ์ด ์ฌ๋ฌ ๋ฒ ์ค๋ ๊ฒฝ์ฐ
Future๋ ๊ฐ์ด ํ ๋ฒ ์ค๊ณ , Stream์ ์ฌ๋ฌ ๋ฒ ์จ๋ค. ์ง๋ ๊ธ์์ ๋ง๋ drift์ watchAll()์ด Stream์ด์๋ค. ๊ฐ์ ๋ชจ์์ StreamBuilder๋ก ์ฐ๊ฒฐํ๋ค.
StreamBuilder<List<Todo>>(
stream: db.watchAll(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Text('์๋ฌ: ${snapshot.error}');
}
if (!snapshot.hasData) {
return const CircularProgressIndicator();
}
final todos = snapshot.data!;
return ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) => ListTile(title: Text(todos[index].title)),
);
},
)๊ตฌ์กฐ๋ FutureBuilder์ ๊ฑฐ์ ๊ฐ๊ณ future ๋์ stream์ ๋๊ธด๋ค. ์ฐจ์ด๋ ์ ๊ฐ์ด ์ฌ ๋๋ง๋ค builder๊ฐ ๋ค์ ์คํ๋๋ค๋ ์ ์ด๋ค. ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ํญ๋ชฉ์ ์ถ๊ฐํ๋ฉด watchAll()์ด ์ ๋ชฉ๋ก์ ํ๋ ค๋ณด๋ด๊ณ , ํ๋ฉด์ด setState ์์ด ์ ์ ๋ก ๊ฐฑ์ ๋๋ค. Stream๋ build ์์์ ์๋ก ๋ง๋ค๋ฉด ๊ตฌ๋
์ด ๋ฐ๋ณต๋๋ฏ๋ก, FutureBuilder์ ๋๊ฐ์ด ํ ๋ฒ๋ง ๋ง๋ค์ด์ ์ด๋ค.
ํด๊ฒฐ 3: Riverpod์ AsyncValue - ์ธ ์ํ๋ฅผ ํ ๊ฐ์ฒด๋ก
Riverpod ๊ธ์์ ์๋ฒ ๊ฐ์ FutureProvider๋ฅผ ์ด๋ค๊ณ ํ๋ค. ์ด๊ฑธ ํ๋ฉด์์ ์ฝ์ผ๋ฉด Future๊ฐ ์๋๋ผ AsyncValue ๊ฐ ๋์จ๋ค. ๋ก๋ฉ, ๋ฐ์ดํฐ, ์๋ฌ ์ธ ์ํ๋ฅผ ํ๋์ ๊ฐ์ฒด์ ๋ด์ ๊ฒ์ผ๋ก, Bloc ๊ธ์์ sealed class๋ก ์ง์ ๋ฌถ์ ์ํ์ ๊ฐ์ ๋ฐ์์ด๋ค.
final userProvider = FutureProvider<User>((ref) async {
final response = await dio.get('/users/1');
return User.fromJson(response.data as Map<String, dynamic>);
});
class UserView extends ConsumerWidget {
const UserView({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final user = ref.watch(userProvider);
return user.when(
data: (user) => Text('์๋
, ${user.name}'),
loading: () => const CircularProgressIndicator(),
error: (error, stack) => Text('์๋ฌ: $error'),
);
}
}ref.watch(userProvider):AsyncValue<User>๋ฅผ ๋๋ ค์ค๋ค.when(data:, loading:, error:): ์ธ ์ํ ๋ชจ๋์ ํ๋ฉด์ ๋ฐ๋์ ์ ๊ฒ ํ๋ค. ํ๋๋ผ๋ ๋นผ๋ฉด ์๋ฌ๊ฐ ๋์,FutureBuilder์ฒ๋ผ ์๋ฌ ๋ถ๊ธฐ๋ฅผ ๊น๋นกํ๋ ์ค์๊ฐ ์๋คFutureProvider๋ Riverpod์ด ์์ฒญ์ ํ ๋ฒ๋ง ์คํํ๊ณ ๊ฒฐ๊ณผ๋ฅผ ๋ณด๊ดํ๋ค.FutureBuilder์ฒ๋ผfuture๋ฅผ State์ ๋ฐ๋ก ๋ ํ์๊ฐ ์๋ค
๋ค์ ๋ถ๋ฌ์ค๊ณ ์ถ์ ๋๋ provider๋ฅผ ๋ฌดํจํํ๋ค.
ElevatedButton(
onPressed: () => ref.invalidate(userProvider), // ๋ค์ ์์ฒญ
child: const Text('๋ค์ ๋ถ๋ฌ์ค๊ธฐ'),
)ref.invalidate๊ฐ ํธ์ถ๋๋ฉด ๋ค์์ ์ด ๊ฐ์ ์ฝ์ ๋ ๋ค์ ์์ฒญ์ ๋ณด๋ธ๋ค. 3.0๋ถํฐ๋ ์คํจํ provider๋ฅผ ์๋์ผ๋ก ์ฌ์๋ํ๊ธฐ๋ ํด์, ์คํจ ํ ์๋ฌ ํ๋ฉด์ ๋ณด์ฌ์ฃผ๊ธฐ๊น์ง ์๊ฐ์ด ๊ฑธ๋ฆด ์ ์๋ค๋ ์ ์ ์์๋์.
์ธ ๋ฐฉ๋ฒ, ์ธ์ ๋ญ ์ฐ๋
FutureBuilder,StreamBuilder: ๋ณ๋ ํจํค์ง ์์ด ํ ํ๋ฉด์์ ํ ๋ฒ ์ฝ์ ๋. ์ํ ๊ด๋ฆฌ ํจํค์ง๋ฅผ ์ฐ์ง ์๋ ๊ฐ๋จํ ํ๋ฉด์ ์๋ง๋คAsyncValue(Riverpod): ์ด๋ฏธ Riverpod์ ์ฐ๊ณ ์์ ๋. ์ฌ๋ฌ ํ๋ฉด์ด ๊ฐ์ ์์ฒญ ๊ฒฐ๊ณผ๋ฅผ ๊ณต์ ํ๊ณ , ๋ค์ ๋ถ๋ฌ์ค๊ธฐ์ ์ฌ์๋๋ฅผ ํ ๊ณณ์์ ๋ค๋ฃจ๊ธฐ ํธํ๋ค- Cubit๊ณผ
BlocBuilder: Bloc์ ์ด๋ค๋ฉดsealed class์ํ๋ฅผBlocBuilder์์switch๋ก ๋๋๋ค
Compose, SwiftUI์ ๋น๊ตํ๋ฉด
// Compose - collectAsState + when
val state by viewModel.state.collectAsState()
when (val s = state) {
is UiState.Loading -> CircularProgressIndicator()
is UiState.Success -> Text("์๋
, ${s.user.name}")
is UiState.Error -> Text("์๋ฌ: ${s.message}")
}// SwiftUI - .task + enum ์ํ
@State private var state: LoadState = .loading
var body: some View {
switch state {
case .loading: ProgressView()
case .loaded(let user): Text("์๋
, \(user.name)")
case .error(let message): Text("์๋ฌ: \(message)")
}
}
.task { await load() }- ์ํ๋ฅผ ์ธ ๋ชจ์ต์ผ๋ก ๋๋๊ธฐ: Flutter
AsyncValue.when/ Composesealed interface UiState์when/ SwiftUIenum๊ณผswitch - ์์ฒญ ์์ ์์ : Flutter๋
initState๋FutureProvider/ Compose๋ViewModel์์ฑ ๋ / SwiftUI๋.task - ๊ฐ์ด ์ฌ๋ฌ ๋ฒ ์ฌ ๋: Flutter
StreamBuilder/ ComposeFlow๋ฅผcollectAsState/ SwiftUI๋AsyncSequence๋ฅผ.task์์ ์ํ
Compose์์ UiState๋ฅผ when์ผ๋ก ๋๋๋ ๊ตฌ์กฐ์ Flutter์ AsyncValue.when์ด ๊ฐ์ฅ ๋ฎ์๋ค. ๋ค๋ง Compose๋ ์ํ๋ฅผ ์ง์ sealed interface๋ก ๋ง๋ค์ด์ผ ํ๋ ๋ฐ๋ฉด, Riverpod์ ๋ก๋ฉ, ๋ฐ์ดํฐ, ์๋ฌ๋ฅผ AsyncValue๊ฐ ์ด๋ฏธ ๊ฐ์ง๊ณ ์๋ค.
์ ๋ฆฌ
Future๋ ์์ง ๊ฐ์ด ์๋๋ผ์ ์์ ฏ์ ๋ฐ๋ก ๋ชป ๋ฃ๊ณ ,FutureBuilder๋ก ๊ธฐ๋ค๋ฆฌ๋ ์ค, ๋์ฐฉ, ์คํจ๋ฅผ ๋๋ ๊ทธ๋ฆฐ๋คFutureBuilder์future๋ฅผbuild์์์ ๋ง๋ค๋ฉด ํ๋ฉด์ด ๋ค์ ๊ทธ๋ ค์ง ๋๋ง๋ค ์์ฒญ์ด ๋ฐ๋ณต๋๋ฏ๋ก,initState์์late final๋ก ํ ๋ฒ๋ง ๋ง๋ค์ด ๋ณด๊ดํ๋ค- ๊ฐ์ด ์ฌ๋ฌ ๋ฒ ์ค๋
Stream์StreamBuilder๋ก ์ฐ๊ฒฐํ๊ณ , ์ ๊ฐ์ด ์ฌ ๋๋ง๋คbuilder๊ฐ ๋ค์ ์คํ๋๋ค - Riverpod์
AsyncValue๋ ๋ก๋ฉ, ๋ฐ์ดํฐ, ์๋ฌ๋ฅผ ํ ๊ฐ์ฒด์ ๋ด๊ณ ,.when์์ ์ธ ํ๋ฉด์ ๋ชจ๋ ์ ๋๋ก ๊ฐ์ ํ๋ค - ๋ค์ ๋ถ๋ฌ์ค๊ธฐ๋
ref.invalidate(provider)๋ก ํ๋ค - ๊ฐ๋จํ ํ๋ฉด์
FutureBuilder, Riverpod์ ์ฐ๊ณ ์๋ค๋ฉดAsyncValue, Bloc์ ์ด๋ค๋ฉดBlocBuilder๋ก ๋๋๋ค
๋ค์ ๊ธ์์๋ ๋ก๋ฉ, ์๋ฌ, ๋น ๋ชฉ๋ก ๊ฐ์ ์ํ๋ฅผ ํ๋ฉด์์ ์ด๋ป๊ฒ ๋ณด์ฌ์ค์ง, ์๋ฌ ์ฒ๋ฆฌ์ ๋ก๋ฉ ์ํ UI๋ฅผ ๋ค๋ค๋ณด๊ฒ ๋ค.
