In today’s article, I will solve some of the problems that the architecture of the previous post include:
- Solving of current problems.
- Single Instance and Scoped Instance (BLoC access)
- Navagation.
- RxDart’s Transformers.
1. Solving of current problems.
In the previous post, the first error is that I create a dispose () method in the MoviesBloc class, which is in effect close or dispose all streams to avoid memory leaks. but it’s not called anywhere, this could cause a memory leak. Another error is that I am making a network call inside the build method.
The MovieList class in the code is currently using StatelessWidget, and the build function will be called whenever it is added to the Widget tree. Because I put the function bloc.fetchAllMovies () (see code part 1 ) inside the build function, so it was called many times. Why do I say that because the MovieList class is a class inherited from StatelessWidget, but you already know a StatelessWidget class will call the build function again when that class depends on the class that contains it, if the parent class Update UI then the class MovieList will perform callback of build function. The MovieList class does not have initState and dispose functions like the StatefulWidget class. The initState method calls only once when resource is allocated and the dispose method calls disposing that allocated the resource. Now I will switch the MovieList class inheriting from StatefulWidget. In class movie_list.dart replace the following code:
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | import 'package:flutter/material.dart'; import '../models/item_model.dart'; import '../blocs/movies_bloc.dart'; class MovieList extends StatefulWidget { @override State<StatefulWidget> createState() { return MovieListState(); } } class MovieListState extends State<MovieList> { @override void initState() { super.initState(); bloc.fetchAllMovies(); } @override void dispose() { bloc.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Popular Movies'), ), body: StreamBuilder( stream: bloc.allMovies, builder: (context, AsyncSnapshot<ItemModel> snapshot) { if (snapshot.hasData) { return buildList(snapshot); } else if (snapshot.hasError) { return Text(snapshot.error.toString()); } return Center(child: CircularProgressIndicator()); }, ), ); } Widget buildList(AsyncSnapshot<ItemModel> snapshot) { return GridView.builder( itemCount: snapshot.data.results.length, gridDelegate: new SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2), itemBuilder: (BuildContext context, int index) { return GridTile( child: Image.network( 'https://image.tmdb.org/t/p/w185${snapshot.data .results[index].poster_path}', fit: BoxFit.cover, ), ); }); } } |
In the above code I call bloc.fetchAllMovies () in initState method and bloc.dispose () in dispose method of MovieListState class.
Note : Never make network calls or handle db in the build method and make sure you dispose or close the stream.
2. New feature implementation
I will perform more movile detail feature when users click on a movie, you watch the following video: https://www.youtube.com/embed/krXb9CzGRxU?feature=o
3. Plan your app flow
Before adding a screen, it is best to write in advance on paper to visualize how your flow is going. For example :
I will explain the flow of the picture above:
- Movie List Screen: will display the list of movies in grid list format.
- Movie List Bloc: this is the bridge to get data from the repository and transmit it to the Movie List Screen.
- Movie Detail Screen: This is the screen where you will see details of the movie.
- Repository: This is the center from which the data stream is controlled by the controller.
- API provider: Implement the call api.
2. Single Instance and Scoped Instance
On the Movie List Screen screen you can get bloc in 2 ways Single Instance, Scoped Instance. Single Instance can be accessed anywhere on the app while Scoped Instance has limited access, it is only accessible on the screen it is linked to.
Looking at the diagram, Bloc can only access the screen and 2 widgets below the screen. We can use InheritedWidget to keep Bloc in it. InheritedWidget will wrap Screen widget, allowing Screen and the widget under it to be accessible. Single Instance is a way to access Bloc for small apps but working with large apps using Scoped Instance is a great way.
3. Adding the detail screen
Add detail screen for the app. When the user clicks on a movie, the detail screen will display and show information about the film. Create file movie_detail.dart, add the following code:
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | import 'package:flutter/material.dart'; class MovieDetail extends StatefulWidget { final posterUrl; final description; final releaseDate; final String title; final String voteAverage; final int movieId; MovieDetail({ this.title, this.posterUrl, this.description, this.releaseDate, this.voteAverage, this.movieId, }); @override State<StatefulWidget> createState() { return MovieDetailState( title: title, posterUrl: posterUrl, description: description, releaseDate: releaseDate, voteAverage: voteAverage, movieId: movieId, ); } } class MovieDetailState extends State<MovieDetail> { final posterUrl; final description; final releaseDate; final String title; final String voteAverage; final int movieId; MovieDetailState({ this.title, this.posterUrl, this.description, this.releaseDate, this.voteAverage, this.movieId, }); @override Widget build(BuildContext context) { return Scaffold( body: SafeArea( top: false, bottom: false, child: NestedScrollView( headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { return <Widget>[ SliverAppBar( expandedHeight: 200.0, floating: false, pinned: true, elevation: 0.0, flexibleSpace: FlexibleSpaceBar( background: Image.network( "https://image.tmdb.org/t/p/w500$posterUrl", fit: BoxFit.cover, )), ), ]; }, body: Padding( padding: const EdgeInsets.all(10.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Container(margin: EdgeInsets.only(top: 5.0)), Text( title, style: TextStyle( fontSize: 25.0, fontWeight: FontWeight.bold, ), ), Container(margin: EdgeInsets.only(top: 8.0, bottom: 8.0)), Row( children: <Widget>[ Icon( Icons.favorite, color: Colors.red, ), Container( margin: EdgeInsets.only(left: 1.0, right: 1.0), ), Text( voteAverage, style: TextStyle( fontSize: 18.0, ), ), Container( margin: EdgeInsets.only(left: 10.0, right: 10.0), ), Text( releaseDate, style: TextStyle( fontSize: 18.0, ), ), ], ), Container(margin: EdgeInsets.only(top: 8.0, bottom: 8.0)), Text(description), ], ), ), ), ), ); } } |
You may have this class’s constructor need to add some parameters. I will add logic to bring the list screen to the detail screen.
1. Navigation
In Flutter if you want to switch from one screen to another you use Navigator . Implement the navigation logic inside the movie_list.dart file. When you tap a movie, the detail screen will open and the detail screen will show data transferred from the movie list screen to the detail screen.
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | import 'package:flutter/material.dart'; import '../models/item_model.dart'; import '../blocs/movies_bloc.dart'; import 'movie_detail.dart'; class MovieList extends StatefulWidget { @override State<StatefulWidget> createState() { return MovieListState(); } } class MovieListState extends State<MovieList> { @override void initState() { super.initState(); bloc.fetchAllMovies(); } @override void dispose() { bloc.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Popular Movies'), ), body: StreamBuilder( stream: bloc.allMovies, builder: (context, AsyncSnapshot<ItemModel> snapshot) { if (snapshot.hasData) { return buildList(snapshot); } else if (snapshot.hasError) { return Text(snapshot.error.toString()); } return Center(child: CircularProgressIndicator()); }, ), ); } Widget buildList(AsyncSnapshot<ItemModel> snapshot) { return GridView.builder( itemCount: snapshot.data.results.length, gridDelegate: new SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2), itemBuilder: (BuildContext context, int index) { return GridTile( child: InkResponse( enableFeedback: true, child: Image.network( 'https://image.tmdb.org/t/p/w185${snapshot.data .results[index].poster_path}', fit: BoxFit.cover, ), onTap: () => openDetailPage(snapshot.data, index), ), ); }); } openDetailPage(ItemModel data, int index) { Navigator.push( context, MaterialPageRoute(builder: (context) { return MovieDetail( title: data.results[index].title, posterUrl: data.results[index].backdrop_path, description: data.results[index].overview, releaseDate: data.results[index].release_date, voteAverage: data.results[index].vote_average.toString(), movieId: data.results[index].id, ); }), ); } } |
Now, at the movie detail screen, show the intro from the API:
1 2 | https://api.themoviedb.org/3/movie/<movie_id>/videos?api_key=your_api_key |
In the above api we replace the <movie_id> and your_api_key. The response of the api returned is:
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | { "id": 299536, "results": [ { "id": "5a200baa925141033608f5f0", "iso_639_1": "en", "iso_3166_1": "US", "key": "6ZfuNTqbHE8", "name": "Official Trailer", "site": "YouTube", "size": 1080, "type": "Trailer" }, { "id": "5a200bcc925141032408d21b", "iso_639_1": "en", "iso_3166_1": "US", "key": "sAOzrChqmd0", "name": "Action...Avengers: Infinity War", "site": "YouTube", "size": 720, "type": "Clip" }, { "id": "5a200bdd0e0a264cca08d39f", "iso_639_1": "en", "iso_3166_1": "US", "key": "3VbHg5fqBYw", "name": "Trailer Tease", "site": "YouTube", "size": 720, "type": "Teaser" }, { "id": "5a7833440e0a26597f010849", "iso_639_1": "en", "iso_3166_1": "US", "key": "pVxOVlm_lE8", "name": "Big Game Spot", "site": "YouTube", "size": 1080, "type": "Teaser" }, { "id": "5aabd7e69251413feb011276", "iso_639_1": "en", "iso_3166_1": "US", "key": "QwievZ1Tx-8", "name": "Official Trailer #2", "site": "YouTube", "size": 1080, "type": "Trailer" }, { "id": "5aea2ed2c3a3682bf7001205", "iso_639_1": "en", "iso_3166_1": "US", "key": "LXPaDL_oILs", "name": ""Legacy" TV Spot", "site": "YouTube", "size": 1080, "type": "Teaser" }, { "id": "5aea2f3e92514172a7001672", "iso_639_1": "en", "iso_3166_1": "US", "key": "PbRmbhdHDDM", "name": ""Family" Featurette", "site": "YouTube", "size": 1080, "type": "Featurette" } ] } |
For the response above we need to create POJO class. Create trailer_model .dart file in package model.
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | class TrailerModel { int _id; List<_Result> _results = []; TrailerModel.fromJson(Map<String, dynamic> parsedJson) { _id = parsedJson['id']; List<_Result> temp = []; for (int i = 0; i < parsedJson['results'].length; i++) { _Result result = _Result(parsedJson['results'][i]); temp.add(result); } _results = temp; } List<_Result> get results => _results; int get id => _id; } class _Result { String _id; String _iso_639_1; String _iso_3166_1; String _key; String _name; String _site; int _size; String _type; _Result(result) { _id = result['id']; _iso_639_1 = result['iso_639_1']; _iso_3166_1 = result['iso_3166_1']; _key = result['key']; _name = result['name']; _site = result['site']; _size = result['size']; _type = result['type']; } String get id => _id; String get iso_639_1 => _iso_639_1; String get iso_3166_1 => _iso_3166_1; String get key => _key; String get name => _name; String get site => _site; int get size => _size; String get type => _type; } |
In the movie_api_provider.dart file, please page the following code:
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 29 30 31 32 33 34 | import 'dart:async'; import 'package:http/http.dart' show Client; import 'dart:convert'; import '../models/item_model.dart'; import '../models/trailer_model.dart'; class MovieApiProvider { Client client = Client(); final _apiKey = '802b2c4b88ea1183e50e6b285a27696e'; final _baseUrl = "http://api.themoviedb.org/3/movie"; Future<ItemModel> fetchMovieList() async { final response = await client.get("$_baseUrl/popular?api_key=$_apiKey"); if (response.statusCode == 200) { // If the call to the server was successful, parse the JSON return ItemModel.fromJson(json.decode(response.body)); } else { // If that call was not successful, throw an error. throw Exception('Failed to load post'); } } Future<TrailerModel> fetchTrailer(int movieId) async { final response = await client.get("$_baseUrl/$movieId/videos?api_key=$_apiKey"); if (response.statusCode == 200) { return TrailerModel.fromJson(json.decode(response.body)); } else { throw Exception('Failed to load trailers'); } } } |
Update the following code to the file ** repository.dart **:
1 2 3 4 5 6 7 8 9 10 11 12 13 | import 'dart:async'; import 'movie_api_provider.dart'; import '../models/item_model.dart'; import '../models/trailer_model.dart'; class Repository { final moviesApiProvider = MovieApiProvider(); Future<ItemModel> fetchAllMovies() => moviesApiProvider.fetchMovieList(); Future<TrailerModel> fetchTrailers(int movieId) => moviesApiProvider.fetchTrailer(movieId); } |
Now let’s deploy Scoped Instance BLoC. Create file movie_detail_bloc.dart in blocs package, create file movie_detail_bloc_provider.dart also in blocs package. The code of movie_detail_bloc_provider.dart file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | import 'package:flutter/material.dart'; import 'movie_detail_bloc.dart'; export 'movie_detail_bloc.dart'; class MovieDetailBlocProvider extends InheritedWidget { final MovieDetailBloc bloc; MovieDetailBlocProvider({Key key, Widget child}) : bloc = MovieDetailBloc(), super(key: key, child: child); @override bool updateShouldNotify(_) { return true; } static MovieDetailBloc of(BuildContext context) { return (context.inheritFromWidgetOfExactType(MovieDetailBlocProvider) as MovieDetailBlocProvider) .bloc; } } |
The class is extensible from the InheritedWidget class and provides block access through the context. This context is wrapped in InheritedWidget. Add the code movie_detail_bloc.dart:
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 29 30 31 32 33 34 35 | import 'dart:async'; import 'package:rxdart/rxdart.dart'; import '../models/trailer_model.dart'; import '../resources/repository.dart'; class MovieDetailBloc { final _repository = Repository(); final _movieId = PublishSubject<int>(); final _trailers = BehaviorSubject<Future<TrailerModel>>(); Function(int) get fetchTrailersById => _movieId.sink.add; Observable<Future<TrailerModel>> get movieTrailers => _trailers.stream; MovieDetailBloc() { _movieId.stream.transform(_itemTransformer()).pipe(_trailers); } dispose() async { _movieId.close(); await _trailers.drain(); _trailers.close(); } _itemTransformer() { return ScanStreamTransformer( (Future<TrailerModel> trailer, int id, int index) { print(index); trailer = _repository.fetchTrailers(id); return trailer; }, ); } } |
I will explain to you a little bit: To get the data trailer from the server we will send the movile id and get the list of trailers. To implement the above idea I will use RxDart – Transformers . I will introduce this part in the lesson.
References :
https://medium.com/codechai/architect-your-flutter-project-using-bloc-pattern-part-2-d8dd1eca9ba5