diff --git a/docs/user-facing/local_data.md b/docs/user-facing/local_data.md index 49f6ff6..6286445 100644 --- a/docs/user-facing/local_data.md +++ b/docs/user-facing/local_data.md @@ -1,123 +1,141 @@ # 📒 Caching, storing local data -We use [Hive](https://docs.hivedb.dev/#/) database to store data locally. Hive is a lightweight, powerful database which runs fast on the device. -Unless you absolutely need to model your data with many relationships, choosing this pure-Dart package with no native dependencies can be the best option. -Hive is centered around the idea of `boxes`. `Box` has to be opened before use. In addition to the plain-flavored Boxes, -there are also options which support lazy-loading of values and encryption. +We use [sembast](https://pub.dev/packages/sembast) database to store data locally. Sembast is a NoSQL database that works in both Flutter and Flutter web projects. It is simple, fast, and supports cross-platform persistence, making it an ideal choice for our Flutter template. Fully compatible with Dart VM and Flutter without the need for additional plugins. Implemented entirely in Dart, ensuring seamless operation across all platforms, including macOS, Android, iOS, Linux, and Windows. For web, separate package [sembast_web](https://pub.dev/packages/sembast_web) is required. ## Initialization -Hive needs to be ​initialized​ to, among other things, know in which directory it stores the data. A service for hive was created. -The `setupHive` method initializes hive for flutter and registers adapters and is called in `main`. -`IHiveRepository` is an mixin that manages Hive box opening, where `E` is a specific type depending on the type of data being stored. +Sembast needs to be initialized to set up the database instance. A service for `sembast` was created. The `setupSembast` method initializes the database differently for web and non-web platforms and is called in `main.dart`. -Hive service +### `sembast` service ```dart -Future setupHive() async { - WidgetsFlutterBinding.ensureInitialized(); - await Hive.initFlutter(); - _registerAdapters(); -} - -void _registerAdapters() { - Hive.registerAdapter(UserAdapter()); -} - -abstract class IHiveRepository { - Box? _box; +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:flutter/material.dart'; +import 'package:sembast/sembast_io.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:sembast_web/sembast_web.dart'; - String get boxKey; +const String _databaseName = 'app_database.db'; +late Database _database; - Future> get box async { - _box ??= await Hive.openBox(boxKey); - return _box!; +Future setupSembast() async { + WidgetsFlutterBinding.ensureInitialized(); + if (kIsWeb) { + _database = await databaseFactoryWeb.openDatabase(_databaseName); + } else { + final appDir = await getApplicationDocumentsDirectory(); + final dbPath = '${appDir.path}/$_databaseName'; + _database = await databaseFactoryIo.openDatabase(dbPath); } } + +Database get database => _database; ``` -## Boxes +## Store -Data can be stored and read only from an opened `Box`. Opening a `Box` loads all of its data from the local storage into memory for immediate access. +Sembast works with `StoreRef` to hold collections of data. Data is stored in key-value pairs within a store. -- Open box +- Define a store ```dart -Hive.openBox('userBox'); +final StoreRef> _store = stringMapStoreFactory.store('userStore'); ``` -- Get an already opened instance +- Add or update data ```dart -Hive.box('name'); +await _store.record('userKey').put(database, user.toJson()); ``` -There are two basic options of adding data - either call `put(key, value)` and specify the key yourself, -or call `add` and utilize Hive's auto-incrementing keys. Unless you absolutely need to define the keys manually, -calling add is the better and simpler option. +- Retrieve data ```dart -userBox.add(User('Test User', 28)); +final record = await _store.record('userKey').get(database); ``` -### TypeAdapter +- Delete data -Hive works with binary data. While it's entirely possible to write a custom adapter which fumbles with a ​​​​​BinaryWriter and a BinaryReader, -it's much easier to let the ​`hive_generator`​ package do the hard job for you. Making an adapter for specific class is then as simple as adding a few annotations. +```dart +await _store.record('userKey').delete(database); +``` -Creating a TypeAdapter +The test model here is `User`. ```dart -import 'package:hive/hive.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +part 'user.freezed.dart'; part 'user.g.dart'; -@HiveType() -class User { - @HiveField(0) - final String name; - - @HiveField(1) - final int age; - - User(this.name, this.age); +@freezed +class User with _$User { + const factory User({ + required int pk, + String? email, + String? phone, + String? firstName, + String? lastName, + }) = _User; + + factory User.fromJson(Map json) => _$UserFromJson(json); } ``` -To generate TypeAdapter you should run `flutter packages pub run build_runner build`. Thanks to the `Makefile` scripts, we can do this with `make generate-code` -`make watch-and-generate-code` until stopped will watch for file changes and automatically build code if necessary. -It's useful when dealing with a lot of code generation since it'll do a whole project build only at start and then do smaller builds only for affected files. -The created adapter must be registered. - ## Repositories -`IHiveRepository` should be used with every repository that is using Hive. +We implement the `IUserRepository` interface with sembast operations. Result is a type that represents either `Success` or `Failure`. Inspired by functional programming. Thanks to this, we have clear and easy-to-use error handling. Example ```dart -class UserRepository with IHiveRepository implements IUserRepository { - @override - String get boxKey => 'userInfoBoxKey'; +import 'package:sembast/sembast.dart'; +import 'package:result_type/result_type.dart'; +import 'package:template/src/services/sembast.dart'; + +class UserRepository implements IUserRepository { + static const String _storeName = 'userStore'; + final StoreRef> _store = stringMapStoreFactory.store(_storeName); @override - Future getUser(String userKey) async { - return (await box).get(userKey); + Future> getUser(String userKey) async { + try { + final record = await _store.record(userKey).get(database); + return Success(record != null ? User.fromJson(record) : null); + } catch (e) { + return Failure(UserRepositoryException(MainScreenErrorType.fetchError)); + } } @override - Future saveUser(String userKey, User user) async { - await (await box).put(userKey, user); + Future> saveUser(String userKey, User user) async { + try { + await _store.record(userKey).put(database, user.toJson()); + return Success(null); + } catch (e) { + return Failure(UserRepositoryException(MainScreenErrorType.saveError)); + } } @override - Future deleteUser(String userKey) async { - await (await box).delete(userKey); + Future> deleteUser(String userKey) async { + try { + await _store.record(userKey).delete(database); + return Success(null); + } catch (e) { + return Failure(UserRepositoryException(MainScreenErrorType.deleteError)); + } } +} + +class UserRepositoryException implements Exception { + UserRepositoryException(this.errorType); + final MainScreenErrorType errorType; +} ``` -### Dependency injection +## Dependency injection Dependency injection is an object-oriented technique that sends the dependencies of another object to an object. Using dependency injection, we can also move the creation and restriction of dependent objects outside the classes. This concept brings a more significant level of adaptability, decoupling, and simpler testing. diff --git a/docs/user-facing/state_management.md b/docs/user-facing/state_management.md index cfb6bc3..259342a 100644 --- a/docs/user-facing/state_management.md +++ b/docs/user-facing/state_management.md @@ -7,7 +7,7 @@ We're using bloc (mostly) as out state management. It provides us easy separatio 3. Data (work with network or local data will be located here) We recommend to combine it with [freezed](https://pub.dev/packages/freezed) package. It reduce significant amount of boilerplate code and make it easier to read. -Also it helps with generating code for models, but please be aware and don't use freezed generation to classes you want to store using hive, while this [issue](https://github.com/hivedb/hive/issues/795) is open. +Also it helps with generating code for models. ## BLoC or Cubit? @@ -56,6 +56,7 @@ class CounterState with _$CounterState { }) = _CounterState; } ``` + 1) State is represented by object of a single class 2) Fields are always the same for this state. @@ -64,6 +65,7 @@ Which one is the best to use? None of them, it depends on logic you want to crea ## Freezed + BLoC, Events Creation of events should be always implemented using the factory method pattern, it makes it easy to read and reduce boilerplate code we had in previous version of template + ```dart @freezed class CounterEvent with _$CounterEvent { @@ -101,7 +103,6 @@ void _onSomeEvent( - Second one, is modification of current state with specified state class. - ```dart // Class @freezed @@ -134,6 +135,8 @@ We have to be really careful with this solution, to don't allow type cast except - Install [this](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) VSCode extension to save your time while you're creating your blocs - Separate your models, API fetches, UI screens, and blocs/cubits by features - Write tests for each of your bloc/cubit -* Put only one [BlocProvider](https://pub.dev/documentation/flutter_bloc/latest/flutter_bloc/BlocProvider-class.html) in the tree, then just use [BlocBuilder](https://pub.dev/documentation/flutter_bloc/latest/flutter_bloc/BlocBuilder-class.html) to have access to your bloc or cubit + +- Put only one [BlocProvider](https://pub.dev/documentation/flutter_bloc/latest/flutter_bloc/BlocProvider-class.html) in the tree, then just use [BlocBuilder](https://pub.dev/documentation/flutter_bloc/latest/flutter_bloc/BlocBuilder-class.html) to have access to your bloc or cubit + - If your bloc contains some work with streams, don't forget to close it in [close()](https://pub.dev/documentation/bloc/latest/bloc/Bloc/close.html) method of your bloc - **Do** use [MultiBlocProvider](https://pub.dev/documentation/flutter_bloc/latest/flutter_bloc/MultiBlocProvider-class.html) in case you need to provide more than one bloc to your module