Notice:
This post is older than 5 years – the content might be outdated.
Previously we have created the basic structure of the Flutter application by defining the GeneralPage and the DetailsPage. In this article, I will concentrate on routing, layout, animation and data sharing.
The Flutter Profile App
The basic structure of the profile app is defined by the Scaffold widget. In order to build the UI, we create custom widgets as we refactor the application.
Scaffold Sections
- appBar: newAppBar(…)
- body: new Container(…)
- bottomNavigationBar: new BottomAppBar(…)
When creating custom widgets, we have to think about the UI. A responsive UI can be achieved with the MediaQuery.of(…) function. This function provides the device dimensions and can therefore be used to adjust all widgets in the widget tree.
Setting up the Pages
Create init function to get device dimensions
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// GeneralPage.dart and DetailsPage.dart double calc_height = 0.0; double calc_width = 0.0; int counter = 0; _init(){ if(counter == 0) { this.calc_height = MediaQuery.of(context).size.height; this.calc_width = MediaQuery.of(context).size.width; } counter++; } |
The counter will trigger the dimension calculation only once. This will prevent MediaQuery.of(…) being triggered with every state change. The next step will be to define the custom widgets that have to be built.
Widgets to create for GeneralPage.dart and DetailsPage.dart
- AppBar
- Grid and Stack
- BottomNavigationBar
Create the Widgets
Let’s create the AppBar and BottomNavigationBar first, since they are not as complex as the Stack or Grid. I prefer creating a new widget and passing it to the Scaffold attribute. This will make refactoring at a later stage much easier.
Get Widget Title
1 |
String title = widget.title; |
In order to retrieve the widget title, we have to call the parent widget in the same context.
AppBar
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// Scaffold appBar: _getAppBar(), ... _getAppBar(){ return new AppBar( title: new Text(widget.title, style: TextStyle(color: Colors.blueGrey)), backgroundColor: Colors.white, ); } |
As long as the types match, we can use the same concept to create a custom BottomNavigationBar.
BottomNavigationBar (custom)
1 2 3 4 5 6 7 8 9 10 11 |
// Scaffold bottomNavigationBar: _getBottomNavBar(), ... _getBottomNavBar(){ return new CustomBottomNavBar(); } |
The CustomBottomNavBar has a few minor functions and is therefore moved to a separate Dart file. I have included a simple switch-case to alternate the naming of the buttons according to their page (General / Details). At this point in development, you will notice something about the application’s architecture. We are able to drill down all pages to a widget within widgets containing widgets. It becomes crucial to have a clear structure in your application. The structure helps you to keep your code organized and traceable. The Flutter Development Team recommends using patterns for that case as your app grows (BLoC).
The CustomBottomNavBar is a widget with two capsulated tasks. First, the correct widget according to their parent page is returned. Second, an event from a generic button is triggered. There is only one BottomNavBar template implemented. The only difference is the naming of the button and the connected events.
Set correct naming of buttons according to parent page
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
// CustomBottomAppBar.dart ... _switchNamesByPage(){ if(widgetName == "general"){ menu_left = "menu"; menu_right = "donate"; menu_center = "contact"; } else if(widgetName == "detail"){ menu_left = "menu"; menu_center = "back"; menu_right = "contact"; } } |
Set event trigger according to button name / tag
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
// CustomBottomAppBar.dart ... _triggerEventByName(String tag){ if(tag == "back"){ Navigator.pop(context); } else if(tag == "menu"){ ... } else if(tag == "contact"){ ... } else if(tag == "donate"){ ... } } } |
The complete CustomBottomNavBar can be seen as a collection of low level widgets. You can now connect all widgets and call the CustomBottomNavBar everywhere in your application.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
class CustomBottomBar extends StatelessWidget { CustomBottomBar(this.widgetName); @override Widget build(BuildContext context) { _switchNamesByPage(); return SafeArea( child: BottomAppBar( child: Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ _getMenu(context, menu_left), _getMenu(context, menu_center), _getMenu(context, menu_right), ], ), ), ); } _getMenu(BuildContext context, String tag){ return Container( child: FlatButton( child: Text(tag,style: TextStyle(color: Colors.blue)), onPressed: () { _triggerEventByName(tag); } ), ); } } |
Bottom and Top widgets are done and only the main content per page remains. The main content is placed in the body attribute of the Scaffold widget. Each page has a different UI. The GeneralPage should contain something like a grid. The DetailsPage should be structured with a stack widget, because we need to build layers of widgets.
Grid
A grid is made out of slivers and it is not scrollable by default. To make it scrollable requires a wrapper around the SliverGrid within a CustomScrollView widget. Before we implement the grid, I suggest you prepare a list of data that we can display in our grid. We have prepared the application structure, included assets in the previous article and can use them for this matter.
Create display data
- Create a model
- Create display data
- Add display data models to a list
- List <Model> list = [model_00, model_01, …]
Please refer to the Github repository and find the Model.dart and Provider.dart file. These files can help you to implement a data set, which we use to populate the grid widgets.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
// GeneralPage.dart - Scaffold body: _getGeneralGrid(), ... _getGeneralGrid(){ return CustomScrollView( slivers: <Widget>[ SliverGrid( gridDelegate: _getGridDelegate(), delegate: _getSliverDelegate(), ) ], ); } |
The gridDelegate attribute defines the grid attributes, such as the number of axis and the space in between the axis.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
_getGridDelegate(){ return SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, mainAxisSpacing: 2.0, crossAxisSpacing: 2.0, ); } |
The delegate attribute defines the individual sliver inside the grid. This sliver can be as complex as you want as long as it does not violate the layout of the parent widget.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
_getSliverDelegate(){ return SliverChildBuilderDelegate((BuildContext context, int index){ // define a custom sliver with display data return GeneralCell(list[index]); }, childCount: list.length ); } |
As you may notice, we are going down the widget tree wrapping widgets inside of widgets. The GeneralCell widget is a custom-built widget. It is used as a container for the content of each grid sliver.
GenreralCell
In this project, I decided to place the routing function inside the GeneralCell widget. Basically, the widget is a plain StatefulWidget and provides a state where the device dimensions are calculated. Furthermore, it receives the model for each sliver from the GeneralPage widget.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
class GeneralCell extends StatefulWidget { Model cellItem; GeneralCell(this.cellItem); @override _GeneralCellState createState() => new _GeneralCellState(); } class _GeneralCellState extends State<GeneralCell> { double calc_width = 0.0; double calc_height = 0.0; @override Widget build(BuildContext context) { this.calc_height = MediaQuery.of(context).size.height; this.calc_width = MediaQuery.of(context).size.width; return Container( alignment: Alignment.center, child: Column( children: <Widget>[ GestureDetector( child: _getImage(), onTap: _goTo, ), _getText(), ], ), );// card } } |
From here on matters become repetitive and we have to define our UI again. The structure has been laid out in the previous article. The code fragment below shows the two required sections:
- Image
- Text
Each section stands for a new widget that holds some kind of content. In addition, the whole sliver is wrapped with a GestureDetector widget. This widget provides functions to detect gestures like onTap, onDoubleTap, etc. The next steps are to define how we navigate to another page and where we have to place the animation logic for this. We need three functions, as shown in the code fragment above, to get the required widgets and to manage the routing.
TEXT: according to the generated model item
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
// GeneralCell.dart ... _getText(){ return Container( child: Center( child: Text(widget.cellItem.name, style: TextStyle(color: Colors.blueGrey) ) ) ); } |
IMAGE: according to the generated model item, with Hero animation
I removed the implementation for padding, decoration and borders in order to keep the code readable. Please refer to the Github repository and find the GeneralCell.dart for the complete code example.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
// GeneralCell.dart ... _getImage(){ return Padding( padding: ..., child: Container( child: Hero( tag: widget.cellItem.pathToImage, child: CircleAvatar( backgroundImage: new AssetImage(widget.cellItem.pathToImage), backgroundColor: Colors.black, radius: calc_height * 0.06, ), ), ), ); } |
Please notice that the Hero widget, which wraps only the CircleAvatar widget (image), has an identifier tag. This widget manages the animation for the page change. We have to ensure that the identifiers match in type and tag.
The GeneralCell widget contains the routing logic as well. We attach the model that we tap and send it to the DetailsWidget. To simply send the data we use the MaterialPageRoute builder.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// GeneralCell.dart ... _goTo(){ // slow down animation timeDilation = 2.0; Navigator.push(context, MaterialPageRoute( builder: (context) => DetailsPage(widget.cellItem) ) ); } |
Fifty percent of the application is completed and only the UI of the DetailsPage widget remains. This I will leave to you since it will be the same process as with the GeneralPage all over again. I will continue with the initial setup of the Stack widget, the Hero animation, and the extraction of the model.
Stack
A stack is a common principle in development. It can be seen as layers on top of each other. The structure of the DetailsPage is shown below and is organized into different sections:
- Header
- SubHeader
- Body
- Image
All widgets, except the image, are collected in a Column widget, which we return to the Scaffold widget’s body.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
// DetailsPage.dart ... _getStack(){ return Stack( children: <Widget>[ Column( children: <Widget>[ _getHeader(), _getSubHeader(), _getBody(), ], ), _getImage(), ], ); } |
You can organize these sections as you like and create layer by layer. Here, I will skip defining those layers except for the image layer.
The image layer holds the tag that is identified during the routing process from GeneralPage → GeneralCell → DetailsPage with a Hero → Hero animation.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
// DetailsPage.dart ... _getImage(){ return Container( child: Hero( tag: widget.model_item.pathToImage, child: CircleAvatar( backgroundImage: new AssetImage(widget.model_item.pathToImage), backgroundColor: Colors.black, radius: calc_width*0.18, ), ), ), } |
The Hero tag must contain the same identifier in both widgets. Otherwise, we cannot determine which widget we should animate. As mentioned above, you can continue implementing new custom widgets to fill the DetailsPage. When you’re ready, try to run the application and test the Hero animation.
And thus, the simple Profil App is completed.
We have generated two pages with a nice animation. Feel free to adopt and extend this structure in your projects!
Conclusion
Building a simple two page application is not so hard, but even here you can see that structure is important. Widgets should be handled in a pattern (BLoC or ScopeModel) to keep the project organized. The application architecture can be deeply nested and hard to follow. Make sure you document all interfaces and define Keys for each widget. In this project, I removed all Keys, but keep in mind that they are crucial for testing.
If you are interested in more detailed concepts and processes, you can visit my other articles as mentioned below.
The complete example can be found on GitHub. Please be aware that I adjusted the code to make it more readable. Therefore, I refactored the code and removed imports from the article.
All articles in this series
- Flutter: The Beginning of New Era? (Part 1)
- Flutter: New Concepts? (Part 2)
- Flutter: The Profiler (Part 3)
Read on
Find out more about our Android and iOS portfolio or join us as a mobile developer! You can read more about Flutter development in particular on my Flutter Medium Blog.
One thought on “Flutter: The Finalizer (Part 4)”