← Back to posts Cover image for Demystifying Dart's Underscore: Private Members, Unused Variables, and More

Demystifying Dart's Underscore: Private Members, Unused Variables, and More

· 4 min read
Weekly Digest

The Flutter news you actually need

No spam, ever. Unsubscribe in one click.

Chris
By Chris

If you’ve spent any time reading Dart or Flutter code, you’ve undoubtedly encountered the humble underscore (_). It’s a small character with a surprisingly heavy workload, and its meaning can shift depending on the context. This often leads to confusion: is this variable private? Is it being ignored? Let’s demystify Dart’s underscore once and for all by exploring its three primary roles.

1. The Privacy Enforcer (Library-Level Privacy)

The most significant use of the underscore is to declare library-private members. This is a crucial Dart concept. Unlike languages with class-level privacy (e.g., private in Java), Dart’s privacy operates at the library level. A library is typically a single .dart file.

When you prefix an identifier (like a variable, function, or class name) with an underscore, it becomes accessible only within the same library where it’s defined.

Example: Library Privacy in Action

// File: user_profile.dart
library user_profile;

class UserProfile {
  String name;
  int _age; // Private variable, only visible in user_profile.dart

  UserProfile(this.name, this._age);

  // Public method can use the private member.
  String get ageDescription => 'User is $_age years old';

  // Private helper method.
  void _calculateBirthYear() {
    // Logic here is only accessible within this file.
  }
}

// This function is also library-private.
void _internalHelper() {
  print('This is a private helper.');
}
// File: main.dart
import 'user_profile.dart';

void main() {
  var user = UserProfile('Alice', 30);

  print(user.name); // OK: 'Alice'
  // print(user._age); // COMPILE ERROR: The getter '_age' isn't defined.
  print(user.ageDescription); // OK: Uses the private _age internally.

  // _internalHelper(); // COMPILE ERROR: Not visible here.
}

Key Takeaway: Use the underscore for members that are implementation details of your library. This keeps your public API clean and prevents external code from depending on internal logic that might change.

2. The Intentional Ignorer (Ignoring Unused Parameters)

Often, when defining callbacks for Flutter widgets or working with functions that provide multiple parameters, you don’t need to use all of them. Dart will generate a warning if you declare a parameter but never reference it. The underscore comes to the rescue by signaling, “I know this parameter exists, but I’m intentionally not using it.”

This is common with:

  • forEach loops on Map entries
  • Callbacks like setState
  • Layout builders where you might not need the constraints

Example: Silencing Unused Parameter Warnings

// A map where we only care about the keys.
Map<String, int> scores = {'Math': 95, 'Science': 88};

// We ignore the value parameter.
scores.forEach((subject, _) { // The underscore ignores the 'score' value.
  print('Exam for $subject is complete.');
});

// In a Flutter widget's setState, you often ignore the previous state.
ElevatedButton(
  onPressed: () {
    setState((_) { // We don't need the previous state argument.
      _counter++;
    });
  },
  child: Text('Increment'),
);

Best Practice: Use a single underscore (_) for the first unused parameter. If you have multiple unused parameters, you still need unique identifiers for the compiler, so you can use _, __, ___, or more descriptive names like _unusedContext.

3. The Unnamed Instance (Private Constructors & Singletons)

Sometimes you’ll see a class with a constructor named _. This is simply a named constructor (like ClassName.myConstructor()), but its name is an underscore, making it a private constructor. Since it’s private, it can only be invoked from within its own library. This pattern is the cornerstone of implementing a singleton.

Example: Implementing a Singleton Service

// File: app_logger.dart
class AppLogger {
  // Static instance of the class itself.
  static final AppLogger _instance = AppLogger._internal();

  // Public factory constructor that returns the single instance.
  factory AppLogger() {
    return _instance;
  }

  // Private named constructor. Cannot be called from outside.
  AppLogger._internal() {
    print('Logger initialized.');
  }

  void log(String message) => print('[LOG] $message');
}
// File: main.dart
import 'app_logger.dart';

void main() {
  var logger1 = AppLogger();
  var logger2 = AppLogger();

  logger1.log('Hello from logger1');
  logger2.log('Hello from logger2');

  print(identical(logger1, logger2)); // true: They are the same object.
  // AppLogger._internal(); // COMPILE ERROR: Private constructor.
}

Common Pitfalls to Avoid

  1. False Sense of Security: Remember, privacy is library-level, not class-level. Any other class in the same file can access a private member. Organize your code into separate files/libraries to enforce proper encapsulation.
  2. Overusing Privacy in Small Apps: In a simple, single-file Flutter widget demo, making everything private is unnecessary and can hinder readability. Use it judiciously as your codebase grows.
  3. Confusing Context: Always look at where the underscore is used. _variable (declaration) means private. (_, value) (parameter) means ignored. ClassName._() (constructor) is a private constructor.

By understanding these distinct roles—privacy modifier, parameter ignorer, and private constructor enabler—you can write clearer, more intentional Dart code. The underscore stops being a cryptic symbol and becomes a precise tool for managing visibility, signaling intent, and controlling object creation.

This blog is produced with the assistance of AI by a human editor. Learn more

Related Posts

Cover image for Localizing Dynamic Content in Flutter: A Guide to Backend-Driven Translations

Localizing Dynamic Content in Flutter: A Guide to Backend-Driven Translations

Many Flutter apps need to display content that changes based on user locale, but also comes from a backend (like Firebase). This post will explore best practices for fetching and integrating dynamic, localized content from a backend, ensuring a seamless user experience across different languages and regions without hardcoding translations.

Cover image for Unraveling Type Mismatch Errors in Flutter: A Guide to 'X can't be assigned to Y' and '_InternalLinkedHashMap' Issues

Unraveling Type Mismatch Errors in Flutter: A Guide to 'X can't be assigned to Y' and '_InternalLinkedHashMap' Issues

Developers frequently encounter cryptic type mismatch errors like 'The argument type X can't be assigned to the parameter type Y' or '_InternalLinkedHashMap has no instance method 'cast''. This post will demystify these common Flutter/Dart type errors, explain their root causes (e.g., conflicting imports, dynamic typing pitfalls, JSON deserialization issues), and provide practical solutions to diagnose and fix them, improving code robustness and reducing debugging time.