Writing Flutter Mobile Application with Meteor backend
By: Tanut Apiwong
Tags: Meteor, Flutter, Dart

Update: On July 1st, 2021, I published the 2.0.0 version of dart_meteor with the null-safety support and some API changes. Update: On May 1st, 2020, I published dart_meteor_web, the web version of the dart_meteor package which working with flutter web.

For the past few years, Meteor was a rising star in creating real-time web applications. However this day its popularity has dropped down. Many users have migrated to another modern framework. But the Meteor itself yet still working well for small and medium-size projects, and many of them are still in the production live on the Internet.

Meteor natively supports mobile application via its embedded Cordova framework. It plays well for a web developer who wants to have a mobile application as an extra of their work. Since Cordova is based on WebView, its look and feel are not seems like the native mobile application. If you want to leverage your mobile app you may consider creating your app with Google’s Flutter and yet still able to connect to your existing Meteor backend.

Reader Experience

Existing Meteor Web Application Experience

Starter Experience of Flutter Development

What we are going to implement?

First, we will create a simple chat app that has a login feature with Meteor and then we will create a Flutter app that talks with our Meteor server.

Make sure that the example able to run on your machine

Clone an example “simple-meteor-chat” project from my Github repository: https://github.com/tanutapi/simple-meteor-chat Open your shell and run “meteor npm install” and then “meteor”. Open your browser to see the example application is working.

You should see something like this in your browser at http://localhost:3000

There are two user accounts built in the application.

user1 and password1 user2 and password2

Try to login with any user and send some messages.

After sent Hello world! message.

Everything is working well. Now it’s time to create our Flutter application.

Creating a Flutter application

All the flutter source codes are available at https://github.com/tanutapi/flutter_meteor_dart_demo

With your favorite IDE, Visual Studio Code or Android Studio, create a new flutter project. Add dart_meteor as a dependency in pubspec.yaml file and do a flutter pub get command.

dependencies:

dart_meteor: ^2.0.0

Create a Meteor instance, Connecting Page, Login Page, and Chat Page.

In the main.dart , simply create an instance of Meteor in the global scope just before the main() function so that it can be used in other modules.

import 'package:dart_meteor/dart_meteor.dart';

... var meteor = MeteorClient.connect(url: 'http://localhost:3000'); void main() => runApp(MyApp());

...

Now let’s create a page that indicates that we are trying to make a connection to our Meteor server. The connecting_page.dart , This page does a simple thing, showing a circular progress indicator and a button that makes a connection retry immediately.

import 'package:flutter/material.dart';

import 'main.dart'; class ConnectingPage extends StatelessWidget {

@override

Widget build(BuildContext context) {

return Scaffold(

body: Container(

color: Colors.white,

child: Center(

child: Column(

mainAxisAlignment: MainAxisAlignment.center,

children: <Widget>[

CircularProgressIndicator(),

SizedBox(

height: 12.0,

),

Text('Connecting to the server...'),

SizedBox(

height: 12.0,

),

ElevatedButton(

child: Text('Retry to connect now!'),

onPressed: () {

meteor.reconnect();

},

)

],

),

),

),

);

}

}

Then in our MyApp widget ( main.dart ), we can detect that our Meteor client is connecting to the server by using a StreamBuilder with meteor.status(). Replace MyApp class with the following code.

import 'connecting_page.dart';

... class MyApp extends StatelessWidget {

@override

Widget build(BuildContext context) {

return MaterialApp(

title: 'Flutter Demo',

theme: ThemeData(

primarySwatch: Colors.blue,

),

home: StreamBuilder<DdpConnectionStatus>(

stream: meteor.status(),

builder: (context, snapshot) {

if (snapshot.hasData) {

if (snapshot.data != null && snapshot.data!.connected) {

return StreamBuilder<Map<String, dynamic>>(

stream: meteor.user(),

builder: (context, snapshot) {

if (snapshot.hasData) {

return ChatPage();

}

return LoginPage();

},

);

}

return ConnectingPage();

}

return Container();

}),

);

}

}

From the above code, if our DdpConnectionStatus was returned from the “meteor.status()” stream has a connected property equals to true then we will present a login-page or chat-page based on another StreamBuilder with “meteor.user()”, else we will present a connecting-page. Let’s break the code down to see it more clearly. First, we use a StreamBuilder to make a selection between connecting-page and other pages.

StreamBuilder<DdpConnectionStatus>(

stream: meteor.status(),

builder: (context, snapshot) {

if (snapshot.hasData) {

if (snapshot.data != null && snapshot.data!.connected) {

return ...

}

return ConnectingPage();

}

return Container();

}),

And another StreamBuilder to make a selection between chat-page and login-page .

... StreamBuilder<Map<String, dynamic>>(

stream: meteor.user(),

builder: (context, snapshot) {

if (snapshot.hasData) {

return ChatPage();

}

return LoginPage();

},

);

...

Do a method call

In our login page ( login_page.dart ), we will have two TextFields and one ElevatedButton. When users press on the button we will make login to our server.

meteor

.loginWithPassword(_username, _password)

.catchError((err) {

if (err is MeteorError) {

showDialog(

context: context,

builder: (context) {

return AlertDialog(

title: Text('Error'),

content: Text(err.details ?? "Error with no details."),

actions: <Widget>[

TextButton(

child: Text('Dismiss'),

onPressed: () {

Navigator.of(context).pop(0);

},

),

],

);

});

}

You must always catch an error when making a call to the server else if any error has been thrown your application will crash.

Also on the chat page ( chat_page.dart ), you have a delete-all-messages button. When users press that button you will make a method call to the server.

meteor.call('clearAllMessages').catchError((_) {});

Do a subscription

Now let’s see an example on the chat page that has a subscription logic.

import 'package:dart_meteor/dart_meteor.dart';

import 'package:flutter/material.dart';

import 'package:flutter_app/main.dart'; class ChatPage extends StatefulWidget {

@override

_ChatPageState createState() => _ChatPageState();

} class _ChatPageState extends State<ChatPage> {

final TextEditingController _textEditingControllerMessage =

TextEditingController();

SubscriptionHandler? _sub;

Map<String, dynamic>? _users; @override

void initState() {

super.initState();

_sub = meteor.subscribe('messages'); meteor.collection('users').listen((data) {

_users = data;

});

} @override

void dispose() {

_textEditingControllerMessage.dispose();

_sub.stop();

_sub = null;

super.dispose();

} @override

Widget build(BuildContext context) {

return ...;

}

}

and a StreamBuilder that used to create a list of chat messages.

StreamBuilder(

stream: meteor.collection('messages'),

builder:

(context, AsyncSnapshot<Map<String, dynamic>> snapshot) {

if (snapshot.hasData && snapshot.data != null) {

List<dynamic> messages = snapshot.data!.values.toList();

messages.sort((x, y) {

return x['createdAt'].compareTo(y['createdAt']);

});

if (messages.length > 0) {

return ListView.builder(

itemCount: messages.length,

itemBuilder: (context, idx) {

String from = messages[idx]['from'];

if (_users != null) {

from = _users![messages[idx]['from']]['username'];

}

return ListTile(

leading: Text(from),

title: Text(messages[idx]['msg']),

);

},

);

}

}

return ListTile(

title: Text('No chat message... Please send one.'),

);

This is how our Flutter application looks like.