일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- HTML
- Flutter
- React JS
- 리엑트
- 플러터
- Maui
- Firebase
- 깃허브
- AnimationController
- MS-SQL
- 자바스크립트
- Animation
- spring boot
- 닷넷
- 바인딩
- 애니메이션
- 오류
- GitHub
- page
- db
- Binding
- MSSQL
- .NET
- MVVM
- 파이어베이스
- 함수
- JavaScript
- typescript
- listview
- 마우이
- Today
- Total
개발노트
74. [Flutter] http 패키지 사용하기 (API 서버 통신) 본문
http 패키지
Flutter에서 네트워크 요청을 보내고 받는 데 사용되는 가장 일반적인 패키지 중 하나가 http 패키지입니다.
이 패키지는 HTTP 요청을 만들고 응답을 처리하는 데 도움이 되는 다양한 클래스와 함수를 제공합니다.
HTTP 패키지의 주요 특징
- 간단한 인터페이스: http 패키지는 간단한 API를 제공하여 HTTP 요청을 쉽게 생성하고 수행할 수 있습니다. 이를 통해 개발자는 쉽게 네트워크 요청을 관리할 수 있습니다.
- 비동기 지원: 대부분의 네트워크 작업은 비동기적으로 처리되므로 http 패키지는 Future 기반 API를 제공하여 비동기 코드 작성을 간단하게 해줍니다. 이를 통해 UI를 차단하지 않고 네트워크 요청을 수행할 수 있습니다.
- HTTP 요청 설정: http 패키지를 사용하면 HTTP 요청을 보낼 때 다양한 설정을 할 수 있습니다. 헤더, 쿼리 매개변수, 요청 본문 등을 쉽게 추가하고 수정할 수 있습니다.
- 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/
- http 패키지 설치링크: https://pub.dev/packages/http/install
위 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),
));
...
'앱 개발 > Flutter' 카테고리의 다른 글
76. [Flutter] 웹소켓 통신하기 (웹소켓과 http와의 차이) (0) | 2024.08.06 |
---|---|
75. [Flutter] API 서버 통신에 상태 관리 적용하기 (http, riverpod 패키지 응용) (0) | 2024.04.24 |
73. [Flutter] GoRoute, Stack, OffStage 조합으로 bottomNavigationBar 화면 만들기 (0) | 2024.03.07 |
72. [Flutter] MediaQueryData로 BuildContext 없이 기기 size 구하기(Device Size 가져오기) (0) | 2024.02.28 |
71. [Flutter] flutter_animate 패키지로 손쉽게 애니메이션 효과 주기 (1) | 2024.02.26 |