Codementor Events

Architect your Flutter project using BLOC pattern (Part 2)

Published Jan 21, 2019

Hi Folks! This article is a continuation of my previous article “Architect your Flutter project”  . As promised in my previous article I will be addressing some of the flaws in the current architecture design and add some new features to the app we were building. Before starting the journey let me show you the 4 places where we will be visiting and learn about them. Below are the topics we will cover in this article.

Topics we will cover:

  1. Solving the flaws in current architecture design
  2. Single Instance vs Scoped Instance (BLoC access)
  3. Navigation
  4. RxDart’s Transformers

Note : Before reading any further. I highly recommend going through my previous article to have a better understanding of the app we are building and the architecture design(BLoC pattern) we are following.

Flaws in the current architecture design

Before solving the problem or discussing about the changes. I guess you all must be having a question in your mind(if you don’t have still read it). Let me answer it.

Why you didn’t update the previous article with the changes instead of writing a new one?

Yes! of course I would have done that. I can directly present you a working and bug free code which would have made you happy. But here I want to teach you what I have gone through while building this app. The problems or failures I have encountered. This way you can architect any Flutter apps and debug or solve any problems you encounter while going through the process. So with no further delay. Let’s get back to our objective.

If you have gone through my previous article and code, the first flaw is I created a method called dispose() inside the MoviesBloc class. This method is responsible to close or dispose all the streams which are open to avoid memory leaks. I created the method but never called it anywhere in my movie_list.dart file. This would lead to a memory leak. Another major flaw is I am making a network call inside the build method which is quite risky. Let’s try to solve these 2 major flaws.

Currently MovieList class is a StatelessWidget and the way a StatelessWidget work is, the build will be called whenever it is added to the Widget tree and all its properties are immutable. The build method is the entry point and can be called multiple times due to configuration changes. So its not a good place to make any network call(which I did in my previous article). We even don’t have a method inside the StatelessWidget where we can call the dispose method of the bloc. We have to find a place where we can make a network call and at the end call the dispose method.

The point here is I don’t have initState and dispose method in a StatelessWidget as provided in StatefulWidget . initState method in a StatefulWidget is called first to allocate resources and dispose method is called while disposing those allocated resources(read about them in detail here). So let’s convert MovieList class from a StatelessWidget to a StatefulWidget and make the network call inside initState() and MovieBloc’s dispose() inside the StatefulWidget’s dispose().

Just replace movie_list.dart code with the below implementation.

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 have called bloc.fetchAllMovies() inside the initState() and bloc.dispose() inside the dispose() of the MovieListState class. Run the app and you can see the app loading the list of movies as usual. You won’t see any visual change but internally you have made sure that, there won’t any multiple network calls and there won’t be any memory leaks. Wow! this looks neat. 😍

Keynote : never make any network or db call inside the build method and always make sure you dispose or close the open streams.

New feature implementation 😃

It’s high time that we add a new feature to our existing app. Before we start discussing or implementing the new feature. Let me show you the find product. Here is a small video of it.

As you can see we have added a new screen where you can see the detail of a particular movie you select from the list.

Let’s Plan the app flow

It is always a best practice that you do some pen paper work(deciding the flow) before adding any new feature to the app. So here I share the app flow I came up with after analysing all the features of the app.

I guess most of you will easily understand the flow after looking at the diagram. But still let me explain you the above diagram as there are few new terminologies that I have used.

  1. Movie List Screen: This is the screen where you see the grid list of all the movies.
  2. Movie List Bloc: This is the bridge which will get data from the repository on demand and pass it to the Movie List Screen(I will explain about Single Instance in a while).
  3. Movie Detail Screen: This is the screen where you will see the detail of the movie you selected from the list screen. Here you can see the name of the movie, rating, release date, description and trailers(I will explain about Scoped Instance in a while).
  4. Repository: This is the central point from where the data flow is controlled.
  5. API provider: This hold the network call implementation.

Now you must be wondering. What is “ Single Instance and Scoped Instance ” in the diagram. Let’s understand them in detail.

Single Instance vs Scoped Instance

As you can see in the diagram both the screens have access to their respective BLoC class.You can expose these BLoC classes to their respective screens in two ways i.e Single Instance or Scoped Instance. When I say Single Instance, I mean a single reference(Singleton) of the BLoC class will be exposed to the screen. This type of BLoC class can be accessible from any part of the app. Any screen can use a Single Instance BLoC class.

But Scoped Instance BLoC class has limited access. I mean its only accessible to the screen its associated with or exposed to. Here is a small diagram to explain it.


Scoped Instance

As you can see in the above diagram the bloc is only accessible to the screen widget and 2 other custom widgets below the Screen. We are using the InheritedWidget which will hold the BLoC inside it. InheritedWidget will wrap the Screen widget and let the Screen widget along with widgets below it have access to the BLoC. No parent widgets of the Screen Widget will have access to the BLoC.

Hope you understand the difference between a Single Instance and Scoped Instance. Single Instance way of accessing BLoC is useful when you are working on a small app. But if you are working on a big project then Scoped Instance is the preferred way.

Adding the detail screen

Its time we add the detail screen to our app. The logic behind the Detail Screen is, user will click on a movie item from the list of movies. User will be taken to a detail screen where the user can the details of the movie. Some of the details(movie name, rating, release date, description, poster) will be passed from the list screen to the detail screen. Trailer will be loaded from server. Let’s keep the trailer part away and focus on showing the data which is passed from the list screen.

Before creating the file I hope you are following the same project structure which I mentioned in my previous article. If yes then create a file named movie_detail.dart inside the ui package.Copy paste the below code inside the file.

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),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

As you can see the constructor of this class is expecting few parameters. These data will be provided to this class from the list screen. Next step is to implement navigation logic that will take us from list screen to detail screen.

In Flutter if you want to move from one screen to another we use Navigator class. Let’s implement the navigation logic inside movie_list.dart file.

So idea is, on tapping of each grid item we will open the detail screen and show the content we passed from the list screen to the detail screen. Here is the code for the movie_list.dart file.

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,
        );
      }),
    );
  }
}

In the above code as you can see the method openDetailPage() has the navigation logic. We are passing the data that we will show in the detail screen. Run the app and you can navigate to the new screen.

Wow! I can navigate 😍

Now its time to show the trailers in the detail screen. Let’s understand the API we will be consuming to get the trailers from server. Below is the link we will be hitting to get the JSON response.

https://api.themoviedb.org/3/movie/<movie_id>/videos?api_key=your_api_key

In the above API we have to input two things. First the movie_id and second is the api key. This is how the response looks after you hit the API.

{
  "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 above response we need to have a POJO class. Let’s build that first. Create a file named trailer_model.dart inside the models package. Copy paste the below code inside it.

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;
}

Now let’s implement the network call inside the movie_api_provider.dart file. Copy and paste the below inside the file.

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');
    }
  }
}

fetchTrailer(movie_id) is the method which we make the hit the API and convert the JSON response to a TrailerModel object and return a Future<TrailerModel>.

Now let’s update the repository.dart file by adding this new network call implementation. Copy and paste below code inside the repository.dart file.

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 its time to implement the Scoped Instance BLoC approach. Create a new file movie_detail_bloc.dart inside the blocs package. Create one more file movie_detail_bloc_provider.dart inside the same blocs package.

Here is the code for movie_detail_bloc_provider.dart file.

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;
  }
}

This class extends the InheritedWidget and provide access to the bloc through the of(context) method. As you can see the of(context) is expecting a context as parameter. This context belongs to the screen which InheritedWidget has wrapped. In our case it is the movie detail screen.

Let’s write the code for movie_detail_bloc.dart. Copy paste the below code inside the bloc file.

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;
      },
    );
  }
}

Let me explain you the above code a bit. The idea behind getting the trailer list from server is, we have to pass a movieId to the trailer API and in return it will send us the list of trailer. To implement this idea we will be using one important feature of RxDart i.e Transformers .

Transformers

Transformers mostly helps in chaining two or more Subjects and get the final result. Idea is, if you want to pass data from one Subject to another after performing some operations over the data. We will be using transformers to perform operation on the input data from the first Subject and will pipe it to the next Subject.

In our app we will be adding the movieId to the _movieId which is a PublishSubject. We will pass the movieId to the ScanStreamTransformer which in turn will make a network call the trailer API and get the results and pipe it to the _trailers which is a BehaviorSubject. Here is a small diagram to illustrate my explanation.


Role of transformer

The final step that we are left with is, making the movieDetailBloc accessible to the MovieDetail screen. For this we need to update the openDetailPage() method. Here is the updated code for movie_list.dart file.

import 'package:flutter/material.dart';
import '../models/item_model.dart';
import '../blocs/movies_bloc.dart';
import 'movie_detail.dart';
import '../blocs/movie_detail_bloc_provider.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 MovieDetailBlocProvider(
          child: 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,
          ),
        );
      }),
    );
  }
}

As you can see inside the MaterialPageRoute we are returning the MovieDetailBlocProvider (InheritedWidget) and wrapping the MovieDetail screen into it. So that the MovieDetailBloc class will be accessible inside the detail screen and to all the widgets below it.

Finally, here is the code for the movie_detail.dart file.

import 'dart:async';

import 'package:flutter/material.dart';
import '../blocs/movie_detail_bloc_provider.dart';
import '../models/trailer_model.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;

  MovieDetailBloc bloc;

  MovieDetailState({
    this.title,
    this.posterUrl,
    this.description,
    this.releaseDate,
    this.voteAverage,
    this.movieId,
  });

  @override
  void didChangeDependencies() {
    bloc = MovieDetailBlocProvider.of(context);
    bloc.fetchTrailersById(movieId);
    super.didChangeDependencies();
  }

  @override
  void dispose() {
    bloc.dispose();
    super.dispose();
  }

  @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),
                Container(margin: EdgeInsets.only(top: 8.0,
                    bottom: 8.0)),
                Text(
                  "Trailer",
                  style: TextStyle(
                    fontSize: 25.0,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                Container(margin: EdgeInsets.only(top: 8.0,
                    bottom: 8.0)),
                StreamBuilder(
                  stream: bloc.movieTrailers,
                  builder:
                      (context, AsyncSnapshot<Future<TrailerModel>> snapshot) {
                    if (snapshot.hasData) {
                      return FutureBuilder(
                        future: snapshot.data,
                        builder: (context,
                            AsyncSnapshot<TrailerModel> itemSnapShot) {
                          if (itemSnapShot.hasData) {
                            if (itemSnapShot.data.results.length > 0)
                              return trailerLayout(itemSnapShot.data);
                            else return noTrailer(itemSnapShot.data);
                          } else {
                            return Center(child: CircularProgressIndicator());
                          }
                        },
                      );
                    } else {
                      return Center(child: CircularProgressIndicator());
                    }
                  },
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }

  Widget noTrailer(TrailerModel data) {
    return Center(
      child: Container(
        child: Text("No trailer available"),
      ),
    );
  }

  Widget trailerLayout(TrailerModel data) {
    if (data.results.length > 1) {
      return Row(
        children: <Widget>[
          trailerItem(data, 0),
          trailerItem(data, 1),
        ],
      );
    } else {
      return Row(
        children: <Widget>[
          trailerItem(data, 0),
        ],
      );
    }
  }

  trailerItem(TrailerModel data, int index) {
    return Expanded(
      child: Column(
        children: <Widget>[
          Container(
            margin: EdgeInsets.all(5.0),
            height: 100.0,
            color: Colors.grey,
            child: Center(child: Icon(Icons.play_circle_filled)),
          ),
          Text(
            data.results[index].name,
            maxLines: 1,
            overflow: TextOverflow.ellipsis,
          ),
        ],
      ),
    );
  }
}

There are fews things to be noted here. We are initialising the MovieDetailBloc inside the didChangeDependencies() because of this. Also you can see the StreamBuilder’s snapshot data contains a Future<TrailerModel> which can be only consumed by a FutureBuilder.

I wont be explaining this stuff in depth as this article is already growing big and I have introduced many new things. Please feel free to connect with me at LinkedIn, Twitter or post your comments below if you need any help.

I hope you loved the content and learnt many new things. I just wanted to tell you that at first it will be difficult to understand it. But trust me if you do some further reading about all the topics I have touched here you will find it very simple. Please do clap out loud 😄.

If you want the complete code. Here is the github repository of the project.


The Flutter Pub is a medium publication to bring you the latest and amazing resources such as articles, videos, codes, podcasts etc. about this great technology to teach you how to build beautiful apps with it. You can find us on Facebook, Twitter, and Medium or learn more about us here. We’d love to connect! And if you are a writer interested in writing for us, then you can do so through these guidelines.

Discover and read more posts from sagar suri
get started