├── .gitignore ├── .metadata ├── README.md ├── analysis_options.yaml ├── art └── demo.gif ├── lib ├── core │ ├── app │ │ ├── app.dart │ │ └── movie_app.dart │ ├── bloc │ │ ├── theme_bloc.dart │ │ ├── theme_event.dart │ │ └── theme_state.dart │ ├── constants │ │ └── constants.dart │ ├── core.dart │ └── extensions │ │ └── string_x.dart ├── features │ ├── counter │ │ ├── counter.dart │ │ └── presentation │ │ │ ├── bloc │ │ │ ├── counter_bloc.dart │ │ │ ├── counter_event.dart │ │ │ └── counter_state.dart │ │ │ ├── presentation.dart │ │ │ └── screen │ │ │ └── counter_screen.dart │ ├── features.dart │ └── home │ │ ├── data │ │ ├── data.dart │ │ └── repositories │ │ │ ├── movies_repository.dart │ │ │ └── repositories.dart │ │ ├── domain │ │ ├── domain.dart │ │ ├── entities │ │ │ ├── entities.dart │ │ │ ├── movie.dart │ │ │ └── movies_result.dart │ │ └── repositories │ │ │ ├── i_movies_repository.dart │ │ │ └── repositories.dart │ │ ├── home.dart │ │ └── presentation │ │ ├── bloc │ │ ├── home_bloc.dart │ │ ├── home_event.dart │ │ └── home_state.dart │ │ ├── presentation.dart │ │ └── screen │ │ └── home_screen.dart └── main.dart └── pubspec.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | 35 | # Symbolication related 36 | app.*.symbols 37 | 38 | # Obfuscation related 39 | app.*.map.json 40 | 41 | /android/ 42 | /ios/ 43 | pubspec.lock 44 | .vscode 45 | 46 | *.freezed.dart 47 | *g.dart -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled. 5 | 6 | version: 7 | revision: 135454af32477f815a7525073027a3ff9eff1bfd 8 | channel: stable 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 135454af32477f815a7525073027a3ff9eff1bfd 17 | base_revision: 135454af32477f815a7525073027a3ff9eff1bfd 18 | - platform: android 19 | create_revision: 135454af32477f815a7525073027a3ff9eff1bfd 20 | base_revision: 135454af32477f815a7525073027a3ff9eff1bfd 21 | - platform: ios 22 | create_revision: 135454af32477f815a7525073027a3ff9eff1bfd 23 | base_revision: 135454af32477f815a7525073027a3ff9eff1bfd 24 | - platform: linux 25 | create_revision: 135454af32477f815a7525073027a3ff9eff1bfd 26 | base_revision: 135454af32477f815a7525073027a3ff9eff1bfd 27 | - platform: macos 28 | create_revision: 135454af32477f815a7525073027a3ff9eff1bfd 29 | base_revision: 135454af32477f815a7525073027a3ff9eff1bfd 30 | - platform: web 31 | create_revision: 135454af32477f815a7525073027a3ff9eff1bfd 32 | base_revision: 135454af32477f815a7525073027a3ff9eff1bfd 33 | - platform: windows 34 | create_revision: 135454af32477f815a7525073027a3ff9eff1bfd 35 | base_revision: 135454af32477f815a7525073027a3ff9eff1bfd 36 | 37 | # User provided section 38 | 39 | # List of Local paths (relative to this file) that should be 40 | # ignored by the migrate tool. 41 | # 42 | # Files that are not part of the templates will be ignored by default. 43 | unmanaged_files: 44 | - 'lib/main.dart' 45 | - 'ios/Runner.xcodeproj/project.pbxproj' 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flutter_Bloc desde cero 2 | 3 | Ejemplo usando flutter_bloc con los siguientes features: 4 | 5 | - Listado de películas usando API. 6 | - Pull refresh para refrescar el listado. 7 | - Paginado de peliculas. 8 | - Cambio de Theme mediante un botón. 9 | - Usando flutter_bloc, inyección de dependencias y clean architecture. 10 | 11 | # Demo 12 | 13 | 14 | [![](art/demo.gif)](https://www.youtube.com/watch?v=kv9dK-xQPYw ) 15 | 16 | 17 | ## Instalación 18 | 19 | - clone repository 20 | - flutter pub get 21 | - flutter pub run build_runner build --delete-conflicting-outputs 22 | - add your api key in lib/core/constants/constants.dart 23 | - run proyect 24 | 25 | 26 | # Video Tutorial completo en español 27 | 28 | [![](http://img.youtube.com/vi/kv9dK-xQPYw/0.jpg)](https://www.youtube.com/watch?v=kv9dK-xQPYw ) 29 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:very_good_analysis/analysis_options.yaml 2 | 3 | linter: 4 | rules: 5 | # To change 6 | file_names: false 7 | avoid_positional_boolean_parameters: false 8 | # To review 9 | avoid_dynamic_calls: false 10 | overridden_fields: false 11 | unawaited_futures: false 12 | # Permanent rules 13 | lines_longer_than_80_chars: false 14 | use_build_context_synchronously: false 15 | annotate_overrides: false 16 | prefer_if_elements_to_conditional_expressions: false 17 | always_use_package_imports: false 18 | public_member_api_docs: false 19 | unnecessary_lambdas: false 20 | 21 | analyzer: 22 | strong-mode: 23 | implicit-casts: true 24 | implicit-dynamic: true 25 | exclude: 26 | - "**/*.g.dart" 27 | - "**/*.gr.dart" 28 | - "**/*.config.dart" 29 | - "**/*.freezed.dart" 30 | errors: 31 | unused_import: error 32 | invalid_annotation_target: ignore 33 | sort_pub_dependencies: ignore 34 | avoid_renaming_method_parameters: ignore 35 | type_annotate_public_apis: ignore 36 | one_member_abstracts: ignore 37 | -------------------------------------------------------------------------------- /art/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/br-programmer/flutter_bloc_example/637a018431cdc5b8bd0f9be82dded648d86dbae7/art/demo.gif -------------------------------------------------------------------------------- /lib/core/app/app.dart: -------------------------------------------------------------------------------- 1 | export 'movie_app.dart'; 2 | -------------------------------------------------------------------------------- /lib/core/app/movie_app.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_bloc_example/core/bloc/theme_bloc.dart'; 4 | import 'package:flutter_bloc_example/features/features.dart'; 5 | 6 | class MovieApp extends StatelessWidget { 7 | const MovieApp({super.key}); 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | return MultiRepositoryProvider( 12 | providers: [ 13 | RepositoryProvider( 14 | create: (_) => MoviesRepository(), 15 | ), 16 | ], 17 | child: BlocProvider( 18 | create: (_) => ThemeBloc(), 19 | child: Builder( 20 | builder: (context) => BlocBuilder( 21 | builder: (_, state) => MaterialApp( 22 | title: 'Super Cines', 23 | theme: state.dark ? ThemeData.dark() : ThemeData.light(), 24 | home: const HomeScreen(), 25 | ), 26 | ), 27 | ), 28 | ), 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/core/bloc/theme_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | 4 | part 'theme_event.dart'; 5 | part 'theme_state.dart'; 6 | 7 | class ThemeBloc extends Bloc { 8 | ThemeBloc() : super(ThemeState.initialState()) { 9 | on<_ChangeTheme>(_changeThemeToState); 10 | } 11 | 12 | void _changeThemeToState(_, Emitter emit) { 13 | emit(state.copyWith(dark: !state.dark)); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/core/bloc/theme_event.dart: -------------------------------------------------------------------------------- 1 | part of 'theme_bloc.dart'; 2 | 3 | abstract class ThemeEvent { 4 | factory ThemeEvent.onChangeTheme() => const _ChangeTheme(); 5 | } 6 | 7 | class _ChangeTheme implements ThemeEvent { 8 | const _ChangeTheme(); 9 | } 10 | -------------------------------------------------------------------------------- /lib/core/bloc/theme_state.dart: -------------------------------------------------------------------------------- 1 | part of 'theme_bloc.dart'; 2 | 3 | class ThemeState extends Equatable { 4 | const ThemeState({this.dark = false}); 5 | 6 | factory ThemeState.initialState() => const ThemeState(); 7 | 8 | final bool dark; 9 | 10 | @override 11 | List get props => [dark]; 12 | 13 | ThemeState copyWith({bool? dark}) => ThemeState( 14 | dark: dark ?? this.dark, 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /lib/core/constants/constants.dart: -------------------------------------------------------------------------------- 1 | class Constants { 2 | const Constants._(); 3 | 4 | // TODO(all): your api key here 5 | static String get apiKey => ''; 6 | } 7 | -------------------------------------------------------------------------------- /lib/core/core.dart: -------------------------------------------------------------------------------- 1 | export 'app/app.dart'; 2 | export 'constants/constants.dart'; 3 | export 'extensions/string_x.dart'; 4 | -------------------------------------------------------------------------------- /lib/core/extensions/string_x.dart: -------------------------------------------------------------------------------- 1 | extension StringX on String { 2 | String get image => 'https://image.tmdb.org/t/p/w500$this'; 3 | } 4 | -------------------------------------------------------------------------------- /lib/features/counter/counter.dart: -------------------------------------------------------------------------------- 1 | export 'presentation/presentation.dart'; 2 | -------------------------------------------------------------------------------- /lib/features/counter/presentation/bloc/counter_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | 4 | part 'counter_event.dart'; 5 | part 'counter_state.dart'; 6 | 7 | class CounterBloc extends Bloc { 8 | CounterBloc() : super(CounterState.initialState()) { 9 | on(_incrementToState); 10 | on(_decrementToState); 11 | } 12 | 13 | void _incrementToState(_, Emitter emit) { 14 | emit(state.copyWith(count: state.count + 1)); 15 | } 16 | 17 | void _decrementToState(_, Emitter emit) { 18 | emit(state.copyWith(count: state.count - 1)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/features/counter/presentation/bloc/counter_event.dart: -------------------------------------------------------------------------------- 1 | part of 'counter_bloc.dart'; 2 | 3 | abstract class CounterEvent {} 4 | 5 | class Increment extends CounterEvent {} 6 | 7 | class Decrement extends CounterEvent {} 8 | -------------------------------------------------------------------------------- /lib/features/counter/presentation/bloc/counter_state.dart: -------------------------------------------------------------------------------- 1 | part of 'counter_bloc.dart'; 2 | 3 | class CounterState extends Equatable { 4 | const CounterState({this.count = 0}); 5 | 6 | factory CounterState.initialState() => const CounterState(); 7 | 8 | final int count; 9 | 10 | @override 11 | List get props => [count]; 12 | 13 | CounterState copyWith({int? count}) { 14 | return CounterState(count: count ?? this.count); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/features/counter/presentation/presentation.dart: -------------------------------------------------------------------------------- 1 | export 'bloc/counter_bloc.dart'; 2 | export 'screen/counter_screen.dart'; 3 | -------------------------------------------------------------------------------- /lib/features/counter/presentation/screen/counter_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_bloc_example/features/counter/counter.dart'; 4 | 5 | class CounterScreen extends StatelessWidget { 6 | const CounterScreen({super.key}); 7 | 8 | @override 9 | Widget build(BuildContext context) { 10 | return BlocProvider( 11 | create: (_) => CounterBloc(), 12 | child: Scaffold( 13 | appBar: AppBar( 14 | title: const Text('Counter Example'), 15 | elevation: 0, 16 | ), 17 | body: const _Body(), 18 | ), 19 | ); 20 | } 21 | } 22 | 23 | class _Body extends StatelessWidget { 24 | const _Body(); 25 | 26 | @override 27 | Widget build(BuildContext context) { 28 | final countBloc = context.read(); 29 | return Column( 30 | mainAxisAlignment: MainAxisAlignment.center, 31 | children: [ 32 | BlocSelector( 33 | selector: (state) => state.count, 34 | builder: (context, count) => Text('$count'), 35 | ), 36 | const SizedBox(height: 10), 37 | Row( 38 | mainAxisAlignment: MainAxisAlignment.center, 39 | children: [ 40 | IconButton( 41 | onPressed: () => countBloc.add(Decrement()), 42 | color: Colors.red, 43 | icon: const Icon(Icons.remove), 44 | ), 45 | const SizedBox(width: 20), 46 | IconButton( 47 | onPressed: () => countBloc.add(Increment()), 48 | color: Colors.green, 49 | icon: const Icon(Icons.add), 50 | ), 51 | ], 52 | ), 53 | ], 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/features/features.dart: -------------------------------------------------------------------------------- 1 | export 'counter/counter.dart'; 2 | export 'home/home.dart'; 3 | -------------------------------------------------------------------------------- /lib/features/home/data/data.dart: -------------------------------------------------------------------------------- 1 | export 'repositories/repositories.dart'; 2 | -------------------------------------------------------------------------------- /lib/features/home/data/repositories/movies_repository.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:dio/dio.dart'; 4 | import 'package:flutter_bloc_example/core/core.dart'; 5 | import 'package:flutter_bloc_example/features/features.dart'; 6 | 7 | class MoviesRepository implements IMoviesRepository { 8 | MoviesRepository() 9 | : _dio = Dio( 10 | BaseOptions( 11 | baseUrl: 'https://api.themoviedb.org/3/', 12 | queryParameters: {'api_key': Constants.apiKey}, 13 | ), 14 | ); 15 | 16 | final Dio _dio; 17 | 18 | StreamController? _streamController; 19 | 20 | @override 21 | Future fetchMovies({ 22 | required int page, 23 | String? language, 24 | }) async { 25 | try { 26 | final response = await _dio.get>( 27 | 'movie/now_playing', 28 | queryParameters: { 29 | 'page': page, 30 | if (language != null) 'language': language, 31 | }, 32 | ); 33 | final data = response.data; 34 | if (data != null) { 35 | if (_streamController != null) { 36 | _streamController!.add('New Page Load'); 37 | } 38 | return MoviesResult.fromJson(data); 39 | } 40 | throw Exception('data is null'); 41 | } catch (_) { 42 | rethrow; 43 | } 44 | } 45 | 46 | @override 47 | Stream get onNewMovie { 48 | _streamController ??= StreamController.broadcast(); 49 | return _streamController!.stream; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/features/home/data/repositories/repositories.dart: -------------------------------------------------------------------------------- 1 | export 'movies_repository.dart'; 2 | -------------------------------------------------------------------------------- /lib/features/home/domain/domain.dart: -------------------------------------------------------------------------------- 1 | export 'entities/entities.dart'; 2 | export 'repositories/repositories.dart'; 3 | -------------------------------------------------------------------------------- /lib/features/home/domain/entities/entities.dart: -------------------------------------------------------------------------------- 1 | export 'movie.dart'; 2 | export 'movies_result.dart'; 3 | -------------------------------------------------------------------------------- /lib/features/home/domain/entities/movie.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'movie.freezed.dart'; 4 | part 'movie.g.dart'; 5 | 6 | @freezed 7 | class Movie with _$Movie { 8 | @JsonSerializable(fieldRename: FieldRename.snake) 9 | const factory Movie({ 10 | String? posterPath, 11 | required bool adult, 12 | required String overview, 13 | required DateTime releaseDate, 14 | required List genreIds, 15 | required int id, 16 | required String originalTitle, 17 | required String originalLanguage, 18 | required String title, 19 | String? backdropPath, 20 | required double popularity, 21 | required int voteCount, 22 | required bool video, 23 | required double voteAverage, 24 | }) = _Movie; 25 | 26 | factory Movie.fromJson(Map json) => _$MovieFromJson(json); 27 | } 28 | -------------------------------------------------------------------------------- /lib/features/home/domain/entities/movies_result.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_bloc_example/features/features.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | 4 | part 'movies_result.freezed.dart'; 5 | part 'movies_result.g.dart'; 6 | 7 | @freezed 8 | class MoviesResult with _$MoviesResult { 9 | @JsonSerializable(fieldRename: FieldRename.snake) 10 | factory MoviesResult({ 11 | @JsonKey(name: 'page') required int currentPage, 12 | @JsonKey(name: 'results') @Default([]) List movies, 13 | required int totalPages, 14 | }) = _MoviesResult; 15 | 16 | factory MoviesResult.fromJson(Map json) => 17 | _$MoviesResultFromJson(json); 18 | } 19 | -------------------------------------------------------------------------------- /lib/features/home/domain/repositories/i_movies_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_bloc_example/features/features.dart'; 2 | 3 | abstract class IMoviesRepository { 4 | Future fetchMovies({required int page, String? language}); 5 | Stream get onNewMovie; 6 | } 7 | -------------------------------------------------------------------------------- /lib/features/home/domain/repositories/repositories.dart: -------------------------------------------------------------------------------- 1 | export 'i_movies_repository.dart'; 2 | -------------------------------------------------------------------------------- /lib/features/home/home.dart: -------------------------------------------------------------------------------- 1 | export 'data/data.dart'; 2 | export 'domain/domain.dart'; 3 | export 'presentation/presentation.dart'; 4 | -------------------------------------------------------------------------------- /lib/features/home/presentation/bloc/home_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:equatable/equatable.dart'; 4 | import 'package:flutter/foundation.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter_bloc/flutter_bloc.dart'; 7 | import 'package:flutter_bloc_example/features/features.dart'; 8 | 9 | part 'home_event.dart'; 10 | part 'home_state.dart'; 11 | 12 | class HomeBloc extends Bloc { 13 | HomeBloc(this._moviesRepository) : super(HomeState.initialState()) { 14 | on<_FetchMovies>(_fetchMoviesToState); 15 | on<_ChangeNameMovie>(_changeNameMovieToState); 16 | } 17 | 18 | final IMoviesRepository _moviesRepository; 19 | 20 | late final ScrollController _scrollController; 21 | ScrollController get scrollController => _scrollController; 22 | late StreamSubscription _subscription; 23 | 24 | void init() { 25 | _scrollController = ScrollController()..addListener(_listener); 26 | _subscription = _moviesRepository.onNewMovie.listen(_newMovieListener); 27 | } 28 | 29 | var _page = 1; 30 | var _hasNextPage = true; 31 | 32 | void _listener() { 33 | final position = _scrollController.position; 34 | final maxScrollExtent = position.maxScrollExtent; 35 | final pixel = position.pixels + 40; 36 | final hasPaginate = pixel >= maxScrollExtent && _hasNextPage; 37 | if (hasPaginate && !state.moviesLoading) { 38 | add(HomeEvent.onFecthMovies()); 39 | } 40 | } 41 | 42 | Future _fetchMoviesToState( 43 | _FetchMovies event, 44 | Emitter emit, 45 | ) async { 46 | final refresh = event.refresh; 47 | emit(state.copyWith(status: HomeStatus.loading)); 48 | final movies = [...state.movies]; 49 | if (refresh) { 50 | movies.clear(); 51 | _page = 1; 52 | } 53 | try { 54 | emit(state.copyWith(movies: movies)); 55 | final moviesResult = await _moviesRepository.fetchMovies(page: _page); 56 | movies.addAll(moviesResult.movies); 57 | _page = moviesResult.currentPage + 1; 58 | _hasNextPage = moviesResult.currentPage < moviesResult.totalPages; 59 | emit(state.copyWith(movies: movies, status: HomeStatus.success)); 60 | } catch (_) { 61 | emit(state.copyWith(status: HomeStatus.error)); 62 | } 63 | } 64 | 65 | @override 66 | Future close() { 67 | _scrollController 68 | ..removeListener(_listener) 69 | ..dispose(); 70 | _subscription.cancel(); 71 | return super.close(); 72 | } 73 | 74 | void _newMovieListener(String event) { 75 | if (kDebugMode) { 76 | print(event); 77 | } 78 | } 79 | 80 | void _changeNameMovieToState( 81 | _ChangeNameMovie event, 82 | Emitter emit, 83 | ) { 84 | final movies = [...state.movies]; 85 | movies[event.index] = movies[event.index].copyWith( 86 | title: event.newTitle, 87 | ); 88 | emit(state.copyWith(movies: movies)); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /lib/features/home/presentation/bloc/home_event.dart: -------------------------------------------------------------------------------- 1 | part of 'home_bloc.dart'; 2 | 3 | abstract class HomeEvent { 4 | factory HomeEvent.onFecthMovies({bool refresh = false}) => 5 | _FetchMovies(refresh); 6 | 7 | factory HomeEvent.onChangeNameMovie({ 8 | required String newTitle, 9 | required int index, 10 | }) => 11 | _ChangeNameMovie(newTitle, index); 12 | } 13 | 14 | class _FetchMovies implements HomeEvent { 15 | const _FetchMovies(this.refresh); 16 | final bool refresh; 17 | } 18 | 19 | class _ChangeNameMovie implements HomeEvent { 20 | const _ChangeNameMovie(this.newTitle, this.index); 21 | 22 | final String newTitle; 23 | final int index; 24 | } 25 | -------------------------------------------------------------------------------- /lib/features/home/presentation/bloc/home_state.dart: -------------------------------------------------------------------------------- 1 | part of 'home_bloc.dart'; 2 | 3 | enum HomeStatus { initial, loading, success, error } 4 | 5 | class HomeState extends Equatable { 6 | const HomeState({ 7 | this.movies = const [], 8 | this.status = HomeStatus.initial, 9 | }); 10 | 11 | factory HomeState.initialState() => const HomeState(); 12 | 13 | HomeState copyWith({ 14 | List? movies, 15 | HomeStatus? status, 16 | }) => 17 | HomeState( 18 | movies: movies ?? this.movies, 19 | status: status ?? this.status, 20 | ); 21 | 22 | final List movies; 23 | final HomeStatus status; 24 | 25 | @override 26 | List get props => [movies, status]; 27 | } 28 | 29 | extension HomeStateX on HomeState { 30 | bool get _moviesEmpty => movies.isEmpty; 31 | bool get _error => status == HomeStatus.error; 32 | bool get moviesLoading => status == HomeStatus.loading; 33 | bool get loading => moviesLoading && _moviesEmpty; 34 | bool get error => _error && _moviesEmpty; 35 | bool get errorLoadMoreMovie => _error && !_moviesEmpty; 36 | } 37 | -------------------------------------------------------------------------------- /lib/features/home/presentation/presentation.dart: -------------------------------------------------------------------------------- 1 | export 'screen/home_screen.dart'; 2 | -------------------------------------------------------------------------------- /lib/features/home/presentation/screen/home_screen.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_bloc_example/core/bloc/theme_bloc.dart'; 4 | import 'package:flutter_bloc_example/core/extensions/string_x.dart'; 5 | import 'package:flutter_bloc_example/features/home/home.dart'; 6 | import 'package:flutter_bloc_example/features/home/presentation/bloc/home_bloc.dart'; 7 | 8 | class HomeScreen extends StatelessWidget { 9 | const HomeScreen({super.key}); 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | final themeBloc = context.read(); 14 | return BlocListener( 15 | listener: (context, state) { 16 | late String text; 17 | if (state.dark) { 18 | text = 'Dark Theme'; 19 | } else { 20 | text = 'Light Theme'; 21 | } 22 | ScaffoldMessenger.of(context) 23 | ..hideCurrentSnackBar() 24 | ..showSnackBar(SnackBar(content: Text(text))); 25 | }, 26 | child: BlocProvider( 27 | create: (_) => HomeBloc(context.read()) 28 | ..init() 29 | ..add(HomeEvent.onFecthMovies()), 30 | child: Scaffold( 31 | appBar: AppBar( 32 | title: const Text('Movies'), 33 | elevation: 0, 34 | actions: [ 35 | IconButton( 36 | onPressed: () => themeBloc.add(ThemeEvent.onChangeTheme()), 37 | icon: const Icon(Icons.offline_bolt), 38 | ), 39 | ], 40 | ), 41 | body: const _Body(), 42 | ), 43 | ), 44 | ); 45 | } 46 | } 47 | 48 | class _Body extends StatelessWidget { 49 | const _Body(); 50 | 51 | @override 52 | Widget build(BuildContext context) { 53 | final homeBloc = context.read(); 54 | return BlocBuilder( 55 | builder: (context, state) { 56 | if (state.loading) { 57 | return const Center(child: CircularProgressIndicator()); 58 | } 59 | if (state.error) { 60 | return const Center(child: Text('Error load movies')); 61 | } 62 | final movies = state.movies; 63 | return RefreshIndicator( 64 | onRefresh: () { 65 | homeBloc.add(HomeEvent.onFecthMovies(refresh: true)); 66 | return Future.value(); 67 | }, 68 | child: ListView.builder( 69 | itemCount: movies.length, 70 | controller: homeBloc.scrollController, 71 | itemBuilder: (_, index) => GestureDetector( 72 | onTap: () { 73 | homeBloc.add( 74 | HomeEvent.onChangeNameMovie( 75 | newTitle: 'Change Title', 76 | index: index, 77 | ), 78 | ); 79 | }, 80 | child: _MovieItem(movie: movies[index]), 81 | ), 82 | ), 83 | ); 84 | }, 85 | ); 86 | } 87 | } 88 | 89 | class _MovieItem extends StatelessWidget { 90 | const _MovieItem({required this.movie}); 91 | final Movie movie; 92 | 93 | @override 94 | Widget build(BuildContext context) { 95 | return Padding( 96 | padding: const EdgeInsets.symmetric(horizontal: 8).copyWith(top: 8), 97 | child: AspectRatio( 98 | aspectRatio: 16 / 9, 99 | child: Stack( 100 | children: [ 101 | Image.network(movie.backdropPath?.image ?? ''), 102 | Center( 103 | child: ColoredBox( 104 | color: Theme.of(context).scaffoldBackgroundColor, 105 | child: Text( 106 | movie.title, 107 | style: const TextStyle(fontSize: 20), 108 | ), 109 | ), 110 | ), 111 | ], 112 | ), 113 | ), 114 | ); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc_example/core/core.dart'; 3 | 4 | void main() { 5 | runApp(const MovieApp()); 6 | } 7 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_bloc_example 2 | description: A new Flutter project. 3 | 4 | publish_to: 'none' 5 | version: 1.0.0+1 6 | 7 | environment: 8 | sdk: '>=2.17.0 <3.0.0' 9 | 10 | dependencies: 11 | cached_network_image: ^3.2.1 12 | dio: ^4.0.6 13 | equatable: ^2.0.5 14 | flutter: 15 | sdk: flutter 16 | flutter_bloc: ^8.1.1 17 | cupertino_icons: ^1.0.5 18 | freezed_annotation: ^2.2.0 19 | json_annotation: ^4.8.0 20 | 21 | dev_dependencies: 22 | build_runner: ^2.2.0 23 | flutter_test: 24 | sdk: flutter 25 | freezed: ^2.3.2 26 | json_serializable: ^6.6.0 27 | very_good_analysis: ^3.1.0 28 | 29 | flutter: 30 | 31 | uses-material-design: true 32 | --------------------------------------------------------------------------------