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();
}

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 than print() for serious logging — it appears in the DevTools Console with metadata (name, level, timestamp, error) that can be filtered and searched.
  • Timeline.startSync/finishSync to mark code blocks you want to profile — they appear in DevTools Timeline as colored blocks showing execution duration.
  • Timeline.timeSync as a practical wrapper — handles start/finish automatically, including on exceptions.
  • TimelineTask for asynchronous operations — unlike startSync/finishSync, which can’t span awaits, a TimelineTask can be started and finished at different times.
  • debugger(when: condition) for programmatic conditional breakpoints — more flexible than IDE breakpoints because it can use complex Dart logic.
  • registerExtension to add custom commands to DevTools — useful for cache control, flag toggling, and exporting state from DevTools without restarting.
  • UserTag makes CPU profiles more readable — replacing meaningless method/class names with labels you define yourself.
  • postEvent for custom events capturable by external monitoring tools — useful for A/B testing, analytics, and tracing.
  • Integration with the logging package lets you route logs to dart:developer and other logging systems (files, remote servers) in a single handler.
  • dart:developer has 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
About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact