Skip to content

Instantly share code, notes, and snippets.

@TechieBlossom
Created September 28, 2020 10:32
Show Gist options
  • Save TechieBlossom/d30784d81be678ced09d8a0a24c87527 to your computer and use it in GitHub Desktop.
Save TechieBlossom/d30784d81be678ced09d8a0a24c87527 to your computer and use it in GitHub Desktop.

Welcome back,

We are building a Movie App with the best coding practices and tools. In previous articles, DataSources, Repositories & UseCase, and Dependency Injection.

In this article, we'll see create the top part in HomeScreen that contains Animated Carousel. Together with this, we'll also do the basic setup for UI.

Assets

I have gathered some assets and put them in the GitHub already, you can check out the master branch or 5_homescreen_carousel branch. You can take them from there. Now, open pubspec.yaml and update the assets there as well.

https://gist.github.com/eee0c3edd872c24b107c2c22d087cf0d

Colors

In the presentation/themes folder, create a new file app_color.dart. This file will have all the colors required in the application, except for those which are already defined in Flutter framework. MovieApp only uses 3 colors throughout the application:

https://gist.github.com/d68b6b606e7767eb4f3d29a7c3f93385

  1. Create a class AppColor.
  2. Add a private constructor since it is not required to instantiate the class.
  3. Declare the colors Vulcan, royal blue, violet.

Naming colors - You might often find naming colors in the app tedious. Use this website. Some colors would be hard to spell out, so you can name them anything you want, as I have done with violet in this app.

We're done with the colors.

Initialize App

Open main.dart and clear out the main(): https://gist.github.com/9fbe03bc2a5f9ac190c5b102e21c5121

  1. You don't need async now, as we're using unawaited. In the future, we'll need it when we do Hive initialization.
  2. As per official Flutter documentation, this is the glue that binds the framework to the Flutter engine.
  3. As we're only supporting portrait till now, you should force it to have portraitUp orientation. If in the future, you support other orientations, you can remove this snippet.
  4. Initialize GetIt to provide us with dependencies.
  5. Instead of having the app in this file itself, make this class small by moving out the MaterialApp widget in movie_app.dart (more on that soon).

Custom ScreenUtil

We all know, even though it is claimed that Flutter can create the UI for any screen size. But in certain situations when you give fixed width and height to a widget, they don't seem to be proportional to the screen for obvious reasons.

I have been using flutter_screenutil in my recent work and this plugin helps in creating pixel-perfect UI in Flutter. With recent advancements in mobile form factors with some of them having notches at the top or bottom of the screen, the current calculation of scaleHeight should also consider the notches.

Go to Flutter ScreenUtil GitHub Repository, and copy the code.

Create a screenutil sub-folder in common folder and paste the code in a new file screenutil.dart

Change two things in the file: https://gist.github.com/e972cd587cdb36b72cd4508697906689

  1. Here, defaultWidth and defaultHeight are the width and height of the designs that the designer has used. They are not your mobile screen width and height. If you don't want to specify the defaultWidth and defaultHeight, you can do so by invoking ScreenUtil.init(width: 414,height: 896,); before returning MaterialApp.
  2. The new scaleHeight factor now considers top and bottom notches if present in some phones.

If you've seen my previous videos, I have shown you how to use ScreenUtil in th dimensions. You generally call ScreenUtil().setWidth(100), ScreenUtil().setHeight(100) or ScreenUtil().setSp(100). Here 100 is the dimension that you scale according to the screen dimensions. To reduce some boilerplate, let's create extensions for this.

In the common/extensions folder, create a new file size_extensions.dart

Create an extension on num: https://gist.github.com/52eeaad5a00a1d2af3be4056c1f668a6

This is straight forward. Now, you can invoke methods like 100.w, 100.h or 100.sp whenever required.

Sizes

As you've seen we directly used 100. This is straight forward, but imagine 100 is used 10-20 times in the app, Will you every time use 100. The answer is NO. This will consume more memory. Instead, declare all the dimensions in one file.

In the common/constants folder, create a file size_constants.dart

Create class Sizes and declare dimensions as static const double: https://gist.github.com/42244d02d1eaa568abd6204acf2d0942

In the presentation/themes folder, create a file theme_text.dart

Create a class in similar fashion as Sizes and AppColor: https://gist.github.com/6dece86ee278cf0296fef0e2884ced4d

  1. We're using Poppins Font.
  2. Create a white headline6 (Refer below guidelines for font sizes from Material Design), by copying the Poppins' headline6. We add .sp so that the text sizes are also according to the screen width.
  3. Create a public static method that returns the TextTheme. This will be used by MaterialApp, rest all methods can be private.

MaterialApp

As you can recall, before creating screen_util.dart I updated main.dart with MovieApp.

In the presentation folder, create a new file movie_app.dart

Create MovieApp as a stateless widget:

https://gist.github.com/c52952363c674c19c1dd9f9de73e332a

  1. Initialize ScreenUtil so that we can use it while defining
  2. Use MaterialApp widget with debugBanner as false or true.
  3. Define the ThemeData of the app. Make Vulcan as primary as well as scaffoldBackground color. All our screens have Vulcan as their background color. Also, give the text theme.
  4. Our first screen - HomeScreen (we're yet to design this).

HomeScreen (Top Part)

In this video/article, I'll show only the top part of HomeScreen as this also contains a basic starting setup.

Movie Carousel Bloc

Before we move to UI, let's add the Bloc as well which will manage the state of our Carousel.

In the presentation/blocs folder,

With the help of bloc extension, create a new bloc with movie_carousel name. Rename the auto-generated bloc folder with movie_carousel.

You can see bloc, state, and event file auto-generated.

In movie_carousel_event.dart add CarouselLoadEvent:

https://gist.github.com/e817c3d164bf67631573f7db53955e98

  1. Extend the event with abstract class because the bloc's definition accepts the type of MovieCarouselEvent. This event will be dispatched when the user comes to the screen.
  2. defaultIndex will give us the flexibility to decide which movie will be in the center of our carousel at the start.
  3. A const constructor with defaultIndex as 0, if not passed.
  4. props as explained in previous videos/articles is used for comparison between 2 objects of the same type.

In movie_carousel_state.dart, create 3 states:

https://gist.github.com/75e3e70e4a5179f315b23fec44c02303

  1. MovieCarouselInitial to be emitted as the first state when the bloc initializes.
  2. MovieCarouselError to be emitted if there is an error thrown from API. I'll not observe this state in this video/article.
  3. MovieCarouselLoaded to be emitted with a list of trending movies and default index, which is passed from CarouselLoadEvent earlier.

In movie_carousel_bloc.dart, declare GetTrending usecase:

https://gist.github.com/f4922bc5b5a06bef59ce88c0c75ab24a

  1. GetTrending will be the final variable.
  2. Clearly, MovieCarouselBloc is dependent on GetTrending, if you remember from the previous video/article about Dependency Injection.

Replace the TODO with call to GetTrending and handle the response: https://gist.github.com/0376ff660a9fa6eca0e2640177f2c480

  1. Handle if the event dispatched is CarouselLoadEvent
  2. Call the getTrending usecase with NoParams()
  3. Use fold operator to handle the response
  4. When error(left), yield error state (Future-proof), not handling this state in this video/article.
  5. When success(right), yield success state with movies and default index.

Before we move to Widgets using this, let's add the dependency in get_it.dart: https://gist.github.com/4481be148013904e2e632b495ebddb7b

This time we'll declare the factory because we want a new instance of the bloc whenever we need the carousel bloc. Since this is a home screen, the first screen, you can also declare this bloc as a singleton, totally your choice.

We're ready with Bloc, Colors, Fonts, Dimensions. Let's jump to UI creation now.

Widget Segregation

I am listing down all the widgets that we'll create from the bottom-up approach. Preferably sequence is a small widget to a Bigger widget, but in some case, it is the order of creation as well.

  1. Home Screen
  2. Logo Widget
  3. Movie App Bar Widget
  4. Movie Carousel Widget
  5. Movie Card Widget
  6. Movie Page View Widget
  7. Animated Movie Card Widget
  8. Movie Backdrop Widget
  9. Movie Data Widget
  10. Separator Widget

HomeScreen

We're starting our first journey, create home folder in journeys folder.

In the journeys/home, create a new file home_screen.dart

Create a Stateful widget - HomeScreen https://gist.github.com/22713bd84c29cff7bd74dc23a9ca9d79

  1. Initialize the MovieCarouselBloc from GetIt.
  2. When the home screen initializes, dispatch the only event for MovieCarouselBloc This will make an API call and yield the MovieCarouselLoaded or MovieCarouselError state.
  3. In dispose(), don't forget to close the bloc.

The home screen has 2 sections - top and bottom. To make these sections proportional for any mobile size, we'll use FractionallySizedBox. The top section is 60% of the screen and the bottom section is 40%. Let's create Stack with 2 FractionallySizedBox.

https://gist.github.com/237bafe7b043c1454dfe1597db1f8a70

  1. When you use FractionallySizedBox, you should use StackFit.expand, because this allows stack to take the available space.
  2. FractionallySizedBox that uses fractions to decide on the proportion of screen that it will take.
  3. The top part should have topCenter as its alignment.
  4. The top section is 60% of the screen, hence it should be aligned top with 60% of the height of the available space, which in this case is a complete screen. Once, we add the MovieCarouselWidget, it will go in this part, replacing the Placeholder.
  5. The bottom FractionallySizedBox, obviously will have bottomCenter as its alignment.
  6. The heightFactor of this remaining part of the screen which will be 0.4.
  7. MovieTabbedWidget will replace the Placeholder widget, in the next video.

If you run the app with this code, you'll see the screen perfectly divided into 2 parts. Check this screen in different screen sizes.

Logo Widget

As I said, we'll develop widgets from bottom to top. So, before moving to actual MovieCarouselWidget, let's create logo widget that will go in MovieAppBar and MovieAppBar goes in MovieCarouselWidget.

In the presentation/widgets folder, create a new file logo.dart:

https://gist.github.com/116f1b4cc30b7d6a381a3d38b2753c15

  1. Logo is a stateless widget with a dynamic height that will be provided by the calling widget. This is important here because the same Logo widget will be used in the NavigationDrawer, when we implement that.
  2. Constructor with height as required field and add some assertions, that make this widget fail-safe. With these two assertions, this widget unknowingly can't be called with height as null or <= 0.
  3. Just use the logo image from assets/pngs folder. Notice the usage of .h.

MovieAppBar

Even though this app has only one instance of the custom AppBar, it is always good to create a separate widget for maintainability and scalability. We'll also use svg images now, so add flutter_svg dependency:

https://gist.github.com/0dd3c167f2a022b3b19085daa777fbba

In the presentation/widgets folder, create a new file movie_app_bar.dart:

https://gist.github.com/6603ab6d87e2b0d16b60be1ea9d16e62

  1. Because we're creating our own app bar, it is necessary to have padding from left, right, and top. Notice, we're considering the notch height in padding-top to make it work for phones with the notch at top. It is useless to mention the use of .w when considering the horizontal spacing and .h when considering the vertical spacing.
  2. Use Row to layout the elements in horizontal.
  3. In Row, at start and end, add the 2 IconButtons. One being SvgPicture and the other being taken from the Flutter framework itself.
  4. The remaining space in between these 2 images, use the Logo widget.

MovieCarouselWidget

This widget requires the list of movies, and the default movie index that will appear in the center of the carousel. Let's use the bloc to get the fetched movies.

Update the HomeScreen: https://gist.github.com/bf85941f6122d5926e0ffbcb50771f1b

  1. Use BlocProvider to provide the MovieCarouselBloc instance down the tree.
  2. You need not create the bloc here as it is already done in initState().
  3. Use BlocBuilder to read the current state of MovieCarouselBloc. The builder takes in context and state.
  4. When loading of trending movies is a success, we show the previously used Stack having two sections - Top and Bottom. Give the MovieCarouselWidget with instead of first PlaceHolder.
  5. When loading of trending movies is an error, we show an empty sized box as of now. Later, incoming videos I'll show you how to handle UI when error.

If you remember the MovieCarouselLoaded state contains movies and default index, so we'll create MovieCarouselWidget that will be used in the top section of the Stack.

In the presentation/journeys/home/movie_carousel, create a new file movie_carousel_widget.dart:

https://gist.github.com/f2becbb54dccdc1403d1dbe1585ad149

  1. MovieCarouselWidget is a stateless widget, that works on a list of movies and the defaultIndex.
  2. Create a constructor with both the fields as required and add the assertion, that we've been adding everywhere for the defaultIndex. Assertions are a very good way for reducing the number of errors when working individually or as part of a team.
  3. A column with just 2 elements. First is the MovieAppBar that we created before.
  4. Next in Column is MoviePageView, below MovieAppBar that we'll create now. This widget also takes in the list of movies and the defaultIndex.

MovieCardWidget

In general, MoviePageView is a PageView, that takes in multiple children. Each child is a MovieCardWidget. Let's create that first.

We're about to load image from the internet, let's add a dependency:

https://gist.github.com/dee3f577a1063d1c5e61dc8b8ff57bb6

In the presentation/journeys/home/movie_carousel, create a new file movie_card_widget.dart:

https://gist.github.com/7f575f9717a0f17aa5182d16ab273394

  1. You'll need movieId in the future when we tap on this card to move to the movie detail screen.
  2. The posterPath is required to load the image. This will be taken from MovieEntity and will be in this format kqjL17yufvn9OVLyXYpvtyrFfak.jpg.
  3. Use CachedNetworkImage with the imageUrl prepended with BASE_IMAGE_URL. In DataSources, I have explained the use of BASE_IMAGE_URL.
  4. Use ClipRRect to clip the image, with a borderRadius. This will add the curves on all the vertices of the images.
  5. Use GestureDetector to enable tappable events on the card.
  6. Use Material to give elevation to the card.

MoviePageView

Let's create the MoviePageView now:

In the presentation/journeys/home/movie_carousel, create a new file movie_page_view.dart:

https://gist.github.com/67f4d386eecc3c7687f408d192b1117a

  1. Create a stateful widget with a list of movies and initialPage. The initialPage is the same as the defaultIndex, so apply the same assertion to this as well.
  2. In the State class of MoviePageView, declare a PageController.
  3. In initState(), instantiate _pageController with viewportFraction as 0.7. viewportFraction decides how much screen share each item of PageView will take.

In the same file, create the UI.

https://gist.github.com/6f08a8c4ede2f0ecba3f84ca3d9ff8d8

  1. Use PageView.Builder. Builder is efficient when you don't know about how many children will be drawn. To manipulate how much part is visible on the screen, we use _pageController.
  2. In the itemBuilder, based on index return the MovieCardWidget. Generally, we get 20 movies from the API, so itemBuilder will create 20 cards.
  3. When you're in between of a complete scroll transaction, pageSnapping true makes it complete the scroll action.
  4. You should mention the itemCount because itemBuilder will be called only with indices greater than or equal to zero and less than itemCount. In short, it's a for loop with for (int i = 0;i<length;i++). we're doing very safe code here, but still, if movies are null, then this will throw an error. So, use ?? and return 0.
  5. To update the backdrop image and title of the movie below PageView, we'll need to get the callback when the PageView is scrolled.
  6. Wrap the PageView.Builder with Container, so that we can give height and margin to it.
  7. To maintain some space between MovieAppBar and the other details of the movie, we need the vertical margin.
  8. Once we add the animation to the MovieCardWidget, we'll need the height. So, after some calculation 35% of the screen height is the perfect value here. Don't hardcode any heights in this case, using ratios is best. As we used 0.6 for FractionallySizedBox in HomeScreen, 0.35 here makes total sense.

When you run, you'll see the PageView horizontally scrollable with all the MovieCardWidgets touching each other. We need to add animation while scrolling, also give some spacing between each individual MovieCardWidget.

AnimatedMovieCardWidget

This widget will animate the MovieCardWidget's height, using the _pageController's value.

In the presentation/journeys/home/movie_carousel, create a new file animated_movie_card_widget.dart:

https://gist.github.com/82453dd41fa5c044624448565080e422

  1. Create a stateless widget with 2 extra fields index and pageController, that will be used to calculate the height.
  2. Just call the MovieCardWidget from here.

We'll now wrap this with AnimatedBuilder and determine the value that will manipulate the height of cards in focus and that not in focus.

If you want a complete explanation, I have already explained similar stuff in AnimatedCarousel video.

Update the build() of AnimatedMovieCardWidget:

https://gist.github.com/28f071879bc9fd4f88aed47f9c14166b

  1. Wrap the MovieCardWidget with AnimatedBuilder.
  2. In the animation, use the pageController so that when pageController value changes, the AnimatedBuilder will re-draw the child with builder.
  3. value starts with 1 and when you scroll, the value changes to 0.9 over frames.
  4. If statement executes when you scroll. Else executes in the default state.
  5. For height, we use value to transform the height of the container.

This complete logic is very well explained in the video. Do check it out.

Now, in MoviePageView, instead of MovieCardWidget, use AnimatedMovieCardWidget:

https://gist.github.com/132a9ef0f3cf18a806836f8fcde10d19

This is self-explanatory. Now run the app, and you'll see the carousel cards animating.

MovieBackdropWidget

What do we want to achieve? This widget is behind the MovieCarouselWidget and shows the backdrop image of the movie in focus.
On scrolling the MoviePageView, you load the backdrop image of the movie in focus.

First, create a bloc because the image will change on the scroll.

In the presentation/blocs folder, create a new folder movie_backdrop .

In movie_backdrop_event.dart add MovieBackdropChangedEvent: https://gist.github.com/ffe26f55a11d2c36382b079d8ebafe4a

  1. This event will be dispatched when the page changes in MoviePageView. It takes the current movie.

In movie_backdrop_state.dart, create 2 states: https://gist.github.com/c157306bffa8ce81f0124856919befc0

  1. This will be the initial state because before any Page changes you cannot load any image.
  2. MovieBackdropChanged is simple again, as it just takes in the movie. In the UI, we'll fetch the backdropPath and title.

In movie_backdrop_bloc.dart, handle the single event:

https://gist.github.com/8bcc19b5fe3ff19b4873f178c761b632

  1. This is straight-forward, we're just yielding the state with the movie received from the event.

Register the MovieBackdropBloc in get_it.dart:

https://gist.github.com/4e7feb9dca06f81589bd7f84f4c18c7d

  1. Register the bloc as Factory.

Update home_screen.dart, to get instance of MovieBackdropBloc, and use MultiBlocProvider now, as we need two Blocs:

https://gist.github.com/376a886e6ecb33f23b9d943f9acced25

  1. Fetch the instance of MovieBackdropBloc from getIt.
  2. In dispose(), don't forget to close the bloc.
  3. MultiBlocProvider takes an array of BlocProvider, so add one more in the same fashion as movieCarouselBloc.

Now, In movie_page_view.dart, dispatch the MovieBackdropChangedEvent when page changes:

https://gist.github.com/c43f86ef4dad2eeb27fb7c1dfb000998

  1. Since, home_screen provided the bloc, it can be used in the descendants by using BlocProvider.of(context).
  2. You'll dispatch the event in with the movie in focus, with the help of index.

Let's add the UI now in MovieCarouselWidget:

https://gist.github.com/2369e0ed9460c9f33e15f7306e4c2bb7

  1. Declare the movieBackdropBloc as final and use it in the constructor.
  2. Dispatch the event with a movie at the defaultIndex, which is 0 at the start. There is a BlocBuilder in MovieBackdropWidget that will receive this event and load the image of the first movie.

In get_it.dart, update the registration of MovieCarouselBloc:

https://gist.github.com/ef48adf972c39845001b12c4864ab92d

  1. Use getItInstance() to provide us with the instance of MovieBackdropBloc in MovieCarouselBloc:

If you run, you'll still see that the backdrop is not loaded until the first-page change. This is happening because of instance resolution. The movieBackdropBloc instance in home_screen and that in movieCarouselBloc is different. They should be the same. There are 2 ways to make the same.

  • Use Singleton for MovieBackdropBloc
  • Use the movieBackdropBloc from movieCarouselBloc in HomeScreen

We're going with second approach. Open home_screen.dart and update initState():

https://gist.github.com/4ac138959d82b7a416183bb2b49354c8

  1. Do not take the instance from GetIt, instead take it from MovieCarouselBloc. This same instance will be used in the MovieCarouselBloc to dispatch the MovieBackdropChangedEvent of MovieBackdropBloc.

Now, run the app. You'll see the backdrop image load from initialization.

MovieDataWidget

To show the title of the movie of the current page in MoviePageView, we'll use the MovieBackdropBloc's state:

In the presentation/journeys/home/movie_carousel folder, create a new file movie_data_widget.dart:

https://gist.github.com/c51300d84426ec11c6cbb8a4f48a3f7f

  1. Use BlocBuilder for MovieBackdropBloc.
  2. If state is MovieBackdropChanged, then show the text with the movie title. To properly layout the title, you can restrict the number of lines.
  3. Use the overflow property, in case text doesn't fit in one line.
  4. Use headline6 font style, that we created in ThemeText. Always use from Theme.of(context), so that when you change theme to dark or any other theme, the app remains consistent.

Add the MovieDataWidget below MoviePageView in MovieCarouselWidget:

https://gist.github.com/209547a8ce9222f711f38065b343dcd3

Separator

In the next video/article, I'll show the tabbed view at the bottom. To make it separate out with Carousel, let's add a simple separator.

In the presentation/widgets folder, create a new file separator.dart:

class Separator extends StatelessWidget {

 @override
 Widget build(BuildContext context) {
   //1
   return Container(
     height: Sizes.dimen_1.h,
     width: Sizes.dimen_80.w,
     //2
     padding: EdgeInsets.only(
       top: Sizes.dimen_2.h,
       bottom: Sizes.dimen_6.h,
     ),
     //3
     decoration: BoxDecoration(
       borderRadius: BorderRadius.all(Radius.circular(Sizes.dimen_1.h)),
       gradient: LinearGradient(
         colors: [
           AppColor.violet,
           AppColor.royalBlue,
         ],
       ),
     ),
   );
 }
}
  1. A simple container with width and height. We could've used Divider, but divider doesn't have radius and uses indents to decide the width.
  2. Padding from top and bottom for separation.
  3. To make round edges, use BoxDecoration with BorderRadius. Give a simple gradient to the separator.

Run the app for the final time and play with it.

This was all about creating a carousel and top part in HomeScreen with the initial setup. See you in the next part of the series.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment