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.
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
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
- Create a class AppColor.
- Add a private constructor since it is not required to instantiate the class.
- 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.
Open main.dart and clear out the main()
:
https://gist.github.com/9fbe03bc2a5f9ac190c5b102e21c5121
- You don't need async now, as we're using
unawaited
. In the future, we'll need it when we do Hive initialization. - As per official Flutter documentation, this is the glue that binds the framework to the Flutter engine.
- 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. - Initialize GetIt to provide us with dependencies.
- 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).
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
- Here,
defaultWidth
anddefaultHeight
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 thedefaultWidth
anddefaultHeight
, you can do so by invokingScreenUtil.init(width: 414,height: 896,);
before returningMaterialApp
. - 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.
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
- We're using
Poppins
Font. - 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. - Create a public static method that returns the TextTheme. This will be used by
MaterialApp
, rest all methods can be private.
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
- Initialize ScreenUtil so that we can use it while defining
- Use
MaterialApp
widget with debugBanner as false or true. - Define the
ThemeData
of the app. Make Vulcan asprimary
as well asscaffoldBackground
color. All our screens have Vulcan as their background color. Also, give the text theme. - Our first screen - HomeScreen (we're yet to design this).
In this video/article, I'll show only the top part of HomeScreen as this also contains a basic starting setup.
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
- 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. defaultIndex
will give us the flexibility to decide which movie will be in the center of our carousel at the start.- A
const
constructor withdefaultIndex
as 0, if not passed. 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
MovieCarouselInitial
to be emitted as the first state when the bloc initializes.MovieCarouselError
to be emitted if there is an error thrown from API. I'll not observe this state in this video/article.MovieCarouselLoaded
to be emitted with a list of trending movies and default index, which is passed fromCarouselLoadEvent
earlier.
In movie_carousel_bloc.dart, declare GetTrending
usecase:
https://gist.github.com/f4922bc5b5a06bef59ce88c0c75ab24a
- GetTrending will be the final variable.
- Clearly,
MovieCarouselBloc
is dependent onGetTrending
, 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
- Handle if the event dispatched is
CarouselLoadEvent
- Call the
getTrending
usecase withNoParams()
- Use
fold
operator to handle the response - When error(left), yield error state (Future-proof), not handling this state in this video/article.
- 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.
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.
- Home Screen
- Logo Widget
- Movie App Bar Widget
- Movie Carousel Widget
- Movie Card Widget
- Movie Page View Widget
- Animated Movie Card Widget
- Movie Backdrop Widget
- Movie Data Widget
- Separator Widget
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
- Initialize the
MovieCarouselBloc
from GetIt. - When the home screen initializes, dispatch the only event for
MovieCarouselBloc
This will make an API call and yield theMovieCarouselLoaded
orMovieCarouselError
state. - 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
- When you use FractionallySizedBox, you should use
StackFit.expand
, because this allows stack to take the available space. - FractionallySizedBox that uses fractions to decide on the proportion of screen that it will take.
- The top part should have
topCenter
as its alignment. - 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
. - The bottom FractionallySizedBox, obviously will have
bottomCenter
as its alignment. - The
heightFactor
of this remaining part of the screen which will be0.4
. MovieTabbedWidget
will replace thePlaceholder
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.
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
Logo
is a stateless widget with a dynamic height that will be provided by the calling widget. This is important here because the sameLogo
widget will be used in theNavigationDrawer
, when we implement that.- 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.
- Just use the logo image from assets/pngs folder. Notice the usage of
.h
.
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
- 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. - Use
Row
to layout the elements in horizontal. - In Row, at start and end, add the 2
IconButtons
. One being SvgPicture and the other being taken from the Flutter framework itself. - The remaining space in between these 2 images, use the
Logo
widget.
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
- Use
BlocProvider
to provide theMovieCarouselBloc
instance down the tree. - You need not create the bloc here as it is already done in
initState()
. - Use
BlocBuilder
to read the current state ofMovieCarouselBloc
. Thebuilder
takes incontext
andstate
. - When loading of trending movies is a success, we show the previously used
Stack
having two sections - Top and Bottom. Give theMovieCarouselWidget
with instead of firstPlaceHolder
. - 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
MovieCarouselWidget
is a stateless widget, that works on a list ofmovies
and thedefaultIndex
.- 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. - A column with just 2 elements. First is the
MovieAppBar
that we created before. - Next in Column is
MoviePageView
, belowMovieAppBar
that we'll create now. This widget also takes in the list ofmovies
and thedefaultIndex
.
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
- You'll need
movieId
in the future when we tap on this card to move to the movie detail screen. - The
posterPath
is required to load the image. This will be taken fromMovieEntity
and will be in this format kqjL17yufvn9OVLyXYpvtyrFfak.jpg. - Use CachedNetworkImage with the imageUrl prepended with
BASE_IMAGE_URL
. In DataSources, I have explained the use ofBASE_IMAGE_URL
. - Use
ClipRRect
to clip the image, with aborderRadius
. This will add the curves on all the vertices of the images. - Use
GestureDetector
to enable tappable events on the card. - Use
Material
to give elevation to the card.
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
- Create a stateful widget with a list of
movies
andinitialPage
. TheinitialPage
is the same as thedefaultIndex
, so apply the same assertion to this as well. - In the
State
class of MoviePageView, declare aPageController
. - In
initState()
, instantiate _pageController
withviewportFraction
as 0.7.viewportFraction
decides how much screen share each item ofPageView
will take.
In the same file, create the UI.
https://gist.github.com/6f08a8c4ede2f0ecba3f84ca3d9ff8d8
- 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
. - In the
itemBuilder
, based on index return theMovieCardWidget
. Generally, we get 20 movies from the API, soitemBuilder
will create 20 cards. - When you're in between of a complete scroll transaction,
pageSnapping
true
makes it complete the scroll action. - You should mention the itemCount because
itemBuilder
will be called only with indices greater than or equal to zero and less thanitemCount
. In short, it's afor
loop withfor (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 return0
. - To update the backdrop image and title of the movie below
PageView
, we'll need to get the callback when thePageView
is scrolled. - Wrap the
PageView.Builder
with Container, so that we can give height and margin to it. - To maintain some space between MovieAppBar and the other details of the movie, we need the vertical margin.
- 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
inHomeScreen
,0.35
here makes total sense.
When you run, you'll see the PageView
horizontally scrollable with all the MovieCardWidget
s touching each other. We need to add animation while scrolling, also give some spacing between each individual MovieCardWidget
.
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
- Create a stateless widget with 2 extra fields
index
andpageController
, that will be used to calculate the height. - 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
- Wrap the
MovieCardWidget
withAnimatedBuilder
. - In the
animation
, use thepageController
so that whenpageController
value changes, the AnimatedBuilder will re-draw the child withbuilder
. value
starts with 1 and when you scroll, the value changes to 0.9 over frames.If
statement executes when you scroll.Else
executes in the default state.- For
height
, we usevalue
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.
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
- 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
- This will be the initial state because before any Page changes you cannot load any image.
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
- 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
- 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
- Fetch the instance of
MovieBackdropBloc
from getIt. - In
dispose()
, don't forget to close the bloc. MultiBlocProvider
takes an array of BlocProvider, so add one more in the same fashion asmovieCarouselBloc
.
Now, In movie_page_view.dart, dispatch the MovieBackdropChangedEvent
when page changes:
https://gist.github.com/c43f86ef4dad2eeb27fb7c1dfb000998
- Since, home_screen provided the bloc, it can be used in the descendants by using
BlocProvider.of(context)
. - 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
- Declare the
movieBackdropBloc
as final and use it in the constructor. - 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
- Use
getItInstance()
to provide us with the instance ofMovieBackdropBloc
inMovieCarouselBloc
:
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
frommovieCarouselBloc
inHomeScreen
We're going with second approach. Open home_screen.dart and update initState()
:
https://gist.github.com/4ac138959d82b7a416183bb2b49354c8
- Do not take the instance from GetIt, instead take it from
MovieCarouselBloc
. This same instance will be used in theMovieCarouselBloc
to dispatch theMovieBackdropChangedEvent
ofMovieBackdropBloc
.
Now, run the app. You'll see the backdrop image load from initialization.
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
- Use
BlocBuilder
forMovieBackdropBloc
. - If
state
isMovieBackdropChanged
, then show the text with the movie title. To properly layout the title, you can restrict the number of lines. - Use the
overflow
property, in case text doesn't fit in one line. - Use
headline6
font style, that we created inThemeText
. Always use fromTheme.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
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,
],
),
),
);
}
}
- 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. - Padding from top and bottom for separation.
- To make round edges, use
BoxDecoration
withBorderRadius
. 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.