Timeline tiles are an awesome way to showcase events or milestones in your app. While there are many timeline tile packages available such as timeline_tile package that we previously covered, creating custom timeline tile widgets can give your app a unique and personalized touch.
In this post, we’ll provide you with a step-by-step guide on building an advanced Flutter timeline tile example for a social app activity timeline. We will cover advanced techniques for designing and styling your timeline tiles that will the app a professional look.
By the end of this post, you will have the knowledge and skills to create beautiful and functional Flutter reusable widgets that we’ll use to build the custom timeline tile widget.
Table of Contents
- Flutter Timeline Tile Example Overview
- Implementing the Custom Flutter Timeline Tile Example
- Conclusion
Flutter Timeline Tile Example Overview
Timeline tile can be an effective choice to display the activity of a social app, and can provide a quick overview about the latest activities and interactions. In this example, we’ll create a custom timeline widgets that’s personalized to showcase the latest activities in an app. We’ll build separate reusable widgets that will be composed together to form a very beautiful timeline with a professional look.

If you analyze the above UI, you’d notice that there are some repeatable components which have the exact same styling. If you try to break the UI into separate parts based on the activities displayed, you’d have the following:
- first tile
- new follower tile
- service review tile
- hotel review tile
Now of course we’re not going to build a single tile for each activity as we’ll be building flutter reusable widgets. Therefore, we’ll only build two types of tiles which are the first tile and the activity tile.
The first tile is a simple round-shaped tile with an icon only whereas the activity tile consists of different parts which includes:
- profile picture
- after indicator
- action taken (follow, like)
- right content
Now the right content will depends on the activity so we’ll have a 3 types of widgets to fill this part
- New follower widget
- service review widget
- hotel review widget
Always remember that it’s important to analyze the UI and identify the components it consists of to construct a well-defined structure with minimum errors. Practicing this helps you write better code in a faster pace and consistent way.
Implementing the Custom Flutter Timeline Tile Example
This is a really great example to practice creating custom widgets and adding your own touch to your apps. Using existing packages and plugins is absolutely useful but at some point, you’ll want to create your own widgets that have special characteristics which you decide.
Constants
I always like to start my project with declaring constants that I’ll be using within the project in a separate file to keep consistency within the app. These constants could be strings, colors, sizes, etc. For the timeline tile example, I’ll declare a constants file ‘constants.dart
‘ that will contain strings of images paths and statements, as well as size variables.
const String follow = 'followed you.'; const String likedReview = 'Liked your review.'; const String avatar1 = 'assets/avatar1.jpg'; const String avatar2 = 'assets/avatar2.jpg'; const String avatar3 = 'assets/avatar3.png'; const String avatar4 = 'assets/avatar4.jpg'; const String hotel = 'assets/hotel.jpg'; const double followerTileLength = 30; const double serviceTileLength = 220; const double hotelTileLength = 162;
First Tile
The first tile is quite simple as it only consists of two parts, a circle with an icon, and the after indicator. To make this customizable and reusable, we’ll create variables for the circle color, the icon, and the indicator and add them to the widget constructor. Doing this allows us to specify the values for these components at the time of initialization so we can customize them based on their position. The indicator is another separate widget which we’ll create in the next step.
class FirstTile extends StatelessWidget { final Color color; final Indicator afterIndicator; final Icon icon; const FirstTile({Key? key, required this.color, required this.afterIndicator, required this.icon}) : super(key: key); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(left: 20, top: 10), child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ Container( width: 50, height: 50, decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(25) ), child: icon, ), const SizedBox(height: 2,), afterIndicator, ], ), ); } }
Indicator Widget
This widget represents the vertical line displayed after the timeline tile. It’s a simple line with a line style property which allows us to change the length and color of the indicator. LineStyle is an internal data class that has the variables for the indicator’s length and color which will be specified at the time of the indicator’s initialization.
class Indicator extends StatelessWidget { final LineStyle lineStyle; const Indicator({Key? key, required this.lineStyle}) : super(key: key); @override Widget build(BuildContext context) { return Container( margin: const EdgeInsets.only(top: 3, bottom: 3), width: 1.5, height: lineStyle.height, color: lineStyle.color, ); } } class LineStyle { final Color color; final double height; LineStyle({required this.color,required this.height}); }
Activity tile widget
The activity tile widget consists of three parts which are the image, the right content, and the after indicator. So, in the constructor of the activity tile, we’ll pass the image path, right widget, and the indicator.
class TimelineTile extends StatelessWidget { final Indicator afterIndicator; final Widget rightContent; final String imagePath; const TimelineTile({Key? key, required this.afterIndicator, required this.rightContent, required this.imagePath}) : super(key: key); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(left: 15), child: Row( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ Column( children: [ Container( width: 60, height: 50, decoration: BoxDecoration( color: Colors.black, borderRadius: BorderRadius.circular(20), image: DecorationImage( fit: BoxFit.fitWidth, image: AssetImage( imagePath, ) ) ), ), afterIndicator, ], ), rightContent, ], ), ); } }
Action Taken Widget
This is the part that specifies the activity action in the timeline. For example, Rick followed you or Sarah liked your review. It’s a simple text widget that has a time, a name, and an action. These values will also be specified at the time of initialization. I’ve specified the actions in the constants file to keep consistency and standardize the way actions are presented in the timeline. This widget will later be passed to the widgets which will be displayed as the right content (new follower, service review, and hotel review).
class ActionWidget extends StatelessWidget { final String time; final String action; final String name; const ActionWidget({Key? key, required this.time, required this.name, required this.action}) : super(key: key); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(left: 10.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 10), Text(time, style: const TextStyle(color: Colors.grey, fontSize: 16.0),), const SizedBox(height: 5,), Row( children: [ Text(name, style: const TextStyle(color: Colors.black, fontSize: 18.0),), const SizedBox(width: 4), Text(action, style: const TextStyle(color: Colors.grey, fontSize: 16.0),) ], ) ], ), ); } }
New follower widget
The new follower widget consists of two parts, the action widget and the follow back icon. The action widget will be added to the constructor so we can specify its details when we initialize the new follower widget.
class NewFollower extends StatelessWidget { final ActionWidget action; const NewFollower({Key? key, required this.action}) : super(key: key); @override Widget build(BuildContext context) { return Expanded( child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(20) ), child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ action, Container( margin: const EdgeInsets.only(right: 20), width: 40, height: 40, decoration: BoxDecoration( borderRadius: BorderRadius.circular(25), gradient: const LinearGradient( tileMode: TileMode.clamp, begin: Alignment.topLeft, colors: <Color> [ Colors.purple, Colors.pink, ] ) ), child: const Icon( Icons.add, color: Colors.white, ), ), ], ), const SizedBox(height: 15), const Divider( thickness: 1, endIndent: 30, ) ], ), ), ); } }
Service review widget
The service review widget is more complex than the previous ones we created but we are Flutter heroes and we can do it, right? I’m sure you are
The first is the action part which we already implemented and we only need to pass at this point. The second part is the white container which consists of the review details. This container has a column layout which in turn has two rows. (still confused? just follow the code structure and you’ll get it. Oh, and don’t worry source code will be provided at the end of the post)
class ServiceReview extends StatelessWidget { final ActionWidget action; const ServiceReview({Key? key, required this.action}) : super(key: key); @override Widget build(BuildContext context) { return Expanded( child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), ), child: Column( children: [ action, const SizedBox(height: 20,), Container( margin: const EdgeInsets.only(right: 20, left: 0), padding: const EdgeInsets.only(top: 20, left: 12, right: 12, bottom: 8), decoration: BoxDecoration( borderRadius: BorderRadius.circular(15), color: Colors.white, ), child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: const[ Text('Good Service!', style: TextStyle( color: Colors.black87,fontSize: 16,fontWeight: FontWeight.bold)), Text('52 MIN AGO', style: TextStyle(color: Colors.black54)), ], ), const SizedBox(height: 10,), Row( children: const [ Icon( Icons.star, size: 15, color: Colors.yellow ), Icon( Icons.star, size: 15, color: Colors.yellow ), Icon( Icons.star, size: 15, color: Colors.yellow ), Icon( Icons.star, size: 15, color: Colors.yellow ), Icon( Icons.star, size: 15, color: Colors.grey ) ], ), const Divider( thickness: 1.5, endIndent: 10, ), const SizedBox(height: 15,), const Text('This is Awesome, totally cool. Keep going', style: TextStyle(color: Colors.black54)), const SizedBox(height: 15,), Row( children: const [ Icon(Icons.favorite_outline, color: Colors.black54), SizedBox(width: 10), Text('271' ,style: TextStyle(color: Colors.black54)), Expanded(child: SizedBox(),), Icon(Icons.more_horiz, color: Colors.black54) ], ), ], ), ), const SizedBox(height: 20), const Divider( thickness: 1, endIndent: 30, ) ], ), ), ); } }
Hotel review widget
Hotel review widget is kind of similar to the service review but with slight difference the container’s components.
class HotelReviewWidget extends StatelessWidget { final String hotelImage; final String name, price, location, likes; final ActionWidget action; const HotelReviewWidget({Key? key, required this.action, required this.hotelImage, required this.name, required this.price, required this.location, required this.likes}) : super(key: key); @override Widget build(BuildContext context) { return Container( width: MediaQuery.of(context).size.width * 0.75, decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), ), child: Column( children: [ action, const SizedBox(height: 20,), Container( width: 500, padding: const EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 10), decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), color: Colors.white ), child: Row( children: [ Container( width: 75, height: 75, decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), color: Colors.blue, image: DecorationImage( fit: BoxFit.fill, image: AssetImage( hotelImage, ) ) ), ), const SizedBox(width: 10), SizedBox( width: MediaQuery.of(context).size.width * 0.45, child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(name, style: const TextStyle( color: Colors.black87,fontSize: 16,fontWeight: FontWeight.bold),), const SizedBox(height: 5), Text('\$price', style: const TextStyle(color: Colors.black87),), ], ), const Icon(Icons.more_vert, color: Colors.black54,) ], ), const SizedBox(height: 20,), Row( mainAxisAlignment: MainAxisAlignment.start, children: [ const Icon(Icons.location_pin, size: 20, color: Colors.black54), Text(location, style: const TextStyle(color: Colors.black54)), const Expanded(child: SizedBox()), const Icon(Icons.favorite_outline, color: Colors.black54), const SizedBox(width: 5), Text(likes, style: const TextStyle(color: Colors.black54),), ], ) ], ), ) ], ), ), const SizedBox(height: 20), const Divider( thickness: 1, endIndent: 30, ) ], ), ); } }
Composing the reusable widgets in one UI
Now that we’re done building our reusable widgets, it’s time to implement them in one place. Let’s create a file with a Scaffold layout that has a SingleChildScrollView in its body with a column child where we’ll start adding our widgets to create the UI in the previous picture.
class CustomTimelineTile extends StatelessWidget { const CustomTimelineTile({super.key}); @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.grey[200], body: SingleChildScrollView( child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 20,), FirstTile( color: Colors.white, afterIndicator: Indicator( lineStyle: LineStyle(color: Colors.grey, height: followerTileLength ), ), icon: const Icon(Icons.access_time, color: Colors.pink,), ), TimelineTile( afterIndicator: Indicator( lineStyle: LineStyle( color: Colors.grey, height: followerTileLength, ), ), rightContent: const NewFollower( action: ActionWidget( name: 'Rick', time: '50 MIN AGO', action: follow, ), ), imagePath: avatar1), TimelineTile( afterIndicator: Indicator( lineStyle: LineStyle( color: Colors.grey, height: serviceTileLength, ), ), rightContent: const ServiceReview( action: ActionWidget( name: 'Jonathan', time: '1 HOUR AGO', action: likedReview, ), ), imagePath: avatar2), TimelineTile( afterIndicator: Indicator( lineStyle: LineStyle( height: followerTileLength, color: Colors.grey, ), ), rightContent: const NewFollower( action: ActionWidget( name: 'Ali', time: '1 HOUR AGO', action: follow, ), ), imagePath: avatar3, ), TimelineTile( afterIndicator: Indicator( lineStyle: LineStyle( height: hotelTileLength, color: Colors.grey, ), ), rightContent: const HotelReviewWidget( action: ActionWidget( name: 'Sarah', time: '2 HOURS AGO', action: likedReview, ), name: 'Hotel Seawatch', price: '234', location: 'Chicago', likes: '50K', hotelImage: hotel, ), imagePath: avatar4, ), TimelineTile( afterIndicator: Indicator( lineStyle: LineStyle( height: 20, color: Colors.grey ), ), rightContent: const NewFollower( action: ActionWidget( name: 'Mohammad', time: '2 HOURS AGO', action: follow, ), ), imagePath: avatar4) ], ), ) ); } }
Conclusion
Voila, you’ve made it! I know, creating a custom timeline tile can be challenging but rewarding task. By following the step-by-step guide and advanced techniques that we’ve covered in this post, you now have the tools and knowledge to create a personalized and engaging timeline for your app. Not only will this enhance the user experience and encourage more engagement, but it will also showcase your development’s unique capabilities. So what are you waiting for? Start designing and implementing your custom timeline today, and see how it can improve your app’s performance and user retention. If you have any questions or feedback, feel free to leave a comment below or contact me directly. I would love to hear your thoughts and see how you’ve used the tips and techniques in this post to create your own custom timeline for a social app activity.
Finally as promised, yon can get the full source code from my GitHub. Don’t forget to follow me and give it a star, I really can’t say how happy you make me when you show your support even in simple actions.
I hope you enjoyed this tutorial and found it helpful, Happy coding!