Flutter for Beginners: Dart Fundamentals for Building Apps

Capítulo 2

Estimated reading time: 9 minutes

+ Exercise

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.dart

Flutter 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).

Continue in our app.
  • Listen to the audio with the screen off.
  • Earn a certificate upon completion.
  • Over 5000 courses for you to explore!
Or continue reading below...
Download App

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 final variable named username with your name.
  • Declare a const variable named defaultPadding set to 12.0.
  • Change an int variable count from 0 to 1. Then try the same with a final variable 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 null

If 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 null

In 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' when name is null or empty, otherwise returns the trimmed name.
  • Given String? city;, create a non-nullable String variable cityLabel that becomes 'Unknown city' when city is 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 returns true if 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 using map.
  • 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 the name value into a String variable.

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 User class with fields id (int), name (String), and email (String?). Use a named-parameter constructor where email is 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 a Todo using Todo.fromJson.
  • Call toJson() on a Todo and 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? phone to your User model and parse it from JSON using as 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 returns 42.
  • 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 verify emailLabel(user.email) would return the email.
  • Add a new field String? avatarUrl to User and parse it from JSON. Provide a fallback string like 'no-avatar' using ??.

Now answer the exercise about the content:

In Dart null safety, which code best creates a non-nullable label that falls back when a nullable String is null, using the null-coalescing operator (??)?

You are right! Congratulations, now go to the next page

You missed! Try again.

?? provides a fallback value when a nullable variable is null, producing a non-nullable String. Using ! can crash if the value is null, and ?. keeps the result nullable.

Next chapter

Flutter for Beginners: Understanding Widgets and the Widget Tree

Arrow Right Icon
Free Ebook cover Flutter for Beginners: Build Your First Cross-Platform Apps from Scratch
17%

Flutter for Beginners: Build Your First Cross-Platform Apps from Scratch

New course

12 pages

Download the app to earn free Certification and listen to the courses in the background, even with the screen off.