Today we will take a look at probably the most complex app UI I shared with you until now.

Until now I never touched on CustomPainters, Path, BezierCurve and all of those things, but when I saw this design caught my eye, so here we go.

Before we begin you should get the source code from Github.

Are you ready? Let’s begin!

In order to make things a bit more realistic, I created these two variables that will host our Habits data.

List<Map<String, dynamic>> habits = [ { 'color': Colors.blue, 'title': 'YP', 'fulltext': 'Yoga Practice', }, { 'color': Colors.red, 'title': 'GE', 'fulltext': 'Get Up Early', }, { 'color': Colors.cyan, 'title': 'NS', 'fulltext': 'No Sugar', }, ]; List<Map<String, dynamic>> habits2 = [ { 'color': Color(0xff7524ff), 'objectif': 'Learn 5 new words', 'progress': '5 from 7 this week' }, { 'color': Color(0xfff03244), 'objectif': 'Get Up Early', 'progress': '5 from 7 this week' }, { 'color': Color(0xff00d5e2), 'objectif': 'Create an App a day', 'progress': '6 from 7 this week' }, ];

First, we gave the Scaffold a background-color:

backgroundColor: Color(0xff131b26),

The first child of the Scaffold is a Container, we set the Left and Top padding only to give the effect you see on the design:

padding: EdgeInsets.only(top: 25.0, left: 25.0),

Then we add our Column, something I hate with Columns is that the CrossAxisAlignment is set to Center by default. Here we set it to Start.

You know that I hate using Appbar, I prefer using a Row just like this:

Row( children: <Widget>[ Expanded( child: RichText( softWrap: true, text: TextSpan( children: [ TextSpan( text: "Most Popular", style: TextStyle( fontWeight: FontWeight.bold, color: Colors.white, fontSize: 25, ), ), TextSpan( text: " Habits", style: TextStyle( fontSize: 25, color: Colors.white, ), ), ], ), ), ), Material( color: Colors.transparent, child: InkWell( onTap: () {}, child: Container( padding: EdgeInsets.all(9.0), decoration: BoxDecoration( shape: BoxShape.circle, color: Color(0xff6f1bff), boxShadow: [ BoxShadow( color: Color(0xff6f1bff), offset: Offset(0, 3), blurRadius: 5.0), ], ), child: Icon( Icons.add, color: Colors.white, ), ), ), ), SizedBox( width: 25.0, ) ], ),

RichText allows us to give different parts of the Text a specific TextStyle. Here I wanted “Most Popular” to be bold, and the rest in normal weight.

Below that, we have a ListView.builder with a scrollDirection set to Axis.horizontal. When you use a ListView this way you always need to constrain its height.

Container( width: 150, margin: EdgeInsets.only(right: 15.0, top: 9.0, bottom: 9.0), padding: EdgeInsets.all(13.0), decoration: BoxDecoration( color: habits[id]['color'], borderRadius: BorderRadius.circular(15), boxShadow: [ BoxShadow( color: habits[id]['color'], blurRadius: 5.0, offset: Offset(0, 3), ), ], ), child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Text( habits[id]['title'], style: TextStyle( fontWeight: FontWeight.bold, fontSize: 27, color: Colors.white), ), Text( habits[id]['fulltext'], style: TextStyle( fontWeight: FontWeight.bold, color: Colors.white, fontSize: 15, ), ), ], ), );

Those little boxes are Containers, we get there Color, Title, FullText and BoxShadow color from our Habits List.

This part was pretty easy to design but getting the values were pretty challenging. I wanted to make this a bit realistic instead of just using hard-coded Text.

The first thing you need to know is that we are using a ListView.builder, when you use this Widget you can set the children count, here it’s set to 7. The Item Builder gives us the ID of each Item.

itemBuilder: (ctx, f) { int day = DateTime.now().day + f; return FittedBox( child: Container( width: 90, height: 90, margin: EdgeInsets.only(right: 15.0), alignment: Alignment.center, decoration: BoxDecoration( color: day == DateTime.now().day ? Color(0xff727be8) : Color(0xff131b26), borderRadius: BorderRadius.circular(5.0), ), padding: EdgeInsets.all(15.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text( "${DateTime.now().day + f}", style: TextStyle( fontSize: 25, fontWeight: day == DateTime.now().day ? FontWeight.bold : FontWeight.normal, color: day == DateTime.now().day ? Colors.white : Colors.grey[500], ), ), Text( DateFormat('EE').format( DateTime.now().add( Duration(days: f), ), ), style: TextStyle( color: day == DateTime.now().day ? Colors.white : Colors.grey[700], fontWeight: day == DateTime.now().day ? FontWeight.bold : FontWeight.normal), ) ], ), ), );

Inside our ItemBuilder, we keep the current value of the Day as DateTime.now().day + f; in the beginning f = 0, so it starts from today until the end of the list.

Now to get the day “name” we use a flutter package called: Intl.

In your pubspec.yaml just add this under dependencies: intl:

This out of the way we can use it like this:

DateFormat('EE').format( DateTime.now().add( Duration(days: f), ), ),

DateFormat(‘EE’) gives us the day name in a String. DateTime.now().add allows us to get the next days of the list as you know f = 0 in the beginning.

Now that my ego is broken, and I realize that I can’t explain these kinds of things I will just go back to the UI.

The first-day container has a green color, and its Text is white and bold, we achieve this by checking for if “f” == 0, if it is then we give these parametres to the widgets.

decoration: BoxDecoration( color: day == DateTime.now().day ? Color(0xff727be8) : Color(0xff131b26), borderRadius: BorderRadius.circular(5.0), ),

Text( "${DateTime.now().day + f}", style: TextStyle( fontSize: 25, fontWeight: day == DateTime.now().day ? FontWeight.bold : FontWeight.normal, color: day == DateTime.now().day ? Colors.white : Colors.grey[500], ), ), Text( DateFormat('EE').format( DateTime.now().add( Duration(days: f), ), ), style: TextStyle( color: day == DateTime.now().day ? Colors.white : Colors.grey[700], fontWeight: day == DateTime.now().day ? FontWeight.bold : FontWeight.normal), )

For the last part, we have some pretty easy stuff.

RichText( text: TextSpan( children: [ TextSpan( text: "Your Habits ", style: TextStyle( fontWeight: FontWeight.bold, fontSize: 21, ), ), TextSpan( text: " 5", style: TextStyle( color: Colors.grey[500], fontSize: 21, ), ), ], ), ),

Again we use a RichText in order to differentiate between the static text and the value we get from our database (here both are static but that wouldn’t be the case in a real-world app).

Just under it, we have our Expanded ListView.builder that will hold all of our habits.

Expanded( child: ListView.builder( itemCount: habits2.length, itemBuilder: (ctx, id) { return ListItem(id: id); }, ), )

class ListItem extends StatelessWidget { final int id; const ListItem({Key key, this.id}) : super(key: key); @override Widget build(BuildContext context) { return InkWell( onTap: () { Navigator.pushNamed(context, '/details'); }, child: Container( // height: 150, margin: EdgeInsets.symmetric(vertical: 21.0), padding: EdgeInsets.only(right: 25.0), child: Column( children: <Widget>[ Row( children: <Widget>[ Container( padding: EdgeInsets.all(5.0), decoration: BoxDecoration( shape: BoxShape.circle, color: id == 0 ? habits2[id]['color'] : Colors.transparent, border: id == 0 ? Border() : Border.all( color: Colors.grey[500], ), ), child: Icon( Icons.check, color: id == 0 ? Colors.white : Colors.grey[500], ), ), SizedBox(width: 15), Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Text( habits2[id]['objectif'], style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 19), ), SizedBox( height: 3, ), Text( habits2[id]['progress'], style: TextStyle(color: Colors.grey[500], fontSize: 17), ), SizedBox( height: 15, ), ], ) ], ), LinearProgressIndicator( value: .71, backgroundColor: Color(0xff1c232d), valueColor: AlwaysStoppedAnimation( habits2[id]['color'], ), ), ], ), ), ); }

If we click on any single habit it should take us to the Details section so we need to use an InkWell to handle the onTap event.

The Check icon is inside a Container with a shape set to circle. If it’s the first habit then the container’s background-color will be set to purple and the icon to white, otherwise, both are in grey.

I have set the value of the LinearProgressIndicator to a static value of 71%, the backgroundColor to a Greyish color, and the valueColor to the color of that specific habit, and we get it from our list of habits.

That’s it we are done!

To be honest writing this was more difficult than writing the code, I am sorry if I didn’t explain certain things, however, if you have any questions please feel free to DM me on twitter.

In the next part, we will see how I was able to create that BezierCurved path in the details UI. So stay tuned!

Till then I wish you all the best, see you next time.