Flutter for Beginners
Flutter for Beginners
Table of Content
Chapter 1: Introduction to Flutter
• What is Flutter?
o Overview of Flutter
o Benefits of using Flutter for app development
• Setting Up Your Environment
o Installing Flutter SDK
o Setting up an IDE (VS Code, Android Studio)
o Running your first Flutter app
2
o Simple Navigation (Navigator.push and Navigator.pop)
o Passing Data Between Screens
• Advanced Navigation
o Named Routes
o Navigation with the BottomNavigationBar
3
o Transitioning Between Screens with Animations
Additional Resources:
• Appendix A: Flutter Resources
o Recommended Documentation, Tutorials, and Courses
• Appendix B: Common Errors and Troubleshooting
o Fixing Common Issues in Flutter Development
4
Chapter 1: Introduction to Flutter
1.1 What is Flutter?
Flutter is an open-source UI software development kit (SDK) created by Google. It is used
to develop natively compiled applications for mobile, web, and desktop from a single
codebase. Flutter is particularly known for its fast development cycle, expressive and flexible
UI, and native performance.
• Key Features of Flutter:
• Single Codebase: With Flutter, you can write your code once and run it on multiple
platforms such as Android, iOS, web, and desktop. This reduces development time
and effort.
• Hot Reload: One of Flutter's standout features is hot reload, which allows you to see
the results of your code changes almost instantly. This feature speeds up the
development process by allowing you to experiment, build UIs, add features, and fix
bugs quickly.
• Rich Set of Widgets: Flutter provides a comprehensive set of pre-designed widgets
that make it easy to create beautiful and responsive UIs. These widgets are
customizable and can be used to build complex layouts with ease.
• High Performance: Flutter apps are compiled directly into machine code, which
means they run with the performance of native applications. This ensures smooth
animations and a responsive user experience.
• Why Choose Flutter?
Whether you're a beginner or an experienced developer, Flutter offers a range of benefits
that make it an attractive option for building modern applications:
• Fast Development: The combination of a single codebase, hot reload, and a rich set
of widgets allows you to build apps quickly without compromising on quality.
• Beautiful UIs: Flutter's widgets are designed to follow platform-specific conventions,
which means your app will look and feel like a native app on both Android and iOS.
You also have the flexibility to create custom designs.
• Growing Community and Ecosystem: Flutter has a rapidly growing community of
developers and a vibrant ecosystem of plugins and packages that extend its
capabilities. This means you can find plenty of resources, libraries, and tools to help
you with your development journey.
1.2 Setting Up Your Environment
Before you can start building apps with Flutter, you need to set up your development
environment. This section will guide you through the process of installing the necessary tools
and running your first Flutter app.
1.2.1 Installing Flutter SDK
To get started with Flutter, you need to install the Flutter SDK on your machine. Follow the
steps below based on your operating system:
For Windows:
1. Download Flutter SDK:
o Visit the Flutter website and download the latest stable version of the Flutter
SDK for Windows.
5
2. Extract the SDK:
o Extract the downloaded file to a location of your choice, such as C:\flutter.
3. Update Your Path:
o Add the Flutter SDK's bin directory to your system's PATH environment
variable to run Flutter commands from any directory.
4. Verify Installation:
o Open a new command prompt and run the following command to verify your
installation:
bash
flutter doctor
o The flutter doctor command checks your environment and displays a report of
the status of your installation. It also suggests further actions if necessary.
For macOS:
1. Download Flutter SDK:
o Visit the Flutter website and download the latest stable version of the Flutter
SDK for macOS.
2. Extract the SDK:
o Extract the downloaded file to a location of your choice, such as ~/flutter.
3. Update Your Path:
o Open your terminal and run the following command to add Flutter to your
PATH:
bash
export PATH="$PATH:`pwd`/flutter/bin"
o To make this change permanent, add the above line to your .bashrc, .zshrc,
or .bash_profile file.
4. Verify Installation:
o Run the following command to verify your installation:
bash
flutter doctor
o The flutter doctor command will check for any issues and provide guidance
on fixing them.
For Linux:
1. Download Flutter SDK:
o Visit the Flutter website and download the latest stable version of the Flutter
SDK for Linux.
2. Extract the SDK:
o Extract the downloaded file to a location of your choice, such as ~/flutter.
3. Update Your Path:
o Open your terminal and run the following command to add Flutter to your
PATH:
6
bash
export PATH="$PATH:`pwd`/flutter/bin"
o To make this change permanent, add the above line to your .bashrc, .zshrc,
or .profile file.
4. Verify Installation:
o Run the following command to verify your installation:
bash
flutter doctor
o The flutter doctor command will check for any issues and provide guidance
on fixing them.
• 1.2.2 Setting Up an Integrated Development Environment (IDE)
To develop Flutter apps, you'll need an IDE. The two most popular options are Visual Studio
Code (VS Code) and Android Studio. Both offer excellent support for Flutter development.
Option 1: Visual Studio Code (VS Code)
1. Install VS Code:
o Download and install Visual Studio Code from its official website.
2. Install Flutter and Dart Extensions:
o Open VS Code and go to the Extensions view by clicking on the Extensions
icon in the Activity Bar on the side of the window.
o Search for the Flutter extension and install it. The Dart extension will be
installed automatically as a dependency.
3. Create a New Flutter Project:
o Open the Command Palette (Ctrl+Shift+P or Cmd+Shift+P on macOS) and
type Flutter: New Project.
o Select a project name and location, and VS Code will create a new Flutter
project for you.
Option 2: Android Studio
1. Install Android Studio:
o Download and install Android Studio from its official website.
2. Install Flutter Plugin:
o Open Android Studio and go to File > Settings > Plugins.
o Search for Flutter and install the plugin. The Dart plugin will be installed
automatically as a dependency.
3. Create a New Flutter Project:
o Go to File > New > New Flutter Project.
o Follow the prompts to create a new Flutter project.
• 1.2.3 Running Your First Flutter App
7
Now that your environment is set up, it's time to run your first Flutter app. When you create a
new Flutter project, it comes with a simple counter app that you can run immediately.
1. Open Your Project:
o Open your Flutter project in your IDE (VS Code or Android Studio).
2. Connect a Device:
o Ensure that a physical device is connected to your computer, or start an
Android emulator or iOS simulator.
3. Run the App:
o In VS Code, press F5 to start debugging and run the app.
o In Android Studio, click the Run button in the toolbar.
4. Explore the App:
o The app will display a simple UI with a button that increments a counter each
time it's pressed. This app demonstrates the basic structure of a Flutter app,
including widgets, state management, and hot reload.
1.3 Summary
In this chapter, we've introduced Flutter, discussed its benefits, and guided you through the
process of setting up your development environment. You should now have Flutter installed
on your machine, an IDE configured, and your first Flutter app running on a device or
emulator.
In the next chapter, we'll dive into the basics of the Dart programming language, which is
essential for building Flutter apps. Understanding Dart will help you write effective and
efficient Flutter code as you progress through the book.
8
Chapter 2: Dart Programming Basics
9
• double: Represents floating-point values.
• String: Represents a sequence of characters.
• bool: Represents Boolean values (true or false).
• var: Dart infers the type based on the assigned value.
2.2.2 Control Flow Statements
Control flow statements are used to direct the flow of execution in your code.
If-Else Statement:
dart
void main() {
int score = 85;
switch (grade) {
case 'A':
print("Excellent!");
break;
case 'B':
print("Good Job!");
break;
case 'C':
10
print("Well Done!");
break;
default:
print("Try Again!");
}
}
Loops:
dart
void main() {
// For Loop
for (int i = 0; i < 5; i++) {
print("Iteration $i");
}
// While Loop
int j = 0;
while (j < 5) {
print("Iteration $j");
j++;
}
}
• 2.2.3 Functions and Methods
Functions are the building blocks of reusable code. In Dart, functions can be defined with or
without a return type.
Basic Function:
dart
void greet() {
print("Hello, World!");
}
void main() {
greet(); // Calling the function
11
}
Function with Parameters:
dart
int add(int a, int b) {
return a + b;
}
void main() {
int result = add(5, 3);
print(result); // Output: 8
}
Anonymous Functions (Lambdas):
dart
void main() {
var multiply = (int a, int b) => a * b;
print(multiply(3, 4)); // Output: 12
}
// Constructor
Person(this.name, this.age);
12
// Method
void displayInfo() {
print("Name: $name, Age: $age");
}
}
void main() {
Person person1 = Person("Alice", 30);
person1.displayInfo(); // Output: Name: Alice, Age: 30
}
2.3.2 Inheritance and Polymorphism
Inheritance allows a class to inherit properties and methods from another class.
Inheritance Example:
dart
class Animal {
void makeSound() {
print("Animal sound");
}
}
void main() {
Dog dog = Dog();
dog.makeSound(); // Output: Bark!
}
13
In this example, the Dog class inherits the makeSound method from the Animal class and
overrides it with its own implementation.
2.3.3 Handling Errors and Exceptions
Error handling in Dart is done using try-catch blocks.
Try-Catch Example:
dart
void main() {
try {
int result = 10 ~/ 0; // Division by zero
} catch (e) {
print("An error occurred: $e");
} finally {
print("This is the finally block");
}
}
• try: The block of code that might throw an exception.
• catch: The block that handles the exception.
• finally: The block that is always executed, regardless of whether an exception was
thrown.
2.4 Summary
In this chapter, we covered the basics of Dart programming, including variables, control flow,
functions, and object-oriented programming concepts like classes and inheritance.
Understanding these fundamentals is essential for effective Flutter development, as Dart is
the language you'll use to build your apps.
In the next chapter, we'll explore Flutter's widget tree, where you'll learn how to create and
manage the building blocks of your app's user interface. This knowledge will serve as the
foundation for building functional and visually appealing Flutter applications.
14
Chapter 3: Understanding Flutter's Widget Tree
void main() {
runApp(MyApp());
}
15
),
body: Center(
child: Text('Hello, Flutter!'),
),
),
);
}
}
In this example:
• The MaterialApp widget is at the root of the widget tree.
• Inside MaterialApp, we have a Scaffold, which provides a basic visual structure like
an app bar and body.
• The AppBar widget is a child of Scaffold, displaying the title.
• The Center widget centers its child, which in this case is a Text widget displaying
"Hello, Flutter!"
void main() {
runApp(MyApp());
16
}
17
Icon(Icons.star, color: Colors.blue),
],
),
],
),
),
);
}
}
This example creates a simple UI with a text widget, a styled container, and a row of colored
icons.
3.2.2 Aligning and Positioning Widgets
Positioning widgets within the Flutter UI is key to creating an appealing and functional
design. Here are some ways to control the alignment and positioning:
• Center Widget: Centers a child widget both vertically and horizontally.
• Padding Widget: Adds padding around a child widget.
• Align Widget: Aligns a child widget within its parent using an alignment parameter.
Example:
dart
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
18
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Align(
alignment: Alignment.topRight,
child: Container(
width: 100,
height: 100,
color: Colors.amber,
child: Center(child: Text('Aligned!')),
),
),
),
),
);
}
}
In this example, the Container is aligned to the top-right corner of the screen using the Align
widget.
void main() {
runApp(MyApp());
}
19
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('TextField Example'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: <Widget>[
TextField(
decoration: InputDecoration(
labelText: 'Enter your name',
border: OutlineInputBorder(),
),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () {
print('Button Pressed!');
},
child: Text('Submit'),
),
],
),
),
),
);
}
20
}
In this example, we create a TextField for user input and a button that prints a message to
the console when pressed.
• 3.3.2 Button Widgets
Buttons are essential for user interaction. Flutter offers several types of buttons, such as
ElevatedButton, TextButton, and IconButton.
Example:
dart
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
21
}
This example demonstrates how to create a basic ElevatedButton that triggers an action
when pressed.
3.4 Summary
In this chapter, we've explored the fundamental concepts of widgets in Flutter, including
stateless and stateful widgets, the widget tree, and how to build a basic user interface. You
also learned how to align and position widgets and handle user input with text fields and
buttons.
With these concepts in hand, you can start creating more complex UIs and interactive
elements in your Flutter apps. In the next chapter, we'll dive into navigation in Flutter, where
you'll learn how to move between different screens in your app.
22
Chapter 4: Navigating Between Screens
23
body: Center(
child: ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondScreen()),
);
},
child: Text('Go to Second Screen'),
),
),
);
}
}
24
• The FirstScreen pushes a new route to the stack when the button is pressed,
displaying the SecondScreen.
• The SecondScreen pops the route off the stack, returning to the FirstScreen when
the button is pressed.
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondScreen()),
);
In this example, SecondScreen is the new route being pushed onto the stack. The current
screen will be replaced by SecondScreen.
4.2.2 Popping a Route
To go back to the previous screen, you use Navigator.pop. This removes the current route
from the stack and shows the previous one.
Example:
dart
Navigator.pop(context);
This command will remove the top route from the stack, effectively navigating back to the
previous screen.
25
Example:
dart
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SecondScreen(data: 'Hello from the First Screen'),
),
);
Then, in the SecondScreen, you can access this data:
dart
SecondScreen({required this.data});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Second Screen')),
body: Center(
child: Text(data),
),
);
}
}
This example demonstrates how to pass a string of data from the first screen to the second
screen.
• 4.3.2 Returning Data to the Previous Screen
You can also return data from a screen when popping it off the stack. This is useful when
you need to pass results or user inputs back to the previous screen.
Example:
dart
26
// In the SecondScreen
Navigator.pop(context, 'Data from Second Screen');
To receive this data in the previous screen:
dart
27
}
Navigating Using Named Routes:
dart
Navigator.pushNamed(context, '/second');
In this example, you define the routes in a Map object and then navigate to them by name.
4.4.2 Generating Routes Dynamically
Sometimes you might need to generate routes dynamically, based on conditions or
arguments.
Example:
dart
void main() {
runApp(MaterialApp(
onGenerateRoute: (settings) {
if (settings.name == '/second') {
final String data = settings.arguments as String;
return MaterialPageRoute(
builder: (context) => SecondScreen(data: data),
);
}
return null;
},
));
}
In this example, the onGenerateRoute callback is used to dynamically create routes based
on the route name and arguments.
4.5 Navigation with BottomNavigationBar
In many apps, you’ll find a bottom navigation bar for switching between different sections or
tabs. Flutter provides a BottomNavigationBar widget for this purpose.
28
4.5.1 Implementing Bottom Navigation
Example:
dart
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
@override
Widget build(BuildContext context) {
return MaterialApp(
29
home: Scaffold(
appBar: AppBar(
title: const Text('BottomNavigationBar Example'),
),
body: Center(
child: _widgetOptions.elementAt(_selectedIndex),
),
bottomNavigationBar: BottomNavigationBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.search),
label: 'Search',
),
BottomNavigationBarItem(
icon: Icon(Icons.person),
label: 'Profile',
),
],
currentIndex: _selectedIndex,
selectedItemColor: Colors.amber[800],
onTap: _onItemTapped,
),
),
);
}
}
In this example, the BottomNavigationBar allows the user to switch between different pages
by tapping on the icons. The state is managed to reflect the selected tab.
30
4.6 Summary
In this chapter, we’ve covered the fundamentals of navigation in Flutter. You’ve learned how
to navigate between screens using Navigator.push and Navigator.pop, how to pass data
between screens, and how to implement more advanced navigation techniques like named
routes and dynamic route generation. We also explored using a BottomNavigationBar for
tab-based navigation.
These navigation skills are crucial for building apps that require multi-screen interactions. In
the next chapter, we'll dive into state management in Flutter, where you'll learn how to
manage and maintain the state of your app across different screens and components.
31
Chapter 5: Working with State Management
MyStatelessWidget({required this.title});
@override
Widget build(BuildContext context) {
return Text(title);
}
}
• Stateful Widgets: These widgets can change dynamically. They maintain a mutable
state that can be modified, and the widget can rebuild itself when the state changes.
Example:
dart
class MyStatefulWidget extends StatefulWidget {
@override
_MyStatefulWidgetState createState() => _MyStatefulWidgetState();
}
32
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Text('Counter: $_counter'),
ElevatedButton(
onPressed: _incrementCounter,
child: Text('Increment'),
),
],
);
}
}
In this example, the StatefulWidget maintains a counter that increments each time the button
is pressed. The setState() method triggers a rebuild of the widget, updating the UI to reflect
the new state.
33
dart
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('You have pressed the button this many times:'),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
ElevatedButton(
onPressed: _incrementCounter,
child: Text('Increment'),
),
],
);
34
}
}
In this example, _counter is a private variable that is modified using the _incrementCounter
method. When setState is called, the widget rebuilds with the updated counter value.
• 5.2.2 Using Local State in Forms
Local state is also commonly used in forms, where you need to manage user input. For
instance, you might want to capture text input from a TextField widget.
Example:
dart
void _submitData() {
print('Submitted: ${_controller.text}');
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
TextField(
35
controller: _controller,
decoration: InputDecoration(labelText: 'Enter text'),
),
ElevatedButton(
onPressed: _submitData,
child: Text('Submit'),
),
],
);
}
}
In this example, the TextEditingController is used to capture and manage the text input from
the TextField. When the submit button is pressed, the text is printed to the console.
dependencies:
flutter:
sdk: flutter
provider: ^6.0.0
2. Create a Model Class: Create a class to represent your state. This class extends
ChangeNotifier to notify listeners of state changes.
dart
36
class CounterModel with ChangeNotifier {
int _counter = 0;
void increment() {
_counter++;
notifyListeners();
}
}
3. Wrap Your App with Provider: In your main.dart file, wrap your app with a
ChangeNotifierProvider to make the model available to the entire widget tree.
dart
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CounterModel(),
child: MyApp(),
),
);
}
4. Access the Model in Widgets: Use Provider.of or Consumer to access the state
and update the UI.
dart
return Scaffold(
appBar: AppBar(
37
title: Text('Provider Example'),
),
body: Center(
child: Text('Counter: ${counterModel.counter}'),
),
floatingActionButton: FloatingActionButton(
onPressed: counterModel.increment,
child: Icon(Icons.add),
),
);
}
}
In this example, the counter value is shared across the widget tree, and any widget can
access and modify it.
• 5.3.2 Riverpod (Optional)
Riverpod is another state management solution that offers more flexibility and safety
compared to Provider. It's particularly useful for large-scale apps with complex state
management needs.
Basic Setup for Riverpod:
1. Add Dependency: In your pubspec.yaml file, add the flutter_riverpod package:
yaml
dependencies:
flutter:
sdk: flutter
flutter_riverpod: ^1.0.0
2. Create a Provider: Define a provider for your state:
dart
38
class MyHomePage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final counter = ref.watch(counterProvider);
return Scaffold(
appBar: AppBar(
title: Text('Riverpod Example'),
),
body: Center(
child: Text('Counter: $counter'),
),
floatingActionButton: FloatingActionButton(
onPressed: () => ref.read(counterProvider.notifier).state++,
child: Icon(Icons.add),
),
);
}
}
In this example, Riverpod provides a way to manage and update the counter state efficiently.
• 5.3.3 SetState vs. ScopedModel
While setState is effective for local state, and Provider or Riverpod work well for shared
state, ScopedModel is another approach you might encounter. It is less common now but
still worth mentioning.
Example:
dart
import 'package:scoped_model/scoped_model.dart';
39
void increment() {
_counter++;
notifyListeners();
}
}
You then wrap your app with ScopedModel and use ScopedModelDescendant to access the
model. However, for most new Flutter projects, Provider is the recommended approach.
5.4 Summary
In this chapter, we explored the concept of state in Flutter and how to manage it effectively.
We started by distinguishing between stateless and stateful widgets and then moved on to
managing state locally within widgets. For more complex applications, we introduced state
management techniques like Provider and Riverpod, which help in sharing and managing
state across the app efficiently.
Understanding state management is crucial for building responsive and interactive apps.
With the knowledge from this chapter, you’re now equipped to manage state effectively in
your Flutter projects. In the next chapter, we’ll build on this by developing a simple app—a
to-do list—where you can apply your state management skills in a practical context.
40
Chapter 6: Building a Simple App: A To-Do List
cd todo_list_app
41
2. Set Up Dependencies: For this simple app, we’ll use provider for state management
and shared_preferences for local storage. Open pubspec.yaml and add these
dependencies:
yaml
dependencies:
flutter:
sdk: flutter
provider: ^6.0.0
shared_preferences: ^2.0.0
Run flutter pub get to install the dependencies.
• 6.2.2 Creating the Task Model
First, let's create a Task model to represent each task in the to-do list:
dart
class Task {
String title;
bool isCompleted;
// Convert a Task into a Map. The keys must correspond to the names of fields in the
database.
Map<String, dynamic> toMap() {
return {
'title': title,
'isCompleted': isCompleted,
};
}
42
title: map['title'],
isCompleted: map['isCompleted'],
);
}
}
• 6.2.3 Creating the Task Provider
Next, we’ll create a TaskProvider class that manages the list of tasks. This class will handle
adding, editing, and deleting tasks, as well as saving and retrieving tasks from local storage
using shared_preferences.
dart
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';
TaskProvider() {
loadTasks();
}
43
notifyListeners();
}
if (encodedData != null) {
_tasks = (jsonDecode(encodedData) as List<dynamic>)
.map<Task>((item) => Task.fromMap(item))
.toList();
notifyListeners();
44
}
}
}
• 6.2.4 Building the Home Screen
Now, let's build the main screen where tasks are displayed.
dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'task_provider.dart';
import 'task_form_screen.dart';
return Scaffold(
appBar: AppBar(
title: Text('To-Do List'),
),
body: ListView.builder(
itemCount: taskProvider.tasks.length,
itemBuilder: (context, index) {
final task = taskProvider.tasks[index];
return ListTile(
title: Text(
task.title,
style: TextStyle(
decoration:
task.isCompleted ? TextDecoration.lineThrough : null,
45
),
),
leading: Checkbox(
value: task.isCompleted,
onChanged: (bool? value) {
taskProvider.toggleTaskCompletion(index);
},
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
IconButton(
icon: Icon(Icons.edit),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => TaskFormScreen(
task: task,
index: index,
),
),
);
},
),
IconButton(
icon: Icon(Icons.delete),
onPressed: () {
taskProvider.deleteTask(index);
},
),
],
46
),
);
},
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => TaskFormScreen()),
);
},
),
);
}
}
• 6.2.5 Creating the Task Form Screen
The Task Form Screen allows users to add or edit a task.
dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'task_provider.dart';
import 'task.dart';
TaskFormScreen({this.task, this.index});
47
final _titleController = TextEditingController();
@override
Widget build(BuildContext context) {
final taskProvider = Provider.of<TaskProvider>(context);
if (task != null) {
_titleController.text = task!.title;
}
return Scaffold(
appBar: AppBar(
title: Text(task == null ? 'Add Task' : 'Edit Task'),
),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
children: <Widget>[
TextFormField(
controller: _titleController,
decoration: InputDecoration(labelText: 'Task Title'),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter a task title';
}
return null;
},
),
SizedBox(height: 20),
ElevatedButton(
48
child: Text(task == null ? 'Add' : 'Save'),
onPressed: () {
if (_formKey.currentState!.validate()) {
if (task == null) {
taskProvider.addTask(Task(title: _titleController.text));
} else {
taskProvider.editTask(
index!,
Task(title: _titleController.text),
);
}
Navigator.pop(context);
}
},
),
],
),
),
),
);
}
}
• 6.2.6 Putting It All Together
In your main.dart, set up the app to use the TaskProvider and navigate to the HomeScreen.
dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'task_provider.dart';
import 'home_screen.dart';
void main() {
49
runApp(
ChangeNotifierProvider(
create: (context) => TaskProvider(),
child: MyApp(),
),
);
}
6.4 Summary
In this chapter, we built a simple To-Do List app using Flutter. We started by planning the
app, then moved on to implementing it step by step. You learned how to manage state with
Provider, build a user interface with Flutter widgets, and persist data using
shared_preferences. This project ties together many of the concepts you’ve learned so far,
giving you practical experience in developing a real-world application.
In the next chapter, we’ll dive into networking in Flutter, where you’ll learn how to fetch data
from the internet and display it in your app.
50
Chapter 7: Networking in Flutter
dependencies:
flutter:
sdk: flutter
http: ^0.13.0
2. Import the Package: In your Dart files where you need to make HTTP requests,
import the http package:
dart
51
• 7.2.1 Making a GET Request
A GET request is used to fetch data from a server. In this example, we'll make a GET
request to retrieve data from a public API.
Example:
dart
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
52
if (response.statusCode == 200) {
setState(() {
data = json.decode(response.body)['title'];
});
} else {
throw Exception('Failed to load data');
}
}
@override
void initState() {
super.initState();
fetchData();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Networking Example'),
),
body: Center(
child: data != null ? Text(data!) : CircularProgressIndicator(),
),
);
}
}
In this example:
• The app makes a GET request to https://jsonplaceholder.typicode.com/posts/1, a
test API that returns a post with an ID of 1.
• The response is decoded from JSON format and the title of the post is displayed on
the screen.
• If the request is successful, the data is displayed; otherwise, an error is thrown.
53
• 7.2.2 Making a POST Request
A POST request is used to send data to a server. In this example, we'll send data to a server
and handle the response.
Example:
dart
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
54
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode(<String, String>{
'title': 'Flutter Networking',
'body': 'This is a sample POST request.',
'userId': '1',
}),
);
if (response.statusCode == 201) {
setState(() {
responseMessage = 'Data sent successfully!';
});
} else {
throw Exception('Failed to send data');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('POST Request Example'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
responseMessage != null
? Text(responseMessage!)
: ElevatedButton(
55
onPressed: sendData,
child: Text('Send Data'),
),
],
),
),
);
}
}
In this example:
• The app sends a POST request to https://jsonplaceholder.typicode.com/posts,
creating a new post.
• The body of the request is encoded in JSON format, and the server responds with a
confirmation.
• If the request is successful, a success message is displayed; otherwise, an error is
thrown.
import 'dart:convert';
void parseJson() {
String jsonString = '{"title": "Flutter Networking", "userId": 1}';
Map<String, dynamic> userMap = jsonDecode(jsonString);
print('Title: ${userMap['title']}');
print('User ID: ${userMap['userId']}');
56
}
In this example, a JSON string is parsed into a Map in Dart, making it easy to access
individual properties like title and userId.
• 7.3.2 Encoding JSON
To send data to a server, you need to encode it into JSON format using json.encode().
Example:
dart
import 'dart:convert';
void encodeJson() {
Map<String, dynamic> taskMap = {
'title': 'Learn Flutter',
'isCompleted': false,
};
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
57
void main() => runApp(MyApp());
class Post {
final int id;
final String title;
final String body;
58
class _PostListScreenState extends State<PostListScreen> {
Future<List<Post>> fetchPosts() async {
final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/posts'));
if (response.statusCode == 200) {
List jsonResponse = json.decode(response.body);
return jsonResponse.map((post) => Post.fromJson(post)).toList();
} else {
throw Exception('Failed to load posts');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Posts'),
),
body: FutureBuilder<List<Post>>(
future: fetchPosts(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
} else if (!snapshot.hasData || snapshot.data!.isEmpty) {
return Center(child: Text('No posts found'));
} else {
return ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (context, index) {
59
final post = snapshot.data![index];
return ListTile(
title: Text(post.title),
subtitle: Text(post.body),
);
},
);
}
},
),
);
}
}
In this example:
• Post Model: We define a Post model that represents each post with id, title, and
body properties. The Post.fromJson factory constructor allows us to create a Post
instance from a JSON object.
• Fetching Data: The fetchPosts method makes an HTTP GET request to the
https://jsonplaceholder.typicode.com/posts endpoint. The response is parsed from
JSON into a list of Post objects.
• Displaying Data: The FutureBuilder widget is used to build the UI based on the state
of the fetchPosts future. While the data is loading, a CircularProgressIndicator is
displayed. If there's an error, an error message is shown. Once the data is
successfully fetched, it’s displayed in a ListView.
• 7.4.2 Displaying API Data in a List
The PostListScreen now displays a list of posts fetched from the API. Each ListTile shows
the title and body of a post. This pattern is common in apps that need to display dynamic
data from the web.
Customizing the List:
You can further customize the list by adding features such as:
• Pull-to-Refresh: Using RefreshIndicator to allow users to refresh the list.
• Detail Screen: Navigating to a detailed view of the post when a ListTile is tapped.
• Pagination: Loading more posts as the user scrolls down the list.
60
Network requests can fail for various reasons, such as no internet connection, server errors,
or unexpected data formats. Handling these errors gracefully is essential for providing a
good user experience.
• 7.5.1 Common Network Errors
• Timeouts: When the server takes too long to respond.
• No Internet Connection: When the user is offline.
• Server Errors: When the server returns an error code (e.g., 500 Internal Server
Error).
• Client Errors: When the request is invalid (e.g., 400 Bad Request).
• 7.5.2 Handling Errors in Flutter
You can handle errors in Flutter using try-catch blocks and by checking the status code of
HTTP responses.
Example:
dart
if (response.statusCode == 200) {
// Parse the data
} else if (response.statusCode == 404) {
throw Exception('Resource not found');
} else {
throw Exception('Failed to load data');
}
} catch (e) {
// Handle any exceptions
print('An error occurred: $e');
}
}
In this example:
61
• Status Code Checks: The status code of the response is checked to determine
whether the request was successful. Different status codes can trigger different
actions, such as displaying an error message.
• Exception Handling: The try-catch block catches any exceptions that occur during
the request, such as network issues or JSON parsing errors.
7.6 Summary
In this chapter, you learned how to perform network operations in Flutter, including making
HTTP GET and POST requests, handling JSON data, and integrating external APIs. We
built a simple app that fetches and displays a list of posts from a public API, demonstrating
how to fetch, parse, and display data from the internet.
Handling network operations is a critical skill in mobile app development, allowing your apps
to interact with online services and provide dynamic, up-to-date content to users. In the next
chapter, we’ll explore adding animations and visual effects to your Flutter apps, enhancing
the user experience further.
This chapter equips you with the skills to interact with web services, enabling you to build
more connected and interactive applications. As you continue developing your Flutter
projects, networking will be a key component in delivering rich and engaging user
experiences.
62
Chapter 8: Adding Animations and Effects
import 'package:flutter/material.dart';
63
);
}
}
void _changeShape() {
setState(() {
_width = _width == 100 ? 200 : 100;
_height = _height == 100 ? 200 : 100;
_color = _color == Colors.blue ? Colors.red : Colors.blue;
});
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _changeShape,
child: AnimatedContainer(
width: _width,
height: _height,
color: _color,
alignment: Alignment.center,
duration: Duration(seconds: 1),
64
curve: Curves.easeInOut,
child: Text(
'Tap Me!',
style: TextStyle(color: Colors.white),
),
),
);
}
}
In this example:
• The AnimatedContainer widget animates changes in its size and color when the
container is tapped.
• The duration property controls how long the animation takes, and the curve property
defines the animation's speed and style (e.g., ease in, ease out).
• 8.2.2 AnimatedOpacity
AnimatedOpacity is another simple but effective widget that allows you to animate the
opacity of a widget, making it fade in or out.
Example:
dart
import 'package:flutter/material.dart';
65
),
);
}
}
void _fade() {
setState(() {
_opacity = _opacity == 1.0 ? 0.0 : 1.0;
});
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _fade,
child: AnimatedOpacity(
opacity: _opacity,
duration: Duration(seconds: 2),
child: Container(
width: 200,
height: 200,
color: Colors.green,
child: Center(
child: Text(
66
'Fade Me!',
style: TextStyle(color: Colors.white, fontSize: 24),
),
),
),
),
);
}
}
In this example:
• The AnimatedOpacity widget animates the change in opacity of the container when it
is tapped.
• The duration defines how long the fade effect lasts.
import 'package:flutter/material.dart';
67
body: Center(
child: ExplicitAnimationExample(),
),
),
);
}
}
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
)..repeat(reverse: true);
68
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
width: _animation.value,
height: _animation.value,
color: Colors.blue,
child: Center(
child: Text(
'Zoom In and Out',
style: TextStyle(color: Colors.white),
),
),
);
}
}
In this example:
• AnimationController: Controls the animation’s duration and progress. It is set to
repeat, causing the animation to loop back and forth.
• Tween: Interpolates between 100 and 200, causing the container to grow and shrink
in size.
• 8.3.2 CurvedAnimation
CurvedAnimation allows you to apply non-linear curves to your animations, creating effects
like ease-in, ease-out, and bounce.
Example:
dart
import 'package:flutter/material.dart';
69
void main() => runApp(MyApp());
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 2),
70
vsync: this,
)..repeat(reverse: true);
_animation = CurvedAnimation(
parent: _controller,
curve: Curves.bounceIn,
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ScaleTransition(
scale: _animation,
child: Container(
width: 100,
height: 100,
color: Colors.orange,
child: Center(
child: Text(
'Bounce',
style: TextStyle(color: Colors.white),
),
),
),
);
}
71
}
In this example:
• CurvedAnimation: Applies a bounce effect to the scaling animation.
• ScaleTransition: A widget that scales its child based on the animation value.
import 'package:flutter/material.dart';
72
child: ElevatedButton(
onPressed: () {
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) => SecondScreen(),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
const begin = Offset(1.0, 0.0);
const end = Offset.zero;
const curve = Curves.easeInOut;
return SlideTransition(
position: animation.drive(tween),
child: child,
);
},
),
);
},
child: Text('Go to Second Screen'),
),
),
);
}
}
73
appBar: AppBar(title: Text('Second Screen')),
body: Center(
child: Text('Hello from the Second Screen!'),
),
);
}
}
In this example:
• SlideTransition: The new screen slides in from the right when the button is pressed,
thanks to the custom page transition provided by PageRouteBuilder.
• Tween: Defines the starting and ending positions for the slide animation.
8.5 Summary
In this chapter, you explored the basics and advanced concepts of adding animations in
Flutter. You learned how to implement implicit animations with widgets like
AnimatedContainer and AnimatedOpacity, as well as explicit animations using
AnimationController and Tween. Additionally, you discovered how to enhance user
experience by animating transitions between screens.
Animations are a powerful tool to make your apps more dynamic and engaging. By
mastering Flutter’s animation capabilities, you can create smooth, visually appealing apps
that provide a polished user experience. In the next chapter, we’ll dive into deploying your
Flutter app, covering everything from preparing your app for release to publishing it on the
Google Play Store and Apple App Store.
This chapter adds an exciting dimension to your Flutter apps, allowing you to create visually
engaging experiences. With these animation techniques, you can enhance both the
functionality and aesthetics of your mobile applications.
74
Chapter 9: Deploying Your Flutter App
<application
android:label="MyAppName"
android:icon="@mipmap/ic_launcher">
...
</application>
3. For iOS, open ios/Runner/Info.plist.
4. Update the CFBundleName key with your app’s name.
xml
<key>CFBundleName</key>
<string>MyAppName</string>
Changing the App Icon:
75
1. Create your app icon in multiple sizes. You can use a tool like App Icon Generator to
generate icons for both Android and iOS.
2. Replace the existing icons in android/app/src/main/res/mipmap-* for Android and
ios/Runner/Assets.xcassets/AppIcon.appiconset for iOS.
Changing the Package Name:
The package name uniquely identifies your app. Changing it requires updating several files.
1. For Android:
o Open android/app/build.gradle.
o Update the applicationId under android with your new package name.
gradle
defaultConfig {
applicationId "com.example.mynewapp"
...
}
o Change the package name in AndroidManifest.xml and rename the
directories under android/app/src/main/java/ to match your new package
name.
2. For iOS:
o Open ios/Runner.xcodeproj in Xcode.
o Go to the General tab, and update the Bundle Identifier with your new
package name.
76
• Use Xcode to run your app on a connected iOS device in release mode. Make sure
to check for UI issues, performance, and any platform-specific behavior.
9.3.2 Testing for Different Screen Sizes
Flutter apps need to handle different screen sizes and orientations effectively. Use Flutter’s
built-in tools and device simulators to test your app on various screen sizes.
• Responsive Layouts: Ensure that your app’s layout adapts to different screen sizes,
including tablets and small screens.
• Orientation Changes: Test your app’s behavior when the device orientation
changes from portrait to landscape and vice versa.
keytool -genkey -v -keystore ~/my-release-key.jks -keyalg RSA -keysize 2048 -validity 10000
-alias my-key-alias
2. Configure Gradle:
o Open android/app/build.gradle and add the following signing configuration:
gradle
android {
...
signingConfigs {
release {
keyAlias 'my-key-alias'
keyPassword 'your-key-password'
storeFile file('path/to/your/keystore.jks')
storePassword 'your-store-password'
}
}
77
buildTypes {
release {
signingConfig signingConfigs.release
}
}
}
3. Build the APK:
o Run flutter build apk --release to generate a signed APK file ready for
distribution.
5. 9.4.2 Signing an iOS App
1. Set Up Certificates:
o Open Xcode and navigate to Runner > General > Signing & Capabilities.
o Ensure that you have an Apple Developer account linked and that the
appropriate provisioning profile is selected.
2. Archive the App:
o In Xcode, go to Product > Archive to create an archive of your app.
o Once the archive is created, select Distribute App and follow the prompts to
sign and export the app.
78
1. In the Google Play Console, navigate to App releases > Production.
2. Click Create Release and upload your signed APK or AAB file.
3. Review any warnings or issues, then click Start rollout to Production to publish your
app.
9.7 Summary
In this chapter, you learned the steps involved in deploying your Flutter app, including
preparing your app for production, signing it, and publishing it on the Google Play Store and
Apple App Store. We covered everything from configuring your app’s name, icon, and
package name to testing, signing, and finally, submitting your app for public release.
Deploying your app is the final step in your Flutter development journey, allowing you to
share your creation with the world. With your app now available on major app stores, you’re
ready to reach a global audience and make an impact with your Flutter development skills.
79
This chapter provides you with the knowledge and steps necessary to take your Flutter app
from development to deployment. With your app successfully published, you’ve completed
the full cycle of app development, from conception to distribution. Congratulations on
reaching this milestone!
80
Chapter 10: Expanding Your Flutter Skills
dependencies:
flutter_bloc: ^8.0.0
2. Create a BLoC:
dart
import 'package:flutter_bloc/flutter_bloc.dart';
81
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0);
@override
Stream<int> mapEventToState(CounterEvent event) async* {
switch (event) {
case CounterEvent.increment:
yield state + 1;
break;
case CounterEvent.decrement:
yield state - 1;
break;
}
}
}
3. Using BLoC in the UI:
dart
82
floatingActionButton: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
FloatingActionButton(
onPressed: () => context.read<CounterBloc>().add(CounterEvent.increment),
child: Icon(Icons.add),
),
SizedBox(width: 10),
FloatingActionButton(
onPressed: () => context.read<CounterBloc>().add(CounterEvent.decrement),
child: Icon(Icons.remove),
),
],
),
),
);
}
}
In this example, the BLoC handles the increment and decrement logic, and the UI listens to
the BLoC's state changes.
83
flutter create my_web_app
3. Run the Web App: Navigate to your project directory and run:
bash
84
As you continue to develop more complex Flutter apps, following best practices will help you
maintain a clean, scalable, and maintainable codebase.
• 10.4.1 Code Organization
• Modularize Your Code: Break down your app into smaller, reusable widgets and
files. Use directories to organize your widgets, models, services, and state
management logic.
• Use State Management Wisely: Choose a state management solution that fits your
app’s complexity. For large apps, consider Provider, BLoC, or Riverpod.
• Follow the DRY Principle: Avoid duplicating code. Reuse widgets and functions
where possible.
• 10.4.2 Performance Optimization
• Avoid Rebuilding Unnecessarily: Use const constructors where possible to
prevent unnecessary widget rebuilds.
• Profile Your App: Use Flutter’s DevTools to profile and optimize your app’s
performance. Monitor memory usage, CPU usage, and frame rendering.
• Lazy Load Data: Load data on-demand rather than all at once, especially for lists or
paginated content.
• 10.4.3 Testing and Debugging
• Write Unit Tests: Test your logic independently from the UI. This ensures that your
business logic is sound and can handle various edge cases.
• Use Widget Tests: Test the UI components of your app to ensure that widgets
behave as expected.
• Continuous Integration: Set up a CI/CD pipeline to automatically test and build
your app on every code change.
85
• Join the Flutter Community: Participate in forums like Flutter Community on
Discord and Stack Overflow to ask questions and share knowledge.
• Attend Meetups and Conferences: Attend Flutter meetups, online webinars, and
conferences like Flutter Engage and Google I/O to connect with other developers
and learn from industry experts.
• Contribute to Open Source: Contribute to open-source Flutter packages and
projects on GitHub. It’s a great way to give back to the community and improve your
skills.
10.7 Summary
In this chapter, you explored advanced Flutter concepts, including state management with
BLoC, building Flutter apps for web and desktop, and best practices for development. You
also learned how to stay updated with the latest Flutter developments and engage with the
community.
Flutter offers vast opportunities, and mastering it opens the door to creating powerful, cross-
platform applications. Continue to experiment, build, and contribute to the community, and
you’ll become a proficient Flutter developer, capable of tackling any challenge.
This final chapter is a gateway to further learning and specialization in Flutter. With the
knowledge and experience you’ve gained, you’re well-equipped to continue your journey,
whether that’s building more complex applications, contributing to the Flutter ecosystem, or
exploring new frontiers in mobile and web development. Keep pushing your boundaries, and
enjoy the endless possibilities with Flutter!
86