개발노트

74. [Flutter] http 패키지 사용하기 (API 서버 통신) 본문

앱 개발/Flutter

74. [Flutter] http 패키지 사용하기 (API 서버 통신)

mroh1226 2024. 4. 11. 14:53
반응형

http 패키지

Flutter에서 네트워크 요청을 보내고 받는 데 사용되는 가장 일반적인 패키지 중 하나가 http 패키지입니다.
이 패키지는 HTTP 요청을 만들고 응답을 처리하는 데 도움이 되는 다양한 클래스와 함수를 제공합니다.

HTTP 패키지의 주요 특징

  1. 간단한 인터페이스: http 패키지는 간단한 API를 제공하여 HTTP 요청을 쉽게 생성하고 수행할 수 있습니다. 이를 통해 개발자는 쉽게 네트워크 요청을 관리할 수 있습니다.
  2. 비동기 지원: 대부분의 네트워크 작업은 비동기적으로 처리되므로 http 패키지는 Future 기반 API를 제공하여 비동기 코드 작성을 간단하게 해줍니다. 이를 통해 UI를 차단하지 않고 네트워크 요청을 수행할 수 있습니다.
  3. HTTP 요청 설정: http 패키지를 사용하면 HTTP 요청을 보낼 때 다양한 설정을 할 수 있습니다. 헤더, 쿼리 매개변수, 요청 본문 등을 쉽게 추가하고 수정할 수 있습니다.
  4. HTTP 응답 처리: 받은 HTTP 응답을 처리하는 데 도움이 되는 다양한 메서드와 클래스를 제공합니다. 이를 통해 응답 데이터를 쉽게 분석하고 필요한 작업을 수행할 수 있습니다.

이전에 노마드코더에서 만들었던 웹튠 앱과, 영화 예매앱 에서 http 패키지를 이용하여 API 통신을 적용하였습니다.


http 패키지로 API 통신이 적용된 웹튠 앱, 영화 앱 소스입니다.


1. 첫번째로 웹튠앱

Model
class WebtoonDetailModel {
  final String title, about, genre, age;
  WebtoonDetailModel.fromJson(Map<String, dynamic> json)
      : title = json['title'],
        about = json['about'],
        genre = json['genre'],
        age = json['age'];
}

class WebtoonEpisodeModel {
  final String id, title, rating, date;

  WebtoonEpisodeModel.fromJson(Map<String, dynamic> json)
      : id = json['id'],
        title = json['title'],
        rating = json['rating'],
        date = json['date'];
}

class WebtoonModel {
  final String title, thumb, id;
  WebtoonModel.fromJson(Map<String, dynamic> json)
      //named constructor(생성자) 생성자에 이름(fromJson)을 정해주는 형식 Dart에만 있는 형식임
      : id = json['id'],
        title = json['title'],
        thumb = json['thumb'];
//{id: 802872,
//title: DARK MOON: 회색 도시,
//thumb: https://image-comic.pstatic.net/webtoon/802872/thumbnail/thumbnail_IMAG21_c80f6d62-0971-4806-9aa5-9cc0d9e5599e.jpg}
}

 

API 통신 (with http 패키지)
import 'dart:convert';
import 'package:flutter_application_webtoon/models/webtoon_episode_model.dart';
import 'package:flutter_application_webtoon/models/webtoon_model.dart';
import 'package:flutter_application_webtoon/models/webtoon_detail_model.dart';
import 'package:http/http.dart' as http;
//as http 를 사용하면 http.~~~ 를 사용할 수 있음

class ApiService {
  static const String baseUrl =
      "https://webtoon-crawler.nomadcoders.workers.dev";
  static const String today = "today";

  static Future<List<WebtoonModel>> getTodayToons() async {
    //API로부터 웹튠 JSON 정보를 가져오고 가공시킨 값을 List로 반환하는 매소드
    List<WebtoonModel> webtoonIstances = [];
    final url = Uri.parse('$baseUrl/$today');
    final response = await http.get(url);
    //get을 통해 Future라는 타입의 결과 값을 받을 때까지 응답을 기다리기 위해 awiat과 async을 추가함
    //await, async를 사용할 때는 return 값에도 Future라고 명시해야함
    //Future란 당장 완료될 수 없는 작업을 말함

    if (response.statusCode == 200) {
      //statusCode 는 http 응답코드를 말함 200일 경우 OK(요청이 수행되었음)를 의미함
      final List<dynamic> webtoons = jsonDecode(response.body);
      for (var webtoon in webtoons) {
        webtoonIstances.add(WebtoonModel.fromJson(webtoon));
        //webtoonIstances 리스트에 WebtoonNodel 클래스의 fromJson named 생성자를 통해 webtoon을 가공하여 추가함
      }
      return webtoonIstances;
    }
    throw Error();
  }

  static Future<WebtoonDetailModel> getToonById(String id) async {
    final WebtoonDetailModel detailModel;
    final url = Uri.parse('$baseUrl/$id');
    final response = await http.get(url);

    if (response.statusCode == 200) {
      final webtoonDetail = jsonDecode(response.body);
      detailModel = WebtoonDetailModel.fromJson(webtoonDetail);
      return detailModel;
    }
    throw Error();
  }

  static Future<List<WebtoonEpisodeModel>> getEpisodeId(String id) async {
    final List<WebtoonEpisodeModel> episodeModels = [];
    final url = Uri.parse('$baseUrl/$id/episodes');
    final response = await http.get(url);

    if (response.statusCode == 200) {
      final webtoonEpisodes = jsonDecode(response.body);
      for (var episodes in webtoonEpisodes) {
        episodeModels.add(WebtoonEpisodeModel.fromJson(episodes));
      }
      return episodeModels;
    }
    throw Error();
  }
}

 

👆🏻위 소스(API 통신) 설명.

import 'package:http/http.dart' as http;

http 패키지를 임포트하고 **as http**를 사용하여 패키지에 대한 짧은 별칭을 정의합니다. 이렇게 하면 이후에 http 패키지의 함수나 클래스를 사용할 때 짧은 이름으로 접근할 수 있습니다.

네트워크 요청 보내기

final response = await http.get(url);

http.get() 함수를 사용하여 주어진 URL로 GET 요청을 보냅니다. 이 함수는 비동기적으로 작동하므로 await 키워드를 사용하여 응답을 기다립니다. 서버에서 응답이 도착하면 해당 응답이 response 변수에 저장됩니다.

응답 처리

if (response.statusCode == 200) {
  final List<dynamic> webtoons = jsonDecode(response.body);
  // 응답 데이터를 JSON으로 디코딩하여 리스트로 변환합니다.
  for (var webtoon in webtoons) {
    webtoonIstances.add(WebtoonModel.fromJson(webtoon));
    // 변환된 JSON 데이터를 모델로 변환하여 리스트에 추가합니다.
  }
  return webtoonIstances;
} else {
  throw Error();
}

응답 상태 코드가 200인지 확인하여 요청이 성공했는지 확인합니다. 성공한 경우, 응답 데이터를 JSON으로 디코딩하고 모델 객체로 변환하여 리스트에 추가합니다. 그렇지 않은 경우, 에러를 throw하여 예외 처리를 합니다.

이 소스 코드에서는 http 패키지를 사용하여 네트워크 요청을 보내고 받는 방법을 보여주고 있으며, JSON 데이터를 처리하여 Flutter 애플리케이션에서 사용할 수 있는 모델 객체로 변환하는 방법을 보여줍니다.

 

메인화면
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_application_webtoon/firebase_options.dart';
import 'package:flutter_application_webtoon/models/webtoon_model.dart';
import 'package:flutter_application_webtoon/services/api_service.dart';
import 'package:flutter_application_webtoon/widgets/webtoon_widget.dart';

void main() async {
  await ApiService.getTodayToons();
  //ApiService.getTodayToons: List<WebtoonModel> 를 return하는 메소드
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: HomeScreen(),
    );
  }
}

class HomeScreen extends StatelessWidget {
  HomeScreen({super.key});
  final Future<List<WebtoonModel>> webtoons = ApiService.getTodayToons();
  //ApiService.getTodayToons로 return 받은 List<WebtoonModel> 을 webtoons에 넣음

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
          title: Text(
        'WebToonAPI',
        style: TextStyle(
            color: Colors.purpleAccent.shade700,
            fontSize: 12,
            fontWeight: FontWeight.bold),
      )),
      body: FutureBuilder(
        //FutureBuilder: 비동기적으로 데이터(Future)를 가져올 때 사용하는 위젯(주로 Network요청,DB조회할 때 사용)
        future: webtoons,
        //furure: 처리할 future 객체를 전달
        builder: (context, futureResult) {
          //builder 콜백함수: Build할 context와 snapshot 이름을 정하고 future상태를 정해진 snapshot을 통해 체크함(hasData 등)
          if (futureResult.hasData) {
            //hasData: snapshot에 데이터가 있다면
            return Column(
              children: [
                const SizedBox(
                  height: 20,
                ),
                Expanded(child: makeList(futureResult)),
                //makeList
              ],
            );
          } else {
            return const Center(child: CircularProgressIndicator());
          }
        },
      ),
    );
  }

  ListView makeList(AsyncSnapshot<List<WebtoonModel>> futureResult) {
    return ListView.separated(
      scrollDirection: Axis.vertical,
      //scrollDirection: 스크롤 방향을 정함
      itemCount: futureResult.data!.length,
      //itemCount: 총 data의 수를 넣어 주면됨
      itemBuilder: (context, index) {
        //itemBuilder: 리스트 아이템을 생성할 위젯을 반환하는 함수로, 입력된 context와 index를 통해 아이템을 생성함
        var webtoon = futureResult.data![index];
        //위에 if문에 futureResult.hasData를 타고 들어왔기 때문에 data!로 값에 Null이 존재하지않다는 명시를 해줌
        return WebtoonListPage(
            title: webtoon.title, thumb: webtoon.thumb, id: webtoon.id);
      },
      separatorBuilder: (context, index) => const SizedBox(
        //separatorBuilder: ListView의 item과 함께 구분자로 사용할 위젯을 추가할 수 있음
        width: 20,
      ),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  List<WebtoonModel> webtoons = [];
  bool isLoading = true;
  void waitForwebToons() async {
    webtoons = await ApiService.getTodayToons();
    //.getTodayToons에서 webtoons로 List<WebtoonModel> 값을 return 해주고
    isLoading = false;
    setState(() {});
    //setState: 현재 상태를 적용한 화면을 다시 그려주는 역할
  }

  @override
  void initState() {
    //initState: 앱이 시작할때 동작(생명주기)
    super.initState();
    waitForwebToons();
  }

  @override
  void setState(VoidCallback fn) {
    super.setState(fn);
  }

  @override
  void dispose() {
    //dispose: 위젯이 제거되기 전에 한번만 호출되는 함수 (initState와 쌍을 이룸)
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      appBar: AppBar(
        elevation: 0,
        backgroundColor: Colors.white,
        title: Text(
          'WebToons',
          style: TextStyle(
              color: Colors.indigo.shade900, fontWeight: FontWeight.bold),
        ),
      ),
    );
  }
}

 

웹튠을 클릭했을 때 나오는 디테일 화면
import 'package:flutter/material.dart';
import 'package:flutter_application_webtoon/models/webtoon_detail_model.dart';
import 'package:flutter_application_webtoon/models/webtoon_episode_model.dart';
import 'package:flutter_application_webtoon/services/api_service.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../widgets/episode_widget.dart';

class DetailScreen extends StatefulWidget {
  final String title, thumb, id;

  const DetailScreen(
      {super.key, required this.title, required this.thumb, required this.id});

  @override
  State<DetailScreen> createState() => _DetailScreenState();
}

class _DetailScreenState extends State<DetailScreen> {
  late Future<List<WebtoonEpisodeModel>> webToonEpisode;
  late Future<WebtoonDetailModel> webToonDetail;
  //final Future<WebtoonDetailModel> webtoonDetail = ApiService.getToonById(widget.id); 이 안되는 이유는
  //constructor에서 extends로 상속받은 wiget(부모)이 참조될 수 없기 때문 그래서 initState에서 widget을 사용해서 받아와야함

  bool isLiked = false;
  late SharedPreferences preferences;
  Future initPreferences() async {
    preferences = await SharedPreferences.getInstance();
    final likedToons = preferences.getStringList('likedToons');
    if (likedToons != null) {
      if (likedToons.contains(widget.id) == true) {
        isLiked = true;
        setState(() {});
      }
    } else {
      preferences.setStringList('likedToons', []);
      setState(() {});
    }
  }

  void onClickLike() async {
    final likedToons = preferences.getStringList('likedToons');
    if (likedToons != null) {
      if (isLiked) {
        likedToons.remove(widget.id);
      } else {
        likedToons.add(widget.id);
      }
      await preferences.setStringList('likedToons', likedToons);
      setState(() {
        isLiked = !isLiked;
      });
    }
  }

  @override
  void initState() {
    webToonDetail = ApiService.getToonById(widget.id);
    webToonEpisode = ApiService.getEpisodeId(widget.id);
    initPreferences();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        actions: [
          IconButton(
              icon: Icon(isLiked ? Icons.favorite : Icons.favorite_border),
              onPressed: () => onClickLike())
        ],
        title: Text(
          widget.title,
          //title이 아닌 widget.title을 써야하는 이유는 Stateful이기때문에 부모 class에 가서 값을 받아야하기 때문
          style: TextStyle(
              color: Colors.green.shade900,
              fontSize: 20,
              fontWeight: FontWeight.bold),
        ),
      ),
      body: Hero(
        //Hero: tag에 유니크한 값을 갖는 변수를 넣어주면 Widget을 공유하여 화면 전환 할때 에니메이션 효과를 줄 수 있음
        tag: widget.id,
        child: SingleChildScrollView(
          child: Column(children: [
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Container(
                    clipBehavior: Clip.hardEdge,
                    height: 300,
                    decoration: const BoxDecoration(
                        borderRadius: BorderRadius.all(Radius.circular(100))),
                    padding: const EdgeInsets.only(left: 10, right: 10),
                    child: Image.network(widget.thumb, headers: const {
                      "User-Agent":
                          "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36"
                    })),
              ],
            ),
            FutureBuilder(
              future: webToonDetail,
              builder: (context, snapshot) {
                if (snapshot.hasData) {
                  return Padding(
                    padding: const EdgeInsets.all(40),
                    child: Column(
                      children: [
                        Row(
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: [
                            Text(snapshot.data!.genre),
                            const SizedBox(
                              width: 90,
                            ),
                            Text(snapshot.data!.age)
                          ],
                        ),
                        const SizedBox(
                          height: 20,
                        ),
                        Text(
                          snapshot.data!.about,
                          style: const TextStyle(),
                          softWrap: true,
                        ),
                      ],
                    ),
                  );
                } else {
                  return const CircularProgressIndicator();
                }
              },
            ),
            FutureBuilder(
              future: webToonEpisode,
              builder: (context, snapshot) {
                if (snapshot.hasData) {
                  return SizedBox(
                    height: 500,
                    child: ListView.separated(
                      padding: const EdgeInsets.only(
                          left: 20, right: 20, bottom: 400, top: 20),
                      shrinkWrap: true,
                      scrollDirection: Axis.vertical,
                      itemCount: snapshot.data!.length,
                      //snapshot.data는 future: 에 입력한 webToonEpisode를 말함
                      itemBuilder: (context, index) {
                        var episode = snapshot.data![index];
                        return Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: [
                            Episode(
                              episode: episode,
                              webtoonId: widget.id,
                            )
                          ],
                        );
                      },
                      separatorBuilder: (context, index) {
                        return const SizedBox(
                          height: 10,
                        );
                      },
                    ),
                  );
                } else {
                  return const CircularProgressIndicator();
                }
              },
            )
          ]),
        ),
      ),
    );
  }
}

 

 

 


 

2. 두번째 영화예매 앱

 

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:flutter_rating_bar/flutter_rating_bar.dart';
import 'package:http/http.dart' as http;

void main(List<String> args) {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: MovieHomePage(),
    );
  }
}

class MovieHomePage extends StatefulWidget {
  const MovieHomePage({super.key});

  @override
  State<MovieHomePage> createState() => _MovieHomePageState();
}

//위젯 작성
class _MovieHomePageState extends State<MovieHomePage> {
  Future<List<MovieInfo>> moviesPopular = ApiService.getPopularMovie();
  Future<List<MovieInfo>> moviesNowPlay = ApiService.getNowMovie();
  Future<List<MovieInfo>> movieSoon = ApiService.getSoonMovie();
  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: SingleChildScrollView(
      scrollDirection: Axis.vertical,
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.start,
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Container(
              padding: const EdgeInsets.symmetric(vertical: 20),
              decoration: const BoxDecoration(
                  borderRadius: BorderRadius.all(Radius.circular(20))),
              height: 280,
              child: Column(
                children: [
                  const Row(
                    mainAxisAlignment: MainAxisAlignment.start,
                    children: [
                      Text(
                        'Popular Movies',
                        style: TextStyle(
                            fontSize: 18, fontWeight: FontWeight.bold),
                      ),
                    ],
                  ),
                  const SizedBox(
                    height: 10,
                  ),
                  FutureBuilder(
                    future: moviesPopular,
                    builder: (context, snapshot) {
                      if (snapshot.hasData) {
                        return Expanded(
                          flex: 1,
                          child: ListView.separated(
                              scrollDirection: Axis.horizontal,
                              shrinkWrap: true,
                              itemBuilder: (context, index) {
                                var movie = snapshot.data![index];
                                return GestureDetector(
                                  onTap: () {
                                    Navigator.push(
                                        context,
                                        MaterialPageRoute(
                                          fullscreenDialog: true,
                                          builder: (context) => DetailPage(
                                              id: movie.id.toString()),
                                        ));
                                  },
                                  child: Hero(
                                    tag: movie.id.toString(),
                                    child: ClipRRect(
                                        borderRadius: const BorderRadius.all(
                                            Radius.circular(10)),
                                        child: SizedBox(
                                            width: 400,
                                            child: Image.network(
                                              movie.posterPath,
                                              fit: BoxFit.cover,
                                            ))),
                                  ),
                                );
                              },
                              separatorBuilder: (context, index) =>
                                  const SizedBox(
                                    width: 20,
                                    height: 10,
                                  ),
                              itemCount: snapshot.data!.length),
                        );
                      } else {
                        return const Center(
                          child: CircularProgressIndicator(),
                        );
                      }
                    },
                  )
                ],
              ),
            ),
            Container(
              padding: const EdgeInsets.symmetric(vertical: 10),
              decoration: const BoxDecoration(
                  borderRadius: BorderRadius.all(Radius.circular(20))),
              height: 280,
              child: Column(
                children: [
                  const Row(
                    children: [
                      Text(
                        'Now In Cinemas',
                        style: TextStyle(
                            fontSize: 18, fontWeight: FontWeight.bold),
                      ),
                    ],
                  ),
                  const SizedBox(
                    height: 10,
                  ),
                  FutureBuilder(
                    future: moviesNowPlay,
                    builder: (context, snapshot) {
                      if (snapshot.hasData) {
                        return Expanded(
                          flex: 1,
                          child: ListView.separated(
                              scrollDirection: Axis.horizontal,
                              shrinkWrap: true,
                              itemBuilder: (context, index) {
                                var movie = snapshot.data![index];
                                return Column(
                                  children: [
                                    SizedBox(
                                        width: 200,
                                        height: 200,
                                        child: GestureDetector(
                                          onTap: () {
                                            Navigator.push(
                                                context,
                                                MaterialPageRoute(
                                                    builder: (context) =>
                                                        DetailPage(
                                                            id: movie.id
                                                                .toString()),
                                                    fullscreenDialog: true));
                                          },
                                          child: ClipRRect(
                                              borderRadius:
                                                  const BorderRadius.all(
                                                      Radius.circular(20)),
                                              child: Image.network(
                                                movie.posterPath,
                                                fit: BoxFit.none,
                                              )),
                                        )),
                                    Text(
                                      movie.title,
                                      overflow: TextOverflow.ellipsis,
                                      maxLines: 1,
                                      style: const TextStyle(
                                          color: Colors.black87,
                                          fontWeight: FontWeight.w600,
                                          fontSize: 14),
                                    )
                                  ],
                                );
                              },
                              separatorBuilder: (context, index) =>
                                  const SizedBox(
                                    width: 20,
                                    height: 10,
                                  ),
                              itemCount: snapshot.data!.length),
                        );
                      } else {
                        return const Center(
                          child: CircularProgressIndicator(),
                        );
                      }
                    },
                  )
                ],
              ),
            ),
            Container(
              padding: const EdgeInsets.symmetric(vertical: 10),
              decoration: const BoxDecoration(
                  borderRadius: BorderRadius.all(Radius.circular(20))),
              height: 300,
              child: Column(
                children: [
                  const Row(
                    children: [
                      Text(
                        'Coming Soon',
                        style: TextStyle(
                            fontSize: 18, fontWeight: FontWeight.bold),
                      ),
                    ],
                  ),
                  const SizedBox(
                    height: 10,
                  ),
                  FutureBuilder(
                    future: movieSoon,
                    builder: (context, snapshot) {
                      if (snapshot.hasData) {
                        return Expanded(
                          flex: 1,
                          child: ListView.separated(
                              scrollDirection: Axis.horizontal,
                              shrinkWrap: true,
                              itemBuilder: (context, index) {
                                var movie = snapshot.data![index];
                                return GestureDetector(
                                  onTap: () {
                                    Navigator.push(
                                        context,
                                        MaterialPageRoute(
                                          builder: (context) => DetailPage(
                                              id: movie.id.toString()),
                                        ));
                                  },
                                  child: ClipRRect(
                                      borderRadius: const BorderRadius.all(
                                          Radius.circular(20)),
                                      child: Image.network(movie.posterPath)),
                                );
                              },
                              separatorBuilder: (context, index) =>
                                  const SizedBox(
                                    width: 20,
                                    height: 10,
                                  ),
                              itemCount: snapshot.data!.length),
                        );
                      } else {
                        return const Center(
                          child: CircularProgressIndicator(),
                        );
                      }
                    },
                  )
                ],
              ),
            )
          ],
        ),
      ),
    ));
  }
}

//디테일 페이지

class DetailPage extends StatelessWidget {
  final String id;
  final Future<MovieDetail> movie;
  DetailPage({Key? key, required this.id})
      : movie = ApiService.getMovieId(id),
        super(key: key);

  void onClick() {}
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        extendBodyBehindAppBar: true,
        appBar: AppBar(
          title: const Text('Back To The List'),
          backgroundColor: Colors.transparent,
          elevation: 0,
        ),
        body: SingleChildScrollView(
          scrollDirection: Axis.vertical,
          child: Column(
            children: [
              FutureBuilder(
                future: movie,
                builder: (context, snapshot) {
                  if (snapshot.hasData) {
                    var selectedMovie = snapshot.data;
                    return Hero(
                      transitionOnUserGestures: true,
                      tag: id,
                      child: Stack(
                        children: [
                          SizedBox(
                            height: 920,
                            child: Opacity(
                              opacity: 1,
                              child: ColorFiltered(
                                colorFilter: const ColorFilter.matrix([
                                  0.5,
                                  0,
                                  0,
                                  0,
                                  0,
                                  0,
                                  0.5,
                                  0,
                                  0,
                                  0,
                                  0,
                                  0,
                                  0.5,
                                  0,
                                  0,
                                  0,
                                  0,
                                  0,
                                  1,
                                  0,
                                ]),
                                child: Image.network(
                                  selectedMovie!.backdroppath.toString(),
                                  fit: BoxFit.cover,
                                ),
                              ),
                            ),
                          ),
                          Center(
                            child: Padding(
                              padding: const EdgeInsets.all(30),
                              child: Column(
                                crossAxisAlignment: CrossAxisAlignment.start,
                                mainAxisAlignment: MainAxisAlignment.start,
                                children: [
                                  const SizedBox(
                                    height: 300,
                                  ),
                                  Text(
                                    selectedMovie.title,
                                    style: const TextStyle(
                                        color: Colors.white,
                                        fontWeight: FontWeight.bold,
                                        fontSize: 30),
                                  ),
                                  const SizedBox(
                                    height: 10,
                                  ),
                                  RatingBar(
                                    initialRating:
                                        selectedMovie.voteaverage / 2,
                                    direction: Axis.horizontal,
                                    allowHalfRating: true,
                                    itemCount: 5,
                                    ratingWidget: RatingWidget(
                                      full: Icon(
                                        Icons.star,
                                        color: Colors.amber.shade400,
                                      ),
                                      half: Icon(
                                        Icons.star_half,
                                        color: Colors.amber.shade400,
                                      ),
                                      empty: Icon(
                                        Icons.star_border,
                                        color: Colors.amber.shade400,
                                      ),
                                    ),
                                    itemPadding: const EdgeInsets.symmetric(
                                        horizontal: 4.0),
                                    onRatingUpdate: (rating) {},
                                  ),
                                  const SizedBox(
                                    height: 5,
                                  ),
                                  Text(
                                    '${selectedMovie.releasedate} | ${selectedMovie.runtime ~/ 60} hour ${selectedMovie.runtime % 60} minute\n${selectedMovie.genres}',
                                    style: const TextStyle(
                                        color: Colors.white60, fontSize: 15),
                                  ),
                                  const SizedBox(
                                    height: 20,
                                  ),
                                  const Text(
                                    'Storyline',
                                    style: TextStyle(
                                        color: Colors.white,
                                        fontWeight: FontWeight.bold,
                                        fontSize: 30),
                                  ),
                                  const SizedBox(
                                    height: 10,
                                  ),
                                  Text(
                                    selectedMovie.overview,
                                    style: const TextStyle(
                                        color: Colors.white, fontSize: 15),
                                  ),
                                  const SizedBox(
                                    height: 60,
                                  ),
                                  Row(
                                    mainAxisAlignment: MainAxisAlignment.center,
                                    children: [
                                      TextButton(
                                          onPressed: onClick,
                                          child: Container(
                                              decoration: BoxDecoration(
                                                  color: Colors.amber.shade300,
                                                  borderRadius:
                                                      const BorderRadius.all(
                                                          Radius.circular(10))),
                                              padding:
                                                  const EdgeInsets.symmetric(
                                                      horizontal: 50,
                                                      vertical: 10),
                                              child: const Text(
                                                'Buy Ticket',
                                                style: TextStyle(
                                                    color: Colors.black,
                                                    fontSize: 15,
                                                    fontWeight:
                                                        FontWeight.w500),
                                              ))),
                                    ],
                                  ),
                                ],
                              ),
                            ),
                          )
                        ],
                      ),
                    );
                  } else {
                    return const Center(
                      child: CircularProgressIndicator(),
                    );
                  }
                },
              )
            ],
          ),
        ));
  }
}

//API 서버에서 받아온 JOSN 파싱
class ApiService {
  //URI 부분
  static const String baseUrl = "https://movies-api.nomadcoders.workers.dev";
  static const String imgUrl = "https://image.tmdb.org/t/p/w500";
  static const String getMovieIdUrl = "movie?id=";
  static const String popular = "popular";
  static const String nowPlay = "now-playing";
  static const String soon = "coming-soon";

  static Future<List<MovieInfo>> getPopularMovie() async {
    List<MovieInfo> movieInfo = [];
    final url = Uri.parse('$baseUrl/$popular');
    final response = await http.get(url);

    if (response.statusCode == 200) {
      final Map<String, dynamic> moviesJason = jsonDecode(response.body);
      final List<dynamic> movies = moviesJason['results'];
      //data.map((movies) => MovieInfo.fromJson(movies)).toList();
      for (var movie in movies) {
        movieInfo.add(MovieInfo.fromJson(movie));
      }
    }
    return movieInfo;
  }

  static Future<List<MovieInfo>> getNowMovie() async {
    List<MovieInfo> movieInfo = [];
    final url = Uri.parse('$baseUrl/$nowPlay');
    final response = await http.get(url);
    if (response.statusCode == 200) {
      final List<dynamic> movies = jsonDecode(response.body)['results'];
      for (var movie in movies) {
        movieInfo.add(MovieInfo.fromJson(movie));
      }
    }
    return movieInfo;
  }

  static Future<List<MovieInfo>> getSoonMovie() async {
    List<MovieInfo> movieInfo = [];
    final url = Uri.parse('$baseUrl/$soon');
    final response = await http.get(url);
    if (response.statusCode == 200) {
      final Map<String, dynamic> movieJson = jsonDecode(response.body);
      final List<dynamic> movies = movieJson['results'];
      for (var movie in movies) {
        movieInfo.add(MovieInfo.fromJson(movie));
      }
    }
    return movieInfo;
  }

  static Future<MovieDetail> getMovieId(String id) async {
    final url = Uri.parse('$baseUrl/$getMovieIdUrl$id');
    final response = await http.get(url);
    if (response.statusCode == 200) {
      final Map<String, dynamic> movieJson = jsonDecode(response.body);
      return MovieDetail.fromJson(movieJson);
    }
    throw Exception("Failed to get movie info");
  }
}

//JSON(영화정보 데이터) 파싱할 객체
class MovieInfo {
  final int id; //id
  final String title; //제목
  final String posterPath; //포스터 경로

  MovieInfo({
    required this.id,
    required this.title,
    required this.posterPath,
  });

  factory MovieInfo.fromJson(Map<String, dynamic> json) {
    return MovieInfo(
      id: json['id'],
      title: json['title'],
      posterPath: "https://image.tmdb.org/t/p/w500/${json['poster_path']}",
    );
  }
}

class MovieDetail {
  final int id; //id
  final String title; //제목
  final String backdroppath; //포스터 경로
  final String overview; //영화 개요
  final double voteaverage; //영화 평점
  final String releasedate; //개봉일
  final int runtime;
  final String genres;

  MovieDetail(
      {required this.id,
      required this.title,
      required this.backdroppath,
      required this.overview,
      required this.voteaverage,
      required this.releasedate,
      required this.runtime,
      required this.genres});

  factory MovieDetail.fromJson(Map<String, dynamic> json) {
    return MovieDetail(
        id: json['id'],
        title: json['title'],
        backdroppath: "https://image.tmdb.org/t/p/w500${json['backdrop_path']}",
        overview: json['overview'],
        voteaverage: json['vote_average'],
        releasedate: json['release_date'],
        runtime: json['runtime'],
        genres: getGenres(json['genres']));
  }

  static String getGenres(List<dynamic> json) {
    List<String> genreName = [];
    for (int i = 0; i < json.length; i++) {
      genreName.add(json[i]['name']);
    }
    return genreName.join(", ");
  }
}


다음 시간에는

새로운 API 서버로 부터 Riverpod 을 이용한 MVVM패턴을 적용하여 통신하는 기능을 구현하겠습니다.

http 패키지로 JSON 파일을 통신하기 위해 편의상 무료로 제공되는 picsum 이라는 사이트의 이미지 API를 사용하겠습니다.

- 무료 이미지 API 사이트: https://picsum.photos/

 

Lorem Picsum

Lorem Ipsum... but for photos

picsum.photos

 

- http 패키지 설치링크: https://pub.dev/packages/http/install

 

http install | Dart package

A composable, multi-platform, Future-based API for HTTP requests.

pub.dev

위 pub dev 링크에서 http 패키지를 설치해줍니다.


아래 링크로 API를 호출하면 JSON 데이터 구조를 알 수 있습니다.
이 구조를 Flutter에 정의하고, API 서버와 json 데이터를 주고 받을 메소드를 작성해줍니다.

- 이미지 리스트를 가져오는 URL: https://picsum.photos/v2/list

 

https://picsum.photos/v2/list

다은 포스팅에는 위 URL로 여러개의 이미지가 담긴 JSON 리스트 호출하고, 이를 받아오는 메소드를 만들어봅니다.

 

예시)

picsum.dart (Model)
class Picsum {
  final String id;
  final String author;
  final int width;
  final int height;
  final String url;
  final String download_url;

  Picsum(
      {required this.id,
      required this.author,
      required this.width,
      required this.height,
      required this.url,
      required this.download_url});

  //Json 데이터 > Map형으로 변환
  Picsum.fromJson(Map<String, dynamic> json)
      : id = json["id"],
        author = json["author"],
        width = json["width"],
        height = json["height"],
        url = json["url"],
        download_url = json["download_url"];

  //Map형 > Json 데이터로 변환
  Map<String, dynamic> toJson() {
    return {
      "id": id,
      "author": author,
      "width": width,
      "height": height,
      "url": url,
      "dounload_url": download_url
    };
  }
}

 

picsum_vm.dart (ViewModel)
final picsumProvider = AsyncNotifierProvider<PicsumViewModel, Picsum>(
  () => PicsumViewModel(),
);

class PicsumViewModel extends AsyncNotifier<Picsum> {

  Future<List<Picsum>> getPicsumList() async {
    final List<Picsum> picsumList = [];
    final response = await http.get(Uri.parse(getlist));
    if (response == 200) {
      final List<dynamic> picsums = jsonDecode(response.body);
      for (var picsum in picsums) {
        picsumList.add(Picsum.fromJson(picsum));
      }
      return picsumList;
    } else {
      throw Error();
    }
  }
  ...
picsum_screen.dart (View)

 

class _PicsumScreenState extends ConsumerState<PicsumScreen> {
  @override
  Widget build(BuildContext context) {
    final pic = ref.read(picsumProvider.notifier).getPicsum();
    return Scaffold(
        appBar: AppBar(
          title: const Text("http 패키지 예시"),
        ),
        body: Center(
          child: ListView.separated(
              itemBuilder: (context, index) {
                return ListTile(
                  title: Text(pic.id),
                  subtitle: Text(pic.url),
                  leading: const Icon(Icons.abc),
                );
              },
              separatorBuilder: (context, index) => const SizedBox(
                    height: 10,
                  ),
              itemCount: 10),
        ));
...
반응형
Comments