Skip to content
This repository has been archived by the owner on Feb 9, 2024. It is now read-only.

Commit

Permalink
Migrated to Hive (#16)
Browse files Browse the repository at this point in the history
## Purpose

Migrating from Shared Preferences to Hive.
https://concordium.atlassian.net/browse/CBW-1474

## Changes

* Migrated from Shared Preferences to Hive
* Renamed from `<Prefix>Service` to `<Prefix>Provider` for
`StorageProvider` to follow the naming conventions of the Bloc
architecture.
https://bloclibrary.dev/#/architecture?id=data-provider
* Renamed pipeline to just `ci.yml` since test step has been added.

## Checklist

- [x] My code follows the style of this project.
- [x] The code compiles without warnings.
- [x] I have performed a self-review of the changes.
- [x] I have documented my code, in particular the intent of the
      hard-to-understand areas.
  • Loading branch information
Søren Schwartz authored Nov 29, 2023
2 parents 99f2fd7 + 68a7f4a commit 17c6730
Show file tree
Hide file tree
Showing 18 changed files with 468 additions and 209 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Analyze and format
name: Analyze, format and test

on:
# Trigger the workflow on pushes to the main and feature branches as well as PRs targeting them.
Expand Down Expand Up @@ -31,3 +31,20 @@ jobs:

- name: Check format
run: dart format --output=none --set-exit-if-changed . -l 150

test:
runs-on: ubuntu-22.04
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.flutter_version }}
channel: 'stable'
cache: true

- name: Test
run: flutter test

3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,6 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release

# Hive testing
/test/hive_storage_test
12 changes: 6 additions & 6 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
PODS:
- Flutter (1.0.0)
- shared_preferences_foundation (0.0.1):
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- url_launcher_ios (0.0.1):
- Flutter

DEPENDENCIES:
- Flutter (from `Flutter`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)

EXTERNAL SOURCES:
Flutter:
:path: Flutter
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"

SPEC CHECKSUMS:
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
url_launcher_ios: bf5ce03e0e2088bad9cc378ea97fa0ed5b49673b

PODFILE CHECKSUM: 70d9d25280d0dd177a5f637cdb0f0b0b12c6a189

Expand Down
16 changes: 16 additions & 0 deletions lib/entities/accepted_terms_and_conditions.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import 'package:hive_flutter/hive_flutter.dart';

part 'accepted_terms_and_conditions.g.dart';

/// Version of the Terms & Conditions accepted by the user.
@HiveType(typeId: 1)
class AcceptedTermsAndConditions {
static const table = "accepted_terms_and_conditions";

@HiveField(0)
final String version;
@HiveField(1)
final DateTime acceptedAt;

AcceptedTermsAndConditions({required this.version, required this.acceptedAt});
}
41 changes: 41 additions & 0 deletions lib/entities/accepted_terms_and_conditions.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

73 changes: 53 additions & 20 deletions lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import 'package:concordium_wallet/repositories/terms_and_conditions_repository.dart';
import 'package:concordium_wallet/screens/routes.dart';
import 'package:concordium_wallet/services/http.dart';
import 'package:concordium_wallet/services/shared_preferences/service.dart';
import 'package:concordium_wallet/providers/storage.dart';
import 'package:concordium_wallet/services/wallet_proxy/service.dart';
import 'package:concordium_wallet/state/config.dart';
import 'package:concordium_wallet/state/network.dart';
import 'package:concordium_wallet/state/services.dart';
import 'package:concordium_wallet/state/terms_and_conditions.dart';
import 'package:concordium_wallet/theme.dart';
import 'package:concordium_wallet/types/future_value.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:shared_preferences/shared_preferences.dart';

void main() {
runApp(const App());
Expand All @@ -33,13 +34,13 @@ Future<Config> loadConfig(HttpService http) async {
Future<ServiceRepository> bootstrap() async {
const http = HttpService();
final configFuture = loadConfig(http);
final prefsFuture = SharedPreferences.getInstance();
final storageFuture = StorageProvider.init();
final config = await configFuture;
final prefs = await prefsFuture;
final storageService = await storageFuture;
return ServiceRepository(
config: config,
http: http,
sharedPreferences: SharedPreferencesService(prefs),
storage: storageService,
);
}

Expand All @@ -53,21 +54,11 @@ class App extends StatelessWidget {
return _WithServiceRepository(
child: _WithSelectedNetwork(
initialNetwork: initialNetwork,
child: MultiBlocProvider(
providers: [
BlocProvider(
create: (context) {
// Initialize T&C by loading the currently accepted version from shared preferences.
final prefs = context.read<ServiceRepository>().sharedPreferences;
return TermsAndConditionAcceptance(prefs);
},
),
],
child: MaterialApp(
routes: appRoutes,
theme: concordiumTheme(),
),
),
child: _WithTermsAndConditionAcceptance(
child: MaterialApp(
routes: appRoutes,
theme: concordiumTheme(),
)),
),
);
}
Expand Down Expand Up @@ -158,6 +149,48 @@ class _WithSelectedNetworkState extends State<_WithSelectedNetwork> {
}
}

class _WithTermsAndConditionAcceptance extends StatefulWidget {
final Widget child;

const _WithTermsAndConditionAcceptance({required this.child});

@override
State<_WithTermsAndConditionAcceptance> createState() => _WithTermsAndConditionAcceptanceState();
}

class _WithTermsAndConditionAcceptanceState extends State<_WithTermsAndConditionAcceptance> {
late final Future<FutureValue<AcceptedTermsAndConditionsState?>> _lastAccepted;
late final TermsAndConditionsRepository _repository;

@override
void initState() {
super.initState();
final storage = context.read<ServiceRepository>().storage;
setState(() {
_repository = TermsAndConditionsRepository(storageProvider: storage);
_lastAccepted = _repository.getAcceptedTermsAndConditions().then(FutureValue.new);
});
}

@override
Widget build(BuildContext context) {
return FutureBuilder(
future: _lastAccepted,
builder: (_, snapshot) {
if (snapshot.data != null) {
return BlocProvider(
create: (_) {
return TermsAndConditionAcceptance(_repository, snapshot.requireData.value);
},
child: widget.child);
} else if (snapshot.hasError) {
// TODO Handle error
}
return const _Initializing();
});
}
}

class _Initializing extends StatelessWidget {
const _Initializing();

Expand Down
53 changes: 53 additions & 0 deletions lib/providers/storage.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import 'package:concordium_wallet/entities/accepted_terms_and_conditions.dart';
import 'package:hive_flutter/hive_flutter.dart';

/// Service for interacting with [Hive].
class StorageProvider {
final LazyBox<AcceptedTermsAndConditions> _acceptedTermsAndConditionBox;

const StorageProvider._(this._acceptedTermsAndConditionBox);

static Future<StorageProvider> init() async {
await Hive.initFlutter();
_registerAdapters();
await _openBoxes();

return StorageProvider._(Hive.lazyBox<AcceptedTermsAndConditions>(AcceptedTermsAndConditions.table));
}

/// Register all adapters needed for typed boxes.
static void _registerAdapters() {
Hive.registerAdapter(AcceptedTermsAndConditionsAdapter());
Hive.registerAdapter(PreciseDateTimeAdapter(), override: true, internal: true);
}

/// Opens all boxes asynchronously.
static Future<void> _openBoxes() async {
final atcFuture = Hive.openLazyBox<AcceptedTermsAndConditions>(AcceptedTermsAndConditions.table);
await Future.wait([atcFuture]);
}

LazyBox<AcceptedTermsAndConditions> get acceptedTermsAndConditionBox => _acceptedTermsAndConditionBox;
}

/// A bit modified DateTimeWithTimezoneAdapter (https://github.com/isar/hive/blob/470473ffc1ba39f6c90f31ababe0ee63b76b69fe/hive/lib/src/adapters/date_time_adapter.dart#L25)
/// This adapter is relevant because by default, [Hive] only stores datetimes down to millisecond precision.
/// It's derived from issue in link and proposed as a solution (https://github.com/isar/hive/issues/474#issuecomment-730562545).
/// The type ID needs to be 18 as it's required to overwrite the existing one registered. (https://github.com/isar/hive/blob/470473ffc1ba39f6c90f31ababe0ee63b76b69fe/hive/lib/src/adapters/date_time_adapter.dart#L28).
class PreciseDateTimeAdapter extends TypeAdapter<DateTime> {
@override
final typeId = 18;

@override
DateTime read(BinaryReader reader) {
var micros = reader.readInt();
var isUtc = reader.readBool();
return DateTime.fromMicrosecondsSinceEpoch(micros, isUtc: isUtc);
}

@override
void write(BinaryWriter writer, DateTime obj) {
writer.writeInt(obj.microsecondsSinceEpoch);
writer.writeBool(obj.isUtc);
}
}
38 changes: 38 additions & 0 deletions lib/repositories/terms_and_conditions_repository.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import 'package:concordium_wallet/providers/storage.dart';
import 'package:concordium_wallet/state/terms_and_conditions.dart';
import 'package:concordium_wallet/entities/accepted_terms_and_conditions.dart';

class TermsAndConditionsRepository {
static const String key = "accepted_terms_and_condition";

final StorageProvider _storageProvider;

const TermsAndConditionsRepository({required StorageProvider storageProvider}) : _storageProvider = storageProvider;

/// Reads the currently accepted T&C version.
Future<AcceptedTermsAndConditionsState?> getAcceptedTermsAndConditions() async {
var model = await _storageProvider.acceptedTermsAndConditionBox.get(key);
return _toState(model);
}

/// Writes the currently accepted T&C version.
Future<void> writeAcceptedTermsAndConditions(AcceptedTermsAndConditionsState acceptedTermsAndConditions) {
return _storageProvider.acceptedTermsAndConditionBox.put(key, _fromState(acceptedTermsAndConditions));
}

/// Deletes the currently accepted T&C version.
Future<void> deleteTermsAndConditionsAcceptedVersion() {
return _storageProvider.acceptedTermsAndConditionBox.delete(key);
}

AcceptedTermsAndConditions _fromState(AcceptedTermsAndConditionsState state) {
return AcceptedTermsAndConditions(version: state.version, acceptedAt: state.acceptedAt);
}

AcceptedTermsAndConditionsState? _toState(AcceptedTermsAndConditions? model) {
if (model == null) {
return null;
}
return AcceptedTermsAndConditionsState(version: model.version, acceptedAt: model.acceptedAt);
}
}
2 changes: 1 addition & 1 deletion lib/screens/home/screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ class _HomeScreenState extends State<HomeScreen> {
const SizedBox(height: 8),
ElevatedButton(
onPressed: () {
const tac = AcceptedTermsAndConditions(version: '1.2.3');
final tac = AcceptedTermsAndConditionsState.acceptNow('1.2.3');
context.read<TermsAndConditionAcceptance>().userAccepted(tac);
},
child: const Text('Set accepted T&C version to 1.2.3'),
Expand Down
4 changes: 1 addition & 3 deletions lib/screens/terms_and_conditions/screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,7 @@ class _TermsAndConditionsScreenState extends State<TermsAndConditionsScreen> {
Function()? _onAcceptButtonPressed(BuildContext context) {
if (isAccepted) {
return () {
final tac = AcceptedTermsAndConditions(
version: widget.validTermsAndConditions.version,
);
final tac = AcceptedTermsAndConditionsState.acceptNow(widget.validTermsAndConditions.version);
context.read<TermsAndConditionAcceptance>().userAccepted(tac);
};
}
Expand Down
25 changes: 0 additions & 25 deletions lib/services/shared_preferences/service.dart

This file was deleted.

8 changes: 4 additions & 4 deletions lib/state/services.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'package:concordium_wallet/services/http.dart';
import 'package:concordium_wallet/services/shared_preferences/service.dart';
import 'package:concordium_wallet/providers/storage.dart';
import 'package:concordium_wallet/services/wallet_proxy/service.dart';
import 'package:concordium_wallet/state/config.dart';
import 'package:concordium_wallet/state/network.dart';
Expand Down Expand Up @@ -36,10 +36,10 @@ class ServiceRepository {
/// Global service for performing HTTP calls.
final HttpService http;

/// Global service for interacting with shared preferences.
final SharedPreferencesService sharedPreferences;
/// Global service for interacting with storage.
final StorageProvider storage;

ServiceRepository({required this.config, required this.http, required this.sharedPreferences});
ServiceRepository({required this.config, required this.http, required this.storage});

/// Activate the network with the provided name.
///
Expand Down
Loading

0 comments on commit 17c6730

Please sign in to comment.