Cloning Medium Notification UI with Flutter — Slicing Challenge #1

Matatias Situmorang
8 min readMay 24, 2023

--

An example of how I convert a screenshot image into Flutter code.

medium notification clone

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….

  1. 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

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

icon type

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 theperson 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.

Avatar 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 button

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

notif 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.

bottomNavigationBar Medium

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 as Icon in the BottomNavigationBar (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:

shimmer widget

Next, I use ListView.builder + Shimmer.fromColor to add the number of widgets and shimmer effect.

And this is the final result:

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.

--

--

Matatias Situmorang

Flutter developer | Blogger | Reach me on twitter @pmatatias_