부트캠프 강의를 들으며 비트코인 티커 앱을 만들던 중 데이터를 가져와 가공하는 과정에서 자꾸 문제가 생겼다.
그래서 다시 한번 개념을 제대로 정리하기위해 Flutter 공식문서를 읽어보며 공부하는게 나을 것 같았다.
개인적 공부를 위해 flutter 공식문서 내용을 대충 한국어로 번역해봤다.
(정확하지 않을 수 있음)
해당 공식문서의 주소는 아래와 같다.
인터넷에서 데이터를 가져오는데에 네가지 단계가 있다고 한다.
데이터를 가져올 수 있는 가장 간단한 방법은 http 패키지를 사용하는 것이라고 한다.
http 패키지를 설치하기 위해선, pubspec.yaml
파일의 dependencies 섹션에 http의 최신 버전을 추가하면 된다.
http의 최신 버전은 pub.dev에서도 찾아볼 수 있다.
dependencies:
http: <latest_version>
그리고 사용하고자 하는 파일에서 패키지를 import 한다.
import 'package:http/http.dart' as http;
추가적으로, AndroidManifest.xml
파일에서 Internet permission을 추가한다.
<!-- Required to fetch data from the internet. -->
<uses-permission android:name="android.permission.INTERNET" />
아래의 코드는 http.get()
메서드를 통해 JSONPlaceholder에서 샘플 앨범을 fetch 해오는 코드이다.
Future<http.Response> fetchAlbum() {
return http.get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1'));
}
http.get()
메서드는 Response
를 갖고있는 Future
를 리턴한다.
Future object
는 미래 한 때에 사용될 수 있는 잠재적 값이나 에러를 나타낸다. http.Response
class는 http 콜에서 성공적으로 받아진 데이터를 갖고있다.네트워크 request를 하기는 쉽지만, 순수 Future<http.Response>
를 가지고 작업을 하는것은 간편하지 않다. 좀 더 쉽게 작업할 수 있도록 http.Response
를 Dart object로 변환하자.
(Flutter 공식문서에서 예제로 사용하고 있는 api가 앨범 커버 api 이기 때문에 Album 클래스를 만드는 것이다.)
우선, network request로 받은 데이터를 갖고있는 Album
class를 만들자. Album
class는 JSON으로부터 Album을 만드는 factory 생성자 함수를 갖고있다.
class Album {
final int userId;
final int id;
final String title;
const Album({
required this.userId,
required this.id,
required this.title,
});
factory Album.fromJson(Map<String, dynamic> json) {
return Album(
userId: json['userId'],
id: json['id'],
title: json['title'],
);
}
}
이제, fetchAlbum()
함수를 업데이트 해서 Future<Album>
을 리턴 하도록 다음의 단계를 이용하자.
dart:convert
패키지를 통해 response body를 JSON Map으로 변환시키기.Album
으로 변환하기.Future<Album> fetchAlbum() async {
final response = await http
.get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1'));
if (response.statusCode == 200) {
// If the server did return a 200 OK response,
// then parse the JSON.
return Album.fromJson(jsonDecode(response.body));
} else {
// If the server did not return a 200 OK response,
// then throw an exception.
throw Exception('Failed to load album');
}
}
이제 당신은 인터넷에서 album을 가져오는 함수를 갖고있다!
fetchAlbum()
메서드를 initState()
나 didChangeDependencies()
메서드에서 call하자.
initState() 메서드는 정확히 한 번 실행된 후 다시는 실행되지 않는다. InheritedWidget
의 변화에 따라 API가 다시 로드 되는 것을 원한다면, fetchAlbum()
메서드를 didChangeDependencies()
메서드 안에서 실행하자. State에 대해 더 많은 정보를 원한다면 State를 클릭하자.
class _MyAppState extends State<MyApp> {
late Future<Album> futureAlbum;
void initState() {
super.initState();
futureAlbum = fetchAlbum();
}
// ···
}
이 Future는 다음 단계에서 사용될 것이다.
데이터를 스크린에 나타내기 위해서 FutureBuilder 위젯을 사용하자. FutureBuilder 위젯은 Flutter에 포함되어있으며 비동기식 데이터 소스를 대상으로 작업하는것을 편하게 해준다.
두 매개 변수를 제공해줘야 한다.
Future
. 지금의 경우, fetchAlbum()
함수에서 리턴된 future.Future
의 상태(loading, success 또는 error)에 따라서 Flutter에게 어떤 것을 만들어야하는지 알려주는 builder
함수.snapshot.hasData
는 snapshot이 non-nullable data 값을 가졌을때만 true를 반환하는 것에 주의하자.
fetchAlbum은 non-nullable 값만을 리턴할 수 있기 때문에, "404 Not Found" server response 의 경우에도 exception을 발생시켜야한다. Exception 발생이 에러 메세지를 나타내는데 사용될 수 있는 snapshot.hasError
를 true
로 만들어준다.
그 외에는 spinner가 나타날 것이다.
FutureBuilder<Album>(
future: futureAlbum,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(snapshot.data!.title);
} else if (snapshot.hasError) {
return Text('${snapshot.error}');
}
// By default, show a loading spinner.
return const CircularProgressIndicator();
},
)
편리하긴 해도, API call을 build()
메서드 안에 넣는것을 추천하지 않는다.
플러터는 무언가 바뀌어야 할 때마다 build()
메서드를 call 하는데, 이것은 생각보다 자주 일어나게 된다. 만약 fetchAlbum()
메서드가 build()
메서드 안에 위치했다면 매 rebuild 마다 반복적으로 실행되어 앱을 느려지게 만들었을것이다.
fetchAlbum()
의 결과를 상태 변수에 저장하는것은 Future가 한 번만 실행된 후 차후의 rebuild들을 위해 cache 된다.
어떻게 이 기능들을 시험해볼 수 있는지를 위해서는 다음과 같은 recipe들을 보면 된다.
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
Future<Album> fetchAlbum() async {
final response = await http
.get(Uri.parse('https://jsonplaceholder.typicode.com/albums/1'));
if (response.statusCode == 200) {
// If the server did return a 200 OK response,
// then parse the JSON.
return Album.fromJson(jsonDecode(response.body));
} else {
// If the server did not return a 200 OK response,
// then throw an exception.
throw Exception('Failed to load album');
}
}
class Album {
final int userId;
final int id;
final String title;
const Album({
required this.userId,
required this.id,
required this.title,
});
factory Album.fromJson(Map<String, dynamic> json) {
return Album(
userId: json['userId'],
id: json['id'],
title: json['title'],
);
}
}
void main() => runApp(const MyApp());
class MyApp extends StatefulWidget {
const MyApp({super.key});
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
late Future<Album> futureAlbum;
void initState() {
super.initState();
futureAlbum = fetchAlbum();
}
Widget build(BuildContext context) {
return MaterialApp(
title: 'Fetch Data Example',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
appBar: AppBar(
title: const Text('Fetch Data Example'),
),
body: Center(
child: FutureBuilder<Album>(
future: futureAlbum,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(snapshot.data!.title);
} else if (snapshot.hasError) {
return Text('${snapshot.error}');
}
// By default, show a loading spinner.
return const CircularProgressIndicator();
},
),
),
),
);
}
}
번역해놓고 보니 차라리 구글 번역기로 한 번에 돌리는 게 더 빠르고 편했을 것 같기도 하다.
일단 번역만 해놓아서 한 번에 이해는 안 되지만, 내일 다시 한번 천천히 읽어보면서 직접 코드도 짜보고 하며 공부해 봐야겠다.
화이팅