Flutter Integration
Build cross-platform mobile applications with Flutter connected to your APSO backend.
Setup
Add Dependencies
# pubspec.yaml
dependencies:
http: ^1.1.0
shared_preferences: ^2.2.0
provider: ^6.1.0Create API Client
// lib/services/api_client.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
class ApiClient {
static const String baseUrl = 'https://api.yourapp.com';
String? _token;
Future<void> init() async {
final prefs = await SharedPreferences.getInstance();
_token = prefs.getString('token');
}
Future<void> setToken(String token) async {
_token = token;
final prefs = await SharedPreferences.getInstance();
await prefs.setString('token', token);
}
Future<void> clearToken() async {
_token = null;
final prefs = await SharedPreferences.getInstance();
await prefs.remove('token');
}
Map<String, String> get _headers => {
'Content-Type': 'application/json',
if (_token != null) 'Authorization': 'Bearer $_token',
};
Future<dynamic> get(String path) async {
final response = await http.get(
Uri.parse('$baseUrl$path'),
headers: _headers,
);
return _handleResponse(response);
}
Future<dynamic> post(String path, Map<String, dynamic> body) async {
final response = await http.post(
Uri.parse('$baseUrl$path'),
headers: _headers,
body: jsonEncode(body),
);
return _handleResponse(response);
}
Future<dynamic> patch(String path, Map<String, dynamic> body) async {
final response = await http.patch(
Uri.parse('$baseUrl$path'),
headers: _headers,
body: jsonEncode(body),
);
return _handleResponse(response);
}
Future<void> delete(String path) async {
final response = await http.delete(
Uri.parse('$baseUrl$path'),
headers: _headers,
);
_handleResponse(response);
}
dynamic _handleResponse(http.Response response) {
if (response.statusCode >= 200 && response.statusCode < 300) {
if (response.body.isEmpty) return null;
return jsonDecode(response.body);
}
throw ApiException(
statusCode: response.statusCode,
message: response.body,
);
}
}
class ApiException implements Exception {
final int statusCode;
final String message;
ApiException({required this.statusCode, required this.message});
}Models
// lib/models/project.dart
class Project {
final String id;
final String name;
final String? description;
final String organizationId;
final DateTime createdAt;
final DateTime updatedAt;
final List<Task>? tasks;
Project({
required this.id,
required this.name,
this.description,
required this.organizationId,
required this.createdAt,
required this.updatedAt,
this.tasks,
});
factory Project.fromJson(Map<String, dynamic> json) {
return Project(
id: json['id'],
name: json['name'],
description: json['description'],
organizationId: json['organizationId'],
createdAt: DateTime.parse(json['createdAt']),
updatedAt: DateTime.parse(json['updatedAt']),
tasks: json['tasks'] != null
? (json['tasks'] as List).map((t) => Task.fromJson(t)).toList()
: null,
);
}
}Services
// lib/services/project_service.dart
import 'api_client.dart';
import '../models/project.dart';
class ProjectService {
final ApiClient _client;
ProjectService(this._client);
Future<List<Project>> getAll({int? limit, int? offset}) async {
var path = '/api/v1/projects';
final params = <String>[];
if (limit != null) params.add('limit=$limit');
if (offset != null) params.add('offset=$offset');
if (params.isNotEmpty) path += '?${params.join('&')}';
final data = await _client.get(path);
return (data['data'] as List).map((p) => Project.fromJson(p)).toList();
}
Future<Project> getById(String id, {bool includeTasks = false}) async {
var path = '/api/v1/projects/$id';
if (includeTasks) path += '?include=tasks';
final data = await _client.get(path);
return Project.fromJson(data);
}
Future<Project> create({
required String name,
String? description,
}) async {
final data = await _client.post('/api/v1/projects', {
'name': name,
if (description != null) 'description': description,
});
return Project.fromJson(data);
}
Future<Project> update(String id, {String? name, String? description}) async {
final data = await _client.patch('/api/v1/projects/$id', {
if (name != null) 'name': name,
if (description != null) 'description': description,
});
return Project.fromJson(data);
}
Future<void> delete(String id) async {
await _client.delete('/api/v1/projects/$id');
}
}State Management
Auth Provider
// lib/providers/auth_provider.dart
import 'package:flutter/material.dart';
import '../services/api_client.dart';
class User {
final String id;
final String email;
final String name;
User({required this.id, required this.email, required this.name});
factory User.fromJson(Map<String, dynamic> json) {
return User(id: json['id'], email: json['email'], name: json['name']);
}
}
class AuthProvider extends ChangeNotifier {
final ApiClient _client;
User? _user;
bool _loading = true;
AuthProvider(this._client);
User? get user => _user;
bool get loading => _loading;
bool get isAuthenticated => _user != null;
Future<void> init() async {
await _client.init();
try {
final data = await _client.get('/api/v1/auth/me');
_user = User.fromJson(data);
} catch (_) {
// Not authenticated
}
_loading = false;
notifyListeners();
}
Future<void> login(String email, String password) async {
final data = await _client.post('/api/v1/auth/login', {
'email': email,
'password': password,
});
await _client.setToken(data['token']);
_user = User.fromJson(data['user']);
notifyListeners();
}
Future<void> logout() async {
await _client.clearToken();
_user = null;
notifyListeners();
}
}Projects Provider
// lib/providers/projects_provider.dart
import 'package:flutter/material.dart';
import '../models/project.dart';
import '../services/project_service.dart';
class ProjectsProvider extends ChangeNotifier {
final ProjectService _service;
List<Project> _projects = [];
bool _loading = false;
String? _error;
ProjectsProvider(this._service);
List<Project> get projects => _projects;
bool get loading => _loading;
String? get error => _error;
Future<void> fetchProjects() async {
_loading = true;
_error = null;
notifyListeners();
try {
_projects = await _service.getAll();
} catch (e) {
_error = e.toString();
}
_loading = false;
notifyListeners();
}
Future<void> createProject(String name, String? description) async {
final project = await _service.create(
name: name,
description: description,
);
_projects.insert(0, project);
notifyListeners();
}
Future<void> deleteProject(String id) async {
await _service.delete(id);
_projects.removeWhere((p) => p.id == id);
notifyListeners();
}
}Screens
Projects List
// lib/screens/projects_screen.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/projects_provider.dart';
class ProjectsScreen extends StatefulWidget {
@override
_ProjectsScreenState createState() => _ProjectsScreenState();
}
class _ProjectsScreenState extends State<ProjectsScreen> {
@override
void initState() {
super.initState();
context.read<ProjectsProvider>().fetchProjects();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Projects')),
body: Consumer<ProjectsProvider>(
builder: (context, provider, _) {
if (provider.loading) {
return Center(child: CircularProgressIndicator());
}
if (provider.error != null) {
return Center(child: Text('Error: ${provider.error}'));
}
return RefreshIndicator(
onRefresh: provider.fetchProjects,
child: ListView.builder(
itemCount: provider.projects.length,
itemBuilder: (context, index) {
final project = provider.projects[index];
return ListTile(
title: Text(project.name),
subtitle: Text(project.description ?? ''),
onTap: () => Navigator.pushNamed(
context,
'/project',
arguments: project.id,
),
);
},
),
);
},
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () => Navigator.pushNamed(context, '/create-project'),
),
);
}
}Create Project
// lib/screens/create_project_screen.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/projects_provider.dart';
class CreateProjectScreen extends StatefulWidget {
@override
_CreateProjectScreenState createState() => _CreateProjectScreenState();
}
class _CreateProjectScreenState extends State<CreateProjectScreen> {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
final _descriptionController = TextEditingController();
bool _loading = false;
Future<void> _submit() async {
if (!_formKey.currentState!.validate()) return;
setState(() => _loading = true);
try {
await context.read<ProjectsProvider>().createProject(
_nameController.text,
_descriptionController.text.isNotEmpty
? _descriptionController.text
: null,
);
Navigator.pop(context);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: $e')),
);
}
setState(() => _loading = false);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Create Project')),
body: Form(
key: _formKey,
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
children: [
TextFormField(
controller: _nameController,
decoration: InputDecoration(labelText: 'Name'),
validator: (v) => v!.isEmpty ? 'Required' : null,
),
SizedBox(height: 16),
TextFormField(
controller: _descriptionController,
decoration: InputDecoration(labelText: 'Description'),
maxLines: 3,
),
SizedBox(height: 24),
ElevatedButton(
onPressed: _loading ? null : _submit,
child: _loading
? CircularProgressIndicator()
: Text('Create'),
),
],
),
),
),
);
}
}App Setup
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'services/api_client.dart';
import 'services/project_service.dart';
import 'providers/auth_provider.dart';
import 'providers/projects_provider.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final apiClient = ApiClient();
final authProvider = AuthProvider(apiClient);
await authProvider.init();
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider.value(value: authProvider),
ChangeNotifierProvider(
create: (_) => ProjectsProvider(ProjectService(apiClient)),
),
],
child: MyApp(),
),
);
}Related
Last updated on