Skip to content

feat/HttpCacheStoragePaged #8

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

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
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
1 change: 1 addition & 0 deletions .rest
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
GET https://dummyjson.com/products?limit=15&skip=1 HTTP/1.1
2 changes: 1 addition & 1 deletion doc/handle_request_example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ packages:
path: "../.."
relative: true
source: path
version: "0.0.2"
version: "0.1.0"
http_parser:
dependency: transitive
description:
Expand Down
8 changes: 8 additions & 0 deletions lib/http_cache_flutter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
6 changes: 5 additions & 1 deletion lib/src/http_cache.dart
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,12 @@ class HttpCache<T> extends StatefulWidget {
static Future<HttpCacheStorage> init(
{required Directory storageDirectory, HttpCacheChiper? chiper}) async {
HttpCacheStorage storage = await HttpCacheStorage.initialize(
storageDirectory: storageDirectory, chiper: chiper);
storageDirectory: storageDirectory,
boxName: "http_cache_storage",
chiper: chiper);

HttpCache.storage = storage;

return storage;
}

Expand Down
236 changes: 236 additions & 0 deletions lib/src/http_cache_paged/http_cache_paged.dart
Original file line number Diff line number Diff line change
@@ -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<T> extends StatefulWidget {
final String storageKey;

///the initial url
final String url;

///your initial header to make this package fetching data from http
final Map<String, String>? headers;

///your initial header to make this package fetching data from http
final Future<Map<String, String>>? futureHeaders;

///you can return your layout with this attribute.
final Widget Function(BuildContext context, HttpCachePagedBuilderData<T> 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<http.Response> Function(
String url, Map<String, String>? 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<HttpCachePaged<T>> createState() => _HttpCachePagedState<T>();

static Future<HttpCacheStorage> 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<T> extends State<HttpCachePaged<T>> {
late String _url;
Map<String, String>? _headers;
bool _isLoading = false;
bool _isLoadingMoreData = false;
late HttpResponsePaged pagedKey;

List<HttpResponsePagedItem> _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<void> _fetchWithLoading(
{String? url, Map<String, String>? headers}) async {
_setLoading(true);
await _fetch(url: url, headers: headers);
}

Future<void> _fetch({String? url, Map<String, String>? 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<http.Response> _handleRequest(
{String? url, Map<String, String>? 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<void> _addMoreData(String url, {Map<String, String>? 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<T>(
responses: _data,
isLoading: _isLoading,
isLoadingMoreData: _isLoadingMoreData,
isError: false,
error: null,
actions: HttpCachePagedActions(
fetch: _fetch,
fetchWithLoading: _fetchWithLoading,
addMoreData: _addMoreData),
),
);
}
}
16 changes: 16 additions & 0 deletions lib/src/http_cache_paged/http_cache_paged_actions.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
class HttpCachePagedActions {
final Future<void> Function({String? url, Map<String, String>? headers})
fetch;
final Future<void> Function({String? url, Map<String, String>? headers})
fetchWithLoading;

///handle get more data from a new http url
final Future<void> Function(String url, {Map<String, String>? headers})
addMoreData;

const HttpCachePagedActions({
required this.fetch,
required this.fetchWithLoading,
required this.addMoreData,
});
}
24 changes: 24 additions & 0 deletions lib/src/http_cache_paged/http_cache_paged_builder_data.dart
Original file line number Diff line number Diff line change
@@ -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<T> {
HttpCachePagedBuilderData(
{required this.responses,
required this.isLoading,
required this.isError,
required this.error,
required this.actions,
required this.isLoadingMoreData});

final List<HttpResponsePagedItem> responses;

final bool isLoading;

final bool isError;

final Object? error;

final bool isLoadingMoreData;

final HttpCachePagedActions actions;
}
76 changes: 76 additions & 0 deletions lib/src/http_cache_paged/http_responses.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import 'package:flutter/foundation.dart';

class HttpResponsePaged {
final int staleAt;
final String key;
final List<HttpResponsePagedItem> items;
HttpResponsePaged({
required this.staleAt,
required this.key,
required this.items,
});

HttpResponsePaged copyWith({
int? staleAt,
String? key,
List<HttpResponsePagedItem>? 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<HttpResponsePagedItem>.from(
(map['items'] as List<int>).map<HttpResponsePagedItem>(
(x) => HttpResponsePagedItem.fromMap(x as Map),
),
),
);
}
}

class HttpResponsePagedItem {
final String body;
final int statusCode;
final Uint8List? bodyBytes;
final Map<String, String>? 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,
};
}
Loading