• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Duit-Foundation / flutter_duit / 19115039532

05 Nov 2025 08:15PM UTC coverage: 86.358% (+8.9%) from 77.409%
19115039532

push

github

web-flow
major: flutter_duit v4 (#310)

2151 of 2405 new or added lines in 109 files covered. (89.44%)

36 existing lines in 7 files now uncovered.

3773 of 4369 relevant lines covered (86.36%)

35.87 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

63.58
/lib/src/duit_impl/driver.dart
1
import "dart:async";
2

3
import "package:duit_kernel/duit_kernel.dart";
4
import "package:flutter/material.dart";
5
import "package:flutter/services.dart";
6
import "package:flutter_duit/flutter_duit.dart";
7
import "package:flutter_duit/src/duit_impl/hooks.dart";
8
import "package:flutter_duit/src/ui/index.dart";
9
import "package:flutter_duit/src/view_manager/index.dart";
10
import "package:flutter_duit/src/transport/index.dart";
11

12
final class DuitDriver with DriverHooks implements UIDriver {
13
  @protected
14
  @override
15
  final String source;
16

17
  @override
18
  Transport? transport;
19

20
  @protected
21
  @override
22
  TransportOptions transportOptions;
23

24
  @protected
25
  @override
26
  late BuildContext buildContext;
27

28
  @override
316✔
29
  set context(BuildContext value) {
30
    buildContext = value;
316✔
31
  }
32

33
  final _eventStreamController = StreamController<UIDriverEvent>.broadcast();
34

35
  @override
316✔
36
  Stream<UIDriverEvent> get eventStream => _eventStreamController.stream;
632✔
37

38
  @override
39
  ExternalEventHandler? externalEventHandler;
40

41
  @override
42
  ScriptRunner? scriptRunner;
43

44
  @protected
45
  final Map<String, dynamic>? initialRequestPayload;
46

47
  late final bool _useStaticContent;
48
  bool _isChannelInitialized = false, _isDriverInitialized = false;
49

50
  late final Map<String, dynamic>? content;
51

52
  @override
53
  late final ActionExecutor actionExecutor;
54

55
  @override
56
  late final EventResolver eventResolver;
57

58
  @override
59
  MethodChannel? driverChannel;
60

61
  @override
62
  late final bool isModule;
63

64
  @override
65
  DebugLogger? logger;
66

67
  late ViewManager _viewManager;
68

69
  final _dataSources = <int, StreamSubscription<ServerEvent>>{};
70

UNCOV
71
  DuitDriver(
×
72
    this.source, {
73
    required this.transportOptions,
74
    this.externalEventHandler,
75
    this.initialRequestPayload,
76
    this.logger,
77
    EventResolver? customEventResolver,
78
    ActionExecutor? customActionExecutor,
79
    DebugLogger? customLogger,
80
    bool shared = false,
81
  }) {
82
    logger = customLogger ?? DefaultLogger.instance;
×
83

84
    _useStaticContent = false;
×
85
    actionExecutor = customActionExecutor ??
×
86
        DefaultActionExecutor(
×
87
          driver: this,
88
          logger: logger,
×
89
        );
90
    eventResolver = customEventResolver ??
×
91
        DefaultEventResolver(
×
92
          driver: this,
93
          logger: logger,
×
94
        );
95
    isModule = false;
×
96
    _viewManager = shared ? MultiViewManager() : SimpleViewManager();
×
97
  }
98

99
  /// Creates a new instance of [DuitDriver] with the specified [content] without establishing a initial transport connection.
100
  DuitDriver.static(
316✔
101
    this.content, {
102
    required this.transportOptions,
103
    this.externalEventHandler,
104
    this.logger,
105
    EventResolver? customEventResolver,
106
    ActionExecutor? customActionExecutor,
107
    DebugLogger? customLogger,
108
    this.source = "",
109
    this.initialRequestPayload,
110
    bool shared = false,
111
  }) {
112
    logger = customLogger ?? DefaultLogger.instance;
632✔
113

114
    _useStaticContent = true;
316✔
115
    isModule = false;
316✔
116
    eventResolver = customEventResolver ??
316✔
117
        DefaultEventResolver(
316✔
118
          driver: this,
119
          logger: logger,
316✔
120
        );
121
    actionExecutor = customActionExecutor ??
316✔
122
        DefaultActionExecutor(
316✔
123
          driver: this,
124
          logger: logger,
316✔
125
        );
126
    _viewManager = shared ? MultiViewManager() : SimpleViewManager();
636✔
127
  }
128

129
  /// Creates a new [DuitDriver] instance that is controlled from native code
130
  DuitDriver.module()
×
131
      : _useStaticContent = false,
132
        source = "",
133
        initialRequestPayload = null,
134
        isModule = true,
135
        externalEventHandler = null,
136
        transportOptions = EmptyTransportOptions(),
×
137
        driverChannel = const MethodChannel("duit:driver"),
138
        _viewManager = SimpleViewManager();
×
139

140
  @protected
316✔
141
  @override
142
  void attachController(String id, UIElementController controller) =>
143
      _viewManager.addController(id, controller);
632✔
144

145
  @protected
316✔
146
  @override
147
  void detachController(String id) =>
148
      _viewManager.removeController(id)?.dispose();
948✔
149

150
  @protected
4✔
151
  @override
152
  UIElementController? getController(String id) =>
153
      _viewManager.getController(id);
8✔
154

155
  Future<Map<String, dynamic>> _connect() async {
316✔
156
    Map<String, dynamic>? json;
157

158
    try {
159
      if (_useStaticContent) {
316✔
160
        assert(content != null && content!.isNotEmpty);
948✔
161
        json = content!;
316✔
162
      } else {
163
        json = await transport?.connect(
×
164
          initialData: initialRequestPayload,
×
165
        );
166
      }
167
    } catch (e, s) {
168
      logger?.error(
×
169
        "Failed conneting to server",
170
        error: e,
171
        stackTrace: s,
172
      );
NEW
173
      _eventStreamController.sink.addError(e);
×
174
    }
175

176
    if (transport is Streamer) {
632✔
177
      final streamer = transport as Streamer;
×
178
      streamer.eventStream.listen(
×
179
        (d) async {
×
180
          try {
181
            if (buildContext.mounted) {
×
182
              await eventResolver.resolveEvent(buildContext, d);
×
183
            }
184
          } catch (e, s) {
185
            logger?.error(
×
186
              "Error while processing event from transport stream",
187
              error: e,
188
              stackTrace: s,
189
            );
NEW
190
            _eventStreamController.sink.addError(e);
×
191
          }
192
        },
193
      );
194
    }
195

196
    return json ?? {};
×
197
  }
198

199
  @override
316✔
200
  Future<void> init() async {
201
    if (!_isDriverInitialized) {
316✔
202
      _isDriverInitialized = true;
316✔
203
    } else {
204
      return;
205
    }
206

207
    _viewManager.driver = this;
632✔
208
    _addParsers();
316✔
209

210
    onInit?.call();
316✔
211

212
    if (isModule && !_isChannelInitialized) {
316✔
213
      await _initMethodChannel();
×
214
    }
215

216
    transport ??= _getTransport(
628✔
217
      transportOptions.type,
624✔
218
    );
219

220
    await scriptRunner?.initWithTransport(transport!);
316✔
221

222
    final json = await _connect();
316✔
223

224
    try {
225
      final view = await _viewManager.prepareLayout(json);
632✔
226

227
      if (view != null) {
228
        _eventStreamController.sink.add(
948✔
229
          UIDriverViewEvent(view),
316✔
230
        );
231
      } else {
232
        final err = FormatException(
4✔
233
            "Invalid layout structure. Received map keys: ${json.keys}");
8✔
234
        throw err;
235
      }
236
    } catch (e, s) {
237
      logger?.error(
8✔
238
        "Layout parse failed",
239
        error: e,
240
        stackTrace: s,
241
      );
242
      _eventStreamController.addError(
8✔
243
        UIDriverErrorEvent(
4✔
244
          "Layout parse failed",
245
          error: e,
246
          stackTrace: s,
247
        ),
248
      );
249
    }
250
  }
251

252
  @override
4✔
253
  void dispose() {
254
    onDispose?.call();
4✔
255
    transport?.dispose();
8✔
256
    _eventStreamController.close();
8✔
257
    for (final subscription in _dataSources.values) {
8✔
NEW
258
      subscription.cancel();
×
259
    }
260
    _dataSources.clear();
8✔
261
  }
262

263
  @override
×
264
  Widget? build() {
265
    return _viewManager.build();
×
266
  }
267

268
  void _addParsers() {
316✔
269
    try {
270
      ServerAction.setActionParser(const DefaultActionParser());
316✔
271
      ServerEvent.eventParser = const DefaultEventParser();
316✔
272
    } catch (e) {
273
      //Safely handle the case of assigning parsers during
274
      //multiple driver initializations as part of running tests
275
      logger?.warn(
×
276
        e.toString(),
×
277
      );
278
    }
279
  }
280

281
  @override
36✔
282
  Future<void> execute(ServerAction action) async {
283
    beforeActionCallback?.call(action);
36✔
284

285
    try {
286
      final event = await actionExecutor.executeAction(
72✔
287
        action,
288
      );
289

290
      if (event != null && buildContext.mounted) {
72✔
291
        eventResolver.resolveEvent(buildContext, event);
108✔
292
      }
293
    } catch (e) {
294
      logger?.error(
×
295
        "Error executing action",
296
        error: e,
297
      );
298
    } finally {
299
      afterActionCallback?.call();
36✔
300
    }
301
  }
302

303
  Future<void> _resolveComponentUpdate(
4✔
304
    UIElementController controller,
305
    Map<String, dynamic> json,
306
  ) async {
307
    final tag = controller.tag;
4✔
308
    final description = DuitRegistry.getComponentDescription(tag!);
4✔
309

310
    if (description != null) {
311
      final component = ComponentBuilder.build(
4✔
312
        description,
313
        json,
314
      );
315

316
      controller.updateState(component);
4✔
317
    }
318
  }
319

320
  @protected
×
321
  @override
NEW
322
  Future<void> evalScript(String source) async => scriptRunner?.eval(source);
×
323

324
  /// Returns a transport based on the specified transport type.
325
  ///
326
  /// This method is used internally to create and return a transport object based
327
  /// on the specified [type]. It switches on the [type] parameter and returns an
328
  /// instance of the corresponding transport class.
329
  ///
330
  /// Parameters:
331
  /// - [type]: The transport type.
332
  ///
333
  /// Returns:
334
  /// - An instance of the transport class based on the specified [type].
335
  /// - If the [type] is not recognized, it returns an instance of [HttpTransport].
336
  Transport _getTransport(String type) {
312✔
337
    if (isModule) {
312✔
338
      return NativeTransport(this);
×
339
    }
340

341
    switch (type) {
342
      case TransportType.http:
312✔
343
        {
344
          return HttpTransport(
24✔
345
            source,
24✔
346
            options: transportOptions as HttpTransportOptions,
24✔
347
          );
348
        }
349
      case TransportType.ws:
304✔
350
        {
351
          return WSTransport(
×
352
            source,
×
353
            options: transportOptions as WebSocketTransportOptions,
×
354
          );
355
        }
356
      default:
357
        {
358
          return EmptyTransport();
304✔
359
        }
360
    }
361
  }
362

363
  /// Initializes the driver as a module.
364
  Future<void> _initMethodChannel() async {
×
365
    driverChannel?.setMethodCallHandler((call) async {
×
366
      switch (call.method) {
×
367
        case "duit_event":
×
368
          await eventResolver.resolveEvent(
×
369
            buildContext,
×
370
            call.arguments as Map<String, dynamic>,
×
371
          );
372
          break;
373
        case "duit_layout":
×
374
          final json = call.arguments as Map<String, dynamic>;
×
375
          final view = await _viewManager.prepareLayout(json);
×
376
          if (view != null) {
NEW
377
            _eventStreamController.sink.add(
×
378
              UIDriverViewEvent(view),
×
379
            );
380
          }
381
          break;
382
        default:
383
          break;
384
      }
385
    });
386
    _isChannelInitialized = true;
×
387
  }
388

389
  @visibleForTesting
212✔
390
  Future<void> updateTestAttributes(
391
    String id,
392
    Map<String, dynamic> json,
393
  ) =>
394
      updateAttributes(
212✔
395
        id,
396
        json,
397
      );
398

399
  @visibleForTesting
×
400
  Future<void> executeTestAction(ServerAction action) async {
401
    await execute(action);
×
402
  }
403

404
  @visibleForTesting
×
405
  Future<void> resolveTestEvent(dynamic eventData) async {
406
    await eventResolver.resolveEvent(buildContext, eventData);
×
407
  }
408

409
  @visibleForTesting
8✔
410
  int get controllersCount => _viewManager.controllersCount;
16✔
411

412
  @override
12✔
413
  Map<String, dynamic> preparePayload(
414
    Iterable<ActionDependency> dependencies,
415
  ) {
416
    final Map<String, dynamic> payload = {};
12✔
417

418
    if (dependencies.isNotEmpty) {
12✔
419
      for (final dependency in dependencies) {
8✔
420
        final controller = _viewManager.getController(dependency.id);
12✔
421
        if (controller != null) {
422
          final attribute = controller.attributes.payload;
8✔
423
          payload[dependency.target] = attribute["value"];
12✔
424
        }
425
      }
426
    }
427

428
    return payload;
429
  }
430

431
  @override
240✔
432
  Future<void> updateAttributes(
433
    String controllerId,
434
    Map<String, dynamic> json,
435
  ) async {
436
    final controller = _viewManager.getController(controllerId);
480✔
437
    if (controller != null) {
438
      if (controller.type == ElementType.component.name) {
720✔
439
        await _resolveComponentUpdate(controller, json);
4✔
440
        return;
441
      }
442
      controller.updateState(json);
236✔
443
    }
444
  }
445

446
  @override
316✔
447
  void notifyWidgetDisplayStateChanged(
448
    String viewTag,
449
    int state,
450
  ) {
451
    _viewManager.notifyWidgetDisplayStateChanged(viewTag, state);
632✔
452
    logger?.info(
632✔
453
      "Widget with tag ${viewTag.isEmpty ? "*root*" : viewTag} state changed to $state",
632✔
454
    );
455
  }
456

457
  @override
×
458
  bool isWidgetReady(String viewTag) {
459
    return _viewManager.isWidgetReady(viewTag);
×
460
  }
461

462
  /// Adds an event stream to be listened to and processed by the driver.
463
  ///
464
  /// Each element of the stream must be a Map<String, dynamic>, which will be converted into a [ServerEvent].
465
  /// If the event is a [NullEvent], a [NullEventException] will be thrown.
466
  /// For all other events, [eventResolver.resolveEvent] is called with the current [buildContext].
467
  ///
468
  /// The stream subscription is stored in the internal [_dataSources] list for lifecycle management.
469
  ///
470
  /// [stream] - the stream of events coming from the server or another data source.
471
  void addExternalEventStream(
4✔
472
    Stream<Map<String, dynamic>> stream,
473
  ) {
474
    final id = DateTime.now().millisecondsSinceEpoch;
8✔
475

476
    void cancelSub() {
4✔
477
      _dataSources.remove(id);
8✔
478
    }
479

480
    final sub = stream.map(ServerEvent.parseEvent).listen(
8✔
481
      (event) {
4✔
482
        if (event is NullEvent) {
4✔
483
          throw const NullEventException("NullEvent received from data source");
484
        } else {
485
          eventResolver.resolveEvent(
8✔
486
            // ignore: use_build_context_synchronously
487
            buildContext,
4✔
488
            event,
489
          );
490
        }
491
      },
492
      onDone: cancelSub,
NEW
493
      onError: (e, s) => cancelSub(),
×
494
    );
495

496
    _dataSources[id] = sub;
8✔
497
  }
498
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc