flutter-http-json-networking#
한마디로#
이 스킬은 Flutter 앱이 인터넷 서버와 대화하는 방법을 정리한 안내서입니다. 앱이 서버에 "이 정보 주세요"라고 묻고 답을 받아오는 과정을, 우체국에 편지를 보내고 답장을 받는 일에 비유할 수 있습니다. 또한 서버가 보내준 데이터(JSON)를 앱이 알아들을 수 있는 모양으로 풀어내는 규칙도 담고 있습니다. 한마디로, 앱과 서버 사이의 "심부름"과 "통역"을 안전하게 처리하는 표준 방법입니다.
무엇을·언제#
- 무엇을 해주나요: 앱이 서버에서 데이터를 가져오거나(GET), 새로 만들거나(POST), 바꾸거나(PUT), 지우는(DELETE) 요청을 안전하게 처리하도록 도와줍니다.
- 서버에서 온 데이터를 앱이 쓸 수 있는 형태로 변환(통역)하고, 그 반대 방향도 처리합니다.
- 데이터가 아주 많을 때는 화면이 버벅이지 않도록 작업을 뒤에서 따로 돌립니다.
- AI 모델(예: Gemini)에게 정해진 형식의 답을 받도록 규칙을 정하는 경우도 다룹니다.
- 언제 작동하나요: 앱이 REST API(서버)를 호출해야 하거나, JSON 데이터를 주고받아야 할 때 이 안내서가 사용됩니다.
핵심 용어#
| 용어 | 쉬운 설명 |
|---|---|
| HTTP | 앱과 서버가 인터넷에서 주고받는 대화 방식(우편 규칙 같은 것) |
| REST API | 서버가 "이런 식으로 요청하세요"라고 정해 둔 창구 |
| GET / POST / PUT / DELETE | 각각 가져오기 / 새로 만들기 / 바꾸기 / 지우기 요청 |
| JSON | 데이터를 주고받을 때 쓰는 표준 글자 형식(서버의 공통 언어) |
| 직렬화(serialization) | 앱의 데이터를 JSON 글자로 바꾸는 것(편지로 옮겨 적기) |
| 역직렬화(deserialization) | 받은 JSON을 앱의 데이터로 푸는 것(편지를 읽어 이해하기) |
| isolate / compute | 무거운 작업을 별도 작업 공간에서 돌려 화면이 멈추지 않게 하는 방법 |
| 상태 코드(status code) | 요청 결과를 알려주는 숫자(200=성공, 201=새로 만듦 등) |
| LLM | 사람 말처럼 답을 만들어 주는 AI 모델(예: Gemini) |
Goal#
Manages HTTP networking and JSON data handling in Flutter applications. Implements secure, asynchronous REST API calls (GET, POST, PUT, DELETE) using the
http package. Handles JSON serialization, background parsing via isolates for large datasets, and structured JSON schemas for AI model integrations. Assumes the
http package is added to pubspec.yaml and the environment supports Dart 3 pattern matching and null safety.
Decision Logic#
When implementing JSON parsing and serialization, evaluate the following decision tree:
-
Payload Size:
- If the JSON payload is small, parse synchronously on the main thread.
- If the JSON payload is large (takes >16ms to parse), use background parsing via
compute()to avoid UI jank.
-
Model Complexity:
- If the data model is simple or a quick prototype, use manual serialization (
dart:convert). - If the data model is highly nested or part of a large production app, STOP AND ASK THE USER:
"Should we configure
json_serializableandbuild_runnerfor automated code generation?"
- If the data model is simple or a quick prototype, use manual serialization (
Instructions#
1. Configure Platform Permissions#
Before making network requests, ensure the target platforms have the required internet permissions.
Android (android/app/src/main/AndroidManifest.xml):
< manifest xmlns:android= " http://schemas.android.com/apk/res/android " >
< !-- Required to fetch data from the internet. -- >
< uses-permission android:name= " android.permission.INTERNET " / >
< application ... >
< /manifest >
macOS (macos/Runner/DebugProfile.entitlements and Release.entitlements):
< dict >
< !-- Required to fetch data from the internet. -- >
< key > com.apple.security.network.client < /key >
< true/ >
< /dict >
2. Define the JSON Data Model#
Create a strongly typed Dart class to represent the JSON data. Use factory constructors for deserialization and a
toJson method for serialization.
import 'dart:convert';
class ItemModel {
final int id;
final String title;
const ItemModel({required this.id, required this.title});
// Deserialize using Dart 3 pattern matching
factory ItemModel.fromJson(Map<String, dynamic> json) {
return switch (json) {
{'id': int id, 'title': String title} => ItemModel(id: id, title: title),
_ => throw const FormatException('Failed to parse ItemModel.'),
};
}
// Serialize to JSON
Map<String, dynamic> toJson() => {
'id': id,
'title': title,
};
}
3. Implement HTTP Operations (CRUD)#
Use the http package to perform network requests. Always use Uri.https for safe URL encoding. Validate the status code and throw exceptions on failure.
import 'dart:convert';
import 'package:http/http.dart' as http;
class ApiService {
final http.Client client;
ApiService(this.client);
// GET Request
Future<ItemModel> fetchItem(int id) async {
final uri = Uri.https('api.example.com', '/items/$id');
final response = await client.get(uri);
if (response.statusCode == 200) {
return ItemModel.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
} else {
throw Exception('Failed to load item: ${response.statusCode}');
}
}
// POST Request
Future<ItemModel> createItem(String title) async {
final uri = Uri.https('api.example.com', '/items');
final response = await client.post(
uri,
headers: <String, String>{'Content-Type': 'application/json; charset=UTF-8'},
body: jsonEncode(<String, String>{'title': title}),
);
if (response.statusCode == 201) {
return ItemModel.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
} else {
throw Exception('Failed to create item: ${response.statusCode}');
}
}
// DELETE Request
Future<void> deleteItem(int id) async {
final uri = Uri.https('api.example.com', '/items/$id');
final response = await client.delete(
uri,
headers: <String, String>{'Content-Type': 'application/json; charset=UTF-8'},
);
if (response.statusCode != 200) {
throw Exception('Failed to delete item: ${response.statusCode}');
}
}
}
4. Implement Background Parsing for Large JSON Arrays#
If fetching a large list of objects, move the JSON decoding and mapping to a separate isolate using compute().
import 'package:flutter/foundation.dart';
// Top-level function required for compute()
List<ItemModel> parseItems(String responseBody) {
final parsed = (jsonDecode(responseBody) as List<Object?>).cast<Map<String, Object?>>();
return parsed.map<ItemModel>(ItemModel.fromJson).toList();
}
Future<List<ItemModel>> fetchLargeItemList(http.Client client) async {
final uri = Uri.https('api.example.com', '/items');
final response = await client.get(uri);
if (response.statusCode == 200) {
// Run parseItems in a separate isolate
return compute(parseItems, response.body);
} else {
throw Exception('Failed to load items');
}
}
5. Define Structured JSON Output for AI Models#
When integrating LLMs (like Gemini), enforce reliable JSON output by passing a strict schema in the generation configuration and system instructions.
import 'package:firebase_vertexai/firebase_vertexai.dart';
// Define the expected JSON schema
final _responseSchema = Schema(
SchemaType.object,
properties: {
'width': Schema(SchemaType.integer),
'height': Schema(SchemaType.integer),
'items': Schema(
SchemaType.array,
items: Schema(
SchemaType.object,
properties: {
'id': Schema(SchemaType.integer),
'name': Schema(SchemaType.string),
},
),
),
},
);
// Initialize the model with the schema
final model = FirebaseAI.googleAI().generativeModel(
model: 'gemini-2.5-pro',
generationConfig: GenerationConfig(
responseMimeType: 'application/json',
responseSchema: _responseSchema,
),
);
Future<Map<String, dynamic>> analyzeData(String prompt) async {
final content = [Content.text(prompt)];
final response = await model.generateContent(content);
// Safely decode the guaranteed JSON response
return jsonDecode(response.text!) as Map<String, dynamic>;
}
Constraints#
-
Immutable URL Construction: Always use
Uri.https()orUri.parse()to build URLs. Never use raw string concatenation for endpoints with query parameters. -
Error Handling: Never return
nullon a failed network request. Always throw anExceptionor a custom error class so the UI (e.g.,FutureBuilder) can catch and display the error state viasnapshot.hasError. -
Status Code Validation: Always validate
response.statusCode. Use200for successful GET/PUT/DELETE and201for successful POST. -
Library Restriction: Do not use
dart:ioHttpClientdirectly for standard cross-platform networking. Always use thehttppackage to ensure web compatibility. -
Isolate Communication: When using
compute(), ensure the parsing function is a top-level function or a static method, and only pass primitive values or simple objects (likeStringresponse bodies) across the isolate boundary. Do not passhttp.Responseobjects.