From f8c713a9fa4cbb3d8c71599a078c61a4eceaa3bd Mon Sep 17 00:00:00 2001 From: Gilang Pratama Date: Sun, 8 Jan 2023 12:10:56 +0700 Subject: [PATCH 1/2] feat/HttpCacheStoragePaged initialize storage --- lib/src/http_cache.dart | 20 +++++++++++++++++++- lib/src/http_cache_storage.dart | 7 +++---- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/lib/src/http_cache.dart b/lib/src/http_cache.dart index 5cad1ee..f884e00 100644 --- a/lib/src/http_cache.dart +++ b/lib/src/http_cache.dart @@ -91,13 +91,22 @@ class HttpCache extends StatefulWidget { static Future init( {required Directory storageDirectory, HttpCacheChiper? chiper}) async { HttpCacheStorage storage = await HttpCacheStorage.initialize( - storageDirectory: storageDirectory, chiper: chiper); + storageDirectory: storageDirectory, + boxName: "http_cache_storage", + chiper: chiper); + HttpCacheStorage storagePaged = await HttpCacheStorage.initialize( + storageDirectory: storageDirectory, + boxName: "http_cache_storage_paged", + chiper: chiper); HttpCache.storage = storage; + HttpCache.storagePaged = storagePaged; return storage; } static HttpCacheStorage? _storage; + static HttpCacheStorage? _storagePaged; + ///initialize the storage static set storage(HttpCacheStorage storage) => _storage = storage; @@ -106,6 +115,15 @@ class HttpCache extends StatefulWidget { if (_storage == null) throw NoStorage(); return _storage!; } + + ///initialize the storage + static set storagePaged(HttpCacheStorage storage) => _storagePaged = storage; + + ///get the storage instance + static HttpCacheStorage get storagePaged { + if (_storagePaged == null) throw NoStorage(); + return _storagePaged!; + } } class _HttpCacheState extends State> { diff --git a/lib/src/http_cache_storage.dart b/lib/src/http_cache_storage.dart index b19821a..3d40bdc 100644 --- a/lib/src/http_cache_storage.dart +++ b/lib/src/http_cache_storage.dart @@ -56,6 +56,7 @@ class HttpCacheStorage { static Future initialize({ required Directory storageDirectory, + required String boxName, HttpCacheChiper? chiper, }) async { if (_instance != null) return _instance!; @@ -63,12 +64,10 @@ class HttpCacheStorage { Box box; if (storageDirectory == webStorageDirectory) { - box = await hive.openBox('http_cache_flutter', - encryptionCipher: chiper); + box = await hive.openBox(boxName, encryptionCipher: chiper); } else { hive.init(storageDirectory.path); - box = await hive.openBox('http_cache_flutter', - encryptionCipher: chiper); + box = await hive.openBox(boxName, encryptionCipher: chiper); } return HttpCacheStorage(box); From b3d75c3484e48e2227ccee7610797afdbdb2ad6f Mon Sep 17 00:00:00 2001 From: Gilang Pratama Date: Wed, 8 Feb 2023 18:41:52 +0700 Subject: [PATCH 2/2] create widget --- .rest | 1 + doc/handle_request_example/pubspec.lock | 2 +- lib/http_cache_flutter.dart | 8 + lib/src/http_cache.dart | 18 +- .../http_cache_paged/http_cache_paged.dart | 236 ++++++++++++++++++ .../http_cache_paged_actions.dart | 16 ++ .../http_cache_paged_builder_data.dart | 24 ++ lib/src/http_cache_paged/http_responses.dart | 76 ++++++ 8 files changed, 364 insertions(+), 17 deletions(-) create mode 100644 .rest create mode 100644 lib/src/http_cache_paged/http_cache_paged.dart create mode 100644 lib/src/http_cache_paged/http_cache_paged_actions.dart create mode 100644 lib/src/http_cache_paged/http_cache_paged_builder_data.dart create mode 100644 lib/src/http_cache_paged/http_responses.dart diff --git a/.rest b/.rest new file mode 100644 index 0000000..6e4e785 --- /dev/null +++ b/.rest @@ -0,0 +1 @@ +GET https://dummyjson.com/products?limit=15&skip=1 HTTP/1.1 \ No newline at end of file diff --git a/doc/handle_request_example/pubspec.lock b/doc/handle_request_example/pubspec.lock index 024a345..6737b36 100644 --- a/doc/handle_request_example/pubspec.lock +++ b/doc/handle_request_example/pubspec.lock @@ -108,7 +108,7 @@ packages: path: "../.." relative: true source: path - version: "0.0.2" + version: "0.1.0" http_parser: dependency: transitive description: diff --git a/lib/http_cache_flutter.dart b/lib/http_cache_flutter.dart index 7244169..7839592 100644 --- a/lib/http_cache_flutter.dart +++ b/lib/http_cache_flutter.dart @@ -5,3 +5,11 @@ export './src/http_cache_storage.dart' show HttpCacheStorage; export './src/debug_configuration.dart' show DeveloperDebug, HttpLog; export './src/http_cache_actions.dart' show HttpCacheActions; export './src/http_cache_builder_data.dart' show HttpCacheBuilderData; + +export './src/http_cache_paged/http_cache_paged.dart' show HttpCachePaged; +export './src/http_cache_paged/http_cache_paged_builder_data.dart' + show HttpCachePagedBuilderData; +export './src/http_cache_paged/http_cache_paged_actions.dart' + show HttpCachePagedActions; +export './src/http_cache_paged/http_responses.dart' + show HttpResponsePaged, HttpResponsePagedItem; diff --git a/lib/src/http_cache.dart b/lib/src/http_cache.dart index f884e00..e72a925 100644 --- a/lib/src/http_cache.dart +++ b/lib/src/http_cache.dart @@ -94,19 +94,14 @@ class HttpCache extends StatefulWidget { storageDirectory: storageDirectory, boxName: "http_cache_storage", chiper: chiper); - HttpCacheStorage storagePaged = await HttpCacheStorage.initialize( - storageDirectory: storageDirectory, - boxName: "http_cache_storage_paged", - chiper: chiper); + HttpCache.storage = storage; - HttpCache.storagePaged = storagePaged; + return storage; } static HttpCacheStorage? _storage; - static HttpCacheStorage? _storagePaged; - ///initialize the storage static set storage(HttpCacheStorage storage) => _storage = storage; @@ -115,15 +110,6 @@ class HttpCache extends StatefulWidget { if (_storage == null) throw NoStorage(); return _storage!; } - - ///initialize the storage - static set storagePaged(HttpCacheStorage storage) => _storagePaged = storage; - - ///get the storage instance - static HttpCacheStorage get storagePaged { - if (_storagePaged == null) throw NoStorage(); - return _storagePaged!; - } } class _HttpCacheState extends State> { diff --git a/lib/src/http_cache_paged/http_cache_paged.dart b/lib/src/http_cache_paged/http_cache_paged.dart new file mode 100644 index 0000000..1ac6e82 --- /dev/null +++ b/lib/src/http_cache_paged/http_cache_paged.dart @@ -0,0 +1,236 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:http_cache_flutter/http_cache_flutter.dart'; +import 'package:http/http.dart' as http; +import 'package:http_cache_flutter/src/error_impl.dart'; +import 'package:http_cache_flutter/src/hc_log.dart'; +import 'package:http_cache_flutter/src/hc_request.dart'; +import 'package:http_cache_flutter/src/http_cache_chiper.dart'; +import 'package:http_cache_flutter/src/http_response.dart'; + +class HttpCachePaged extends StatefulWidget { + final String storageKey; + + ///the initial url + final String url; + + ///your initial header to make this package fetching data from http + final Map? headers; + + ///your initial header to make this package fetching data from http + final Future>? futureHeaders; + + ///you can return your layout with this attribute. + final Widget Function(BuildContext context, HttpCachePagedBuilderData data) + builder; + + ///stale time of the fetching, it will automatically refetch when the key already stale + final Duration staleTime; + + ///you can debugging with this attribute + final HttpLog log; + + ///you can use it to handle http request + final Future Function( + String url, Map? headers)? handleRequest; + + ///this attribute used for unit test, you can mock the `http.Client` with `mockito` package + @visibleForTesting + final http.Client? clientSpy; + + const HttpCachePaged({ + Key? key, + required this.storageKey, + required this.url, + this.headers, + this.futureHeaders, + required this.builder, + this.staleTime = const Duration(minutes: 5), + this.log = const HttpLog(), + this.handleRequest, + this.clientSpy, + }) : super(key: key); + + @override + State> createState() => _HttpCachePagedState(); + + static Future init( + {required Directory storageDirectory, HttpCacheChiper? chiper}) async { + HttpCacheStorage storage = await HttpCacheStorage.initialize( + storageDirectory: storageDirectory, + boxName: "http_cache_storage", + chiper: chiper); + + HttpCachePaged.storage = storage; + + return storage; + } + + static HttpCacheStorage? _storage; + + ///initialize the storage + static set storage(HttpCacheStorage storage) => _storage = storage; + + ///get the storage instance + static HttpCacheStorage get storage { + if (_storage == null) throw NoStorage(); + return _storage!; + } +} + +class _HttpCachePagedState extends State> { + late String _url; + Map? _headers; + bool _isLoading = false; + bool _isLoadingMoreData = false; + late HttpResponsePaged pagedKey; + + List _data = []; + + @override + void initState() { + super.initState(); + initialize(); + } + + void initialize() async { + _url = widget.url; + _headers = widget.futureHeaders != null + ? await widget.futureHeaders + : widget.headers; + + _initKey(); + + if (_data.isEmpty) { + await _fetchWithLoading(); + return; + } + + if (_data.isEmpty || pagedKey.staleAt <= currentTime) { + await _fetchWithLoading(); + return; + } + setState(() {}); + } + + int get currentTime => DateTime.now().millisecondsSinceEpoch; + + int get _nextStale => currentTime + widget.staleTime.inMilliseconds; + + void _initKey() { + final data = HttpCachePaged.storage.read(widget.storageKey); + if (data == null) { + pagedKey = HttpResponsePaged( + staleAt: currentTime + widget.staleTime.inMilliseconds, + key: widget.storageKey, + items: []); + return; + } + + pagedKey = HttpResponsePaged.fromMap(data); + _data = pagedKey.items; + } + + void _setLoading(bool loading) => setState(() { + _isLoading = loading; + }); + + void _setLoadingMore(bool loading) => setState(() { + _isLoadingMoreData = loading; + }); + + Future _fetchWithLoading( + {String? url, Map? headers}) async { + _setLoading(true); + await _fetch(url: url, headers: headers); + } + + Future _fetch({String? url, Map? headers}) async { + if (url != null) { + _url = url; + } + if (headers != null) { + _headers = headers; + } + + http.Response response = await _handleRequest(); + + pagedKey = HttpResponsePaged( + staleAt: _nextStale, + key: widget.storageKey, + items: [ + HttpResponsePagedItem( + body: response.body, + statusCode: response.statusCode, + url: _url, + bodyBytes: response.bodyBytes, + headers: response.headers) + ], + ); + + _data = pagedKey.items; + + await HttpCachePaged.storage.write(widget.storageKey, pagedKey.toMap()); + + _setLoading(false); + } + + Future _handleRequest( + {String? url, Map? headers}) async { + late http.Response response; + if (widget.handleRequest != null) { + response = await widget.handleRequest!(url ?? _url, headers ?? _headers); + } else { + response = await HcRequest( + widget.clientSpy != null ? widget.clientSpy! : http.Client()) + .get(url ?? _url, null, null, headers ?? _headers); + } + + HCLog.handleLog( + type: HCLogType.server, + log: widget.log, + response: HttpResponse( + body: response.body, + statusCode: response.statusCode, + expiredAt: 0, + staleAt: 0, + ), + ); + + return response; + } + + Future _addMoreData(String url, {Map? headers}) async { + _setLoadingMore(true); + http.Response response = await _handleRequest(url: url, headers: headers); + _data.add(HttpResponsePagedItem( + body: response.body, + statusCode: response.statusCode, + url: url, + bodyBytes: response.bodyBytes, + headers: response.headers, + )); + await HttpCachePaged.storage + .write(widget.storageKey, pagedKey.copyWith(items: _data).toMap()); + _setLoadingMore(false); + } + + @override + Widget build(BuildContext context) { + return widget.builder( + context, + HttpCachePagedBuilderData( + responses: _data, + isLoading: _isLoading, + isLoadingMoreData: _isLoadingMoreData, + isError: false, + error: null, + actions: HttpCachePagedActions( + fetch: _fetch, + fetchWithLoading: _fetchWithLoading, + addMoreData: _addMoreData), + ), + ); + } +} diff --git a/lib/src/http_cache_paged/http_cache_paged_actions.dart b/lib/src/http_cache_paged/http_cache_paged_actions.dart new file mode 100644 index 0000000..b0c5c2b --- /dev/null +++ b/lib/src/http_cache_paged/http_cache_paged_actions.dart @@ -0,0 +1,16 @@ +class HttpCachePagedActions { + final Future Function({String? url, Map? headers}) + fetch; + final Future Function({String? url, Map? headers}) + fetchWithLoading; + + ///handle get more data from a new http url + final Future Function(String url, {Map? headers}) + addMoreData; + + const HttpCachePagedActions({ + required this.fetch, + required this.fetchWithLoading, + required this.addMoreData, + }); +} diff --git a/lib/src/http_cache_paged/http_cache_paged_builder_data.dart b/lib/src/http_cache_paged/http_cache_paged_builder_data.dart new file mode 100644 index 0000000..9153d40 --- /dev/null +++ b/lib/src/http_cache_paged/http_cache_paged_builder_data.dart @@ -0,0 +1,24 @@ +import 'package:http_cache_flutter/src/http_cache_paged/http_cache_paged_actions.dart'; +import 'package:http_cache_flutter/src/http_cache_paged/http_responses.dart'; + +class HttpCachePagedBuilderData { + HttpCachePagedBuilderData( + {required this.responses, + required this.isLoading, + required this.isError, + required this.error, + required this.actions, + required this.isLoadingMoreData}); + + final List responses; + + final bool isLoading; + + final bool isError; + + final Object? error; + + final bool isLoadingMoreData; + + final HttpCachePagedActions actions; +} diff --git a/lib/src/http_cache_paged/http_responses.dart b/lib/src/http_cache_paged/http_responses.dart new file mode 100644 index 0000000..3479689 --- /dev/null +++ b/lib/src/http_cache_paged/http_responses.dart @@ -0,0 +1,76 @@ +import 'package:flutter/foundation.dart'; + +class HttpResponsePaged { + final int staleAt; + final String key; + final List items; + HttpResponsePaged({ + required this.staleAt, + required this.key, + required this.items, + }); + + HttpResponsePaged copyWith({ + int? staleAt, + String? key, + List? items, + }) { + return HttpResponsePaged( + staleAt: staleAt ?? this.staleAt, + key: key ?? this.key, + items: items ?? this.items, + ); + } + + Map toMap() => { + "staleAt": staleAt, + "key": key, + "items": items.map((e) => e.toMap()).toList(), + }; + + factory HttpResponsePaged.fromMap(Map map) { + return HttpResponsePaged( + staleAt: map['staleAt'] as int, + key: map['key'] as String, + items: List.from( + (map['items'] as List).map( + (x) => HttpResponsePagedItem.fromMap(x as Map), + ), + ), + ); + } +} + +class HttpResponsePagedItem { + final String body; + final int statusCode; + final Uint8List? bodyBytes; + final Map? headers; + final String url; + + HttpResponsePagedItem({ + required this.body, + required this.statusCode, + this.bodyBytes, + this.headers, + required this.url, + }); + + factory HttpResponsePagedItem.fromMap(Map map) { + return HttpResponsePagedItem( + body: map['body'] as String, + statusCode: map['statusCode'] as int, + bodyBytes: map['bodyBytes'], + headers: map['headers'], + url: map['url'] as String, + ); + } + + Map toMap() => { + "body": body, + "statusCode": statusCode, + "bodyBytes": bodyBytes, + "headers": headers, + "url": url, + }; +}