http는 여러 플랫폼 간에 통신하기 위한 수단이 될 수 있다.
http 요청을 간단하기 보내기 위해 http
패키지를 사용할 수 있다.
플랫폼별로 네트워크 기능을 사용할 수 있도록 풀어줘야 한다.
Android의 경우
프로젝트명/android/app/src/profile/AndroidManifest.xml
파일 안에
<uses-permission android:name="android.permission.INTERNET" />
Ios의 경우 참고
프로젝트명/ios/Runner/Info.plist
파일 안에
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key><true/>
</dict>
Map<String, dynamic> bonjour = jsonDecode(json_data)
return switch(어떤 변수) {케이스1 => 리턴값1, _ => 기본_리턴값,}
클래스이름.fromJson(){}
은 클래스이름.fromJson
이라는 생성자를 정의한 것. (원래는 클래스이름
생성자만 있었겠지)요청해서 가져온 Json을 object로 바꿔서 써먹는게 키 아이디어다. 좀 더 자세히 설명하자면
Map<String, dynamic>
형태로 만든다 (=역직렬화=decode)코드로 설명하면 이렇다.
** 코드에서 보면 fetchPhoto 호출을 build가 아닌 initState메소드에서 한다. 매번 빌드될때 패치를 하면 성능이 박살나서 매우 슬프기 때문에 그렇게 한다.
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
const appTitle = 'Theme Demo';
return const MaterialApp(
title: appTitle,
home: Scaffold(
body: SafeArea(
child: DemoBody(),
),
),
);
}
}
class DemoBody extends StatefulWidget {
const DemoBody({super.key});
State<DemoBody> createState() => _DemoBodyState();
}
class _DemoBodyState extends State<DemoBody> {
late Future<Photo> photo;
Future<Photo> fetchPhoto() async {
final http.Response res = await http
.get(Uri.parse('https://jsonplaceholder.typicode.com/photos/1'));
print(res);
print('-------------------');
print(res.headers);
print('-------------------');
print(res.body);
if (res.statusCode == 200) {
return Photo.fromJson(jsonDecode(res.body));
// 뒤에 명확하게 decode된 뒤의 타입을 적어줘도 된다.
// return Photo.fromJson(jsonDecode(res.body) as Map<String, dynamic>);
} else {
throw Exception('요청 실패함');
}
}
void initState() {
super.initState();
photo = fetchPhoto();
print(photo);
}
Widget build(BuildContext context) {
return Center(
child: FutureBuilder(
future: photo,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Column(
children: <Widget>[
Image.network(snapshot.data!.url),
Text(snapshot.data!.title),
Text('Album Id : ${snapshot.data!.albumId.toString()}'),
Text('Id : ${snapshot.data!.id.toString()}'),
],
);
} else if (snapshot.hasError) {
return Text('ERROR :: ${snapshot.error}');
}
// photo에 아직 데이터가 안들어왔을 때 보여줄 기본 위젯
return const CircularProgressIndicator();
},
),
);
}
}
class Photo {
final int albumId;
final int id;
final String title;
final String url;
final String thumbnailUrl;
Photo({
required this.albumId,
required this.id,
required this.title,
required this.url,
required this.thumbnailUrl,
});
// 그냥 Photo.fromJson이라는 이름의 생성자임. 별거 아님.
// 근데 factory생성자인.
factory Photo.fromJson(Map<String, dynamic> json) {
return switch (json) {
// 타입과 항목을 잘 맞춰서 들어온 경우, Photo객체 만들기
{
'albumId': int albumId,
'id': int id,
'title': String title,
'url': String url,
'thumbnailUrl': String thumbnailUrl,
} =>
Photo(
albumId: albumId,
id: id,
title: title,
url: url,
thumbnailUrl: thumbnailUrl,
),
// 잘못된게 들어온 경우 에러 뱉기
_ => throw Exception('잘못된 형식의 json입니다.'),
};
}
}
get요청이랑 거의 똑같다.
http.post
메소드에 데이터유형과 데이터(바디)만 실어 보내주면 된다.
final http.Response res = await http.post(
Uri.parse('https://jsonplaceholder.typicode.com/photos'),
headers: <String, String>{
'Content-Type': 'application/json; charset=utf-8'
},
body: jsonEncode(
<String, String>{'title': title, 'url': url},
),
);
내용 적어서 post(인척 하지만 사실 echo인)하는 예시를 보면 이렇다.
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
// --------------------------------
// UI
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
const appTitle = 'Theme Demo';
return const MaterialApp(
title: appTitle,
home: Scaffold(
body: SafeArea(
child: DemoBody(),
),
),
);
}
}
class DemoBody extends StatefulWidget {
const DemoBody({super.key});
State<DemoBody> createState() => _DemoBodyState();
}
class _DemoBodyState extends State<DemoBody> {
final TextEditingController _titleController = TextEditingController();
final TextEditingController _urlController = TextEditingController();
Future<Photo>? _createdPhoto;
Widget build(BuildContext context) {
return Center(
child: (_createdPhoto == null) ? buildCreateForm() : buildPhotoView(),
);
}
Column buildCreateForm() {
return Column(
children: <Widget>[
TextField(
controller: _titleController,
decoration: const InputDecoration(
labelText: 'Title',
hintText: 'Enter title',
),
),
TextField(
controller: _urlController,
decoration: const InputDecoration(
labelText: 'URL',
hintText: 'Enter URL',
),
),
ElevatedButton(
onPressed: () {
setState(() {
_createdPhoto = createPhoto(
_titleController.text,
_urlController.text,
);
});
},
child: const Text('SUBMIT'),
),
],
);
}
FutureBuilder<Photo> buildPhotoView() {
return FutureBuilder(
future: _createdPhoto,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Column(
children: <Widget>[
Image.network(snapshot.data!.url),
Text(
snapshot.data!.title,
style: const TextStyle(fontSize: 24),
),
],
);
} else if (snapshot.hasError) {
return const Text('에러났다');
}
// 통신 중에 보여줄 것
return const CircularProgressIndicator();
});
}
}
// --------------------------------
// Post 통신 메소드
Future<Photo> createPhoto(String title, String url) async {
// 받는 응답은 내가 보냈던 데이터.
final http.Response res = await http.post(
Uri.parse('https://jsonplaceholder.typicode.com/photos'),
headers: <String, String>{
'Content-Type': 'application/json; charset=utf-8'
},
body: jsonEncode(
<String, String>{'title': title, 'url': url},
),
);
if (res.statusCode == 201) {
final decoded = Photo.fromJson(jsonDecode(res.body));
print(jsonDecode(res.body));
return decoded;
} else {
throw Exception('요청 실패함');
}
}
// --------------------------------
// 모델
class Photo {
final String title;
final String url;
Photo({
required this.title,
required this.url,
});
factory Photo.fromJson(Map<String, dynamic> json) {
return switch (json) {
{
'title': String title,
'url': String url,
} =>
Photo(
title: title,
url: url,
),
_ => throw Exception('잘못된 형식의 json입니다.'),
};
}
}
구현에 따라 다르겠지만.
아무튼 put method를 쓴다고 하면
final http.Response res = await http.put(
Uri.parse('https://jsonplaceholder.typicode.com/photos/1'),
headers: <String, String>{
'Content-Type': 'application/json; charset=utf-8'
},
body: jsonEncode(
<String, String>{'title': title, 'url': url},
),
);
이것도 구현에 따라 다르겠지만
어쨌든 delete method를 꼭 써야겠다고 하면
final http.Response res = await http.delete(
Uri.parse('https://jsonplaceholder.typicode.com/photos'),
headers: <String, String>{
'Content-Type': 'application/json; charset=utf-8'
},
);
인증토큰 넣고싶으면 넣으면 된다.
import 'dart:io'; // HttpHeaders가 여기서 나오는듯
import 'package:http/http.dart' as http;
Future<뭐시기> fetchPhoto() async {
final http.Response res = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/photos/1'),
headers: {
HttpHeaders.authorizationHeader: 'MY_TOKEN',
},
);
}
https://docs.flutter.dev/cookbook/networking/web-sockets
web_socket_channel 패키지로 웹소켓 서버에 연결할 수 있음.
다시 말하면, 제공되는 기능은
// 소켓서버에 연결하기
final channel = WebSocketChannel.connect(
Uri.parse('웹소켓서버 주소');
);
// 메시지 push하기
channel.sink.add(보낼것);
// 소켓서버 연결 끊기
channel.sink.close();
소켓서버에서 응답받은 내용을 UI에 표시할 때는 StreamBuilder
위젯 사용
StreamBuilder(
stream: channel.stream,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text('${snapshot.data}');
} else if (snapshot.hasError) {
throw Exception('슬프다');
}
return Text('기다리는 중');
},
)
dart.convert로 수동 직렬화를 해보자
사실 지금까지 이 포스트에서 하던 json직렬화는 모두 (상대적으로 낮은 수준의) 수동 직렬화였다.
아래처럼 jsonDecode만 해서 나온 Map을 그대로 사용해도 된다.
그러나 jsonDecode는 dynamic타입을 리턴하므로 타입체킹이나 프로퍼티 오타의 컴파일시점 체킹이 곤란하다. 근데 아무튼 동작한다.
final somethingMap = jsonDecode(somethingJson) as Map<String, dynamic>;
print (somethingMap.prop1);
그래서 앞서 http통신 부분에서 했던 대로 디코딩된 Map을 모델클래스로 다시 바꾼다.
final somethingMap = jsonDecode(somethingJson) as Map<String, dynamic>;
final something = Something.fromJson(somethingMap);
print (something.member1);
// class def
class Something{
int member1;
Something({required int member1});
Something.fromJson(json) {
.......
return Something(json에서 얻은 member1);
}
}
// 놀랍게도 인스턴스에서 스트링으로 바로 직렬화가 된다.
// String serialized = jsonEncode(something);
이렇게 간단하게 항상 해결이 되면 좋겠지만, 프로젝트가 규모가 커지고 Json의 구조가 복잡해지면 삶이 고달파지기 때문에 외부 라이브러리의 힘을 빌리게 된다.
json_serializable로 직렬화를 날로 먹어보자. 대신 다른 귀찮은일이 생긴다.
그래서 자동인지에는 의문이 생기지만.. 프로젝트가 커지면 도움이 될 듯.
코드생성 라이브러리를 이용해 Json직렬화 보일러플레이트를 자동생성 할 수 있음.
플러터 독스에서 소개 하는 패키지는 json_serializable이고, 어노테이션을 적어서 코드생성을 처리하는 특징이 있음.
설치는 이렇게 (개발종속성에 패키지가 들어감)
flutter pub add json_annotation dev:build_runner dev:json_serializable
이 라이브러리를 쓰는 순간 직렬화/역직렬화의 동작은 라이브러리의 책임이다. 테스트 안해도 된다. (라고 주장함)
대충 이런 순서로 작업한다
json_Serializable을 이용할 때 모델클래스는 이렇게 정의한다.
@JsonSerializable()
붙여주면 직렬화 코드를 자동생성클래스명.g.dart
part '클래스명.g.part';
_$클래스명FromJson(json)
: map에서 클래스로 역직렬화_$클래스명ToJson(json)
: 클래스에서 map으로 직렬화()
class Photo {
final int id;
final String title;
Photo({
required this.id,
required this.title,
});
factory Photo.fromJson(Map<String, dynamic> json) => _$PhotoFromJson(json);
Map<String, dynamic> toJson() => _$PhotoToJson(this);
// 참고로 저 Map타입의 짭Json말고,
// String타입의 진짜배기 Json을 뱉고 싶다면
// 그냥 원래 하던대로 하면 된다
// String str_ㄹㅇ_json = jsonEncode(Photo인스턴스);
만약 Map의 key와 모델의 네이밍 케이스가 다르다면 그걸 각각 지정해줄 수 있다. 그러면 Map을 모델로 전환할 때, 모델을 Map으로 전환할 때 문제가 발생할 수 있을 것.
(e.g., Map에서의 키는 user_id
인데, 플러터 모델에서 죽어도 userId
로 써야겠는 경우)
이 기능은 모델 전체의 멤버에 적용된다.
(fieldRename: FieldRename.snake)
class User {
// Map에서 얘의 key는 'user_id'로 간주된다. 위에서 그렇게 설정했으니까.
final int userId;
...
}
멤버(필드)에 붙이는 어노테이션인데, 여러가지 용도로 사용할 수 있다.
위에서 FieldRename으로 json(map) key와, 모델 필드이름을 각각 지정해줬다.
그것처럼 여기서도 모델 필드이름과 다르게 실제 map에서의 key는 무엇인지 알려줄 수 있다.
(name: 'user_id')
final int userId;
이렇게 하면 필수값으로 지정하거나 기본값을 넣어주는 일도 가능하다
(required: true)
final int something;
(defaultValue: 1234)
final int somethingAnother;
두 가지 방법이 있다.
프로젝트 루트에서 이거 하면 된다. 이거 하면 코드 생김.
dart run build_runner build --delete-conflicting-outputs
약간 시간이 걸리다가 이렇게 잘 됐다고 한다.
dart run build_runner build --delete-conflicting-outputs
Building package executable... (9.1s)
Built build_runner:build_runner.
[INFO] Generating build script completed, took 343ms
[INFO] Precompiling build script... completed, took 4.8s
[INFO] Building new asset graph completed, took 1.6s
[INFO] Checking for unexpected pre-existing outputs. completed, took 1ms
[INFO] Generating SDK summary completed, took 5.9s
[WARNING] source_gen:combining_builder on lib/main.dart:
main.g.dart must be included as a part directive in the input library with:
part 'main.g.dart';
[INFO] Running build completed, took 19.8s
[INFO] Caching finalized dependency graph completed, took 86ms
[INFO] Succeeded after 19.8s with 53 outputs (113 actions)
와쳐를 켜두는 것임. 프로젝트 루트에서 이거 하면 된다.
dart run build_runner watch --delete-conflicting-outputs
모델의 필드가 다른 클래스인 경우
모델 --> Map 변환이 동작하지 않을 수 있음. (_$UserToJson(this)
)
@JsonSerializable(explicitToJson: true)
를 붙여서 명시적 JSON직렬화를 시키도록 하자
백그라운드에서 무거운 작업을 돌려보도록 하자
플러터는 웬만해서는 버벅거림 없을만큼 잘 만들긴 했다.
근데 아무리 그래도 겁나 복잡하고 거대한 JSON같은거 파싱 시키거나 네트워크 통신을 하면 성능저하가 체감될 수 있다. 보통 16ms이상 걸리면 유저가 버벅거림을 느낀다.
그러니까 오래걸리는건 백그라운드로 보내서 처리되도록 하고, 때가 되면 꺼내오도록 하자.
정확하게 안드로이드에서는 다른 thread로 보내는 것이고, 플러터는 이를 위해 분리된 공간인 Isolate를 제공한다.
아래처럼 compute(함수, 보낼 파라미터)
를 호출하면 백그라운드에서 돌아간다.
Future<List<Gorani>> fetchGoranis() async {
final res = await http.get(Uri.parse('http://example.com/path'));
return compute( parseGoranis, res.body );
}
List<Gorani> parseGoranis(String body) {
...
return goranis;
}
compute는 기본형(int, String)이나 단순한 오브젝트(List)는 잘 처리한다
근데 Future, http.Response같은걸 compute로 넘겨서 처리하려면 문제가 생길 수 있다.
그런 복잡한 데이터를 넘겨 처리할 때는 아래와 같은 패키지들을 사용해볼 수 있다.