Skip to content

Instantly share code, notes, and snippets.

@hyeoksuhan
Last active April 15, 2023 11:39
Show Gist options
  • Save hyeoksuhan/da592d40642d7c81635f28ead64c0264 to your computer and use it in GitHub Desktop.
Save hyeoksuhan/da592d40642d7c81635f28ead64c0264 to your computer and use it in GitHub Desktop.
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'hello movie world',
home: MainScreen(),
);
}
}
class MainScreen extends StatefulWidget {
const MainScreen({super.key});
@override
State<MainScreen> createState() => _MainScreenState();
}
class _MainScreenState extends State<MainScreen> {
late final Future<List<MainMovieModel>> popularMovies;
late final Future<List<MainMovieModel>> nowInCinemasMovies;
late final Future<List<MainMovieModel>> comingSoonMovies;
@override
initState() {
super.initState();
popularMovies = APIHelper.popularMovies();
nowInCinemasMovies = APIHelper.nowInCinemas();
comingSoonMovies = APIHelper.commingSoon();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: Padding(
padding: const EdgeInsets.all(10),
child: Column(
children: [
const SizedBox(height: 90),
FutureBuilder(
future: popularMovies,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator(),
);
}
final movies = snapshot.data!;
return MainMoviePopularSection(
title: 'Popular Movies',
data: movies,
);
},
),
const SizedBox(
height: 30,
),
FutureBuilder(
future: nowInCinemasMovies,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator(),
);
}
final movies = snapshot.data!;
return MainMovieSection(
title: 'Now in Cinemas',
data: movies,
);
},
),
const SizedBox(
height: 10,
),
FutureBuilder(
future: comingSoonMovies,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator(),
);
}
final movies = snapshot.data!;
return MainMovieSection(
title: 'Coming soon',
data: movies,
);
},
),
],
),
),
);
}
}
class MainMoviePopularSection extends StatelessWidget {
final List<MainMovieModel> data;
final String title;
const MainMoviePopularSection({
super.key,
required this.data,
required this.title,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
color: Colors.black,
fontSize: 20,
fontWeight: FontWeight.w800,
),
),
const SizedBox(
height: 15,
),
SizedBox(
height: 180,
child: ListView.separated(
scrollDirection: Axis.horizontal,
separatorBuilder: (context, index) => const SizedBox(
width: 10,
),
itemCount: data.length,
itemBuilder: (context, index) {
final item = data[index];
return GestureDetector(
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (context) {
return DetailMovieScreen(
id: item.id,
posterURL: item.posterURL,
);
}));
},
child: MovieThumbnail(
url: item.backdropURL,
alt: item.title,
),
);
},
),
),
],
);
}
}
class MainMovieSection extends StatelessWidget {
final List<MainMovieModel> data;
final String title;
const MainMovieSection({
required this.data,
required this.title,
super.key,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
color: Colors.black,
fontSize: 20,
fontWeight: FontWeight.w800,
),
),
const SizedBox(
height: 15,
),
SizedBox(
height: 180,
child: ListView.separated(
scrollDirection: Axis.horizontal,
separatorBuilder: (context, index) => const SizedBox(
width: 10,
),
itemCount: data.length,
itemBuilder: (context, index) {
final item = data[index];
return GestureDetector(
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (context) {
return DetailMovieScreen(
id: item.id,
posterURL: item.posterURL,
);
}));
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MovieThumbnail(
url: item.backdropURL,
alt: item.title,
width: 120,
height: 120,
fit: BoxFit.cover,
),
const SizedBox(
height: 10,
),
SizedBox(
width: 120,
child: Text(
item.title,
style: const TextStyle(
fontSize: 12, fontWeight: FontWeight.w700),
),
),
],
),
);
},
),
),
],
);
}
}
class MovieThumbnail extends StatelessWidget {
final String? url;
final String alt;
final BoxFit? fit;
final double? width;
final double? height;
const MovieThumbnail({
required this.alt,
this.url,
this.fit,
this.width,
this.height,
super.key,
});
@override
Widget build(BuildContext context) {
return Container(
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(borderRadius: BorderRadius.circular(10)),
child: url == null
? Container(
width: 200,
alignment: Alignment.center,
color: Colors.black12,
child: Text(alt),
)
: Image.network(
url!,
width: width,
height: height,
fit: fit,
),
);
}
}
typedef JSON = Map<String, dynamic>;
class APIHelper {
static const baseURL = 'movies-api.nomadcoders.workers.dev';
static Future<List<MainMovieModel>> popularMovies() async {
return mainMovies('popular');
}
static Future<List<MainMovieModel>> nowInCinemas() async {
return mainMovies('now-playing');
}
static Future<List<MainMovieModel>> commingSoon() async {
return mainMovies('coming-soon');
}
static Future<DetailMovieModel> movieDetailById(int id) async {
final url = Uri.https(baseURL, 'movie', {'id': '$id'});
final response = await http.get(url);
if (response.statusCode != 200) {
throw Error();
}
final JSON raw = jsonDecode(response.body);
return DetailMovieModel.fromJson(raw);
}
static Future<List<MainMovieModel>> mainMovies(String path) async {
final url = Uri.https(baseURL, path);
final response = await http.get(url);
if (response.statusCode != 200) {
throw Error();
}
final List<MainMovieModel> models = [];
final JSON raw = jsonDecode(response.body);
final List<dynamic> results = raw['results'];
for (var result in results) {
models.add(MainMovieModel.fromJson(result));
}
return models;
}
}
class MainMovieModel {
late final int id;
late final String title;
late final String? backdropURL;
late final String posterURL;
static const baseImageURL = 'https://image.tmdb.org/t/p/w500';
MainMovieModel({
required this.id,
required this.title,
required this.backdropURL,
required this.posterURL,
});
factory MainMovieModel.fromJson(JSON json) {
final String? backdropPath = json['backdrop_path'];
final String posterPath = json['poster_path'];
return MainMovieModel(
id: json['id'],
title: json['title'],
posterURL: '$baseImageURL$posterPath',
backdropURL:
backdropPath == null ? null : '$baseImageURL$backdropPath');
}
}
class DetailMovieModel {
final String title;
final int runtime;
final String overview;
final double rating;
final String genres;
final String posterURL;
DetailMovieModel({
required this.title,
required this.runtime,
required this.overview,
required this.rating,
required this.genres,
required this.posterURL,
});
factory DetailMovieModel.fromJson(JSON json) {
double rating = json['vote_average'].toDouble();
List<String> genres = [];
List<dynamic> genreInfos = json['genres'];
for (var genreInfo in genreInfos) {
genres.add(genreInfo['name']);
}
final posterPath = json['poster_path'];
return DetailMovieModel(
title: json['title'],
runtime: json['runtime'],
overview: json['overview'],
rating: rating,
genres: genres.join(','),
posterURL: 'https://image.tmdb.org/t/p/w500$posterPath',
);
}
}
class DetailMovieScreen extends StatefulWidget {
final int id;
final String posterURL;
const DetailMovieScreen({
required this.id,
required this.posterURL,
super.key,
});
@override
State<DetailMovieScreen> createState() => _DetailMovieScreenState();
}
class _DetailMovieScreenState extends State<DetailMovieScreen> {
late final Future<DetailMovieModel> detailMovie;
@override
initState() {
super.initState();
detailMovie = APIHelper.movieDetailById(widget.id);
}
String runtimeString(int runtime) {
final h = (runtime / 60).floor();
final m = runtime % 60;
return h > 0 ? '${h}h ${m}min' : '${m}min';
}
int starCount(double rating, double maxRating, int maxStarCount) {
final ratio = (maxRating / maxStarCount).floor();
return (rating / ratio).floor();
}
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
image: DecorationImage(
image: NetworkImage(widget.posterURL),
fit: BoxFit.cover,
opacity: 0.5,
),
),
child: Scaffold(
backgroundColor: Colors.transparent,
body: FutureBuilder(
future: detailMovie,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Container();
}
final data = snapshot.data!;
const maxStarCount = 5;
final stars = starCount(data.rating, 10.0, maxStarCount);
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 25,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 200),
Text(
data.title,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
Row(
children: [
for (int i = 0; i < stars; i++)
const Icon(
Icons.star,
color: Colors.yellow,
),
for (int i = 0; i < maxStarCount - stars; i++)
const Icon(
Icons.star,
color: Colors.grey,
),
],
),
const SizedBox(height: 15),
Row(
children: [
Text(
'${runtimeString(data.runtime)} | ${data.genres}',
style: const TextStyle(
color: Colors.white,
),
),
],
),
const SizedBox(height: 25),
const Text(
'Storyline',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 20,
),
),
const SizedBox(
height: 10,
),
Text(
data.overview,
style: const TextStyle(
color: Colors.white,
),
),
const Expanded(child: SizedBox()),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 15,
),
margin: const EdgeInsets.symmetric(horizontal: 50),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Colors.yellow,
),
alignment: Alignment.bottomCenter,
child: const Text('Buy ticket',
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
)),
)
],
),
);
},
),
appBar: AppBar(
elevation: 0,
title: const Text('Back to list',
style: TextStyle(
fontSize: 16,
)),
centerTitle: false,
backgroundColor: Colors.transparent,
),
),
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment