Skip to content

Commit

Permalink
Add example showcasing storage of binary data
Browse files Browse the repository at this point in the history
  • Loading branch information
tp committed Nov 10, 2024
1 parent 0603dc9 commit c527f82
Show file tree
Hide file tree
Showing 5 changed files with 309 additions and 0 deletions.
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

0 comments on commit c527f82

Please sign in to comment.