flutterflutter-futurebuilderrxdartflutter-streambuilder

StreamBuilder that waits for a stream on a future to emit


return ref.watch(findStoreProvider).when(
            data: (store) {
              _store = store;
              return _map;
            },
            error: (error, stackTrace) => Text(error.toString()),
            loading: () => VpLoadingPage(text: 'Loading Google map'));
      }

I need the code above to keep in a loading state until store.map$ has had one emission (which happens in findVgnItms() down the bottom of the provider class). So it is essentially a StreamBuilder that waits for a stream on a future to emit. How is that done? I'm expecting that this: ref.watch(findStoreProvider) is converted to a stream and combined with store.$map but that is on the Future so seems impossible?:

    return ref.watch(findStoreProvider).when(
        data: (store) {
          _store = store;
          return _map;
        },
        error: (error, stackTrace) => Text(error.toString()),
        loading: () => VpLoadingPage(text: 'Loading Google map'));
  }
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    if (defaultTargetPlatform == TargetPlatform.android) {
      AndroidGoogleMapsFlutter.useAndroidViewSurface = true;
    }
    _vm = ref.watch(findPageVm);
    _widgetRef = ref;

    return ref.watch(findStoreProvider).when(
        data: (store) {
          _store = store;
          return _map;
        },
        error: (error, stackTrace) => Text(error.toString()),
        loading: () => VpLoadingPage(text: 'Loading Google map'));
  }

  Widget get _map {
    return StreamBuilder(
        stream: _store.map$,
        builder: (context, AsyncSnapshot<VpMap> snapshot) {
          return _googleMap(map: snapshot.data);
        });
  }

  Widget _googleMap({VpMap? map}) {
    final x = _widgetRef.watch(provider1);
    return Stack(children: [
      VpLoadingPage(text: 'Loading Google map'),
      AnimatedOpacity(
          curve: Curves.fastOutSlowIn,
          opacity: x ? 1.0 : 0,
          duration: const Duration(milliseconds: 600),
          child: GoogleMap(
            markers: map?.markers ?? const <Marker>{},
            myLocationEnabled: true,
            initialCameraPosition: CameraPosition(
              target: LatLng(
                  _store.userPosition.latitude, _store.userPosition.longitude),
              zoom: map?.zoom ?? 13,
            ),
            onMapCreated: (GoogleMapController controller) {
              _vm.controller.complete(controller);

              Future.delayed(const Duration(milliseconds: 550),
                  () => _widgetRef.read(provider1.notifier).isLoaded());
            },
          )),
    ]);
  }

the class that has the future provider with a stream on it:

final findStoreProvider = FutureProvider((provider) async {
  final userStore = await provider.watch(userStoreProvider.future);
  return FindStore(provider, userStore: userStore);
});

class FindStore extends Store {
  FindStore(this.$provider, {required UserStore userStore}) {
    map$ ??= _mapController.stream;
    findVgnItms();
    _userStore = userStore;
  }

  final Ref $provider;
  Stream<VpMap>? map$;
  late final StreamController<VpMap> _mapController =
      StreamController.broadcast();
  late UserStore _userStore;
  Set<Marker> markers = <Marker>{};
  Stream<String?>? searchTerm;

  Position get userPosition => _userStore.userPosition;

  Future<List<VgnItmEst>> _$search(String searchTerm) async {
    final result =
        await $provider.watch(searchResultsProvider(searchTerm).future);
    return result;
  }

  Set<Marker> _mapSearchResultsToMarkers(List<VgnItmEst> searchResults) {
    return searchResults
        .map((searchResult) => Marker(
            markerId: MarkerId(searchResult.id.toString()),
            position: LatLng(searchResult.establishment!.location!.lat,
                searchResult.establishment!.location!.lng)))
        .toSet();
  }

  Future<FindStore> findVgnItms([String searchTerm = '']) async {
    markers = _mapSearchResultsToMarkers(await _$search(searchTerm));
    final map = VpMap(position: _userStore.userPosition, markers: markers);
    _mapController.add(map);
    return this;
  }
}

Solution

  • If you need a Stream that should wait on a Future to complete, you might want to look into async generators and the async* and yield* keywords.

    Here's a short example:

    Stream<MyObject> getStreamThatWaitsOnFuture() async* {
      await functionThatReturnsAFuture();
      yield* functionThatReturnsAStream();
    }
    

    I've written a short post about it here: https://medium.com/@dumazy/create-a-stream-that-requires-a-future-in-dart-692f6f089a7e