Last active
February 9, 2022 16:26
-
-
Save aloisdeniel/e148980534dae261ef1174a15f7ac5cf to your computer and use it in GitHub Desktop.
Flutter - Separation of concern - #3
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:convert'; | |
import 'package:flutter/material.dart'; | |
import 'package:provider/provider.dart'; | |
import 'package:http/http.dart'; | |
// main.dart | |
void main() { | |
runApp(const MyApp()); | |
} | |
class MyApp extends StatelessWidget { | |
const MyApp({Key? key}) : super(key: key); | |
@override | |
Widget build(BuildContext context) { | |
return Provider<DataUsaApiClient>( | |
create: (context) => const DataUsaApiClient(), | |
child: const MaterialApp( | |
home: HomePage(), | |
), | |
); | |
} | |
} | |
// home.dart | |
class HomePage extends StatelessWidget { | |
const HomePage({ | |
Key? key, | |
}) : super(key: key); | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar( | |
title: const Text('Years'), | |
), | |
body: ListView( | |
children: [ | |
ListTile( | |
title: const Text('Open demo'), | |
onTap: () { | |
const demoMeasure = Measure( | |
year: 2022, | |
population: 425484, | |
nation: 'United States', | |
); | |
Navigator.push( | |
context, | |
MaterialPageRoute( | |
settings: RouteSettings(arguments: demoMeasure.year), | |
builder: (context) { | |
return Provider<DataUsaApiClient>( | |
create: (context) => | |
const DemoDataUsaApiClient(demoMeasure), | |
child: const DetailScreen(), | |
); | |
}, | |
), | |
); | |
}, | |
), | |
for (var i = DateTime.now().year; i > 2000; i--) | |
ListTile( | |
title: Text('$i'), | |
onTap: () { | |
Navigator.push( | |
context, | |
MaterialPageRoute( | |
settings: RouteSettings(arguments: i), | |
builder: (context) { | |
return const DetailScreen(); | |
}, | |
), | |
); | |
}, | |
) | |
], | |
), | |
); | |
} | |
} | |
// api.dart | |
class DataUsaApiClient { | |
const DataUsaApiClient({ | |
this.endpoint = 'https://datausa.io/api/data', | |
}); | |
final String endpoint; | |
Future<Measure?> getMeasure(int year) async { | |
final uri = | |
Uri.parse('$endpoint?drilldowns=Nation&measures=Population&year=$year'); | |
final result = await get(uri); | |
final body = jsonDecode(result.body); | |
final data = body['data'] as List<dynamic>; | |
if (data.isNotEmpty) { | |
return Measure.fromJson(data.first as Map<String, Object?>); | |
} | |
return null; | |
} | |
} | |
class Measure { | |
const Measure({ | |
required this.year, | |
required this.population, | |
required this.nation, | |
}); | |
factory Measure.fromJson(Map<String, Object?> json) { | |
return Measure( | |
nation: json['Nation'] as String, | |
population: (json['Population'] as num).toInt(), | |
year: (json['ID Year'] as num).toInt(), | |
); | |
} | |
final int year; | |
final int population; | |
final String nation; | |
@override | |
bool operator ==(Object other) => | |
identical(this, other) || | |
(other is Measure && | |
year == other.year && | |
population == other.population && | |
nation == other.nation); | |
@override | |
int get hashCode => Object.hash(year, population, nation); | |
} | |
class DemoDataUsaApiClient implements DataUsaApiClient { | |
const DemoDataUsaApiClient(this.measure); | |
final Measure measure; | |
@override | |
String get endpoint => ''; | |
@override | |
Future<Measure?> getMeasure(int year) { | |
return Future.value(measure); | |
} | |
} | |
// state.dart | |
abstract class DetailState { | |
const DetailState(this.year); | |
final int year; | |
const factory DetailState.notLoaded(int year) = NotLoadedDetailState; | |
const factory DetailState.loading(int year) = LoadingDetailState; | |
const factory DetailState.noData(int year) = NoDataDetailState; | |
const factory DetailState.loaded({ | |
required int year, | |
required Measure measure, | |
}) = LoadedDetailState; | |
const factory DetailState.unknownError({ | |
required int year, | |
required dynamic error, | |
}) = UnknownErrorDetailState; | |
@override | |
bool operator ==(Object other) => | |
identical(this, other) || | |
(other is DetailState && | |
runtimeType == other.runtimeType && | |
year == other.year); | |
@override | |
int get hashCode => runtimeType.hashCode ^ year; | |
} | |
class NotLoadedDetailState extends DetailState { | |
const NotLoadedDetailState(int year) : super(year); | |
} | |
class LoadedDetailState extends DetailState { | |
const LoadedDetailState({ | |
required int year, | |
required this.measure, | |
}) : super(year); | |
final Measure measure; | |
@override | |
bool operator ==(Object other) => | |
identical(this, other) || | |
(other is LoadedDetailState && measure == other.measure); | |
@override | |
int get hashCode => runtimeType.hashCode ^ measure.hashCode; | |
} | |
class NoDataDetailState extends DetailState { | |
const NoDataDetailState(int year) : super(year); | |
} | |
class LoadingDetailState extends DetailState { | |
const LoadingDetailState(int year) : super(year); | |
} | |
class UnknownErrorDetailState extends DetailState { | |
const UnknownErrorDetailState({ | |
required int year, | |
required this.error, | |
}) : super(year); | |
final dynamic error; | |
@override | |
bool operator ==(Object other) => | |
identical(this, other) || | |
(other is UnknownErrorDetailState && | |
year == other.year && | |
error == other.error); | |
@override | |
int get hashCode => Object.hash(super.hashCode, error.hashCode); | |
} | |
// notifier.dart | |
class DetailNotifier extends ValueNotifier<DetailState> { | |
DetailNotifier({ | |
required int year, | |
required this.api, | |
}) : super(DetailState.notLoaded(year)); | |
final DataUsaApiClient api; | |
int get year => value.year; | |
Future<void> refresh() async { | |
if (value is! LoadingDetailState) { | |
value = DetailState.loading(year); | |
try { | |
final result = await api.getMeasure(year); | |
if (result != null) { | |
value = DetailState.loaded( | |
year: year, | |
measure: result, | |
); | |
} else { | |
value = DetailState.noData(year); | |
} | |
} catch (error) { | |
value = DetailState.unknownError( | |
year: year, | |
error: error, | |
); | |
} | |
} | |
} | |
} | |
// detail.dart | |
class DetailScreen extends StatelessWidget { | |
const DetailScreen({ | |
Key? key, | |
}) : super(key: key); | |
@override | |
Widget build(BuildContext context) { | |
final year = ModalRoute.of(context)!.settings.arguments as int; | |
return ChangeNotifierProvider<DetailNotifier>( | |
create: (context) { | |
final notifier = DetailNotifier( | |
year: year, | |
api: context.read<DataUsaApiClient>(), | |
); | |
notifier.refresh(); | |
return notifier; | |
}, | |
child: Consumer<DetailNotifier>( | |
builder: (context, notifier, child) { | |
final state = notifier.value; | |
return Scaffold( | |
appBar: AppBar( | |
title: Text('Year ${state.year}'), | |
), | |
body: () { | |
if (state is NotLoadedDetailState || | |
state is LoadingDetailState) { | |
return const Center( | |
child: CircularProgressIndicator(), | |
); | |
} | |
if (state is LoadedDetailState) { | |
final theme = Theme.of(context); | |
return Center( | |
child: Column( | |
mainAxisAlignment: MainAxisAlignment.center, | |
crossAxisAlignment: CrossAxisAlignment.center, | |
children: [ | |
Text( | |
state.measure.nation, | |
style: theme.textTheme.headline5, | |
), | |
Row( | |
mainAxisSize: MainAxisSize.min, | |
children: [ | |
Text( | |
'${state.measure.population}', | |
style: theme.textTheme.headline4, | |
), | |
Icon( | |
Icons.people, | |
color: theme.textTheme.headline4?.color, | |
size: theme.textTheme.headline4?.fontSize, | |
), | |
], | |
), | |
], | |
), | |
); | |
; | |
} | |
if (state is UnknownErrorDetailState) { | |
return Center( | |
child: Text('Failed : ${state.error}'), | |
); | |
} | |
return const Center( | |
child: Text('No data'), | |
); | |
}(), | |
); | |
}, | |
), | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment