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:

The beginning of the file initialize Discret:

8//...
9import './messages/discret.pb.dart';
10import 'discret.dart';
11// the application unique identifier
12// ignore: constant_identifier_names
13const String APPLICATION_KEY = "github.com/discretlib/flutter_example_simple_chat";
14void 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//
33class ChatState extends ChangeNotifier {
34//...

Lines 9 and 10 import the two packages of the API.

Line 13 defines the application unique identifier:

12//...
13const 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,
}

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:

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:

58Future<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:

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:

77Future<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:

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:

113Future<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.