Today we are building a crypto dashboard app, it may sound like it’s not the right time for it because of what happened to Bitcoin and other cryptos but I believe that cryptos are going to make a comeback pretty soon for a short period of time 😉

This tutorial will be special because I am writing it as I code, this allows me to share more information and share the code step by step. And hopefully, if you follow along you should get the same result.

Final source code is here: https://github.com/cybdom/crypto_app

Original design: https://dribbble.com/shots/7622608-Transaction-ETH-App

Project preparation:

When I build these apps that only require the UI and no special state management I start by the usual flutter create command:

flutter create crypto_app

Then I head over to Github where I create a repo for the app and set it up locally by running these commands inside my project folder:

git init git remote add origin "my_repo_url"

After that, I use this small Batch file that I created to simplify the process of setting up new projects.

These are the folders I always use:

ui ui\screens ui\widgets

As well as these files:

(The default main.dart) global.dart ui/screens.dart (Used to export all the screens as one file) ui/screens/home.dart (Another file usually details.dart) ui/widgets/widgets.dart (The other widgets go here too)

And this is the code for start.bat

mkdir ui cd ui mkdir widgets mkdir screens cd .. type NUL > global.dart type NUL > ui/screens/screens.dart type NUL > ui/widgets/widgets.dart

Now that we have all our files we can set up our fonts. I fell in love with QuickSand so that’s what I always use on my apps (unless the client requires something else).

Create a folder named fonts or assets/fonts inside your folder directory and save your fonts file there. Then inside your pubspec.yaml add these lines:

fonts: - family: Quicksand fonts: - asset: fonts/Quicksand-Regular.ttf - asset: fonts/Quicksand-Bold.ttf weight: 700 - asset: fonts/Quicksand-Light.ttf weight: 100

Make sure to change the names and folder location according to your own specifications.

Once you’ve done that head to your main.dart file and add the set the fontFamily value to your own custom font.

class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, title: 'Flutter Demo', theme: ThemeData( fontFamily: 'QuickSand', primarySwatch: Colors.blue, ), home: HomeScreen(), ); } }

Home Screen:

To begin I like to set my Scaffold backgroundColor to white like this:

return Scaffold( backgroundColor: Colors.white, );

Then, we build our app bar, this time using the default Flutter AppBar widget is enough:

appBar: AppBar( backgroundColor: Colors.white, iconTheme: IconThemeData(color: Colors.black87), elevation: 0, leading: IconButton( icon: Icon(Icons.menu), onPressed: () {}, ), actions: <Widget>[ IconButton( icon: Icon(Icons.notifications_none), onPressed: () {}, ) ], ),

When we get to our Scaffold body it is usually a good idea to start with a SingleChildScrollView, this allows you to avoid content overflowing your vertical axis.

As most apps children are vertically layed out we usually add a Column as a child to the SingleChildScrollView

body: SingleChildScrollView( padding: EdgeInsets.all(15.0), child: Column( children: <Widget>[ ], ), ),

Our hello text, as well as the username text, are pretty large so instead of setting manually their size we can use the TextTheme of our MaterialApp as follows.

Text( "Hello,", style: Theme.of(context) .textTheme .display1 .apply(color: Colors.grey[500]), ), Text( "Mr. $username", style: Theme.of(context) .textTheme .display1 .apply(color: darkBlue, fontWeightDelta: 2),

You can see that I have a $username variable and a darkBlue color, and this is where my global.dart file comes to rescue. I use it to store the global variables, however, in a real-world app, the username would be retrieved using an API Call or something like that.

This is what we have so far.

The next part is the main container in this screen, it holds all of the important information you may need about your crypto account.

Here we start with a basic container, we give it the same darkBlue as well as a borderRadius and padding.

Container( padding: EdgeInsets.all(25.0), decoration: BoxDecoration( color: darkBlue, borderRadius: BorderRadius.circular(15.0), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[] ) ),

As you can see the child of the Container is a column and it will allow us to align the next widgets vertically.

We start with the “Account Balance” text widget, then a TextArea with the balance amount and the currency in a smaller font. After that, we have a Row with a Lock Icon and some text. Below that, we have another Row with 3 flexible RaisedButtons, two of which are “transparent” (here I gave them the same color as the main Container color which is darkBlue) and another button which has a nice blue color.

Container( padding: EdgeInsets.all(25.0), decoration: BoxDecoration( color: darkBlue, borderRadius: BorderRadius.circular(15.0), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Text( "Account Balance", style: TextStyle( fontSize: 19, fontWeight: FontWeight.bold, color: Colors.white, ), ), SizedBox( height: 11.0, ), RichText( text: TextSpan( children: [ TextSpan( text: "291.01", style: Theme.of(context) .textTheme .display1 .apply(color: Colors.white, fontWeightDelta: 2), ), TextSpan(text: " ETH") ], ), ), Row( children: <Widget>[ Icon(Icons.lock, color: Colors.grey[300]), SizedBox(width: 5.0), Text( "Freezing amount: 1.0173 ETH", style: TextStyle(color: Colors.grey[300]), ) ], ), SizedBox( height: 11.0, ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ Flexible( child: RaisedButton( padding: EdgeInsets.symmetric( horizontal: 15.0, vertical: 11.0), color: darkBlue, onPressed: () {}, child: Text( 'Deposit', style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold), ), shape: RoundedRectangleBorder( borderRadius: new BorderRadius.circular(9.0), side: BorderSide(color: Colors.white)), ), ), Flexible( child: RaisedButton( padding: EdgeInsets.symmetric( horizontal: 15.0, vertical: 11.0), color: darkBlue, onPressed: () {}, child: Text( 'Cash', style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold), ), shape: RoundedRectangleBorder( borderRadius: new BorderRadius.circular(9.0), side: BorderSide(color: Colors.white)), ), ), Flexible( child: RaisedButton( padding: EdgeInsets.symmetric( horizontal: 15.0, vertical: 11.0), color: Color(0xff1b4dff), onPressed: () {}, child: Text( 'Deposit', style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold), ), shape: RoundedRectangleBorder( borderRadius: new BorderRadius.circular(9.0), ), ), ), ], ) ], ), )

Now let’s build this list of I don’t know how to call it … List of History Containers? Let’s go with this name.

Again when dealing with horizontal lists always put them inside a limitedHeight widget such as a Container for which you set the height.

Container( margin: EdgeInsets.symmetric(vertical: 15.0), height: MediaQuery.of(context).size.height / 4, child: ListView.builder( scrollDirection: Axis.horizontal, itemCount: historyContainerList.length, itemBuilder: (ctx, i) { return HistoryContainer(id: i); }, ), )

In my global.dart I created this list of map:

List<Map<String, dynamic>> historyContainerList = [ { 'title': 'Launch a transaction', 'subtitle': '3811 Launches', 'actionType': actions.add, 'usersImg': [ 'https://cdn.pixabay.com/photo/2016/11/21/14/53/adult-1845814_960_720.jpg', 'https://cdn.pixabay.com/photo/2015/01/08/18/29/entrepreneur-593358_960_720.jpg', 'https://cdn.pixabay.com/photo/2015/01/08/18/30/entrepreneur-593371_960_720.jpg', ], }, { 'title': 'Launch a transaction', 'subtitle': '3811 Launches', 'actionType': actions.receive, 'usersImg': [ 'https://cdn.pixabay.com/photo/2015/09/18/11/46/man-945482_960_720.jpg', 'https://cdn.pixabay.com/photo/2014/07/31/23/49/guitarist-407212_960_720.jpg', 'https://cdn.pixabay.com/photo/2016/09/24/03/20/passion-1690965_960_720.jpg', ], } ];

In our widget director, I created a file named historycontainer.dart and added it to my widgets.dart export.

class HistoryContainer extends StatelessWidget { final int id; const HistoryContainer({Key key, @required this.id}) : super(key: key); @override Widget build(BuildContext context) { IconData _icon; if (historyContainerList[id]['actionType'] == actions.add) _icon = Icons.add; else if (historyContainerList[id]['actionType'] == actions.receive) _icon = Icons.mail_outline; return Container( margin: EdgeInsets.symmetric(horizontal: 15.0, vertical: 15.0), width: MediaQuery.of(context).size.width / 1.5, padding: EdgeInsets.all(15.0), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(15.0), boxShadow: [ BoxShadow(color: Colors.grey[200], blurRadius: 14), ], ), child: Column( // mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Spacer(), Row( children: <Widget>[ Container( decoration: BoxDecoration( color: lightBlue, borderRadius: BorderRadius.circular(5.0), ), child: IconButton( icon: Icon( _icon, color: Colors.white, ), onPressed: () {}, ), ), Spacer(), ...List.generate( historyContainerList[id]['usersImg'].length, (f) { return CircleAvatar( backgroundImage: NetworkImage(historyContainerList[id]['usersImg'][f]), ); }, ) ], ), Spacer(), Text( "${historyContainerList[id]['title']}", style: Theme.of(context) .textTheme .title .apply(color: darkBlue, fontWeightDelta: 2), ), Text( "${historyContainerList[id]['subtitle']}", style: Theme.of(context).textTheme.subhead, ), Spacer(), ], ), ); } }

The container takes an Id as a final argument which allows us to access the appropriate data from our list of map.

We start by checking which type of action we need (the design shows an Add Icon and Mail Icon so I use an if statement to know which one to show).

Inside a Row, I use a list.generate to build CircleAvatar widgets for each of the userImg we have on the global list.

This part is pretty simple so I will go over it quickly. Always on the same Column, we add a Row, the first child is a Text Widget with a Title theme, add a spacer then we can add our clock icon and the “Nearly 3 days” text widget.

Add a divider, below we have another row, but it’s children are both columns. On the left, we have a Title text “BlockChain Analysis Report” below is the creation date and the originator name”. On the right side, we have the transaction amount in green and a light blue button to view the transaction details.

Row( children: <Widget>[ Expanded( child: Text( "Tender Transaction", style: Theme.of(context) .textTheme .title .apply(color: darkBlue, fontWeightDelta: 2), ), ), Icon(Icons.timelapse, color: Colors.black.withOpacity(.71)), Text( "Nearly 3 days", style: TextStyle(color: Colors.black.withOpacity(.71)), ), ], ), Divider( height: 30, ), Row( children: <Widget>[ Flexible( flex: 3, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Text( "BlockChain Analysis Report", style: Theme.of(context) .textTheme .title .apply(color: darkBlue, fontWeightDelta: 2), ), Text( "Created 20.10.2019", style: TextStyle(color: Colors.black.withOpacity(.71)), ), Text( "Originator: Cybdom Tech", style: TextStyle(color: Colors.black.withOpacity(.71)), ), ], ), ), Flexible( child: Column( children: <Widget>[ Text( "17.00 ETH", style: Theme.of(context).textTheme.title.apply( color: Color(0xff17dcb0), fontWeightDelta: 2), textAlign: TextAlign.center, ), RaisedButton( color: lightBlue, child: Text("View", style: TextStyle(color: Colors.white),), onPressed: () { Navigator.pushNamed(context, 'transaction'); }, ) ], ), ) ], )

To be continued!

This is the end of the first part of this tutorial, we finished building our home screen, in the next episode we will build the transaction details screen.

The full source code is downloadable for free here: https://github.com/cybdom/crypto_app

Make sure to follow me on Twitter: https://twitter.com/cybdom to be notified when the second part of the tutorial will be released.

Creating these tutorials and coding these UI’s take time and thus money. Maintaining this website has also a cost, so if you want to help me cover these costs please donate any amount at https://www.buymeacoffee.com/bi3cp0Zk5

Thank you! See you in the next episode.