Skip to content

Instantly share code, notes, and snippets.

@mkiisoft
Forked from slightfoot/backend.dart
Created August 16, 2018 19:17
Show Gist options
  • Save mkiisoft/e1a3d9e99029bc7a8750ec82e171b5fc to your computer and use it in GitHub Desktop.
Save mkiisoft/e1a3d9e99029bc7a8750ec82e171b5fc to your computer and use it in GitHub Desktop.
import 'model.dart';
typedef OnChatMessageCallback = void Function(int index, ChatMessage message);
abstract class ChatManager {
ChatManager();
ChatSession getNamedSession(String name);
}
abstract class ChatSession {
final String name;
ChatSession(this.name);
ChatMessage operator [](int index);
int get messageCount;
double get rating;
OnChatMessageCallback onMessageInserted;
OnChatMessageCallback onMessageRemoved;
void sendMessage(String text);
void start();
void close();
}
import 'dart:async';
import 'dart:math' show Random;
import 'backend.dart';
import 'model.dart';
class MockChatManager extends ChatManager {
Map<String, ChatSession> _cache = {};
@override
ChatSession getNamedSession(String name) {
var session = _cache[name];
if (session == null) {
session = MockChatSession(name);
_cache[name] = session;
}
return session;
}
}
class MockChatSession extends ChatSession {
final Random _random = Random();
static const _fakeText = <String>[
'Hi there, what\'s your name?',
'Welcome, %! What\'s your email?',
'%, got it! is it ok if we reach out.'
];
int _fakeCount = 0;
double _rating;
List<ChatMessage> _messages = <ChatMessage>[];
@override
ChatMessage operator [](int index) => _messages[index];
@override
int get messageCount => _messages.length;
@override
double get rating => double.parse(_rating.toStringAsFixed(1));
MockChatSession(String name) : super(name) {
_rating = _random.nextDouble() * 5.0;
}
@override
void start() {
_sendToServer(null);
}
@override
void close() {
// TODO: End session
}
@override
void sendMessage(String text) {
_insertMessage(ChatMessage.fromMyself(text));
_sendToServer(text);
}
void _insertMessage(ChatMessage message) {
_messages.insert(0, message);
if (onMessageInserted != null) {
onMessageInserted(0, message);
}
if (message.from == ChatMessageFrom.Myself && _messages.length >= 2) {
final item = _messages[1];
if (item.from == ChatMessageFrom.AutoReply) {
_messages.removeAt(1);
if (onMessageRemoved != null) {
onMessageRemoved(1, item);
}
}
}
}
void _sendToServer(String text) async {
if (text != null) {
await Future.delayed(Duration(seconds: _random.nextInt(3)));
}
if (_fakeCount < _fakeText.length) {
var response = _fakeText[_fakeCount];
if (text != null) {
response = response.replaceAll('%', text);
}
_insertMessage(ChatMessage.fromServer(response));
}
_fakeCount++;
if (_fakeCount == _fakeText.length) {
_insertMessage(ChatMessage.forAutoReply(['Yes', 'No']));
}
}
}
import 'package:flutter/material.dart';
import 'backend.dart';
import 'model.dart';
import 'widgets.dart';
class ChatScreen extends StatefulWidget {
static Route<dynamic> route(ChatSession session) {
return MaterialPageRoute(
builder: (_) => ChatScreen(session: session),
);
}
const ChatScreen({
Key key,
@required this.session,
}) : super(key: key);
final ChatSession session;
@override
_ChatScreenState createState() => _ChatScreenState();
}
class _ChatScreenState extends State<ChatScreen> {
ChatSession _session;
@override
void initState() {
super.initState();
_session = widget.session;
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (_session.messageCount == 0) {
WidgetsBinding.instance.addPostFrameCallback(
(_) => _session.start(),
);
}
}
@override
void dispose() {
_session.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppToolbar(
title: _session.name,
actions: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: RatingBar(
rating: _session.rating,
color: Colors.white,
),
),
],
),
body: DecoratedBox(
decoration: const BoxDecoration(color: Colors.white),
child: Stack(
alignment: Alignment.bottomLeft,
children: <Widget>[
DefaultTextStyle.merge(
style: const TextStyle(
fontSize: 16.0,
fontWeight: FontWeight.w500,
),
child: ChatList(
padding: const EdgeInsets.only(bottom: 72.0),
chatSession: _session,
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 24.0),
child: ChatEntryField(
sendMessage: _session.sendMessage,
),
),
],
),
),
);
}
}
class ChatList extends StatefulWidget {
const ChatList({
Key key,
@required this.chatSession,
this.padding,
}) : super(key: key);
final ChatSession chatSession;
final EdgeInsets padding;
@override
_ChatListState createState() => _ChatListState();
}
class _ChatListState extends State<ChatList> {
final GlobalKey<AnimatedListState> _listKey = GlobalKey();
@override
void initState() {
super.initState();
widget.chatSession.onMessageInserted = _onMessageInserted;
widget.chatSession.onMessageRemoved = _onMessageRemoved;
}
@override
void didUpdateWidget(ChatList old) {
super.didUpdateWidget(old);
if (old.chatSession != widget.chatSession) {
old.chatSession.onMessageInserted = null;
old.chatSession.onMessageRemoved = null;
widget.chatSession.onMessageInserted = _onMessageInserted;
widget.chatSession.onMessageRemoved = _onMessageRemoved;
}
}
@override
void dispose() {
widget.chatSession.onMessageInserted = null;
widget.chatSession.onMessageRemoved = null;
super.dispose();
}
void _onMessageInserted(int index, ChatMessage message) {
_listKey.currentState.insertItem(0, duration: const Duration(milliseconds: 750));
}
void _onMessageRemoved(int index, ChatMessage message) {
_listKey.currentState
.removeItem(index, _buildRemoveMessageBuilder(message), duration: const Duration(milliseconds: 750));
}
@override
Widget build(BuildContext context) {
return AnimatedList(
key: _listKey,
reverse: true,
padding: widget.padding,
initialItemCount: widget.chatSession.messageCount,
itemBuilder: _buildShowMessage,
);
}
Widget _buildShowMessage(BuildContext context, int index, Animation<double> animation) {
final message = widget.chatSession[index];
final sizeAnimation = CurvedAnimation(parent: animation, curve: const ElasticOutCurve(4.0));
final inAnimation = CurvedAnimation(parent: animation, curve: Curves.elasticOut);
return SizeTransition(
sizeFactor: sizeAnimation,
axisAlignment: -1.0,
child: ScaleTransition(
alignment: (message.from == ChatMessageFrom.Myself) ? Alignment.topRight : Alignment.topLeft,
scale: inAnimation,
child: FadeTransition(
opacity: inAnimation,
child: _buildMessage(context, message),
),
),
);
}
_buildRemoveMessageBuilder(ChatMessage message) {
return (BuildContext context, Animation<double> animation) {
final outAnimation = CurvedAnimation(parent: animation, curve: Curves.fastOutSlowIn);
return SizeTransition(
sizeFactor: outAnimation,
axisAlignment: 1.0,
child: ScaleTransition(
alignment: (message.from == ChatMessageFrom.Myself) ? Alignment.bottomRight : Alignment.bottomLeft,
scale: outAnimation,
child: FadeTransition(
opacity: outAnimation,
child: _buildMessage(context, message),
),
),
);
};
}
Widget _buildMessage(BuildContext context, ChatMessage message) {
final theme = Theme.of(context);
final radius = Radius.circular(24.0);
final myself = (message.from == ChatMessageFrom.Myself);
return Align(
alignment: myself ? Alignment.topRight : Alignment.topLeft,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0), // TODO
child: Builder(
builder: (BuildContext context) {
if (message.from == ChatMessageFrom.AutoReply) {
return Row(
children: message.replies
.map(
(text) => Padding(
padding: const EdgeInsetsDirectional.only(end: 8.0),
child: FlatButton(
onPressed: () => widget.chatSession.sendMessage(text),
textColor: theme.accentColor,
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 36.0),
shape: StadiumBorder(
side: BorderSide(color: Colors.blueGrey[200]),
),
child: Text(text),
),
),
)
.toList(growable: false),
);
} else {
return Container(
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 36.0),
decoration: BoxDecoration(
color: myself ? Colors.blue[100] : Colors.grey[200],
borderRadius: BorderRadius.only(
topLeft: myself ? radius : Radius.zero,
topRight: !myself ? radius : Radius.zero,
bottomLeft: radius,
bottomRight: radius,
),
),
child: Text(message.text),
);
}
},
),
),
);
}
}
class ChatEntryField extends StatefulWidget {
const ChatEntryField({
Key key,
@required this.sendMessage,
}) : super(key: key);
final ValueChanged<String> sendMessage;
@override
_ChatEntryFieldState createState() => _ChatEntryFieldState();
}
class _ChatEntryFieldState extends State<ChatEntryField> {
final TextEditingController _messageController = TextEditingController();
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Material(
elevation: 6.0,
child: SizedBox(
height: 48.0,
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Expanded(
child: TextField(
decoration: const InputDecoration(
contentPadding: const EdgeInsets.symmetric(vertical: 14.0, horizontal: 8.0),
border: InputBorder.none,
),
controller: _messageController,
onSubmitted: (_) => _sendMessage(),
),
),
IconButton(
color: theme.accentColor,
icon: Icon(Icons.arrow_upward),
onPressed: _sendMessage,
),
],
),
),
);
}
void _sendMessage() {
final text = _messageController.value.text;
_messageController.clear();
widget.sendMessage(text);
}
}
import 'package:flutter/material.dart';
import 'chat_screen.dart';
import 'providers.dart';
import 'widgets.dart';
class HomeScreen extends StatefulWidget {
@override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppToolbar(
title: 'Chat App',
),
body: ListView.separated(
itemCount: 6,
itemBuilder: (BuildContext context, int index) {
final name = 'Mock ${index + 1}';
final session = ChatProvider.of(context).getNamedSession(name);
return Material(
child: InkWell(
onTap: () => Navigator.of(context).push(ChatScreen.route(session)),
child: ListTile(
leading: CircleAvatar(
backgroundColor: Colors.amber[800],
child: Text(
session.rating.toString(),
style: const TextStyle(color: Colors.white),
),
),
title: Text(name),
subtitle: RatingBar(
rating: session.rating,
),
),
),
);
},
separatorBuilder: (BuildContext context, int index) => Divider(),
padding: const EdgeInsets.symmetric(vertical: 8.0),
),
);
}
}
import 'package:flutter/material.dart';
import 'backend_mock.dart';
import 'home_screen.dart';
import 'providers.dart';
void main() {
final manager = MockChatManager();
runApp(
ChatProvider(
manager: manager,
child: MaterialApp(
title: 'Chat App',
theme: ThemeData(
primaryColor: Colors.blue,
accentColor: Colors.blueAccent,
splashColor: Colors.blueAccent.withOpacity(0.3),
highlightColor: Colors.blueAccent.withOpacity(0.3),
),
home: HomeScreen(),
),
),
);
}
enum ChatMessageFrom {
Myself,
Server,
AutoReply,
}
class ChatMessage {
final ChatMessageFrom from;
final String text;
final List<String> replies;
const ChatMessage.forAutoReply(this.replies)
: this.from = ChatMessageFrom.AutoReply,
this.text = null;
const ChatMessage.fromServer(this.text)
: this.from = ChatMessageFrom.Server,
this.replies = null;
const ChatMessage.fromMyself(this.text)
: this.from = ChatMessageFrom.Myself,
this.replies = null;
}
import 'package:flutter/material.dart';
import 'backend.dart';
class ChatProvider extends InheritedWidget {
const ChatProvider({
Key key,
this.manager,
Widget child,
}) : super(key: key, child: child);
final ChatManager manager;
static ChatManager of(BuildContext context) {
ChatProvider provider = context.inheritFromWidgetOfExactType(ChatProvider);
return provider?.manager;
}
@override
bool updateShouldNotify(ChatProvider old) => old.manager != manager;
}
import 'package:flutter/material.dart';
class AppToolbar extends StatelessWidget implements PreferredSizeWidget {
const AppToolbar({
Key key,
this.leading,
@required this.title,
this.actions,
}) : super(key: key);
final Widget leading;
final String title;
final List<Widget> actions;
@override
Size get preferredSize => Size.fromHeight(kToolbarHeight);
@override
Widget build(BuildContext context) {
return Hero(
tag: 'app_bar',
child: AppBar(
leading: leading,
title: Text(title),
actions: actions,
),
);
}
}
class RatingBar extends StatelessWidget {
const RatingBar({
Key key,
@required this.rating,
this.color = Colors.amber,
}) : super(key: key);
final double rating;
final Color color;
@override
Widget build(BuildContext context) {
return Row(
children: List.generate(5, (int index) {
IconData icon = Icons.star_border;
if (index < rating.floor()) {
icon = Icons.star;
} else if (index + 0.5 <= rating) {
icon = Icons.star_half;
}
return Icon(icon, color: color);
}),
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment