vexio 1.0.0
vexio: ^1.0.0 copied to clipboard
Vexio — The Ultimate Flutter HTTP Client. Zero dependencies, simple as http, powerful as Dio + Copper combined. Auto token refresh, caching, retry, WebSocket, GraphQL, offline queue, multipart, interc [...]
// ============================================================
// example/lib/main.dart — Vexio Full Demo App
// ============================================================
import 'package:flutter/material.dart';
import 'package:vexio/vexio.dart';
// ── Models ────────────────────────────────────────────────────
class User {
final int id;
final String name;
final String email;
const User({required this.id, required this.name, required this.email});
factory User.fromJson(dynamic json) => User(
id: (json['id'] as num).toInt(),
name: json['name'] as String,
email: json['email'] as String,
);
@override
String toString() => 'User($id, $name, $email)';
}
class Post {
final int id;
final String title;
final String body;
const Post({required this.id, required this.title, required this.body});
factory Post.fromJson(dynamic json) => Post(
id: (json['id'] as num).toInt(),
title: json['title'] as String,
body: json['body'] as String,
);
}
// ── Main ──────────────────────────────────────────────────────
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Vexio.init(
baseUrl: 'https://jsonplaceholder.typicode.com',
enableCache: true,
// Envelope keys — jsonplaceholder returns raw arrays/objects
// so we treat any 2xx as success
successKey: 'id',
successValue: null, // raw responses — always treat 2xx as success
);
runApp(const VexioExampleApp());
}
// ── App ───────────────────────────────────────────────────────
class VexioExampleApp extends StatelessWidget {
const VexioExampleApp({super.key});
@override
Widget build(BuildContext context) => MaterialApp(
title: 'Vexio Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const HomePage(),
);
}
// ── Home ──────────────────────────────────────────────────────
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
int _tab = 0;
final _pages = const [UsersPage(), PostsPage(), FeaturesPage()];
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: const Text('⚡ Vexio Demo'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: _pages[_tab],
bottomNavigationBar: BottomNavigationBar(
currentIndex: _tab,
onTap: (i) => setState(() => _tab = i),
items: const [
BottomNavigationBarItem(icon: Icon(Icons.people), label: 'Users'),
BottomNavigationBarItem(icon: Icon(Icons.article), label: 'Posts'),
BottomNavigationBarItem(icon: Icon(Icons.bolt), label: 'Features'),
],
),
);
}
// ── Users Page — GET list ─────────────────────────────────────
class UsersPage extends StatefulWidget {
const UsersPage({super.key});
@override
State<UsersPage> createState() => _UsersPageState();
}
class _UsersPageState extends State<UsersPage> {
List<User> _users = [];
bool _loading = false;
String? _error;
// final bool _fromCache = false;
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
setState(() { _loading = true; _error = null; });
await Vexio.instance
.get('/users',
parser: (json) => (json as List).map(User.fromJson).toList(),
useCache: true)
.handle(
onSuccess: (data, _) => setState(() {
_users = data ?? [];
_loading = false;
}),
onFailure: (msg, _) => setState(() {
_error = msg;
_loading = false;
}),
);
}
@override
Widget build(BuildContext context) {
if (_loading) return const Center(child: CircularProgressIndicator());
if (_error != null) {
return Center(child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Error: $_error', style: const TextStyle(color: Colors.red)),
ElevatedButton(onPressed: _load, child: const Text('Retry')),
],
));
}
return RefreshIndicator(
onRefresh: _load,
child: ListView.builder(
itemCount: _users.length,
itemBuilder: (_, i) {
final u = _users[i];
return ListTile(
leading: CircleAvatar(child: Text('${u.id}')),
title: Text(u.name),
subtitle: Text(u.email),
);
},
),
);
}
}
// ── Posts Page — GET with cancel token ───────────────────────
class PostsPage extends StatefulWidget {
const PostsPage({super.key});
@override
State<PostsPage> createState() => _PostsPageState();
}
class _PostsPageState extends State<PostsPage> {
List<Post> _posts = [];
bool _loading = false;
String? _error;
VexioCancelToken? _token;
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
_token?.cancel();
_token = VexioCancelToken();
setState(() { _loading = true; _error = null; });
try {
final res = await Vexio.instance.get('/posts',
parser: (json) => (json as List).map(Post.fromJson).toList(),
cancelToken: _token,
queryParams: {'_limit': 20},
);
if (mounted) {
setState(() {
_posts = res.data ?? [];
_loading = false;
});
}
} on VexioCancelledException {
// Navigated away
} catch (e) {
if (mounted) setState(() { _error = e.toString(); _loading = false; });
}
}
@override
void dispose() {
_token?.cancel('Widget disposed');
super.dispose();
}
@override
Widget build(BuildContext context) {
if (_loading) return const Center(child: CircularProgressIndicator());
if (_error != null) return Center(child: Text('Error: $_error'));
return ListView.builder(
itemCount: _posts.length,
itemBuilder: (_, i) {
final p = _posts[i];
return ListTile(
leading: CircleAvatar(child: Text('${p.id}')),
title: Text(p.title, maxLines: 1, overflow: TextOverflow.ellipsis),
subtitle: Text(p.body, maxLines: 2, overflow: TextOverflow.ellipsis),
);
},
);
}
}
// ── Features Page — showcase all Vexio features ───────────────
class FeaturesPage extends StatefulWidget {
const FeaturesPage({super.key});
@override
State<FeaturesPage> createState() => _FeaturesPageState();
}
class _FeaturesPageState extends State<FeaturesPage> {
String _result = 'Tap a button to test a feature';
bool _loading = false;
void _setResult(String r) =>
setState(() { _result = r; _loading = false; });
void _setLoading() => setState(() { _loading = true; });
// ── Feature demos ─────────────────────────────────────────────
Future<void> _testGet() async {
_setLoading();
final res = await Vexio.instance.get('/users/1', parser: User.fromJson);
_setResult(res.isSuccess
? 'GET ✅\nUser: ${res.data?.name}\nTime: ${res.responseTime?.inMilliseconds}ms'
: 'GET ❌ ${res.message}');
}
Future<void> _testPost() async {
_setLoading();
final res = await Vexio.instance.post('/posts',
body: {'title': 'Vexio Post', 'body': 'Hello from Vexio', 'userId': 1},
);
_setResult(res.isSuccess
? 'POST ✅\nStatus: ${res.statusCode}'
: 'POST ❌ ${res.message}');
}
Future<void> _testCache() async {
_setLoading();
// First call — network
final t1 = DateTime.now();
await Vexio.instance.get('/users/2', useCache: true);
final net = DateTime.now().difference(t1).inMilliseconds;
// Second call — cache
final t2 = DateTime.now();
final res = await Vexio.instance.get('/users/2',
parser: User.fromJson, useCache: true);
final cached = DateTime.now().difference(t2).inMilliseconds;
_setResult('Cache demo ✅\n'
'Network: ${net}ms\n'
'Cache: ${cached}ms\n'
'fromCache: ${res.fromCache}');
}
Future<void> _testBatch() async {
_setLoading();
final results = await Vexio.instance.batch([
() => Vexio.instance.get('/users/1', parser: User.fromJson),
() => Vexio.instance.get('/users/2', parser: User.fromJson),
() => Vexio.instance.get('/users/3', parser: User.fromJson),
]);
final names = results
.where((r) => r.isSuccess)
.map((r) => (r.data as User).name)
.join(', ');
_setResult('Batch ✅\nFetched in parallel:\n$names');
}
Future<void> _testExtensions() async {
_setLoading();
String? name;
int? nameLength;
await Vexio.instance
.get('/users/4', parser: User.fromJson)
.onSuccess((u) => name = u?.name)
.mapData((u) => u.name.length)
.handle(
onSuccess: (len, _) => nameLength = len,
onFailure: (msg, _) => _setResult('Extensions ❌ $msg'),
);
_setResult('Extensions ✅\nUser name: $name\nName length: $nameLength');
}
Future<void> _testTypedErrors() async {
_setLoading();
try {
await Vexio.instance.get('/users/99999999',
parser: User.fromJson);
_setResult('No error thrown (server returned 2xx)');
} on VexioNoInternetException {
_setResult('VexioNoInternetException ✅');
} on VexioTimeoutException {
_setResult('VexioTimeoutException ✅');
} on VexioServerException catch (e) {
_setResult('VexioServerException ✅\nCode: ${e.statusCode}');
} on VexioException catch (e) {
_setResult('VexioException ✅\n${e.runtimeType}: ${e.message}');
}
}
Future<void> _testCancel() async {
_setLoading();
final token = VexioCancelToken();
final future = Vexio.instance.get('/posts',
parser: (j) => (j as List).length,
cancelToken: token);
// Cancel immediately
token.cancel('Demo cancellation');
try {
await future;
_setResult('Request completed before cancel');
} on VexioCancelledException catch (e) {
_setResult('Cancel ✅\nReason: ${e.message}');
}
}
Future<void> _testConnectivity() async {
_setLoading();
final online = await Vexio.instance.isOnline();
_setResult('Connectivity ✅\nOnline: $online');
}
Future<void> _testWebSocket() async {
_setLoading();
// Demo with a public echo WS server
final ws = VexioWebSocket(
url: 'wss://echo.websocket.events',
autoReconnect: false,
);
try {
await ws.connect().timeout(const Duration(seconds: 5));
ws.send('Hello from Vexio!');
final msg = await ws.messages.first
.timeout(const Duration(seconds: 5));
_setResult('WebSocket ✅\nEcho: $msg');
} catch (e) {
_setResult('WebSocket demo\n(No public echo server — '
'use wss://your-server.com/ws in production)\n\nStatus: ${ws.status}');
} finally {
ws.dispose();
}
}
// ── Build ─────────────────────────────────────────────────────
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Result display
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.black87,
borderRadius: BorderRadius.circular(12),
),
child: _loading
? const Center(
child: CircularProgressIndicator(color: Colors.white))
: Text(_result,
style: const TextStyle(
color: Colors.greenAccent,
fontFamily: 'monospace',
fontSize: 13)),
),
const SizedBox(height: 16),
// Feature buttons
_FeatureButton('GET Request', Icons.download, _testGet),
_FeatureButton('POST Request', Icons.upload, _testPost),
_FeatureButton('Cache Demo', Icons.cached, _testCache),
_FeatureButton('Batch Requests', Icons.layers, _testBatch),
_FeatureButton('Fluent Extensions', Icons.link, _testExtensions),
_FeatureButton('Typed Exceptions', Icons.error, _testTypedErrors),
_FeatureButton('Cancel Token', Icons.cancel, _testCancel),
_FeatureButton('Connectivity', Icons.wifi, _testConnectivity),
_FeatureButton('WebSocket', Icons.cable, _testWebSocket),
],
),
);
}
}
class _FeatureButton extends StatelessWidget {
final String label;
final IconData icon;
final VoidCallback onTap;
const _FeatureButton(this.label, this.icon, this.onTap);
@override
Widget build(BuildContext context) => Padding(
padding: const EdgeInsets.only(bottom: 8),
child: ElevatedButton.icon(
onPressed: onTap,
icon: Icon(icon),
label: Text(label),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14),
),
),
);
}