패키지, 저장소 및 앱 카테고리 검색

패키지 상세v5.6.1

loon

Loon은 Dart 및 Flutter용 반응형 컬렉션 데이터 저장소입니다.

AndroidiOSWebmacOS
30일 다운로드
86
좋아요
28
Pub 점수
140

패키지 설치

flutter pub add loon

패키지 개요

공개 정보를 바탕으로 생성한 선택 가이드입니다.

Loon은 Dart 및 Flutter 애플리케이션을 위한 반응형 컬렉션 데이터 저장소로, NoSQL 및 SQL 기능을 갖춘 지속성 저장소를 제공합니다. 데이터가 변경될 때 자동으로 UI를 업데이트할 수 있도록 반응형으로 데이터 컬렉션을 관리할 수 있습니다. 크로스플랫폼 사용을 위해 설계되어 Android, iOS, Web, macOS를 지원하며, 오프라인 우선 기능과 실시간 데이터 동기화가 필요한 앱에 적합합니다. 가벼운 설계로 Flutter 워크플로우에 원활하게 통합됩니다.

사용 사례

  • 오프라인 우선 모바일 애플리케이션
  • 실시간 데이터 동기화
  • 크로스 플랫폼 데이터 지속성
  • 반응형 데이터를 갖춘 동적 UI
  • 웹 앱에서의 로컬 데이터 관리

주요 기능

  • 반응형 데이터 바인딩
  • 지속 가능한 NoSQL 및 SQL 저장소
  • 크로스 플랫폼 호환성
  • 데이터 변경 시 자동 UI 업데이트
  • 경량이며 쉽게 통합 가능

적합한 대상

  • 로컬 데이터 지속성을 찾는 플러터 개발자
  • 오프라인 기능이 필요한 앱
  • 반응형 상태 관리가 필요한 프로젝트
  • 다중 플랫폼 앱을 개발하는 팀
  • 간단하면서도 강력한 데이터 저장소를 원하는 개발자

고려 사항

  • SQL 기능을 익히기 위해 학습 곡선이 필요할 수 있습니다
  • 성숙한 대안에 비해 문서가 제한적
  • 성능은 데이터 크기와 복잡성에 따라 달라집니다
  • 대규모 서버 측 데이터베이스에 적합하지 않습니다
  • 백엔드 서비스보다 클라이언트 측 데이터에 더 적합

원본 README

연결된 소스 저장소의 README 원문 스냅샷입니다. 최신 내용은 저장소에서 확인하세요.

Loon



Android iOS Web macOS
✅ ✅ ✅ ✅

Loon is a reactive document data store for Flutter.

Features

  • Synchronous reading, writing and querying of documents.
  • Streaming of changes to documents and queries.
  • Out of the box persistence and encryption.

Install

flutter pub add loon

➕ Creating documents

Loon makes it easy to work with collections of documents.

import 'package:loon/loon.dart';

Loon.collection('users').doc('1').create({
  'name': 'John',
  'age': 28,
});

Documents are stored under collections in a tree structure. They can contain any type of data, like a String, Map or typed data model:

import 'package:loon/loon.dart';
import './models/user.dart';

Loon.collection<UserModel>(
  'users',
  fromJson: UserModel.fromJson,
  toJson: (user) => user.toJson(),
).doc('1').create(
  UserModel(
    name: 'John',
    age: 28,
  )
);

If 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:

class UserModel {
  final String name;
  final int age;

  UserModel({
    required this.name,
    required this.age,
  });

  static final Collection<UserModel> store = Loon.collection(
    'users',
    fromJson: UserModel.fromJson,
    toJson: (user) => user.toJson(),
  );
}

Documents can then be read/written using the index:

UserModel.store.doc('1').create(
  UserModel(
    name: 'John',
    age: 28,
  ),
);

📚 Reading documents

final snap = UserModel.store.doc('1').get();

if (snap != null && snap.data.name == 'John') {
  print('Hi John!');
}

Reading a document returns a DocumentSnapshot? which exposes a document's data and ID:

print(snap.id) // 1
print(snap.data) // UserModel(...)

To watch for changes to a document, you can listen to its stream:

UserModel.store.doc('1').stream().listen((snap) {});

You can then use Flutter's built-in StreamBuilder or the library's DocumentStreamBuilder widget to access data from widgets:

class MyWidget extends StatelessWidget {
  @override
  build(context) {
    return DocumentStreamBuilder(
      doc: UserModel.store.doc('1'),
      builder: (context, snap) {
        final user = snap?.data;

        if (user == null) {
          return Text('Missing user');
        }

        return Text('Found user ${user.name}');
      }
    )
  }
}

𖢞 Subcollections

Documents can be nested under subcollections. Documents in subcollections are uniquely identified by the path to their collection and their document ID.

final friendsCollection = UserModel.store.doc('1').subcollection('friends');

friendsCollection.doc('2').create(UserModel(name: 'Jack', age: 17));
friendsCollection.doc('3').create(UserModel(name: 'Brenda', age: 40));
friendsCollection.doc('4').create(UserModel(name: 'Bill', age: 70));

final snaps = friendsCollection.get();

for (final snap in snaps) {
  print("${snap.data.name}: ${snap.path}");
  // Jack: users__1__friends__2
  // Brenda: users__1__friends__3
  // Bill: users__1__friends__4
}

🔎 Queries

Documents can be filtered using queries:

final snapshots = friendsCollection.where((snap) => snap.data.name.startsWith('B')).get();
for (final snap in snapshots) {
  print(snap.data.name);
  // Brenda
  // Bill
}

Queries can also be streamed, optionally using the QueryStreamBuilder:

class MyWidget extends StatelessWidget {
  @override
  build(context) {
    return QueryStreamBuilder(
      query: UserModel.store.where((snap) => snap.data.age >= 18),
      builder: (context, snaps) {
        return ListView.builder(
          itemCount: snaps.length,
          builder: (context, snap) {
            return Text('${snap.data.name} is old enough to vote!');
          }
        )
      }
    )
  }
}

✏️ Updating documents

Assuming a model has a copyWith function, documents can be updated as shown below:

final doc = UserModel.store.doc('1');
final snap = doc.get();

doc.update(snap.data.copyWith(name: 'John Smith'));

The reading and writing of a document can be combined using the modify API.

UserModel.store.doc('1').modify((snap) {
  return snap.data.copyWith(name: 'John Smitherson');
});

❌ Deleting documents

Deleting a document removes it and all of its subcollections from the store.

UserModel.store.doc('1').delete();

🌊 Streaming changes

Documents and queries can be streamed for changes which provides the previous and current document data as well as the event type of the change:

UserModel.store.streamChanges().listen((changes) {
  for (final changeSnap in changes) {
     switch(changeSnap.event) {
      case BroadcastEvents.added:
        print('New document ${changeSnap.id} was added to the collection.');
        break;
      case BroadcastEvents.modified:
        print('The document ${changeSnap.id} was modified from ${changeSnap.prevData} to ${changeSnap.data}.');
        break;
      case BroadcastEvents.removed:
        print('${changeSnap.id} was removed from the collection.');
        break;
      case BroadcastEvents.hydrated:
        print('${changeSnap.id} was hydrated from the persisted data.');
        break;
    }
  }
});

🔁 Data Dependencies

Data relationships in the store can be established using the data dependencies builder.

class PostModel {
  final String message;
  final String userId;

  PostModel({
    required this.message,
    required this.userId,
  })
}

final posts = Loon.collection<PostModel>(
  'posts',
  dependenciesBuilder: (snap) {
    return {
      UserModel.store.doc(snap.data.userId),
    };
  },
);

In this example, whenever a post's associated user is updated, the post will also be rebroadcast to its active listeners.

Additionally, whenever a document is updated, it will rebuild its set of dependencies, allowing documents to support dynamic dependencies that can change in response to updated document data.

🪴 Root collection

Not all documents necessarily make sense to be grouped together under any particular collection. In this scenario, any one-off documents can be stored on the root collection:

Loon.doc('current_user_id').create('1');

🗄️ Data Persistence

Persistence is supported on both web and native platforms. It works out of the box and can be configured on app start.

Native platforms (iOS, Android, macOS) use a default file-based persistence implementation, while web persists data to IndexedDB.

The currently available persistence options are broken down by platform:

Native
  • SqlitePersistor: A SQLite persistence implementation using sqflite. Documents are distributed in rows based on their persistence configuration.
  • FilePersistor: A file-based persistence implementation for native platforms. Documents are stored in one or more files based on the persistence configuration.
Web
  • IndexedDBPersistor: The default persistence implementation for web platforms. Documents are stored in IndexedDB, a low-level API for web-based client-side storage.
    • Note: Encryption on web is experimental through flutter_secure_storage. It is important to enable the correct headers in order to ensure security of encryption on web.

⚙️ Configuration

Specify your preferred persistor on startup or use Persistor.current() to dynamically select the default persistence implementation for the current platform (FilePersistor on Android/iOS, IndexedDBPersistor on web).

void main() {
  Loon.configure(persistor: Persistor.current());
  // Loon.configure(persistor: FilePersistor());
  // Loon.configure(persistor: SqlitePersistor());

  Loon.hydrate().then(() {
    print('Hydration complete');
  });

  runApp(const MyApp());
}

No persisted data is available on startup until it is manually requested by calling hydrate.

The 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.

await Loon.hydrate([
  Loon.doc('current_user_id'),
  Loon.collection('users'),
]);

⚙️ Dynamic options

Persistence options can be specified both globally as well as on a per-collection basis.

// main.dart
void main() {
  // Globally enable encryption.
  Loon.configure(persistor: FilePersistor(settings: PersistorSettings(encrypted: true));

  Loon.hydrate().then(() {
    print('Hydration complete');
  });
}

// models/user.dart
class UserModel {
  final String name;
  final int age;

  UserModel({
    required this.name,
    required this.age,
  });

  static Collection<UserModel> get store {
    return Loon.collection(
      'users',
      fromJson: UserModel.fromJson,
      toJson: (user) => user.toJson(),
      // Disable encryption specifically for this collection and its subcollections.
      settings: PersistorSettings(encrypted: false),
    )
  }
}

In this example, file encryption is enabled globally for all collections, but disabled specifically for the users collection and its subcollections in the store.

By default all data in a single __store__.json persistence file.

loon >
  __store__.json

If data needs to be persisted differently, either by merging data across collections into a single store or by breaking down a collection into multiple stores, then a custom persistence key can be specified on the collection:

class UserModel {
  final String name;
  final int age;

  UserModel({
    required this.name,
    required this.age,
  });

  static Collection<UserModel> get store {
    return Loon.collection(
      'users',
      fromJson: UserModel.fromJson,
      toJson: (user) => user.toJson(),
      settings: PersistorSettings(
        key: Persistor.key('users'),
      ),
    )
  }
}

In the updated example, data from the users collection is now stored in a separate file store:

loon >
  __store__.json
  users.json

If documents need to be stored in different files based on their data, then a Persistor.keyBuilder can be used:

class UserModel {
  final String name;
  final int age;

  UserModel({
    required this.name,
    required this.age,
  });

  static Collection<UserModel> get store {
    return Loon.collection(
      'users',
      fromJson: UserModel.fromJson,
      toJson: (user) => user.toJson(),
      settings: PersistorSettings(
        key: Persistor.keyBuilder((snap) {
          if (snap.data.age >= 18) {
            return 'adult_users';
          }
          return 'users';
        }),
      ),
    )
  }
}
loon >
  __store__.json
  users.json
  adult_users.json

Now 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 whenever 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.

🎨 Custom persistence

If you would prefer to persist data using an alternative implementation than the default persistors, you can implement the persistence interface:

import 'package:loon/loon.dart';

class MyPersistor extends Persistor {
  /// Initialization function called when the persistor is instantiated to execute and setup work.
  Future<void> init();

  /// Persist function called with the bath of documents that have changed (including been deleted) within the last throttle window
  /// specified by the [Persistor.persistenceThrottle] duration.
  Future<void> persist(Set<Document> docs);

  /// Hydration function called to read data from persistence. If no references are specified,
  /// then it hydrates all persisted data. if refs are specified, it hydrates only the data at
  /// and under those paths.
  Future<Json> hydrate([Set<StoreReference>? refs]);

  /// Clear function used to clear all documents under the given store paths.
  Future<void> clear(Set<StoreReference> refs);

  /// Clears all documents and removes all persisted data.
  Future<void> clearAll();
}

Extensions

Documents

  • Architecture: An architecture doc that breaks down the inner workings of the library.
  • Debugging: A debugging doc that calls out any known issues or troubleshooting suggestions across different platforms.

Happy coding

That's all for now! Want a feature? Found a bug? Create an issue!

카테고리

의존성

의존성 목록 7 개

소스 저장소

이 패키지를 사용하는 프로젝트

아직 데이터가 없습니다.