LCOV - code coverage report
Current view: top level - src - activities_bloc.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 177 214 82.7 %
Date: 2021-10-27 10:12:28 Functions: 0 0 -

          Line data    Source code
       1             : import 'package:flutter/foundation.dart';
       2             : import 'package:flutter/material.dart';
       3             : import 'package:rxdart/rxdart.dart';
       4             : import 'package:stream_feed_flutter_core/stream_feed_flutter_core.dart';
       5             : 
       6             : class ActivitiesControllers<A, Ob, T, Or> {
       7             :   final Map<String,
       8             :           BehaviorSubject<List<GenericEnrichedActivity<A, Ob, T, Or>>>>
       9             :       _controller = {};
      10             : 
      11           1 :   List<GenericEnrichedActivity<A, Ob, T, Or>>? getActivities(
      12             :           String feedGroup) =>
      13           2 :       _getController(feedGroup)?.valueOrNull;
      14             : 
      15           2 :   Stream<List<GenericEnrichedActivity<A, Ob, T, Or>>>? getStream(
      16             :           String feedGroup) =>
      17           4 :       _getController(feedGroup)?.stream;
      18             : 
      19           6 :   void init(String feedGroup) => _controller[feedGroup] =
      20           2 :       BehaviorSubject<List<GenericEnrichedActivity<A, Ob, T, Or>>>();
      21             : 
      22           0 :   void clearActivities(String feedGroup) {
      23           0 :     _getController(feedGroup)!.value = [];
      24             :   }
      25             : 
      26           0 :   void clearAllActivities(List<String> feedGroups) {
      27           0 :     feedGroups.forEach((feedGroups) => init(feedGroups));
      28             :   }
      29             : 
      30           1 :   void close() {
      31           3 :     _controller.forEach((key, value) {
      32           1 :       value.close();
      33             :     });
      34             :   }
      35             : 
      36             :   /// Check if controller is not empty.
      37           2 :   bool hasValue(String feedGroup) =>
      38           4 :       _getController(feedGroup)?.hasValue != null;
      39             : 
      40           2 :   void add(String feedGroup,
      41             :       List<GenericEnrichedActivity<A, Ob, T, Or>> activities) {
      42           2 :     if (hasValue(feedGroup)) {
      43           4 :       _getController(feedGroup)!.add(activities);
      44             :     } //TODO: handle null safety
      45             :   }
      46             : 
      47           2 :   BehaviorSubject<List<GenericEnrichedActivity<A, Ob, T, Or>>>? _getController(
      48             :           String feedGroup) =>
      49           4 :       _controller[feedGroup];
      50             : 
      51           1 :   void update(String feedGroup,
      52             :       List<GenericEnrichedActivity<A, Ob, T, Or>> activities) {
      53           1 :     if (hasValue(feedGroup)) {
      54           2 :       _getController(feedGroup)!.value = activities;
      55             :     }
      56             :   }
      57             : 
      58           0 :   void addError(String feedGroup, Object e, StackTrace stk) {
      59           0 :     if (hasValue(feedGroup)) {
      60           0 :       _getController(feedGroup)!.addError(e, stk);
      61             :     } //TODO: handle null safety
      62             :   }
      63             : }
      64             : 
      65             : class GenericFeedBloc<A, Ob, T, Or> {
      66           4 :   GenericFeedBloc({required this.client, this.analyticsClient});
      67             : 
      68             :   final StreamFeedClient client;
      69           0 :   StreamUser? get currentUser => client.currentUser;
      70             : 
      71             :   final StreamAnalytics? analyticsClient;
      72             : 
      73             :   @visibleForTesting
      74           4 :   late ReactionsControllers reactionsControllers = ReactionsControllers();
      75             : 
      76             :   @visibleForTesting
      77           2 :   late ActivitiesControllers<A, Ob, T, Or> activitiesController =
      78           2 :       ActivitiesControllers<A, Ob, T, Or>();
      79             : 
      80             :   /// The current activities list.
      81           1 :   List<GenericEnrichedActivity<A, Ob, T, Or>>? getActivities(
      82             :           String feedGroup) =>
      83           2 :       activitiesController.getActivities(feedGroup);
      84             : 
      85             :   /// The current reactions list.
      86           2 :   List<Reaction> getReactions(String activityId, [Reaction? reaction]) =>
      87           4 :       reactionsControllers.getReactions(activityId, reaction);
      88             : 
      89             :   /// The current activities list as a stream.
      90           2 :   Stream<List<GenericEnrichedActivity<A, Ob, T, Or>>>? getActivitiesStream(
      91             :           String feedGroup) =>
      92           4 :       activitiesController.getStream(feedGroup);
      93             : 
      94             :   /// The current reactions list as a stream.
      95           2 :   Stream<List<Reaction>>? getReactionsStream(
      96             :       //TODO: better name?
      97             :       String activityId,
      98             :       [String? kind]) {
      99           4 :     return reactionsControllers.getStream(activityId, kind);
     100             :   }
     101             : 
     102           0 :   void clearActivities(String feedGroup) =>
     103           0 :       activitiesController.clearActivities(feedGroup);
     104             : 
     105           0 :   void clearAllActivities(List<String> feedGroups) =>
     106           0 :       activitiesController.clearAllActivities(feedGroups);
     107             : 
     108             :   final _queryActivitiesLoadingController = BehaviorSubject.seeded(false);
     109             : 
     110             :   final Map<String, BehaviorSubject<bool>> _queryReactionsLoadingControllers =
     111             :       {};
     112             : 
     113             :   /// The stream notifying the state of queryReactions call.
     114           0 :   Stream<bool> queryReactionsLoadingFor(String activityId) =>
     115           0 :       _queryReactionsLoadingControllers[activityId]!;
     116             : 
     117             :   /// The stream notifying the state of queryActivities call.
     118           0 :   Stream<bool> get queryActivitiesLoading =>
     119           0 :       _queryActivitiesLoadingController.stream;
     120             : 
     121             :   /// Add an activity to the feed.
     122           1 :   Future<Activity> onAddActivity({
     123             :     required String feedGroup,
     124             :     Map<String, String>? data,
     125             :     required String verb,
     126             :     required String object,
     127             :     String? userId,
     128             :     List<FeedId>? to,
     129             :   }) async {
     130           1 :     final activity = Activity(
     131           3 :       actor: client.currentUser?.ref,
     132             :       verb: verb,
     133             :       object: object,
     134             :       extraData: data,
     135             :       to: to,
     136             :     );
     137             : 
     138           2 :     final flatFeed = client.flatFeed(feedGroup, userId);
     139             : 
     140           2 :     final addedActivity = await flatFeed.addActivity(activity);
     141             : 
     142             :     // TODO(Sacha): merge activity and enriched activity classes together
     143           1 :     final enrichedActivity = await flatFeed
     144           2 :         .getEnrichedActivityDetail<A, Ob, T, Or>(addedActivity.id!);
     145             : 
     146           2 :     final _activities = getActivities(feedGroup) ?? [];
     147             : 
     148             :     // ignore: cascade_invocations
     149           1 :     _activities.insert(0, enrichedActivity);
     150             : 
     151           2 :     activitiesController.add(feedGroup, _activities);
     152             : 
     153           2 :     await trackAnalytics(
     154             :       label: 'post',
     155           1 :       foreignId: activity.foreignId,
     156             :       feedGroup: feedGroup,
     157             :     ); //TODO: remove hardcoded value
     158             :     return addedActivity;
     159             :   }
     160             : 
     161             :   /// Remove child reaction.
     162           1 :   Future<void> onRemoveChildReaction({
     163             :     required String kind,
     164             :     required GenericEnrichedActivity activity,
     165             :     required Reaction childReaction,
     166             :     required Reaction parentReaction,
     167             :   }) async {
     168           5 :     await client.reactions.delete(childReaction.id!);
     169           2 :     final _reactions = getReactions(activity.id!, parentReaction);
     170             : 
     171           1 :     final reactionPath = _reactions.getReactionPath(parentReaction);
     172             : 
     173           1 :     final indexPath = _reactions.indexWhere(
     174           4 :         (r) => r.id! == parentReaction.id); //TODO: handle null safety
     175             : 
     176             :     final childrenCounts =
     177           2 :         reactionPath.childrenCounts.unshiftByKind(kind, ShiftType.decrement);
     178           1 :     final latestChildren = reactionPath.latestChildren
     179           1 :         .unshiftByKind(kind, childReaction, ShiftType.decrement);
     180           1 :     final ownChildren = reactionPath.ownChildren
     181           1 :         .unshiftByKind(kind, childReaction, ShiftType.decrement);
     182             : 
     183           1 :     final updatedReaction = reactionPath.copyWith(
     184             :       ownChildren: ownChildren,
     185             :       latestChildren: latestChildren,
     186             :       childrenCounts: childrenCounts,
     187             :     );
     188             : 
     189             :     // remove reaction from rxstream
     190           1 :     reactionsControllers
     191           2 :       ..unshiftById(activity.id!, childReaction, ShiftType.decrement)
     192           3 :       ..update(activity.id!, _reactions.updateIn(updatedReaction, indexPath));
     193             :   }
     194             : 
     195           1 :   Future<Reaction> onAddChildReaction({
     196             :     required String kind,
     197             :     required Reaction reaction,
     198             :     required GenericEnrichedActivity activity,
     199             :     Map<String, Object>? data,
     200             :     String? userId,
     201             :     List<FeedId>? targetFeeds,
     202             :   }) async {
     203           5 :     final childReaction = await client.reactions.addChild(kind, reaction.id!,
     204             :         data: data, userId: userId, targetFeeds: targetFeeds);
     205           2 :     final _reactions = getReactions(activity.id!, reaction);
     206           1 :     final reactionPath = _reactions.getReactionPath(reaction);
     207             :     final indexPath = _reactions
     208           5 :         .indexWhere((r) => r.id! == reaction.id); //TODO: handle null safety
     209             : 
     210           2 :     final childrenCounts = reactionPath.childrenCounts.unshiftByKind(kind);
     211             :     final latestChildren =
     212           2 :         reactionPath.latestChildren.unshiftByKind(kind, childReaction);
     213             :     final ownChildren =
     214           2 :         reactionPath.ownChildren.unshiftByKind(kind, childReaction);
     215             : 
     216           1 :     final updatedReaction = reactionPath.copyWith(
     217             :       ownChildren: ownChildren,
     218             :       latestChildren: latestChildren,
     219             :       childrenCounts: childrenCounts,
     220             :     );
     221             : 
     222             :     // adds reaction to the rxstream
     223           1 :     reactionsControllers
     224           2 :       ..unshiftById(activity.id!, childReaction)
     225           3 :       ..update(activity.id!, _reactions.updateIn(updatedReaction, indexPath));
     226             :     // return reaction;
     227             :     return childReaction;
     228             :   }
     229             : 
     230             :   /// Remove reaction from the feed.
     231           1 :   Future<void> onRemoveReaction({
     232             :     required String kind,
     233             :     required GenericEnrichedActivity<A, Ob, T, Or> activity,
     234             :     required Reaction reaction,
     235             :     required String feedGroup,
     236             :   }) async {
     237           5 :     await client.reactions.delete(reaction.id!);
     238           2 :     await trackAnalytics(
     239           2 :         label: 'un$kind', foreignId: activity.foreignId, feedGroup: feedGroup);
     240           2 :     final _activities = getActivities(feedGroup) ?? [activity];
     241           1 :     final activityPath = _activities.getEnrichedActivityPath(activity);
     242             : 
     243             :     final indexPath = _activities
     244           5 :         .indexWhere((a) => a.id! == activity.id); //TODO: handle null safety
     245             : 
     246             :     final reactionCounts =
     247           2 :         activityPath.reactionCounts.unshiftByKind(kind, ShiftType.decrement);
     248             : 
     249             :     // final reaction =
     250             :     //     reactionsFor(activity.id!).firstWhere((reaction) => reaction.id == id);
     251           1 :     final latestReactions = activityPath.latestReactions
     252           1 :         .unshiftByKind(kind, reaction, ShiftType.decrement);
     253             : 
     254           1 :     final ownReactions = activityPath.ownReactions
     255           1 :         .unshiftByKind(kind, reaction, ShiftType.decrement);
     256             : 
     257           1 :     final updatedActivity = activityPath.copyWith(
     258             :       ownReactions: ownReactions,
     259             :       latestReactions: latestReactions,
     260             :       reactionCounts: reactionCounts,
     261             :     );
     262             : 
     263             :     // remove reaction from the stream
     264           2 :     reactionsControllers.unshiftById(
     265           1 :         activity.id!, reaction, ShiftType.decrement);
     266             : 
     267           2 :     activitiesController.update(
     268           1 :         feedGroup, _activities.updateIn(updatedActivity, indexPath));
     269             :   }
     270             : 
     271             :   /// Add a new reaction to the feed.
     272           1 :   Future<Reaction> onAddReaction({
     273             :     Map<String, Object>? data,
     274             :     required String kind,
     275             :     required GenericEnrichedActivity<A, Ob, T, Or> activity,
     276             :     List<FeedId>? targetFeeds,
     277             :     required String feedGroup,
     278             :   }) async {
     279           3 :     final reaction = await client.reactions
     280           2 :         .add(kind, activity.id!, targetFeeds: targetFeeds, data: data);
     281           2 :     await trackAnalytics(
     282           1 :         label: kind, foreignId: activity.foreignId, feedGroup: feedGroup);
     283           2 :     final _activities = getActivities(feedGroup) ?? [activity];
     284           1 :     final activityPath = _activities.getEnrichedActivityPath(activity);
     285             :     final indexPath = _activities
     286           5 :         .indexWhere((a) => a.id! == activity.id); //TODO: handle null safety
     287             : 
     288           2 :     final reactionCounts = activityPath.reactionCounts.unshiftByKind(kind);
     289             :     final latestReactions =
     290           2 :         activityPath.latestReactions.unshiftByKind(kind, reaction);
     291             :     final ownReactions =
     292           2 :         activityPath.ownReactions.unshiftByKind(kind, reaction);
     293             : 
     294           1 :     final updatedActivity = activityPath.copyWith(
     295             :       ownReactions: ownReactions,
     296             :       latestReactions: latestReactions,
     297             :       reactionCounts: reactionCounts,
     298             :     );
     299             : 
     300             :     // adds reaction to the stream
     301           3 :     reactionsControllers.unshiftById(activity.id!, reaction);
     302             : 
     303           2 :     activitiesController.update(
     304             :         feedGroup,
     305             :         _activities //TODO: handle null safety
     306           1 :             .updateIn(updatedActivity, indexPath));
     307             :     return reaction;
     308             :   }
     309             : 
     310             :   /// Track analytics.
     311           1 :   Future<void> trackAnalytics({
     312             :     required String label,
     313             :     String? foreignId,
     314             :     required String feedGroup,
     315             :   }) async {
     316           1 :     analyticsClient != null
     317           0 :         ? await analyticsClient!.trackEngagement(Engagement(
     318           0 :             content: Content(foreignId: FeedId.fromId(foreignId)),
     319             :             label: label,
     320           0 :             feedId: FeedId.fromId(feedGroup),
     321             :           ))
     322           1 :         : print('warning: analytics: not enabled'); //TODO:logger
     323             :   }
     324             : 
     325           2 :   Future<void> queryReactions(
     326             :     LookupAttribute lookupAttr,
     327             :     String lookupValue, {
     328             :     Filter? filter,
     329             :     int? limit,
     330             :     String? kind,
     331             :     EnrichmentFlags? flags,
     332             :   }) async {
     333           4 :     reactionsControllers.init(lookupValue);
     334           4 :     _queryReactionsLoadingControllers[lookupValue] =
     335           2 :         BehaviorSubject.seeded(false);
     336           8 :     if (_queryReactionsLoadingControllers[lookupValue]?.value == true) return;
     337             : 
     338           4 :     if (reactionsControllers.hasValue(lookupValue)) {
     339           4 :       _queryReactionsLoadingControllers[lookupValue]!
     340           2 :           .add(true); //TODO: fix null
     341             :     }
     342             : 
     343             :     try {
     344           4 :       final oldReactions = List<Reaction>.from(getReactions(lookupValue));
     345           8 :       final reactionsResponse = await client.reactions.filter(
     346             :         lookupAttr,
     347             :         lookupValue,
     348             :         filter: filter,
     349             :         flags: flags,
     350             :         limit: limit,
     351             :         kind: kind,
     352             :       );
     353           2 :       final temp = oldReactions + reactionsResponse;
     354           4 :       reactionsControllers.add(lookupValue, temp);
     355             :     } catch (e, stk) {
     356             :       // reset loading controller
     357           0 :       _queryReactionsLoadingControllers[lookupValue]?.add(false);
     358           0 :       if (reactionsControllers.hasValue(lookupValue)) {
     359           0 :         _queryReactionsLoadingControllers[lookupValue]?.addError(e, stk);
     360             :       } else {
     361           0 :         reactionsControllers.addError(lookupValue, e, stk);
     362             :       }
     363             :     }
     364             :   }
     365             : 
     366           1 :   Future<void> queryEnrichedActivities({
     367             :     required String feedGroup,
     368             :     int? limit,
     369             :     int? offset,
     370             :     String? session,
     371             :     Filter? filter,
     372             :     EnrichmentFlags? flags,
     373             :     String? ranking,
     374             :     String? userId,
     375             : 
     376             :     //TODO: no way to parameterized marker?
     377             :   }) async {
     378           2 :     activitiesController.init(feedGroup);
     379           3 :     if (_queryActivitiesLoadingController.value == true) return;
     380             : 
     381           2 :     if (activitiesController.hasValue(feedGroup)) {
     382           2 :       _queryActivitiesLoadingController.add(true);
     383             :     }
     384             : 
     385             :     try {
     386           2 :       final activitiesResponse = await client
     387           1 :           .flatFeed(feedGroup, userId)
     388           1 :           .getEnrichedActivities<A, Ob, T, Or>(
     389             :             limit: limit,
     390             :             offset: offset,
     391             :             session: session,
     392             :             filter: filter,
     393             :             flags: flags,
     394             :             ranking: ranking,
     395             :           );
     396             : 
     397           2 :       activitiesController.add(feedGroup, activitiesResponse);
     398           2 :       if (activitiesController.hasValue(feedGroup) &&
     399           2 :           _queryActivitiesLoadingController.value) {
     400           3 :         _queryActivitiesLoadingController.sink.add(false);
     401             :       }
     402             :     } catch (e, stk) {
     403             :       // reset loading controller
     404           0 :       _queryActivitiesLoadingController.add(false);
     405           0 :       if (activitiesController.hasValue(feedGroup)) {
     406           0 :         _queryActivitiesLoadingController.addError(e, stk);
     407             :       } else {
     408           0 :         activitiesController.addError(feedGroup, e, stk);
     409             :       }
     410             :     }
     411             :   }
     412             : 
     413             :   /// Follows the given [flatFeed].
     414           1 :   Future<void> followFlatFeed(
     415             :     String otherUser,
     416             :   ) async {
     417           2 :     final timeline = client.flatFeed('timeline');
     418           2 :     final user = client.flatFeed('user', otherUser);
     419           2 :     await timeline.follow(user);
     420             :   }
     421             : 
     422             :   /// Unfollows the given [actingFeed].
     423           1 :   Future<void> unfollowFlatFeed(
     424             :     String otherUser,
     425             :   ) async {
     426           2 :     final timeline = client.flatFeed('timeline');
     427           2 :     final user = client.flatFeed('user', otherUser);
     428           2 :     await timeline.unfollow(user);
     429             :   }
     430             : 
     431             :   /// Checks whether the current user is following a feed with the given
     432             :   /// [userId].
     433             :   ///
     434             :   /// It filters the request such that if the current user is in fact
     435             :   /// following the given user, one user will be returned that matches the
     436             :   /// current user, thus indicating that the current user does follow the given
     437             :   /// user. If no results are found, this means that the current user is not
     438             :   /// following the given user.
     439           1 :   Future<bool> isFollowingUser(String userId) async {
     440           4 :     final following = await client.flatFeed('timeline').following(
     441             :       limit: 1,
     442             :       offset: 0,
     443           1 :       filter: [
     444           2 :         FeedId.id('user:$userId'),
     445             :       ],
     446             :     );
     447           1 :     return following.isNotEmpty;
     448             :   }
     449             : 
     450           1 :   void dispose() {
     451           2 :     activitiesController.close();
     452           2 :     reactionsControllers.close();
     453           2 :     _queryActivitiesLoadingController.close();
     454           3 :     _queryReactionsLoadingControllers.forEach((key, value) {
     455           1 :       value.close();
     456             :     });
     457             :   }
     458             : 
     459           0 :   Future<void> onRemoveActivity({
     460             :     required String feedGroup,
     461             :     required String activityId,
     462             :   }) async {
     463           0 :     await client.flatFeed(feedGroup).removeActivityById(activityId);
     464             :   }
     465             : }
     466             : 
     467             : class GenericFeedProvider<A, Ob, T, Or> extends InheritedWidget {
     468           3 :   const GenericFeedProvider({
     469             :     Key? key,
     470             :     required this.bloc,
     471             :     required Widget child,
     472           3 :   }) : super(key: key, child: child);
     473             : 
     474           3 :   factory GenericFeedProvider.of(BuildContext context) {
     475           3 :     final result = context.dependOnInheritedWidgetOfExactType<
     476             :         GenericFeedProvider<A, Ob, T, Or>>();
     477           1 :     assert(result != null,
     478           1 :         'No GenericFeedProvider<$A, $Ob, $T, $Or> found in context');
     479             :     return result!;
     480             :   }
     481             :   final GenericFeedBloc<A, Ob, T, Or> bloc;
     482             : 
     483           0 :   @override
     484           0 :   bool updateShouldNotify(GenericFeedProvider old) => bloc != old.bloc; //
     485             : 
     486           0 :   @override
     487             :   void debugFillProperties(DiagnosticPropertiesBuilder properties) {
     488           0 :     super.debugFillProperties(properties);
     489             :     properties
     490           0 :         .add(DiagnosticsProperty<GenericFeedBloc<A, Ob, T, Or>>('bloc', bloc));
     491             :   }
     492             : }
     493             : 
     494             : class ReactionsControllers {
     495             :   final Map<String, BehaviorSubject<List<Reaction>>> _controller = {};
     496             : 
     497             :   /// Init controller for given activityId.
     498           2 :   void init(String lookupValue) =>
     499           6 :       _controller[lookupValue] = BehaviorSubject<List<Reaction>>();
     500             : 
     501             :   /// Retrieve with activityId the corresponding StreamController from the map
     502             :   /// of controllers.
     503           2 :   BehaviorSubject<List<Reaction>>? _getController(String lookupValue) =>
     504           4 :       _controller[lookupValue]; //TODO: handle null safety
     505             : 
     506             :   ///Retrieve Stream of reactions with activityId and filter it if necessary
     507           2 :   Stream<List<Reaction>>? getStream(String lookupValue, [String? kind]) {
     508             :     final isFiltered = kind != null;
     509           4 :     final reactionStream = _getController(lookupValue)?.stream;
     510             :     return isFiltered
     511           2 :         ? reactionStream?.map((reactions) =>
     512           5 :             reactions.where((reaction) => reaction.kind == kind).toList())
     513             :         : reactionStream; //TODO: handle null safety
     514             :   }
     515             : 
     516             :   /// Convert the Stream of reactions to a List of reactions.
     517           2 :   List<Reaction> getReactions(String lookupValue, [Reaction? reaction]) =>
     518           4 :       _getController(lookupValue)?.valueOrNull ??
     519           3 :       (reaction != null ? [reaction] : <Reaction>[]);
     520             : 
     521             :   /// Check if controller is not empty.
     522           2 :   bool hasValue(String lookupValue) =>
     523           4 :       _getController(lookupValue)?.hasValue != null;
     524             : 
     525             :   /// Lookup latest Reactions by Id and inserts the given reaction to the
     526             :   /// beginning of the list.
     527           1 :   void unshiftById(String lookupValue, Reaction reaction,
     528             :           [ShiftType type = ShiftType.increment]) =>
     529           2 :       _controller.unshiftById(lookupValue, reaction, type);
     530             : 
     531             :   /// Close every stream controllers.
     532           4 :   void close() => _controller.forEach((key, value) {
     533           1 :         value.close();
     534             :       });
     535             : 
     536             :   /// Update controller value with given reactions.
     537           1 :   void update(String lookupValue, List<Reaction> reactions) {
     538           1 :     if (hasValue(lookupValue)) {
     539           2 :       _getController(lookupValue)!.value = reactions;
     540             :     }
     541             :   }
     542             : 
     543             :   /// Add given reactions to the correct controller.
     544           2 :   void add(String lookupValue, List<Reaction> temp) {
     545           2 :     if (hasValue(lookupValue)) {
     546           4 :       _getController(lookupValue)!.add(temp);
     547             :     } //TODO: handle null safety
     548             :   }
     549             : 
     550             :   /// Add error to the correct controller.
     551           0 :   void addError(String lookupValue, Object e, StackTrace stk) {
     552           0 :     if (hasValue(lookupValue)) {
     553           0 :       _getController(lookupValue)!.addError(e, stk);
     554             :     } //TODO: handle null safety
     555             :   }
     556             : }

Generated by: LCOV version 1.15