{"id":19858583,"url":"https://github.com/danreynolds/loon","last_synced_at":"2025-07-27T05:32:14.197Z","repository":{"id":163993131,"uuid":"639465114","full_name":"danReynolds/loon","owner":"danReynolds","description":"Loon is a reactive, key/value data store for Dart \u0026 Flutter.","archived":false,"fork":false,"pushed_at":"2024-05-27T19:53:03.000Z","size":704,"stargazers_count":9,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2024-05-28T03:17:41.543Z","etag":null,"topics":["dart","datastore","flutter","hacktoberfest","reactive"],"latest_commit_sha":null,"homepage":"https://pub.dev/packages/loon","language":"Dart","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/danReynolds.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-05-11T14:00:09.000Z","updated_at":"2024-05-30T02:56:22.599Z","dependencies_parsed_at":"2023-12-23T22:16:47.489Z","dependency_job_id":"5a3042bc-6240-44a5-9c4c-488cd8066171","html_url":"https://github.com/danReynolds/loon","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danReynolds%2Floon","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danReynolds%2Floon/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danReynolds%2Floon/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/danReynolds%2Floon/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/danReynolds","download_url":"https://codeload.github.com/danReynolds/loon/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":227769377,"owners_count":17817119,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dart","datastore","flutter","hacktoberfest","reactive"],"created_at":"2024-11-12T14:24:01.652Z","updated_at":"2025-07-27T05:32:14.190Z","avatar_url":"https://github.com/danReynolds.png","language":"Dart","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Loon\n\n\u003cimg src=\"https://github.com/danReynolds/loon/assets/2192930/8d03dc8f-9d43-4b7e-951d-5e54ec857897\" width=\"300\" height=\"300\"\u003e\n\u003cbr /\u003e\n\u003cbr /\u003e\n\n\u003ctable border=\"1\"\u003e\n  \u003ctr\u003e\n    \u003cth\u003eAndroid\u003c/th\u003e\n    \u003cth\u003eiOS\u003c/th\u003e\n    \u003cth\u003eWeb\u003c/th\u003e\n    \u003cth\u003emacOS\u003c/th\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003e✅\u003c/td\u003e\n    \u003ctd\u003e✅\u003c/td\u003e\n    \u003ctd\u003e✅\u003c/td\u003e\n    \u003ctd\u003e✅\u003c/td\u003e\n  \u003c/tr\u003e\n\u003c/table\u003e\n\n\u003cbr /\u003e\n\nLoon is a reactive document data store for Flutter.\n\n## Features\n\n* Synchronous reading, writing and querying of documents.\n* Streaming of changes to documents and queries.\n* Out of the box persistence and encryption.\n\n## Install\n\n```dart\nflutter pub add loon\n```\n\n## ➕ Creating documents\n\nLoon makes it easy to work with collections of documents.\n\n```dart\nimport 'package:loon/loon.dart';\n\nLoon.collection('users').doc('1').create({\n  'name': 'John',\n  'age': 28,\n});\n```\n\nDocuments are stored under collections in a tree structure. They can contain any type of data, like a `String`, `Map` or typed data model:\n\n```dart\nimport 'package:loon/loon.dart';\nimport './models/user.dart';\n\nLoon.collection\u003cUserModel\u003e(\n  'users',\n  fromJson: UserModel.fromJson,\n  toJson: (user) =\u003e user.toJson(),\n).doc('1').create(\n  UserModel(\n    name: 'John',\n    age: 28,\n  )\n);\n```\n\nIf persistence is enabled, then a typed collection needs to specify a `fromJson/toJson` serialization pair. In order to avoid having to specify types or serializers whenever a collection is accessed, it can be helpful to store the collection in a variable or as an index on the data model:\n\n```dart\nclass UserModel {\n  final String name;\n  final int age;\n\n  UserModel({\n    required this.name,\n    required this.age,\n  });\n\n  static final Collection\u003cUserModel\u003e store = Loon.collection(\n    'users',\n    fromJson: UserModel.fromJson,\n    toJson: (user) =\u003e user.toJson(),\n  );\n}\n```\n\nDocuments can then be read/written using the index:\n\n```dart\nUserModel.store.doc('1').create(\n  UserModel(\n    name: 'John',\n    age: 28,\n  ),\n);\n```\n\n## 📚 Reading documents\n\n```dart\nfinal snap = UserModel.store.doc('1').get();\n\nif (snap != null \u0026\u0026 snap.data.name == 'John') {\n  print('Hi John!');\n}\n```\n\nReading a document returns a `DocumentSnapshot?` which exposes a document's data and ID:\n\n```dart\nprint(snap.id) // 1\nprint(snap.data) // UserModel(...)\n```\n\nTo watch for changes to a document, you can listen to its stream:\n\n```dart\nUserModel.store.doc('1').stream().listen((snap) {});\n```\n\nYou can then use Flutter's built-in `StreamBuilder` or the library's `DocumentStreamBuilder` widget to access data from widgets:\n\n```dart\nclass MyWidget extends StatelessWidget {\n  @override\n  build(context) {\n    return DocumentStreamBuilder(\n      doc: UserModel.store.doc('1'),\n      builder: (context, snap) {\n        final user = snap?.data;\n\n        if (user == null) {\n          return Text('Missing user');\n        }\n\n        return Text('Found user ${user.name}');\n      }\n    )\n  }\n}\n```\n\n## 𖢞 Subcollections\n\nDocuments can be nested under subcollections. Documents in subcollections are uniquely identified by the path to their collection and\ntheir document ID.\n\n```dart\nfinal friendsCollection = UserModel.store.doc('1').subcollection('friends');\n\nfriendsCollection.doc('2').create(UserModel(name: 'Jack', age: 17));\nfriendsCollection.doc('3').create(UserModel(name: 'Brenda', age: 40));\nfriendsCollection.doc('4').create(UserModel(name: 'Bill', age: 70));\n\nfinal snaps = friendsCollection.get();\n\nfor (final snap in snaps) {\n  print(\"${snap.data.name}: ${snap.path}\");\n  // Jack: users__1__friends__2\n  // Brenda: users__1__friends__3\n  // Bill: users__1__friends__4\n}\n```\n\n## 🔎 Queries\n\nDocuments can be filtered using queries:\n\n```dart\nfinal snapshots = friendsCollection.where((snap) =\u003e snap.data.name.startsWith('B')).get();\nfor (final snap in snapshots) {\n  print(snap.data.name);\n  // Brenda\n  // Bill\n}\n```\n\nQueries can also be streamed, optionally using the `QueryStreamBuilder`:\n\n```dart\nclass MyWidget extends StatelessWidget {\n  @override\n  build(context) {\n    return QueryStreamBuilder(\n      query: UserModel.store.where((snap) =\u003e snap.data.age \u003e= 18),\n      builder: (context, snaps) {\n        return ListView.builder(\n          itemCount: snaps.length,\n          builder: (context, snap) {\n            return Text('${snap.data.name} is old enough to vote!');\n          }\n        )\n      }\n    )\n  }\n}\n```\n\n## ✏️ Updating documents\n\nAssuming a model has a `copyWith` function, documents can be updated as shown below:\n\n```dart\nfinal doc = UserModel.store.doc('1');\nfinal snap = doc.get();\n\ndoc.update(snap.data.copyWith(name: 'John Smith'));\n```\n\nThe reading and writing of a document can be combined using the `modify` API. If the document does not yet exist, then its snapshot is `null`.\n\n```dart\nUserModel.store.doc('1').modify((snap) {\n  return snap?.data.copyWith(name: 'John Smitherson');\n});\n```\n\n## ❌ Deleting documents\n\nDeleting a document removes it and all of its subcollections from the store.\n\n```dart\nUserModel.store.doc('1').delete();\n```\n\n## 🌊 Streaming changes\n\nDocuments and queries can be streamed for changes which provides the previous and current document data as well as the event type of the change:\n\n```dart\nUserModel.store.streamChanges().listen((changes) {\n  for (final changeSnap in changes) {\n     switch(changeSnap.event) {\n      case BroadcastEvents.added:\n        print('New document ${changeSnap.id} was added to the collection.');\n        break;\n      case BroadcastEvents.modified:\n        print('The document ${changeSnap.id} was modified from ${changeSnap.prevData} to ${changeSnap.data}.');\n        break;\n      case BroadcastEvents.removed:\n        print('${changeSnap.id} was removed from the collection.');\n        break;\n      case BroadcastEvents.hydrated:\n        print('${changeSnap.id} was hydrated from the persisted data.');\n        break;\n    }\n  }\n});\n```\n\n## 🔁 Data Dependencies\n\nData relationships in the store can be established using the data dependencies builder.\n\n```dart\nclass PostModel {\n  final String message;\n  final String userId;\n\n  PostModel({\n    required this.message,\n    required this.userId,\n  })\n}\n\nfinal posts = Loon.collection\u003cPostModel\u003e(\n  'posts',\n  dependenciesBuilder: (snap) {\n    return {\n      UserModel.store.doc(snap.data.userId),\n    };\n  },\n);\n```\n\nIn this example, whenever a post's associated user is updated, the post will also be rebroadcast to its active listeners.\n\nAdditionally, whenever a document is updated, it will rebuild its set of dependencies, allowing documents to support dynamic dependencies\nthat can change in response to updated document data.\n\n## 🪴 Root collection\n\nNot all documents necessarily make sense to be grouped together under any particular collection. In this scenario, any one-off documents can be stored\non the root collection:\n\n```dart\nLoon.doc('current_user_id').create('1');\n```\n\n## 🗄️ Data Persistence\n\nPersistence is supported on both web and native platforms. It works out of the box and can be configured on app start.\n\n*Native* platforms (iOS, Android, macOS) use a default file-based persistence implementation, while *web* persists data to IndexedDB.\n\nThe currently available persistence options are broken down by platform:\n\n### Native\n\n* **SqlitePersistor**: A SQLite persistence implementation using [sqflite](https://pub.dev/packages/sqflite). Documents are distributed in rows based on their persistence configuration.\n* **FilePersistor**: A file-based persistence implementation for native platforms. Documents are stored in one or more files based on the persistence configuration.\n\n### Web\n\n* **IndexedDBPersistor**: The default persistence implementation for web platforms. Documents are stored in [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API), a low-level API for web-based client-side storage.\n  * \u003e Note: Encryption on web is experimental through [flutter_secure_storage](https://pub.dev/packages/flutter_secure_storage#configure-web-version). It is important to enable the correct headers in order to ensure security of encryption on web.\n\n## ⚙️ Configuration\n\nA persistor can be specified explicitly on startup, or `Persistor.current()` can be used to dynamically select the default persistence implementation for the current platform.\n\n\n```dart\nvoid main() {\n  Loon.configure(persistor: Persistor.current());\n\n  Loon.hydrate().then(() {\n    print('Hydration complete');\n  });\n\n  runApp(const MyApp());\n}\n```\n\nThe call to `hydrate` returns a `Future` that resolves when the data has been hydrated from the persistence layer. By default, calling `hydrate()` will hydrate all persisted data. If only certain data should be hydrated, then it can be called with a list of documents and collections to hydrate. All subcollections of the specified paths are also hydrated.\n\n```dart\nawait Loon.hydrate([\n  Loon.doc('current_user_id'),\n  Loon.collection('users'),\n]);\n```\n\n## ⚙️ Dynamic options\n\nPersistence options can be specified both globally as well as on a per-collection basis.\n\n```dart\n// main.dart\nvoid main() {\n  // Globally enable encryption.\n  Loon.configure(persistor: FilePersistor(settings: PersistorSettings(encrypted: true));\n\n  Loon.hydrate().then(() {\n    print('Hydration complete');\n  });\n}\n\n// models/user.dart\nclass UserModel {\n  final String name;\n  final int age;\n\n  UserModel({\n    required this.name,\n    required this.age,\n  });\n\n  static Collection\u003cUserModel\u003e get store {\n    return Loon.collection(\n      'users',\n      fromJson: UserModel.fromJson,\n      toJson: (user) =\u003e user.toJson(),\n      // Disable encryption specifically for this collection and its subcollections.\n      settings: PersistorSettings(encrypted: false),\n    )\n  }\n}\n```\n\nIn this example, file encryption is enabled globally for all collections, but disabled\nspecifically for the users collection and its subcollections in the store.\n\nBy default all data in a single  `__store__.json` persistence file.\n\n```\nloon \u003e\n  __store__.json\n```\n\nIf data needs to be persisted differently, either by merging data across collections into a single store or by breaking down a collection\ninto multiple stores, then a custom persistence key can be specified on the collection:\n\n```dart\nclass UserModel {\n  final String name;\n  final int age;\n\n  UserModel({\n    required this.name,\n    required this.age,\n  });\n\n  static Collection\u003cUserModel\u003e get store {\n    return Loon.collection(\n      'users',\n      fromJson: UserModel.fromJson,\n      toJson: (user) =\u003e user.toJson(),\n      settings: PersistorSettings(\n        key: Persistor.key('users'),\n      ),\n    )\n  }\n}\n```\n\nIn the updated example, data from the users collection is now stored in a separate file store:\n\n```dart\nloon \u003e\n  __store__.json\n  users.json\n```\n\nIf documents need to be stored in different files based on their data, then a `Persistor.keyBuilder` can be used:\n\n```dart\nclass UserModel {\n  final String name;\n  final int age;\n\n  UserModel({\n    required this.name,\n    required this.age,\n  });\n\n  static Collection\u003cUserModel\u003e get store {\n    return Loon.collection(\n      'users',\n      fromJson: UserModel.fromJson,\n      toJson: (user) =\u003e user.toJson(),\n      settings: PersistorSettings(\n        key: Persistor.keyBuilder((snap) {\n          if (snap.data.age \u003e= 18) {\n            return 'adult_users';\n          }\n          return 'users';\n        }),\n      ),\n    )\n  }\n}\n```\n\n```dart\nloon \u003e\n  __store__.json\n  users.json\n  adult_users.json\n```\n\nNow instead of storing all users in the `users.json` file, they will be distributed across multiple files based on the user's age. The key is recalculated\nwhenever a document's data changes and if its associated key is updated, then the document is moved from its previous file to its updated location.\n\n## 🎨 Custom persistence\n\nIf you would prefer to persist data using an alternative implementation than the default persistors, you can implement the persistence interface:\n\n```dart\nimport 'package:loon/loon.dart';\n\nclass MyPersistor extends Persistor {\n  /// Initialization function called when the persistor is instantiated to execute and setup work.\n  Future\u003cvoid\u003e init();\n\n  /// Persist function called with the bath of documents that have changed (including been deleted) within the last throttle window\n  /// specified by the [Persistor.persistenceThrottle] duration.\n  Future\u003cvoid\u003e persist(Set\u003cDocument\u003e docs);\n\n  /// Hydration function called to read data from persistence. If no references are specified,\n  /// then it hydrates all persisted data. if refs are specified, it hydrates only the data at\n  /// and under those paths.\n  Future\u003cJson\u003e hydrate([Set\u003cStoreReference\u003e? refs]);\n\n  /// Clear function used to clear all documents under the given collections.\n  Future\u003cvoid\u003e clear(Set\u003cCollection\u003e collections);\n\n  /// Clears all documents and removes all persisted data.\n  Future\u003cvoid\u003e clearAll();\n}\n```\n\n## Extensions\n\n* [Firestore](https://pub.dev/packages/cloud_firestore): The [loon_extension_firestore](https://github.com/danReynolds/loon_extension_firestore) package is used to sync documents fetched from Firestore into Loon.\n\n## Documents\n\n* [Architecture](./docs/architecture.md): An architecture doc that breaks down the inner workings of the library.\n* [Debugging](./docs/debug.md): A debugging doc that calls out any known issues or troubleshooting suggestions across different platforms.\n\n## Happy coding\n\nThat's all for now! Want a feature? Found a bug? Create an issue!\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdanreynolds%2Floon","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdanreynolds%2Floon","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdanreynolds%2Floon/lists"}