Dart in a Flutter App: Where Your Code Lives
In Flutter, you write most of your app logic in Dart files. Understanding the default project structure helps you find where to place UI code, models, and utilities.
Key folders and files you will use
- lib/: Your main Dart source code. Most of your work happens here.
- lib/main.dart: The entry point that calls
runApp(...). - test/: Unit/widget tests (optional early on, but good to know).
- pubspec.yaml: Dependencies and assets configuration (you will edit this when adding packages or images).
A common beginner-friendly organization inside lib/ looks like this:
lib/ main.dart models/ user.dart todo.dart screens/ home_screen.dart widgets/ todo_tile.dart services/ api_client.dart utils/ formatters.dartFlutter does not force a folder structure, but separating models, screens, and reusable widgets makes code easier to navigate.
Exercise
- Create folders in
lib/:models,screens,widgets. - Create an empty file
lib/models/todo.dart. You will fill it in later.
Variables and Types (What You See Everywhere in Flutter)
Dart is strongly typed. You can write explicit types or let Dart infer them with var. In Flutter code, you will constantly define values for widget properties (colors, strings, callbacks), so being comfortable with types matters.
Basic variable declarations
void main() { String title = 'My App'; int counter = 0; double price = 9.99; bool isLoading = false; var inferred = 'Dart infers this as String'; final createdOnce = DateTime.now(); const compileTime = 16;}Use final when a variable is assigned once (very common in Flutter widgets). Use const for compile-time constants (also common for constant widgets and values).
- Listen to the audio with the screen off.
- Earn a certificate upon completion.
- Over 5000 courses for you to explore!
Download the app
Flutter-style example: values for widget parameters
final String screenTitle = 'Home';final int maxItems = 20;const double padding = 16.0;Exercise
- Declare a
finalvariable namedusernamewith your name. - Declare a
constvariable nameddefaultPaddingset to12.0. - Change an
intvariablecountfrom0to1. Then try the same with afinalvariable and observe the error.
Null Safety (Handling Missing Values Safely)
Dart uses null safety to prevent common runtime crashes. A variable is either non-nullable (cannot be null) or nullable (can be null) depending on whether it has a ?.
Nullable vs non-nullable
String name = 'Ava';String? middleName; // can be nullIf a value might be absent (for example, optional API fields), make it nullable.
Common operators you will use in Flutter code
- Null-aware access
?.: call only if not null - Default value
??: use fallback if null - Null assertion
!: tell Dart “I’m sure it’s not null” (use carefully)
String? subtitle;final display = subtitle ?? 'No subtitle';final length = subtitle?.length; // int? because subtitle may be nullIn Flutter UI, ?? is frequently used to provide safe defaults for text or numbers.
Step-by-step: safely format a nullable value
Imagine you want to show a user’s bio, but it may be missing.
String formatBio(String? bio) { final trimmed = bio?.trim(); if (trimmed == null || trimmed.isEmpty) { return 'No bio yet'; } return trimmed;}Exercise
- Create a function
String displayName(String? name)that returns'Guest'whennameis null or empty, otherwise returns the trimmed name. - Given
String? city;, create a non-nullableStringvariablecityLabelthat becomes'Unknown city'whencityis null.
Functions (Including Named Parameters Used by Widgets)
Functions in Dart can take positional parameters, named parameters, optional parameters, and return values. Flutter APIs heavily use named parameters because they make widget code readable.
Positional parameters
int add(int a, int b) { return a + b;}Named parameters (very common in Flutter)
String greet({required String name, String prefix = 'Hi'}) { return '$prefix, $name';}Named parameters go inside {}. Mark them required when callers must provide them. Provide defaults for optional named parameters.
Step-by-step: create a helper similar to Flutter styling helpers
1) Define a function with named parameters. 2) Provide defaults. 3) Return a computed string.
String formatCount({required int count, String label = 'items'}) { if (count == 1) return '1 $label'; return '$count $label';}Exercise
- Write
String formatPrice({required double amount, String currency = 'USD'})that returns something like'USD 9.99'. - Write
bool isValidEmail(String value)that returnstrueif it contains'@'and'.'.
Arrow Syntax and Anonymous Functions (Callbacks)
Flutter uses callbacks everywhere (for example, button presses). Dart supports concise arrow functions and anonymous functions.
Arrow function
int square(int x) => x * x;Anonymous function (often passed as a callback)
final items = [1, 2, 3];final doubled = items.map((n) => n * 2).toList();Exercise
- Given
final names = ['a', 'be', 'see'];create a list of lengths usingmap. - Create a function variable
final onTap = () { print('tapped'); };and call it.
Lists and Maps (Collections You’ll Use for UI and JSON)
Lists represent ordered collections (like items in a list UI). Maps represent key-value pairs (like JSON objects).
Lists
final List<String> tags = ['flutter', 'dart'];tags.add('widgets');final first = tags[0];Maps
final Map<String, dynamic> json = { 'id': 1, 'title': 'Buy milk', 'done': false,};final title = json['title'];When dealing with JSON-like data, you often use Map<String, dynamic> because values can be different types.
Step-by-step: filter and transform a list like you would for a UI list
1) Start with raw items. 2) Filter. 3) Map to display strings.
final todos = [ {'title': 'Buy milk', 'done': false}, {'title': 'Read', 'done': true},];final pendingTitles = todos .where((t) => t['done'] == false) .map((t) => t['title'] as String) .toList();Exercise
- Create a list of numbers
[1,2,3,4,5]and produce a new list containing only even numbers. - Create a map representing a user with keys
id,name,email. Read thenamevalue into aStringvariable.
Classes and Objects (Modeling App Data)
Flutter apps often define model classes to represent data (User, Todo, Product). A class groups fields and behavior. You will frequently pass model objects into widgets.
A simple model class
class Todo { final int id; final String title; final bool done; Todo(this.id, this.title, this.done);}This uses a positional constructor. In Flutter code, named parameters are often preferred for readability.
Named-parameter constructor (recommended for models)
class Todo { final int id; final String title; final bool done; const Todo({required this.id, required this.title, this.done = false});}this.done = false provides a default value. Marking the constructor const can be useful when all fields are immutable and values are known at compile time.
Step-by-step: create a model file in your Flutter project
1) Open lib/models/todo.dart. 2) Add the class. 3) Import and use it from another file when needed.
// lib/models/todo.dartclass Todo { final int id; final String title; final bool done; const Todo({required this.id, required this.title, this.done = false});}Exercise
- Create a
Userclass with fieldsid(int),name(String), andemail(String?). Use a named-parameter constructor whereemailis optional. - Create an instance:
User(id: 1, name: 'Sam')and ensure it compiles.
Factory Constructors and JSON Parsing (Common in Real Apps)
Apps often receive JSON from APIs. A common pattern is a factory constructor that creates an instance from a map.
Todo model with fromJson
class Todo { final int id; final String title; final bool done; const Todo({required this.id, required this.title, required this.done}); factory Todo.fromJson(Map<String, dynamic> json) { return Todo( id: json['id'] as int, title: json['title'] as String, done: json['done'] as bool, ); } Map<String, dynamic> toJson() { return {'id': id, 'title': title, 'done': done}; }}The as casts help Dart understand the types. In production code, you may add validation, but this is a solid starting point.
Exercise
- Create a map
{'id': 2, 'title': 'Walk', 'done': false}and build aTodousingTodo.fromJson. - Call
toJson()on aTodoand verify the resulting map contains the same values.
Working with Nullable Fields in Models
Some API fields are optional. Use nullable types in your model and handle them safely when reading JSON.
Example: optional description
class Todo { final int id; final String title; final String? description; final bool done; const Todo({required this.id, required this.title, this.description, required this.done}); factory Todo.fromJson(Map<String, dynamic> json) { return Todo( id: json['id'] as int, title: json['title'] as String, description: json['description'] as String?, done: json['done'] as bool, ); }}When you display description in UI, use ?? to provide a fallback.
String descriptionLabel(String? description) { return description?.trim().isNotEmpty == true ? description!.trim() : 'No description';}Exercise
- Add
String? phoneto yourUsermodel and parse it from JSON usingas String?. - Write a function that returns
'No phone'when the phone is null or empty.
Async Basics (Future, async/await) for Loading Data
Flutter apps frequently load data (from disk, network, or databases). Dart represents a value that will be available later with a Future. Use async and await to write asynchronous code that reads like synchronous code.
A simple async function
Future<String> fetchGreeting() async { await Future.delayed(Duration(milliseconds: 300)); return 'Hello';}Using await
Future<void> main() async { final greeting = await fetchGreeting(); print(greeting);}In Flutter, you often call async functions from event handlers or during initialization, then update UI state.
Step-by-step: simulate loading a list of todos
1) Return a Future<List<Todo>>. 2) Delay to simulate network. 3) Convert maps to model objects.
Future<List<Todo>> loadTodos() async { await Future.delayed(Duration(milliseconds: 500)); final data = [ {'id': 1, 'title': 'Buy milk', 'done': false}, {'id': 2, 'title': 'Read', 'done': true}, ]; return data.map((m) => Todo.fromJson(m)).toList();}Exercise
- Write
Future<int> fetchCount()that waits 200ms and returns42. - Write
Future<List<String>> loadNames()that returns a list after a delay.
Putting It Together: A Flutter-Style Data Flow (Model + Helper + Async)
The following snippet mirrors a common Flutter pattern: define a model, load data asynchronously, and format values safely for display.
class User { final int id; final String name; final String? email; const User({required this.id, required this.name, this.email}); factory User.fromJson(Map<String, dynamic> json) { return User( id: json['id'] as int, name: json['name'] as String, email: json['email'] as String?, ); }}String emailLabel(String? email) => (email == null || email.trim().isEmpty) ? 'No email' : email.trim();Future<User> loadUser() async { await Future.delayed(Duration(milliseconds: 300)); final json = {'id': 1, 'name': 'Sam', 'email': null}; return User.fromJson(json);}Exercise
- Change the JSON in
loadUser()to include an email and verifyemailLabel(user.email)would return the email. - Add a new field
String? avatarUrltoUserand parse it from JSON. Provide a fallback string like'no-avatar'using??.