코딩 일기장/Flutter

[Flutter] Base Response 사용하여 Response 받기 + build_runner(중복 코드 줄이기)

minWachya 2023. 7. 27. 10:18
반응형

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랑 비슷한(ㅎㅎ) 방법으로 찾아봤다.

적용이 쉬워서 다행...ㅎ


참고

반응형