Skip to content

Commit

Permalink
Cleanup dependencies into separate classes.
Browse files Browse the repository at this point in the history
  • Loading branch information
dam5s committed Jul 31, 2023
1 parent 1c8a8f8 commit 3e01f64
Show file tree
Hide file tree
Showing 25 changed files with 288 additions and 216 deletions.
37 changes: 10 additions & 27 deletions lib/app_dependencies.dart
Original file line number Diff line number Diff line change
@@ -1,35 +1,18 @@
import 'dart:async';

import 'package:flutter/foundation.dart' as foundation;
import 'package:flutter/widgets.dart';
import 'package:flutter_starter/prelude/async_compute.dart';
import 'package:flutter_starter/prelude/http.dart' as http;
import 'package:flutter/widgets.dart' as widgets;
import 'package:flutter_starter/networking/async_compute.dart';
import 'package:flutter_starter/networking/http_client_provider.dart';
import 'package:flutter_starter/prelude/time_source.dart';
import 'package:http/http.dart';
import 'package:provider/provider.dart';

abstract class AppDependencies implements http.HttpClientProvider, AsyncCompute, TimeSource {}

final class DefaultAppDependencies implements AppDependencies {
@override
T withHttpClient<T>(T Function(Client) block) {
final client = Client();
try {
return block(client);
} finally {
client.close();
}
}

@override
Future<R> compute<M, R>(FutureOr<R> Function(M) callback, M message, {String? debugLabel}) {
return foundation.compute<M, R>(callback, message, debugLabel: debugLabel);
}
final class AppDependencies {
final HttpClientProvider httpClientProvider;
final AsyncCompute asyncCompute;
final TimeSource timeSource;

@override
DateTime now() => DateTime.now();
AppDependencies(
{required this.httpClientProvider, required this.asyncCompute, required this.timeSource});
}

extension AppDependenciesGetter on BuildContext {
extension AppDependenciesGetter on widgets.BuildContext {
AppDependencies appDependencies() => Provider.of<AppDependencies>(this, listen: false);
}
5 changes: 4 additions & 1 deletion lib/app_pages/app_pages.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ final class _AppPagesState extends State<AppPages> {
final appDependencies = context.appDependencies();

_locations = [_boulder];
_forecastsRepo = ForecastsRepository(appDependencies);
_forecastsRepo = ForecastsRepository(
appDependencies.httpClientProvider,
appDependencies.asyncCompute,
);
}

@override
Expand Down
17 changes: 11 additions & 6 deletions lib/forecast/forecast_api.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import 'package:flutter_starter/app_dependencies.dart';
import 'package:flutter_starter/location_search/location_search_api.dart';
import 'package:flutter_starter/prelude/http.dart';
import 'package:flutter_starter/prelude/json_decoder.dart';
import 'package:flutter_starter/networking/async_compute.dart';
import 'package:flutter_starter/networking/http.dart';
import 'package:flutter_starter/networking/http_client_provider.dart';
import 'package:flutter_starter/networking/json_decoder.dart';

final class ApiForecast {
final ApiHourlyForecast hourly;
Expand Down Expand Up @@ -32,15 +33,19 @@ final class ApiHourlyForecast {

const forecastApiUrl = 'https://api.open-meteo.com/v1/forecast';

HttpFuture<ApiForecast> fetchForecast(AppDependencies dependencies, ApiLocation location) {
HttpFuture<ApiForecast> fetchForecast(
HttpClientProvider httpClientProvider,
AsyncCompute asyncCompute,
ApiLocation location,
) {
final lat = location.latitude;
final long = location.longitude;
final query = 'latitude=$lat&longitude=$long&hourly=temperature_2m&timezone=auto';
final url = Uri.parse('$forecastApiUrl?$query');

return dependencies.withHttpClient((client) async {
return httpClientProvider.withHttpClient((client) async {
final httpResult = await client.sendRequest(HttpMethod.get, url);

return httpResult.expectStatusCode(200).tryParseJson(dependencies, ApiForecast.fromJson);
return httpResult.expectStatusCode(200).tryParseJson(asyncCompute, ApiForecast.fromJson);
});
}
7 changes: 5 additions & 2 deletions lib/forecast/forecast_page.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_starter/app_dependencies.dart';
import 'package:flutter_starter/location_search/location_search_api.dart';
import 'package:flutter_starter/prelude/http.dart';
import 'package:flutter_starter/networking/http.dart';
import 'package:flutter_starter/widgets/card_header.dart';
import 'package:flutter_starter/widgets/http_future_builder.dart';

Expand All @@ -28,7 +28,10 @@ final class ForecastPage extends StatelessWidget {

Widget _loadedWidget(BuildContext context, ApiForecast forecastJson) {
final appDependencies = context.appDependencies();
final forecast = Forecast.present(appDependencies, forecastJson);
final forecast = Forecast.present(
appDependencies.timeSource,
forecastJson,
);

return Column(children: [
_hourlyListWidget(context, forecast),
Expand Down
16 changes: 11 additions & 5 deletions lib/forecast/forecasts_repository.dart
Original file line number Diff line number Diff line change
@@ -1,23 +1,29 @@
import 'dart:collection';

import 'package:flutter_starter/app_dependencies.dart';
import 'package:flutter_starter/forecast/forecast_api.dart';
import 'package:flutter_starter/location_search/location_search_api.dart';
import 'package:flutter_starter/prelude/http.dart';
import 'package:flutter_starter/networking/async_compute.dart';
import 'package:flutter_starter/networking/http.dart';
import 'package:flutter_starter/networking/http_client_provider.dart';

class ForecastsRepository {
final AppDependencies dependencies;
final HttpClientProvider httpClientProvider;
final AsyncCompute asyncCompute;
final Map<ApiLocation, HttpFuture<ApiForecast>> _cache = HashMap();

ForecastsRepository(this.dependencies);
ForecastsRepository(this.httpClientProvider, this.asyncCompute);

HttpFuture<ApiForecast> fetch(ApiLocation location) {
final cached = _cache[location];
if (cached != null) {
return cached;
}

final newValue = fetchForecast(dependencies, location);
final newValue = fetchForecast(
httpClientProvider,
asyncCompute,
location,
);
_cache[location] = newValue;
return newValue;
}
Expand Down
17 changes: 11 additions & 6 deletions lib/location_search/location_search_api.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:flutter_starter/app_dependencies.dart';
import 'package:flutter_starter/prelude/http.dart';
import 'package:flutter_starter/prelude/json_decoder.dart';
import 'package:flutter_starter/networking/async_compute.dart';
import 'package:flutter_starter/networking/http.dart';
import 'package:flutter_starter/networking/http_client_provider.dart';
import 'package:flutter_starter/networking/json_decoder.dart';

final class ApiLocation {
final String name;
Expand Down Expand Up @@ -38,15 +39,19 @@ final class ApiLocation {

const searchApiUrl = 'https://geocoding-api.open-meteo.com/v1/search';

HttpFuture<Iterable<ApiLocation>> searchLocation(AppDependencies dependencies, String name) async {
HttpFuture<Iterable<ApiLocation>> searchLocation(
HttpClientProvider httpClientProvider,
AsyncCompute asyncCompute,
String name,
) async {
final nameParam = Uri.encodeComponent(name);
final url = Uri.parse('$searchApiUrl?name=$nameParam&count=10&language=en&format=json');

return dependencies.withHttpClient((client) async {
return httpClientProvider.withHttpClient((client) async {
final httpResult = await client.sendRequest(HttpMethod.get, url);

return httpResult
.expectStatusCode(200)
.tryParseJson(dependencies, (json) => json.objectArray('results', ApiLocation.fromJson));
.tryParseJson(asyncCompute, (json) => json.objectArray('results', ApiLocation.fromJson));
});
}
9 changes: 7 additions & 2 deletions lib/location_search/location_search_page.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_starter/app_dependencies.dart';
import 'package:flutter_starter/prelude/http.dart';
import 'package:flutter_starter/networking/http.dart';
import 'package:flutter_starter/widgets/http_future_builder.dart';

import 'location_search_api.dart';
Expand Down Expand Up @@ -62,8 +62,13 @@ final class _LocationSearchPageState extends State<LocationSearchPage> {
}

void _startSearch(String value) {
final appDependencies = context.appDependencies();
setState(() {
_searchFuture = searchLocation(context.appDependencies(), value);
_searchFuture = searchLocation(
appDependencies.httpClientProvider,
appDependencies.asyncCompute,
value,
);
});
}

Expand Down
11 changes: 9 additions & 2 deletions lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import 'package:flutter/material.dart';
import 'package:flutter_starter/app_pages/app_pages.dart';
import 'package:flutter_starter/networking/async_compute.dart';
import 'package:flutter_starter/networking/http_client_provider.dart';
import 'package:flutter_starter/prelude/time_source.dart';
import 'package:provider/provider.dart';

import 'app_dependencies.dart';

void main() {
runApp(Provider<AppDependencies>(
create: (_) => DefaultAppDependencies(),
runApp(Provider(
create: (_) => AppDependencies(
httpClientProvider: ConcreteHttpClientProvider(),
asyncCompute: FoundationAsyncCompute(),
timeSource: SystemTimeSource(),
),
child: const App(),
));
}
Expand Down
14 changes: 14 additions & 0 deletions lib/networking/async_compute.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import 'dart:async';

import 'package:flutter/foundation.dart' as foundation;

abstract class AsyncCompute {
Future<R> compute<M, R>(FutureOr<R> Function(M) callback, M message, {String? debugLabel});
}

class FoundationAsyncCompute implements AsyncCompute {
@override
Future<R> compute<M, R>(FutureOr<R> Function(M) callback, M message, {String? debugLabel}) {
return foundation.compute<M, R>(callback, message, debugLabel: debugLabel);
}
}
70 changes: 70 additions & 0 deletions lib/networking/http.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import 'package:flutter_starter/prelude/result.dart';
import 'package:http/http.dart';
import 'package:logger/logger.dart';

import 'async_compute.dart';
import 'http_error.dart';
import 'json_decoder.dart';

enum HttpMethod {
get,
post,
put,
delete;

@override
String toString() => switch (this) {
get => 'GET',
post => 'POST',
put => 'PUT',
delete => 'DELETE',
};
}

typedef HttpResult<T> = Result<T, HttpError>;
typedef HttpFuture<T> = Future<HttpResult<T>>;

extension SendRequest on Client {
HttpFuture<Response> sendRequest(HttpMethod method, Uri url) async {
try {
final request = Request(method.toString(), url);
final streamedResponse = await send(request);
final response = await Response.fromStream(streamedResponse);

return Ok(response);
} on Exception catch (e) {
return Err(HttpConnectionError(e));
}
}
}

extension ResponseHandling on HttpResult<Response> {
HttpResult<Response> expectStatusCode(int expected) => flatMapOk((response) {
if (response.statusCode == expected) {
return Ok(response);
} else {
return Err(HttpUnexpectedStatusCodeError(expected, response.statusCode));
}
});

HttpFuture<T> tryParseJson<T>(AsyncCompute async, JsonDecode<T> decode) async {
return switch (this) {
Ok(value: final response) => async.compute(
(response) {
try {
final jsonObject = JsonDecoder.fromString(response.body);
final object = decode(jsonObject);
return Ok(object);
} on TypeError catch (e) {
_logger.e('Failed to parse json: ${response.body}', e);
return Err(HttpDeserializationError(e, response.body));
}
},
response,
),
Err(error: final error) => Future.value(Err(error)),
};
}
}

final _logger = Logger();
17 changes: 17 additions & 0 deletions lib/networking/http_client_provider.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import 'package:http/http.dart';

abstract class HttpClientProvider {
T withHttpClient<T>(T Function(Client client) block);
}

class ConcreteHttpClientProvider implements HttpClientProvider {
@override
T withHttpClient<T>(T Function(Client) block) {
final client = Client();
try {
return block(client);
} finally {
client.close();
}
}
}
29 changes: 29 additions & 0 deletions lib/networking/http_error.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
sealed class HttpError {}

final class HttpConnectionError implements HttpError {
const HttpConnectionError(this.exception);

final Exception exception;
}

final class HttpUnexpectedStatusCodeError implements HttpError {
const HttpUnexpectedStatusCodeError(this.expected, this.actual);

final int expected;
final int actual;
}

final class HttpDeserializationError implements HttpError {
const HttpDeserializationError(this.error, this.responseBody);

final TypeError error;
final String responseBody;
}

extension HttpErrorMessage on HttpError {
String message() => switch (this) {
HttpConnectionError() => 'There was an error connecting',
HttpUnexpectedStatusCodeError() => 'Unexpected response from api',
HttpDeserializationError() => 'Failed to parse response',
};
}
File renamed without changes.
5 changes: 0 additions & 5 deletions lib/prelude/async_compute.dart

This file was deleted.

Loading

0 comments on commit 3e01f64

Please sign in to comment.