Developer #
dart:developer is Dart’s built-in library providing debugging and profiling tools integrated directly with Dart DevTools. Unlike print(), which only outputs text to the console, dart:developer enables structured logging with levels and zones, marking code blocks for timeline profiling, triggering breakpoints programmatically, inspecting objects in the debugger, and even adding new commands to DevTools through service extensions. This library is what professional Dart developers use to understand the performance and behavior of their applications.
An Overview of dart:developer
#
flowchart LR
DD["dart:developer"] --> LOG["log()\nStructured logging\nto the DevTools Console"]
DD --> TL["Timeline\nPerformance profiling\nin DevTools Timeline"]
DD --> DBG["debugger()\nTrigger breakpoints\nprogrammatically"]
DD --> INSP["inspect()\nHighlight objects\nin DevTools Inspector"]
DD --> EXT["registerExtension()\nAdd commands\nto DevTools"]
DD --> EVT["postEvent()\nSend custom events\nto DevTools"]
DD --> TAG["UserTag\nLabel code regions\nfor the profiler"]log() — Structured Logging
#
log() from dart:developer is more powerful than print() because messages appear in the DevTools Console with metadata: time, zone, severity level, and can be filtered:
import 'dart:developer';
void main() {
// Basic log — a message with a logger name
log('Aplikasi dimulai', name: 'main');
// log with levels — uses constants from the logging package
// Levels: ALL=0, FINEST=300, FINER=400, FINE=500, CONFIG=700,
// INFO=800, WARNING=900, SEVERE=1000, SHOUT=1200, OFF=2000
log('Regular information', name: 'auth', level: 800); // INFO
log('Warning!', name: 'db', level: 900); // WARNING
log('Critical error!', name: 'network', level: 1000); // SEVERE
// log with an error and stack trace
try {
throw Exception('Connection failed');
} catch (e, stackTrace) {
log(
'Failed to connect to the database',
name: 'database',
level: 1000, // SEVERE
error: e,
stackTrace: stackTrace,
);
}
// log with a zone — determines the execution context
log('Message from a special zone', name: 'app', zone: Zone.current);
// log with sequence numbers — for manual ordering
log('First event', name: 'stream', sequenceNumber: 1);
log('Second event', name: 'stream', sequenceNumber: 2);
log('Third event', name: 'stream', sequenceNumber: 3);
// log with an explicit time
log(
'Event with a custom timestamp',
name: 'audit',
time: DateTime.now(),
);
}
Integration with the logging Package
#
dart:developer’s log() works very well together with the logging package for more structured logging:
import 'dart:developer' as developer;
import 'package:logging/logging.dart';
// Set up once in main or app configuration
void setupLogging() {
Logger.root.level = Level.ALL;
Logger.root.onRecord.listen((record) {
// Forward all logs to dart:developer
developer.log(
record.message,
name: record.loggerName,
level: record.level.value,
error: record.error,
stackTrace: record.stackTrace,
time: record.time,
);
// Also print to the console for development
if (record.level >= Level.WARNING) {
print('[${record.level.name}] ${record.loggerName}: ${record.message}');
if (record.error != null) print(' Error: ${record.error}');
}
});
}
// Usage throughout the app
final _log = Logger('ProdukService');
class ProdukService {
Future<List<Produk>> ambilSemua() async {
_log.info('Fetching all products');
try {
final data = await _api.get('/produk');
_log.fine('Successfully fetched ${data.length} products');
return data;
} catch (e, st) {
_log.severe('Failed to fetch products', e, st);
rethrow;
}
}
}
Timeline — Performance Profiling
#
Timeline lets you mark code blocks with labels that appear in DevTools Timeline — helping identify slow code sections:
import 'dart:developer';
Future<void> main() async {
// Way 1: startSync / finishSync — for synchronous code
Timeline.startSync('Parsing JSON');
final data = parseJson(rawJson);
Timeline.finishSync();
// Way 2: TimelineTask for asynchronous code
final task = TimelineTask();
task.start('HTTP Request', arguments: {'url': '/api/produk'});
try {
final response = await http.get(Uri.parse('/api/produk'));
task.finish(arguments: {'statusCode': response.statusCode});
} catch (e) {
task.finish(arguments: {'error': e.toString()});
}
// Way 3: timeSync — a practical wrapper for synchronous functions
final hasil = Timeline.timeSync(
'Sorting 10000 items',
() {
final list = List.generate(10000, (i) => 10000 - i);
list.sort();
return list;
},
arguments: {'ukuran': 10000},
);
// arguments — additional metadata shown in DevTools
Timeline.startSync(
'Database Query',
arguments: {
'query': 'SELECT * FROM produk WHERE aktif = true',
'estimatedRows': 500,
},
);
final rows = await database.query('produk');
Timeline.finishSync();
}
Automatically Profiling Functions #
import 'dart:developer';
// A wrapper for automatic profiling
T profileSync<T>(String nama, T Function() fungsi, {Map<String, dynamic>? args}) {
return Timeline.timeSync(nama, fungsi, arguments: args);
}
Future<T> profileAsync<T>(String nama, Future<T> Function() fungsi, {Map<String, dynamic>? args}) async {
final task = TimelineTask();
task.start(nama, arguments: args);
try {
final hasil = await fungsi();
task.finish(arguments: {'sukses': true});
return hasil;
} catch (e) {
task.finish(arguments: {'sukses': false, 'error': e.toString()});
rethrow;
}
}
// Usage
Future<void> jalankanAplikasi() async {
final produk = await profileAsync(
'Muat Produk',
() => service.ambilProduk(),
args: {'source': 'API'},
);
final terfilter = profileSync(
'Filter Produk',
() => produk.where((p) => p.harga > 100000).toList(),
args: {'kriteria': 'harga > 100000'},
);
print('Filtered products: ${terfilter.length}');
}
debugger() — Programmatic Breakpoints
#
debugger() triggers a breakpoint in the connected debugger — very useful for stopping at specific conditions without setting manual breakpoints in the IDE:
import 'dart:developer';
void prosesData(List<dynamic> data) {
for (int i = 0; i < data.length; i++) {
final item = data[i];
// Stop in the debugger only under specific conditions
// when: false = don't stop (skip the breakpoint)
debugger(
when: item == null, // only stop if the item is null
message: 'Null item found at index $i',
);
if (item != null) prosesItem(item);
}
}
// Useful for debugging hard-to-reproduce conditions
Future<void> prosesPembayaran(Map<String, dynamic> data) async {
final jumlah = data['jumlah'] as double?;
// Stop if the amount doesn't make sense
debugger(
when: jumlah != null && jumlah > 1000000000,
message: 'Very large payment amount: $jumlah',
);
await prosesTransaksi(data);
}
debugger() does nothing if no debugger is connected — it’s safe in production code, but it’s generally better to remove it after debugging.inspect() — Highlighting in the Inspector
#
inspect() sends objects to the DevTools Objects Inspector for in-depth examination:
import 'dart:developer';
class Pengguna {
final String id;
final String nama;
final String email;
final List<String> peran;
Pengguna({required this.id, required this.nama,
required this.email, required this.peran});
}
void main() {
final pengguna = Pengguna(
id: 'U001',
nama: 'Budi Santoso',
email: '[email protected]',
peran: ['admin', 'editor'],
);
// inspect() causes the object to appear in the DevTools Inspector
// useful for examining the state of complex objects
inspect(pengguna);
// Can be used with collections too
final semuaPengguna = [pengguna, pengguna];
inspect(semuaPengguna);
}
postEvent() — Custom Events to DevTools
#
postEvent() sends custom events that can be captured by DevTools or other service extensions:
import 'dart:developer';
// Send a custom event with data
void lacakAksi(String aksi, Map<String, dynamic> data) {
postEvent('ActionTracked', {
'aksi': aksi,
'timestamp': DateTime.now().toIso8601String(),
...data,
});
}
// Usage
void main() {
lacakAksi('login', {'userId': 'U001', 'platform': 'mobile'});
lacakAksi('purchase', {'productId': 'P123', 'amount': 150000});
// Events can be monitored via the Observatory/DevTools VM service
}
registerExtension() — Adding Commands to DevTools
#
Service extensions let you add custom commands that can be invoked from DevTools or external tools via the VM Service Protocol:
import 'dart:developer';
import 'dart:convert';
void main() {
// Register a service extension
registerExtension('ext.myApp.clearCache', (method, params) async {
// Clear the cache
await _cacheService.clear();
return ServiceExtensionResponse.result(
jsonEncode({'status': 'cache cleared', 'timestamp': DateTime.now().toIso8601String()}),
);
});
registerExtension('ext.myApp.getStats', (method, params) async {
final stats = {
'cacheSize': _cacheService.ukuran,
'activeConnections': _connectionPool.aktif,
'requestCount': _metrics.totalRequest,
'errorRate': _metrics.errorRate,
};
return ServiceExtensionResponse.result(jsonEncode(stats));
});
// An extension with parameters
registerExtension('ext.myApp.setLogLevel', (method, params) async {
final level = params['level'] ?? 'INFO';
Logger.root.level = Level.LEVELS.firstWhere(
(l) => l.name == level,
orElse: () => Level.INFO,
);
return ServiceExtensionResponse.result(
jsonEncode({'level': Logger.root.level.name}),
);
});
// Invoke via curl or DevTools:
// curl 'http://localhost:8181/ext.myApp.getStats'
}
UserTag — Labeling Code Regions for the Profiler
#
UserTag marks code regions that appear in the CPU profiler with meaningful labels, replacing hard-to-read stack frame names:
import 'dart:developer';
// Create UserTags — usually once at startup
final tagParsing = UserTag('JSON Parsing');
final tagRendering = UserTag('UI Rendering');
final tagNetwork = UserTag('Network I/O');
// Use tags to mark code regions
Future<void> muatHalaman() async {
// Mark while fetching data
tagNetwork.makeCurrent();
final jsonData = await fetchDataFromApi();
// Mark while parsing
tagParsing.makeCurrent();
final objects = parseJsonToObjects(jsonData);
// Mark while rendering
tagRendering.makeCurrent();
renderToScreen(objects);
// Return to the default
UserTag.defaultTag.makeCurrent();
}
Monitoring Performance with Timeline + log #
The combination of Timeline and log provides comprehensive observability:
import 'dart:developer';
class PerformanceMonitor {
static final Map<String, int> _hitungPanggilan = {};
static final Map<String, Duration> _totalDurasi = {};
static T ukur<T>(String nama, T Function() fungsi) {
_hitungPanggilan[nama] = (_hitungPanggilan[nama] ?? 0) + 1;
final mulai = DateTime.now();
late T hasil;
try {
hasil = Timeline.timeSync(nama, fungsi);
} finally {
final durasi = DateTime.now().difference(mulai);
_totalDurasi[nama] = (_totalDurasi[nama] ?? Duration.zero) + durasi;
// Log if slow (> 16ms = jank at 60fps)
if (durasi.inMilliseconds > 16) {
log(
'$nama takes ${durasi.inMilliseconds}ms (SLOW)',
name: 'PerformanceMonitor',
level: 900, // WARNING
);
}
}
return hasil;
}
static void laporan() {
log('=== Performance Report ===', name: 'PerformanceMonitor', level: 800);
for (final entry in _totalDurasi.entries) {
final nama = entry.key;
final total = entry.value;
final panggilan = _hitungPanggilan[nama] ?? 1;
final rataRata = total.inMicroseconds ~/ panggilan;
log(
'$nama: called ${panggilan}x, average ${rataRata}µs',
name: 'PerformanceMonitor',
level: 800,
);
}
}
}
// Usage
void main() {
final data = PerformanceMonitor.ukur('ParseJSON', () => parseJson(rawData));
final sorted = PerformanceMonitor.ukur('Sort', () => data..sort());
PerformanceMonitor.laporan();
}
print vs log
#
import 'dart:developer';
// print — output to stdout, no metadata, no filtering
print('Debug message'); // always appears, no context
// log — output to the DevTools Console, with metadata
log(
'Debug message',
name: 'ComponentName', // logger name — filterable in DevTools
level: 500, // level — filterable
time: DateTime.now(), // timestamp
);
// In production:
// print() still appears in device logs (Android logcat, iOS Console)
// log() is only visible in DevTools when a debug session is active
// → cleaner for deployment
dart:developer Anti-Patterns
#
Using print for Production Logging
#
// ANTI-PATTERN: print for all logging
void prosesOrder(Order order) {
print('Processing order: ${order.id}'); // ✗ can't be filtered
print('Total: ${order.total}'); // ✗ no level/name
print('Error: failed to send email'); // ✗ can't be distinguished from info
}
// CORRECT: structured logging with names and levels
final _log = Logger('OrderService');
void prosesOrder(Order order) {
log('Processing order: ${order.id}', name: 'OrderService', level: 800);
log('Total: ${order.total}', name: 'OrderService', level: 500);
if (gagalKirimEmail) {
log('Failed to send email', name: 'OrderService', level: 900); // WARNING
}
}
Leaving debugger() in Production Code
#
// ANTI-PATTERN: debugger() left behind in production code
void prosesData(List data) {
debugger(when: data.isEmpty); // ✗ should be removed after debugging
// Not harmful if no debugger is connected,
// but it makes the code dirty and could be unwanted
}
// CORRECT: remove after debugging, or use assert for invariants
void prosesData(List data) {
assert(data.isNotEmpty, 'Data cannot be empty'); // ✓ more appropriate
// debugger removed
}
Summary #
log()is better thanprint()for serious logging — it appears in the DevTools Console with metadata (name, level, timestamp, error) that can be filtered and searched.Timeline.startSync/finishSyncto mark code blocks you want to profile — they appear in DevTools Timeline as colored blocks showing execution duration.Timeline.timeSyncas a practical wrapper — handles start/finish automatically, including on exceptions.TimelineTaskfor asynchronous operations — unlikestartSync/finishSync, which can’t span awaits, aTimelineTaskcan bestarted andfinished at different times.debugger(when: condition)for programmatic conditional breakpoints — more flexible than IDE breakpoints because it can use complex Dart logic.registerExtensionto add custom commands to DevTools — useful for cache control, flag toggling, and exporting state from DevTools without restarting.UserTagmakes CPU profiles more readable — replacing meaningless method/class names with labels you define yourself.postEventfor custom events capturable by external monitoring tools — useful for A/B testing, analytics, and tracing.- Integration with the
loggingpackage lets you route logs todart:developerand other logging systems (files, remote servers) in a single handler.dart:developerhas no impact in production when no DevTools/debugger is connected — safe to use without worrying about overhead in release builds (most functions become no-ops).
← Previous: Isolate