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.
class DateFormatter {
String getVerboseDateTimeRepresentation(DateTime dateTime) {
}
}
Just now
The first case we deal with is returning “Just now” if the given DateTime is less than a minute old.
DateTime now = DateTime.now();
DateTime justNow = DateTime.now().subtract(Duration(minutes: 1));
DateTime localDateTime = dateTime.toLocal();
if (!localDateTime.difference(justNow).isNegative) {
return 'Just now';
}
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.
String roughTimeString = DateFormat('jm').format(dateTime);
if (localDateTime.day == now.day && localDateTime.month == now.month && localDateTime.year == now.year) {
return roughTimeString;
}
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.
DateTime yesterday = now.subtract(Duration(days: 1));
if (localDateTime.day == yesterday.day && localDateTime.month == yesterday.month && localDateTime.year == yesterday.year) {
return 'Yesterday, ' + roughTimeString;
}
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.
if (now.difference(localDateTime).inDays < 4) {
String weekday = DateFormat('EEEE').format(localDateTime);
return '$weekday, $roughTimeString';
}
Otherwise, return year, month and day
Now if none of the above conditons match, we want to display the date and the time.
return '${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 on my blog for information on how to set that up.
import 'package:flutterclutter/app_localizations.dart';
import 'package:intl/intl.dart';
class DateFormatter {
DateFormatter(this.localizations);
AppLocalizations localizations;
String getVerboseDateTimeRepresentation(DateTime dateTime) {
DateTime now = DateTime.now();
DateTime justNow = now.subtract(Duration(minutes: 1));
DateTime localDateTime = dateTime.toLocal();
if (!localDateTime.difference(justNow).isNegative) {
return localizations.translate('dateFormatter_just_now');
}
String roughTimeString = DateFormat('jm').format(dateTime);
if (localDateTime.day == now.day && localDateTime.month == now.month && localDateTime.year == now.year) {
return roughTimeString;
}
DateTime yesterday = now.subtract(Duration(days: 1));
if (localDateTime.day == yesterday.day && localDateTime.month == now.month && localDateTime.year == now.year) {
return localizations.translate('dateFormatter_yesterday');
}
if (now.difference(localDateTime).inDays < 4) {
String weekday = DateFormat('EEEE', localizations.locale.toLanguageTag()).format(localDateTime);
return '$weekday, $roughTimeString';
}
return '${DateFormat('yMd', localizations.locale.toLanguageTag()).format(dateTime)}, $roughTimeString';
}
}
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.
import 'package:flutter/material.dart';
import 'date_formatter.dart';
import 'app_localizations.dart';
class Chat {
Chat({
@required this.sender,
@required this.text,
@required this.lastMessageSentAt
});
String sender;
String text;
DateTime lastMessageSentAt;
}
class MessageBubble extends StatelessWidget{
MessageBubble({
this.message
});
final Chat message;
@override
Widget build(BuildContext context) {
return Padding(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(
DateFormatter(AppLocalizations.of(context)).getVerboseDateTimeRepresentation(message.lastMessageSentAt),
textAlign: TextAlign.end,
style: TextStyle(
color: Colors.grey
)
)
]
),
Padding(
padding: EdgeInsets.only(bottom: 16),
child: Text(
message.sender,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold
)
),
),
Text(
message.text,
),
],
),
padding: EdgeInsets.all(16)
);
}
}
class DateTimeList extends StatelessWidget {
final List<Chat> messages = [
Chat(
sender: 'Sam',
text: 'Sorry man, I was busy!',
lastMessageSentAt: DateTime.now().subtract(Duration(seconds: 27))
),
Chat(
sender: 'Neil',
text: 'Hey! Are you there?',
lastMessageSentAt: DateTime.now().subtract(Duration(days: 3))
),
Chat(
sender: 'Patrick',
text: 'Hey man, what\'s up?',
lastMessageSentAt: DateTime.now().subtract(Duration(days: 7))
),
];
@override
Widget build(BuildContext context) {
return ListView.separated(
itemBuilder: (BuildContext context, int index) {
return MessageBubble(message: messages[index]);
},
separatorBuilder: (BuildContext context, int index) => Divider(),
itemCount: messages.length
);
}
}
Result
This is how it looks without the DateFormatter
The widget with our new DateFormatter and English locale
The same conditions 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.
Top comments (0)