Last active
June 9, 2021 08:57
-
-
Save tolo/31cc61c2510f06b30057e767acb99780 to your computer and use it in GitHub Desktop.
Flutter Labinar DemoApp (complete)
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 'package:flutter/material.dart'; | |
import 'dart:async'; | |
import 'dart:convert'; | |
// For DartPad: | |
import 'dart:html' as http; // Cannot use https://pub.dev/packages/http in DartPad... | |
// For mobile (and web): | |
//import 'package:http/http.dart' as http; | |
void main() { | |
runApp(DemoApp()); | |
} | |
/// APPLICATION | |
class DemoApp extends StatefulWidget { | |
@override | |
DemoAppState createState() => DemoAppState(); | |
} | |
class DemoAppState extends State<DemoApp> { | |
late DemoRepository demoApi; | |
late DemoUseCase demoUseCase; | |
@override | |
void initState() { | |
// In a real world app, we would use something like GetIt to setup below, before initializing the main App class: | |
demoApi = DemoApiMocked(); | |
//demoApi = DemoApi(); // Uncomment to use http calls instead of mocked data | |
demoUseCase = DemoUseCase(demoApi); | |
super.initState(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
final app = MaterialApp( | |
title: 'DemoApp', | |
theme: ThemeData( | |
primarySwatch: Colors.blue, | |
), | |
// Setup routing, handling page not found | |
onGenerateRoute: (routeSettings) { | |
if (routeSettings.name == '/') return MaterialPageRoute(builder: (_) => LandingPage()); | |
else if (routeSettings.name == '/list') return MaterialPageRoute(builder: (_) => ListPage()); | |
else return MaterialPageRoute(builder: (_) => Four04()); | |
}, | |
); | |
return SharedStateWidget(state: this, child: app); | |
} | |
static DemoAppState? of(BuildContext context) { | |
return context.dependOnInheritedWidgetOfExactType<SharedStateWidget>()?.state; | |
} | |
} | |
// Inherited widget for shared application "state" | |
class SharedStateWidget extends InheritedWidget { | |
final DemoAppState state; | |
const SharedStateWidget({Key? key, required this.state, required Widget child}) : super(key: key, child: child); | |
@override | |
bool updateShouldNotify(SharedStateWidget oldWidget) => true; | |
} | |
/// PRESENTATION | |
class LandingPage extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
void login(BuildContext context) => Navigator.of(context).pushNamed('/list'); | |
return Scaffold( | |
backgroundColor: Colors.grey[200], | |
body: Center( | |
child: SafeArea( | |
child: Column( children: [ | |
Container(height: MediaQuery.of(context).size.height * 0.33, | |
margin: EdgeInsets.all(20), | |
decoration: BoxDecoration( | |
gradient: LinearGradient( | |
begin: Alignment.topRight, | |
end: Alignment.bottomLeft, | |
colors: [Colors.blue, Colors.red], | |
), | |
borderRadius: BorderRadius.circular(8), | |
),), | |
Text('Welcome', style: Theme.of(context).textTheme.headline3,), | |
SizedBox(height: 40), | |
CustomIconButton(icon: Icons.login, title: 'Login', onPressed: () => login(context)), | |
]), | |
), | |
), | |
); | |
} | |
} | |
class ListPage extends StatefulWidget { | |
ListPage({Key? key}) : super(key: key); | |
@override | |
_ListPageState createState() => _ListPageState(); | |
} | |
class _ListPageState extends State<ListPage> { | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar(title: Text('List page')), | |
body: _body(), | |
); | |
} | |
Widget _body() { | |
// Get the shared "state", containing references to other parts/layers of the application | |
final sharedState = DemoAppState.of(context); | |
if (sharedState != null) { | |
// Get a reference to the "use case" (clean arch nomenclature), to be able to execute business logic | |
final data = sharedState.demoUseCase.getDemoData(); | |
// Using a future builder to "consume" a Future, handling loading/waiting, error and value states | |
return FutureBuilder(future: data, builder: (context, snapshot) { | |
final data = snapshot.data as List<DemoModel>?; | |
if (snapshot.error != null) { | |
return Text('ERROR!').centered(); | |
} else if (data != null) { | |
return _list(data); | |
} else { | |
return CircularProgressIndicator().paddedAll(16); | |
} | |
}); | |
} else { | |
return Text('ERROR!').centered(); | |
} | |
} | |
Widget _list(List<DemoModel> data) { | |
return ListView.builder( | |
itemBuilder: (context, index) => _listRow(context, data[index], index), | |
itemCount: data.length, | |
); | |
} | |
Widget _listRow(BuildContext context, DemoModel data, int index) { | |
return Column(mainAxisSize: MainAxisSize.min, children: <Widget>[ | |
ListTile( | |
contentPadding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 10.0), | |
leading: Icon(Icons.favorite, color: Colors.purple), | |
title: Text('Row #${data.id}'), | |
subtitle: Text(data.title), | |
trailing: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ | |
Icon(Icons.keyboard_arrow_right, color: Colors.grey[600], size: 30.0), | |
]), | |
onTap: () { | |
// Navigate to "detail" page (which currently doesn't exist) on tap | |
Navigator.of(context).pushNamed('/detail'); | |
}), | |
Divider( | |
color: Color.fromARGB(255, 215, 212, 207), | |
height: 1, | |
) | |
]); | |
} | |
} | |
class Four04 extends StatelessWidget { | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar(title: Text('Not found...'),), | |
body: Center( | |
child: | |
Column(mainAxisAlignment: MainAxisAlignment.center, children: [ | |
Icon(Icons.warning, size: 96, color: Colors.orange), | |
Text('404', style: Theme.of(context).textTheme.headline1), | |
Text('Page not found...'), | |
Text('😬', style: TextStyle(fontSize: 72)), | |
]) | |
), | |
); | |
} | |
} | |
/// DOMAIN | |
// Use case | |
class DemoUseCase { | |
static const Duration memoryCacheTTL = const Duration(seconds: 10); | |
final DemoRepository demoRepository; | |
List<DemoModel>? _demoDataCache; | |
DateTime _lastModified = DateTime.now(); | |
DemoUseCase(this.demoRepository); | |
Future<List<DemoModel>> getDemoData() { | |
if (_demoDataCache != null && DateTime.now().difference(_lastModified) < memoryCacheTTL) { | |
return Future.value(_demoDataCache); | |
} else { | |
final completer = Completer<List<DemoModel>>(); | |
demoRepository.getSomeDomainObject() | |
.then((value) => completer.complete(value)) | |
.catchError((error) => completer.completeError(error)); | |
return completer.future; | |
} | |
} | |
} | |
// Model | |
class DemoModel { | |
final int id; | |
final String title; | |
DemoModel(this.id, this.title); | |
} | |
// Contract | |
abstract class DemoRepository { | |
Future<List<DemoModel>> getSomeDomainObject(); | |
} | |
/// API | |
class DemoApi implements DemoRepository { | |
// When using DartPad | |
Future<String> _executeHttpGet(String url) { | |
return http.HttpRequest.getString(url); | |
} | |
// When building (locally) for mobile/web | |
// Future<String> _executeHttpGet(String url) { | |
// return http.get(Uri.parse(url)).then((response) => response.body); | |
// } | |
Future<List<DemoModel>> getSomeDomainObject() async { | |
// Make call using package http: | |
final data = await _executeHttpGet('https://jsonplaceholder.typicode.com/todos'); | |
// Make call using dart:html package (only when using DartPad): | |
//final data = await _executeHttpGetDartPad('https://jsonplaceholder.typicode.com/todos'); | |
final jsonList = jsonDecode(data) as List<dynamic>; | |
final result = jsonList.map((e) => DemoModelMapper.fromJson((e as Map<String, dynamic>))); | |
return List.of(result); | |
} | |
} | |
class DemoApiMocked implements DemoRepository { | |
Future<List<DemoModel>> getSomeDomainObject() async { | |
return Future.delayed(Duration(seconds: 2), () { | |
return [DemoModel(1, 'Hello'), DemoModel(2, 'World')]; | |
}); | |
} | |
} | |
class DemoModelMapper { | |
static DemoModel fromJson(Map<String, dynamic> json) { | |
return DemoModel(json['id'], json['title']); | |
} | |
} | |
/// WIDGET UTILS | |
class CustomIconButton extends StatelessWidget { | |
final IconData icon; | |
final String title; | |
final VoidCallback onPressed; | |
const CustomIconButton({Key? key, required this.icon, required this.title, required this.onPressed}) : super(key: key); | |
@override | |
Widget build(BuildContext context) { | |
final buttonContent = Container(height: 44, child: | |
Row(children: [ | |
Icon(icon, size: 24), | |
SizedBox(width: 8), | |
Text(title) , | |
]), | |
); | |
final button = ElevatedButton(onPressed: onPressed, child: buttonContent); | |
return Row(mainAxisSize: MainAxisSize.max, children: [ | |
SizedBox(width: 32), | |
Expanded(child: button), | |
SizedBox(width: 32), | |
]); | |
} | |
} | |
extension WidgetExtensions on Widget { | |
Widget inSafeArea() { | |
return SafeArea(child: this); | |
} | |
Align aligned({Alignment alignment = Alignment.center, double? widthFactor, double? heightFactor}) { | |
return Align(alignment: alignment, widthFactor: widthFactor, heightFactor: heightFactor, child: this); | |
} | |
Center centered({double? widthFactor, double? heightFactor}) => Center(child: this, widthFactor: widthFactor, heightFactor: heightFactor); | |
Padding padded(EdgeInsetsGeometry padding) { | |
return Padding(padding: padding, child: this); | |
} | |
Padding paddedFromLTRB(double left, double top, double right, double bottom) { | |
return Padding(padding: EdgeInsets.fromLTRB(left, top, right, bottom), child: this); | |
} | |
Padding paddedAll(double padding) { | |
return Padding(padding: EdgeInsets.all(padding), child: this); | |
} | |
Padding paddedOnly({double left = 0.0, double top = 0.0, double right = 0.0, double bottom = 0.0}) { | |
return Padding(padding: EdgeInsets.only(left: left, top: top, right: right, bottom: bottom), child: this); | |
} | |
FittedBox scaledToFit({BoxFit fit = BoxFit.scaleDown}) { | |
return FittedBox(fit: fit, child: this); | |
} | |
Expanded expanded({Key? key, int flex = 1}) { | |
return Expanded(key: key, flex: flex, child: this); | |
} | |
Flexible flexible({flex = 1, fit = FlexFit.loose}) { | |
return Flexible(flex: flex, fit: fit, child: this); | |
} | |
Widget opacity(double opacity) { | |
if (opacity == 1) | |
return this; | |
else | |
return Opacity(opacity: opacity, child: this); | |
} | |
} | |
extension NavigatorStateExtensions on NavigatorState { | |
void popToRoot() { | |
popUntil((route) => route.settings.name == '/'); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment