When building a full-stack application, you’d typically separate your frontend from your backend. In most cases, each of these implementations are written using different frameworks for the frontend and backend services, and are hosted on separate remote servers.
For these services to perform holistic business functionalities, there must be some communication between them, such as in the form of commonly used mechanisms like REST, SOAP, GraphQL, etc.
Flutter is an open-source, cross-platform tool created by Google. It is used to develop interactive, reactive, and adaptive frontend applications (desktop, web, and mobile) that can run on several operating systems using one codebase. Flask is an open-source, beginner-friendly Python framework suitable for building monolithic and microservice backend applications with a lightweight codebase.
Using a basic to-do app, this tutorial will demonstrate how to integrate your frontend Flutter application with a Flask backend service. The application will send HTTP requests from the frontend to the RESTful backend to perform custom operations on a local or remote server.
Jump ahead:
The Replay is a weekly newsletter for dev and engineering leaders.
Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.

The Flutter app will serve as the frontend component that the user will interact with to create, execute, and delete to-do tasks. At the start of the application, the Flutter app will automatically send an HTTP GET request to the Flask server to fetch all the created tasks stored in the database. For the create, execute, and delete operations, the user will explicitly trigger the requests to the Flask server by clicking their respective buttons.
While features like these can be achieved using only a Flutter app without the need for a backend service, the aim here is to demonstrate how you’d typically perform frontend-backend integrations, using Flutter and Flask as case studies.
The operations performed by the Flask app will interact with the database where the to-do tasks reside. Flask-SQLAlchemy, a library for easily integrating SQLAlchemy with Flask, will manage the database operations. The Flask-Marshmallow and Marshmallow-SQLAlchemy packages will be responsible for serializing and deserializing the Python objects.
The processes involved in this application can be applied to any other full-stack integration, so long as it abides by the REST architecture of building and consuming APIs. That is, you can build your backend service using any other web framework, and expose the endpoints to the Flutter app and vice versa.
Let’s begin by building the backend service. Run the following commands in your terminal to set up the Flask app:
mkdir flask_todo cd flask_todo
The first step is to create and activate the virtual environment for the application with the below commands:
pip install pipenv pipenv shell
The above commands activate your virtual environment by creating a Pipfile that contains all the packages and dependencies you will install in the application.
Next, install the Flask framework and the dependencies necessary to build and run the application.
pipenv install flask flask-sqlalchemy flask-marshmallow marshmallow-sqlalchemy
After successfully installing the framework and dependencies, your Pipfile should be similar to the below image:

Create a Python file named main.py and add the following code to the code as ordered below:
from flask import Flask, request, jsonify from flask_sqlalchemy import SQLAlchemy from flask_marshmallow import Marshmallow import os app = Flask(__name__) basedir = os.path.abspath(os.path.dirname(__file__)) app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(basedir, 'db.sqlite') # suppress SQLALCHEMY_TRACK_MODIFICATIONS warning app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False db = SQLAlchemy(app) ma = Marshmallow(app)
The code snippet above sets up and initializes the Flask app and its database configurations. The basedir variable represents the root directory, within which you will create a file with the name db.sqlite. The db and ma variables represent the initialized SQLAlchemy and Marshmallow objects, respectively.
Todo model and schemaHere is the code snippet for the Todo model, schema creation, and initialization:
class TodoItem(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100))
is_executed = db.Column(db.Boolean)
def __init__(self, name, is_executed):
self.name = name
self.is_executed = is_executed
# Todo schema
class TodoSchema(ma.Schema):
class Meta:
fields = ('id', 'name', 'is_executed')
# Initialize schema
todo_schema = TodoSchema()
todos_schema = TodoSchema(many=True)
Each TodoItem entity has an ID, a name, and an execution status represented by the is_executed property. A task or to-do that has been executed or done will have an is_executed value of True and, if otherwise, False.
At any point in time, you’re either performing an operation on a single TodoItem entity or on multiple entities. The todo_schema handles a single entity, while the todos_schema handles multiple entities as declared using the many=True argument.
Open the Python shell by running the python command in your terminal. This will switch the CLI to the Python shell, where you will write the below Python scripts to create the database as shown below:
from main import db db.create_all() exit()
from main import db: This imports the SQLAlchemy object initialized with the db variable in the main.py filedb.create_all(): This creates the SQLite database. You’ll notice a db.sqlite file with encoded content automatically created in your project root folder after running this methodexit(): Exit the Python shellHere’s a visual representation of the above process:

Create an endpoint for the Flutter application to trigger to add a new to-do task to the database.
@app.route('/todo', methods=['POST'])
def add_todo():
name = request.json['name']
is_executed = request.json['is_executed']
new_todo_item = TodoItem(name, is_executed)
db.session.add(new_todo_item)
db.session.commit()
return todo_schema.jsonify(new_todo_item)
The above endpoint receives a POST request and expects a JSON payload with the name and is_executed keys, as well as their respective values. Using the request package imported from Flask at the top of the main.py file, the values of the name and is_executed keys are extracted into the name and is_executed variables, respectively. Then a new TodoItem object is created and saved to the database. The endpoint returns a JSON object of the newly created entity.
To verify that the endpoint works as expected, add the below to the end of the file:
if __name__ == '__main__':
app.run(debug=True)
Then run the application from your terminal with the below command:
python main.py
By default, the application will run on port 5000, as shown below:

Proceed to test the endpoint from Postman by sending a POST request to localhost:5000/todo.

The endpoint below receives a GET request through the /todo path and is responsible for fetching all the TodoItem entities that have been created in the database.
@app.route('/todo', methods=['GET'])
def get_todos():
all_todos = TodoItem.query.all()
result = todos_schema.dump(all_todos)
return jsonify(result)

@app.route('/todo/<id>', methods=['PUT', 'PATCH'])
def execute_todo(id):
todo = TodoItem.query.get(id)
todo.is_executed = not todo.is_executed
db.session.commit()
return todo_schema.jsonify(todo)
The endpoint receives the ID of the TodoItem through a PUT or PATCH request to fetch the entity from the database. Then, the endpoint converts the current value of the is_executed property to its opposite value before saving it again to the database — that is, if its current value is True, it converts it to False, and vice versa.

Here’s the endpoint to delete a TodoItem from the database:
@app.route('/todo/<id>', methods=['DELETE'])
def delete_todo(id):
todo_to_delete = TodoItem.query.get(id)
db.session.delete(todo_to_delete)
db.session.commit()
return todo_schema.jsonify(todo_to_delete)
The endpoint receives the ID of the TodoItem through a DELETE request to fetch the entity from the database and deletes the item from the database.

Proceed to create the Flutter application by running the following command in your terminal:
flutter create flutter_todo
Install the following dependencies:
flutter pub add http flutter pub add provider
Create a Dart file with the name todo_item and add the below snippet to the file:
class TodoItem{
dynamic id;
String itemName;
bool isExecuted;
TodoItem({this.id, required this.itemName, required this.isExecuted});
}
The properties of the class declared above are similar to those of the TodoItem we created earlier, in the Flask app. The values of each of the Flutter TodoItem object’s properties will be displayed on the screen.
Create another Dart file with the name todo_provider and add the following code to it:
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:todo/models/todo_item.dart';
import 'package:http/http.dart' as http;
class TodoProvider with ChangeNotifier{
List<TodoItem> _items = [];
final url = 'http://localhost:5000/todo';
List<TodoItem> get items {
return [..._items];
}
Future<void> addTodo(String task) async {
if(task.isEmpty){
return;
}
Map<String, dynamic> request = {"name": task, "is_executed": false};
final headers = {'Content-Type': 'application/json'};
final response = await http.post(Uri.parse(url), headers: headers, body: json.encode(request));
Map<String, dynamic> responsePayload = json.decode(response.body);
final todo = TodoItem(
id: responsePayload["id"],
itemName: responsePayload["name"],
The asynchronous addTodo method receives the name of the task to be created and uses this value to construct the request payload for adding a new TodoItem to the Flask service. Recall that the Flask endpoint to create a new to-do expects a POST request and a request body with the keys name and is_executed. The value of the is_executed key in the request is set to false because, by default, the newly created to-do should not be executed unless explicitly triggered by the user through the execute endpoint. The json.encode operation formats the request map to a JSON body so that it is compatible with the format and structure that the Flask server expects.
Note: If you’re running the application from an emulator, replace the base URL from localhost to 10.0.2.2. That is, the value of the url variable should be http://10.0.2.2:5000/todo. This is because localhost on the emulator does not point to 127.0.0.1; this could cause your application to fail when sending HTTP requests to a local server from an emulator. The response body from the Flask server contains a JSON body in the following format:
{
"id": 1,
"is_executed": false,
"name": "buy food"
}
The server automatically creates an ID for the newly created TodoItem and returns it as part of the response body. The values of the JSON body are unmarshalled into the Flutter TodoItem object and added to the _items list. The notifyListeners() method from the ChangeNotifierclass notifies all the listeners of the state of the application about the new TodoItem addition.
Here’s the implementation for fetching all the TodoItem entities from the backend:
Future<void> get getTodos async {
var response;
try{
response = await http.get(Uri.parse(url));
List<dynamic> body = json.decode(response.body);
_items = body.map((e) => TodoItem(
id: e['id'],
itemName: e['name'],
isExecuted: e['is_executed']
)
).toList();
}catch(e){
print(e);
}
notifyListeners();
}
The getTodos method above asynchronously sends a GET request to the backend server to fetch all the created TodoItem entities as a JSON list. Then it iterates through the response body to unmarshal the values into TodoItem objects.
Future<void> deleteTodo(int todoId) async {
var response;
try{
response = await http.delete(Uri.parse("$url/$todoId"));
final body = json.decode(response.body);
_items.removeWhere((element) => element.id == body["id"]);
}catch(e){
print(e);
}
notifyListeners();
}
The deleteTodo method is responsible for deleting the TodoItem entity in the database and in the Flutter app. It performs this operation by making an HTTP DELETE request to the backend API in the expected format. The response body contains a JSON payload of the deleted item. Using the ID of this item, the object is also identified in the _items list and removed.
Lastly, the executeTask method below expects the ID of the TodoItem and sends a PATCH request to the Flask endpoint expecting this request. The server executes the task, as earlier illustrated in the Flask API section of this tutorial, then it returns a response body that contains the executed TodoItem with the value of its is_executed property flipped. That is, if the TodoItem had an is_executed value of false before sending the request to the server, it will return with a true value and vice versa.
Future<void> executeTask(int todoId) async {
try{
final response = await http.patch(Uri.parse("$url/$todoId"));
Map<String, dynamic> responsePayload = json.decode(response.body);
_items.forEach((element) => {
if(element.id == responsePayload["id"]){
element.isExecuted = responsePayload["is_executed"]
}
});
}catch(e){
print(e);
}
notifyListeners();
}
task widgetCreate a tasks.dart file and following code to the file:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/todo_provider.dart';
class TasksWidget extends StatefulWidget {
const TasksWidget({Key? key}) : super(key: key);
@override
State<TasksWidget> createState() => _TasksWidgetState();
}
class _TasksWidgetState extends State<TasksWidget> {
TextEditingController newTaskController = TextEditingController();
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
children: [
Row(
children: [
Expanded(
child: TextFormField(
controller: newTaskController,
decoration: InputDecoration(
labelText: 'New Task',
border: OutlineInputBorder(),
),
),
),
SizedBox(width: 10,),
ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(Colors.amberAccent),
foregroundColor: MaterialStateProperty.all(Colors.purple)
),
child: Text("Add"),
onPressed: () {
Provider.of<TodoProvider>(context, listen: false).addTodo(newTaskController.text);
newTaskController.clear();
}
)
],
),
FutureBuilder(
future: Provider.of<TodoProvider>(context, listen: false).getTodos,
builder: (ctx, snapshot) =>
snapshot.connection state == ConnectionState.waiting
? Center(child: CircularProgressIndicator())
:
Consumer<TodoProvider>(
child: Center(
heightFactor: MediaQuery.of(context).size.height * 0.03,
child: const Text('You have no tasks.', style: TextStyle(fontSize: 18),),
),
builder: (ctx, todoProvider, child) => todoProvider.items.isEmpty
? child as Widget
: Padding(
padding: const EdgeInsets.only(top: 20),
child: Container(
height: MediaQuery.of(context).size.height * 0.6,
child: ListView.builder(
itemCount: todoProvider.items.length,
itemBuilder: (ctx, i) => Padding(
padding: const EdgeInsets.only(bottom: 10.0),
child: ListTile(
tileColor: Colors.black12,
leading: Checkbox(
value: todoProvider.items[i].isExecuted,
activeColor: Colors.purple,
onChanged:(newValue) {
Provider.of<TodoProvider>(context, listen: false).executeTask(todoProvider.items[i].id);
}
),
title: Text(todoProvider.items[i].itemName),
trailing: IconButton(
icon: Icon(Icons.delete, color: Colors.red),
onPressed: () {
Provider.of<TodoProvider>(context, listen: false).deleteTodo(todoProvider.items[i].id);
}
) ,
onTap: () {},
),
)
),
),
),
),
)
],
),
);
}
}
The TaskWidget class is a stateful widget that is responsible for building the interface and widgets of the application that the user will interact with. The widget renders the TextInputField to receive the value of the new task, ElevatedButton to submit the input value by invoking the addTodo method of the TodoProvider class and a ListView of the created tasks.

When the widget builds, it fetches all the created items in the database by calling the getTodos method in the TodoProvider class. Each task on the screen is a ListTile that contains a checkbox, the name of the task and a delete icon. Each click on the checkbox invokes the TodoProvider execute method to change the isExecuted value of that TodoItem object in the backend server and within the Flutter app. Clicking the delete icon invokes the deleteTodo method to delete the selected task.
![]()
![]()
Here’s the content of the main.dart file that is the entry point for the application:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:todo/providers/todo_provider.dart';
import 'package:todo/widgets/tasks.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider.value(
value: TodoProvider(),
child: MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.purple,
),
home: const MyHomePage(title: 'Todo app'),
)
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
title: Text(widget.title),
centerTitle: true,
),
body: TasksWidget(),
);
}
}

The process of integrating a frontend app with a backend service is quite similar across different frontend and backend frameworks, depending on the chosen network communication mechanism. This tutorial demonstrated how you would integrate your Flutter application on the frontend with a Flask service on the backend through Restful API architecture. The concept of HTTP communication adopted here can be applied to other frontend and backend frameworks you may choose.
You can check out the codebase for the Flutter app in this repo, and here’s the code for the Flask service.

Discover what’s new in The Replay, LogRocket’s newsletter for dev and engineering leaders, in the November 5th issue.

A senior developer discusses how developer elitism breeds contempt and over-reliance on AI, and how you can avoid it in your own workplace.

Examine AgentKit, Open AI’s new tool for building agents. Conduct a side-by-side comparison with n8n by building AI agents with each tool.

AI agents powered by MCP are redefining interfaces, shifting from clicks to intelligent, context-aware conversations.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up now
One Reply to "Integrating Flask and Flutter apps"
Hey!
I really appreciated this example!
Thank you so much, I was able to reproduce it and learned a lot along the way 🙂
I would like to leave some typos I found just in case somebody else might want to try it too, if I may.
@lib/widgets/tasks.dart:
49 FutureBuilder(
50 future: Provider.of(context, listen: false).getTodos,
51 builder: (ctx, snapshot) => snapshot.connection state == ConnectionState.waiting
In line 51, instead of snapshot.connection state, it is snapshot.connectionState == …
51 builder: (ctx, snapshot) => snapshot.connectionState == ConnectionState.waiting
@lib/providers/todo_provider.dart:
23 final todo = TodoItem(
24 id: responsePayload[“id”],
25 itemName: responsePayload[“name”],
26 isExecuted: responsePayload[“is_executed”],
27 );
29 getTodos;
30 }
31
In line 26, I added isExecuted: responsePayload[“is_executed”] as it was required and also called getTodos in order to update the list after a new Entry has been created.
The files in flutter are located in
lib/models/todo_item.dart, lib/providers/todo_provider.dart and lib/widgets/tasks.dart.
It took me some time to figure this out, that is why I thought about sharing it.
Thank you again for the nice example 🙂