Flutter, Chopper로 TMDB API 호출해서 영화 정보 받아오기
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);
}