Date & Time #

Time seems simple until you start working with it in code. Timezones, daylight saving time, local vs UTC differences, and inconsistent formats across platforms are very common bug sources — even in production applications. DateTime in Dart provides a representation of a moment in time, while Duration represents the interval between two moments. Understanding the difference between local time and UTC, when to use each, and how to do date calculations correctly is the skill that separates reliable code from code that produces mysterious bugs in certain timezones.

Creating DateTime #

// Specific date and time — device local timezone
DateTime ulangTahun = DateTime(1998, 5, 20);          // May 20 1998, 00:00:00
DateTime rapat = DateTime(2024, 12, 25, 14, 30);      // Dec 25 2024, 14:30:00
DateTime presisi = DateTime(2024, 1, 15, 9, 0, 0, 0, 0); // full: milliseconds, microseconds

// Current time — local timezone
DateTime sekarang = DateTime.now();

// UTC — universal time coordinate
DateTime sekarangUtc = DateTime.now().toUtc();
DateTime utcSpesifik = DateTime.utc(2024, 6, 15, 12, 0); // Jun 15 2024, 12:00 UTC

// From a Unix timestamp (milliseconds since Jan 1 1970 UTC)
DateTime dariMs = DateTime.fromMillisecondsSinceEpoch(1718449200000);
DateTime dariMsUtc = DateTime.fromMillisecondsSinceEpoch(1718449200000, isUtc: true);

// From a Unix timestamp in microseconds
DateTime dariUs = DateTime.fromMicrosecondsSinceEpoch(1718449200000000);

Local vs UTC — the Most Common Trap #

This is the most frequently encountered bug source when working with DateTime. DateTime.now() produces the time in the device’s local timezone — when the app runs in a different timezone, the result differs.

DateTime local = DateTime.now();
DateTime utc = DateTime.now().toUtc();

print(local.isUtc);          // false
print(utc.isUtc);            // true

// Two-way conversion
DateTime kembaliLocal = utc.toLocal();
print(utc.isAtSameMomentAs(kembaliLocal)); // true — same moment, different representation

// Compare correctly — always convert to UTC before comparing
DateTime a = DateTime(2024, 6, 15, 7, 0);           // local time WIB (UTC+7)
DateTime b = DateTime.utc(2024, 6, 15, 0, 0);       // UTC, same as 07:00 WIB

print(a == b);                     // ✗ false — different representations
print(a.isAtSameMomentAs(b));      // ✓ true — the same moment
// ANTI-PATTERN: storing or comparing DateTime without timezone consistency
void simpanTanggal(DateTime tanggal) {
  final db = ambilDatabase();
  db.simpan({'tanggal': tanggal.toString()}); // ✗ different format for local vs UTC
  // '2024-06-15 07:00:00.000' (local) vs '2024-06-15 00:00:00.000Z' (utc)
}

// CORRECT: always convert to UTC before storing, convert back when displaying
void simpanTanggal(DateTime tanggal) {
  final utc = tanggal.toUtc();
  db.simpan({'tanggal': utc.toIso8601String()}); // '2024-06-15T00:00:00.000Z'
}

DateTime bacaTanggal(String isoString) {
  return DateTime.parse(isoString).toLocal(); // convert to local when displaying
}
flowchart LR
    A["DateTime.now()\n(local device timezone)"] -->|toUtc| B["DateTime UTC\n(universal)"]
    B -->|toLocal| A
    C["API/Database\n(always store UTC)"] -->|parse + toLocal| A
    A -->|toUtc + toIso8601String| C

DateTime Properties #

DateTime dt = DateTime(2024, 11, 15, 14, 30, 45, 123, 456);

// Date components
print(dt.year);          // 2024
print(dt.month);         // 11 (November)
print(dt.day);           // 15
print(dt.weekday);       // 5 (Friday — 1=Monday, 7=Sunday)

// Time components
print(dt.hour);          // 14
print(dt.minute);        // 30
print(dt.second);        // 45
print(dt.millisecond);   // 123
print(dt.microsecond);   // 456

// Timezone info
print(dt.isUtc);         // false (local)
print(dt.timeZoneName);  // 'WIB' or 'Asia/Jakarta' etc
print(dt.timeZoneOffset); // Duration(hours: 7) for WIB

// Timestamps
print(dt.millisecondsSinceEpoch); // unix timestamp in ms
print(dt.microsecondsSinceEpoch); // unix timestamp in µs

// Day of the year (1-366)
int hariKeberapa = dt.difference(DateTime(dt.year, 1, 1)).inDays + 1;
print(hariKeberapa); // ~320 for November 15

Parsing Strings to DateTime #

Parsing is an operation that often fails when input doesn’t match the expected format. Always use tryParse for untrusted input:

// parse — throws FormatException if the format is wrong
DateTime valid = DateTime.parse('2024-06-15');
DateTime iso = DateTime.parse('2024-06-15T14:30:00');
DateTime isoZ = DateTime.parse('2024-06-15T14:30:00.000Z');    // UTC
DateTime isoOffset = DateTime.parse('2024-06-15T21:30:00+07:00'); // with offset

// tryParse — returns null if the format is wrong, doesn't throw
DateTime? aman = DateTime.tryParse('2024-06-15');         // DateTime
DateTime? gagal = DateTime.tryParse('15/06/2024');        // null — unsupported format
DateTime? kosong = DateTime.tryParse('not a date');    // null

// Handle null with a fallback
DateTime tanggalInput = DateTime.tryParse(inputUser) ?? DateTime.now();
// ANTI-PATTERN: parsing without error handling for user input
String input = ambilInputUser();
DateTime tanggal = DateTime.parse(input); // ✗ crashes if the format is wrong

// CORRECT: tryParse with a fallback or error message
String input = ambilInputUser();
final tanggal = DateTime.tryParse(input);
if (tanggal == null) {
  tampilkanError('Invalid date format. Use the YYYY-MM-DD format');
  return;
}
// continue with the valid date

Formats Supported by parse #

// All valid ISO 8601 formats
DateTime.parse('2024-06-15');                    // date only
DateTime.parse('2024-06-15 14:30:00');           // with time
DateTime.parse('2024-06-15T14:30:00');           // with T separator
DateTime.parse('2024-06-15T14:30:00.000');       // with milliseconds
DateTime.parse('2024-06-15T14:30:00.000000');    // with microseconds
DateTime.parse('2024-06-15T14:30:00Z');          // UTC (Z suffix)
DateTime.parse('2024-06-15T21:30:00+07:00');     // with timezone offset

// Formats NOT supported (need intl or manual parsing)
// '15/06/2024'
// '15 Juni 2024'
// 'June 15, 2024'
// '15-Jun-24'

Manipulating DateTime #

DateTime in Dart is immutable — all operations produce a new object, not modify the existing one.

Adding and Subtracting Durations #

DateTime sekarang = DateTime(2024, 6, 15, 14, 30);

// add — add a Duration
DateTime besok = sekarang.add(Duration(days: 1));
DateTime sejamKemudian = sekarang.add(Duration(hours: 1));
DateTime semingguDepan = sekarang.add(Duration(days: 7));
DateTime tigaPuluhMenit = sekarang.add(Duration(minutes: 30));

// subtract — subtract a Duration
DateTime kemarin = sekarang.subtract(Duration(days: 1));
DateTime sejamLalu = sekarang.subtract(Duration(hours: 1));

// Duration can be combined
DateTime nanti = sekarang.add(Duration(
  days: 1,
  hours: 2,
  minutes: 30,
  seconds: 15,
));
// ANTI-PATTERN: adding months with Duration(days: 30) — inaccurate
DateTime bulanDepan = sekarang.add(Duration(days: 30));
// ✗ February only has 28/29 days, July has 31 — the result is inconsistent

// CORRECT: use copyWith for accurate calendar manipulation
DateTime bulanDepanBenar = DateTime(
  sekarang.year,
  sekarang.month + 1,  // Dart automatically rolls over to the next year
  sekarang.day,
  sekarang.hour,
  sekarang.minute,
);
// December + 1 = January of the next year — handled automatically!

copyWith — Changing Specific Components #

Dart 2.6+ introduced copyWith, which lets you create a new DateTime with some components changed:

DateTime asli = DateTime(2024, 6, 15, 14, 30, 45);

// Change only the hour and minute
DateTime jamBaru = asli.copyWith(hour: 9, minute: 0, second: 0);
// 2024-06-15 09:00:00

// Change to the start of the month
DateTime awalBulan = asli.copyWith(day: 1, hour: 0, minute: 0, second: 0);
// 2024-06-01 00:00:00

// Change the year only
DateTime tahunDepan = asli.copyWith(year: asli.year + 1);
// 2025-06-15 14:30:45

Duration — Time Intervals #

Duration represents a length of time — not a point in time. It’s used both as an argument for add/subtract and as the result of difference.

// Creating a Duration
Duration satuHari = Duration(days: 1);
Duration setengahJam = Duration(minutes: 30);
Duration detail = Duration(
  days: 1,
  hours: 2,
  minutes: 30,
  seconds: 15,
  milliseconds: 500,
  microseconds: 250,
);

// Duration properties
Duration d = Duration(hours: 25, minutes: 30, seconds: 45);

print(d.inDays);          // 1 (rounded down)
print(d.inHours);         // 25 (total hours, not just the remaining hours)
print(d.inMinutes);       // 1530 (total minutes)
print(d.inSeconds);       // 91845 (total seconds)
print(d.inMilliseconds);  // 91845000

// For individual components (not totals):
print(d.inHours % 24);    // 1 (remaining hours after days)
print(d.inMinutes % 60);  // 30 (remaining minutes after hours)
print(d.inSeconds % 60);  // 45 (remaining seconds after minutes)
// Operations on Duration
Duration a = Duration(hours: 2);
Duration b = Duration(minutes: 90);

print(a + b);         // 0:03:30.000000 (3 hours 30 minutes)
print(a - b);         // 0:00:30.000000 (30 minutes)
print(a * 2);         // 0:04:00.000000 (4 hours)
print(a > b);         // true (2 hours > 90 minutes)
print(a.compareTo(b)); // positive (a is larger)
print(a.isNegative);   // false

// Negative Duration — valid in Dart
Duration negatif = Duration(hours: -2);
print(negatif.isNegative); // true

Calculating Time Differences #

DateTime mulai = DateTime(2024, 6, 15, 9, 0);
DateTime selesai = DateTime(2024, 6, 15, 17, 30);

Duration kerja = selesai.difference(mulai);

print(kerja.inHours);                    // 8
print(kerja.inMinutes);                  // 510
print('${kerja.inHours}j ${kerja.inMinutes % 60}m'); // '8j 30m'

// Date differences
DateTime tanggalLahir = DateTime(1998, 5, 20);
DateTime sekarang = DateTime.now();
Duration umur = sekarang.difference(tanggalLahir);

print('Age approx: ${umur.inDays ~/ 365} years');

Comparing DateTimes #

DateTime a = DateTime(2024, 6, 15);
DateTime b = DateTime(2024, 6, 20);
DateTime c = DateTime(2024, 6, 15);

// Comparison methods
print(a.isBefore(b));             // true
print(b.isAfter(a));              // true
print(a.isAtSameMomentAs(c));    // true

// Comparison as int with compareTo
print(a.compareTo(b));  // negative (a before b)
print(b.compareTo(a));  // positive (b after a)
print(a.compareTo(c));  // 0 (equal)

// Comparison operators (== only for identical objects, not moments)
// DON'T use == to compare moments in time
print(a == c);                    // may be true if exactly identical, but not reliable
print(a.isAtSameMomentAs(c));    // ✓ the correct way

Checking Time Ranges #

// Is the date within a certain range?
bool dalamRentang(DateTime tanggal, DateTime mulai, DateTime akhir) {
  return !tanggal.isBefore(mulai) && !tanggal.isAfter(akhir);
}

DateTime deadline = DateTime(2024, 12, 31);
DateTime sekarang = DateTime.now();

bool masaBerlaku = dalamRentang(
  sekarang,
  DateTime(2024, 1, 1),
  deadline,
);

// Sorting a list of DateTimes
List<DateTime> tanggal = [DateTime(2024, 3, 1), DateTime(2024, 1, 15), DateTime(2024, 6, 10)];
tanggal.sort((a, b) => a.compareTo(b));  // ascending (earliest first)
tanggal.sort((a, b) => b.compareTo(a));  // descending (newest first)

Calendar Operations #

Some calendar operations are commonly needed but have no built-in method:

// Start and end of day
DateTime awalHari(DateTime dt) =>
    DateTime(dt.year, dt.month, dt.day);

DateTime akhirHari(DateTime dt) =>
    DateTime(dt.year, dt.month, dt.day, 23, 59, 59, 999, 999);

// Start and end of month
DateTime awalBulan(DateTime dt) =>
    DateTime(dt.year, dt.month, 1);

DateTime akhirBulan(DateTime dt) =>
    DateTime(dt.year, dt.month + 1, 0); // Day 0 of the next month = the last day of this month

// End-of-month examples:
print(DateTime(2024, 3, 0));   // 2024-02-29 (because 2024 is a leap year)
print(DateTime(2024, 4, 0));   // 2024-03-31
print(DateTime(2024, 2, 0));   // 2024-01-31

// Number of days in a month
int hariDalamBulan(int tahun, int bulan) {
  return DateTime(tahun, bulan + 1, 0).day;
}
print(hariDalamBulan(2024, 2)); // 29 (leap year)
print(hariDalamBulan(2024, 4)); // 30

// Is it a leap year?
bool tahunKabisat(int tahun) =>
    (tahun % 4 == 0 && tahun % 100 != 0) || (tahun % 400 == 0);

// Next business day (skip Saturday and Sunday)
DateTime hariKerjaBerikutnya(DateTime dt) {
  DateTime besok = dt.add(Duration(days: 1));
  while (besok.weekday == DateTime.saturday || besok.weekday == DateTime.sunday) {
    besok = besok.add(Duration(days: 1));
  }
  return besok;
}

// Start of the week (Monday) from a given date
DateTime awalMinggu(DateTime dt) {
  return dt.subtract(Duration(days: dt.weekday - 1));
}

Formatting DateTime — the intl Package #

DateTime.toString() produces an ISO 8601 format that’s good for machines but not for user display. The intl package provides fully customizable formatting:

dart pub add intl
import 'package:intl/intl.dart';

DateTime dt = DateTime(2024, 11, 15, 14, 30, 45);

// Common formats
print(DateFormat('dd-MM-yyyy').format(dt));        // 15-11-2024
print(DateFormat('yyyy/MM/dd').format(dt));         // 2024/11/15
print(DateFormat('d MMMM yyyy').format(dt));        // 15 November 2024
print(DateFormat('EEEE, d MMMM yyyy').format(dt)); // Jumat, 15 November 2024
print(DateFormat('HH:mm').format(dt));             // 14:30
print(DateFormat('HH:mm:ss').format(dt));          // 14:30:45
print(DateFormat('d MMM yyyy, HH:mm').format(dt)); // 15 Nov 2024, 14:30

// With the Indonesian locale
print(DateFormat('EEEE, d MMMM yyyy', 'id').format(dt)); // Jumat, 15 November 2024
print(DateFormat('d MMM', 'id').format(dt));             // 15 Nov

// Built-in ISO 8601 format
print(dt.toIso8601String());  // 2024-11-15T14:30:45.000
print(dt.toUtc().toIso8601String()); // 2024-11-15T07:30:45.000Z (UTC)

DateFormat Format Symbols #

SymbolMeaningExample
yyyy4-digit year2024
yy2-digit year24
MM2-digit month11
MMMShort month nameNov
MMMMFull month nameNovember
dd2-digit day05
dDay without padding5
EEEEFull day nameFriday
EEEShort day nameFri
HH24-hour hour14
hh12-hour hour02
mmMinutes30
ssSeconds45
aAM/PMPM

Stopwatch — Measuring Execution Duration #

To measure how long code runs, use Stopwatch:

// Dart's built-in Stopwatch
final stopwatch = Stopwatch()..start();

// The code you want to measure
await operasiMahal();

stopwatch.stop();
print('Duration: ${stopwatch.elapsed}');               // 0:00:02.345678
print('Duration ms: ${stopwatch.elapsedMilliseconds}'); // 2345
print('Duration µs: ${stopwatch.elapsedMicroseconds}'); // 2345678

// Reset and restart
stopwatch.reset();
stopwatch.start();

DateTime Anti-Patterns #

Comparing DateTimes with == #

DateTime a = DateTime(2024, 6, 15);
DateTime b = DateTime(2024, 6, 15);

// ANTI-PATTERN: == comparison isn't reliable for DateTimes from different sources
print(a == b); // may be true, but not guaranteed for DateTimes from parse/now

// CORRECT: use isAtSameMomentAs or compareTo == 0
print(a.isAtSameMomentAs(b));  // ✓ always reliable
print(a.compareTo(b) == 0);    // ✓ always reliable

Adding Months with Duration(days: 30) #

DateTime januari31 = DateTime(2024, 1, 31);

// ANTI-PATTERN: using Duration for "add one month"
DateTime salah = januari31.add(Duration(days: 30));
print(salah); // 2024-03-01 — not February 1st!

// CORRECT: use calendar component manipulation
DateTime benar = DateTime(
  januari31.year,
  januari31.month + 1,
  // Clamp to the maximum day in the target month
  januari31.day.clamp(1, hariDalamBulan(januari31.year, januari31.month + 1)),
);
print(benar); // 2024-02-29 (leap year) or 2024-02-28

Storing Time Without a Timezone #

// ANTI-PATTERN: store in an ambiguous format
final transaksi = {
  'waktu': DateTime.now().toString(), // '2024-06-15 07:30:00.000' — which timezone?
};

// CORRECT: always store as ISO 8601 UTC
final transaksi = {
  'waktu': DateTime.now().toUtc().toIso8601String(), // '2024-06-15T00:30:00.000Z'
};

Summary #

  • Local vs UTCDateTime.now() produces the device’s local time. Always store to the database/API in UTC (toUtc().toIso8601String()), convert to local when displaying to users.
  • isAtSameMomentAs, not == — to compare whether two DateTimes represent the same moment, use isAtSameMomentAs(). The == operator isn’t reliable for DateTimes from different sources.
  • tryParse, not parse — for untrusted input (user input, API data), always use tryParse, which returns null, instead of parse, which throws FormatException.
  • copyWith for changing components — the cleanest way to change one or several DateTime components without touching the others.
  • Don’t add months with Duration(days: 30) — use direct calendar component manipulation: DateTime(tahun, bulan + 1, hari). Dart automatically handles month and year roll-over.
  • akhirBulan can be computed with DateTime(tahun, bulan + 1, 0) — day 0 of the next month is the last day of this month.
  • Duration represents a time interval, not a point in time. Properties like inHours return the total hours, not just the hour component — use modulo for individual components.
  • Use Stopwatch to measure code execution duration — more accurate than DateTime.now().difference() because it isn’t affected by system clock changes.
  • The intl package is the only idiomatic way to format dates for human readability. toString() only produces ISO 8601.
  • Always store timestamps as UTC in the database and API — let the presentation layer convert to the user’s local timezone.

← Previous: Map   Next: Regex →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact