Skip to Content
🚀 APSO is now in public beta. Get started →

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.0

Create 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(), ), ); }
Last updated on