-
-
Save mkiisoft/e1a3d9e99029bc7a8750ec82e171b5fc to your computer and use it in GitHub Desktop.
Mock Chat App - Results of the #HumpDayQandA - Wednesday 15 August 2018 https://discourse-cdn-sjc1.com/business6/uploads/unbounce/original/2X/3/384cb074e5a7aa68d90224be5dc75f390d31647d.gif
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 '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(); | |
} |
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: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'])); | |
} | |
} | |
} |
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 '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); | |
} | |
} |
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 '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), | |
), | |
); | |
} | |
} |
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 '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(), | |
), | |
), | |
); | |
} |
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
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; | |
} |
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 '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; | |
} |
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 '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