19 HTTP: POST
Great! Let’s dive into HTTP POST requests and the FutureBuilder
widget. I’ll break this down into digestible parts.
19.1 Understanding HTTP POST vs GET
The key difference between POST and GET requests:
GET: Request data from server (read operation)
POST: Send data to server to create something new (write operation)
19.2 Step 1: The HTTP POST Request Function
Future<Album> createAlbum(String title) async {
final response = await http.post(
.parse('https://jsonplaceholder.typicode.com/albums'),
Uri: <String, String>{
headers'Content-Type': 'application/json; charset=UTF-8',
},
: jsonEncode(<String, String>{'title': title}),
body
);
if (response.statusCode == 201) {
return Album.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
} else {
throw Exception('Failed to create album.');
}
}
Let’s break down the POST request components:
URL: Notice we’re posting to /albums
(without the /1
). This is typically the collection endpoint where new resources are created.
Headers: The headers tell the server what kind of data we’re sending:
: <String, String>{
headers'Content-Type': 'application/json; charset=UTF-8',
}
This is like setting headers in Python’s requests:
= {'Content-Type': 'application/json; charset=UTF-8'} headers
Request Body: We convert our data to JSON and send it in the body:
: jsonEncode(<String, String>{'title': title}) body
This creates a JSON string like {"title": "My Album Title"}
. In Python, this would be:
= json.dumps({'title': title}) body
Status Code Check: POST requests typically return 201 Created
instead of 200 OK
, indicating a new resource was successfully created.
19.3 Step 2: Understanding the Application State
The app has two key pieces of state:
class _MyAppState extends State<MyApp> {
final TextEditingController _controller = TextEditingController();
Future<Album>? _futureAlbum;
// ...
}
TextEditingController: Manages the text input field, similar to controlled components in React or form handling in web frameworks.
**_futureAlbum**: This is nullable (Future<Album>?
) and represents the current state of our POST request:
null
= No request has been made yetFuture<Album>
= Request is in progress or completed
19.4 Step 3: The User Interface Flow
The app shows different content based on the state:
: (_futureAlbum == null) ? buildColumn() : buildFutureBuilder(), child
This creates a simple state machine:
Initial State: Show input form
After button press: Show request progress/result
19.5 Step 4: Understanding FutureBuilder
FutureBuilder
is Flutter’s way of handling asynchronous operations in the UI. It’s similar to React’s useEffect with async operations:
<Album> buildFutureBuilder() {
FutureBuilderreturn FutureBuilder<Album>(
: _futureAlbum,
future: (context, snapshot) {
builderif (snapshot.hasData) {
return Text(snapshot.data!.title);
} else if (snapshot.hasError) {
return Text('${snapshot.error}');
}
return const CircularProgressIndicator();
},
);}
How FutureBuilder Works:
Initial State: When the Future starts,
snapshot.hasData
andsnapshot.hasError
are both false, so it shows the loading spinner.Success State: When the HTTP request completes successfully,
snapshot.hasData
becomes true, andsnapshot.data
contains the Album object.Error State: If the request fails,
snapshot.hasError
becomes true, andsnapshot.error
contains the exception.
The AsyncSnapshot Object:
AsyncSnapshot<Album> snapshot
├── hasData: bool // True when data is available
├── hasError: bool // True when an error occurred
├── data: Album? // The actual data (null if no data)
└── error: Object? // The error object (null if no error)
19.6 Step 5: The Complete Request Flow
Here’s what happens when you press the “Create Data” button:
Button Press:
: () { onPressed{ setState(() = createAlbum(_controller.text); _futureAlbum }); }
State Update:
setState()
triggers a rebuild, and now_futureAlbum
is not null, sobuildFutureBuilder()
is called.HTTP Request:
createAlbum()
starts executing:- Creates JSON payload:
{"title": "user input"}
- Sends POST request with proper headers
- Waits for server response
- Creates JSON payload:
UI Updates:
FutureBuilder
automatically rebuilds as the Future progresses:- Initially: Shows
CircularProgressIndicator
- On success: Shows the created album title
- On error: Shows the error message
- Initially: Shows
19.7 Comparing to Python/JavaScript
If you were to write this in Python with a web framework like FastAPI:
import asyncio
import aiohttp
import json
async def create_album(title: str):
async with aiohttp.ClientSession() as session:
async with session.post(
'https://jsonplaceholder.typicode.com/albums',
={'Content-Type': 'application/json'},
headers={'title': title}
jsonas response:
) if response.status == 201:
= await response.json()
data return Album(**data)
else:
raise Exception('Failed to create album')
Or in JavaScript with fetch:
async function createAlbum(title) {
const response = await fetch('https://jsonplaceholder.typicode.com/albums', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
,
}body: JSON.stringify({title: title}),
;
})
if (response.status === 201) {
return await response.json();
else {
} throw new Error('Failed to create album');
} }
19.8 Key Concepts to Remember
State Management: Flutter uses setState()
to trigger UI rebuilds when data changes. This is similar to React’s useState hook.
FutureBuilder Pattern: This widget automatically manages the three states of async operations (loading, success, error) without you having to manually track them.
Immutable State: Each time you call setState()
, Flutter creates a new build of your widget tree, ensuring consistent state management.
Error Handling: The FutureBuilder
automatically catches exceptions thrown by your Future and provides them in snapshot.error
.
The beauty of this pattern is that it separates the HTTP logic from the UI logic while still providing a smooth user experience. The user gets immediate feedback (loading spinner) and clear success/error states without any additional complexity in your code.
19.9 Complete Example
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
Future<Album> createAlbum(String title) async {
final response = await http.post(
.parse('https://jsonplaceholder.typicode.com/albums'),
Uri: <String, String>{
headers'Content-Type': 'application/json; charset=UTF-8',
},
: jsonEncode(<String, String>{'title': title}),
body
);
if (response.statusCode == 201) {
// If the server did return a 201 CREATED response,
// then parse the JSON.
return Album.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
} else {
// If the server did not return a 201 CREATED response,
// then throw an exception.
throw Exception('Failed to create album.');
}
}
class Album {
final int id;
final String title;
const Album({required this.id, required this.title});
factory Album.fromJson(Map<String, dynamic> json) {
return switch (json) {
{'id': int id, 'title': String title} => Album(id: id, title: title),
=> throw const FormatException('Failed to load album.'),
_ };
}
}
void main() {
const MyApp());
runApp(}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
<MyApp> createState() {
Statereturn _MyAppState();
}
}
class _MyAppState extends State<MyApp> {
final TextEditingController _controller = TextEditingController();
Future<Album>? _futureAlbum;
@override
{
Widget build(BuildContext context) return MaterialApp(
: 'Create Data Example',
title: ThemeData(
theme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
colorScheme,
): Scaffold(
home: AppBar(title: const Text('Create Data Example')),
appBar: Container(
body: Alignment.center,
alignment: const EdgeInsets.all(8),
padding: (_futureAlbum == null) ? buildColumn() : buildFutureBuilder(),
child,
),
)
);}
{
Column buildColumn() return Column(
: MainAxisAlignment.center,
mainAxisAlignment: <Widget>[
children
TextField(: _controller,
controller: const InputDecoration(hintText: 'Enter Title'),
decoration,
)
ElevatedButton(: () {
onPressed{
setState(() = createAlbum(_controller.text);
_futureAlbum });
},
: const Text('Create Data'),
child,
),
]
);}
<Album> buildFutureBuilder() {
FutureBuilderreturn FutureBuilder<Album>(
: _futureAlbum,
future: (context, snapshot) {
builderif (snapshot.hasData) {
return Text(snapshot.data!.title);
} else if (snapshot.hasError) {
return Text('${snapshot.error}');
}
return const CircularProgressIndicator();
},
);}
}