惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

WordPress大学
WordPress大学
T
The Blog of Author Tim Ferriss
F
Fortinet All Blogs
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
阮一峰的网络日志
阮一峰的网络日志
The GitHub Blog
The GitHub Blog
Y
Y Combinator Blog
MyScale Blog
MyScale Blog
雷峰网
雷峰网
博客园 - 叶小钗
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
GbyAI
GbyAI
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
博客园 - 三生石上(FineUI控件)
云风的 BLOG
云风的 BLOG
V
V2EX
宝玉的分享
宝玉的分享
酷 壳 – CoolShell
酷 壳 – CoolShell
N
Netflix TechBlog - Medium
Vercel News
Vercel News
美团技术团队
人人都是产品经理
人人都是产品经理
The Cloudflare Blog

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
SignalApp UI Home Screen but with Flutter
Mathieu Kerjouan · 2026-06-23 · via DEV Community

He who can copy can do.
-- Leonardo Da Vinci

My UI skill is very low, I'm not really good with colors or themes and it's always a pain to design a full interface by myself... In short: I'm not an artist and it needs to be fixed. SignalApp is one of my main messenger, the interface is simple and functional. The application source code is also available on Github, and the Android application is using Material3. I think it can be a good target.

SignalApp Screenshot from appgefahren.de

Trying to reproduce it completely will take a while, only a small subset of the interface will be re-implemented, the home and the user chat screens, both can be seen from the screenshot above.

Home Screen

Good artists copy, great artists steal.
-- Pablo Picasso

The Home screen will be the entry-point of the application, where the active conversations can be seen by the user. It is also the place where SignalApp can be configured, when tapping on the User avatar on the top left of the application, a Drawer menu should open. This menu is not displayed on the screenshot, then, only an empty one will be created.

Top part of the Home Screen

The first easily identifiable element of the application is the top bar. I will assume this one can be created with the help of the AppBar class, where the leading parameter will contain the user picture (rounded). This element can be created with a CircleAvatar class. The title parameter will display the text "Signal" in bold, a TextStyle class can be set properly inside the Text class style attribute. The parameters actions will be set with 2 icons, one icon will represent a camera with Icons.camera and the last one will represent a pencil using the pencil constant. Finally, the background color of this element is grey (#fafafa).

AppBar homeBar(BuildContext context) {
  return AppBar(
    backgroundColor: Colors.grey[50],
    titleTextStyle: TextStyle(
      color: Colors.black,
      fontWeight: FontWeight.bold
    ),
    leading: Padding(
      padding: EdgeInsets.all(8.0),
      child: CircleAvatar(
        backgroundColor: Colors.amber,
        child: Text('AH')
      ),
    ),
    title: Center(child: Text('SignalCopyCat')),
    actions: [
      IconButton(
        onPressed: () {}, 
        icon: Icon(Icons.camera_alt_outlined)
      ),
      IconButton(
        onPressed: () {},
        icon: Icon(Icons.edit_outlined)
      ),
    ],
  );
}

The top Bar using by Signal is not the most challenging part of the design, at least, when we don't have state to manage. The homeBar() function created above is returning an AppBar() object with some custom parameters.

The backgroundColor parameter is set to grey. The titleTextStyle parameter has been set with a custom Color and FontWeight, this should be applied on the rest of the widgets instantiated below this bar.

The leading parameter (the right part of the bar) is using a Padding widget to reduce the side of its child element with the help of an EdgeInsets object. The offset configured (8.0) is applied on all sides, thanks to the EdgeInsets.all() constructor. Its only child is CircleAvatar widget containing only the initials of the users as a Text() widget.

The title parameter is only containing the title of the application as in a Text() object, centered via a Center() widget.

The actions parameter is using a list of IconButton widget, which one set with an Icon(). The number of icons available with Material is astonishing... I took me a moment to find what I was looking for, Icons.camera_alt_outlined for the camera and Icons.edit_outlined for the pencil. Other icons can also be used via the CubertinoIcons class (usually designed for iOS though).

No callback functions have been set in the onPressed parameters, this is not the goal of this article.

Middle and bottom part of the Home screen

The next big part of the Home screen is taking the remaining place. This section can be scrolled. It is made of many similar elements probably derived from a ListTile class. Those are split in 3 part, from the left to the right, the first part is an icon containing a picture of a remote user where we can reuse the CircleAvatar class. The second part contains the name of the user (in bold) above the last messages (if any). The last part contains the date of the last message received (with an attachment or an acknowledgement if any). When the user is tapping on one of these elements, it is redirected to a Chat Screen. The background color of this part of the application is white (#ffffff).

String initials(String str) {
  return str
    .split(" ")
    .map((x) => x.isEmpty ? "?" : x[0])
    .take(2)
    .join("");
}

Firstly, I think creating a function to convert a first name and last name (or any kind of string) to their initials can be helpful for the next part. This is a quick and dirty implementation, the string must be sanitized first and only characters (including UTF8 characters) should be used. It will do the job for know though, it could be nice to see what kind of algorithm SignalApp is using, I think the getAbbreviation() method from org.thoughtcrime.securesms.util.NameUtil is the one we are looking for.

ListTile tile({
  String? title,
  String? subtitle,
  String? trailing,
  MaterialColor? avatarColor,
  String? notifications,
  bool? received,
}) {
  return ListTile(
    leading: Badge(
      label: Text(notifications ?? "0"),
      isLabelVisible: notifications == null ? false : true,
      textColor: Colors.white,
      backgroundColor: Colors.blue,
      child: CircleAvatar(
        backgroundColor: avatarColor ?? Colors.amber,
        child: Container(
          decoration: BoxDecoration(shape: BoxShape.circle),
          child: Text(title == null ? "??" : initials(title)),
        ),
      ),
    ),
    title: Flex(
      direction: Axis.horizontal,
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: [
        Align(
          alignment: Alignment.topLeft,
          child: Text(
            title ?? "John Smith",
            style: TextStyle(fontWeight: FontWeight.bold, fontFamily: "Roboto")
            )
          ),
        Align(
          alignment: Alignment.topRight,
          child: Text(
            trailing ?? "11:11 AM",
            style: notifications != null 
              ? TextStyle(fontWeight: FontWeight.bold, fontSize: 12, fontFamily: "Roboto")
              : TextStyle(fontWeight: FontWeight.normal, fontSize: 12, fontFamily: "Roboto"),
          ),
        ),
      ],
    ),
    subtitle: Flex(
      direction: Axis.horizontal,
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: [
        Expanded(
          flex: 1,
          child: Text(
            subtitle ?? "Last message...",
            overflow: TextOverflow.ellipsis,
            style: notifications != null 
              ? TextStyle(fontWeight: FontWeight.bold, fontSize: 14, fontFamily: "Roboto")
              : TextStyle(fontWeight: FontWeight.normal, fontSize:14, fontFamily: "Roboto")
          ),
        ),
        received == true
            ? Icon(Icons.library_add_check_rounded, size: 16)
            : Container(),
      ],
    ),
    onTap: () {},
  );
}

Another function helper called tile() will help us to generate each ListTile() objects. This function can take 6 optional arguments, if those are not set, some default values are used instead.

The leading parameter is containing the user avatar, material gives us the Badge() object to deal with that. This one contains a label (the blue circle containing the number of notifications). I tried to add a white border on this label, without success, perhaps my future me will fix that. If there is no notification, the isLabelVisible parameter can be set to false, in this case, the label will be removed. The only child is a CircleAvatar using a specific BoxDecoration to produce a circle via BoxShape.circle. By default, the initials of the title variable will be used.

Regarding badges, 2 modules can probably fix most of my issues here:

  • info_label: A high-performance Flutter label widget built on CustomPainter. Background, border, text, and overlay indicators are painted directly on canvas for minimal widget overhead;

  • badges: A package for creating badges. Badges can be used for an additional marker for any widget, e.g. show a number of items in a shopping cart.

The title parameter was configured to set the user name on the left and the date on the right. I thought at first I could use the trailing parameter to do that, but it was kinda messy, and way more complex. Anyway, a Flex widget is used to let us have a flexible positioning across the elements. Indeed, we want the widgets on an horizontal Axis (see Axis.horizontal) with a space between them (see MainAxisAlignment.spaceBetween). Then the 2 widgets children can be aligned using the Alignment.topLeft and Alignment.topRight constants. Nothing smart here, both of them are Text() widgets.

The subtitle is using the same hack than the title parameter previously described. An element to left (last message from the remote user) and another one to the right (message ack).

onTap is not configured yet, but the idea when someone is tapping on one tile, he will be redirected to the chat screen.

class Conversations extends StatelessWidget {
  const Conversations({super.key});

  @override
  Widget build(BuildContext context) {
    return ListView(
      children: [
        tile(
          avatarColor: Colors.blueGrey,
          title: "Tina Ukuku",
          subtitle: "You set disappearing message time to 1...",
          avatarText: "TU",
        ),
        tile(
          avatarColor: Colors.indigo,
          title: "Chairman Meow",
          subtitle: "Missed call",
          avatarText: "CM",
          notifications: "2",
        ),
        tile(
          avatarColor: Colors.lightGreen,
          title: "Myles Larson",
          subtitle: "🎤 Voice Message",
          trailing: "11:07 AM",
          notifications: "7",
        ),
        tile(
          avatarColor: Colors.lime,
          title: "Cat Chat 🐈 🐱",
          subtitle: "This is the instruction manual. 📎 Attachment",
          trailing: "11:02 AM",
        ),
        tile(
          avatarColor: Colors.blueGrey,
          title: "Ali Smith",
          subtitle: "📷 Attachment",
          trailing: "10:38 AM",
          received: true,
        ),
        tile(
          avatarColor: Colors.indigo,
          title: "Kirk Family",
          subtitle: "Happy birthday to you. Happy birthday to you!",
          trailing: "9:13 AM",
          notifications: "1",
        ),
        tile(
          avatarColor: Colors.lightGreen,
          title: "Jordan B.",
          subtitle: "Sticker Message",
          received: true,
        ),
        tile(
          avatarColor: Colors.lime,
          title: "Sunsets 🌅",
          subtitle: "View-once media",
          trailing: "TUE",
          received: true,
        ),
        tile(
          avatarColor: Colors.blueGrey,
          title: "🧗‍♂️ Rock Climbers",
          subtitle: "Which route should we take?",
          trailing: "TUE",
        ),
        tile(
          avatarColor: Colors.indigo,
          title: "Nikki R.",
          subtitle: "Thanks! What a wonderful message to read in the morning!",
          trailing: "TUE",
        ),
        tile(
          avatarColor: Colors.lightGreen,
          title: "Weather Forecasts",
          subtitle: "Raining all day 📷 Attachment",
          trailing: "MON",
        ),
        tile(avatarColor: Colors.lime),
        tile(avatarColor: Colors.blueGrey),
        tile(avatarColor: Colors.indigo),
        tile(avatarColor: Colors.lightGreen),
        tile(avatarColor: Colors.lime),
      ],
    );
  }
}

Now our tile() function is ready, we can create our Conversations() object containing the list of user chats. This part creates a ListView object, containing a list of ListTile created by our helper function. This part seems complex, but only one function is used many time to generated content and mimic the screenshot.

class Home extends StatelessWidget {
  const Home({super.key});

  Widget build(BuildContext context) {
    return Scaffold(
      appBar: homeBar(context),
      body: Conversations()
    );
  }
}

Everything looks good, we then mix everything together inside an Home class returning a Scaffold() object.

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'SignalCopyCat',
      home: Home(),
      theme: ThemeData(
        fontFamily: "Roboto"
      ),
      themeMode: ThemeMode.light,
      debugShowCheckedModeBanner: false,
    );
  }
}

void main() {
  runApp(const MyApp());
}

Finally, the last step, we can create also our application using a MaterialApp() object.

SignalApp UI Home Screen but with Flutter

Above, a screenshot of this application from the Android emulator. The images/pictures are missing on purpose, I was not able to found the "correct" images. As you can see, the emojis are not correctly rendered as well. Still few bugs to fix, but I did that in few hours.

I forgot something though, a big part of my code is using custom fonts (Roboto). Fonts are kind of assets and must be downloaded then configured in the project. The Use a custom font article on the official Flutter documentation is doing a great job to explain that.

Conclusion

I’ve been imitated so well, I’ve heard people copy my mistakes.
-- Jimi Hendrix

This is not the perfect 1:1 reproduction of the SignalApp Home screen... I still need more experience with Flutter. The terms used are also new to me, finding one icon similar to the one from the screenshot is taking time for example. The infinite amount of solution to display something is also time consuming. Here a list of few resources I checked during this experience.

Anyway, copying and reproducing is a great way to learn. It also train the mind to decompose every applications in small pieces. The same could have been done for Briar, SimpleX or Telegram applications, all of them are open-source and available on Github, Gitlab or somewhere else. Some of them are supporting mobile, desktop and web platform, in short, they are great targets. To summarize, if you want to learn how to design UI, try to recreate the design and the style of your favorite application.

The next step? Why not create an open-source library in Dart/Flutter exposing these styles? Perhaps one day...

Happy Hack and Have fun!


Cover Image by Compagnons on Unsplash