Installation
Addtrailbase to your pubspec.yaml:
pubspec.yaml
dependencies:
trailbase: ^0.7.2
dart pub get
flutter pub get
Initialization
Basic Client
import 'package:trailbase/trailbase.dart';
final client = Client('https://your-server.trailbase.io');
Client with Tokens
Restore a previous session with saved tokens:final tokens = Tokens(authToken, refreshToken, csrfToken);
final client = await Client.withTokens(
'https://your-server.trailbase.io',
tokens,
);
Auth State Listener
final client = Client(
'https://your-server.trailbase.io',
onAuthChange: (client, tokens) {
if (tokens != null) {
print('User logged in');
// Persist tokens
saveTokens(tokens);
} else {
print('User logged out');
// Clear persisted tokens
}
},
);
Authentication
Login
try {
final tokens = await client.login('[email protected]', 'password');
print('Auth token: ${tokens.auth}');
final user = client.user();
print('Logged in as: ${user?.email}');
} catch (e) {
print('Login failed: $e');
}
Login with OAuth (PKCE)
import 'package:trailbase/trailbase.dart';
// Generate PKCE code verifier and challenge
final pkce = PkceHelper.generate();
// Open OAuth authorization URL with the challenge
// After callback with authorization code:
final tokens = await client.loginWithAuthCode(
authorizationCode,
pkceCodeVerifier: pkce.codeVerifier,
);
Logout
await client.logout();
Current User
final user = client.user();
if (user != null) {
print('User ID: ${user.id}');
print('Email: ${user.email}');
}
Access Tokens
final tokens = client.tokens();
if (tokens != null) {
// Persist tokens for later use
await storage.save('tokens', tokens);
}
Refresh Token
await client.refreshAuthToken();
Record API
List Records
final posts = client.records('posts');
final response = await posts.list(
pagination: Pagination(limit: 10, offset: 0),
order: ['-created_at'],
count: true,
);
print('Records: ${response.records}');
print('Total count: ${response.totalCount}');
print('Next cursor: ${response.cursor}');
Read a Record
// Using integer ID
final post = await posts.read(123.id());
// Using UUID string
final post = await posts.read('uuid-string'.id());
// With expanded relationships
final postWithAuthor = await posts.read(
postId.id(),
expand: ['author'],
);
print('Title: ${post['title']}');
Create a Record
final newPost = {
'title': 'Hello World',
'content': 'My first post from Dart',
};
final id = await posts.create(newPost);
print('Created post with ID: $id');
Create Multiple Records
final newPosts = [
{'title': 'Post 1', 'content': 'Content 1'},
{'title': 'Post 2', 'content': 'Content 2'},
];
final ids = await posts.createBulk(newPosts);
print('Created ${ids.length} posts');
Update a Record
await posts.update(
postId.id(),
{'title': 'Updated Title'},
);
Delete a Record
await posts.delete(postId.id());
Filtering
// Simple equality filter
final response = await posts.list(
filters: [
Filter(
column: 'author_id',
value: userId,
),
],
);
// With comparison operators
final recentPosts = await posts.list(
filters: [
Filter(
column: 'created_at',
op: CompareOp.greaterThan,
value: DateTime.now().subtract(Duration(days: 7)).toString(),
),
],
);
// LIKE operator
final searchResults = await posts.list(
filters: [
Filter(
column: 'title',
op: CompareOp.like,
value: '%search%',
),
],
);
// AND composite filter
final filtered = await posts.list(
filters: [
And([
Filter(column: 'status', value: 'published'),
Filter(column: 'author_id', value: userId),
]),
],
);
// OR composite filter
final filtered = await posts.list(
filters: [
Or([
Filter(column: 'category', value: 'tech'),
Filter(column: 'category', value: 'science'),
]),
],
);
Available Comparison Operators
enum CompareOp {
equal,
notEqual,
lessThan,
lessThanEqual,
greaterThan,
greaterThanEqual,
like,
regexp,
stWithin, // Geospatial: within
stIntersects, // Geospatial: intersects
stContains, // Geospatial: contains
}
Real-time Subscriptions
Subscribe to a Single Record
final stream = await posts.subscribe(postId.id());
await for (final event in stream) {
if (event is Insert) {
print('Record inserted: ${event.record}');
} else if (event is Update) {
print('Record updated: ${event.record}');
} else if (event is Delete) {
print('Record deleted: ${event.record}');
} else if (event is Error) {
print('Error: ${event.message}');
}
}
Subscribe to All Records
final stream = await posts.subscribeAll(
filters: [
Filter(column: 'author_id', value: userId),
],
);
await for (final event in stream) {
print('Change event: $event');
}
File Handling
Image URIs
final posts = client.records('posts');
// Single file
final coverImageUri = posts.imageUri(
postId.id(),
'cover_image',
);
// File from files array
final attachmentUri = posts.imageUri(
postId.id(),
'attachments',
filename: 'document.pdf',
);
// Use in Flutter
Image.network(coverImageUri.toString())
Record ID Types
TrailBase supports both integer and UUID record IDs:// Integer ID
final intId = 123.id();
final intId2 = RecordId.integer(123);
// UUID string ID
final uuidId = 'uuid-string'.id();
final uuidId2 = RecordId.uuid('uuid-string');
// Use in operations
await posts.read(intId);
await posts.update(uuidId, data);
Error Handling
try {
final post = await posts.read(postId.id());
} on HttpException catch (e) {
print('HTTP ${e.statusCode}: ${e.body}');
} catch (e) {
print('Error: $e');
}
Flutter Integration Example
import 'package:flutter/material.dart';
import 'package:trailbase/trailbase.dart';
class PostsList extends StatefulWidget {
@override
_PostsListState createState() => _PostsListState();
}
class _PostsListState extends State<PostsList> {
late Client client;
List<Map<String, dynamic>> posts = [];
bool loading = true;
@override
void initState() {
super.initState();
client = Client('https://your-server.trailbase.io');
loadPosts();
}
Future<void> loadPosts() async {
try {
final response = await client.records('posts').list(
order: ['-created_at'],
pagination: Pagination(limit: 20),
);
setState(() {
posts = response.records;
loading = false;
});
} catch (e) {
print('Error loading posts: $e');
setState(() => loading = false);
}
}
@override
Widget build(BuildContext context) {
if (loading) {
return Center(child: CircularProgressIndicator());
}
return ListView.builder(
itemCount: posts.length,
itemBuilder: (context, index) {
final post = posts[index];
return ListTile(
title: Text(post['title']),
subtitle: Text(post['content']),
);
},
);
}
}
Type Definitions
User
class User {
final String id;
final String email;
}
Tokens
class Tokens {
final String auth;
final String? refresh;
final String? csrf;
}
ListResponse
class ListResponse {
final String? cursor;
final List<Map<String, dynamic>> records;
final int? totalCount;
}
Pagination
class Pagination {
final String? cursor;
final int? limit;
final int? offset;
}
Best Practices
Use the
onAuthChange callback to persist tokens and update your app’s authentication state.Store tokens securely using packages like
flutter_secure_storage instead of shared preferences.The client automatically refreshes auth tokens before they expire.