This tutorial explains how to build stateful widget. It is intended to be a blue-print for applications managing its state.
1. Create a stateful widget
In this application you build an example app for handling state. The final result will look very simple like this:
In this exercise we try to follow best practices for an real life application:
-
the data model is separated from the business model
-
the data is provided by a special class, i.e. data provider which makes it easier to adjust the logic of retrieving and saving the data
-
our provider extends `ChangeNotifier, which allows us to notify listeners (typically widgets) when the data changes
-
the provider will be asynchronous, this makes the initial code a bit more complex but this is required once the data is read from a real data source
1.1. Create an empty Flutter project
Create a new empty Flutter project named stateful_widget_example. Open the created directory in the IDE of your choice.
1.2. Create data model
In the lib folder create a new folder named models. Create a file named task.dart in the folder models with the following content.
import 'package:flutter/foundation.dart';
class Task extends ChangeNotifier { (1)
String title;
bool _isDone;
Task({required this.title, bool isDone = false}) : _isDone = isDone;
set isDone(bool value) {
_isDone = value;
notifyListeners(); (2)
}
bool get isDone => _isDone; // Getter method for isDone
}
1 | this allows other classes to register to be notified if the data object changes |
2 | notify any listeners if done status changes |
1.3. Create a data provider
In the lib folder create a new folder named services. Create a file named task_data_provider.dart in the folder services with the following content.
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import '../models/task.dart';
class TaskDataProvider extends ChangeNotifier implements Listenable { (1)
final List<Task> _tasks = [ (2)
Task(title: 'Task 1'),
Task(title: 'Task 2'),
Task(title: 'Task 3'),
Task(title: 'Task 4'),
Task(title: 'Task 5'),
];
@override
void addListener(VoidCallback listener) {
super.addListener(listener);
// Register listeners for existing tasks
for (var task in _tasks) {
task.addListener(listener);
}
}
@override
void removeListener(VoidCallback listener) {
super.removeListener(listener);
// Remove listeners for existing tasks
for (var task in _tasks) {
task.removeListener(listener);
}
}
Future<List<Task>> get tasks => Future.value(_tasks);
Future<bool> addTask(Task newTask) async {
newTask.addListener(notifyListeners); // Register listener to the new task
_tasks.add(newTask);
notifyListeners();
return true; (3)
}
Future<bool> save() async {
final bool saved = await _persistenceService.save(_tasks);
if (saved) {
notifyListeners();
}
return saved;
}
Future<bool> removeTask(int index) async {
_tasks.removeAt(index);
notifyListeners();
return true; (4)
}
}
1 | ChangeNotifier allows other classes to register to be notified if the data object changes and Listenable allows to use easy way to register for changes in the task model if a listener gets attached to the data provider |
2 | we start with a hard-coded data set, this will be later replaced with a persistence layer |
3 | later used to signal to the caller if adding was successful |
4 | later used to signal to the caller if removing was successful |
1.4. Create the screen to display your task
n the lib folder create a new folder named screens. Create a file named tasks_screen.dart in the folder screens. In this file, create the following widget.
import 'package:flutter/material.dart';
import '../models/task.dart';
import '../services/task_data_provider.dart';
class TasksScreen extends StatefulWidget {
const TasksScreen({super.key});
@override
State<StatefulWidget> createState() => TasksScreenState();
}
class TasksScreenState extends State<TasksScreen> {
late TaskDataProvider taskDataProvider;
TextEditingController controller = TextEditingController();
List<Task> tasks = [];
@override
void initState() {
super.initState();
taskDataProvider = TaskDataProvider();
taskDataProvider
.addListener(_onTaskChanges); // Register listener to the new task
_fetchTasks();
}
@override
void dispose() {
taskDataProvider.removeListener(
_onTaskChanges); // Remove listener to avoid memory leaks
super.dispose();
}
void _onTaskChanges() {
// When tasks change, update the UI
_fetchTasks();
}
Future<void> _fetchTasks() async {
try {
List<Task> fetchedTasks = await taskDataProvider.tasks;
setState(() {
tasks = fetchedTasks;
});
} catch (error) {
debugPrint('debug: Error during reading the tasks');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Task List'),
),
body: ListView.builder(
itemCount: tasks.length,
itemBuilder: (context, index) {
return _buildTaskItem(context, index);
},
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () {
_addTask();
},
),
);
}
Widget _buildTaskItem(BuildContext context, int index) {
final task = tasks[index];
return CheckboxListTile(
title: Text(task.title),
value: task.isDone,
onChanged: (value) {
_toggleTask(task, value!);
},
);
}
_addTask() {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Create new task'),
content: TextField(
controller: controller,
autofocus: true,
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('Cancel'),
),
TextButton(
onPressed: () {
final task = Task(title: controller.value.text);
taskDataProvider.addTask(task);
controller.clear();
Navigator.of(context).pop();
},
child: const Text('Add'),
),
],
);
},
);
}
_toggleTask(Task task, bool isChecked) {
task.isDone = isChecked;
taskDataProvider.save();
}
}
1.5. Show your task list widget in your application
Change the main.dart file to the following.
iimport 'package:flutter/material.dart';
import 'screens/tasks_screen.dart';
void main() {
runApp(const MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Tasks',
home: TasksScreen(),
);
}
}
1.6. Run and test
Start your application and test it. You can create new tasks and set existing tasks to completed.
2. Persisting the data as JSON
2.1. Add methods to convert tasks from and to JSON
Add methods to the Task
class to create and save task data from and to JSON.
import 'package:flutter/foundation.dart';
class Task extends ChangeNotifier {
String title;
bool _isDone;
Task({required this.title, bool isDone = false}) : _isDone = isDone;
set isDone(bool value) {
_isDone = value;
notifyListeners(); // Notify listeners when isDone changes
}
bool get isDone => _isDone; // Getter method for isDone
// Convert Task instance to JSON
Map<String, dynamic> toJson() {
return {
'title': title,
'isDone': _isDone,
};
}
// Create Task instance from JSON
factory Task.fromJson(Map<String, dynamic> json) {
return Task(
title: json['title'],
isDone: json['isDone'] ??
false, // Default to false if 'isDone' is not present
);
}
}
2.2. Add library to your project
Now add the path_provider library to your project. You can do this, by running the following command on the command line.
flutter pub add path_provider
This commands adds path_provider
as dependency to your pubspec.yaml
file.
This entry looks similar to the following, you may have a different version and you will have other entries in this file.
// more entries
dependencies:
// more entries
path_provider: ^2.1.2
// more entries
2.3. Create class which allows to save and load task from JSON files
Create the data_service.dart
file in the lib/services
folder with the following content.
import 'dart:io';
import 'dart:convert';
import 'package:path_provider/path_provider.dart';
import '../models/task.dart';
class PersistenceService {
Future<List<Task>> load() async {
try {
final directory = await getApplicationDocumentsDirectory();
File file = File('${directory.path}/tasks.json');
if (await file.exists()) {
String jsonString = await file.readAsString();
List<dynamic> jsonList = json.decode(jsonString);
return jsonList.map((jsonTask) => Task.fromJson(jsonTask)).toList();
}
} catch (e) {
// Something wrong with the file, lets restart
return [];
}
return [];
}
Future<bool> save(List<Task> tasks) async {
try {
final directory = await getApplicationDocumentsDirectory();
File file = File('${directory.path}/tasks.json');
// Convert list to JSON
List<Map<String, dynamic>> jsonList =
tasks.map((task) => task.toJson()).toList();
// Save JSON into the file (create or overwrite)
await file.writeAsString(json.encode(jsonList), flush: true);
return true; // Successfully saved
} catch (e) {
return false;
}
}
}
2.4. Load and save task data in your tasks screen widget
Adjust your TaskDataProvider
widget to load the data at startup and save it whenever a new task is created.
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'data_service.dart';
import '../models/task.dart';
class TaskDataProvider extends ChangeNotifier implements Listenable {
final List<Task> _tasks = [];
bool initialLoadDone = false;
final _persistenceService = PersistenceService();
@override
void addListener(VoidCallback listener) {
super.addListener(listener);
// Register listeners for existing tasks
for (var task in _tasks) {
task.addListener(listener);
}
}
@override
void removeListener(VoidCallback listener) {
super.removeListener(listener);
// Remove listeners for existing tasks
for (var task in _tasks) {
task.removeListener(listener);
}
}
Future<List<Task>> get tasks async {
if (!initialLoadDone) {
_tasks.addAll(await _persistenceService.load());
initialLoadDone = true;
}
return _tasks;
}
Future<bool> addTask(Task newTask) async {
newTask.addListener(notifyListeners); // Register listener to the new task
_tasks.add(newTask);
final bool saved = await _persistenceService.save(_tasks);
if (saved) {
notifyListeners();
}
return saved;
}
Future<bool> removeTask(int index) async {
notifyListeners();
final bool saved = await _persistenceService.save(_tasks);
if (saved) {
notifyListeners();
}
return saved;
}
}
The user interface should not be affected. Data should now be persisted.
If you get an error while running the app, perform a flutter clean. |
3. Links and Literature
3.3. vogella Flutter and Dart example code
If you need more assistance we offer Online Training and Onsite training as well as consulting