Flutter date format: depending on how long ago
Tags: flutter, dart, tutorial
Published: 2020-06-22 14:43:02

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.

```dart
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.

```dart
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.

```dart
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](https://pub.dev/documentation/intl/latest/intl/DateFormat-class.html).

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.

```dart
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.

```dart
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.

```dart
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](https://www.flutterclutter.dev/flutter/tutorials/internationalization-i18n-in-flutter-an-easy-and-quick-approach/2020/213/) for information on how to set that up.

```dart
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.

```dart
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
![Date format without using our DateFormatter](https://dev-to-uploads.s3.amazonaws.com/i/4w1cv6no7nhxvakvu223.png)

### The widget with our new DateFormatter and English locale
![Date format using our DateFormatter](https://dev-to-uploads.s3.amazonaws.com/i/d5h6cl8x0pquj04xekpi.png)

### The same conditions and German locale
![Date format using our DateFormatter with locale de](https://dev-to-uploads.s3.amazonaws.com/i/cyuv86zwx0o450hkz0v5.png)

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