Date & Time #
Waktu tampak sederhana sampai kamu mulai bekerja dengannya dalam kode. Timezone, daylight saving time, perbedaan local vs UTC, dan format yang tidak konsisten antar platform adalah sumber bug yang sangat umum — bahkan di aplikasi yang sudah production. DateTime di Dart menyediakan representasi momen dalam waktu, sedangkan Duration mewakili selang waktu antara dua momen. Memahami perbedaan antara local time dan UTC, kapan menggunakan masing-masing, dan bagaimana melakukan kalkulasi tanggal dengan benar adalah keterampilan yang membedakan kode yang andal dari kode yang menghasilkan bug misterius di timezone tertentu.
Membuat DateTime
#
// Tanggal dan waktu spesifik — local timezone device
DateTime ulangTahun = DateTime(1998, 5, 20); // 20 Mei 1998, 00:00:00
DateTime rapat = DateTime(2024, 12, 25, 14, 30); // 25 Des 2024, 14:30:00
DateTime presisi = DateTime(2024, 1, 15, 9, 0, 0, 0, 0); // lengkap: milidetik, mikrodetik
// Waktu saat ini — local timezone
DateTime sekarang = DateTime.now();
// UTC — koordinat waktu universal
DateTime sekarangUtc = DateTime.now().toUtc();
DateTime utcSpesifik = DateTime.utc(2024, 6, 15, 12, 0); // 15 Jun 2024, 12:00 UTC
// Dari Unix timestamp (milidetik sejak 1 Jan 1970 UTC)
DateTime dariMs = DateTime.fromMillisecondsSinceEpoch(1718449200000);
DateTime dariMsUtc = DateTime.fromMillisecondsSinceEpoch(1718449200000, isUtc: true);
// Dari Unix timestamp dalam mikrodetik
DateTime dariUs = DateTime.fromMicrosecondsSinceEpoch(1718449200000000);
Local vs UTC — Jebakan Paling Umum #
Ini adalah sumber bug yang paling sering ditemui ketika bekerja dengan DateTime. DateTime.now() menghasilkan waktu dalam timezone lokal device — ketika aplikasi dijalankan di timezone berbeda, hasilnya beda.
DateTime local = DateTime.now();
DateTime utc = DateTime.now().toUtc();
print(local.isUtc); // false
print(utc.isUtc); // true
// Konversi dua arah
DateTime kembaliLocal = utc.toLocal();
print(utc.isAtSameMomentAs(kembaliLocal)); // true — momen yang sama, representasi berbeda
// Bandingkan dengan benar — selalu konversi ke UTC sebelum compare
DateTime a = DateTime(2024, 6, 15, 7, 0); // local time WIB (UTC+7)
DateTime b = DateTime.utc(2024, 6, 15, 0, 0); // UTC, sama dengan 07:00 WIB
print(a == b); // ✗ false — representasi berbeda
print(a.isAtSameMomentAs(b)); // ✓ true — momen yang sama
// ANTI-PATTERN: menyimpan atau membandingkan DateTime tanpa konsistensi timezone
void simpanTanggal(DateTime tanggal) {
final db = ambilDatabase();
db.simpan({'tanggal': tanggal.toString()}); // ✗ format berbeda untuk local vs UTC
// '2024-06-15 07:00:00.000' (local) vs '2024-06-15 00:00:00.000Z' (utc)
}
// BENAR: selalu konversi ke UTC sebelum menyimpan, konversi balik saat menampilkan
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(); // konversi ke local saat ditampilkan
}
flowchart LR
A["DateTime.now()\n(local device timezone)"] -->|toUtc| B["DateTime UTC\n(universal)"]
B -->|toLocal| A
C["API/Database\n(selalu simpan UTC)"] -->|parse + toLocal| A
A -->|toUtc + toIso8601String| C
Properti DateTime #
DateTime dt = DateTime(2024, 11, 15, 14, 30, 45, 123, 456);
// Komponen tanggal
print(dt.year); // 2024
print(dt.month); // 11 (November)
print(dt.day); // 15
print(dt.weekday); // 5 (Jumat — 1=Senin, 7=Minggu)
// Komponen waktu
print(dt.hour); // 14
print(dt.minute); // 30
print(dt.second); // 45
print(dt.millisecond); // 123
print(dt.microsecond); // 456
// Informasi timezone
print(dt.isUtc); // false (local)
print(dt.timeZoneName); // 'WIB' atau 'Asia/Jakarta' dst
print(dt.timeZoneOffset); // Duration(hours: 7) untuk WIB
// Timestamp
print(dt.millisecondsSinceEpoch); // unix timestamp dalam ms
print(dt.microsecondsSinceEpoch); // unix timestamp dalam µs
// Hari dalam setahun (1-366)
int hariKeberapa = dt.difference(DateTime(dt.year, 1, 1)).inDays + 1;
print(hariKeberapa); // ~320 untuk 15 November
Parsing String ke DateTime #
Parsing adalah operasi yang sering gagal jika input tidak sesuai format. Selalu gunakan tryParse untuk input yang tidak terpercaya:
// parse — throws FormatException jika format salah
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'); // dengan offset
// tryParse — mengembalikan null jika format salah, tidak throw
DateTime? aman = DateTime.tryParse('2024-06-15'); // DateTime
DateTime? gagal = DateTime.tryParse('15/06/2024'); // null — format tidak didukung
DateTime? kosong = DateTime.tryParse('bukan tanggal'); // null
// Tangani null dengan fallback
DateTime tanggalInput = DateTime.tryParse(inputUser) ?? DateTime.now();
// ANTI-PATTERN: parse tanpa penanganan error untuk input user
String input = ambilInputUser();
DateTime tanggal = DateTime.parse(input); // ✗ crash jika format salah
// BENAR: tryParse dengan fallback atau pesan error
String input = ambilInputUser();
final tanggal = DateTime.tryParse(input);
if (tanggal == null) {
tampilkanError('Format tanggal tidak valid. Gunakan format YYYY-MM-DD');
return;
}
// lanjutkan dengan tanggal yang valid
Format yang Didukung parse
#
// Semua format ISO 8601 yang valid
DateTime.parse('2024-06-15'); // tanggal saja
DateTime.parse('2024-06-15 14:30:00'); // dengan waktu
DateTime.parse('2024-06-15T14:30:00'); // dengan T separator
DateTime.parse('2024-06-15T14:30:00.000'); // dengan milidetik
DateTime.parse('2024-06-15T14:30:00.000000'); // dengan mikrodetik
DateTime.parse('2024-06-15T14:30:00Z'); // UTC (Z suffix)
DateTime.parse('2024-06-15T21:30:00+07:00'); // dengan timezone offset
// Format yang TIDAK didukung (perlu intl atau parsing manual)
// '15/06/2024'
// '15 Juni 2024'
// 'June 15, 2024'
// '15-Jun-24'
Manipulasi DateTime #
DateTime di Dart adalah immutable — semua operasi menghasilkan objek baru, bukan memodifikasi yang ada.
Menambah dan Mengurangi Durasi #
DateTime sekarang = DateTime(2024, 6, 15, 14, 30);
// add — tambahkan 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 — kurangi Duration
DateTime kemarin = sekarang.subtract(Duration(days: 1));
DateTime sejamLalu = sekarang.subtract(Duration(hours: 1));
// Duration bisa kombinasi
DateTime nanti = sekarang.add(Duration(
days: 1,
hours: 2,
minutes: 30,
seconds: 15,
));
// ANTI-PATTERN: menambah bulan dengan Duration(days: 30) — tidak akurat
DateTime bulanDepan = sekarang.add(Duration(days: 30));
// ✗ Februari hanya 28/29 hari, Juli ada 31 hari — hasilnya tidak konsisten
// BENAR: gunakan copyWith untuk manipulasi kalender yang akurat
DateTime bulanDepanBenar = DateTime(
sekarang.year,
sekarang.month + 1, // Dart otomatis roll-over ke tahun berikutnya
sekarang.day,
sekarang.hour,
sekarang.minute,
);
// Desember + 1 = Januari tahun berikutnya — ditangani otomatis!
copyWith — Mengubah Komponen Tertentu
#
Dart 2.6+ memperkenalkan copyWith yang memungkinkan membuat DateTime baru dengan beberapa komponen yang berbeda:
DateTime asli = DateTime(2024, 6, 15, 14, 30, 45);
// Ubah hanya jam dan menit
DateTime jamBaru = asli.copyWith(hour: 9, minute: 0, second: 0);
// 2024-06-15 09:00:00
// Ubah ke awal bulan
DateTime awalBulan = asli.copyWith(day: 1, hour: 0, minute: 0, second: 0);
// 2024-06-01 00:00:00
// Ubah tahun saja
DateTime tahunDepan = asli.copyWith(year: asli.year + 1);
// 2025-06-15 14:30:45
Duration — Selang Waktu
#
Duration mewakili panjang waktu — bukan titik dalam waktu. Ia digunakan baik sebagai argumen untuk add/subtract maupun sebagai hasil dari difference.
// Membuat 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,
);
// Properti Duration
Duration d = Duration(hours: 25, minutes: 30, seconds: 45);
print(d.inDays); // 1 (pembulatan ke bawah)
print(d.inHours); // 25 (total jam, bukan hanya jam yang tersisa)
print(d.inMinutes); // 1530 (total menit)
print(d.inSeconds); // 91845 (total detik)
print(d.inMilliseconds); // 91845000
// Untuk komponen individual (bukan total):
print(d.inHours % 24); // 1 (sisa jam setelah hari)
print(d.inMinutes % 60); // 30 (sisa menit setelah jam)
print(d.inSeconds % 60); // 45 (sisa detik setelah menit)
// Operasi pada Duration
Duration a = Duration(hours: 2);
Duration b = Duration(minutes: 90);
print(a + b); // 0:03:30.000000 (3 jam 30 menit)
print(a - b); // 0:00:30.000000 (30 menit)
print(a * 2); // 0:04:00.000000 (4 jam)
print(a > b); // true (2 jam > 90 menit)
print(a.compareTo(b)); // positif (a lebih besar)
print(a.isNegative); // false
// Duration negatif — valid di Dart
Duration negatif = Duration(hours: -2);
print(negatif.isNegative); // true
Menghitung Selisih Waktu #
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'
// Selisih tanggal
DateTime tanggalLahir = DateTime(1998, 5, 20);
DateTime sekarang = DateTime.now();
Duration umur = sekarang.difference(tanggalLahir);
print('Umur kira-kira: ${umur.inDays ~/ 365} tahun');
Perbandingan DateTime #
DateTime a = DateTime(2024, 6, 15);
DateTime b = DateTime(2024, 6, 20);
DateTime c = DateTime(2024, 6, 15);
// Method perbandingan
print(a.isBefore(b)); // true
print(b.isAfter(a)); // true
print(a.isAtSameMomentAs(c)); // true
// Perbandingan sebagai int dengan compareTo
print(a.compareTo(b)); // negatif (a sebelum b)
print(b.compareTo(a)); // positif (b setelah a)
print(a.compareTo(c)); // 0 (sama)
// Operator perbandingan (== hanya untuk objek yang identik, bukan momen)
// JANGAN gunakan == untuk membandingkan momen waktu
print(a == c); // mungkin true jika sama persis, tapi tidak andal
print(a.isAtSameMomentAs(c)); // ✓ cara yang benar
Cek Rentang Waktu #
// Apakah tanggal dalam rentang tertentu?
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 list DateTime
List<DateTime> tanggal = [DateTime(2024, 3, 1), DateTime(2024, 1, 15), DateTime(2024, 6, 10)];
tanggal.sort((a, b) => a.compareTo(b)); // ascending (paling awal dulu)
tanggal.sort((a, b) => b.compareTo(a)); // descending (paling terbaru dulu)
Operasi Kalender #
Beberapa operasi kalender yang umum dibutuhkan tapi tidak ada method bawaannya:
// Awal dan akhir hari
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);
// Awal dan akhir bulan
DateTime awalBulan(DateTime dt) =>
DateTime(dt.year, dt.month, 1);
DateTime akhirBulan(DateTime dt) =>
DateTime(dt.year, dt.month + 1, 0); // Hari ke-0 bulan berikutnya = hari terakhir bulan ini
// Akhir bulan contoh:
print(DateTime(2024, 3, 0)); // 2024-02-29 (karena 2024 kabisat)
print(DateTime(2024, 4, 0)); // 2024-03-31
print(DateTime(2024, 2, 0)); // 2024-01-31
// Jumlah hari dalam bulan
int hariDalamBulan(int tahun, int bulan) {
return DateTime(tahun, bulan + 1, 0).day;
}
print(hariDalamBulan(2024, 2)); // 29 (kabisat)
print(hariDalamBulan(2024, 4)); // 30
// Apakah tahun kabisat?
bool tahunKabisat(int tahun) =>
(tahun % 4 == 0 && tahun % 100 != 0) || (tahun % 400 == 0);
// Hari kerja berikutnya (skip Sabtu dan Minggu)
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;
}
// Awal minggu (Senin) dari tanggal tertentu
DateTime awalMinggu(DateTime dt) {
return dt.subtract(Duration(days: dt.weekday - 1));
}
Format DateTime — Package intl
#
DateTime.toString() menghasilkan format ISO 8601 yang bagus untuk mesin tapi tidak untuk tampilan pengguna. Package intl memberikan format yang sepenuhnya dapat dikustomisasi:
dart pub add intl
import 'package:intl/intl.dart';
DateTime dt = DateTime(2024, 11, 15, 14, 30, 45);
// Format umum
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
// Dengan locale Indonesia
print(DateFormat('EEEE, d MMMM yyyy', 'id').format(dt)); // Jumat, 15 November 2024
print(DateFormat('d MMM', 'id').format(dt)); // 15 Nov
// Format ISO 8601 bawaan
print(dt.toIso8601String()); // 2024-11-15T14:30:45.000
print(dt.toUtc().toIso8601String()); // 2024-11-15T07:30:45.000Z (UTC)
Simbol Format DateFormat
#
| Simbol | Arti | Contoh |
|---|---|---|
yyyy |
Tahun 4 digit | 2024 |
yy |
Tahun 2 digit | 24 |
MM |
Bulan 2 digit | 11 |
MMM |
Nama bulan pendek | Nov |
MMMM |
Nama bulan penuh | November |
dd |
Tanggal 2 digit | 05 |
d |
Tanggal tanpa padding | 5 |
EEEE |
Nama hari penuh | Jumat |
EEE |
Nama hari pendek | Jum |
HH |
Jam 24 jam | 14 |
hh |
Jam 12 jam | 02 |
mm |
Menit | 30 |
ss |
Detik | 45 |
a |
AM/PM | PM |
Stopwatch — Mengukur Durasi Eksekusi #
Untuk mengukur berapa lama kode berjalan, gunakan Stopwatch:
// Stopwatch bawaan Dart
final stopwatch = Stopwatch()..start();
// Kode yang ingin diukur
await operasiMahal();
stopwatch.stop();
print('Durasi: ${stopwatch.elapsed}'); // 0:00:02.345678
print('Durasi ms: ${stopwatch.elapsedMilliseconds}'); // 2345
print('Durasi µs: ${stopwatch.elapsedMicroseconds}'); // 2345678
// Reset dan mulai ulang
stopwatch.reset();
stopwatch.start();
Anti-Pattern DateTime #
Membandingkan DateTime dengan ==
#
DateTime a = DateTime(2024, 6, 15);
DateTime b = DateTime(2024, 6, 15);
// ANTI-PATTERN: perbandingan dengan == tidak andal untuk DateTime dari sumber berbeda
print(a == b); // mungkin true, tapi tidak dijamin untuk DateTime dari parse/now
// BENAR: gunakan isAtSameMomentAs atau compareTo == 0
print(a.isAtSameMomentAs(b)); // ✓ selalu andal
print(a.compareTo(b) == 0); // ✓ selalu andal
Menambah Bulan dengan Duration(days: 30)
#
DateTime januari31 = DateTime(2024, 1, 31);
// ANTI-PATTERN: menggunakan Duration untuk "tambah satu bulan"
DateTime salah = januari31.add(Duration(days: 30));
print(salah); // 2024-03-01 — bukan 1 Februari!
// BENAR: gunakan manipulasi komponen kalender
DateTime benar = DateTime(
januari31.year,
januari31.month + 1,
// Clamp ke hari maksimal di bulan tujuan
januari31.day.clamp(1, hariDalamBulan(januari31.year, januari31.month + 1)),
);
print(benar); // 2024-02-29 (kabisat) atau 2024-02-28
Menyimpan Waktu tanpa Timezone #
// ANTI-PATTERN: simpan sebagai format yang ambiguous
final transaksi = {
'waktu': DateTime.now().toString(), // '2024-06-15 07:30:00.000' — timezone apa?
};
// BENAR: selalu simpan sebagai ISO 8601 UTC
final transaksi = {
'waktu': DateTime.now().toUtc().toIso8601String(), // '2024-06-15T00:30:00.000Z'
};
Ringkasan #
- Local vs UTC —
DateTime.now()menghasilkan waktu lokal device. Selalu simpan ke database/API dalam UTC (toUtc().toIso8601String()), konversi ke local saat menampilkan ke pengguna.isAtSameMomentAsbukan==— untuk membandingkan apakah dua DateTime mewakili momen yang sama, gunakanisAtSameMomentAs(). Operator==tidak andal untuk DateTime dari sumber berbeda.tryParsebukanparse— untuk input yang tidak terpercaya (user input, data API), selalu gunakantryParseyang mengembalikan null daripadaparseyang throwFormatException.copyWithuntuk mengubah komponen — cara paling bersih mengubah satu atau beberapa komponen DateTime tanpa menyentuh yang lain.- Jangan tambah bulan dengan
Duration(days: 30)— gunakan manipulasi komponen kalender langsung:DateTime(tahun, bulan + 1, hari). Dart otomatis menangani roll-over bulan dan tahun.akhirBulanbisa dihitung denganDateTime(tahun, bulan + 1, 0)— hari ke-0 bulan berikutnya adalah hari terakhir bulan ini.Durationmewakili selang waktu, bukan titik dalam waktu. Properti sepertiinHoursmengembalikan total jam, bukan komponen jam saja — gunakan modulo untuk komponen individual.- Gunakan
Stopwatchuntuk mengukur durasi eksekusi kode — lebih akurat dariDateTime.now().difference()karena tidak terpengaruh perubahan system clock.- Package
intladalah satu-satunya cara idiomatis untuk format tanggal yang bisa dibaca manusia.toString()hanya menghasilkan ISO 8601.- Selalu simpan timestamp sebagai UTC di database dan API — biarkan layer presentasi yang mengonversi ke local timezone pengguna.