Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 82 additions & 64 deletions docs/user-facing/local_data.md
Original file line number Diff line number Diff line change
@@ -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<E>` 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<void> setupHive() async {
WidgetsFlutterBinding.ensureInitialized();
await Hive.initFlutter();
_registerAdapters();
}

void _registerAdapters() {
Hive.registerAdapter<User>(UserAdapter());
}

abstract class IHiveRepository<E> {
Box<E>? _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<Box<E>> get box async {
_box ??= await Hive.openBox<E>(boxKey);
return _box!;
Future<void> 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<String, Map<String, dynamic>> _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<String, Object?> 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<User> 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<String, Map<String, dynamic>> _store = stringMapStoreFactory.store(_storeName);

@override
Future<User?> getUser(String userKey) async {
return (await box).get(userKey);
Future<Result<User?, UserRepositoryException>> 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<void> saveUser(String userKey, User user) async {
await (await box).put(userKey, user);
Future<Result<void, UserRepositoryException>> 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<void> deleteUser(String userKey) async {
await (await box).delete(userKey);
Future<Result<void, UserRepositoryException>> 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.
Expand Down
9 changes: 6 additions & 3 deletions docs/user-facing/state_management.md
Original file line number Diff line number Diff line change
Expand Up @@ -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?

Expand Down Expand Up @@ -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.

Expand All @@ -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 {
Expand Down Expand Up @@ -101,7 +103,6 @@ void _onSomeEvent(

- Second one, is modification of current state with specified state class.


```dart
// Class
@freezed
Expand Down Expand Up @@ -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