bifrosted 0.10.2
bifrosted: ^0.10.2 copied to clipboard
The rainbow bridge connecting your app to APIs. A lightweight REST API client and repository pattern with caching, offline support, and error handling.
Bifrosted #
The rainbow bridge connecting your app to APIs.
"Bifrost" was taken..
This is mostly built for my own internal use and don't expect anyone to use this. It's purely to speed up MY development. I'm using this package to host my common shared patterns across apps, hence the name Bifrost - the bridge that connects everything.
I'm providing myself abstract classes for utility and ensure i match functionality across projects.
The first release focuses on a simple API abstractions that I use:
The ViewModel(not here yet) gets data from Repository, which uses RestAPI classes to make http requests.
Repository expects you to provide implementation of Caching, Network Connection, and System Notifier that implement their respective base class.
Also includes a custom dummy data annotation for data models to generate the fields with empty data for mocking scenarios.
Goal is to have this be used across Flutter, Jaspr web, and Dart Frog
Features #
- REST API Client - Abstract base class with GET, POST, PUT, PATCH, DELETE
- Repository Pattern - Automatic deserialization and caching
- Offline-First - Cache fallback when offline
- Error Notifications - UI-only callbacks via
SystemNotifier(logging usesbifrostLogger) - Pluggable - Bring your own storage, logging, and connectivity
Quick Start #
1. Implement the interfaces #
// Your API client
class MyAPI extends RestAPI {
@override
String get host => 'api.example.com';
@override
Map<String, String> get headers => {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
};
@override
String get shortname => 'my_api';
}
// Your storage service
class SharedPrefService implements StorageService {
late SharedPreferences _prefs;
@override
Future<void> init() async {
_prefs = await SharedPreferences.getInstance();
}
@override
String? getString(String key) => _prefs.getString(key);
@override
Future<void> setString(String key, String value) => _prefs.setString(key, value);
// ... implement other methods
}
// UI-only — do not log here (RestAPI / repository log via bifrostLogger)
class AppNotifier implements SystemNotifier {
@override
void onNetworkError() => showSnackbar('No internet connection');
@override
void onUnauthorized() => navigateTo('/login');
@override
void onRequestFailed({required int statusCode, String? body}) {
showSnackbar(statusCode >= 500 ? 'Server error' : 'Request failed');
}
}
// Your connectivity checker
class AppController implements ConnectionChecker {
@override
bool get isConnected => _hasConnection;
}
2. Create your repository #
// At app startup, once:
// bifrostServiceLocator = <T>() => Get.find<T>();
class UserRepo extends BifrostRepository {
final api = MyAPI();
Future<BifrostResult<User>> getUser(String id) => fetch<User, User>(
apiRequest: () => api.get('/users/$id'),
model: const UserModel(),
cacheKey: 'user_$id',
);
Future<BifrostResult<List<User>>> getUsers() => fetch<List<User>, User>(
apiRequest: () => api.get('/users'),
model: const UserModel(),
cacheKey: 'users',
);
}
UserModel is generated by @generateFake — it carries both fromJson and
fake(), so one model: argument covers parsing real responses and
inventing stand-ins for bifrostUseFakes.
3. Use it #
final repo = UserRepo();
switch (await repo.getUser('123')) {
case BifrostSuccess(:final data):
print(data.name);
case BifrostFailure(:final reason):
// SystemNotifier already showed the user something for `reason`.
// Render an error state here; don't notify again.
print('Could not load user: ${reason.name}');
}
Fake Data Generation #
@generateFake Annotation #
Add the annotation to your freezed models to auto-generate .fake() factory methods:
@freezed
@generateFake
class User with _$User {
factory User({
int? id,
String? name,
String? email,
}) = _User;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}
// After running build_runner:
final user = UserFake.fake(); // Generates fake data based on field names
Just run build_runner - the generator is auto-discovered (no build.yaml config needed):
dart run build_runner build
FakeUtils #
Generate fake data based on field names:
FakeUtils.fakeForKey('email'); // -> "john@example.com"
FakeUtils.fakeForKey('firstName'); // -> "John"
FakeUtils.fakeForKey('age'); // -> 42
FakeUtils.fakeForKey('isActive'); // -> true/false
FakeUtils.fakeForKey('createdAt'); // -> ISO8601 date string
// Generate generic fake JSON
final json = FakeUtils.fakeJson();
final users = FakeUtils.fakeJsonList(count: 10);
Architecture #
┌─────────────────────────────────────────────────────────────┐
│ Your App │
├─────────────────────────────────────────────────────────────┤
│ Repository (UserRepo, ProductRepo, etc.) │
│ ├── fetch<R, M>() (object or List<Model>) │
│ ├── Caching (via StorageService) │
│ └── Error handling (via SystemNotifier) │
├─────────────────────────────────────────────────────────────┤
│ REST API (MyAPI extends RestAPI) │
│ ├── get(), post(), put(), patch(), delete() │
│ └── Logging (via BifrostLogger) │
├─────────────────────────────────────────────────────────────┤
│ Interfaces (you implement these) │
│ ├── StorageService (SharedPreferences, Hive, etc.) │
│ ├── ConnectionChecker (connectivity check) │
│ └── SystemNotifier (error handling) │
├─────────────────────────────────────────────────────────────┤
│ Logging (via logger package) │
│ └── bifrostLogger (customize via bifrostLogger = ...) │
└─────────────────────────────────────────────────────────────┘
Widget test helpers #
// flutter_test_config.dart
testAppBuilder = buildTestApp;
// tests
import 'package:bifrosted/testing.dart';
await tester.pumpApp(route: '/home');
await tester.pumpWithDevice(myWidget);
Register [testAppBuilder] once, then use pumpApp, pumpTestWidget, or pumpWithDevice on [WidgetTester] via BifrostWidgetTester.
Mason bricks (repo-local, not on pub.dev) #
Bricks live under bricks/ and are excluded from dart pub publish via .pubignore.
cd /path/to/bifrost
mason get
mason make flutter_test_setup --project_name my_app --output /path/to/my_app
Generates test/flutter_test_config.dart, test/helpers/pump_app.dart, and dart_test.yaml.
License #
MIT