Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add example showcasing storage of binary data #20

Merged
merged 1 commit into from
Nov 10, 2024
Merged
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
2 changes: 1 addition & 1 deletion analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ analyzer:
always_declare_return_types: error
always_put_required_named_parameters_first: error
always_require_non_null_named_parameters: error
# Disabled due to the usage in "hand-rolled" from JSON, will be removed once we can make use of the JSON macro in tests & the example
# Disabled due to the usage in manual `fromJSON` implementations, will be removed once we can make use of the JSON macro in tests & the example
argument_type_not_assignable: ignore
avoid_bool_literals_in_conditional_expressions: error
avoid_catching_errors: error
Expand Down
2 changes: 2 additions & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:io';

import 'package:flutter/cupertino.dart';
import 'package:indexed_entity_store_example/src/examples/async_value_group_and_detail.dart';
import 'package:indexed_entity_store_example/src/examples/binary_data_storage.dart';
import 'package:indexed_entity_store_example/src/examples/future_value_listenable_group_and_detail.dart';
import 'package:indexed_entity_store_example/src/examples/simple_synchronous.dart';
import 'package:path_provider/path_provider.dart';
Expand Down Expand Up @@ -45,6 +46,7 @@ class _ExampleSelectorState extends State<ExampleSelector> {
AsyncValueGroupDetailExample(),
'`Future<ValueSource<T>>`-based product list & detail view':
AsynchronousGroupDetailExample(),
'Binary data storage': BinaryDataStorageExample(),
};

@override
Expand Down
287 changes: 287 additions & 0 deletions example/lib/src/examples/binary_data_storage.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
import 'dart:convert';
import 'dart:typed_data';

import 'package:flutter/cupertino.dart';
import 'package:http/http.dart' as http;
import 'package:indexed_entity_store/indexed_entity_store.dart';
import 'package:indexed_entity_store_example/src/stores/database_helper.dart';
import 'package:value_listenable_extensions/value_listenable_extensions.dart';

class BinaryDataStorageExample extends StatefulWidget {
const BinaryDataStorageExample({
super.key,
});

@override
State<BinaryDataStorageExample> createState() =>
_BinaryDataStorageExampleState();
}

class _BinaryDataStorageExampleState extends State<BinaryDataStorageExample> {
final database = getNewDatabase();

late final simpleRepository = SimpleImageRepository(
store: database.entityStore(plainImageConnector),
);
late final withMetadataRepository = ImageWithMetadataRepository(
store: database.entityStore(imageWithMetadataConnector),
);

late var simpleImage = simpleRepository.getImage();
late var metadataImage = withMetadataRepository.getImage();

@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Shows 2 approaches of how to store binary data. Press the refresh button to re-init, loading the images anew from memory.',
),
const SizedBox(height: 20),
CupertinoButton.filled(
onPressed: _reInit,
child: const Text('Re-init'),
),
const SizedBox(height: 20),
Expanded(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Simple binary storage',
style: TextStyle(fontWeight: FontWeight.bold),
),
ValueListenableBuilder(
valueListenable: simpleImage,
builder: (context, image, _) {
if (image == null) {
return const CupertinoActivityIndicator();
}

return SizedBox(
width: 100,
// NOTE(tp): Even though the image's (PNG) data is available synchronously now, the decoding still happens asynchronously in the framework as multiple frames,
// thus the UI minimally flickers initially as no explicit height is given here.
child: Image.memory(image),
);
},
),
const SizedBox(height: 20),
const Text(
'Binary storage with metadata prefix',
style: TextStyle(fontWeight: FontWeight.bold),
),
ValueListenableBuilder(
valueListenable: metadataImage,
builder: (context, image, _) {
if (image == null) {
return const CupertinoActivityIndicator();
}

return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 100,
child: Image.memory(image.data),
),
Text(
'Fetched at ${image.metadata.fetchedAt}\nfrom ${image.metadata.url}',
),
],
);
},
),
],
),
),
),
],
);
}

@override
void dispose() {
simpleImage.dispose();
metadataImage.dispose();

database.dispose();

super.dispose();
}

void _reInit() {
setState(() {
simpleImage.dispose();
simpleImage = simpleRepository.getImage();

metadataImage.dispose();
metadataImage = withMetadataRepository.getImage();
});
}
}

/// Shows how to straightforwardly store binary data
///
/// This is generally only advisable when no meta-data is needed, and the users know the key needed to access the items afterwards (as they can not be retrieved with the connector's current `deserialize` interface)
class SimpleImageRepository {
final PlainImageStore _simpleStore;

SimpleImageRepository({
required PlainImageStore store,
}) : _simpleStore = store;

DisposableValueListenable<Uint8List?> getImage() {
const imageKey = 'profile_picture';

final data = _simpleStore.read(imageKey).transform((r) => r?.data);

if (data.value == null) {
debugPrint('ImageRepository: Fetching image from network');
// Fetch the data from the network if we don't have it yet
http
.readBytes(Uri.parse('https://api.multiavatar.com/$imageKey.png'))
.then((response) {
_simpleStore.write((key: imageKey, data: response));
});
} else {
debugPrint('ImageRepository: Using stored image');
}

return data;
}
}

typedef PlainImageStore = IndexedEntityStore<ImageRow, String>;

typedef ImageRow = ({String key, Uint8List data});

/// Connector which stores a plain image (or any binary data) by key
final plainImageConnector = IndexedEntityConnector<ImageRow, String, List<int>>(
entityKey: 'plain_image',
getPrimaryKey: (t) => t.key,
getIndices: (index) {},
serialize: (t) => t.data,
// TODO(tp): In this example the key is not returned, which just shows the need to have support for additional meta-data which is shown in the second connector
deserialize: (s) => (key: '', data: Uint8List.fromList(s)),
);

/// Shows how to store binary data with meta data
class ImageWithMetadataRepository {
final ImageWithMetadataStore _store;

ImageWithMetadataRepository({
required ImageWithMetadataStore store,
}) : _store = store;

DisposableValueListenable<ImageWithMetadata?> getImage() {
const imageId = 12345678;

final data = _store.read(imageId);

if (data.value == null ||
data.value!.metadata.fetchedAt
.isBefore(DateTime.now().subtract(const Duration(seconds: 10)))) {
debugPrint('ImageWithMetadataRepository: Fetching image from network');

final url =
'https://api.multiavatar.com/${DateTime.now().millisecondsSinceEpoch}.png';

// Fetch the data from the network if we don't have it yet
http.readBytes(Uri.parse(url)).then((response) {
_store.write((
metadata: ImageMetadata(
id: imageId,
fetchedAt: DateTime.now(),
url: url,
),
data: response
));
});
} else {
debugPrint('ImageWithMetadataRepository: Using stored image');
}

return data;
}
}

typedef ImageWithMetadataStore = IndexedEntityStore<ImageWithMetadata, int>;

typedef ImageWithMetadata = ({ImageMetadata metadata, Uint8List data});

final imageWithMetadataConnector =
IndexedEntityConnector<ImageWithMetadata, int, Uint8List>(
entityKey: 'metadata_image',
getPrimaryKey: (t) => t.metadata.id,
getIndices: (index) {
index((t) => t.metadata.fetchedAt, as: 'fetchedAt');
},
serialize: (t) {
final metadataJSON = JsonUtf8Encoder().convert(t.metadata.toJSON());

final lengthHeader = Uint8List.view(
// uint32 is enough for 4GB of metadata
(ByteData(4)..setUint32(0, metadataJSON.length)).buffer,
);

return (BytesBuilder(copy: false)
..add(lengthHeader)
..add(metadataJSON)
..add(t.data))
.takeBytes();
},
deserialize: (s) {
final metaDataLength = ByteData.view(s.buffer).getUint32(0);

// This creates a more efficient UTF8 JSON Decoder internally (https://stackoverflow.com/a/79158945)
final jsonDecoder = const Utf8Decoder().fuse(const JsonDecoder());
final metaData = ImageMetadata.fromJSON(
jsonDecoder.convert(Uint8List.view(s.buffer, 4, metaDataLength))
as Map<String, dynamic>,
);

return (
metadata: metaData,

// Assuming that the binary data is much bigger than the meta-data,
// this a view in the underlying storage and doesn't copy it out (this could be made adaptive, to
// copy when the binary data is actually less than e.g. half of the bytes)
data: Uint8List.view(s.buffer, 4 + metaDataLength).asUnmodifiableView(),
);
},
);

class ImageMetadata {
final int id;
final DateTime fetchedAt;
final String url;

ImageMetadata({
required this.id,
required this.fetchedAt,
required this.url,
});

// These would very likely be created by [json_serializable](https://pub.dev/packages/json_serializable)
// or [freezed](https://pub.dev/packages/freezed) already for your models
Map<String, dynamic> toJSON() {
return {
'id': id,
'fetchedAt': fetchedAt.millisecondsSinceEpoch,
'url': url,
};
}

static ImageMetadata fromJSON(Map<String, dynamic> json) {
return ImageMetadata(
id: json['id'],
fetchedAt: DateTime.fromMillisecondsSinceEpoch(json['fetchedAt']),
url: json['url'],
);
}
}

// TODO(tp): Write abstraction for "binary with metadata" store
2 changes: 2 additions & 0 deletions example/macos/Runner/DebugProfile.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>
16 changes: 16 additions & 0 deletions example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,22 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
http:
dependency: "direct main"
description:
name: http
sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010
url: "https://pub.dev"
source: hosted
version: "1.2.2"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
url: "https://pub.dev"
source: hosted
version: "4.0.2"
indexed_entity_store:
dependency: "direct main"
description:
Expand Down
2 changes: 2 additions & 0 deletions example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ dependencies:

riverpod: ^2.6.1

http: ^1.2.2

dev_dependencies:
flutter_test:
sdk: flutter
Expand Down
Loading