Last active
December 3, 2020 03:18
-
-
Save Andrious/20ddc70d7ba22966da17a01af5ea63e0 to your computer and use it in GitHub Desktop.
Write Your First App with a BLoC
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:io' show Platform; | |
import 'package:flutter/material.dart'; | |
import 'package:flutter/cupertino.dart' | |
show | |
CupertinoApp, | |
CupertinoButton, | |
CupertinoColors, | |
CupertinoPageRoute, | |
CupertinoPageScaffold, | |
CupertinoSliverNavigationBar, | |
CustomScrollView; | |
import 'package:english_words/english_words.dart' | |
show WordPair, generateWordPairs; | |
import 'bloc.dart'; | |
void main() => runApp(MyApp()); | |
class MyApp extends StatefulWidget { | |
MyApp({Key key}) : super(key: key); | |
@override | |
State createState() => _MyAppState(); | |
} | |
class _MyAppState extends StateBloc<MyApp> { | |
_MyAppState() { | |
appBloc = _AppEventHandler(this); | |
} | |
_AppEventHandler appBloc; | |
@override | |
Widget build(BuildContext context) { | |
// A new key will re-create the State object. | |
final Widget home = _RandomWords( | |
key: UniqueKey(), | |
); | |
Widget appUI; | |
if (appBloc.useCupertino) { | |
appUI = CupertinoApp( | |
home: home, | |
); | |
} else { | |
appUI = MaterialApp( | |
home: home, | |
); | |
} | |
return appUI; | |
} | |
} | |
/// Logic to switch between UI platform | |
class _AppEventHandler extends Bloc { | |
factory _AppEventHandler([StateBloc state]) => | |
_this ??= _AppEventHandler._(state); | |
_AppEventHandler._(StateBloc state) : super(state) { | |
addState(state); | |
// Determine which platform we're running in. | |
useMaterial = Platform.isAndroid; | |
} | |
static _AppEventHandler _this; | |
_MyAppState appState; | |
@override | |
bool addState(StateBloc state) { | |
appState ??= state; | |
return super.addState(state); | |
} | |
static bool _useMaterial = false; | |
static bool _useCupertino = false; | |
set useMaterial(bool use) { | |
if (use == null) { | |
return; | |
} | |
if (use) { | |
_useMaterial = true; | |
_useCupertino = false; | |
} else { | |
_useMaterial = false; | |
_useCupertino = true; | |
} | |
} | |
// Use Material UI when explicitly specified or even when running in iOS | |
bool get useMaterial => _useMaterial; | |
set useCupertino(bool use) { | |
if (use == null) { | |
return; | |
} | |
if (use) { | |
_useCupertino = true; | |
_useMaterial = false; | |
} else { | |
_useCupertino = false; | |
_useMaterial = true; | |
} | |
} | |
// Use Cupertino UI when explicitly specified or even when running in Android | |
bool get useCupertino => _useCupertino; | |
// Assign to the 'leading' widget on the interface. | |
void leading() => switchUI(); | |
// Switch the app to the other UI platform. | |
void switchUI() { | |
if (appState == null) { | |
return; | |
} | |
final bool use = useMaterial; | |
useMaterial = !use; | |
// Call the AppState's setState(); | |
appState.refresh(); | |
} | |
} | |
class _RandomWords extends StatefulWidget { | |
_RandomWords({Key key}) | |
: appBloc = _AppEventHandler(), | |
super(key: key); | |
final appBloc; | |
final String title = 'Startup Name Generator'; | |
@override | |
State createState() => | |
appBloc.useMaterial ? _RandomWordsAndroid() : _RandomWordsiOS(); | |
} | |
class _RandomWordsAndroid extends _RandomWordsState { | |
@override | |
Widget build(BuildContext context) => Scaffold( | |
appBar: AppBar( | |
title: Text(widget.title), | |
leading: IconButton( | |
icon: const Icon(Icons.switch_right_sharp), | |
onPressed: handler.leading), | |
actions: [ | |
IconButton( | |
icon: const Icon(Icons.list), | |
onPressed: _pushMaterialSaved, | |
), | |
], | |
), | |
body: ListView.builder( | |
padding: const EdgeInsets.all(16), | |
itemBuilder: (context, i) { | |
if (i.isOdd) { | |
return const Divider(); | |
} | |
bloc.build(i); | |
return ListTile( | |
title: Text( | |
bloc.data, | |
style: const TextStyle(fontSize: 25), | |
), | |
trailing: bloc.trailing, | |
onTap: () { | |
bloc.onTap(i); | |
}, | |
); | |
}), | |
); | |
/// push with the MaterialPageRoute | |
void _pushMaterialSaved() { | |
Navigator.of(context).push( | |
MaterialPageRoute<void>( | |
builder: (BuildContext context) { | |
final tiles = bloc.tiles(); | |
final divided = ListTile.divideTiles( | |
context: context, | |
tiles: tiles, | |
).toList(); | |
return Scaffold( | |
appBar: AppBar( | |
title: const Text('Saved Suggestions'), | |
), | |
body: ListView(children: divided), | |
); | |
}, | |
), | |
); | |
} | |
} | |
class _RandomWordsiOS extends _RandomWordsState { | |
@override | |
Widget build(BuildContext context) => CupertinoPageScaffold( | |
child: CustomScrollView( | |
slivers: <Widget>[ | |
CupertinoSliverNavigationBar( | |
largeTitle: Text(widget.title), | |
leading: CupertinoButton( | |
onPressed: handler.leading, | |
child: const Icon(Icons.switch_left_sharp), | |
), | |
trailing: CupertinoButton( | |
onPressed: _pushCupertinoSaved, | |
child: const Icon(Icons.list), | |
), | |
), | |
SliverSafeArea( | |
top: false, | |
minimum: const EdgeInsets.only(top: 8), | |
sliver: SliverList( | |
delegate: SliverChildBuilderDelegate( | |
(context, i) { | |
if (i.isOdd) { | |
return const Divider(); | |
} | |
bloc.build(i); | |
return CupertinoListTile( | |
title: bloc.title, | |
trailing: bloc.trailing, | |
onTap: () { | |
bloc.onTap(i); | |
}, | |
); | |
}, | |
), | |
), | |
) | |
], | |
), | |
); | |
/// push with the CupertinoPageRoute | |
void _pushCupertinoSaved() { | |
Navigator.of(context).push( | |
CupertinoPageRoute<void>( | |
builder: (BuildContext context) { | |
final Iterable<Widget> tiles = bloc.tiles(); | |
final Iterator<Widget> it = tiles.iterator; | |
it.moveNext(); | |
return CupertinoPageScaffold( | |
child: CustomScrollView( | |
slivers: <Widget>[ | |
const CupertinoSliverNavigationBar( | |
largeTitle: Text('Saved Suggestions'), | |
), | |
SliverSafeArea( | |
top: false, | |
minimum: const EdgeInsets.only(top: 8), | |
sliver: SliverList( | |
delegate: SliverChildBuilderDelegate( | |
(context, i) { | |
final tile = it.current; | |
it.moveNext(); | |
return tile; | |
}, | |
childCount: tiles.length, | |
), | |
), | |
) | |
], | |
), | |
); | |
}, | |
), | |
); | |
} | |
} | |
/// The level of abstraction to contain introduce the logic involved. | |
/// subclasses will likely not exist at the same time | |
/// The build function must be implemented anyway, but with a | |
/// completely different interface design. | |
abstract class _RandomWordsState extends StateBloc<_RandomWords> { | |
_RandomWordsState() { | |
bloc = _DataBloc(this); | |
handler = _AppEventHandler(); | |
} | |
_DataBloc bloc; | |
_AppEventHandler handler; | |
@override | |
void initState() { | |
super.initState(); | |
// Register the 'logic' object | |
addBloc(bloc); | |
} | |
@override | |
Widget build(BuildContext context); | |
} | |
/// Logic that deals with the WordPairs | |
class _DataBloc extends Bloc { | |
_DataBloc(this.state) | |
: appBloc = _AppEventHandler(), | |
super(state); | |
StateBloc state; | |
final _AppEventHandler appBloc; | |
static final suggestions = <WordPair>[]; | |
static final Set<WordPair> saved = <WordPair>{}; | |
int index; | |
void build(int i) { | |
index = i ~/ 2; | |
if (index >= suggestions.length) { | |
suggestions.addAll(generateWordPairs().take(10)); | |
} | |
} | |
String get title => data; | |
String get data => current.asPascalCase; | |
Widget get trailing => icon; | |
WordPair get current => suggestions[index]; | |
Icon get icon { | |
bool alreadySaved = saved.contains(suggestions[index]); | |
return Icon( | |
alreadySaved ? Icons.favorite : Icons.favorite_border, | |
color: alreadySaved ? Colors.red : null, | |
); | |
} | |
// Call the RandomWord's setState(); | |
void onTap(int i) => state.setState(() { | |
final int index = i ~/ 2; | |
final WordPair pair = suggestions[index]; | |
if (pair == null) return; | |
if (saved.contains(suggestions[index])) { | |
saved.remove(pair); | |
} else { | |
saved.add(pair); | |
} | |
}); | |
/// Responsible for either UI platform. | |
Iterable<Widget> tiles({TextStyle style = const TextStyle(fontSize: 25)}) => | |
saved.map( | |
(WordPair pair) { | |
Widget widget; | |
if (!appBloc.useMaterial) { | |
widget = CupertinoListTile(title: pair.asPascalCase); | |
} else { | |
widget = ListTile( | |
title: Text( | |
pair.asPascalCase, | |
style: style, | |
), | |
); | |
} | |
return widget; | |
}, | |
); | |
} | |
/// Cupertino needs a ListTile equivalent | |
/// https://github.com/flutter/flutter/issues/50668 | |
/// c/o Asandei Stefan | |
class CupertinoListTile extends StatefulWidget { | |
const CupertinoListTile({ | |
Key key, | |
this.leading, | |
this.title, | |
this.subtitle, | |
this.trailing, | |
this.onTap, | |
}) : super(key: key); | |
final Widget leading; | |
final String title; | |
final String subtitle; | |
final Widget trailing; | |
final Function onTap; | |
@override | |
_StatefulStateCupertino createState() => _StatefulStateCupertino(); | |
} | |
class _StatefulStateCupertino extends State<CupertinoListTile> { | |
@override | |
Widget build(BuildContext context) { | |
Widget leading; | |
if (widget.leading == null) { | |
leading = const SizedBox(); | |
} else { | |
leading = widget.leading; | |
} | |
Widget trailing; | |
if (widget.trailing == null) { | |
trailing = const SizedBox(); | |
} else { | |
trailing = widget.trailing; | |
} | |
return GestureDetector( | |
onTap: () { | |
if (widget.onTap != null) { | |
widget.onTap(); | |
} | |
}, | |
child: Row( | |
mainAxisAlignment: MainAxisAlignment.spaceBetween, | |
children: <Widget>[ | |
Row( | |
children: <Widget>[ | |
leading, | |
const SizedBox(width: 10), | |
Column( | |
crossAxisAlignment: CrossAxisAlignment.start, | |
children: columnChildren(context), | |
), | |
], | |
), | |
trailing, | |
], | |
), | |
); | |
} | |
List<Widget> columnChildren(BuildContext context) { | |
final List<Widget> children = []; | |
final bool isDark = | |
MediaQuery.of(context).platformBrightness == Brightness.dark; | |
final Widget title = widget.title != null | |
? Text( | |
widget.title, | |
style: TextStyle( | |
fontSize: 25, color: isDark ? Colors.white : Colors.black), | |
) | |
: const SizedBox(); | |
children.add(title); | |
if (widget.subtitle != null) { | |
children.add(Text(widget.subtitle, | |
style: const TextStyle(color: CupertinoColors.systemGrey))); | |
} | |
return children; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment