Flutter client SDK for LiveKit (livekit_client on pub.dev), built on top of flutter_webrtc. Supported platforms: iOS, Android, macOS, Windows, Linux, Web (including WASM).
Requires Dart >= 3.6 / Flutter >= 3.27.
flutter pub get
flutter analyze # static analysis
flutter test # unit tests
dart format . --set-exit-if-changed # format (page width 120)
dart run import_sorter:main --no-comments --exit-if-changed # import order (CI-enforced)
dart run scripts/check_version.dart # version consistency (CI-enforced)
dart run build_runner build # json_serializable codegen
make proto # regen protobufs (needs ../protocol checkout)
dart compile js ./web/e2ee.worker.dart -o ./example/web/e2ee.worker.dart.js # E2EE web workerCI (build.yaml) runs all of the above plus example-app builds for every platform including web/WASM.
lib/livekit_client.dart— public API surface; implementation lives inlib/src/.lib/src/core/—Room,Engine,SignalClient,Transport;lib/src/participant/,lib/src/track/,lib/src/publication/— participants, tracks, publications;lib/src/events.dart+lib/src/managers/event.dart— event system.lib/src/proto/— protoc-generated code (from the siblingprotocolrepo); excluded from analysis/format, never edit by hand. Same goes for*.g.dartfiles (json_serializable output inlib/src/json/andlib/src/token_source/).lib/src/e2ee/+ top-levelweb/— E2EE; the web worker (web/e2ee.worker.dart) must be recompiled to JS (command above) when changed.example/— example app, built for all platforms in CI.test/— unit tests with mock infrastructure intest/mock/(mock peer connection, data channel, websocket;e2e_container.dartfor room-level tests).- Platform code in
android/,ios/,macos/,windows/,linux/,shared_cpp/,shared_swift/.
Web/native divergence is handled with conditional imports (e.g. track/processor_native.dart vs processor_web.dart) — new platform-specific code should follow that pattern.
flutter_webrtcis pinned to an exact version on purpose: livekit_client and flutter_webrtc must agree on the same WebRTC-SDK native pods, and mismatches break user builds (CocoaPods conflicts). Bump it only in sync with a matching WebRTC-SDK version.- iOS
AVAudioSessionhandling and reconnection-after-network-loss are the most frequent user-reported problem areas — change that code conservatively. - Don't hand-edit version numbers:
scripts/create_version.dartpropagates the version to.version,pubspec.yaml,README.md,lib/src/livekit.dart, and the podspecs;check_version.dartfails CI on mismatch.
Most regressions live in lib/src/core/ (room.dart, engine.dart, signal_client.dart) and participant/local.dart — connection lifecycle and async event plumbing. Recurring mistake classes:
- Un-awaited async state updates: callers observed half-initialized participants/publications because
updateFromInfo()/updateTrack()weren't awaited. Theunawaited_futures/discarded_futureslints exist for exactly this — writeunawaited(...)only as a deliberate choice. Never emit events from constructors or factories; return data and let the caller sequence emissions. - Iterating a collection across an
await: event handlers mutate participants/publications/listener lists mid-iteration (ConcurrentModificationError— one fix swept 5 files). Snapshot with.toList()before iterating; for queues, copy-then-clear before processing. - The reconnect/disconnect state machine runs on mutable flags (
_isClosed,_isReconnecting, ...) and has historically produced duplicate or missing lifecycle events (doubleDisconnectedEvent, flags not reset on re-connect). When touching engine/room event flow: useRoom.emitWhenConnected(...),createListener(synchronized: true), mind emit-vs-cleanup ordering, and add a regression test (seetest/core/room_e2e_test.dart). - Events or media tracks arriving before their dependent state exists: don't throw or drop — queue and flush (see
lib/src/core/pending_track_queue.dart, TTL'd and flushed on connect/participant updates). - Use-after-close on
StreamControllers crashed data streams: wrap controllers withisClosedguards (seeDataStreamController), and when erroring a stream out, also close it. - Don't rely on protobuf field defaults for state: republish-after-reconnect silently unmuted tracks because
muteddefaults tofalse. Pass prior state explicitly and reconcile local flags with server responses. - Duplicate in-flight async operations: cache the future (
_future ??= ...) and clear it on failure/disconnect (howensurePublisherConnectedis debounced), or uselib/src/support/reusable_completer.dart. - Cleanup: extend
Disposableand register teardown viaonDispose()(run LIFO, each guarded so one failing disposer can't abort the rest); create event listeners throughcreateListener()so they're cancelled with their owner. - Native boundary: methods in
Nativeswallow errors andlogger.warningby default — if a failure must propagate (e.g. a mode change the caller depends on), document why. Platform-channel crossings are expensive in hot paths; batch data instead of calling per-frame/per-sample.
- Lints:
package:lints/recommendedplus repo rules inanalysis_options.yaml— single quotes,prefer_final_locals,unawaited_futures,discarded_futures,avoid_print. dart formatwithpage_width: 120; imports sorted withimport_sorter.- Every source file carries the Apache-2.0 license header.
Every PR needs a changeset file in .changes/ (format: patch|minor|major type="fixed|added|changed|..." "description"); CI checks for it and runs dart-apitool against main to require a major changeset for breaking public-API changes. Releases are tag-driven (vX.Y.Z) and publish to pub.dev.