[Flutter] Base Response 사용하여 Response 받기 + build_runner(중복 코드 줄이기)
0. 시작하기 전에
- 구현하고자 하는 것: 댓글 목록을 받아오는 코드를 작성할 것이다.
- requset:
- get
- url: {baseUrl}/api/v1/talks/messages?page={0}&size={3}
- 파라미터에 int page, int size 넣으면 됨
- response
-
page Number 조회된 페이지 번호 size Number 조회된 한 페이지 크기 messages[].messageId String 메세지 식별자 messages[].createdAt String 메세지 전송 시각 messages[].content String 메세지 내용 messages[].sender.userId String 메세지 전송자 식별자 messages[].sender.nickname String 메세지 전송자 닉네임 messages[].sender.profileImg String 메세지 전송자 프로필사진
-
// 응답 예시
{
"timeStamp" : "2023/07/20 12:04:10",
"code" : "TALK-2001",
"message" : "[라이브톡] 라이브톡 메세지 목록 조회 성공",
"data" : {
"page" : 0,
"size" : 3,
"messages" : [ {
"messageId" : "3723eac0-6b8c-4d8b-8ae0-20ea02c6b333",
"content" : "1 메세지내용 test",
"createdAt" : "2023-07-20T12:04:10.777247500+09:00[Asia/Seoul]",
"sender" : {
"userId" : "71204da5-6377-4418-bdf2-54dcfb4f21e9",
"nickname" : "nicknameTest",
"profileImg" : "http://testurl"
}
}, {
"messageId" : "c78d4754-fc0a-43c1-b497-267f69ddbca2",
"content" : "2 메세지내용 test",
"createdAt" : "2023-07-20T12:04:10.777247500+09:00[Asia/Seoul]",
"sender" : {
"userId" : "71204da5-6377-4418-bdf2-54dcfb4f21e9",
"nickname" : "nicknameTest",
"profileImg" : "http://testurl"
}
} ]
}
}
응답 받아서 ui에 적용하는 것까지 보면 이렇게 된다.
1. 의존성 추가
dependencies:
// ...
json_annotation: ^4.8.0
dev_dependencies:
// ...
build_runner: ^2.3.3
json_serializable: ^6.6.1
2. requset 데이터 생성
- 요청 데이터는 page랑 size만 있었다~~
- 빨간 줄이 나는 것은 터미널에 flutter pub run build_runner build를 입력하여 part에 선언한대로 .g.dart 클래스를 만들어주면 해결된다.(앞으로 나올 다른 클래스들도 마찬가지)
import 'package:json_annotation/json_annotation.dart';
part 'stage_talk_message_request.g.dart';
@JsonSerializable()
class StageTalkMessageRequest {
int page;
int size;
StageTalkMessageRequest({
required this.page,
required this.size,
});
factory StageTalkMessageRequest.fromJson(Map<String, dynamic> json) =>
_$StageTalkMessageRequestFromJson(json);
Map<String, dynamic> toJson() => _$StageTalkMessageRequestToJson(this);
}
3. response 데이터 생성
- 응답 예시는 아래와 같았다.
// 응답 예시
{
"timeStamp" : "2023/07/20 12:04:10",
"code" : "TALK-2001",
"message" : "[라이브톡] 라이브톡 메세지 목록 조회 성공",
"data" : {
"page" : 0,
"size" : 3,
"messages" : [ {
"messageId" : "3723eac0-6b8c-4d8b-8ae0-20ea02c6b333",
"content" : "1 메세지내용 test",
"createdAt" : "2023-07-20T12:04:10.777247500+09:00[Asia/Seoul]",
"sender" : {
"userId" : "71204da5-6377-4418-bdf2-54dcfb4f21e9",
"nickname" : "nicknameTest",
"profileImg" : "http://testurl"
}
}, {
"messageId" : "c78d4754-fc0a-43c1-b497-267f69ddbca2",
"content" : "2 메세지내용 test",
"createdAt" : "2023-07-20T12:04:10.777247500+09:00[Asia/Seoul]",
"sender" : {
"userId" : "71204da5-6377-4418-bdf2-54dcfb4f21e9",
"nickname" : "nicknameTest",
"profileImg" : "http://testurl"
}
} ]
}
}
- 여기서 아래 부분은 모든 응답 시 동일하기 때문에 이 부분을 base response로 만들어보려고 한다.
{
"timeStamp" : "~~",
"code" : "~~",
"message" : "~~",
"data" : T
}
- 만들면 이렇게 된다(BaseObject 코드는 아래)
import 'package:pocket_pose/data/entity/base_object.dart';
class BaseResponse<T> {
String timeStamp;
String code;
String message;
T data;
BaseResponse(
{required this.timeStamp,
required this.code,
required this.message,
required this.data});
factory BaseResponse.fromJson(Map<String, dynamic> json, BaseObject target) {
return BaseResponse(
timeStamp: json['timeStamp'],
code: json['code'],
message: json['message'],
data: target.fromJson(json['data']),
);
}
}
- T인 data를 직렬화해주기 위해... BaseObject라는 클래스를 생성하여 T가 이를 상속받도록 할 것이다.
abstract class BaseObject<T> {
T fromJson(json);
}
- 그 다음 messages 배열의 아이템을 살펴보자.
{
"messageId" : "c78d4754-fc0a-43c1-b497-267f69ddbca2",
"content" : "2 메세지내용 test",
"createdAt" : "2023-07-20T12:04:10.777247500+09:00[Asia/Seoul]",
"sender" : {
"userId" : "71204da5-6377-4418-bdf2-54dcfb4f21e9",
"nickname" : "nicknameTest",
"profileImg" : "http://testurl"
}
- sender라는 객체가 또 들어있다. 이를 위한 데이터 클래스를 먼저 만들어주자.
import 'package:json_annotation/json_annotation.dart';
part 'chat_user_list_item.g.dart';
@JsonSerializable()
class ChatUserListItem {
String userId = "";
String nickname = "";
String? profileImg;
ChatUserListItem(
{required this.userId, required this.nickname, this.profileImg});
factory ChatUserListItem.fromJson(Map<String, dynamic> json) =>
_$ChatUserListItemFromJson(json);
Map<String, dynamic> toJson() => _$ChatUserListItemToJson(this);
}
- 그 다음 messages에 들어가는 객체를 데이터 클래스로 만들고,
import 'package:json_annotation/json_annotation.dart';
import 'package:pocket_pose/domain/entity/chat_user_list_item.dart';
part 'stage_talk_list_item.g.dart';
@JsonSerializable()
class StageTalkListItem {
late String messageId;
late String content;
late String createdAt;
late ChatUserListItem sender;
StageTalkListItem(
{required this.content, required this.sender, required this.createdAt});
factory StageTalkListItem.fromJson(Map<String, dynamic> json) =>
_$StageTalkListItemFromJson(json);
Map<String, dynamic> toJson() => _$StageTalkListItemToJson(this);
}
- 최종적으로 request를 생성해주면 된다!
import 'package:json_annotation/json_annotation.dart';
import 'package:pocket_pose/data/entity/base_object.dart';
import 'package:pocket_pose/domain/entity/stage_talk_list_item.dart';
part 'stage_talk_message_response.g.dart';
@JsonSerializable()
class StageTalkMessageResponse extends BaseObject<StageTalkMessageResponse> {
int page;
int size;
List<StageTalkListItem>? messages;
StageTalkMessageResponse({
required this.page,
required this.size,
required this.messages,
});
factory StageTalkMessageResponse.fromJson(Map<String, dynamic> json) =>
_$StageTalkMessageResponseFromJson(json);
Map<String, dynamic> toJson() => _$StageTalkMessageResponseToJson(this);
@override
StageTalkMessageResponse fromJson(json) {
return StageTalkMessageResponse.fromJson(json);
}
}
4. provider 생성
- 생성한 requset, response를 가지고 서버에 요청 보내고 응답을 받아보자!!
- 여기서 응답형으로 Future<BaseResponse<StageMessageResponse>>를 써서 base response형태를 재사용할 수 있게 된다.
import 'package:pocket_pose/data/entity/base_response.dart';
import 'package:pocket_pose/data/entity/request/stage_talk_message_request.dart';
import 'package:pocket_pose/data/entity/response/stage_talk_message_response.dart';
abstract class StageTalkProvider {
Future<BaseResponse<StageTalkMessageResponse>> getTalkMessages(
StageTalkMessageRequest request);
}
import 'dart:async';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:pocket_pose/config/api_url.dart';
import 'package:pocket_pose/data/entity/base_response.dart';
import 'package:pocket_pose/data/entity/request/stage_talk_message_request.dart';
import 'package:pocket_pose/data/entity/response/stage_talk_message_response.dart';
import 'package:pocket_pose/domain/provider/stage_talk_provider.dart';
class StageTalkProviderImpl implements StageTalkProvider {
@override
Future<BaseResponse<StageTalkMessageResponse>> getTalkMessages(
StageTalkMessageRequest request) async {
// 토큰 가져오기
const storage = FlutterSecureStorage();
const storageKey = 'kakaoAccessToken';
const refreshTokenKey = 'kakaoRefreshToken';
String accessToken = await storage.read(key: storageKey) ?? "";
String refreshToken = await storage.read(key: refreshTokenKey) ?? "";
var dio = Dio();
try {
// 헤더에 토큰 추가
dio.options.headers = {
"cookie": "x-access-token=$accessToken;x-refresh-token=$refreshToken"
};
dio.options.contentType = "application/json";
// url에 request를 파라미터로 넣기
var response = await dio.get(
'${AppUrl.stageTalkUrl}?page=${request.page}&size=${request.size}');
// 응답 리턴
return BaseResponse<StageTalkMessageResponse>.fromJson(response.data,
StageTalkMessageResponse.fromJson(response.data['data']));
} catch (e) {
debugPrint("mmm StageTalkProviderImpl catch: ${e.toString()}");
}
return throw UnimplementedError();
}
}
5. 이제 ui에 예쁘게 출력해주면 된다!
- 이건 채팅 리스트 아이템 클래스이고(프로필, 닉네임, 내용)
import 'package:flutter/material.dart';
import 'package:pocket_pose/domain/entity/stage_talk_list_item.dart';
class TalkListItemWidget extends StatelessWidget {
final StageTalkListItem talk;
const TalkListItemWidget({super.key, required this.talk});
@override
Widget build(BuildContext context) {
return Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(50),
child: Image.asset(
(talk.sender.profileImg == null)
? 'assets/images/charactor_popo_default.png'
: talk.sender.profileImg!,
width: 35,
height: 35,
),
),
const SizedBox(
width: 12,
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
talk.sender.nickname,
style: const TextStyle(color: Colors.white),
),
const SizedBox(
height: 4,
),
Text(
talk.content,
style: const TextStyle(color: Colors.white),
)
],
)
],
);
}
}
- 이건 위의 리스트 아이템을 사용해 리스트를 생성한 코드이다.
- getTalkMessage()라는 함수를 만들어 채팅 데이터들을 가져와 출력하도록 했다.
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:pocket_pose/data/entity/request/stage_talk_message_request.dart';
import 'package:pocket_pose/data/remote/provider/stage_talk_provider_impl.dart';
import 'package:pocket_pose/domain/entity/stage_talk_list_item.dart';
import 'package:pocket_pose/ui/widget/stage/talk_list_item_widget.dart';
class StageLiveChatListWidget extends StatefulWidget {
const StageLiveChatListWidget({super.key});
@override
State<StageLiveChatListWidget> createState() =>
_StageLiveChatListWidgetState();
}
class _StageLiveChatListWidgetState extends State<StageLiveChatListWidget> {
List<StageTalkListItem> _messageList = [];
final ScrollController _scrollController = ScrollController();
final _provider = StageTalkProviderImpl();
@override
void initState() {
super.initState();
getTalkMessage();
}
void getTalkMessage() async {
var result = await _provider
.getTalkMessages(StageTalkMessageRequest(page: 0, size: 3));
setState(() {
_messageList = result.data.messages ?? [];
});
}
@override
void dispose() {
super.dispose();
_scrollController.dispose();
}
@override
Widget build(BuildContext context) {
// build마다 채팅 스크롤 맨 밑으로
if (_messageList.isNotEmpty) {
SchedulerBinding.instance.addPostFrameCallback((_) {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 50),
curve: Curves.easeOut,
);
});
}
return _buildStageChatList(_messageList);
}
SingleChildScrollView _buildStageChatList(List<StageTalkListItem> entries) {
return SingleChildScrollView(
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.transparent,
Colors.black.withOpacity(0.3),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
stops: const [0.0001, 0.25],
),
),
height: 150,
child: ShaderMask(
shaderCallback: (Rect bounds) {
return LinearGradient(
begin: Alignment.center,
end: Alignment.topCenter,
colors: [Colors.white, Colors.white.withOpacity(0.02)],
stops: const [0.2, 1],
).createShader(bounds);
},
child: ListView.separated(
controller: _scrollController,
padding: const EdgeInsets.all(14),
itemCount: entries.length,
separatorBuilder: (context, index) {
return const SizedBox(height: 12);
},
itemBuilder: (BuildContext context, int index) {
return TalkListItemWidget(talk: entries[index]);
}),
),
),
);
}
}
끝!
Android에서도 Base Response를 사용해서 데이터를 받았었다. 아래와 같은 식이다.
data class BaseResponse<T>(
@SerializedName("data")
val `data`: T,
@SerializedName("message")
val message: String,
@SerializedName("status")
val status: Int,
@SerializedName("success")
val success: Boolean
)
Flutter에서도 비슷한 방법을 찾아봤는데 생각보다... 이런 방법을 잘 사용 안하시는듯??
(왜??? 이거 안 쓰면 base response 부분 코드가 중복되지 않나? 아니면 아예 안 받아오나?<왜..?)
암튼 그래서인지 방법도 제각각이었다.
그중에서 가장 사용하기 쉽고 Android랑 비슷한(ㅎㅎ) 방법으로 찾아봤다.
적용이 쉬워서 다행...ㅎ
참고
- base response를 abstract class로 사용
- base response 기본 예제
- base response 는 아닌데 깔끔한 서버 요청법
- 나름 익숙한 방법(이 코드를 제일 많이 참고함!!!!) https://gist.github.com/thanhniencung/7c2b9f3d24bf80d964ec7f8157050ebc