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

Duit-Foundation / flutter_duit / 19737815617

27 Nov 2025 01:26PM UTC coverage: 90.789% (+2.1%) from 88.646%
19737815617

push

github

web-flow
chore: Update dependencies and improve DuitDriver visibility for testing (#319)

88 of 104 new or added lines in 18 files covered. (84.62%)

2 existing lines in 1 file now uncovered.

4268 of 4701 relevant lines covered (90.79%)

36.09 hits per line

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

80.52
/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
  @visibleForTesting
14
  @override
15
  final String source;
16

17
  @visibleForTesting
18
  @override
19
  Transport? transport;
20

21
  @visibleForTesting
22
  @override
23
  TransportOptions transportOptions;
24

25
  @visibleForTesting
26
  @override
27
  late BuildContext buildContext;
28

29
  @override
340✔
30
  // ignore: avoid_setters_without_getters
31
  set context(BuildContext value) {
32
    buildContext = value;
340✔
33
  }
34

35
  final _eventStreamController = StreamController<UIDriverEvent>.broadcast();
36

37
  @override
344✔
38
  Stream<UIDriverEvent> get eventStream => _eventStreamController.stream;
688✔
39

40
  @visibleForTesting
41
  @override
42
  ExternalEventHandler? externalEventHandler;
43

44
  @visibleForTesting
45
  @override
46
  ScriptRunner? scriptRunner;
47

48
  @visibleForTesting
49
  final Map<String, dynamic>? initialRequestPayload;
50

51
  late final bool _useStaticContent;
52
  bool _isChannelInitialized = false, _isDriverInitialized = false;
53

54
  @visibleForTesting
55
  late final Map<String, dynamic>? content;
56

57
  @visibleForTesting
58
  @override
59
  late final ActionExecutor actionExecutor;
60

61
  @visibleForTesting
62
  @override
63
  late final EventResolver eventResolver;
64

65
  @visibleForTesting
66
  @override
67
  MethodChannel? driverChannel;
68

69
  @visibleForTesting
70
  @override
71
  late final bool isModule;
72

73
  @visibleForTesting
74
  @override
75
  DebugLogger? logger;
76

77
  late ViewManager _viewManager;
78

79
  final _dataSources = <int, StreamSubscription<ServerEvent>>{};
80

81
  DuitDriver(
4✔
82
    this.source, {
83
    required this.transportOptions,
84
    this.externalEventHandler,
85
    this.initialRequestPayload,
86
    this.logger,
87
    EventResolver? customEventResolver,
88
    ActionExecutor? customActionExecutor,
89
    DebugLogger? customLogger,
90
    bool shared = false,
91
  }) {
92
    logger = customLogger ?? DefaultLogger.instance;
8✔
93

94
    _useStaticContent = false;
4✔
95
    actionExecutor = customActionExecutor ??
4✔
96
        DefaultActionExecutor(
4✔
97
          driver: this,
98
          logger: logger,
4✔
99
        );
100
    eventResolver = customEventResolver ??
4✔
101
        DefaultEventResolver(
4✔
102
          driver: this,
103
          logger: logger,
4✔
104
        );
105
    isModule = false;
4✔
106
    _viewManager = shared ? MultiViewManager() : SimpleViewManager();
8✔
107
  }
108

109
  /// Creates a new instance of [DuitDriver] with the specified [content] without establishing a initial transport connection.
110
  DuitDriver.static(
340✔
111
    this.content, {
112
    required this.transportOptions,
113
    this.externalEventHandler,
114
    this.logger,
115
    EventResolver? customEventResolver,
116
    ActionExecutor? customActionExecutor,
117
    DebugLogger? customLogger,
118
    this.source = "",
119
    this.initialRequestPayload,
120
    bool shared = false,
121
  }) {
122
    logger = customLogger ?? DefaultLogger.instance;
680✔
123

124
    _useStaticContent = true;
340✔
125
    isModule = false;
340✔
126
    eventResolver = customEventResolver ??
340✔
127
        DefaultEventResolver(
340✔
128
          driver: this,
129
          logger: logger,
340✔
130
        );
131
    actionExecutor = customActionExecutor ??
340✔
132
        DefaultActionExecutor(
340✔
133
          driver: this,
134
          logger: logger,
340✔
135
        );
136
    _viewManager = shared ? MultiViewManager() : SimpleViewManager();
684✔
137
  }
138

139
  /// Creates a new [DuitDriver] instance that is controlled from native code
140
  DuitDriver.module()
×
141
      : _useStaticContent = false,
142
        source = "",
143
        initialRequestPayload = null,
144
        isModule = true,
145
        externalEventHandler = null,
146
        transportOptions = EmptyTransportOptions(),
×
147
        driverChannel = const MethodChannel("duit:driver"),
148
        _viewManager = SimpleViewManager();
×
149

150
  @protected
340✔
151
  @override
152
  void attachController(String id, UIElementController controller) =>
153
      _viewManager.addController(id, controller);
680✔
154

155
  @protected
340✔
156
  @override
157
  void detachController(String id) =>
158
      _viewManager.removeController(id)?.dispose();
1,020✔
159

160
  @protected
12✔
161
  @override
162
  UIElementController? getController(String id) =>
163
      _viewManager.getController(id);
24✔
164

165
  Future<Map<String, dynamic>> _connect() async {
344✔
166
    Map<String, dynamic>? json;
167

168
    try {
169
      if (_useStaticContent) {
344✔
170
        assert(content != null && content!.isNotEmpty);
1,020✔
171
        json = content!;
340✔
172
      } else {
173
        json = await transport?.connect(
8✔
174
          initialData: initialRequestPayload,
4✔
175
        );
176
      }
177
    } catch (e, s) {
178
      logger?.error(
8✔
179
        "Failed conneting to server",
180
        error: e,
181
        stackTrace: s,
182
      );
183
      _eventStreamController.addError(e);
8✔
184
    }
185

186
    if (transport is Streamer) {
688✔
187
      final streamer = transport! as Streamer;
4✔
188
      streamer.eventStream.listen(
8✔
189
        (d) async {
×
190
          try {
191
            if (buildContext.mounted) {
×
192
              await eventResolver.resolveEvent(buildContext, d);
×
193
            }
194
          } catch (e, s) {
195
            logger?.error(
×
196
              "Error while processing event from transport stream",
197
              error: e,
198
              stackTrace: s,
199
            );
NEW
200
            _eventStreamController.addError(e);
×
201
          }
202
        },
203
      );
204
    }
205

206
    return json ?? {};
4✔
207
  }
208

209
  @override
344✔
210
  Future<void> init() async {
211
    if (!_isDriverInitialized) {
344✔
212
      _isDriverInitialized = true;
344✔
213
    } else {
214
      return;
215
    }
216

217
    _viewManager.driver = this;
688✔
218
    _addParsers();
344✔
219

220
    onInit?.call();
344✔
221

222
    if (isModule && !_isChannelInitialized) {
344✔
223
      await _initMethodChannel();
×
224
    }
225

226
    transport ??= _getTransport(
684✔
227
      transportOptions.type,
680✔
228
    );
229

230
    await scriptRunner?.initWithTransport(transport!);
352✔
231

232
    final json = await _connect();
344✔
233

234
    try {
235
      final view = await _viewManager.prepareLayout(json);
688✔
236

237
      if (view != null) {
238
        _eventStreamController.add(
688✔
239
          UIDriverViewEvent(view),
344✔
240
        );
241
      } else {
242
        final err = FormatException(
8✔
243
          "Invalid layout structure. Received map keys: ${json.keys}",
16✔
244
        );
245
        throw err;
246
      }
247
    } catch (e, s) {
248
      logger?.error(
16✔
249
        "Layout parse failed",
250
        error: e,
251
        stackTrace: s,
252
      );
253
      _eventStreamController.addError(
16✔
254
        UIDriverErrorEvent(
8✔
255
          "Layout parse failed",
256
          error: e,
257
          stackTrace: s,
258
        ),
259
      );
260
    }
261
  }
262

263
  @override
4✔
264
  void dispose() {
265
    onDispose?.call();
4✔
266
    transport?.dispose();
8✔
267
    _eventStreamController.close();
8✔
268
    for (final subscription in _dataSources.values) {
8✔
269
      subscription.cancel();
×
270
    }
271
    _dataSources.clear();
8✔
272
  }
273

274
  @override
4✔
275
  Widget? build() {
276
    return _viewManager.build();
8✔
277
  }
278

279
  void _addParsers() {
344✔
280
    try {
281
      ServerAction.setActionParser(const DefaultActionParser());
344✔
282
      ServerEvent.eventParser = const DefaultEventParser();
344✔
283
    } catch (e) {
284
      //Safely handle the case of assigning parsers during
285
      //multiple driver initializations as part of running tests
286
      logger?.warn(
×
287
        e.toString(),
×
288
      );
289
    }
290
  }
291

292
  @visibleForTesting
44✔
293
  @override
294
  Future<void> execute(ServerAction action) async {
295
    beforeActionCallback?.call(action);
44✔
296

297
    try {
298
      final event = await actionExecutor.executeAction(
88✔
299
        action,
300
      );
301

302
      if (event != null && buildContext.mounted) {
88✔
303
        eventResolver.resolveEvent(buildContext, event);
132✔
304
      }
305
    } catch (e) {
306
      logger?.error(
×
307
        "Error executing action",
308
        error: e,
309
      );
310
    } finally {
311
      afterActionCallback?.call();
44✔
312
    }
313
  }
314

315
  Future<void> _resolveComponentUpdate(
4✔
316
    UIElementController controller,
317
    Map<String, dynamic> json,
318
  ) async {
319
    final tag = controller.tag;
4✔
320
    final description = DuitRegistry.getComponentDescription(tag!);
4✔
321

322
    if (description != null) {
323
      final component = ComponentBuilder.build(
4✔
324
        description,
325
        json,
326
      );
327

328
      controller.updateState(component);
4✔
329
    }
330
  }
331

332
  @visibleForTesting
4✔
333
  @override
334
  Future<void> evalScript(String source) async => scriptRunner?.eval(source);
8✔
335

336
  /// Returns a transport based on the specified transport type.
337
  ///
338
  /// This method is used internally to create and return a transport object based
339
  /// on the specified [type]. It switches on the [type] parameter and returns an
340
  /// instance of the corresponding transport class.
341
  ///
342
  /// Parameters:
343
  /// - [type]: The transport type.
344
  ///
345
  /// Returns:
346
  /// - An instance of the transport class based on the specified [type].
347
  /// - If the [type] is not recognized, it returns an instance of [HttpTransport].
348
  Transport _getTransport(String type) {
340✔
349
    if (isModule) {
340✔
350
      return NativeTransport(this);
×
351
    }
352

353
    return switch (type) {
354
      TransportType.http => HttpTransport(
364✔
355
          source,
24✔
356
          options: transportOptions as HttpTransportOptions,
24✔
357
        ),
358
      TransportType.ws => WSTransport(
332✔
NEW
359
          source,
×
NEW
360
          options: transportOptions as WebSocketTransportOptions,
×
361
        ),
362
      _ => EmptyTransport(),
332✔
363
    };
364
  }
365

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

392
  @visibleForTesting
8✔
393
  int get controllersCount => _viewManager.controllersCount;
16✔
394

395
  @visibleForTesting
12✔
396
  @override
397
  Map<String, dynamic> preparePayload(
398
    Iterable<ActionDependency> dependencies,
399
  ) {
400
    final payload = <String, dynamic>{};
12✔
401

402
    if (dependencies.isNotEmpty) {
12✔
403
      for (final dependency in dependencies) {
8✔
404
        final controller = _viewManager.getController(dependency.id);
12✔
405
        if (controller != null) {
406
          final attribute = controller.attributes.payload;
8✔
407
          payload[dependency.target] = attribute["value"];
12✔
408
        }
409
      }
410
    }
411

412
    return payload;
413
  }
414

415
  @visibleForTesting
256✔
416
  @override
417
  Future<void> updateAttributes(
418
    String controllerId,
419
    Map<String, dynamic> json,
420
  ) async {
421
    final controller = _viewManager.getController(controllerId);
512✔
422
    if (controller != null) {
423
      if (controller.type == ElementType.component.name) {
768✔
424
        await _resolveComponentUpdate(controller, json);
4✔
425
        return;
426
      }
427
      controller.updateState(json);
252✔
428
    }
429
  }
430

431
  @override
344✔
432
  void notifyWidgetDisplayStateChanged(
433
    String viewTag,
434
    int state,
435
  ) {
436
    _viewManager.notifyWidgetDisplayStateChanged(viewTag, state);
688✔
437
    logger?.info(
688✔
438
      "Widget with tag ${viewTag.isEmpty ? "*root*" : viewTag} state changed to $state",
688✔
439
    );
440
  }
441

442
  @override
4✔
443
  bool isWidgetReady(String viewTag) {
444
    return _viewManager.isWidgetReady(viewTag);
8✔
445
  }
446

447
  /// Adds an event stream to be listened to and processed by the driver.
448
  ///
449
  /// Each element of the stream must be a Map<String, dynamic>, which will be converted into a [ServerEvent].
450
  /// If the event is a [NullEvent], a [NullEventException] will be thrown.
451
  /// For all other events, [eventResolver.resolveEvent] is called with the current [buildContext].
452
  ///
453
  /// The stream subscription is stored in the internal [_dataSources] list for lifecycle management.
454
  ///
455
  /// [stream] - the stream of events coming from the server or another data source.
456
  void addExternalEventStream(
4✔
457
    Stream<Map<String, dynamic>> stream,
458
  ) {
459
    final id = stream.hashCode;
4✔
460

461
    _dataSources[id] = stream.map(ServerEvent.parseEvent).listen(
16✔
462
      (event) {
4✔
463
        if (event is NullEvent) {
4✔
464
          throw const NullEventException("NullEvent received from data source");
465
        }
466
        eventResolver.resolveEvent(
8✔
467
          // ignore: use_build_context_synchronously
468
          buildContext,
4✔
469
          event,
470
        );
471
      },
472
      onDone: () => _cancelSub(id),
8✔
NEW
473
      onError: (e, s) => _cancelSub(id),
×
474
    );
475
  }
476

477
  void _cancelSub(int code) {
4✔
478
    _dataSources.remove(code)?.cancel();
12✔
479
  }
480
}
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