bifrosted 0.12.1 copy "bifrosted: ^0.12.1" to clipboard
bifrosted: ^0.12.1 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.

Changelog #

0.12.1 #

  • Fixed the serialized cache-mutation queue retaining its completed Future across Flutter's per-test FakeAsync zones. A first widget test could cache successfully, while the next received its response and then waited forever on a completion owned by the previous test's inactive zone. The queue now clears its tail when the last mutation completes while preserving ordering for concurrent mutations.

0.12.0 #

  • Breaking: replaced the shipped BifrostTestEnv service graph with BifrostTestRuntime. Test setup now resets only Bifrost's transport, preferences, clock, decoder, performance tracker, fake mode, notifications, and internal diagnostics. It never replaces bifrostServiceLocator; apps rebuild their own production container through initializeApplication.

    BifrostTestRuntime.install(
      responseFactory: (_) => const <Object>[],
      notifications: notifications,
      diagnostics: diagnostics,
      initializeApplication: AppBinding.reinitialize,
    );
    

    responseFactory, shouldFail, and responder now share the same per-request responder seam, so overrides reach HTTP clients retained by vendor SDKs such as Supabase. Notification tests record outcomes without replacing the app's real SystemNotifier registration.

  • Added non-recoverable FailureReason.internal. Unexpected defects thrown by source adapters, REST hooks, or repository callbacks no longer masquerade as network and get rescued by stale cache. fetch and mutate return an internal failure; the source-compatible boolean send returns false and records an internal diagnostic. Internal failures never invoke SystemNotifier.

  • RestAPI now bounds header resolution, each HTTP attempt, and unauthorized refresh. Override requestTimeout or refreshTimeout for API-specific budgets. Operational ClientException and timeout failures still return null; unexpected hook/client defects preserve their original error and stack in BifrostInternalException for repository containment. Timed-out refreshes remain single-flight until the underlying refresh actually ends.

  • Serialized the complete cache save, per-key clear, and clear-all mutations behind one failure-resilient queue shared by every repository instance. Two concurrent reads can no longer lose a registry entry and leave cached payloads that clearAllCache() cannot enumerate.

  • Added postgrestErrorResponse to package:bifrosted/testing.dart. It builds a complete PostgREST error envelope for tests that exercise a real retained Supabase/PostgREST client; production SQLSTATE/PGRST mapping remains app-owned.

  • Simplified Bifrost's private package test fixture: its locator is installed once per test file while its mutable services still reset before every test.

0.11.0 #

  • Breaking: BifrostTestEnv.reset() no longer assigns bifrostServiceLocator. It was a second dependency-injection system running against the app's own. An app's bindings already point the locator at its container, so reset() and those bindings overwrote each other and which one won depended on call order — a repository test that ran reset() last silently stopped resolving the services its production code uses, while a widget test resolved them normally. Tests should exercise the same injection production does.

    Register the doubles through the app's own container instead:

    bifrostTestEnv.reset();
    Bind.delete<SystemNotifier>(force: true);   // GetX keeps the first permanent registration
    Bind.put<SystemNotifier>(bifrostTestEnv.notifier, permanent: true);
    

    A package with no container of its own — bifrosted included — can call bifrostTestEnv.installDoubles(), which does what reset() used to. It is documented as exactly that: a shim for the no-DI case, not something an app should reach for.

    Migration: if your tests broke with "…" not found after upgrading, add installDoubles() after reset() to restore the old behaviour, then move to container registration when convenient.

0.10.5 #

  • BifrostTestEnv.reset() now installs the mock transport and the mock preference store, not just the service doubles. It already owned the mutable globals; the client and the store were the two pieces every app still had to remember in its own setUp, and forgetting either fails far from the cause — a real http.Client reaching the network, or MissingPluginException from SharedPreferences.getInstance(). useMockStorage in particular has to repeat per test, because the mock store is a static platform instance and a value one test writes is readable in the next until it is cleared. A test harness should be able to call reset() and nothing else.
  • Added jsonResponse(body, {status, headers}). Hand-writing http.Response(jsonEncode(body), 200, headers: {'content-type': 'application/json'}) in every test is how the header gets forgotten, and a client that decodes on it then fails in a way that points at the parser rather than the fixture.
    bifrostMockResponder = (request) async => jsonResponse([row, row]);
    bifrostMockResponder = (request) async =>
        jsonResponse({'message': 'denied'}, status: 403);
    

0.10.4 #

  • Added bifrostMockResponder, and a responder: argument to useMockClient. responseFactory returns a body that is always wrapped in a 200, so a test could not drive a status code, set a header, or change its answer partway through — every app hit this and grew its own MockClient on the side, which is exactly the drift this package exists to prevent.

    A responder returns the whole http.Response:

    useMockClient(responder: (request) async => http.Response('[]', 200));
    
    // ...later, in one test:
    bifrostMockResponder = (request) async => http.Response('denied', 403);
    

    It is read per request rather than captured when the client is built. That is what makes it swappable after a vendor SDK has already resolved its http.Client and kept it — reinstalling a factory never reaches such an SDK, but reassigning this does. BifrostTestEnv.reset clears it, so one test's responder cannot leak into the next.

  • Mocked responses are now stamped with their request. MockClient populates response.request from the response the handler returns, so it stayed null unless the handler set it — and a client that dereferences response.request! while parsing throws a null-check error instead of returning data. PostgREST does exactly that, on both its success and error paths, so every mocked Supabase call failed and was swallowed as a transport error. useMockClient now fills the field in whenever a responder left it empty.

0.10.3 #

  • Added fetchSource, a read path for data that does not arrive as an http.Response. fetch was hard-wired to Future<http.Response?>, so a repository backed by a vendor SDK — Supabase, Firestore, a GraphQL client — could not use it, and lost disk caching, CachePolicy, offline fallback, and SystemNotifier along with it. The alternative was re-implementing that path per app, where it drifts.

    No new types. A source returns the same BifrostResult a repository does, carrying already-decoded JSON:

    Future<BifrostResult<List<Article>>> latest() =>
        fetchSource<List<Article>, Article>(
          source: () => newsService.selectApproved(),   // BifrostResult<Object?>
          model: const ArticleModel(),
          cacheKey: 'articles_latest',
        );
    

    The seam is one function boundary wide: _deserialize already decoded and then branched on List/Map, and everything after the decode was source-agnostic — so it splits there and fetchSource feeds the decoded half directly. Cache writes re-encode, leaving cache reads on the existing string path.

    Translate vendor errors in the service, not the repository and not here, so this package accumulates no backend-specific knowledge. A source that throws is caught and reported as FailureReason.network, so the "repositories never throw" contract holds even when an SDK misbehaves.

    With a cacheKey set, the decoded value must be JSON-encodable, since it is re-encoded to be stored.

  • Added statusForFailureReason, the inverse of failureReasonForStatus. SystemNotifier.onRequestFailed requires a non-null status, but a source reports a reason and may have none — a Supabase row-level-security denial arrives as SQLSTATE 42501, though PostgREST answers 403 for it over HTTP. Synthesizing the canonical status restores information the SDK dropped rather than inventing one, and stops a forbidden degrading silently into "network error". Null for network, offlineNoCache, and parse, none of which describe a response.

    parse notifies nothing, matching fetch, which consults the notifier before deserializing and so has never been able to.

  • Added bifrostHttpClient(). setClientFactory installed the factory but nothing could read it, so code that must hand an http.Client to a vendor SDK could not receive the one useMockClient() installs — which forced constructor injection purely to test SDK-backed repositories, defeating the point of mocking at the HTTP layer.

  • Added bifrostClock, a DateTime Function() global defaulting to DateTime.now, mirroring bifrostJsonDecode and bifrostFakeSeed. Cache expiry read DateTime.now() directly, so expiry behaviour could only be tested by waiting, and any app with time-dependent UI had goldens that changed by the second. BifrostTestEnv.reset restores it.

0.10.2 #

  • Fixed: RestAPI.host rejected any value containing a colon. _buildUri passed host straight into Uri(scheme: 'https', host: host), and Uri's host parameter reads a colon as an IPv6 literal — so a scheme-qualified URL, a port, and therefore any local dev server threw FormatException: Illegal IPv6 address, invalid character (at character 1), an error naming none of the actual causes.

    host now accepts what you'd reasonably put in a config value:

    host Request URL
    api.example.com https://api.example.com/...
    https://api.example.com as given
    http://localhost:8080 as given — scheme and port preserved
    10.0.2.2:3000 https://10.0.2.2:3000/...
    https://example.com/v1 base path preserved, not dropped

    Backward compatible: a bare hostname still resolves to https.

  • An empty host now throws ArgumentError naming the likely cause — an app launched without --dart-define-from-file — instead of silently building https:///path and failing at the transport layer.

0.10.1 #

  • Widened the logger re-export. Logger and Level alone were not enough to reconfigure bifrostLogger: its default DevelopmentFilter drops every log in release builds and ignores level, so keeping warnings in release meant naming ProductionFilter — which required adding a direct logger dependency just to reassign our own global. Now also exports ProductionFilter, DevelopmentFilter, LogFilter, LogPrinter, LogOutput, PrettyPrinter, and SimplePrinter.
    bifrostLogger = Logger(
      level: kReleaseMode ? Level.warning : Level.debug,
      filter: ProductionFilter(),   // required; DevelopmentFilter ignores `level`
    );
    

0.10.0 #

  • Fixed: nothing persisted across app launches. SharedPrefService.init() called SharedPreferences.setMockInitialValues({}), which replaces the platform store with an empty in-memory map (SharedPreferencesStorePlatform.instance = InMemorySharedPreferencesStore.withData(...)). Because init() runs on every launch, no preference, token, onboarding flag, or cached response survived a cold start. init() now only reads. If your app appeared to "forget everything," this was why.
  • Breaking: removed SharedPrefService.updateInitialMock. It was test scaffolding on a production class, and it set the mock store as a side effect. Replace it with useMockStorage.
  • Added useMockStorage({values}) to package:bifrosted/testing.dart — the storage counterpart to useMockClient. Tests need it for two reasons: there is no SharedPreferences platform plugin in a test process, so getInstance() otherwise throws MissingPluginException; and the mock store is a static platform instance, so a value written by one test is still readable in the next unless it is reset. Call it in setUp (or once per file, before initServices):
    useMockStorage();                                   // clean slate
    useMockStorage(values: {'onboarding_done': true});  // seeded state
    
  • Added [OnboardingController], a reusable multi-step flow controller. Subclass it, implement buildResult, and inherit next/back/skip, answer collection, completion, and the analytics funnel.
  • It is a plain ChangeNotifier — no state-management dependency — matching the documented exception for one-time flows that run before the app's main state exists.
  • Emits the events a funnel is computed from: started, step_viewed, answered, skipped, back, completed (with duration, reach and skip count), and abandoned on dispose-before-completion, naming the step the user quit on. The event prefix is configurable.
  • step_viewed fires once per step per session, on first arrival only. Re-counting a revisit after back() would inflate early steps and overstate the funnel's health.
  • The controller reports events, not rates: a completion rate is a population statistic that one session cannot know. completed ÷ started is a query in your analytics tool.
  • back() never clears answers — losing input on a back tap is the most reliable way to cause abandonment.

0.9.0 #

  • Breaking: fetch and mutate return BifrostResult<T> instead of T?. A bare null told callers that something failed but never what, so every screen could only show generic copy. BifrostFailure carries a FailureReason, an optional status code, and diagnostic detail. Migrate call sites from a null check to an exhaustive switch:
    // Before
    final user = await repo.getUser(id);
    if (user != null) {
      print(user.name);
    } else {
      // Error was already handled by SystemNotifier
    }
    
    // After
    switch (await repo.getUser(id)) {
      case BifrostSuccess(:final data):
        print(data.name);
      case BifrostFailure(:final reason):
        // Error was already handled by SystemNotifier; render an error state.
    }
    
  • Breaking: mutate takes a required model: instead of an optional fromJson:. This is not a rename — every existing body-less mutate(apiRequest: ..., invalidateKeys: [...]) call (the pattern fromJson being optional made possible) is now a compile error. Switch those call sites to send(), which already returns bool and takes the same invalidateKeys:.
  • Breaking (behavior): a 2xx response with an empty body through mutate now returns BifrostFailure(FailureReason.parse) instead of nullmutate has nothing to deserialize into model: when there's no body. Cache invalidation still runs. Use send() for writes that don't return a body.
  • Added silent: to fetch, mutate, and send — a background refresh no longer fires SystemNotifier.
  • Added CachePolicy (networkFirst, cacheFirst, networkOnly). Cache fallback now keys on request outcome rather than ConnectionChecker.isConnected: online-but-failing previously returned nothing while holding usable cached data. 4xx is excluded from fallback, since a definitive server answer must not be masked by stale data.
  • cacheDuration now means something under cacheFirst; previously it only bounded the offline fallback.
  • Added RestAPI.resolveHeaders() and RestAPI.onUnauthorized() for one-shot 401 refresh, with a shared in-flight future so concurrent 401s cause one refresh, not five. Defaults preserve prior behaviour.
  • Removed DeserializationException, which was defined, exported, and never thrown.

0.8.0 #

  • Added app-facing service interfaces: [AnalyticsService], [EntitlementService], [PaywallPresenter], and [AttributionService], each with a NoOp default. bifrosted does not call these — they live here so one package covers every project's service shapes. No vendor SDK is added; the app supplies implementations.
  • [EntitlementService] exposes entitlements as a Stream, not a one-shot check, because they change mid-session on purchase, restore, and expiry.
  • [PaywallPresenter] is separate from [EntitlementService] so Superwall, RevenueCat, and custom Flutter paywalls swap without touching call sites.
  • [AttributionService] models the platform asymmetry deliberately: Android's Play Install Referrer yields a per-install code, iOS has no runtime equivalent and captureReferral() returning null there is the expected result.
  • The entitlement and paywall no-ops warn through bifrostLogger when they swallow a user-initiated action; the analytics and attribution no-ops stay silent, since shipping without analytics and returning null on iOS are both legitimate steady states.
  • bifrostTestEnv gains analytics, entitlements, paywalls, and attribution doubles, all resolvable through bifrostServiceLocator.

0.7.0 #

  • Breaking: Removed the firebase_performance dependency. bifrosted now defines [PerformanceTracker] / [HttpTrace] / [BifrostHttpMethod] and ships [NoOpPerformanceTracker]; the app supplies the SDK-specific implementation and assigns bifrostPerformanceTracker.
  • Breaking: Removed bifrostPerformanceEnabled. Assign const NoOpPerformanceTracker() to disable metrics.
  • Breaking: useMockClient moved from package:bifrosted/bifrosted.dart to package:bifrosted/testing.dart. package:http/testing.dart is no longer imported by core.
  • Breaking: Fake data is now seeded by default (kDefaultFakeSeed), making Model.fake() reproducible across runs so golden tests are stable. Set bifrostFakeSeed = null for the previous random behaviour.
  • Traces complete without await on the request path, so a slow monitoring SDK no longer adds latency to every request.
  • Unlike the 0.5.5 tracker, resolution is a plain global with a no-op default rather than bifrostServiceLocator, so no registration is required and tryBifrostServiceLocator is unnecessary.

0.6.0 #

  • Added direct Firebase Performance HTTP metric instrumentation in [RestAPI]
  • Adds firebase_performance as a dependency
  • Toggle via top-level bifrostPerformanceEnabled (defaults to true; auto-disabled when Firebase isn't initialized)
  • Test env disables performance monitoring automatically via bifrostTestEnv.reset()

0.5.6 #

  • Breaking: Removed [PerformanceTracker], [tryBifrostServiceLocator], and HTTP metric hooks from [RestAPI]
  • [RestAPI] reuses one [http.Client] per instance; use [RestAPI.closeClient] when discarding an API

0.5.5 #

  • Added pluggable [PerformanceTracker] via [bifrostServiceLocator] for HTTP metrics (removed in 0.5.6)
  • [RestAPI] uses performance hooks when a tracker is set (no-op when null)
  • [MockPerformanceTracker] in package:bifrosted/testing.dart for tests (removed in 0.5.6)
  • App supplies Firebase/Sentry/etc. implementations; bifrosted has no Firebase dependency

0.5.4 #

  • Breaking: Simplified [SystemNotifier] to three UI-only callbacks:
    • onNetworkError(), onUnauthorized(), onRequestFailed({statusCode, body})
    • Removed onForbidden, onServerError, onApiError
  • Docs: notifiers must handle user-facing UI only; use bifrostLogger for diagnostics

0.5.3 #

  • Breaking: Removed fetchList — use fetch<List<T>> with the same fromJson
  • Breaking: Removed endpoint on fetch — use cacheKey only (it was only used for caching)
  • fetch<R, M> auto-detects top-level JSON array vs object; fromJson is always the item parser (User.fromJson), return type is User or List<User>

0.5.2 #

  • Removed unwrapResponse() override
  • Removed in-memory singleton cache (_memoryCache, clearMemoryCache, useMemoryCache)
  • Simplified fetch/fetchList signatures

0.5.1 #

  • Added bifrostJsonDecode - Global async JSON decoder
    • Defaults to synchronous jsonDecode (works everywhere)
    • Override to decode on a background isolate for large payloads:
      // Dart:
      bifrostJsonDecode = (body) => Isolate.run(() => jsonDecode(body));
      
      // Flutter:
      bifrostJsonDecode = (body) => compute(jsonDecode, body);
      
    • Web-safe: no dart:isolate dependency in bifrosted itself

0.5.0 #

  • Breaking: Removed dart:io dependency - now works on web/Jaspr
  • Breaking: post/put/patch/delete body is now Object? instead of String?
    • Maps and Lists are auto-encoded to JSON
    • Strings are sent as-is
  • Breaking: Removed Deserializer class - deserialization is now inlined
  • Added unwrapResponse() override for wrapped API responses ({"data": {...}})
  • Added mutate<T>() for write operations (POST/PUT/PATCH/DELETE) with optional deserialization
  • Added send() for fire-and-forget writes that return bool
  • Both mutate and send accept invalidateKeys for automatic cache invalidation
  • Added in-memory singleton cache for deserialized objects
    • BifrostRepository.clearMemoryCache() to reset
    • useMemoryCache parameter on fetch/fetchList to opt out per call
  • clearAllCache() now only removes bifrost-prefixed keys (no longer wipes all storage)
  • DeserializationException is still exported for custom use

0.4.5 #

  • update dependencies

0.4.0 #

  • Breaking: Removed generics from BifrostRepository
  • Added bifrostServiceLocator - Set once, used everywhere
    // At app startup:
    bifrostServiceLocator = <T>() => Get.find<T>();
    
  • Repositories now have zero boilerplate:
    class UserRepo extends BifrostRepository {
      Future<User?> getUser(String id) => fetch<User>(...);
    }
    

0.3.0 #

  • Added global mock client support for testing
    • useMockClient() - Enable mock responses for all RestAPI instances
    • useRealClient() - Reset to real HTTP clients
    • setClientFactory() - Set custom client factory
    • No more per-API client overrides needed

0.2.2 #

  • Fixed build.yaml to correctly combine generated code into .g.dart files
    • Changed build_to: cache and build_extensions: .fake.g.part
    • Generator output now properly merges with json_serializable/freezed

0.2.1 #

  • Added build.yaml for auto-discovery by build_runner
    • No manual configuration needed - just add the dependency and run build_runner
    • Works like freezed/json_serializable out of the box

0.2.0 #

  • Added @generateFake annotation for code generation
  • Added FakeUtils utility class (uses faker package)
    • fakeForKey(String key) - generates fake data based on field name
    • create<T>() - generates fake model from factory
    • fakeJson() / fakeJsonList() - generic JSON generators
  • Added FakeGenerator for build_runner integration
    • Generates .fake() extension methods for annotated classes
    • Works with freezed models

0.1.1 #

  • Updated README

0.1.0 #

  • Initial release
  • RestAPI abstract class for REST API clients
    • GET, POST, PUT, PATCH, DELETE methods
    • Automatic error handling and logging
    • Header management with extra headers support
  • BifrostRepository for repository pattern with caching
    • fetch<T>() and fetchList<T>() for automatic deserialization
    • Offline-first with cache fallback
    • Automatic cache expiration
  • SystemNotifier interface for global error handling
    • onNetworkError(), onUnauthorized(), onForbidden()
    • onServerError(), onApiError()
  • StorageService interface for pluggable storage backends
  • ConnectionChecker interface for connectivity detection
  • Uses logger package for logging
  • Comprehensive test suite
0
likes
120
points
450
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