Minimalist Flutter Chat
This document considers that you have followed the previous Installing Components document.
At the end of the previous document, you should have copied example_simple_chat.dart in the main.dart file and run the application.
cp ./lib/example_simple_chat.dart ./lib/main.dart
flutter run
After creating an account, you should see the following screen:
If you copy this program in different folders or different devices on your local network, you should be able to discuss between the different instances.
This document does not explain how to create the Flutter screens but focus on the code that is specific to the Discret API. For simplicity reasons error handling is very basic. A real application would need to have better error handling mechanisms.
Preparing the API
The Discret API requires three parameters:
- a data model: defines the kind of data that can be used in Discret,
- an application unique identifier: once the application in production, this identifier cannot be changed anymore. It is used to derive some secrets that will be used by Discret. If this identifier is changed, the secrets will change and users will not be able to connect anymore.
- a data folder: where data is stored.
The beginning of the file initialize Discret:
8 //...
9
10
11 // the application unique identifier
12 // ignore: constant_identifier_names
13 const String APPLICATION_KEY = "github.com/discretlib/flutter_example_simple_chat";
14 void main() async {
15 // does not work on smartphone,
16 // example_advanced_chat.dart provides a version compatible with smartphone
17 String dataPath = '.discret_data';
18 String datamodel = """chat {
19 Message {
20 content: String
21 }
22 }""";
23 await Discret().initialize(APPLICATION_KEY, dataPath, datamodel);
24
25 runApp(
26 const ChatApp(),
27 );
28 }
29
30 //
31 // This class contains every API calls.
32 //
33 class ChatState extends ChangeNotifier {
34 //...
Lines 9 and 10 import the two packages of the API.
Line 13 defines the application unique identifier:
12 //...
13 const String APPLICATION_KEY = "github.com/discretlib/flutter_example_simple_chat";
14 //...
Lines 18 to 22 define the data model used by this application. It creates a namespace chat that contains an Entity names Message, which contains an unique field named content of the String type.
You can learn more about data models in the documentation Schema and Entities.
17 //...
18 String datamodel = """chat {
19 Message{
20 content:String
21 }
22 }""";
23 //...
One the API initialized, The application will start and instantiate the ChatState class that contains every functions that calls the Discret API
The ResultMsg class
You will see in the rest of the document that most of the API calls returns an ResultMsg object. It contains the following fields.
class ResultMsg{
int id,
bool successful,
String error,
String data,
}
- id is a technical field not useful to the user,
- successful indicate whether the API call was a success or not,
- error contains the error message, if any;
- data contains the result of the API call.
Creating an Account and connecting
Creating an account and connecting to the application uses the same API call.
47 Future<ResultMsg> login(String userName, String password, bool create) async {
48 ResultMsg msg = await Discret().login(userName, password, create);
49 if (msg.successful) {
50 await initialise();
51 }
52 return msg;
53 }
The difference between an account creation and a connection is the create value:
- false: connect if the account exists and return an error otherwise,
- true: create a new account if it does not exists and return an error if it already exists.
If the connection is successful, the initialise() function is called. The Discret library only starts after the authentication, so we have to wait for a successful to finalise ChatState initialisation.
ChatState Initialization
The initialise function is the following:
58 Future<void> initialise() async {
59 ResultMsg msg = await Discret().privateRoom();
60 privateRoom = msg.data;
61
62 eventlistener = Discret().eventStream().listen((event) async {
63 switch (event.type) {
64 case EventType.dataChanged:
65 await refreshChat();
66
67 default:
68 }
69 }, onDone: () {}, onError: (error) {});
70
71 refreshChat();
72 }
Line 60 retrieves the user's private Room. Rooms are the access rights system used to synchronized data with other peers. Data inserted in a Room is synchronized only with peers that have access rights to it.
In this example we will only use the user's private Room, meaning that only him will able to access the data. If this user's is connected on several devices, data will be synchronized between devices.
You can learn more about access right in the Room section of the documentation.
Starting ligne 62, we will listen to the Discret event stream. We are only interested in the EventType.dataChanged event that is triggered when Discret detect that data have been modified.
In this example, this event is triggered when:
- you are sending a message from this instance
- when data from other instances are synchronized with this one
When this event is detected, we call the refreshChat() function that will read the new data.
You can learn more about the different types of events in the System Events section of the documentation.
Once the initialisation done, a first call to refreshChat() is performed to read messages that could have been inserted during a previous session.
Read Messages: the refreshChat() function
The function used to read messages is the following:
77 Future<void> refreshChat() async {
78 String query = """query {
79 res: chat.Message(
80 order_by(mdate asc, id asc),
81 after(\$mdate, \$id),
82 room_id = \$room_id
83 ) {
84 id
85 mdate
86 content
87 }
88 }""";
89 ResultMsg res = await Discret().query(
90 query, {"mdate": lastEntryDate, "id": lastId, "room_id": privateRoom});
91
92 if (!res.successful) {
93 print(res.error);
94 } else {
95 final json = jsonDecode(res.data) as Map<String, dynamic>;
96 final msgArray = json["res"] as List<dynamic>;
97
98 for (var msgObject in msgArray) {
99 var message = msgObject as Map<String, dynamic>;
100 var entry =
101 ChatEntry(message["id"], message["mdate"], message["content"]);
102 lastEntryDate = entry.date;
103 lastId = entry.id;
104
105 chat.add(entry);
106 }
107 notifyListeners();
108 }
109 }
The function uses a query to read data from the discret database. You can learn more about reading database data in the Query section of the documentation.
You can notice that the query defines three parameters:
- $mdate
- $id
- $room_id
that are passed during the API call:
ResultMsg res = await Discret().query(
query, {
"mdate": lastEntryDate,
"id": lastId,
"room_id": privateRoom
}
);
Sending a message
The function used to write messages is the following:
113 Future<void> sendMessage() async {
114 if (chatController.text != "") {
115 String query = """mutate {
116 res : chat.Message{
117 room_id: \$room_id
118 content: \$content
119 }
120 }""";
121
122 ResultMsg res = await Discret().mutate(
123 query, {"room_id": privateRoom, "content": chatController.text});
124
125 if (!res.successful) {
126 print(res.error);
127 } else {
128 chatController.clear();
129 }
130 }
131 }
It used a mutation query to insert a new tuple of the chat.Message entity defined in the data model.
You can notice that the room_id has not been manually defined in the data model. It is a system field available for every entities. $room_id and $message are query parameters that are passed by the Some(params) object.
$room_id et $message sont are two query parameter that are passed during the API call:
ResultMsg res = await Discret().mutate(
query, {
"room_id": privateRoom,
"content": chatController.text
}
);
You can lean more about data insertion and modification in the Mutations and Deletions section of the documentation.