Skip to content

Instantly share code, notes, and snippets.

@rodydavis
Created May 29, 2020 22:44
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rodydavis/5cd5f49eb0981133160f299564130d07 to your computer and use it in GitHub Desktop.
Save rodydavis/5cd5f49eb0981133160f299564130d07 to your computer and use it in GitHub Desktop.
Flutter Gmail Clone
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'dart:math' as math;
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Gmail Clone',
theme: ThemeData(primaryColor: Colors.white, accentColor: Colors.black),
home: GmailApp(),
);
}
}
class GmailApp extends StatefulWidget {
const GmailApp({Key key}) : super(key: key);
@override
_GmailAppState createState() => _GmailAppState();
}
class _GmailAppState extends State<GmailApp> {
final emailsController = ValueNotifier<List<Email>>(null);
final indexController = ValueNotifier<int>(0);
@override
void initState() {
super.initState();
emailsController.value = [];
final rdm = math.Random();
const total = 200;
List.generate(total, (index) {
final _current = emailsController.value;
_current.add(Email(
sender: (EmailContact()
..firstName = createWord()
..lastName = createWord()),
isRead: false,
dateReceived: DateTime.now().subtract(Duration(minutes: total - index)),
subject: createSentence(sentenceLength: 5),
body: createText(
numParagraphs: rdm.nextInt(5),
numSentences: rdm.nextInt(8),
),
));
updateEmails(_current);
});
}
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<List<Email>>(
valueListenable: emailsController,
builder: (context, emails, child) => LayoutBuilder(
builder: (context, dimens) {
if (dimens.maxWidth >= kTabletBreakpoint) {
return Scaffold(
drawer: _Drawer(),
floatingActionButton: _FAB(),
body: Row(
children: [
Builder(
builder: (context) => NavigationRail(
destinations: [
NavigationRailDestination(
icon: Icon(Icons.menu),
label: Text('Menu'),
),
NavigationRailDestination(
icon: Icon(Icons.inbox),
label: Text('Inbox'),
),
NavigationRailDestination(
icon: Icon(Icons.people),
label: Text('Contacts'),
),
NavigationRailDestination(
icon: Icon(Icons.label),
label: Text('Tags'),
),
],
labelType: NavigationRailLabelType.none,
selectedIndex: 0,
onDestinationSelected: (val) {
if (val == 0) {
Scaffold.of(context).openDrawer();
}
},
),
),
SizedBox(
width: kListWidth,
child: buildBody(emails, false),
),
Expanded(
child: _Details(
emailsController: emailsController,
indexController: indexController,
),
),
],
),
);
}
return Scaffold(
floatingActionButton: _FAB(),
drawer: _Drawer(),
body: buildBody(emails, true),
);
},
),
);
}
Widget buildBody(List<Email> emails, bool isMobile) {
return NestedScrollView(
headerSliverBuilder: (context, innerScrolled) {
return [
SliverPadding(
padding: EdgeInsets.only(top: 8),
sliver: SliverFloatingBar(
floating: true,
snap: true,
leading: !isMobile
? null
: InkWell(
child: Icon(Icons.menu),
onTap: () {
Scaffold.of(context).openDrawer();
},
),
automaticallyImplyLeading: false,
title: TextField(
decoration: InputDecoration.collapsed(hintText: "Search mail"),
),
trailing: CircleAvatar(
child: Text("RD"),
),
),
),
];
},
body: Scrollbar(
child: buildEmailList(emails, isMobile),
),
);
}
Widget buildEmailList(List<Email> emails, bool isMobile) {
if (emails == null) {
return Center(child: CircularProgressIndicator());
}
if (emails.isEmpty) {
return Center(child: Text('No Emails Found!'));
}
return ListView.builder(
itemCount: emails.length,
itemBuilder: (context, index) {
final item = emails[index];
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
updateEmailAtIndex(item.copyWith(isRead: true), index);
emailsController.value = emails;
indexController.value = index;
if (isMobile) {
Navigator.of(context)
.push<String>(MaterialPageRoute(
builder: (context) => _Details(
emailsController: emailsController,
indexController: indexController,
),
))
.then((message) {
if (message != null) {
Scaffold.of(context).showSnackBar(
SnackBar(content: Text(message)),
);
}
});
return;
}
},
child: ValueListenableBuilder<int>(
valueListenable: indexController,
builder: (context, itemIndex, child) => Container(
decoration: isMobile || index != itemIndex
? null
: BoxDecoration(
color: Colors.blueGrey[100],
border: Border(
left: BorderSide(
color: Colors.blueGrey,
width: 5,
),
)),
child: _EmailTile(
email: emails[index],
onChanged: (val) {
updateEmailAtIndex(val, index);
},
),
),
),
);
},
);
}
void updateEmails(List<Email> items) {
emailsController.value = items;
emailsController.notifyListeners();
}
void updateEmailAtIndex(Email val, int index) {
final _current = emailsController.value;
_current[index] = val;
emailsController.value = _current;
emailsController.notifyListeners();
}
}
class _Details extends StatelessWidget {
const _Details({
Key key,
@required this.emailsController,
@required this.indexController,
}) : super(key: key);
final ValueNotifier<List<Email>> emailsController;
final ValueNotifier<int> indexController;
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<List<Email>>(
valueListenable: emailsController,
builder: (context, emails, child) => ValueListenableBuilder<int>(
valueListenable: indexController,
builder: (context, index, child) {
final selectedEmail = emails[index];
return Scaffold(
appBar: AppBar(
actions: [
IconButton(
icon: Icon(Icons.archive),
onPressed: () {
final _current = emails;
_current.removeAt(index);
updateEmails(_current);
Navigator.of(context).maybePop('Message moved to Archive');
},
),
IconButton(
icon: Icon(Icons.delete_outline),
onPressed: () {
final _current = emails;
_current.removeAt(index);
updateEmails(_current);
Navigator.of(context).maybePop('Message Deleted');
},
),
IconButton(
icon: Icon(Icons.mail_outline),
onPressed: () {
Navigator.of(context).maybePop();
},
),
IconButton(
icon: Icon(Icons.more_horiz),
onPressed: () {},
),
],
),
body: SingleChildScrollView(
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(
child: Text(
selectedEmail?.subject ?? 'No Subject',
style: TextStyle(
fontSize: 20,
),
),
),
InkWell(
borderRadius: BorderRadius.circular(18),
child: selectedEmail.isFavorite
? Icon(
Icons.star,
color: Colors.amber,
)
: Icon(
Icons.star_border,
color: inactiveColor,
),
onTap: () {
updateEmailAtIndex(
selectedEmail.copyWith(
isFavorite: !selectedEmail.isFavorite),
index,
);
},
),
],
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(selectedEmail?.body ?? 'No Message Content'),
),
],
),
),
);
},
),
);
}
void updateEmails(List<Email> items) {
emailsController.value = items;
emailsController.notifyListeners();
}
void updateEmailAtIndex(Email val, int index) {
final _current = emailsController.value;
_current[index] = val;
emailsController.value = _current;
emailsController.notifyListeners();
}
}
class _Drawer extends StatelessWidget {
const _Drawer({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Drawer(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(top: 40, left: 20, bottom: 10),
child: Text(
'Gmail',
style: TextStyle(
color: Colors.red,
fontSize: 22,
),
),
),
Divider(),
],
),
);
}
}
// -- Custom Widgets --
class _EmailTile extends StatelessWidget {
const _EmailTile({
Key key,
@required this.email,
@required this.onChanged,
}) : super(key: key);
final Email email;
final ValueChanged<Email> onChanged;
@override
Widget build(BuildContext context) {
const kFontSize = 14.0;
return Container(
margin: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CircleAvatar(
radius: 23,
child: email.sender.hasImage
? Image.network(email.sender.photoUrl)
: Text(email.sender.letter)),
const SizedBox(width: 15),
Expanded(
child: Column(
children: [
Row(
children: [
Text(
email.recipients == null || email.recipients.isEmpty
? email.sender.displayName
: 'Test',
style: isReadStyle.copyWith(fontSize: kFontSize),
),
Spacer(),
Text(
email.time,
style: isReadStyle.copyWith(fontSize: kFontSize),
),
],
),
Container(
height: 45,
padding: EdgeInsets.only(top: 6),
child: Row(
children: [
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
email?.subject ?? 'No Subject',
style: isReadStyle.copyWith(fontSize: kFontSize),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
Text(
email?.body ?? 'No Preview Available',
style: TextStyle(color: inactiveColor)
.copyWith(fontSize: kFontSize),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
InkWell(
borderRadius: BorderRadius.circular(18),
child: email?.isFavorite ?? false
? Icon(
Icons.star,
color: Colors.amber,
)
: Icon(
Icons.star_border,
color: inactiveColor,
),
onTap: () {
onChanged(
email.copyWith(isFavorite: !email.isFavorite),
);
},
),
],
),
],
),
),
],
)),
],
),
);
}
TextStyle get isReadStyle {
if (!email.isRead) {
return TextStyle(
fontWeight: FontWeight.bold,
);
}
return TextStyle(
color: inactiveColor,
);
}
}
class _FAB extends StatelessWidget {
const _FAB({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final _isDark = Theme.of(context).brightness == Brightness.dark;
return FloatingActionButton(
backgroundColor: _isDark ? null : Colors.white,
child: CustomPaint(painter: _FloatingPainter(), child: Container()),
onPressed: () {},
);
}
}
class _FloatingPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
Paint amberPaint = Paint()
..color = Colors.amber
..strokeWidth = 5;
Paint greenPaint = Paint()
..color = Colors.green
..strokeWidth = 5;
Paint bluePaint = Paint()
..color = Colors.blue
..strokeWidth = 5;
Paint redPaint = Paint()
..color = Colors.red
..strokeWidth = 5;
canvas.drawLine(Offset(size.width * 0.27, size.height * 0.5),
Offset(size.width * 0.5, size.height * 0.5), amberPaint);
canvas.drawLine(
Offset(size.width * 0.5, size.height * 0.5),
Offset(size.width * 0.5, size.height - (size.height * 0.27)),
greenPaint);
canvas.drawLine(Offset(size.width * 0.5, size.height * 0.5),
Offset(size.width - (size.width * 0.27), size.height * 0.5), bluePaint);
canvas.drawLine(Offset(size.width * 0.5, size.height * 0.5),
Offset(size.width * 0.5, size.height * 0.27), redPaint);
}
@override
bool shouldRepaint(_FloatingPainter oldDelegate) => false;
@override
bool shouldRebuildSemantics(_FloatingPainter oldDelegate) => false;
}
class SliverFloatingBar extends StatefulWidget {
const SliverFloatingBar({
Key key,
this.leading,
this.automaticallyImplyLeading = true,
this.title,
this.trailing,
this.elevation = 5.0,
this.backgroundColor,
this.floating = false,
this.pinned = false,
this.snap = false,
}) : assert(automaticallyImplyLeading != null),
assert(floating != null),
assert(pinned != null),
assert(snap != null),
assert(floating || !snap,
'The "snap" argument only makes sense for floating app bars.'),
super(key: key);
final Widget leading;
final bool automaticallyImplyLeading;
final Widget title;
final Widget trailing;
final double elevation;
final Color backgroundColor;
final bool floating;
final bool pinned;
final bool snap;
@override
_SliverFloatingBarState createState() => _SliverFloatingBarState();
}
class _SliverFloatingBarState extends State<SliverFloatingBar>
with TickerProviderStateMixin {
FloatingHeaderSnapConfiguration _snapConfiguration;
void _updateSnapConfiguration() {
if (widget.snap && widget.floating) {
_snapConfiguration = FloatingHeaderSnapConfiguration(
vsync: this,
curve: Curves.easeOut,
duration: const Duration(milliseconds: 200),
);
} else {
_snapConfiguration = null;
}
}
@override
void initState() {
super.initState();
_updateSnapConfiguration();
}
@override
void didUpdateWidget(SliverFloatingBar oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.snap != oldWidget.snap || widget.floating != oldWidget.floating)
_updateSnapConfiguration();
}
@override
Widget build(BuildContext context) {
final double topPadding = MediaQuery.of(context).padding.top;
final double collapsedHeight =
(widget.pinned && widget.floating) ? topPadding : null;
return MediaQuery.removePadding(
context: context,
removeBottom: true,
child: SliverPersistentHeader(
floating: widget.floating,
pinned: widget.pinned,
delegate: _SliverAppBarDelegate(
leading: widget.leading,
automaticallyImplyLeading: widget.automaticallyImplyLeading,
title: widget.title,
trailing: widget.trailing,
elevation: widget.elevation,
backgroundColor: widget.backgroundColor,
floating: widget.floating,
pinned: widget.pinned,
snapConfiguration: _snapConfiguration,
collapsedHeight: collapsedHeight,
topPadding: topPadding,
),
),
);
}
}
class _FloatingAppBar extends StatefulWidget {
const _FloatingAppBar({Key key, this.child}) : super(key: key);
final Widget child;
@override
_FloatingAppBarState createState() => _FloatingAppBarState();
}
class _FloatingAppBarState extends State<_FloatingAppBar> {
ScrollPosition _position;
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (_position != null)
_position.isScrollingNotifier.removeListener(_isScrollingListener);
_position = Scrollable.of(context)?.position;
if (_position != null)
_position.isScrollingNotifier.addListener(_isScrollingListener);
}
@override
void dispose() {
if (_position != null)
_position.isScrollingNotifier.removeListener(_isScrollingListener);
super.dispose();
}
RenderSliverFloatingPersistentHeader _headerRenderer() {
return context.ancestorRenderObjectOfType(
const TypeMatcher<RenderSliverFloatingPersistentHeader>());
}
void _isScrollingListener() {
if (_position == null) return;
final RenderSliverFloatingPersistentHeader header = _headerRenderer();
if (_position.isScrollingNotifier.value)
header?.maybeStopSnapAnimation(_position.userScrollDirection);
else
header?.maybeStartSnapAnimation(_position.userScrollDirection);
}
@override
Widget build(BuildContext context) => widget.child;
}
class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
_SliverAppBarDelegate({
@required this.leading,
@required this.automaticallyImplyLeading,
@required this.title,
@required this.trailing,
@required this.elevation,
@required this.backgroundColor,
@required this.floating,
@required this.pinned,
@required this.snapConfiguration,
@required this.collapsedHeight,
@required this.topPadding,
});
final Widget trailing;
final bool automaticallyImplyLeading;
final Color backgroundColor;
final double elevation;
final bool floating;
final Widget leading;
final bool pinned;
final Widget title;
final double collapsedHeight;
final double topPadding;
@override
double get minExtent => collapsedHeight ?? (topPadding + kToolbarHeight);
@override
final FloatingHeaderSnapConfiguration snapConfiguration;
@override
double get maxExtent => math.max(topPadding + (kToolbarHeight), minExtent);
@override
bool shouldRebuild(covariant _SliverAppBarDelegate oldDelegate) {
return leading != oldDelegate.leading ||
automaticallyImplyLeading != oldDelegate.automaticallyImplyLeading ||
title != oldDelegate.title ||
trailing != oldDelegate.trailing ||
elevation != oldDelegate.elevation ||
topPadding != oldDelegate.topPadding ||
collapsedHeight != oldDelegate.collapsedHeight ||
backgroundColor != oldDelegate.backgroundColor ||
pinned != oldDelegate.pinned ||
floating != oldDelegate.floating ||
snapConfiguration != oldDelegate.snapConfiguration;
}
@override
String toString() {
return '';
}
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
final double visibleMainHeight = maxExtent - shrinkOffset;
final double toolbarOpacity = !pinned || (floating)
? ((visibleMainHeight) / kToolbarHeight).clamp(0.0, 1.0)
: 1.0;
final Widget appBar = FlexibleSpaceBar.createSettings(
minExtent: minExtent,
maxExtent: maxExtent,
currentExtent: math.max(minExtent, maxExtent - shrinkOffset),
toolbarOpacity: toolbarOpacity,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 12.0),
child: SafeArea(
child: Material(
color: backgroundColor,
borderRadius: BorderRadius.circular(8.0),
elevation: elevation,
child: ListTile(
leading: leading ??
(Scaffold.of(context).hasDrawer && automaticallyImplyLeading
? IconButton(
icon: Icon(Icons.menu),
onPressed: () {
Scaffold.of(context).openDrawer();
},
)
: null),
title: title,
trailing: trailing ??
(Scaffold.of(context).hasEndDrawer &&
automaticallyImplyLeading
? IconButton(
icon: Icon(Icons.menu),
onPressed: () {
Scaffold.of(context).openEndDrawer();
},
)
: null),
),
),
),
),
);
return Container(child: floating ? _FloatingAppBar(child: appBar) : appBar);
}
}
// -- Classes --
class Email {
Email({
this.recipients = const [],
this.dateReceived,
this.subject,
this.body,
this.isFavorite = false,
this.isRead = false,
this.attachments,
this.sender,
});
List<EmailContact> recipients;
DateTime dateReceived;
String subject;
String body;
bool isFavorite = false;
bool isRead = false;
List<EmailAttachment> attachments;
EmailContact sender;
String get time {
final past12 = dateReceived.hour > 12;
final sb = StringBuffer();
if (past12) {
sb.write('${dateReceived.hour - 12}');
} else {
sb.write('${dateReceived.hour}');
}
sb.write(':');
if (dateReceived.minute < 10) {
sb.write('0');
}
sb.write('${dateReceived.minute}');
if (past12) {
sb.write('pm');
} else {
sb.write('am');
}
return sb.toString();
}
Email copyWith({
List<EmailContact> recipients,
DateTime dateReceived,
String subject,
String body,
bool isFavorite,
bool isRead,
List<EmailAttachment> attachments,
EmailContact sender,
}) {
return Email(
recipients: recipients ?? this.recipients,
dateReceived: dateReceived ?? this.dateReceived,
subject: subject ?? this.subject,
body: body ?? this.body,
isFavorite: isFavorite ?? this.isFavorite,
isRead: isRead ?? this.isRead,
attachments: attachments ?? this.attachments,
sender: sender ?? this.sender,
);
}
Map<String, dynamic> toMap() {
return {
'recipients': recipients?.map((x) => x?.toMap())?.toList(),
'dateReceived': dateReceived?.millisecondsSinceEpoch,
'subject': subject,
'body': body,
'isFavorite': isFavorite,
'isRead': isRead,
'attachments': attachments?.map((x) => x?.toMap())?.toList(),
'sender': sender?.toMap(),
};
}
static Email fromMap(Map<String, dynamic> map) {
if (map == null) return null;
return Email(
recipients: List<EmailContact>.from(
map['recipients']?.map((x) => EmailContact.fromMap(x))),
dateReceived: DateTime.fromMillisecondsSinceEpoch(map['dateReceived']),
subject: map['subject'],
body: map['body'],
isFavorite: map['isFavorite'],
isRead: map['isRead'],
attachments: List<EmailAttachment>.from(
map['attachments']?.map((x) => EmailAttachment.fromMap(x))),
sender: EmailContact.fromMap(map['sender']),
);
}
String toJson() => json.encode(toMap());
static Email fromJson(String source) => fromMap(json.decode(source));
@override
String toString() {
return 'Email(recipients: $recipients, dateReceived: $dateReceived, subject: $subject, body: $body, isFavorite: $isFavorite, isRead: $isRead, attachments: $attachments, sender: $sender)';
}
@override
bool operator ==(Object o) {
if (identical(this, o)) return true;
return o is Email &&
listEquals(o.recipients, recipients) &&
o.dateReceived == dateReceived &&
o.subject == subject &&
o.body == body &&
o.isFavorite == isFavorite &&
o.isRead == isRead &&
listEquals(o.attachments, attachments) &&
o.sender == sender;
}
@override
int get hashCode {
return recipients.hashCode ^
dateReceived.hashCode ^
subject.hashCode ^
body.hashCode ^
isFavorite.hashCode ^
isRead.hashCode ^
attachments.hashCode ^
sender.hashCode;
}
}
class EmailAttachment {
String fileName;
EmailAttachment({
this.fileName,
});
String get ext => fileName.split('.').last;
EmailAttachment copyWith({
String fileName,
}) {
return EmailAttachment(
fileName: fileName ?? this.fileName,
);
}
Map<String, dynamic> toMap() {
return {
'fileName': fileName,
};
}
static EmailAttachment fromMap(Map<String, dynamic> map) {
if (map == null) return null;
return EmailAttachment(
fileName: map['fileName'],
);
}
String toJson() => json.encode(toMap());
static EmailAttachment fromJson(String source) =>
fromMap(json.decode(source));
@override
String toString() => 'EmailAttachment(fileName: $fileName)';
@override
bool operator ==(Object o) {
if (identical(this, o)) return true;
return o is EmailAttachment && o.fileName == fileName;
}
@override
int get hashCode => fileName.hashCode;
}
class EmailContact {
String firstName;
String lastName;
String photoUrl;
EmailContact({
this.firstName,
this.lastName,
this.photoUrl,
});
bool get hasImage => stringExists(photoUrl);
String get displayName => '${firstName ?? ''} ${lastName ?? ''}'.trim();
String get letter => displayName.substring(0, 1);
EmailContact copyWith({
String firstName,
String lastName,
String photoUrl,
}) {
return EmailContact(
firstName: firstName ?? this.firstName,
lastName: lastName ?? this.lastName,
photoUrl: photoUrl ?? this.photoUrl,
);
}
Map<String, dynamic> toMap() {
return {
'firstName': firstName,
'lastName': lastName,
'photoUrl': photoUrl,
};
}
static EmailContact fromMap(Map<String, dynamic> map) {
if (map == null) return null;
return EmailContact(
firstName: map['firstName'],
lastName: map['lastName'],
photoUrl: map['photoUrl'],
);
}
String toJson() => json.encode(toMap());
static EmailContact fromJson(String source) => fromMap(json.decode(source));
@override
String toString() =>
'EmailContact(firstName: $firstName, lastName: $lastName, photoUrl: $photoUrl)';
@override
bool operator ==(Object o) {
if (identical(this, o)) return true;
return o is EmailContact &&
o.firstName == firstName &&
o.lastName == lastName &&
o.photoUrl == photoUrl;
}
@override
int get hashCode =>
firstName.hashCode ^ lastName.hashCode ^ photoUrl.hashCode;
}
_randomInt(int min, int max) {
math.Random rnd = new math.Random();
return rnd.nextInt((max - min) + 1) + min;
}
/// Creates [numWords] number of random words.
String createWord({int numWords = 1}) {
math.Random _random;
if (numWords > 1) {
return createSentence(sentenceLength: numWords, numSentences: 1);
}
_random = math.Random();
return words[_random.nextInt(words.length)];
}
/// Creates random sentences.
///
/// Sentences are either exactly [sentenceLength] words in length, or a randomly
/// generated length. [numSentences] defines the number of sentences generated.
/// Returned sentences are punctuated.
String createSentence({int sentenceLength = -1, int numSentences = 1}) {
int wordIndex;
String sentence;
if (numSentences > 1) return createParagraph(numSentences: numSentences);
if (sentenceLength < 0) {
sentenceLength = _randomInt(5, 20);
}
wordIndex = _randomInt(0, words.length - sentenceLength - 1);
sentence = words.getRange(wordIndex, wordIndex + sentenceLength).join(" ");
sentence = sentence[0].toUpperCase() + sentence.substring(1) + ".";
return sentence;
}
/// Creates random paragraphs.
///
/// Paragraphs are comprised of a random number of sentences, or explicitly
/// [numSentences] long. [numParagraphs] specifies the number of paragraphs
/// to generate.
String createParagraph({int numSentences = -1, int numParagraphs = 1}) {
List<String> sentences = [];
if (numParagraphs > 1)
return createText(numSentences: numSentences, numParagraphs: numParagraphs);
if (numSentences < 0) {
numSentences = _randomInt(3, 5);
}
for (var i = 0; i < numSentences; i++) {
sentences.add(createSentence());
}
return sentences.getRange(0, sentences.length).join(" ");
}
/// Creates a text comprised of a number of paragraphs.
///
/// Each text is comprised of [numParagraphs] paragraphs, each of which
/// contain [numSentences] sentences. If either parameter is omitted, a
/// random number is generated.
String createText({int numParagraphs = -1, int numSentences = -1}) {
List<String> paragraphs = [];
if (numParagraphs < 0) {
numParagraphs = _randomInt(3, 7);
}
for (var i = 0; i < numParagraphs; i++) {
paragraphs.add('${createParagraph(numSentences: numSentences)}\n');
}
return paragraphs.getRange(0, paragraphs.length).join("\n");
}
List<String> words = [
"lorem",
"ipsum",
"dolor",
"sit",
"amet",
"consectetur",
"adipiscing",
"elit",
"ut",
"aliquam",
"purus",
"sit",
"amet",
"luctus",
"venenatis",
"lectus",
"magna",
"fringilla",
"urna",
"porttitor",
"rhoncus",
"dolor",
"purus",
"non",
"enim",
"praesent",
"elementum",
"facilisis",
"leo",
"vel",
"fringilla",
"est",
"ullamcorper",
"eget",
"nulla",
"facilisi",
"etiam",
"dignissim",
"diam",
"quis",
"enim",
"lobortis",
"scelerisque",
"fermentum",
"dui",
"faucibus",
"in",
"ornare",
"quam",
"viverra",
"orci",
"sagittis",
"eu",
"volutpat",
"odio",
"facilisis",
"mauris",
"sit",
"amet",
"massa",
"vitae",
"tortor",
"condimentum",
"lacinia",
"quis",
"vel",
"eros",
"donec",
"ac",
"odio",
"tempor",
"orci",
"dapibus",
"ultrices",
"in",
"iaculis",
"nunc",
"sed",
"augue",
"lacus",
"viverra",
"vitae",
"congue",
"eu",
"consequat",
"ac",
"felis",
"donec",
"et",
"odio",
"pellentesque",
"diam",
"volutpat",
"commodo",
"sed",
"egestas",
"egestas",
"fringilla",
"phasellus",
"faucibus",
"scelerisque",
"eleifend",
"donec",
"pretium",
"vulputate",
"sapien",
"nec",
"sagittis",
"aliquam",
"malesuada",
"bibendum",
"arcu",
"vitae",
"elementum",
"curabitur",
"vitae",
"nunc",
"sed",
"velit",
"dignissim",
"sodales",
"ut",
"eu",
"sem",
"integer",
"vitae",
"justo",
"eget",
"magna",
"fermentum",
"iaculis",
"eu",
"non",
"diam",
"phasellus",
"vestibulum",
"lorem",
"sed",
"risus",
"ultricies",
"tristique",
"nulla",
"aliquet",
"enim",
"tortor",
"at",
"auctor",
"urna",
"nunc",
"id",
"cursus",
"metus",
"aliquam",
"eleifend",
"mi",
"in",
"nulla",
"posuere",
"sollicitudin",
"aliquam",
"ultrices",
"sagittis",
"orci",
"a",
"scelerisque",
"purus",
"semper",
"eget",
"duis",
"at",
"tellus",
"at",
"urna",
"condimentum",
"mattis",
"pellentesque",
"id",
"nibh",
"tortor",
"id",
"aliquet",
"lectus",
"proin",
"nibh",
"nisl",
"condimentum",
"id",
"venenatis",
"a",
"condimentum",
"vitae",
"sapien",
"pellentesque",
"habitant",
"morbi",
"tristique",
"senectus",
"et",
"netus",
"et",
"malesuada",
"fames",
"ac",
"turpis",
"egestas",
"sed",
"tempus",
"urna",
"et",
"pharetra",
"pharetra",
"massa",
"massa",
"ultricies",
"mi",
"quis",
"hendrerit",
"dolor",
"magna",
"eget",
"est",
"lorem",
"ipsum",
"dolor",
"sit",
"amet",
"consectetur",
"adipiscing",
"elit",
"pellentesque",
"habitant",
"morbi",
"tristique",
"senectus",
"et",
"netus",
"et",
"malesuada",
"fames",
"ac",
"turpis",
"egestas",
"integer",
"eget",
"aliquet",
"nibh",
"praesent",
"tristique",
"magna",
"sit",
"amet",
"purus",
"gravida",
"quis",
"blandit",
"turpis",
"cursus",
"in",
"hac",
"habitasse",
"platea",
"dictumst",
"quisque",
"sagittis",
"purus",
"sit",
"amet",
"volutpat",
"consequat",
"mauris",
"nunc",
"congue",
"nisi",
"vitae",
"suscipit",
"tellus",
"mauris",
"a",
"diam",
"maecenas",
"sed",
"enim",
"ut",
"sem",
"viverra",
"aliquet",
"eget",
"sit",
"amet",
"tellus",
"cras",
"adipiscing",
"enim",
"eu",
"turpis",
"egestas",
"pretium",
"aenean",
"pharetra",
"magna",
"ac",
"placerat",
"vestibulum",
"lectus",
"mauris",
"ultrices",
"eros",
"in",
"cursus",
"turpis",
"massa",
"tincidunt",
"dui",
"ut",
"ornare",
"lectus",
"sit",
"amet",
"est",
"placerat",
"in",
"egestas",
"erat",
"imperdiet",
"sed",
"euismod",
"nisi",
"porta",
"lorem",
"mollis",
"aliquam",
"ut",
"porttitor",
"leo",
"a",
"diam",
"sollicitudin",
"tempor",
"id",
"eu",
"nisl",
"nunc",
"mi",
"ipsum",
"faucibus",
"vitae",
"aliquet",
"nec",
"ullamcorper",
"sit",
"amet",
"risus",
"nullam",
"eget",
"felis",
"eget",
"nunc",
"lobortis",
"mattis",
"aliquam",
"faucibus",
"purus",
"in",
"massa",
"tempor",
"nec",
"feugiat",
"nisl",
"pretium",
"fusce",
"id",
"velit",
"ut",
"tortor",
"pretium",
"viverra",
"suspendisse",
"potenti",
"nullam",
"ac",
"tortor",
"vitae",
"purus",
"faucibus",
"ornare",
"suspendisse",
"sed",
"nisi",
"lacus",
"sed",
"viverra",
"tellus",
"in",
"hac",
"habitasse",
"platea",
"dictumst",
"vestibulum",
"rhoncus",
"est",
"pellentesque",
"elit",
"ullamcorper",
"dignissim",
"cras",
"tincidunt",
"lobortis",
"feugiat",
"vivamus",
"at",
"augue",
"eget",
"arcu",
"dictum",
"varius",
"duis",
"at",
"consectetur",
"lorem",
"donec",
"massa",
"sapien",
"faucibus",
"et",
"molestie",
"ac",
"feugiat",
"sed",
"lectus",
"vestibulum",
"mattis",
"ullamcorper",
"velit",
"sed",
"ullamcorper",
"morbi",
"tincidunt",
"ornare",
"massa",
"eget",
"egestas",
"purus",
"viverra",
"accumsan",
"in",
"nisl",
"nisi",
"scelerisque",
"eu",
"ultrices",
"vitae",
"auctor",
"eu",
"augue",
"ut",
"lectus",
"arcu",
"bibendum",
"at",
"varius",
"vel",
"pharetra",
"vel",
"turpis",
"nunc",
"eget",
"lorem",
"dolor",
"sed",
"viverra",
"ipsum",
"nunc",
"aliquet",
"bibendum",
"enim",
"facilisis",
"gravida",
"neque",
"convallis",
"a",
"cras",
"semper",
"auctor",
"neque",
"vitae",
"tempus",
"quam",
"pellentesque",
"nec",
"nam",
"aliquam",
"sem",
"et",
"tortor",
"consequat",
"id",
"porta",
"nibh",
"venenatis",
"cras",
"sed",
"felis",
"eget",
"velit",
"aliquet",
"sagittis",
"id",
"consectetur",
"purus",
"ut",
"faucibus",
"pulvinar",
"elementum",
"integer",
"enim",
"neque",
"volutpat",
"ac",
"tincidunt",
"vitae",
"semper",
"quis",
"lectus",
"nulla",
"at",
"volutpat",
"diam",
"ut",
"venenatis",
"tellus",
"in",
"metus",
"vulputate",
"eu",
"scelerisque",
"felis",
"imperdiet",
"proin",
"fermentum",
"leo",
"vel",
"orci",
"porta",
"non",
"pulvinar",
"neque",
"laoreet",
"suspendisse",
"interdum",
"consectetur",
"libero",
"id",
"faucibus",
"nisl",
"tincidunt",
"eget",
"nullam",
"non",
"nisi",
"est",
"sit",
"amet",
"facilisis",
"magna",
"etiam",
"tempor",
"orci",
"eu",
"lobortis",
"elementum",
"nibh",
"tellus",
"molestie",
"nunc",
"non",
"blandit",
"massa",
"enim",
"nec",
"dui",
"nunc",
"mattis",
"enim",
"ut",
"tellus",
"elementum",
"sagittis",
"vitae",
"et",
"leo",
"duis",
"ut",
"diam",
"quam",
"nulla",
"porttitor",
"massa",
"id",
"neque",
"aliquam",
"vestibulum",
"morbi",
"blandit",
"cursus",
"risus",
"at",
"ultrices",
"mi",
"tempus",
"imperdiet",
"nulla",
"malesuada",
"pellentesque",
"elit",
"eget",
"gravida",
"cum",
"sociis",
"natoque",
"penatibus",
"et",
"magnis",
"dis",
"parturient",
"montes",
"nascetur",
"ridiculus",
"mus",
"mauris",
"vitae",
"ultricies",
"leo",
"integer",
"malesuada",
"nunc",
"vel",
"risus",
"commodo",
"viverra",
"maecenas",
"accumsan",
"lacus",
"vel",
"facilisis",
"volutpat",
"est",
"velit",
"egestas",
"dui",
"id",
"ornare",
"arcu",
"odio",
"ut",
"sem",
"nulla",
"pharetra",
"diam",
"sit",
"amet",
"nisl",
"suscipit",
"adipiscing",
"bibendum",
"est",
"ultricies",
"integer",
"quis",
"auctor",
"elit",
"sed",
"vulputate",
"mi",
"sit",
"amet",
"mauris",
"commodo",
"quis",
"imperdiet",
"massa",
"tincidunt",
"nunc",
"pulvinar",
"sapien",
"et",
"ligula",
"ullamcorper",
"malesuada",
"proin",
"libero",
"nunc",
"consequat",
"interdum",
"varius",
"sit",
"amet",
"mattis",
"vulputate",
"enim",
"nulla",
"aliquet",
"porttitor",
"lacus",
"luctus",
"accumsan",
"tortor",
"posuere",
"ac",
"ut",
"consequat",
"semper",
"viverra",
"nam",
"libero",
"justo",
"laoreet",
"sit",
"amet",
"cursus",
"sit",
"amet",
"dictum",
"sit",
"amet",
"justo",
"donec",
"enim",
"diam",
"vulputate",
"ut",
"pharetra",
"sit",
"amet",
"aliquam",
"id",
"diam",
"maecenas",
"ultricies",
"mi",
"eget",
"mauris",
"pharetra",
"et",
"ultrices",
"neque",
"ornare",
"aenean",
"euismod",
"elementum",
"nisi",
"quis",
"eleifend",
"quam",
"adipiscing",
"vitae",
"proin",
"sagittis",
"nisl",
"rhoncus",
"mattis",
"rhoncus",
"urna",
"neque",
"viverra",
"justo",
"nec",
"ultrices",
"dui",
"sapien",
"eget",
"mi",
"proin",
"sed",
"libero",
"enim",
"sed",
"faucibus",
"turpis",
"in",
"eu",
"mi",
"bibendum",
"neque",
"egestas",
"congue",
"quisque",
"egestas",
"diam",
"in",
"arcu",
"cursus",
"euismod",
"quis",
"viverra",
"nibh",
"cras",
"pulvinar",
"mattis",
"nunc",
"sed",
"blandit",
"libero",
"volutpat",
"sed",
"cras",
"ornare",
"arcu",
"dui",
"vivamus",
"arcu",
"felis",
"bibendum",
"ut",
"tristique",
"et",
"egestas",
"quis",
"ipsum",
"suspendisse",
"ultrices",
"fusce",
"ut",
"placerat",
"orci",
"nulla",
"pellentesque",
"dignissim",
"enim",
"sit",
"amet",
"venenatis",
"urna",
"cursus",
"eget",
"nunc",
"scelerisque",
"viverra",
"mauris",
"in",
"aliquam",
"sem",
"fringilla",
"ut",
"morbi",
"tincidunt",
"augue",
"interdum",
"velit",
"euismod",
"in",
"pellentesque",
"massa",
"placerat",
"duis",
"ultricies",
"lacus",
"sed",
"turpis",
"tincidunt",
"id",
"aliquet",
"risus",
"feugiat",
"in",
"ante",
"metus",
"dictum",
"at",
"tempor",
"commodo",
"ullamcorper",
"a",
"lacus",
"vestibulum",
"sed",
"arcu",
"non",
"odio",
"euismod",
"lacinia",
"at",
"quis",
"risus",
"sed",
"vulputate",
"odio",
"ut",
"enim",
"blandit",
"volutpat",
"maecenas",
"volutpat",
"blandit",
"aliquam",
"etiam",
"erat",
"velit",
"scelerisque",
"in",
"dictum",
"non",
"consectetur",
"a",
"erat",
"nam",
"at",
"lectus",
"urna",
"duis",
"convallis",
"convallis",
"tellus",
"id",
"interdum",
"velit",
"laoreet",
"id",
"donec",
"ultrices",
"tincidunt",
"arcu",
"non",
"sodales",
"neque",
"sodales",
"ut",
"etiam",
"sit",
"amet",
"nisl",
"purus",
"in",
"mollis",
"nunc",
"sed",
"id",
"semper",
"risus",
"in",
"hendrerit",
"gravida",
"rutrum",
"quisque",
"non",
"tellus",
"orci",
"ac",
"auctor",
"augue",
"mauris",
"augue",
"neque",
"gravida",
"in",
"fermentum",
"et",
"sollicitudin",
"ac",
"orci",
"phasellus",
"egestas",
"tellus",
"rutrum",
"tellus",
"pellentesque",
"eu",
"tincidunt",
"tortor",
"aliquam",
"nulla",
"facilisi",
"cras",
"fermentum",
"odio",
"eu",
"feugiat",
"pretium",
"nibh",
"ipsum",
"consequat",
"nisl",
"vel",
"pretium",
"lectus",
"quam",
"id",
"leo",
"in",
"vitae",
"turpis",
"massa",
"sed",
"elementum",
"tempus",
"egestas",
"sed",
"sed",
"risus",
"pretium",
"quam",
"vulputate",
"dignissim",
"suspendisse",
"in",
"est",
"ante",
"in",
"nibh",
"mauris",
"cursus",
"mattis",
"molestie",
"a",
"iaculis",
"at",
"erat",
"pellentesque",
"adipiscing",
"commodo",
"elit",
"at",
"imperdiet",
"dui",
"accumsan",
"sit",
"amet",
"nulla",
"facilisi",
"morbi",
"tempus",
"iaculis",
"urna",
"id",
"volutpat",
"lacus",
"laoreet",
"non",
"curabitur",
"gravida",
"arcu",
"ac",
"tortor",
"dignissim",
"convallis",
"aenean",
"et",
"tortor",
"at",
"risus",
"viverra",
"adipiscing",
"at",
"in",
"tellus",
"integer",
"feugiat",
"scelerisque",
"varius",
"morbi",
"enim",
"nunc",
"faucibus",
"a",
"pellentesque",
"sit",
"amet",
"porttitor",
"eget",
"dolor",
"morbi",
"non",
"arcu",
"risus",
"quis",
"varius",
"quam",
"quisque",
"id",
"diam",
"vel",
"quam",
"elementum",
"pulvinar",
"etiam",
"non",
"quam",
"lacus",
"suspendisse",
"faucibus",
"interdum",
"posuere",
"lorem",
"ipsum",
"dolor",
"sit",
"amet",
"consectetur",
"adipiscing",
"elit",
"duis",
"tristique",
"sollicitudin",
"nibh",
"sit",
"amet",
"commodo",
"nulla",
"facilisi",
"nullam",
"vehicula",
"ipsum",
"a",
"arcu",
"cursus",
"vitae",
"congue",
"mauris",
"rhoncus",
"aenean",
"vel",
"elit",
"scelerisque",
"mauris",
"pellentesque",
"pulvinar",
"pellentesque",
"habitant",
"morbi",
"tristique",
"senectus",
"et",
"netus",
"et",
"malesuada",
"fames",
"ac",
"turpis",
"egestas",
"maecenas",
"pharetra",
"convallis",
"posuere",
"morbi",
"leo",
"urna",
"molestie",
"at",
"elementum",
"eu",
"facilisis",
"sed",
"odio",
"morbi",
"quis",
"commodo",
"odio",
"aenean",
"sed",
"adipiscing",
"diam",
"donec",
"adipiscing",
"tristique",
"risus",
"nec",
"feugiat",
"in",
"fermentum",
"posuere",
"urna",
"nec",
"tincidunt",
"praesent",
"semper",
"feugiat",
"nibh",
"sed",
"pulvinar",
"proin",
"gravida",
"hendrerit",
"lectus",
"a",
"molestie",
"gravida",
"dictum"
];
// -- Extensions --
const kTabletBreakpoint = 720.0;
const kListWidth = 370.0;
const kDrawerWidth = 250.0;
bool stringExists(String val) {
return val != null && val.isNotEmpty;
}
Color get inactiveColor => Colors.grey[600];
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment