Cloning Medium Notification UI with Flutter — Slicing Challenge #1
An example of how I convert a screenshot image into Flutter code.
Welcome to the slicing challenge. In this series, I will share an article/story about converting design or image into Flutter code.
Slicing UI is a technique used to convert a user interface image, piece, or screen into Flutter code [1]. It involves breaking down the design into smaller widgets that can be easily implemented in Flutter [2]. This technique is useful for developers who want to create beautiful designs and implement them in their applications quickly and efficiently.
In this first challenge, I’ll start with something easy, which is the medium notification mobile screen.
This is going to be quite a long article, but you don’t need to read every single word. I’ll try to separate each slice of widget. So, as a reader you can skim or read which part you want to focus on.
ok, let's go….
- Prepare a Color Palette.
Converting a design from Figma or Adobe XD, we may have the color palette to be implemented in the UI. But in this case, I randomly take a screenshot of a mobile screen from medium apps. So I don’t have the list of color palettes. That's why I need to pick the color from the image manually.
Anyway, this is just one of the options. There are some other options you can find on the Internet. This is how I get the color from the screenshot image.
- upload your image to the internet. e.g.: WhatsApp web, GitHub, etc.
- open the preview Image in the browser.
- open inspect element. ( ctrl + shift + i )
- create in-line style
background: white.
- Then click on the color and pick the color from the image with the tool.
- now I can get the hex of the button color:
#b77120
- Repeat, until you get all the color that you need from the image.
Since it's only 5 colors, I put it as constant in my code.
konstanta.dart
const kBlue = Color(0xFF3c79f6);
const kGreen = Color(0xFF1d8e22);
const kPink = Color(0xFFf45188);
const kLightGreen = Color(0xFF67a039);
const kOrange = Color(0xFFde573c);
2. Model and Dummy Data
Actually, I do this step in the middle of the process. But it's better to prepare the dummy data first since we have different icons based on their type in the notification. With the data, it helps to create an accurate widget. For example, a notification with the type Followed uses the icon person, Responded uses the bubble comment icon, etc.
After trying to cover all the types of notifications, I end up with this model. (of course, this is only a dummy model 😃)
notif_model.dart
class NotifModel {
int id;
String articleTitle;
String userName;
NotifType notifType;
String? imageAsset;
String time;
String readingListName;
NotifModel({
this.id = 0,
required this.userName,
required this.notifType,
this.articleTitle = "",
this.imageAsset,
required this.time,
this.readingListName = "",
});
static NotifModel empty() =>
NotifModel(userName: "", notifType: NotifType(id: 0, name: ""), time: "");
}
class NotifType {
int id;
String name;
NotifType({required this.id, required this.name});
}
and this is a sample of the data:
List<NotifModel> notifData = [
NotifModel(
userName: "Pmatatias",
notifType: NotifType(id: 1, name: "responded to"),
articleTitle:
"Level up my flutter loading widget with Logo + Flutter animation",
id: 1,
imageAsset: "assets/pmatatias.png",
time: "59 minutes ago"),
.....
If you noticed the time, I use manual string and NOT with date time format. For now, let it be like that. I will share an awesome method to handle differences of date usually used for notification in the next Article.
start slicing UI…………….
3. AppBar
The slicing process starts from the top of the screen. I create it with AppBar
widget and use it on Scaffold.
Actually, by default in Flutter, the arrow left icon will be available in the AppBar when we are navigated from another screen. But since this is only one screen, I will create it manually with BackButton()
widget. Use it on the leading
property.
notif_list_screen.dart
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey.shade200,
appBar: AppBar(
elevation: 0.8,
backgroundColor: Colors.white,
leading: const BackButton(color: Colors.black),
title: const Text("Activity",
style: TextStyle(color: Colors.black, fontWeight: FontWeight.bold),
textScaleFactor: 0.9),
),
4. IconType
Create a small widget of icon type. You can see the image, there are some types of notifications with an icon and some of them without an icon.
- responded: Background blue with the
buble comment
icon. - clapped: Background green with 👏 icon.
- followed: Background pink with the
person
icon. - and other notifications are without icon
In this case, we will return the Notification icon based on its type,
icon_type.dart
class MyIconType extends StatelessWidget {
const MyIconType({super.key, required this.type});
final NotifType type;
@override
Widget build(BuildContext context) {
/// display nothing
if (type.id >4) {
return const SizedBox();
}
IconData iconData;
Color bgColor;
switch (type.id) {
case 1:
iconData = CupertinoIcons.chat_bubble_2_fill;// responded
bgColor = kBlue;
break;
case 2: // we dont have clap icon change to thumbsup icon
iconData = CupertinoIcons.hand_thumbsup_fill; // clapped
bgColor = kGreen;
break;
case 3:
iconData = Icons.person; // followed
bgColor = kPink;
break;
case 4:
iconData = CupertinoIcons.eyedropper; // highlighted
bgColor = kOrange;
break;
default:
iconData = CupertinoIcons.book_fill;
bgColor = kBlue;
}
return Container(
padding: const EdgeInsets.all(3),
decoration: BoxDecoration(
color: bgColor, borderRadius: BorderRadius.circular(50)),
child: Icon(iconData, color: Colors.white, size: 14),
);
}
}
5. Avatar User
Now we create an avatar user and combine it with the icon type using Stack
widget.
In this widget, we have 2 conditions:
- User has profile picture: display profile picture.
- The user doesn’t have a profile picture: set the background color and use the first character of the username. For example, if my name is Pmatatias, then my avatar will be P with some background color.
You may set a static value for 26 of the alphabet for the background color. For now, I will set it with a random color.
avatar_widget.dart
class AvatarWidget extends StatelessWidget {
const AvatarWidget({super.key, required this.data});
final NotifModel data;
@override
Widget build(BuildContext context) {
final showTxt = data.imageAsset == null;
return Stack(
children: [
CircleAvatar(
radius: 24,
backgroundColor: showTxt
? Color.fromRGBO(
Random().nextInt(255),
Random().nextInt(255),
Random().nextInt(255),
1,
)
: Colors.transparent,
foregroundImage: showTxt
? null
: Image.asset(
'${data.imageAsset}',
errorBuilder: (context, error, stackTrace) =>
const FlutterLogo(),
).image,
child: showTxt
? Text(
data.userName[0],
style: const TextStyle(
fontSize: 27,
color: Colors.white,
fontWeight: FontWeight.w500,
),
)
: null,
),
Positioned(
bottom: 1,
right: 0,
child: MyIconType(type: data.notifType),
)
],
);
}
}
6. Follow Button
To create this button, I use MaterialButton
with shape
and Color
property to make it similar. And also by setting the height
will make it more similar.
follow_btn.dart
class FollowBtn extends StatelessWidget {
const FollowBtn({super.key});
@override
Widget build(BuildContext context) {
return MaterialButton(
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(30)),
color: kGreen,
height: 18,
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
onPressed: () {},
child: const Text(
"Follow",
textScaleFactor: 0.8,
style: TextStyle(color: Colors.white),
),
);
}
}
7. Notification Row
Now we can combine all the widgets and wrap them into one widget. Here I named it with NotifRow.
I use RichText
to make the text has a different style at some words in the line.
Also, add some conditions based on each notification type.
for example: display follow button when the notif_type is Followed by
class NotifRow extends StatelessWidget {
const NotifRow({super.key, required this.data});
final NotifModel data;
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
margin: const EdgeInsets.only(bottom: 1),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
AvatarWidget(data: data),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
RichText(
maxLines: 6,
overflow: TextOverflow.ellipsis,
text: TextSpan(
style: const TextStyle(
fontWeight: FontWeight.w400, color: Colors.black),
text: data.userName,
children: [
TextSpan(
text: " ${data.notifType.name} ",
style: const TextStyle(
color: Colors.grey, fontWeight: FontWeight.w400)),
if (data.articleTitle.isNotEmpty)
TextSpan(text: data.articleTitle),
if (data.readingListName.isNotEmpty)
TextSpan(
text: " to their list ${data.readingListName}",
style: const TextStyle(
color: Colors.grey,
fontWeight: FontWeight.w400)),
]),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 5),
child: Text(
data.time,
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
)
],
)),
if (data.notifType.id == 3) const FollowBtn()
],
),
);
}
}
8. BottomNavigationBar
This is not part of the notification list. But since it occurred in the screenshot picture, I will create it to make it more similar to the target.
Some cases you have to know:
BottomNavigationbar
by default maximum 3 items. Here we have 4 items. That's why we need to add one property to avoid the error:type: BottomNavigationBarType.fixed
- The last icon is using profile picture. So we have to use the
Image
asIcon
in theBottomNavigationBar
(see the fourth icon below)
notif_list_screen.dart
....
bottomNavigationBar: BottomNavigationBar(
currentIndex: 0,
selectedItemColor: Colors.black,
showSelectedLabels: false,
showUnselectedLabels: false,
type: BottomNavigationBarType.fixed,
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: "Home"),
BottomNavigationBarItem(icon: Icon(Icons.search), label: "Search"),
BottomNavigationBarItem(
icon: Icon(Icons.bookmarks_outlined), label: "Bookmark"),
BottomNavigationBarItem(
icon: Image(
image: AssetImage('assets/pmatatias.png'),
width: 24,
height: 24,
color: null,
),
label: "Profil"),
]),
....
We have done convert the Screenshot image into flutter code.
But for the last, I add a shimmer effect to show animation loading. I use Shimmer package from pub.dev
9. Shimmer
To make a shimmer widget, we can create a shaped widget. Here I have one circle and 2 rectangle widgets. and here is the final view:
Next, I use ListView.builder + Shimmer.fromColor
to add the number of widgets and shimmer effect.
And this is the final result:
You can find the full code from this repository on GitHub:
I also create a timelapse video. you can watch 3 mins duration here:
Thank you for having the end. If you have any idea to clone and slice the UI, please leave a comment.
And if you like this article, do not forget to clap 👏
Happy reading.