Skip to content

Instantly share code, notes, and snippets.

@tolo
Last active June 9, 2021 08:57
Show Gist options
  • Save tolo/31cc61c2510f06b30057e767acb99780 to your computer and use it in GitHub Desktop.
Save tolo/31cc61c2510f06b30057e767acb99780 to your computer and use it in GitHub Desktop.
Flutter Labinar DemoApp (complete)
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