Movie Data Model and Provider in Flutter
Movie Data Model and Provider in Flutter
dart';
class Movie {
final int id;
final String title;
final String posterPath;
final String backdropPath;
final String overview;
final double voteAverage;
final String releaseDate;
final List<int> genreIds;
Movie({
required [Link],
required [Link],
required [Link],
required [Link],
required [Link],
required [Link],
required [Link],
required [Link],
});
MovieDetail({
required int id,
required String title,
required String posterPath,
required String backdropPath,
required String overview,
required double voteAverage,
required String releaseDate,
required List<int> genreIds,
required [Link],
required [Link],
required [Link],
[Link],
[Link] = const [],
}) : super(
id: id,
title: title,
posterPath: posterPath,
backdropPath: backdropPath,
overview: overview,
voteAverage: voteAverage,
releaseDate: releaseDate,
genreIds: genreIds,
);
return MovieDetail(
id: json['id'] ?? 0,
title: json['title'] ?? 'Unknown Title',
posterPath: json['poster_path'] ?? '',
backdropPath: json['backdrop_path'] ?? '',
overview: json['overview'] ?? 'No overview available',
voteAverage: (json['vote_average'] != null)
? (json['vote_average'] as num).toDouble()
: 0.0,
releaseDate: json['release_date'] ?? '',
genreIds: [], // Not available in detail endpoint
runtime: json['runtime'] ?? 0,
genres: json['genres'] != null
? (json['genres'] as List).map((genre) => [Link](genre)).toList()
: [],
status: json['status'] ?? 'Unknown',
tagline: json['tagline'],
videos: videosList,
);
}
class Genre {
final int id;
final String name;
Genre({
required [Link],
required [Link],
});
class Video {
final String id;
final String key;
final String name;
final String site;
final String type;
final bool official;
Video({
required [Link],
required [Link],
required [Link],
required [Link],
required [Link],
required [Link],
});
String getYoutubeThumbnail() {
if ([Link]() == 'youtube') {
return '[Link]
}
return '';
}
}
import 'package:flutter/[Link]';
import '../models/[Link]';
import '../services/api_service.dart';
MovieDetail? _selectedMovie;
// Getters
List<Movie> get popularMovies => _popularMovies;
List<Movie> get topRatedMovies => _topRatedMovies;
List<Movie> get upcomingMovies => _upcomingMovies;
List<Movie> get nowPlayingMovies => _nowPlayingMovies;
List<Movie> get searchResults => _searchResults;
MovieDetail? get selectedMovie => _selectedMovie;
bool get isLoading => _isLoading;
String get error => _error;
await fetchPopularMovies();
await fetchTopRatedMovies();
await fetchUpcomingMovies();
await fetchNowPlayingMovies();
} catch (e) {
_error = [Link]();
} finally {
_isLoading = false;
notifyListeners();
}
}
// Search movies
Future<void> searchMovies(String query) async {
try {
_isLoading = true;
notifyListeners();
import 'package:flutter/[Link]';
import 'package:provider/[Link]';
import '../models/[Link]';
import '../providers/movie_provider.dart';
import '../utils/[Link]';
import '../widgets/movie_card.dart';
const AllMoviesScreen({
Key? key,
required [Link],
required [Link],
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: [Link],
appBar: AppBar(
title: Text(title),
backgroundColor: [Link],
foregroundColor: [Link],
),
body: Consumer<MovieProvider>(
builder: (context, movieProvider, child) {
List<Movie> movies = [];
if ([Link]) {
return const Center(
child: CircularProgressIndicator(
color: [Link],
),
);
}
return [Link](
padding: const [Link](16),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 0.7,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
),
itemCount: [Link],
itemBuilder: (context, index) {
return MovieCard(
movie: movies[index],
height: [Link],
);
},
);
},
),
);
}
}
import 'package:flutter/[Link]';
import 'package:provider/[Link]';
import 'package:intl/[Link]';
import '../models/[Link]';
import '../providers/movie_provider.dart';
import '../utils/[Link]';
import '../widgets/movie_slider.dart';
import 'search_screen.dart';
import 'all_movies_screen.dart';
@override
State<HomeScreen> createState() => _HomeScreenState();
}
@override
void didChangeDependencies() {
if (!_isInit) {
[Link]<MovieProvider>(context, listen: false).initialize();
_isInit = true;
}
[Link]();
}
return CustomScrollView(
slivers: [
// Featured Movie Section
if ([Link])
SliverToBoxAdapter(
child: FeaturedMovieSection(
movie: [Link],
),
),
SliverPadding(
padding: const [Link](bottom: 24.0),
sliver: SliverToBoxAdapter(
child: MovieSlider(
movies: [Link],
isFullWidth: false,
),
),
),
SliverPadding(
padding: const [Link](bottom: 80.0),
sliver: SliverToBoxAdapter(
child: MovieSlider(
movies: [Link],
isFullWidth: false,
),
),
),
],
);
}
Widget _buildShowsTab() {
return Center(
child: Column(
mainAxisAlignment: [Link],
children: [
Icon(
[Link],
size: 80,
color: [Link](0.7),
),
const SizedBox(height: 16),
Text(
'TV Shows Coming Soon',
style: [Link](context).[Link],
),
const SizedBox(height: 8),
Text(
'This feature is currently under development',
style: [Link](context).[Link],
textAlign: [Link],
),
],
),
);
}
Widget _buildWatchlistTab() {
return Center(
child: Column(
mainAxisAlignment: [Link],
children: [
Icon(
[Link],
size: 80,
color: [Link](0.7),
),
const SizedBox(height: 16),
Text(
'Your Watchlist',
style: [Link](context).[Link],
),
const SizedBox(height: 8),
Text(
'Movies you save will appear here',
style: [Link](context).[Link],
textAlign: [Link],
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () {
setState(() {
_currentIndex = 0;
});
},
style: [Link](
backgroundColor: [Link],
foregroundColor: [Link],
padding: const [Link](horizontal: 24, vertical: 12),
),
child: const Text('Browse Movies'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: [Link],
body: Consumer<MovieProvider>(
builder: (context, movieProvider, child) {
if (_currentIndex == 0) {
return _buildMoviesTab(movieProvider);
} else if (_currentIndex == 1) {
return _buildShowsTab();
} else if (_currentIndex == 3) {
return _buildWatchlistTab();
} else {
// Search tab
return const SearchScreen();
}
},
),
bottomNavigationBar: BottomNavigationBar(
backgroundColor: [Link],
selectedItemColor: [Link],
unselectedItemColor: [Link],
type: [Link],
currentIndex: _currentIndex,
onTap: (index) {
setState(() {
_currentIndex = index;
});
},
items: const [
BottomNavigationBarItem(
icon: Icon([Link]),
label: 'Movies',
),
BottomNavigationBarItem(
icon: Icon([Link]),
label: 'Shows',
),
BottomNavigationBarItem(
icon: Icon([Link]),
label: 'Search',
),
BottomNavigationBarItem(
icon: Icon([Link]),
label: 'Watchlist',
),
],
),
);
}
}
const FeaturedMovieSection({
[Link],
required [Link],
});
@override
Widget build(BuildContext context) {
String formattedDate = '';
try {
final date = [Link]([Link]);
formattedDate = [Link]().format(date);
} catch (_) {
formattedDate = 'Coming Soon';
}
return Stack(
children: [
// Background image
Container(
height: 450,
width: [Link],
decoration: BoxDecoration(
image: DecorationImage(
image: NetworkImage(
'${[Link]}${[Link]}$
{[Link]}',
),
fit: [Link],
),
),
foregroundDecoration: BoxDecoration(
gradient: LinearGradient(
begin: [Link],
end: [Link],
colors: [
[Link],
[Link](0.7),
[Link],
],
stops: const [0.5, 0.8, 1.0],
),
),
),
// Movie info
Positioned(
bottom: 20,
left: 16,
right: 16,
child: Column(
crossAxisAlignment: [Link],
children: [
Text(
[Link],
style: const TextStyle(
fontSize: 28,
fontWeight: [Link],
color: [Link],
),
),
Text(
formattedDate,
style: TextStyle(
fontSize: 16,
color: [Link](0.7),
),
),
const SizedBox(height: 16),
// Dots indicators
Row(
children: [
for (int i = 0; i < 5; i++)
Container(
width: i == 0 ? 24 : 8,
height: 8,
margin: const [Link](right: 4),
decoration: BoxDecoration(
color: i == 0
? [Link]
: [Link](0.3),
borderRadius: [Link](4),
),
),
],
),
],
),
),
],
);
}
}
class MovieSliderSection extends StatelessWidget {
final String title;
final List<Movie> movies;
final bool isLoading;
final bool isFullWidth;
const MovieSliderSection({
[Link],
required [Link],
required [Link],
[Link] = false,
[Link] = false,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: [Link],
children: [
Text(
title,
style: [Link](context).[Link],
),
const SizedBox(height: 16),
if (isLoading && [Link])
const SizedBox(
height: 200,
child: Center(
child: CircularProgressIndicator(),
),
)
else
MovieSlider(
movies: movies,
isFullWidth: isFullWidth,
),
],
);
}
}
import 'package:flutter/[Link]';
import 'package:provider/[Link]';
import '../providers/movie_provider.dart';
import '../widgets/movie_card.dart';
@override
void initState() {
[Link]();
// Clear any previous search results
[Link]((_) {
[Link]<MovieProvider>(context, listen: false).clearSearchResults();
});
}
@override
void dispose() {
_searchController.dispose();
[Link]();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: TextField(
controller: _searchController,
autofocus: true,
decoration: InputDecoration(
hintText: 'Search for movies...',
border: [Link],
suffixIcon: IconButton(
icon: const Icon([Link]),
onPressed: () {
_searchController.clear();
[Link]<MovieProvider>(context, listen: false).clearSearchResults();
},
),
),
onSubmitted: _performSearch,
),
),
body: Consumer<MovieProvider>(
builder: (context, movieProvider, child) {
final searchResults = [Link];
final isLoading = [Link];
final hasError = [Link];
if (_isSearching || isLoading) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (hasError) {
return Center(
child: Column(
mainAxisAlignment: [Link],
children: [
Text(
'Error searching movies',
style: [Link](context).[Link],
),
const SizedBox(height: 8),
Text(
[Link],
style: [Link](context).[Link],
textAlign: [Link],
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => _performSearch(_searchController.text),
child: const Text('Retry'),
),
],
),
);
}
if (_searchController.[Link]) {
return Center(
child: Column(
mainAxisAlignment: [Link],
children: [
const Icon(
[Link],
size: 80,
color: Colors.white30,
),
const SizedBox(height: 16),
Text(
'Search for movies',
style: [Link](context).[Link]?.copyWith(
color: Colors.white70,
),
),
],
),
);
}
if ([Link]) {
return Center(
child: Column(
mainAxisAlignment: [Link],
children: [
const Icon(
Icons.movie_filter,
size: 80,
color: Colors.white30,
),
const SizedBox(height: 16),
Text(
'No movies found',
style: [Link](context).[Link]?.copyWith(
color: Colors.white70,
),
),
const SizedBox(height: 8),
Text(
'Try different keywords',
style: [Link](context).[Link]?.copyWith(
color: Colors.white70,
),
),
],
),
);
}
return [Link](
padding: const [Link](16.0),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 0.7,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
),
itemCount: [Link],
itemBuilder: (context, index) {
return MovieCard(
movie: searchResults[index],
height: 250,
);
},
);
},
),
);
}
}
import 'dart:convert';
import 'package:http/[Link]' as http;
import 'package:flutter/[Link]';
import '../models/[Link]';
import '../utils/[Link]';
class ApiService {
final String apiKey = [Link];
final String accessToken = [Link];
final String baseUrl = [Link];
if ([Link] == 200) {
final decodedData = [Link]([Link]);
debugPrint('API key auth successful. Movies received.');
if ([Link]) {
debugPrint('Results empty, returning dummy data for testing');
return _getDummyMovies();
}
if ([Link]) {
debugPrint('Results empty, returning dummy data for testing');
return _getDummyMovies();
}
return dummyMovies;
}
if ([Link] == 200) {
debugPrint('Movie details received successfully with API key');
final decodedData = [Link]([Link]);
debugPrint('Videos included: ${decodedData['videos'] != null}');
if (decodedData['videos'] != null) {
final videoCount = decodedData['videos']['results']?.length ?? 0;
debugPrint('Number of videos received: $videoCount');
}
return [Link](decodedData);
} else {
debugPrint('Error in movie details with API key: ${[Link]}');
return _getDummyMovieDetail(movieId);
}
}
if ([Link] == 200) {
final decodedData = [Link]([Link]);
final List<dynamic> results = decodedData['results'] ?? [];
debugPrint('Search results count with API key: ${[Link]}');
if ([Link]) {
return _getDummySearchResults(query);
}
if ([Link]) {
return _getDummySearchResults(query);
}
return dummyResults;
}
}
import 'package:flutter/[Link]';
import 'package:flutter_dotenv/flutter_dotenv.dart';
class ApiConstants {
// API credentials
static final String apiKey = [Link]['TMDB_API_KEY'] ??
'2276c8584be8ca4ae8a299da484c645a';
static final String accessToken = [Link]['TMDB_ACCESS_TOKEN'] ??
'eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIyMjc2Yzg1ODRiZThjYTRhZThhMjk5ZGE0ODRjNjQ1YSIsInN1
YiI6IjY1NzYzMWI4YTYzZWM2MDEzM2YzNWU1YSIsIm5iZiI6MTcwMjIyMzI4OCwiZXhwIjoxNzA0O
DE1Mjg4LCJzY29wZSI6ImFwaV9yZWFkIiwianRpIjoiMGMxNGRiNWUzMzMzOTc3ZGQ1NDE5ZG
M3MTYzMWI5ZjUifQ.RWJ3DuPHG-nZGnlZ3H1lJbC-JRUjKxTXIE1f_JDOv24';
// API endpoints
static const String baseUrl = '[Link]
static const String imageBaseUrl = '[Link]
static const String originalImageSize = 'original';
static const String w500ImageSize = 'w500';
static const String w300ImageSize = 'w300';
class AppColors {
static const Color scaffoldBackground = Color(0xFF000000);
static const Color cardColor = Color(0xFF1A1E25);
static const Color primaryColor = Color(0xFF121212);
static const Color secondaryColor = Color(0xFFFF0000);
static const Color ratingColor = Color(0xFFFFCB45);
}
class MovieCategories {
static const String popular = 'Popular';
static const String topRated = 'Top Rated';
static const String upcoming = 'Upcoming';
static const String nowPlaying = 'Now Playing';
}
import 'package:flutter/[Link]';
import 'package:cached_network_image/cached_network_image.dart';
import '../models/[Link]';
import '../screens/movie_detail_screen.dart';
import '../utils/[Link]';
const MovieCard({
[Link],
required [Link],
[Link] = 200,
[Link] = false,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
[Link](
context,
MaterialPageRoute(
builder: (context) => MovieDetailScreen(
movieId: [Link],
posterPath: [Link],
title: [Link],
),
),
);
},
child: Column(
crossAxisAlignment: [Link],
children: [
// Poster image with rating
Stack(
children: [
// Movie poster
ClipRRect(
borderRadius: [Link](12),
child: [Link]
? CachedNetworkImage(
imageUrl: '${[Link]}${ApiConstants.w500ImageSize}$
{[Link]}',
fit: [Link],
height: 180,
width: 120,
placeholder: (context, url) => Container(
color: [Link].shade800,
height: 180,
width: 120,
child: const Center(
child: CircularProgressIndicator(
strokeWidth: 2,
color: [Link],
),
),
),
errorWidget: (context, url, error) => Container(
color: [Link].shade800,
height: 180,
width: 120,
child: const Column(
mainAxisAlignment: [Link],
children: [
Icon(Icons.image_not_supported, color: Colors.white70),
SizedBox(height: 8),
Text(
'No image',
style: TextStyle(color: Colors.white70, fontSize: 12),
),
],
),
),
)
: Container(
color: [Link].shade800,
height: 180,
width: 120,
child: const Column(
mainAxisAlignment: [Link],
children: [
Icon(Icons.image_not_supported, color: Colors.white70),
SizedBox(height: 8),
Text(
'No image',
style: TextStyle(color: Colors.white70, fontSize: 12),
),
],
),
),
),
// Rating badge
Positioned(
bottom: 8,
left: 8,
child: Container(
padding: const [Link](horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: [Link](0.7),
borderRadius: [Link](4),
),
child: Row(
children: [
const Icon(
[Link],
color: [Link],
size: 14,
),
const SizedBox(width: 2),
Text(
([Link] / 2).toStringAsFixed(1),
style: const TextStyle(
color: [Link],
fontWeight: [Link],
fontSize: 12,
),
),
const Text(
'/10',
style: TextStyle(
color: Colors.white70,
fontSize: 10,
),
),
],
),
),
),
],
),
// Movie title
const SizedBox(height: 6),
SizedBox(
width: 120,
child: Text(
[Link],
style: const TextStyle(
color: [Link],
fontWeight: FontWeight.w500,
fontSize: 14,
),
maxLines: 1,
overflow: [Link],
),
),
],
),
);
}
}
import 'package:flutter/[Link]';
import '../models/[Link]';
import 'movie_card.dart';
const MovieSlider({
[Link],
required [Link],
[Link] = false,
});
@override
Widget build(BuildContext context) {
if ([Link]) {
return SizedBox(
height: 200,
child: Center(
child: Text(
'No movies available',
style: [Link](context).[Link],
),
),
);
}
if (isFullWidth) {
return SizedBox(
height: 220,
child: [Link](
controller: PageController(viewportFraction: 0.9),
itemCount: [Link] > 5 ? 5 : [Link],
itemBuilder: (context, index) {
return Padding(
padding: const [Link](horizontal: 8.0),
child: MovieCard(
movie: movies[index],
height: 220,
isFullWidth: true,
),
);
},
),
);
}
return SizedBox(
height: 220,
child: [Link](
scrollDirection: [Link],
padding: const [Link](horizontal: 16.0),
itemCount: [Link],
itemBuilder: (context, index) {
return Padding(
padding: const [Link](right: 12.0),
child: MovieCard(
movie: movies[index],
height: 180,
),
);
},
),
);
}
}
import 'package:flutter/[Link]';
import 'package:provider/[Link]';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter/[Link]';
import 'providers/movie_provider.dart';
import 'screens/home_screen.dart';
import 'utils/[Link]';
runApp(const MyApp());
}
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => MovieProvider(),
child: MaterialApp(
title: 'Movie App',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: [Link](
seedColor: [Link],
brightness: [Link],
),
scaffoldBackgroundColor: [Link],
textTheme: const TextTheme(
titleLarge: TextStyle(
color: [Link],
fontWeight: [Link],
fontSize: 22,
),
titleMedium: TextStyle(
color: [Link],
fontWeight: FontWeight.w600,
fontSize: 18,
),
bodyMedium: TextStyle(
color: Colors.white70,
fontSize: 16,
),
bodySmall: TextStyle(
color: Colors.white54,
fontSize: 14,
),
),
bottomNavigationBarTheme: const BottomNavigationBarThemeData(
backgroundColor: [Link],
selectedItemColor: [Link],
unselectedItemColor: Colors.white54,
type: [Link],
showSelectedLabels: true,
showUnselectedLabels: true,
selectedLabelStyle: TextStyle(fontSize: 12),
unselectedLabelStyle: TextStyle(fontSize: 12),
),
useMaterial3: true,
),
home: const HomeScreen(),
),
);
}
}