Skip to content

Flutter / Dart cheat sheet

This is a short reference for common Dart and Flutter basics. It focuses on the patterns you will use often in a small or medium Flutter project, with examples that match typical app code rather than toy syntax.

this. in Dart

In Dart, this. is usually only needed when it improves clarity. The two common cases are constructor field initialization and name conflicts between a field and a local variable or parameter.

Use this. in constructor parameters

class User {
  final String name;
  final int age;

  User({required this.name, required this.age});
}

This shorthand assigns the constructor arguments directly to the instance fields. It is the most common and most idiomatic use of this. in Dart.

Use this. when a parameter shadows a field

class User {
  String name;

  User(this.name);

  void rename(String name) {
    this.name = name;
  }
}

In rename, name refers to the method parameter, while this.name refers to the field stored on the object. Use this. here to make the distinction explicit.

Skip this. when it adds no value

print(name);

When there is no naming conflict, this.name is usually unnecessary noise. Dart code normally omits it unless it helps disambiguate something.

Rule of thumb

  • Use this. in constructor parameter shorthand.
  • Use this. when a local name conflicts with a field name.
  • Otherwise, prefer the shorter field access.

Leading underscore _name

In Dart, a leading underscore makes a declaration private to its library. In practice, that usually means private to the current file unless you are using part files.

int _counter = 0;

void _loadData() {}

class _BenchmarkPageState extends State<BenchmarkPage> {}

These names can be used inside the same library, but not imported from another file. This is Dart’s normal way to mark implementation details as internal.

Good uses for _

Use a leading underscore for code that should stay local to one file, such as:

  • helper methods used only in that file
  • private widget state classes
  • file-local variables
  • internal widgets that are not reused elsewhere

When not to use _

Keep a declaration public if other files should depend on it. That usually includes shared model classes, reusable widgets, and services or loaders used across features.

Rule of thumb

If another file should use it, do not prefix it with _. If it is an internal detail of one file, prefix it with _.


Interfaces in Dart

Dart does not use a separate interface keyword the way Java or C# often does. Instead, every class defines an interface, and abstract classes are the normal way to express an explicit contract.

Every class exposes an interface

class Animal {
  void speak() {}
}

class Dog implements Animal {
  @override
  void speak() {
    print('woof');
  }
}

This example shows that Dog can implement the interface defined by Animal. The important detail is that implements gives you the contract, not the code.

extends reuses behavior

class BaseLoader {
  void log() {
    print('loading');
  }
}

class AssetLoader extends BaseLoader {}

Use extends when you want to inherit existing behavior. AssetLoader gets the log method automatically because it reuses the implementation from BaseLoader.

implements defines a contract

class BenchLoader {
  Future<void> load() async {}
}

class AssetBenchLoader implements BenchLoader {
  @override
  Future<void> load() async {
    // custom implementation
  }
}

Use implements when you want a type contract without inheriting behavior. Once you implement a class, you must provide your own implementation for its members.

Prefer abstract classes for explicit interfaces

abstract class BenchLoader {
  Future<BenchFile> load();
}

class AssetBenchLoader implements BenchLoader {
  @override
  Future<BenchFile> load() async {
    // load asset
  }
}

This is the most common explicit-interface style in Dart codebases. The abstract class clearly communicates that callers depend on a contract, while concrete implementations decide how the work happens.

Rule of thumb

  • Use extends when you want shared behavior.
  • Use implements when you want a contract without inherited code.
  • Use an abstract class when you want the contract to be explicit and readable.

Project structure in Flutter

There is no single perfect folder layout for every Flutter app. The right choice depends mostly on project size and how many features are evolving at once.

Small app

lib/
  main.dart
  benchmark_page.dart
  benchmark_painter.dart
  bench_model.dart

This is fine for a spike, a prototype, or a very small tool. The main goal at this stage is to keep navigation simple and avoid structure that pretends the app is larger than it is.

Growing app

lib/
  main.dart
  app/
    app.dart
    theme.dart
  features/
    benchmark/
      benchmark_page.dart
      benchmark_painter.dart
      bench_model.dart
      bench_loader.dart

Feature-first grouping is usually the easiest layout once the project starts growing. Related files stay together, which makes the codebase easier to navigate and reduces the need for giant global folders.

Layer-first structure can work, but gets messy faster

lib/
  models/
  pages/
  services/
  widgets/

This layout can feel clean at first, but it often spreads one feature across too many folders. As the app grows, you spend more time jumping between directories to understand one screen or flow.

Rule of thumb

  • For a small app, keep the structure simple.
  • For a growing app, prefer feature-first grouping.
  • Avoid introducing architecture that the current app size does not need yet.

Practical Flutter habits

Most Flutter code stays easier to maintain when responsibilities are obvious. The goal is not perfect architecture. The goal is to keep state, rendering, loading, and models easy to find and reason about.

Keep widgets small and obvious

If a widget is doing too many jobs, split it along real responsibilities. A common split is:

  • the page owns state and layout
  • the painter draws
  • the model parses and stores typed data

This makes each class easier to test, review, and change without dragging unrelated logic with it.

Keep state close to where it is used

Do not introduce app-wide state management just because it exists. If state only matters to one page or one local flow, local widget state is often the simplest and best choice.

Prefer typed models over raw maps

BenchFile -> List<BenchObject> -> painter

Passing typed objects through the app is easier to read and safer to refactor than moving Map<String, dynamic> around everywhere. Types also make parsing, validation, and rendering responsibilities clearer.

Separate loading, state, and drawing

For a benchmark-style screen, a clean split looks like this:

  • the loader reads JSON
  • the page stores UI state
  • the painter draws shapes

Avoid making the painter load files or parse data. Drawing code is easier to maintain when it only draws.

Make nullability explicit

BenchFile?

If a value can be absent, model that directly with a nullable type. Only use ! when you are genuinely certain the value is non-null at that point in the code.

Prefer clear code over clever code

final paint = Paint();
paint.color = Colors.red;
paint.style = PaintingStyle.fill;

This is completely fine if it is easier to read than a more compact style. In Flutter code, straightforward setup is usually better than clever chaining when clarity matters more than brevity.

Keep async work out of build()

Do not start file loading or heavy work directly inside build(). Use lifecycle methods such as initState(), keep the async work in a separate method, and update the UI with setState() when the result is ready.

Keep build() declarative

A good build() method mostly decides what to show for the current state. For example, it might choose between loading UI, an error message, or the final canvas view.

Keep one main responsibility per class

Names should match responsibilities. A class such as BenchmarkPage should own page state and layout, BenchmarkPainter should focus on drawing, and BenchFile or BenchObject should represent parsed data.

Do not over-package early

A small learning app usually does not need many dependencies or layers. Start with the Flutter SDK and a few clear files, then add libraries only when they solve a real problem you already have.


Quick examples

These are short examples of shapes you will see often in real Flutter and Dart code.

Constructor shorthand

class BenchObject {
  final String id;

  BenchObject(this.id);
}

This shows the compact constructor form when all you need is to assign an argument to a field.

File-private helper

Future<void> _loadBenchFile() async {}

The leading underscore keeps this helper private to the current library, which is a good fit when no other file should call it.

Abstract interface

abstract class BenchLoader {
  Future<BenchFile> load();
}

This defines the contract without choosing an implementation yet. It is useful when multiple loaders could exist later.

Public shared model

class BenchFile {
  final List<BenchObject> objects;

  const BenchFile({required this.objects});
}

This stays public because shared model types are often used across files, not hidden as local implementation details.


Summary

Most Dart and Flutter basics become easier once you keep a few simple distinctions in mind. Use this. only when it helps, use _ for file-local details, prefer abstract classes for explicit contracts, keep project structure proportional to app size, and separate state, loading, models, and drawing so each class has a clear job.