You probably have seen applications that format dates in a verbose way:
- “Just now” when it’s been less than a minute
- Only the time (xx:xx) if it was today and longer ago than a minute
- “Yesterday, xx:xx” if it was yesterday
- The weekday and the time if it was within the last couple of days
- The exact date and time if it was longer ago
This type of format can be found in popular chat apps for example in the chat overview. Let’s implement a date formatter that puts out a string in the above mentioned way.
We start by implementing a new class called DateFormatter
with a single public method getVerboseDateTimeRepresentation
. We let it expect a UTC DateTime
as its purpose is to receive a DateTime
and return a string.
Just now
The first case we deal with is returning “Just now” if the given DateTime
is less than a minute old.
1DateTime now = DateTime.now();
2DateTime justNow = DateTime.now().subtract(Duration(minutes: 1));
3DateTime localDateTime = dateTime.toLocal();
4
5if (!localDateTime.difference(justNow).isNegative) {
6 return 'Just now';
7}
It’s important to make the comparison with the local DateTime
as it ensures that it works across every timezone. Otherwise, the result of the difference would always be affected by the difference of the local timezone to the UTC timezone.
Today
Next step is to show the rough time (meaning that it omits the seconds) whenever the given DateTime
is less than a day old.
1String roughTimeString = DateFormat('jm').format(dateTime);
2if (localDateTime.day == now.day && localDateTime.month == now.month && localDateTime.year == now.year) {
3 return roughTimeString;
4}
You might wonder why 'jm'
is used as the positional newPattern
argument for the DateTime
constructor. That’s because it adapts to the circumstances of the current locale. If we were to use DateFormat('HH:mm')
, we would always have 24 hour time format whereas DateFormat('jm')
would use 12 hour time and add am / pm markers if needed. For more information about the difference of skeletons and explicit patterns have a look at the docs.
We could also use the ICU name instead of the skeleton. In this case DateFormat('WEEKDAY')
would work as well and is certainly better readable.
Yesterday
Now we want the DateFormatter
to prepend Yesterday,
if the given DateTime
holds a value that represents the day before today.
1DateTime yesterday = now.subtract(Duration(days: 1));
2
3if (localDateTime.day == yesterday.day && localDateTime.month == yesterday.month && localDateTime.year == yesterday.year) {
4 return 'Yesterday, ' + roughTimeString;
5}
We check whether day, month and year of the current DateTime
subtracted by a day equal the respective values of the given DateTime
and return Yesterday,
followed by the rough time string we stored above if the condition evaluates to true.
Last couple of days
Let’s deal with everything less old than 4 days and return the weekday in the system’s language followed by the hours and minutes.
1if (now.difference(localDateTime).inDays < 4) {
2 String weekday = DateFormat('EEEE').format(localDateTime);
3
4 return '$weekday, $roughTimeString';
5}
We compare the current DateTime
with the given DateTime
and check whether the difference is less than 4 whole days. If so, we use the skeleton EEEE
that represents the verbose weekday. Because we don’t provide the second optional argument, it takes en_US
as the locale and returns the weekday in that language.
Again, we could also use the ICU name instead of the skeleton so DateFormat('<mark>HOUR_MINUTE</mark>')
would work as well.
Otherwise, return year, month and day
Now if none of the above conditions match, we want to display the date and the time.
1return '${DateFormat('yMd').format(dateTime)}, $roughTimeString';
So now we have achieved what we wanted to: depending on how long ago the given DateTime
was, we want to return different strings.
Localization of DateFormatter
One thing that this formatter is still lacking is localization. If we used this on a device whose system language is not English, we would still be faced with English expressions.
In order to fix that, we need the current system’s locale. That’s not enough, though, as we also want the phrases "Just now"
and "Yesterday"
to be translated. That’s why we need localization in general and take the locale from the delegate. Have a look at the i18n tutorial for information on how to set that up.
en.json
de.json
1import 'package:flutterclutter/app_localizations.dart';
2import 'package:intl/intl.dart';
3
4class DateFormatter {
5 DateFormatter(this.localizations);
6
7 AppLocalizations localizations;
8
9 String getVerboseDateTimeRepresentation(DateTime dateTime) {
10 DateTime now = DateTime.now();
11 DateTime justNow = now.subtract(Duration(minutes: 1));
12 DateTime localDateTime = dateTime.toLocal();
13
14 if (!localDateTime.difference(justNow).isNegative) {
15 return localizations.translate('dateFormatter_just_now');
16 }
17
18 String roughTimeString = DateFormat('jm').format(dateTime);
19
20 if (localDateTime.day == now.day && localDateTime.month == now.month && localDateTime.year == now.year) {
21 return roughTimeString;
22 }
23
24 DateTime yesterday = now.subtract(Duration(days: 1));
25
26 if (localDateTime.day == yesterday.day && localDateTime.month == now.month && localDateTime.year == now.year) {
27 return localizations.translate('dateFormatter_yesterday');
28 }
29
30 if (now.difference(localDateTime).inDays < 4) {
31 String weekday = DateFormat('EEEE', localizations.locale.toLanguageTag()).format(localDateTime);
32
33 return '$weekday, $roughTimeString';
34 }
35
36 return '${DateFormat('yMd', localizations.locale.toLanguageTag()).format(dateTime)}, $roughTimeString';
37 }
38}
We add AppLocalization
as the only argument to the constructor of the DateFormatter
. Every occasion of a string that contains a language-specific phrase is now altered by the usage of AppLocalization
or its locale
property.
Now, in order to see our brand new formatter in action, we create a widget that lists hardcoded chats and displays the date of the last sent message in the top right corner.
1import 'package:flutter/material.dart';
2import 'date_formatter.dart';
3import 'app_localizations.dart';
4
5class Chat {
6 Chat({
7 @required this.sender,
8 @required this.text,
9 @required this.lastMessageSentAt
10 });
11
12 String sender;
13 String text;
14 DateTime lastMessageSentAt;
15}
16
17class MessageBubble extends StatelessWidget{
18 MessageBubble({
19 this.message
20 });
21
22 final Chat message;
23
24 @override
25 Widget build(BuildContext context) {
26 return Padding(
27 child: Column(
28 crossAxisAlignment: CrossAxisAlignment.start,
29 children: <Widget>[
30 Row(
31 mainAxisAlignment: MainAxisAlignment.end,
32 children: [
33 Text(
34 DateFormatter(AppLocalizations.of(context)).getVerboseDateTimeRepresentation(message.lastMessageSentAt),
35 textAlign: TextAlign.end,
36 style: TextStyle(
37 color: Colors.grey
38 )
39 )
40 ]
41 ),
42 Padding(
43 padding: EdgeInsets.only(bottom: 16),
44 child: Text(
45 message.sender,
46 style: TextStyle(
47 fontSize: 18,
48 fontWeight: FontWeight.bold
49 )
50 ),
51 ),
52 Text(
53 message.text,
54 ),
55 ],
56 ),
57 padding: EdgeInsets.all(16)
58 );
59 }
60}
61
62class DateTimeList extends StatelessWidget {
63 final List<Chat> messages = [
64 Chat(
65 sender: 'Sam',
66 text: 'Sorry man, I was busy!',
67 lastMessageSentAt: DateTime.now().subtract(Duration(seconds: 27))
68 ),
69 Chat(
70 sender: 'Neil',
71 text: 'Hey! Are you there?',
72 lastMessageSentAt: DateTime.now().subtract(Duration(days: 3))
73 ),
74 Chat(
75 sender: 'Patrick',
76 text: 'Hey man, what\'s up?',
77 lastMessageSentAt: DateTime.now().subtract(Duration(days: 7))
78 ),
79 ];
80
81 @override
82 Widget build(BuildContext context) {
83 return ListView.separated(
84 itemBuilder: (BuildContext context, int index) {
85 return MessageBubble(message: messages[index]);
86 },
87 separatorBuilder: (BuildContext context, int index) => Divider(),
88 itemCount: messages.length
89 );
90 }
91}
Result
Here is a comparison between the above implemented list with:
- Chats with dates being displayed without our DateFormatter
- Chats with dates being displayed with our DateFormatter and English locale
- Chats with dates being displayed with our DateFormatter and German locale
Summary
We have implemented a class that returns a verbose representation of the DateTime
object that is provided and adaptively changes depending on how far the date reaches into the past. We have also made this class handle different locales using localization.
This formatter can be useful in several contexts where a date is supposed to be displayed relative to the current date.
zgorawski
Marc
In reply to zgorawski's comment