bifrosted 0.10.2 copy "bifrosted: ^0.10.2" to clipboard
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 uses bifrostLogger)
  • 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

0
likes
90
points
94
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

The rainbow bridge connecting your app to APIs. A lightweight REST API client and repository pattern with caching, offline support, and error handling.

Repository (GitHub)
View/report issues

Topics

#api #http #rest #repository #caching

License

MIT (license)

Dependencies

analyzer, build, faker, flutter, flutter_test, http, logger, share_plus, shared_preferences, source_gen, web

More

Packages that depend on bifrosted