Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Write Your First App with a BLoC
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