Language/Flutter

Flutter, Chopper로 TMDB API 호출해서 영화 정보 받아오기

jaewpark 2024. 6. 11. 00:27

Chopper

API Endpoint를 쉽게 정의할 수 있는 코드를 생성합니다.
request의 header를 추가하고 URL을 제공하며 처리하는 요청에 interceptor와 converter를 적용하는 역할도 담당합니다.

설치

24년 6월 10일 기준으로 설치를 위해 아래와 같이 추가했습니다.

# pubspec.yaml

dependencies:
  chopper: ^8.0.0

dev_dependencies:
  build_runner: ^2.4.9
  chopper_generator: ^8.0.0

Chopper 코드 생성

ChopperService 클래스를 확장하는 추상 클래스에서 @ChopperApi annotation 사용
@Get 말고도 다른 요청들도 있습니다.

  • @Post
  • @Put
  • @Patch
  • @Delete
  • @Head
// movie_service.dart

import 'dart:async';
import 'package:chopper/chopper.dart';

part 'movie_service.chopper.dart';

@ChopperApi(baseUrl: '/movie')
abstract class MovieService extends ChopperService {

  @Get(path: '/popular')
  Future<Response<MovieList>> getPopular({@Query("page") int page = 1});

  @Get(path: '/top_rated')
  Future<Response> getTopRated({@Query("page") int page = 1});

  static MovieService create() {
    final client = ChopperClient(
      baseUrl: Uri.parse('https://api.themoviedb.org/3'),
      services: [
        _$MovieService(),
      ],
      interceptors: [
        NetworkInterceptor(),
      ],
      converter: const JsonConverter(),
    );
    return _$MovieService(client);
  }
}

chopper 코드 생성하기

API 인터페이스를 정의만 했을 뿐인데, Chopper 코드를 자동으로 생성할 수 있습니다.
위 코드를 작성하고 터미널에서 다음 명령어를 입력하게 되면 코드가 생성됩니다.
movie_service.chopper.dart가 생긴 것을 확인 가능합니다.

flutter pub run build_runner build

MovieService 사용

import 'dart:async';
import 'package:chopper/chopper.dart';

import 'movie_service.dart';

void main() {
  final movieService = MovieService.create();
}

API 연결을 위한 Interceptor 사용

TMDB API를 사용하기 위해서는 api key를 넣어줘야 합니다.
api를 넣어주기 위해 interceptor를 사용하면 아래와 같습니다.

Interceptor를 이해하기로는 MovieService.getPopular을 호출할 때 가로챕니다.
header 혹은 파라미터를 넣어서 원하는 api 호출할 수 있습니다.

class NetworkInterceptor implements Interceptor {
  NetworkInterceptor();

  @override
  FutureOr<Response<BodyType>> intercept<BodyType>(Chain<BodyType> chain) async {
    final updatedParameters = Map<String, dynamic>.from(chain.request.parameters);
    updatedParameters['api_key'] = ''; // API KEY
    updatedParameters['language'] = 'ko-KR';

    final updatedRequest = chain.request.copyWith(parameters: updatedParameters);
    final response = await chain.proceed(updatedRequest);

    return response;
  }
}

기본적으로 이렇게만 해도 API를 호출을 하고 Json 형태의 값을 받을 수 있게 됩니다.
Json의 값을 사용하기 위해서는 해당 객체에서 key에 접근해서 값을 얻어와야 합니다.

요청한 통신은 snapshot.data의 body에서 변환된 model을 정확히 알아야 값에 접근할 수 있게 됩니다.
이걸 해결하기 위해 Converter를 사용했습니다.

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    final movieService = MovieService.create();

    return MaterialApp(
      home: Scaffold(
        body: FutureBuilder(
          future: movieService.getPopular(),
          builder: (context, snapshot) {
            if (snapshot.connectionState == ConnectionState.waiting) {
              return const Center(
                child: CircularProgressIndicator(),
              );
            } else if (snapshot.hasError) {
              return const Text('오류');
            }
            final movies = snapshot.data!.body!['results'];

            return ListView.builder(

            // ...

Converter를 사용하여 원하는 model로 값을 받기

custom converter를 사용하여 원하는 값을 받을 수도 있겠지만, 특정 변환기로 표현하도록 하겠습니다.
단일 endpoint에만 적용할 때 @FactoryConverter annotation을 사용하면 됩니다.

MovieList 값으로 전달하기 위한 MovieListConverter를 만들었습니다.
getPopular의 함수의 반환 타입을 변경했습니다.
해당 객체의 속성 값으로 접근하여 데이터를 얻을 수 있게 됩니다.

// movie_list_converter.dart
class MovieListConverter {
  static FutureOr<Response<MovieList>> response(
    Response<dynamic> response,
  ) async {
    final body = response.body;
    final bodyDecode = json.decode(body as String) as Map<String, dynamic>;
    final bodyParser = MovieList.fromJson(bodyDecode);

    return response.copyWith(body: bodyParser);
  }
}

// movie_service.dart
abstract class MovieService extends ChopperService {

  @FactoryConverter(response: MovieListConverter.response)
  @Get(path: '/popular')
  Future<Response<MovieList>> getPopular({@Query("page") int page = 1});

 

전체 코드

아직 dart 언어가 익숙하지 않았기에 더 나은 코드가 있을 수 있습니다.

댓글로 좀 더 나은 방법이 있거나 틀린 부분이 있다면 댓글로 알려주시면 감사합니다.

// movie_service.dart

@ChopperApi(baseUrl: '/movie')
abstract class MovieService extends ChopperService {

  @FactoryConverter(response: MovieListConverter.response)
  @Get(path: '/popular')
  Future<Response<MovieList>> getPopular({@Query("page") int page = 1});

  @Get(path: '/top_rated')
  Future<Response> getTopRated({@Query("page") int page = 1});

  static MovieService create() {
    final client = ChopperClient(
      baseUrl: Uri.parse('https://api.themoviedb.org/3'),
      services: [
        _$MovieService(),
      ],
      interceptors: [
        NetworkInterceptor(),
      ],
      converter: const JsonConverter(),
    );
    return _$MovieService(client);
  }
}

// network_interceptor.dart

class NetworkInterceptor implements Interceptor {
  NetworkInterceptor();

  @override
  FutureOr<Response<BodyType>> intercept<BodyType>(Chain<BodyType> chain) async {
    final updatedParameters = Map<String, dynamic>.from(chain.request.parameters);
    updatedParameters['api_key'] = ''; // API KEY
    updatedParameters['language'] = 'ko-KR';

    final updatedRequest = chain.request.copyWith(parameters: updatedParameters);
    final response = await chain.proceed(updatedRequest);
    
    return response;
  }
}

// movie_list_converter.dart

class MovieListConverter {
  static FutureOr<Response<MovieList>> response(
    Response<dynamic> response,
  ) async {
    final body = response.body;
    final bodyDecode = json.decode(body as String) as Map<String, dynamic>;
    final bodyParser = MovieList.fromJson(bodyDecode);

    return response.copyWith(body: bodyParser);
  }
}

// movie_list.dart
@JsonSerializable()
class MovieList {
  MovieList(
    this.page,
    this.movies,
    this.totalPages,
    this.totalResults,
  );

  @JsonKey(name: "page")
  final int page;

  @JsonKey(name: "results")
  final List<Movie> movies;

  @JsonKey(name: "total_pages")
  final int totalPages;

  @JsonKey(name: "total_results")
  final int totalResults;

  factory MovieList.fromJson(Map<String, dynamic> json) => _$MovieListFromJson(json);

  Map<String, dynamic> toJson() => _$MovieListToJson(this);
}