Last Updated: 2020-02-21

Author: Muhammad Fathy Rashad

Credit and References: Write your first flutter app, Flutter Movie App

What is Flutter?

Flutter is Google's UI toolkit for building beautiful, natively compiled applications for mobile, web, and desktop from a single codebase. Flutter works with existing code, is used by developers and organizations around the world, and is free and open source.

In this codelab, you'll create a simple mobile Flutter app. If you're familiar with object-oriented code and basic programming concepts—such as variables, loops, and conditionals—then you can complete the codelab. You don't need previous experience with Dart or mobile programming.

What you'll build

In this codelab, you're going to build a simple movie app using Flutter and TMDB API. Your app will fetch the data of several movies from TMDB, and show it in a beautiful list on the app.

The following image shows the resulting app of this codelab:

What you'll learn

What you'll need

You need two pieces of software—the Flutter SDK and an editor. (The codelab assumes that you're using Android Studio, but you can use your preferred editor.)

To install this you can follow the official installation guide or you can use our own guide (Windows only) for simpler setup with images or visual aid.

You can run the codelab by using any of the following devices:

If you want to compile your app to run on the web, you must enable this feature (which is currently in beta). To enable web support, use the following instructions:

$ flutter channel beta
$ flutter upgrade
$ flutter config --enable-web

You need only run the config command once. After enabling web support, every Flutter app you create also compiles for the web. In your IDE under the devices pulldown, or at the command line using flutter devices, you should now see Chrome and Web server listed. The Chrome device automatically starts Chrome. The Web server starts a server that hosts the app so that you can load it from any browser. Use the Chrome device during development so that you can use DevTools, and the web server when you want to test on other browsers. For more information, see Building a web application with Flutter and Write your first Flutter app on the web.

Create a simple, templated Flutter app by using the instructions in Create the app. Enter startup_namer (instead of myapp) as the project name. You'll modify the starter app to create the finished app.

Tip: If you don't see the ability to start a new Flutter project as an option in your IDE, then make sure that you have the plugins installed for Flutter and Dart.

You'll mostly edit lib/main.dart, where the Dart code lives.

Replace the contents of lib/main.dart.

Delete all of the code from lib/main.dart and replace it with the following code, which displays "Hello World" in the center of the screen.

lib/main.dart

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Welcome to Flutter',
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Welcome to Flutter'),
        ),
        body: const Center(
          child: const Text('Hello World'),
        ),
      ),
    );
  }
}

Tip: When pasting code into your app, indentation can become skewed. You can fix it with the following Flutter tools:

Run the app. You should see either Android or iOS output, depending on your device.

Android

iOS

Tip: The first time that you run on a physical device, it can take a while to load. Afterward, you can use hot reload for quick updates. In supported IDEs, Save also performs a hot reload if the app is running. When running an app directly from the console using flutter run, enter r to perform hot reload.

Observations

Plan the app layout

Before we start coding anything, we should plan how we want to layout the screen to achieve our target look.

From this image, you can infer what the widget tree looks like.

Add header text

Replace the Scaffold body with a Column. Column widget has a children property where you can give a list of Widgets to it. Then, put a Text widget as our header inside the Column children.

lib/main.dart

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Movies',                   //Change title to ‘Movies'
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Movies'),   //Change title to ‘Movies'
        ),
        body: Column(            //Add from this line
          children: <Widget>[
            Text("Discovery")
          ],
        )                       //Until this line
      ),
    );
  }
}

After you change this you will see the text shows on the screen. However it looks nothing like our desired look, which is why you will stylize it.

Stylize text

The Text widget has a style property, which expects a TextStyle widget. In TextStyle widget you can set the font size, weight, style and many others properties.

We will increase the font size and bold it.

lib/main.dart

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Movies',
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Movies'),
        ),
        body: Column(
          children: <Widget>[
            Text("Discover",          //Edit from this line
              style: TextStyle(
                fontSize: 24,
                fontWeight: FontWeight.bold
              )
            )                    //To this line
          ],
        )
      ),
    );
  }
}

The result should be like this.

Add padding

It looks better now, but you will notice the text is too close to the edge and we want it to have some space. The solution for this is Padding. You can wrap the Text in a Padding or Container widget. Both have a padding property where it expects EdgeInsets widget.

lib/main.dart

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Movies',
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Movies'),
        ),
        body: Column(
          children: <Widget>[
            Padding(                  //Edit from this line
              padding: const EdgeInsets.all(12.0),
              child: Text("Discover",
                style: TextStyle(
                  fontSize: 24,
                  fontWeight: FontWeight.bold
                )
              ),
            )                     //To this line
          ],
        )
      ),
    );
  }
}

The results should be like this:

Congratulations! Finally, the header looks like what we want. Now we can move to the next step.

In this section we will create a movie card as shown in our final app image, but only one card and not a list of cards..

Add Card

Flutter has a Card widget, which gives us a shadow effect. It has elevation properties which determine how heavy the shadow will be. However Card does not have a width or height property. So we add a Container widget inside the card to set its size.

lib/main.dart

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Movies',
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Movies'),
        ),
        body: Column(
          children: <Widget>[
            Padding(
              padding: const EdgeInsets.all(12.0),
              child: Text("Discover",
                style: TextStyle(
                  fontSize: 24,
                  fontWeight: FontWeight.bold
                )
              ),
            ),
            Padding(          // Add from this line
              padding: const EdgeInsets.all(8.0),
              child: Card(
                elevation: 5,
                child: Container(
                  height: 200,
                ),
              ),
            )                // Until this line
          ],
        )
      ),
    );
  }
}

The results should be like this:

Modularize your code

Although we have a nice card now, the code has become bigger and looks messy. It is time for us to modularize the code to make it easier to manage and also reusable. We will create our own custom widget that will contain the movie card. As the movie card data will not change, it will be a stateless widget.

lib/main.dart

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Movies',
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Movies'),
        ),
        body: Column(
          children: <Widget>[
            Padding(
              padding: const EdgeInsets.all(12.0),
              child: Text("Discover",
                style: TextStyle(
                  fontSize: 24,
                  fontWeight: FontWeight.bold
                )
              ),
            ),
            MovieCard()         //Edit this line
          ],
        )
      ),
    );
  }
}

class MovieCard extends StatelessWidget {     //Add from this line
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Card(
        elevation: 5,
        child: Container(
          height: 200,
        ),
      ),
    );
  }
}

The resulting screen should have no changes from the previous one as we are not adding anything, but only improving our code to be better.

Now you have your own MovieCard widget that can be easily reused. For example, you can add another MovieCard inside the column so that you will have 2 cards.

Add Image to the card

Next, we will add the movie poster inside the card. Note that we will not put Image directly inside the card. Based on what we planned on the layout before, the card has two parts: the movie poster and the movie description. Hence, we need to use Row widget. Additionally we will also wrap the Image in a Container so we are able to set the size.

lib/main.dart

class MovieCard extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Card(
        elevation: 5,
        child: Row(
          children: <Widget>[
            Container(
              height: 180,
              child: Image.network('https://image.tmdb.org/t/p/w500/yJdeWaVXa2se9agI6B4mQunVYkB.jpg')
            )
          ],
        ),
      ),
    );
  }
}

Now the screen should look like this:

Next, you will add the description part.

Add title and description to Card

The text component of the card consists of column of title and the description. We will also use SizedBox widget to quickly create some space between the title and description. This is easier than using padding in some cases.

class MovieCard extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Card(
        elevation: 5,
        child: Row(
          children: <Widget>[
            Container(
              height: 180,
              child: Image.network('https://image.tmdb.org/t/p/w500/yJdeWaVXa2se9agI6B4mQunVYkB.jpg')
            ),
            Expanded(                  // Add from this line
              child: Container(
                height: 180,
                padding: const EdgeInsets.all(12),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    Text('Ip Man 4: The Finale',
                      style: TextStyle(
                        fontSize: 16,
                        fontWeight: FontWeight.bold
                      )
                    ),
                    SizedBox(height: 10),
                    Expanded(child: Text('Following the death of his wife, Ip Man travels to San Francisco to ease tensions between the local kung fu masters and his star student, Bruce Lee, while searching for a better future for his son.'))
                  ],
                ),
              ),
            )               // To this line
          ],
        ),
      ),
    );
  }
}

Observation:

The resulting app should look like this:

In this section, we will now fetch the data from the TMDB API instead of hardcoding it and show it using ListView widget

Using ListView widget

ListView widget is the widget that you can use to show a list. It requires 2 properties that you need to give `itemCount` and `itemBuilder`. `itemBuilder` property expects a function that will be run x times based on the value of itemCount. So if `itemCount` is set to 5, the ListView will run the `itemBuilder` function 5 times.

Expanded widget is a similar widget to Container, however instead of needing us to specify the size, it will fill the rest of the empty space available. Hence, the ListView will be allocated the space from just below the ‘Discovery' text until the end of the screen. Alternatively, you can also use Container widget with the height set instead of Expanded widget.

lib/main.dart

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Movies',
      home: Scaffold(
          appBar: AppBar(
            title: const Text('Movies'),
          ),
          body: Column(
            children: <Widget>[
              Padding(
                padding: const EdgeInsets.all(12.0),
                child: Text("Discover",
                    style: TextStyle(
                        fontSize: 24,
                        fontWeight: FontWeight.bold
                    )
                ),
              ),
              Expanded(      // Edit this line
                child: ListView.builder(
                    itemCount: 6,
                    itemBuilder: (context, i) => MovieCard()),
              ),     // Until this line
            ],
          )
      ),
    );
  }
}

Result will be :

Get TMDB API key

For the movie data, we will be getting it from TMDB as they provide a nice API for it. First we need to get an API key which is what TMDB uses to authorize API requests to their website.

TMDB required people to register to get an API key. Then, you can apply for an API key by clicking the "API" link from the left hand sidebar within your account settings page. TMDB will give you the API key immediately after you fill in the details. Feel free to read their FAQ page for more info.

Fetch Data

Add the following code at the end of your code, this will get the data from TMDB. Make sure you replace the YOUR_API_KEY with your own TMDB API key that you obtained previously.

lib/main.dart

Future<Map> getJson() async {
  var url =
      'http://api.themoviedb.org/3/discover/movie?api_key=YOUR_API_KEY';
  http.Response response = await http.get(url);
  return json.decode(response.body);
}

Adding the code will give you an error as it requires you to have an http package. To add the http package, you need to edit the `pubspec.yaml` and add `http:` under dependencies.

Pubspec.yml is the file where you can configure a lot of settings such as the name and description of the app, library/packages dependencies that are required, and assets that you are using.

You need to add the http package under the dependencies, make sure the indentation is correct:

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  http:     //Add this line

Then, press the `Packages get` or `Get Dependencies` button that appeared to install the required package near the top of android studio. Alternatively, you run `flutter pub get` on theterminal.

Afterwards add these imports on the top of you `main.dart` file.

lib/main.dart

import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;

You should not get an error after this.

Create Stateful Widget for MovieList

As we are fetching data from the internet. The movie data are not constant anymore, since at the start the movies are empty until the moment the app finished getting the data from TMDB.

Type `stful` on your code and press enter. It will generate the code required to create a Stateful Widget. Name the widget `MovieList`.

After typing `stful` you should get a code like this:

lib/main.dart

class MovieList extends StatefulWidget {
  @override
  _MovieListState createState() => _MovieListState();
}

class _MovieListState extends State<MovieList> {
  @override
  Widget build(BuildContext context) {
    return Container();          //Replace this with the Expanded ListView on MyApp
  }
}

Then copy paste the ListView builder with the expanded as well into the Container in _MovieListState

You should get the following code:

lib/main.dart

class MovieList extends StatefulWidget {

  @override
  _MovieListState createState() => _MovieListState();
}

class _MovieListState extends State<MovieList> {
  @override
  Widget build(BuildContext context) {
    return Expanded(         //Replaced the previous Container
      child: ListView.builder(
          itemCount: 6,
          itemBuilder: (context, i) => MovieCard()),
    );
  }
}

So now we have modularized/refactored the ListView code into our new MovieList Widget

Then, we can replace the Expanded(ListView) part with the MovieList widget.

Now our MyApp widget should look like this:

lib/main.dart

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Movies',
      home: Scaffold(
          appBar: AppBar(
            title: const Text('Movies'),
          ),
          body: Column(
            children: <Widget>[
              Padding(
                padding: const EdgeInsets.all(12.0),
                child: Text("Discover",
                    style: TextStyle(
                        fontSize: 24,
                        fontWeight: FontWeight.bold
                    )
                ),
              ),
              MovieList(),                 // Add this
//              Expanded(                  // Remove this
//                child: ListView.builder(
//                    itemCount: 6,
//                    itemBuilder: (context, i) => MovieCard()),
//              ),
            ],
          )
      ),
    );
  }
}

Our app now should still look like previou one but we are using stateful widget this time.

Now we want to show the movies data from the TMDB, so we create a `movies` variable so we can store the movies data that we get. And MovieList widget can access the data through the variable.

lib/main.dart

class _MovieListState extends State<MovieList> {
  var movies;     // Add this

  void getData() async {      //Add this function
    var data = await getJson();
    setState(() {
      movies = data['results'];
    });
  }                          //Until here

  @override
  Widget build(BuildContext context) {
    getData();                           // Dont forget to call the function here, so it run the fetching code
    return Expanded(
      child: ListView.builder(
          itemCount: 6,
          itemBuilder: (context, i) => MovieCard()),
    );
  }
}

Change your MovieCard to receive a parameter which will be the movie data

lib/main.dart

class MovieCard extends StatelessWidget {
  final movie;  // Add this 
  MovieCard(this.movie); // Add this
// the rest of the code

Then edit your itemBuilder and ItemCount property in your ListView Widget.

lib/main.dart

class _MovieListState extends State<MovieList> {
  var movies;     // Add this

  Future<void> getData() async {      //Add this function
    try {
      var data = await getJson();
      setState(() {
        movies = data['results'];
      });
    }
    catch (error) { print(error);}
  }                          //Until here

  @override
  Widget build(BuildContext context) {
    getData();
    return Expanded(
      child: ListView.builder(
          itemCount: movies == null ? 0 : movies.length,      // Edit this
          itemBuilder: (context, i) => MovieCard(movies[i])),   //Edit this
    );
  }
}

Then change your MovieCard title, image, and and description to use the `movie` variable.

lib/main.dart

class MovieCard extends StatelessWidget {
  final movie;
  MovieCard(this.movie);

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Card(
        elevation: 5,
        child: Row(
          children: <Widget>[
            Container(
                height: 180,
                child: Image.network("https://image.tmdb.org/t/p/w500/${movie[‘poster_path']}")    // Edit this
            ),
            Expanded(
              child: Container(
                height: 180,
                padding: const EdgeInsets.all(12),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: <Widget>[
                    Text(movie['title'],     // Edit this
                        style: TextStyle(
                            fontSize: 16,
                            fontWeight: FontWeight.bold
                        )
                    ),
                    SizedBox(height: 10),
                    Expanded(child: Text(movie['overview']))   // Edit this
                  ],
                ),
              ),
            )
          ],
        ),
      ),
    );
  }
}

The final result should be

:

InkWell Widget

Flutter provides a lot of widgets that have a button's functionalities such as FlatButton, RaisedButton, GestureDetector and many others. Most of these Widgets have something in common, which is the `onPressed ` or `onTap` properties where you can specify the function that will be run when you click/tap on the button.

However, we will be using the InkWell widget for our app. The unique thing about InkWell is that they will show a ripple effect when you pressed the button.

In MovieCard class, wrap the Row widget just inside the Card widget with an InkWell widget.

lib/main.dart

class MovieCard extends StatelessWidget {
  final movie;  // Add this
  MovieCard(this.movie); // Add this

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Card(
        elevation: 5,
        child: InkWell(          //Wrapped Row with InkWell
          onTap: () {},          //Add this as well
          child: Row(
            children: <Widget>[
              Container(
                  height: 180,
                  child: Image.network('https://image.tmdb.org/t/p/w500/${movie['poster_path']}')
              ),
              Expanded(
                child: Ink(
                  height: 180,
                  padding: const EdgeInsets.all(12),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: <Widget>[
                      Text(movie['title'],
                          style: TextStyle(
                              fontSize: 16,
                              fontWeight: FontWeight.bold
                          )
                      ),
                      SizedBox(height: 10),
                      Expanded(child: Text(movie['overview']))
                    ],
                  ),
                ),
              )
            ],
          ),
        ),
      ),
    );
  }
}

Observation:

The MovieCard should be clickable after the changes:

In this section we will learn how to navigate between different screens.

Modularizing code for screens

When we have multiple screens for the app, the best practice is to separate each screen into different files to make managing it easier.

Create a folder inside `lib` and name it `screens`. This is where we will put all our screens file.

Create a Home screen

lib/screens/home.dart

import 'package:flutter/material.dart';
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;

class Home extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

Move Movie List to Home screen

The final code should look like these:

lib/main.dart

import 'package:flutter/material.dart';
// Remove the unused import as we moved the getJson function to other file

//import 'dart:async';
//import 'dart:convert';
//import 'package:http/http.dart' as http;

import 'screens/home.dart';       // Import home.dart

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Movies',
      home: Home()           // Replaced Scaffold with the imported home.dart
    );
  }
}

lib/screens/home.dart

import 'package:flutter/material.dart';
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;

class Home extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(                     // Replaced Container with Scaffold copied from main.dart
        appBar: AppBar(
          title: const Text('Movies'),
        ),
        body: Column(
          children: <Widget>[
            Padding(
              padding: const EdgeInsets.all(12.0),
              child: Text("Discover",
                  style: TextStyle(
                      fontSize: 24,
                      fontWeight: FontWeight.bold
                  )
              ),
            ),
            MovieList(),                 // Add this
          ],
        )
    );
  }
}

// Added code

class MovieList extends StatefulWidget {

  @override
  _MovieListState createState() => _MovieListState();
}

class _MovieListState extends State<MovieList> {
  var movies;     // Add this

  Future<void> getData() async {      //Add this function
    try {
      var data = await getJson();
      setState(() {
        movies = data['results'];
      });
    }
    catch (error) { print(error);}
  }                          //Until here

  @override
  Widget build(BuildContext context) {
    getData();
    return Expanded(
      child: ListView.builder(
          itemCount: movies == null ? 0 : movies.length,      // Edit this
          itemBuilder: (context, i) => MovieCard(movies[i])),   //Edit this
    );
  }
}


class MovieCard extends StatelessWidget {
  final movie;  // Add this
  MovieCard(this.movie); // Add this

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Card(
        elevation: 5,
        child: InkWell(
          onTap: () {},
          child: Row(
            children: <Widget>[
              Container(
                  height: 180,
                  child: Image.network('https://image.tmdb.org/t/p/w500/${movie['poster_path']}')
              ),
              Expanded(
                child: Ink(
                  height: 180,
                  padding: const EdgeInsets.all(12),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: <Widget>[
                      Text(movie['title'],
                          style: TextStyle(
                              fontSize: 16,
                              fontWeight: FontWeight.bold
                          )
                      ),
                      SizedBox(height: 10),
                      Expanded(child: Text(movie['overview']))
                    ],
                  ),
                ),
              )
            ],
          ),
        ),
      ),
    );
  }
}

Future<Map> getJson() async {
  var url =
      'http://api.themoviedb.org/3/discover/movie?api_key=20a3f47c1f9a22b518bf93d335169cca';
  http.Response response = await http.get(url);
  return json.decode(response.body);
}

In this section, we will create a new screen where we show a detailed info of the movie selected.

Preview of the detail screen:

Plan the layout

From the image we can plan how we want to layout the app to achieve the desired look.

We can see the screen is based on column, a poster, title, the 3 boxes and finally the description.

Hence our widget tree will look similar to this:

Column
----Poster
----Title
----Row
--------Duration
--------Year
--------Rating
----Description

Create Detail screen

lib/screens/detail.dart

import 'package:flutter/material.dart';

class Detail extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

Replace the container with Scaffold and add an App Bar. Use Column as the body.

lib/screens/detail.dart

import 'package:flutter/material.dart';

class Detail extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Movie Title')
      ),
      body: Column()
    );
  }
}

However, after you will notice that the app is still showing the home screen. So how do we show our Detail screen?

lib/screens/main.dart

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Movies',
      home: Detail()           // Replaced Home with the imported detail.dart
    );
  }
}

Now our app should display the our newly created Detail screen.

Add Poster Image

Add Image widget inside the Column, wrap it with Container to set the height, and wrap it again with Center to center the widget.

lib/screens/detail.dart

class Detail extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Movie Title')
      ),
      body: Column(
        children: <Widget>[
          Center(               // Added from this line
            child: Container(
              height: 400,
              margin: EdgeInsets.all(40),
              child: Image.network('https://image.tmdb.org/t/p/w500/yJdeWaVXa2se9agI6B4mQunVYkB.jpg')
            ),
          ),
        ]
      )
    );
  }
}

Now the movie poster will show up on the app.

Add Movie Title

Next, just add Text widget below the poster image, and stylize it accordingly.

lib/screens/detail.dart

class Detail extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Movie Title')
      ),
      body: Column(
        children: <Widget>[
          Center(
            child: Container(
              height: 400,
              margin: EdgeInsets.fromLTRB(20, 40, 20, 20),
              child: Image.network('https://image.tmdb.org/t/p/w500/yJdeWaVXa2se9agI6B4mQunVYkB.jpg')
            ),
          ),
          Text("Ip Man 4 The Finale",    // Add Movie title
            style: TextStyle(
              fontWeight: FontWeight.bold,
              fontSize: 24
            ),
          )
        ]
      )
    );
  }
}


The app should look like this now:

Adding Info Box

Next, we will create the three boxes showing the movie duration, year, and rating. However, we can see that the boxes have the same structure. Hence, instead of repeating the code 3 times, we can create a new InfoCard widget that we can reuse.

Create a new stateless widget named InfoCard that receives a string parameter to be shown as the inside the card.

lib/screens/detail.dart

class InfoCard extends StatelessWidget {
  String text = "";
  InfoCard(this.text);

  @override
  Widget build(BuildContext context) {
    return Card(
      elevation: 4,
      child: Container(
        height: 80,
        width: 80,
        padding: EdgeInsets.all(10),
          child: Column(
            children: <Widget>[
              Text(text),
            ],
          )
      )
    );
  }
}

Then add the InfoCard widget below the Movie Title to see what the widget look like.

lib/screens/detail.dart

class Detail extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Movie Title')
      ),
      body: Column(
        children: <Widget>[
          Center(
            child: Container(
              height: 400,
              margin: EdgeInsets.fromLTRB(20, 40, 20, 20),
              child: Image.network('https://image.tmdb.org/t/p/w500/yJdeWaVXa2se9agI6B4mQunVYkB.jpg')
            ),
          ),
          Text("Ip Man 4 The Finale",
            style: TextStyle(
              fontWeight: FontWeight.bold,
              fontSize: 24
            ),
          ),
          InfoCard("Some Info")            // Added InfoCard
        ]
      )
    );
  }
}

The app should look like this:

Adding Info Card with Icon

Add an Icon on the InfoCard and also SizedBox to create some space between the icon and the text.

lib/screens/detail.dart

class InfoCard extends StatelessWidget {
  String text = "";
  InfoCard(this.text);

  @override
  Widget build(BuildContext context) {
    return Card(
      elevation: 4,
      child: Container(
        height: 80,
        width: 80,
        padding: EdgeInsets.all(10),
          child: Column(
            children: <Widget>[
              Icon(Icons.timer,       // Added Icon
                size: 35
              ),
              SizedBox(height: 5),    // Added SizeBox
              Text(text),
            ],
          )
      )
    );
  }
}

Then the app will look like this:

However we do not want the icon for three boxes as the same, hence, we need to use the Icon as parameter instead of hardcoding it.

lib/screens/detail.dart

class InfoCard extends StatelessWidget {
  String text = "";
  IconData icon;                      // Add this
  InfoCard(this.text, this.icon);     // Edit this

  @override
  Widget build(BuildContext context) {
    return Card(
      elevation: 4,
      child: Container(
        height: 80,
        width: 80,
        padding: EdgeInsets.all(10),
          child: Column(
            children: <Widget>[
              Icon(icon,       // edit this
                size: 35
              ),
              SizedBox(height: 5),
              Text(text),
            ],
          )
      )
    );
  }
}

Then do not forget to pass the new icon parameter to the InfoCard in Detail class.

lib/screens/detail.dart

InfoCard("Hello", Icons.timer)

Now finally, wrap the InfoCard in Row, and create the three boxes according to its icon and text. Additionally add a SizedBox between the Title and the info boxes.

lib/screens/detail.dart

class Detail extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Movie Title')
      ),
      body: Column(
        children: <Widget>[
          Center(
            child: Container(
              height: 400,
              margin: EdgeInsets.fromLTRB(20, 40, 20, 20),
              child: Image.network('https://image.tmdb.org/t/p/w500/yJdeWaVXa2se9agI6B4mQunVYkB.jpg')
            ),
          ),
          Text("Ip Man 4 The Finale",
            style: TextStyle(
              fontWeight: FontWeight.bold,
              fontSize: 24
            ),
          ),
          SizedBox(               // Added SizedBox
            height: 20,
          ),
          Row(         // Edited this line
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: <Widget>[
              InfoCard('124 min', Icons.timer),
              InfoCard('2018', Icons.calendar_today),
              InfoCard('8.4/10', Icons.star)
            ],
          )
        ]
      )
    );
  }
}

Now your app will look like this:

Add Movie Description

Next we will at the movie description at the bottom.

lib/screens/detail.dart

class Detail extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Movie Title')
      ),
      body: Column(
        children: <Widget>[
          Center(
            child: Container(
              height: 350,
              margin: EdgeInsets.fromLTRB(20, 40, 20, 20),
              child: Image.network('https://image.tmdb.org/t/p/w500/yJdeWaVXa2se9agI6B4mQunVYkB.jpg')
            ),
          ),
          Text("Ip Man 4 The Finale",
            style: TextStyle(
              fontWeight: FontWeight.bold,
              fontSize: 24
            ),
          ),
          SizedBox(
            height: 20,
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: <Widget>[
              InfoCard('124 min', Icons.timer),
              InfoCard('2018', Icons.calendar_today),
              InfoCard('8.4/10', Icons.star),
            ],
          ),
          Padding(         // Added from this line
            padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 40.0),
            child: Text('Following the death of his wife, Ip Man travels to San Francisco to ease tensions between the local kung fu masters and his star student, Bruce Lee, while searching for a better future for his son.',
              style: TextStyle(fontSize: 18)
            ),
          )
        ]
      )
    );
  }
}

However, as you added the text, you will notice you will get an error because the text overflowed the screen. This is because flutter will not make your app scrollable automatically when your widget exceeds the screen height. To solve this, we need to use a scrolling widget which in this we will be using SingleChildScrollView widget.

Wrap your Top most column with a SingleChildScrollView.

lib/screens/detail.dart

class Detail extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Movie Title')
      ),
      body: SingleChildScrollView(
        child: Column(
          children: <Widget>[
            Center(
              child: Container(
                height: 350,
                margin: EdgeInsets.fromLTRB(20, 40, 20, 20),
                child: Image.network('https://image.tmdb.org/t/p/w500/yJdeWaVXa2se9agI6B4mQunVYkB.jpg')
              ),
            ),
            Text("Ip Man 4 The Finale",
              style: TextStyle(
                fontWeight: FontWeight.bold,
                fontSize: 24
              ),
            ),
            SizedBox(
              height: 20,
            ),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: <Widget>[
                InfoCard('124 min', Icons.timer),
                InfoCard('2018', Icons.calendar_today),
                InfoCard('8.4/10', Icons.star),
              ],
            ),
            Padding(    // Added from this line
              padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 40.0),
              child: Text('Following the death of his wife, Ip Man travels to San Francisco to ease tensions between the local kung fu masters and his star student, Bruce Lee, while searching for a better future for his son.',
                style: TextStyle(fontSize: 18)
              ),
            )
          ]
        ),
      )
    );
  }
}

Now your detail screen should be scrollable:

In Flutter, screens or pages are usually called routes. To start navigating between screens, you need to define all the screens available by setting the routes property in MaterialApp.

Add the Home and Detail routes to the MaterialApp in `main.dart`

lib/main.dart

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Movies',
      //home: Home(),              // Remove this
      routes: {                    // Added routes
        '/' : (context) => Home(),
        '/detail': (context) => Detail()
      }
    );
  }
}

Observation

Navigating to other screen

To navigate to other screen, you just need to use the following code:

Navigator.pushNamed(context, ‘/routeName')

Now, we can implement the pressed function in the movie list. If any of the movie cards are tapped, the app should navigate to the movie detail screen.

Add the Navigation code on InkWell onTap property

lib/screens/home.dart

class MovieCard extends StatelessWidget {
  final movie;  // Add this
  MovieCard(this.movie); // Add this

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Card(
        elevation: 5,
        child: InkWell(
          onTap: () {
            Navigator.pushNamed(context, '/detail');      // Add this line
          },
          child: Row(
            children: <Widget>[
              Container(
                  height: 180,
                  child: Image.network('https://image.tmdb.org/t/p/w500/${movie['poster_path']}')
              ),
              Expanded(
                child: Ink(
                  height: 180,
                  padding: const EdgeInsets.all(12),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: <Widget>[
                      Text(movie['title'],
                          style: TextStyle(
                              fontSize: 16,
                              fontWeight: FontWeight.bold
                          )
                      ),
                      SizedBox(height: 10),
                      Expanded(child: Text(movie['overview']))
                    ],
                  ),
                ),
              )
            ],
          ),
        ),
      ),
    );
  }
}

Now, the app should look like this

However, notice that the Movie data on detail screen are hardcoded and not following the tapped movie.

Pass arguments to a named route

The Navigator provides the ability to navigate to a named route from any part of an app using a common identifier. In some cases, you might also need to pass arguments to a named route. In our case, we want to navigate to the /detail route and pass information about the movie to that route.

You can accomplish this task using the arguments parameter of the Navigator.pushNamed() method. Then extract the arguments using the ModalRoute.of() method inside the widget where we navigated to.

Pass the movie data as arguments when navigating

lib/screens/home.dart

class MovieCard extends StatelessWidget {
  final movie;  // Add this
  MovieCard(this.movie); // Add this

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Card(
        elevation: 5,
        child: InkWell(
          onTap: () {
            Navigator.pushNamed(context, '/detail', arguments: {       // Edit this line
              'movie': movie
            });
          },
          child: Row(
            children: <Widget>[
              Container(
                  height: 180,
                  child: Image.network('https://image.tmdb.org/t/p/w500/${movie['poster_path']}')
              ),
              Expanded(
                child: Ink(
                  height: 180,
                  padding: const EdgeInsets.all(12),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: <Widget>[
                      Text(movie['title'],
                          style: TextStyle(
                              fontSize: 16,
                              fontWeight: FontWeight.bold
                          )
                      ),
                      SizedBox(height: 10),
                      Expanded(child: Text(movie['overview']))
                    ],
                  ),
                ),
              )
            ],
          ),
        ),
      ),
    );
  }
}

Next, we need to extract the data on the next screen then replace the hardcoded value with the extracted movie data.

lib/screens/detail.dart

class Detail extends StatelessWidget {
  var movie;              // Added movie variable

  @override
  Widget build(BuildContext context) {
    Map data = ModalRoute.of(context).settings.arguments;  // Added this line
    movie = data['movie'];     // Added this line

    return Scaffold(
      appBar: AppBar(
        title: Text('Movie Title')        // Edited this line
      ),
      body: SingleChildScrollView(
        child: Column(
          children: <Widget>[
            Center(
              child: Container(
                height: 350,
                margin: EdgeInsets.fromLTRB(20, 40, 20, 20),
                child: Image.network("https://image.tmdb.org/t/p/w500/${movie['poster_path']}")         // Edited this line
              ),
            ),
            Text(movie['title'],          // Edited this line
              style: TextStyle(
                fontWeight: FontWeight.bold,
                fontSize: 24
              ),
            ),
            SizedBox(
              height: 20,
            ),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: <Widget>[
                InfoCard('~ min', Icons.timer),    // Edited
                InfoCard(movie['release_date'].substring(0, 4), Icons.calendar_today),   // Edited
                InfoCard('${movie['vote_average']}/10', Icons.star),   // Edited
              ],
            ),
            Padding(
              padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 40.0),
              child: Text(movie['overview'],    // Edited
                style: TextStyle(fontSize: 18)
              ),
            )
          ]
        ),
      )
    );
  }
}

Now your app should load the movie detail based on the one you selected.

Congratulations, you have successfully built a Movie App with Flutter!

Feel free to give a star to this workshop if you liked the tutorial.