brick_offline_first 0.0.6

  • Readme
  • Changelog
  • Example
  • Installing
  • 78

Brick Offline First #

Offline First combines SQLite and a remote provider into one unified repository. And, optionally, a memory cache layer as the entry point. The remote provider could query Firebase or REST, hydrate the results to SQLite, and then deliver those SQLite results back to the app. In this way, the app functions identically when it's online or offline:

OfflineFirst#get

Models #

@ConnectOfflineFirstWithRest decorates the model that can be serialized by one or more providers. Offline First does not have configuration at the class level and only extends configuration held by its providers:

@ConnectOfflineFirstWithRest(
  restConfig: RestSerializable(),
  sqliteConfig: SqliteSerializable(),
)
class MyModel extends OfflineFirstModel {}

Fields #

@OfflineFirst(where:) #

Using unique identifiers, where: can connect multiple providers. It is declared using a map between a local provider (key) and a remote provider (value). This is useful when a remote provider only includes unique identifiers (such as "id": 1) of associations, the OfflineFirstRepository can lookup that instance from another source and deserialize into a complete model.

⚠️ This is a rare instance where the serializer property name is used instead of the field name, such as last_name instead of lastName.

For a concrete example, SQLite is the local data source and REST is the remote data source:

Given the API:

{ "assoc": {
    // These don't have to map to SQLite columns.
    // They can also be String uuids that SQLite considers unique
    "id": 12345,
    "ids": [12345, 6789]
    }}

The association can be automatically mapped to SQLite (note the inclusion of data; this will always be "data" as it specifies the in-progress deserialization):

@OfflineFirst(where: {'id' : "data['assoc']['id']"})
final Assoc assoc;

@OfflineFirst(where: {'id' : "data['assoc']['ids']"})
final List<Assoc> assoc;

@OfflineFirst(where:) only applies to associations or iterable associations. If @OfflineFirst(where:) is not defined, the model will attempt to be instantiated by the REST key that maps to the field.

⚠️ When @OfflineFirst(where:) is defined, the @Rest(toGenerator:) generator will not feature the field unless a toRest custom generator is defined OR only one pair is defined in the map.

OfflineFirstSerdes #

When storing raw data is more optimal than storing it as an association, an OfflineFirstSerdes can be used. For example, a child model has only a few properties but hosts a significant number of computed members and methods:

import 'dart:convert';
class Weight extends OfflineFirstSerdes<Map<int, String>, String> {
  final int size;
  final String unit;

  Weight(this.size, this.unit);

  // A fromRest factory must be defined
  factory Weight.fromRest(Map<String, dynamic> data) {
    if (data == null || data.isEmpty) return null;

    final size = double.parse(data.keys.first.toString() ?? '0');
    return Weight(size, data.values.first);
  }

  // A fromSqlite factory must be defined
  factory Weight.fromSqlite(String data) => Weight.fromRest(jsonDecode(data));

  toRest() => {size: unit};
  toSqlite() => jsonEncode(toRest());
}

OfflineFirstSerdes should not be used when the managed data must be queried. Plainly, Brick does not support JSON searches.

Offline First With Rest Repository #

OfflineFirstWithRestRepository streamlines the REST integration with an OfflineFirstRepository. A serial queue is included to track REST requests in a separate SQLite database, only removing requests when a response is not returned from the host (i.e. the device has lost internet connectivity).

The OfflineFirstWithRest domain uses all the same configurations and annotations as OfflineFirst.

Generating Models from a REST Endpoint #

A utility class is provided to make model generation from a JSON API a snap. Given an endpoint, the converter will infer the type of a field and scaffold a class. For example, the following would be saved to the lib directory of your project and run $ dart lib/converter_script.dart:

// lib/converter_script.dart
import 'package:brick_offline_first/rest_to_offline_first_converter.dart';

const BASE = "http://localhost:3000";
const endpoint = "$BASE/users";

final converter = RestToOfflineFirstConverter(
  endpoint: endpoint,
);

void main() {
  converter.saveToFile();
}

// => dart lib/converter_script.dart

After the model is generated, double check for List<dynamic> and null types. While the converter is smart, it's not smarter than you.

OfflineQueueHttpClient #

All requests to the REST provider in the repository first pass through a queue that tracks unsuccessful requests in a SQLite database separate from the one that maintains application models. Should the application ever lose connectivity, the queue will resend all upserted requests that occurred while the app was offline. All requests are forwarded to an inner client.

The queue is automatically added to all OfflineFirstWithRestRepositorys. This means that a queue should not be used as the RestProvider's client, however, the queue should use the RestProvider's client as its inner client:

final client = OfflineQueueHttpClient(
  restProvider.client, // or http.Client()
  "OfflineQueue",
);

OfflineQueue logic flow

⚠️ The queue ignores requests that are not DELETE, PATCH, POST, and PUT. get requests are not worth tracking as the caller may have been disposed by the time the app regains connectivity.

Testing #

Responses can be stubbed to and from an OfflineFirstWithRest repository. For convenience, the same data can stub for both the API and SQLite:

// test/models/api/user.json
{
  "user": { "name" : "Thomas" }
}

// test/models/user_test.dart
import 'package:brick_sqlite/testing.dart';
import 'package:my_app/app/repository.dart';

void main() {
  group("MySqliteProvider", () {
    setUpAll(() {
      StubOfflineFirstWithRestModel<User>(
        filePath: "api/user.json",
        repository: MyRepository()
      );
    });
  });
}

Currently the same response is returned for both upsert and get methods, with the only variation being in status code.

Handling Endpoint Variations #

As Mockito is rightfully strict in its stubbing, variants in the endpoint must be explicitly declared. For example, /user, /users, /users?by_first_name=Guy are all different. When instantiating, specify any expected variants:

StubOfflineFirstWithRestModel<User>(
  endpoints: ["user", "users", "users?by_first_name=Guy"]
)

Stubbing Multiple Models #

Rarely will only one model need to be stubbed. All classes in an app can be stubbed efficiently using StubOfflineFirstWithRest:

setUpAll() {
  final config = {
    User: ["user", "users"],
    // Even individual member endpoints must be declared for association fetching
    // REST endpoints are manually configured, so the content may vary
    Hat: ["hat/1", "hat/2", "hats"],
  }
  final models = config.entries.map((modelConfig) {
    return StubOfflineFirstWithRest(
      filePath: "api/${modelConfig.key.toString().toLowerCase()}.json",
      model: modelConfig.key,
      endpoints: modelConfig.value,
    );
  });
  StubOfflineFirstWithRest(
    modelStubs: models,
    repository: MyRepository(),
  );
}

💡 MyRepository()'s REST client is now a Mockito instance. verify and other interaction matchers can be called on MyRepository().restProvider.client.

FAQ #

Why can't I declare a model argument?

Due to an open analyzer bug, a custom model cannot be passed to the repository as a type argument.

Unsupported Field Types #

  • Any unsupported field types from RestProvider and SqliteProvider
  • Future iterables of future models (i.e. Future<List<Future<Model>>>.

Unreleased #

0.0.6 #

  • Remove maximumRequests configuration for the OfflineFirstQueue. One request should be processed at a time in serial
  • Optionally ignore Tunnel not found requests (these occur when connectivity exists but the queried endpoint is unreachable) when making repository requests
  • Adds argument to repository to reattempt requests based on the status code from the response
  • OfflineRequestQueue#process became a protected method
  • Added RequestSqliteCacheManager to interact with the queue. This new class receives most static methods from RequestSqliteCache.
  • Added OfflineRequestQueue#requestManager to access queue via a RequestSqliteCacheManager instance.
  • Renamed RequestSqliteCache.unprocessedRequests to RequestSqliteCacheManager.prepareNextRequestToProcess as the expected query only returns one locked row at a time.
  • RequestSqliteCacheManager.prepareNextRequestToProcess locks all unprocessed rows, not just the first one
  • Add ability to toggle serialProcessing for OfflineRequestQueue
  • Private member OfflineFirstWithRestRepository#offlineRequestQueue is now protected
  • Remove isConnected member from OfflineFirstRepository and associated Connectivity code. The connection should not matter to the subclass as it, or a supporting class, should track outbound requests.

0.0.5+1 #

  • Bump dependencies

0.0.5 #

  • Rename Query#params to Query#providerArgs, reflecting the much narrower purpose of the member

0.0.2 #

  • Export REST annotations/classes from OfflineFirstWithRestRepository for convenient access
  • Don't require MemoryCacheProvider in OfflineFirstWithRestRepository as it's not required for OfflineFirstRepository
  • Fix linter hints

example/README.md

Brick Offline First with Rest Example #

FAQ #

Why are generated files not ignored in this project? #

While a normal installation should ignore *.g.dart files, this project has them committed. This is for illustrative purposes to accessibly showcase Brick's output.

Use this package as a library

1. Depend on it

Add this to your package's pubspec.yaml file:


dependencies:
  brick_offline_first: ^0.0.6

2. Install it

You can install packages from the command line:

with Flutter:


$ flutter pub get

Alternatively, your editor might support flutter pub get. Check the docs for your editor to learn more.

3. Import it

Now in your Dart code, you can use:


import 'package:brick_offline_first/offline_first.dart';
import 'package:brick_offline_first/offline_first_with_rest.dart';
import 'package:brick_offline_first/rest_to_offline_first_converter.dart';
import 'package:brick_offline_first/testing.dart';
  
Popularity:
Describes how popular the package is relative to other packages. [more]
59
Health:
Code health derived from static analysis. [more]
100
Maintenance:
Reflects how tidy and up-to-date the package is. [more]
90
Overall:
Weighted score of the above. [more]
78
Learn more about scoring.

We analyzed this package on Mar 31, 2020, and provided a score, details, and suggestions below. Analysis was completed with status completed using:

  • Dart: 2.7.1
  • pana: 0.13.6
  • Flutter: 1.12.13+hotfix.8

Health suggestions

Format lib/offline_first.dart.

Run flutter format to format lib/offline_first.dart.

Format lib/offline_first_with_rest.dart.

Run flutter format to format lib/offline_first_with_rest.dart.

Format lib/rest_to_offline_first_converter.dart.

Run flutter format to format lib/rest_to_offline_first_converter.dart.

Fix additional 6 files with analysis or formatting issues.

Additional issues in the following files:

  • lib/src/offline_first_exception.dart (Run flutter format to format lib/src/offline_first_exception.dart.)
  • lib/src/offline_queue/offline_queue_http_client.dart (Run flutter format to format lib/src/offline_queue/offline_queue_http_client.dart.)
  • lib/src/offline_queue/request_sqlite_cache.dart (Run flutter format to format lib/src/offline_queue/request_sqlite_cache.dart.)
  • lib/src/offline_queue/request_sqlite_cache_manager.dart (Run flutter format to format lib/src/offline_queue/request_sqlite_cache_manager.dart.)
  • lib/src/testing/stub_offline_first_with_rest.dart (Run flutter format to format lib/src/testing/stub_offline_first_with_rest.dart.)
  • lib/src/testing/stub_offline_first_with_rest_model.dart (Run flutter format to format lib/src/testing/stub_offline_first_with_rest_model.dart.)

Maintenance suggestions

Package is pre-v0.1 release. (-10 points)

While nothing is inherently wrong with versions of 0.0.*, it might mean that the author is still experimenting with the general direction of the API.

Dependencies

Package Constraint Resolved Available
Direct dependencies
Dart SDK >=2.4.0 <3.0.0
brick_core ^0.0.5 0.0.5
brick_offline_first_abstract ^0.0.5 0.0.5
brick_rest ^0.0.5 0.0.5
brick_sqlite ^0.0.6 0.0.6
brick_sqlite_abstract ^0.0.4 0.0.4
dart_style ^1.2.4 1.3.3
flutter 0.0.0
http ^0.12.0 0.12.0+4
logging ^0.11.3+2 0.11.4
meta ^1.1.6 1.1.8
mockito ^4.0.0 4.1.1
path ^1.6.3 1.6.4
sqflite ^1.1.6 1.3.0
Transitive dependencies
_fe_analyzer_shared 1.0.3
analyzer 0.39.4
args 1.6.0
async 2.4.1
boolean_selector 2.0.0
charcode 1.1.3
collection 1.14.11 1.14.12
convert 2.1.1
crypto 2.1.4
csslib 0.16.1
glob 1.2.0
html 0.14.0+3
http_parser 3.1.4
js 0.6.1+1
matcher 0.12.6
node_interop 1.0.3
node_io 1.0.1+2
package_config 1.9.3
pedantic 1.9.0
pub_semver 1.4.4
sky_engine 0.0.99
source_span 1.7.0
sqflite_common 1.0.0+1
stack_trace 1.9.3
stream_channel 2.0.0
string_scanner 1.0.5
synchronized 2.2.0
term_glyph 1.1.0
test_api 0.2.15
typed_data 1.1.6
vector_math 2.0.8
watcher 0.9.7+14
yaml 2.2.0
Dev dependencies
flutter_test
test >=1.9.4 <2.0.0