Last active
May 13, 2023 09:24
-
-
Save sharryhong/0adec40dcdb52ceb74d5d5bc4f0fdc85 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import 'dart:convert'; | |
| import 'package:flutter/material.dart'; | |
| import 'package:http/http.dart' as http; | |
| import 'package:url_launcher/url_launcher.dart'; | |
| class MovieModel { | |
| final int id; | |
| final String title, backdropPath; | |
| MovieModel.fromJson(Map<String, dynamic> json) | |
| : id = json['id'], | |
| title = json['title'], | |
| backdropPath = json['backdrop_path']; | |
| } | |
| class MoviesModel { | |
| final List<MovieModel> results; | |
| MoviesModel({ | |
| required this.results, | |
| }); | |
| MoviesModel.fromJson(Map<String, dynamic> json) | |
| : results = (json['results'] as List) | |
| .map((result) => MovieModel.fromJson(result)) | |
| .toList(); | |
| } | |
| class MovieDetailModel { | |
| final int id; | |
| final num voteAverage; | |
| final bool adult; | |
| final String title, posterPath, overview, homepage, releaseDate; | |
| final List genres; | |
| MovieDetailModel.fromJson(Map<String, dynamic> json) | |
| : id = json['id'], | |
| title = json['title'], | |
| adult = json['adult'], | |
| genres = json['genres'], | |
| overview = json['overview'], | |
| homepage = json['homepage'], | |
| releaseDate = json['release_date'], | |
| voteAverage = json['vote_average'], | |
| posterPath = json['poster_path']; | |
| } | |
| class ApiService { | |
| static const String baseUrl = "https://movies-api.nomadcoders.workers.dev"; | |
| static Future<List<MovieModel>> getMovies(String path) async { | |
| List<MovieModel> movieInstances = []; | |
| final url = Uri.parse('$baseUrl/$path'); | |
| final response = await http.get(url); | |
| if (response.statusCode == 200) { | |
| MoviesModel movies = MoviesModel.fromJson(jsonDecode(response.body)); | |
| for (var movie in movies.results) { | |
| movieInstances.add(movie); | |
| } | |
| return movieInstances; | |
| } | |
| throw Error(); | |
| } | |
| static Future<List<MovieModel>> getPopularMovies() async { | |
| return getMovies('popular'); | |
| } | |
| static Future<List<MovieModel>> getNowPlayingMovies() async { | |
| return getMovies('now-playing'); | |
| } | |
| static Future<List<MovieModel>> getComingSoonMovies() async { | |
| return getMovies('coming-soon'); | |
| } | |
| static Future<MovieDetailModel> getDetailMovie(int id) async { | |
| final url = Uri.parse('$baseUrl/movie?id=$id'); | |
| final response = await http.get(url); | |
| if (response.statusCode == 200) { | |
| final movie = jsonDecode(response.body); | |
| return MovieDetailModel.fromJson(movie); | |
| } | |
| throw Error(); | |
| } | |
| } | |
| void main() { | |
| runApp(const App()); | |
| } | |
| class App extends StatelessWidget { | |
| const App({super.key}); | |
| @override | |
| Widget build(BuildContext context) { | |
| return MaterialApp( | |
| home: MovieHomeScreen(), | |
| ); | |
| } | |
| } | |
| class MovieHomeScreen extends StatelessWidget { | |
| MovieHomeScreen({super.key}); | |
| final Future<List<MovieModel>> popularMovies = ApiService.getPopularMovies(); | |
| final Future<List<MovieModel>> nowPlayingovies = | |
| ApiService.getNowPlayingMovies(); | |
| final Future<List<MovieModel>> comingSoonMovies = | |
| ApiService.getComingSoonMovies(); | |
| @override | |
| Widget build(BuildContext context) { | |
| return Scaffold( | |
| backgroundColor: Colors.white, | |
| appBar: AppBar( | |
| title: const Text( | |
| 'Movieflix', | |
| style: TextStyle( | |
| fontSize: 20, | |
| ), | |
| ), | |
| backgroundColor: Colors.white, | |
| elevation: 0, | |
| ), | |
| body: SingleChildScrollView( | |
| child: Padding( | |
| padding: const EdgeInsets.all(20), | |
| child: Column( | |
| children: [ | |
| FutureBuilder( | |
| future: popularMovies, | |
| builder: (context, snapshot) { | |
| if (snapshot.hasData) { | |
| return Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| const Text( | |
| 'Popular Movies', | |
| style: TextStyle( | |
| fontSize: 22, | |
| fontWeight: FontWeight.bold, | |
| ), | |
| ), | |
| const SizedBox( | |
| height: 20, | |
| ), | |
| SizedBox( | |
| height: 220, | |
| child: makeListPoster(snapshot), | |
| ) | |
| ], | |
| ); | |
| } | |
| return const Center( | |
| child: CircularProgressIndicator(), | |
| ); | |
| }, | |
| ), | |
| const SizedBox( | |
| height: 30, | |
| ), | |
| FutureBuilder( | |
| future: nowPlayingovies, | |
| builder: (context, snapshot) { | |
| if (snapshot.hasData) { | |
| return Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| const Text( | |
| 'Now In Cinemas', | |
| style: TextStyle( | |
| fontSize: 22, | |
| fontWeight: FontWeight.bold, | |
| ), | |
| ), | |
| const SizedBox( | |
| height: 20, | |
| ), | |
| SizedBox( | |
| height: 220, | |
| child: makeListPosterTitle(snapshot), | |
| ) | |
| ], | |
| ); | |
| } | |
| return const Text(''); | |
| }, | |
| ), | |
| const SizedBox( | |
| height: 30, | |
| ), | |
| FutureBuilder( | |
| future: comingSoonMovies, | |
| builder: (context, snapshot) { | |
| if (snapshot.hasData) { | |
| return Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| const Text( | |
| 'Coming soon', | |
| style: TextStyle( | |
| fontSize: 22, | |
| fontWeight: FontWeight.bold, | |
| ), | |
| ), | |
| const SizedBox( | |
| height: 20, | |
| ), | |
| SizedBox( | |
| height: 220, | |
| child: makeListPosterTitle(snapshot), | |
| ) | |
| ], | |
| ); | |
| } | |
| return const Text(''); | |
| }, | |
| ), | |
| ], | |
| ), | |
| ), | |
| ), | |
| ); | |
| } | |
| ListView makeListPoster(AsyncSnapshot<List<MovieModel>> snapshot) { | |
| return ListView.separated( | |
| scrollDirection: Axis.horizontal, | |
| itemCount: snapshot.data!.length, | |
| itemBuilder: (context, index) { | |
| var movie = snapshot.data?[index]; | |
| return MovieButton( | |
| movie: movie, | |
| widget: Container( | |
| clipBehavior: Clip.hardEdge, | |
| decoration: BoxDecoration( | |
| borderRadius: BorderRadius.circular(10), | |
| ), | |
| child: Image.network( | |
| 'https://image.tmdb.org/t/p/w500/${movie!.backdropPath}', | |
| fit: BoxFit.cover, | |
| height: 220, | |
| width: 168 * (16 / 9), | |
| ), | |
| ), | |
| ); | |
| }, | |
| separatorBuilder: (context, index) => const SizedBox(width: 14), | |
| ); | |
| } | |
| ListView makeListPosterTitle(AsyncSnapshot<List<MovieModel>> snapshot) { | |
| return ListView.separated( | |
| scrollDirection: Axis.horizontal, | |
| itemCount: snapshot.data!.length, | |
| itemBuilder: (context, index) { | |
| var movie = snapshot.data?[index]; | |
| return MovieButton( | |
| movie: movie, | |
| widget: Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| Container( | |
| clipBehavior: Clip.hardEdge, | |
| decoration: BoxDecoration( | |
| borderRadius: BorderRadius.circular(10), | |
| ), | |
| child: Image.network( | |
| 'https://image.tmdb.org/t/p/w500/${movie!.backdropPath}', | |
| fit: BoxFit.cover, | |
| height: 160, | |
| width: 80 * (16 / 9), | |
| ), | |
| ), | |
| const SizedBox( | |
| height: 7, | |
| ), | |
| SizedBox( | |
| width: 80, | |
| child: Text( | |
| movie.title, | |
| maxLines: 3, | |
| overflow: TextOverflow.ellipsis, | |
| style: const TextStyle( | |
| fontWeight: FontWeight.bold, | |
| ), | |
| ), | |
| ), | |
| ], | |
| ), | |
| ); | |
| }, | |
| separatorBuilder: (context, index) => const SizedBox(width: 14), | |
| ); | |
| } | |
| } | |
| class MovieButton extends StatelessWidget { | |
| final MovieModel? movie; | |
| final Widget? widget; | |
| const MovieButton({ | |
| super.key, | |
| required this.movie, | |
| this.widget, | |
| }); | |
| void _onTapMovie(BuildContext context) { | |
| Navigator.push( | |
| context, | |
| MaterialPageRoute( | |
| builder: (context) => MovieDetailScreen(id: movie!.id), | |
| ), | |
| ); | |
| } | |
| @override | |
| Widget build(BuildContext context) { | |
| return GestureDetector( | |
| onTap: () => _onTapMovie(context), | |
| child: widget, | |
| ); | |
| } | |
| } | |
| class MovieDetailScreen extends StatefulWidget { | |
| final int id; | |
| const MovieDetailScreen({super.key, required this.id}); | |
| @override | |
| State<MovieDetailScreen> createState() => _MovieDetailScreenState(); | |
| } | |
| class _MovieDetailScreenState extends State<MovieDetailScreen> { | |
| late Future<MovieDetailModel> movieDetail; | |
| void _onTapBackToList() { | |
| Navigator.push( | |
| context, | |
| MaterialPageRoute( | |
| builder: (context) => MovieHomeScreen(), | |
| ), | |
| ); | |
| } | |
| String _getGenres(List genres) { | |
| return genres.map((item) => item['name']).join(', '); | |
| } | |
| _onTapHomepage(String url) async { | |
| final Uri homepage = Uri.parse(url); | |
| if (!await launchUrl(homepage)) { | |
| throw Exception('Could not launch'); | |
| } | |
| } | |
| @override | |
| void initState() { | |
| super.initState(); | |
| movieDetail = ApiService.getDetailMovie(widget.id); | |
| } | |
| @override | |
| Widget build(BuildContext context) { | |
| return Scaffold( | |
| body: FutureBuilder( | |
| future: movieDetail, | |
| builder: (context, snapshot) { | |
| if (snapshot.hasData) { | |
| return Stack( | |
| fit: StackFit.expand, | |
| children: [ | |
| Image.network( | |
| 'https://image.tmdb.org/t/p/w500/${snapshot.data!.posterPath}', | |
| fit: BoxFit.cover, | |
| ), | |
| Container( | |
| decoration: BoxDecoration( | |
| gradient: LinearGradient( | |
| begin: Alignment.topCenter, | |
| end: Alignment.bottomCenter, | |
| colors: [ | |
| Colors.black.withOpacity(0.2), | |
| Colors.black.withOpacity(1), | |
| ], | |
| ), | |
| ), | |
| ), | |
| Positioned( | |
| top: 60, | |
| left: 10, | |
| child: GestureDetector( | |
| onTap: _onTapBackToList, | |
| child: Row( | |
| children: const [ | |
| Icon( | |
| Icons.chevron_left, | |
| color: Colors.white, | |
| ), | |
| SizedBox( | |
| width: 10, | |
| ), | |
| Text( | |
| 'Back to list', | |
| style: TextStyle( | |
| color: Colors.white, | |
| fontSize: 16, | |
| ), | |
| ), | |
| ], | |
| ), | |
| ), | |
| ), | |
| Positioned( | |
| width: MediaQuery.of(context).size.width, | |
| bottom: 30, | |
| child: Padding( | |
| padding: const EdgeInsets.all(16), | |
| child: Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| Text( | |
| snapshot.data!.title, | |
| style: const TextStyle( | |
| color: Colors.white, | |
| fontSize: 28, | |
| fontWeight: FontWeight.bold, | |
| ), | |
| ), | |
| const SizedBox( | |
| height: 16, | |
| ), | |
| Row( | |
| children: [ | |
| Text( | |
| snapshot.data!.releaseDate, | |
| style: const TextStyle( | |
| color: Colors.white, | |
| fontSize: 16, | |
| ), | |
| ), | |
| const SizedBox( | |
| width: 6, | |
| ), | |
| const Text( | |
| '|', | |
| style: TextStyle( | |
| color: Colors.white, | |
| fontSize: 16, | |
| ), | |
| ), | |
| const SizedBox( | |
| width: 6, | |
| ), | |
| Row( | |
| children: [ | |
| const Icon( | |
| Icons.star, | |
| color: Colors.yellow, | |
| size: 16, | |
| ), | |
| const SizedBox( | |
| width: 3, | |
| ), | |
| Text( | |
| '${snapshot.data!.voteAverage}', | |
| style: const TextStyle( | |
| color: Colors.white, | |
| fontSize: 16, | |
| ), | |
| ), | |
| ], | |
| ), | |
| ], | |
| ), | |
| const SizedBox( | |
| height: 6, | |
| ), | |
| Row( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| Text( | |
| snapshot.data!.adult ? 'Adult only' : 'All ages', | |
| style: const TextStyle( | |
| color: Colors.white, | |
| fontSize: 16, | |
| ), | |
| ), | |
| const SizedBox( | |
| width: 6, | |
| ), | |
| const Text( | |
| '|', | |
| style: TextStyle( | |
| color: Colors.white, | |
| fontSize: 16, | |
| ), | |
| ), | |
| const SizedBox( | |
| width: 6, | |
| ), | |
| SizedBox( | |
| width: MediaQuery.of(context).size.width - 110, | |
| child: Text( | |
| _getGenres(snapshot.data!.genres), | |
| style: const TextStyle( | |
| color: Colors.white, | |
| fontSize: 16, | |
| ), | |
| ), | |
| ), | |
| ], | |
| ), | |
| const SizedBox( | |
| height: 40, | |
| ), | |
| const Text( | |
| 'Storyline', | |
| style: TextStyle( | |
| color: Colors.white, | |
| fontSize: 22, | |
| fontWeight: FontWeight.bold, | |
| ), | |
| ), | |
| const SizedBox( | |
| height: 16, | |
| ), | |
| SizedBox( | |
| width: MediaQuery.of(context).size.width - 70, | |
| child: Text( | |
| snapshot.data!.overview, | |
| style: const TextStyle( | |
| color: Colors.white, | |
| fontSize: 16, | |
| height: 1.5, | |
| ), | |
| ), | |
| ), | |
| const SizedBox( | |
| height: 40, | |
| ), | |
| Center( | |
| child: FractionallySizedBox( | |
| widthFactor: 0.8, | |
| child: GestureDetector( | |
| onTap: () => | |
| _onTapHomepage(snapshot.data!.homepage), | |
| child: Container( | |
| width: 300, | |
| padding: const EdgeInsets.symmetric( | |
| vertical: 16, | |
| ), | |
| decoration: BoxDecoration( | |
| color: Colors.yellow, | |
| borderRadius: BorderRadius.circular(10), | |
| ), | |
| child: const Text( | |
| 'Homepage', | |
| textAlign: TextAlign.center, | |
| style: TextStyle( | |
| fontSize: 16, | |
| fontWeight: FontWeight.bold), | |
| ), | |
| ), | |
| ), | |
| ), | |
| ), | |
| ], | |
| ), | |
| ), | |
| ), | |
| ], | |
| ); | |
| } | |
| return const Center( | |
| child: CircularProgressIndicator(), | |
| ); | |
| }, | |
| ), | |
| ); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment