dart_meteor
该库可轻松实现 Meteor 后端与 Flutter 应用之间的连接。专为与 StreamBuilder 和 FutureBuilder 无缝配合而设计。
适用于 Dart/Flutter 开发者的 Meteor DDP 库。
^3.0.6^0.28.0^3.0.3^6.1.0^1.26.3^1.19.0以下为英文项目原文快照,最新内容请访问 GitHub。
https://github.com/tanutapi/dart_meteor/workflows/Testing/badge.svg?branch=master
Connect your Flutter app to a Meteor backend over DDP.
Designed to work seamlessly with StreamBuilder and FutureBuilder.
Future-based resultsStreamsDateTime values are converted to/from EJSON $date automaticallyAdd the package to your pubspec.yaml:
dependencies:
dart_meteor: ^4.1.0
Create a single MeteorClient instance in your app's global scope so you can
use it anywhere in your project. The client connects immediately and keeps the
connection alive:
import 'package:flutter/material.dart';
import 'package:dart_meteor/dart_meteor.dart';
final meteor = MeteorClient.connect(url: 'https://yourdomain.com');
void main() => runApp(MyApp());
The url may be https://… or wss://…; the client appends the /websocket
DDP endpoint for you.
Then build widgets from the client's streams:
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('dart_meteor example')),
body: Column(
children: [
// Show the live connection status.
StreamBuilder<DdpConnectionStatus>(
stream: meteor.status(),
builder: (context, snapshot) {
if (!snapshot.hasData) return const Text('Status: ---');
return Text('Status: ${snapshot.data}');
},
),
// Show a login/logout button depending on the current user.
StreamBuilder<String?>(
stream: meteor.userId(),
builder: (context, snapshot) {
if (snapshot.data != null) {
return ElevatedButton(
onPressed: () => meteor.logout(),
child: const Text('Logout'),
);
}
return ElevatedButton(
onPressed: () =>
meteor.loginWithPassword('username', 'password'),
child: const Text('Login'),
);
},
),
],
),
),
);
}
}
A complete runnable app is in /example: a Flutter chat client
(iOS, Android and Web) that connects to the live demo server at
https://simple-meteor-chat.tanutapi.dev and exercises login, subscriptions,
collections and method calls. There is also a longer walk-through covering
connection status, authentication, and subscriptions in this Medium post.
meteor.call() returns a Future. Always handle errors — an unhandled
MeteorError will otherwise crash your app:
try {
final result = await meteor.call('sumMethod', args: [5, 10]);
print('Answer is $result'); // 15
} on MeteorError catch (err) {
print('${err.error}: ${err.reason}');
}
Arguments are optional — meteor.call('helloMethod') works too. A DateTime
anywhere in the arguments or the result is converted to/from Meteor's EJSON
date format automatically.
Method calls also fit naturally into a FutureBuilder:
FutureBuilder<dynamic>(
future: meteor.call('sumMethod', args: [5, 10]),
builder: (context, snapshot) {
if (snapshot.hasError) return Text('Error: ${snapshot.error}');
if (!snapshot.hasData) return const CircularProgressIndicator();
return Text('Answer is: ${snapshot.data}');
},
),
Subscribe to a publication on the server, and read the documents it publishes
through meteor.collection():
class YourWidget extends StatefulWidget {
const YourWidget({super.key});
@override
State<YourWidget> createState() => _YourWidgetState();
}
class _YourWidgetState extends State<YourWidget> {
late SubscriptionHandler _subscription;
@override
void initState() {
super.initState();
_subscription = meteor.subscribe('your_pub', args: ['param_1', 'param_2']);
}
@override
void dispose() {
_subscription.stop();
super.dispose();
}
@override
Widget build(BuildContext context) {
return StreamBuilder<Map<String, dynamic>>(
stream: meteor.collection('your_collection'),
builder: (context, snapshot) {
final docCount = snapshot.data?.length ?? 0;
return Text('Total document count: $docCount');
},
);
}
}
Details worth knowing:
meteor.subscribe() returns a SubscriptionHandler with stop() and a
ready() stream that emits true once the server has sent the initial
batch of documents. Optional onReady and onStop callbacks are also
supported. Subscriptions are re-established automatically after a reconnect.meteor.collection() returns a stream backed by an rxdart
BehaviorSubject: every new listener immediately receives the latest value,
so a StreamBuilder starts with snapshot.hasData == true and an empty map
before any documents arrive.Map<String, dynamic> keyed by document _id, with
the whole document as the value:{
"DGbsysgxzSf7Cr8Jg": {
"_id": "DGbsysgxzSf7Cr8Jg",
"field1": 0,
"field2": "a",
"field3": true,
"field4": "2020-08-30T16:15:57.000Z" // delivered as a Dart DateTime
}
}
There is no minimongo on the client. Use plain Dart collection operations
(where, map, reduce, …) to query the map — they cover the same ground as
minimongo queries in the Meteor web client.
Since the collection is a map keyed by _id, a lookup is just an index
operation:
// Non-reactive read of a document by its id.
final doc = meteor.collectionCurrentValue('your_collection_name')?['DGbsysgxzSf7Cr8Jg'];
if (doc != null) {
// do something
}
// The same works for users.
final user = meteor.collectionCurrentValue('users')?['Sf7Cr8JgDGbsysgxz'];
When you only need the latest value for a condition check — not a reactive rebuild — every major stream has a non-reactive counterpart:
| Reactive stream | Current value |
|---|---|
meteor.collection(name) |
meteor.collectionCurrentValue(name) |
meteor.user() |
meteor.userCurrentValue() |
meteor.userId() |
meteor.userIdCurrentValue() |
// Log in (works with a username or an email address; the password is sent
// as a SHA-256 digest, never in plain text).
final result = await meteor.loginWithPassword('user_or_email', 'password');
// Resume a session with a saved token, e.g. after an app restart.
await meteor.loginWithToken(token: result.token, tokenExpires: result.tokenExpires);
// Log out.
await meteor.logout();
Related APIs: meteor.user(), meteor.userId(), meteor.loggingIn(), and
meteor.logInStatus() are reactive streams of the current account state;
logoutOtherClients(), changePassword(), forgotPassword(), and
resetPassword() cover the rest of the standard accounts flows. After a
reconnect the client re-authenticates automatically using its stored token.
meteor.status(); // Stream<DdpConnectionStatus>: connected/connecting/failed/waiting/offline
meteor.reconnect(); // force a reconnection attempt if not connected
meteor.disconnect(); // close the connection and stop reconnecting
While connected, the client exchanges DDP ping/pong with the server and
reconnects when the connection is considered dead, backing off between
attempts (0s, 5s, 10s, … up to maxRetryInterval) so an unreachable server
does not keep the radio busy. disconnect() is final: the client stays offline
until you call reconnect().
The timings are configurable if the defaults do not suit your server:
final meteor = MeteorClient.connect(
url: 'https://yourdomain.com',
pingInterval: const Duration(seconds: 20),
pongTimeout: const Duration(seconds: 5),
maxRetryInterval: const Duration(seconds: 30),
stalenessThreshold: const Duration(seconds: 25),
);
When a phone sleeps, the OS suspends the process: Dart timers stop firing, and the server can drop the session without the socket ever reporting an error. The app then wakes up believing it is still connected, and stays that way until the next ping happens to time out.
dart_meteor is a pure Dart package, so it does not watch Flutter's lifecycle
itself. Forward it from a WidgetsBindingObserver — this is the whole
integration:
class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
meteor.notifyAppResumed();
} else {
meteor.notifyAppPaused();
}
}
}
On resume the client measures by wall clock how long it was actually away
rather than trusting its timers. If the connection has been silent longer than
stalenessThreshold it is torn down and replaced immediately, re-resuming the
login and re-subscribing. While paused, the client will not tear down a
connection just because a timer fired late.
meteor.checkLiveness() runs the same check on demand — useful if your app
learns from somewhere else (a connectivity plugin, say) that the network may
have changed.
The example app wires this up in lib/main.dart.
Server-side Meteor.Errors are thrown as MeteorError, which exposes
error, reason, message, details, errorType, and isClientSafe — the
same fields you get in a Meteor web client.
A call that was still in flight when the connection dropped — because the
device slept, or the network went away — throws MeteorConnectionError
instead. The two are worth distinguishing: MeteorError means the server
considered the request and said no, while MeteorConnectionError means you
never heard back and the method may or may not have run.
try {
await meteor.call('sendMessage', args: ['hello']);
} on MeteorError catch (err) {
// The server rejected it.
} on MeteorConnectionError catch (err) {
// Never got a reply — offer a retry.
}
Calls are not resent automatically after a reconnect: a method like
sendMessage is not safe to run twice, so whether to retry is left to you.
See CHANGELOG.md for the full history. The notable breaking changes:
MeteorConnectionError instead of hanging forever, so await meteor.call(…)
can now throw where it previously never returned. And reconnect attempts now
back off instead of retrying immediately.web_socket_channel. The DdpClient.PING_SEC_INTERVAL and
DdpClient.PONG_WITHIN_SEC fields were renamed to the static constants
DdpClient.pingIntervalSeconds and DdpClient.pongTimeoutSeconds.meteor.collection() streams start with snapshot.hasData == true and an empty map instead of no data.meteor.call('method', args: [...]), meteor.subscribe('pub', args: [...])
(both optional). prepareCollection() is no longer needed — just call
meteor.collection(). DateTime values are supported directly.Please file feature requests and bugs at the issue tracker.