0

I am trying to learn MVVM. But I'm having a problem with the "StreamBuilder" , when i run app then edit something then press Ctrl+s (save File) thats error show up:

  The following StateError was thrown building OnBoardingView(state: _OnBoardingViewState#f7e20):
    Bad state: Stream has already been listened to.
    
    The relevant error-causing widget was
    OnBoardingView
    lib\…\0-resources\routes_manager.dart:38
    When the exception was thrown, this was the stack

my code => there are two files

1- view Model:

import 'package:bak/Presentation/0-Resources/color_manager.dart';
import 'package:bak/Presentation/2-OnBoarding/viewmodel/onboarding_viewmodel.dart';
import 'package:flutter/material.dart';
import 'package:auto_size_text/auto_size_text.dart';
import 'package:smooth_page_indicator/smooth_page_indicator.dart';

class OnBoardingView extends StatefulWidget {
  const OnBoardingView({super.key});

  @override
  State<OnBoardingView> createState() => _OnBoardingViewState();
}

class _OnBoardingViewState extends State<OnBoardingView> {
  final OnBoardingViewModel _viewModel = OnBoardingViewModel();
  final PageController _pageController = PageController();
  _bind() {
    _viewModel.start();
  }

  @override
  void initState() {
    _bind();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<OnBoardingViewObject>(
      stream: _viewModel.outputOnBoardingViewObject.asBroadcastStream(),
      builder: (context, snapshot) {
        if (snapshot.hasData) {
          return Scaffold(
            backgroundColor: ColorManager.darkPrimary,
            body: SafeArea(
              child: Column(children: [
                ////////////////////
                /* Page */
                Expanded(
                  child: SizedBox(
                    child: PageView.builder(
                      onPageChanged: (i) {
                        if (i == snapshot.data!.pagesData.length - 1) {
                          _viewModel.skip();
                        }
                      },
                      physics: const BouncingScrollPhysics(),
                      controller: _pageController,
                      itemCount: snapshot.data!.pagesData.length,
                      itemBuilder: (context, index) => OnBordingPage(
                          imagePath: snapshot.data!.pagesData[index].imagePath,
                          title: snapshot.data!.pagesData[index].title,
                          subTitle: snapshot.data!.pagesData[index].subTitle),
                    ),
                  ),
                ),
                ////////////////////
                /* Circles */
                Container(
                    alignment: Alignment.center,
                    height: 20,
                    width: double.infinity,
                    child: SmoothPageIndicator(
                        effect: SwapEffect(
                            dotColor: ColorManager.primary,
                            activeDotColor: ColorManager.logo),
                        controller: _pageController,
                        count: snapshot.data!.pagesData.length <= 1
                            ? 2
                            : snapshot.data!.pagesData.length)),
                ////////////////////
                /* Button */
                Container(
                    alignment: Alignment.center,
                    height: 100,
                    width: double.infinity,
                    child: ElevatedButton(
                      onPressed: () {
                        _viewModel.skip();
                        _pageController.animateToPage(
                            snapshot.data!.pagesData.length - 1,
                            duration: const Duration(microseconds: 300),
                            curve: Curves.easeIn);
                      },
                      child: Text(snapshot.data!.buttonText),
                    )),
              ]),
            ),
          );
        } else {
          return const Text('sedf');
        }
      },
    );
  }

  @override
  void dispose() {
    _viewModel.dispose();
    super.dispose();
  }
}

class OnBordingPage extends StatelessWidget {
  const OnBordingPage(
      {super.key,
      required this.imagePath,
      required this.title,
      required this.subTitle});
  final String imagePath;
  final String title;
  final String subTitle;
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        ////////////////////
        /* Icon */
        Container(
          margin:
              const EdgeInsets.only(left: 50, right: 50, top: 100, bottom: 50),
          child: Image.asset(
            imagePath,
          ),
        ),
        ////////////////////
        /* Title */
        Container(
            margin: const EdgeInsets.symmetric(horizontal: 50, vertical: 25),
            child: AutoSizeText(
              title,
              style: Theme.of(context).textTheme.headlineLarge,
            )),
        ////////////////////
        /* subTitle */
        Container(
            margin: const EdgeInsets.symmetric(
              horizontal: 50,
            ),
            child: AutoSizeText(
              subTitle,
              style: Theme.of(context).textTheme.titleMedium,
            )),
      ],
    );
  }
}

second file:

       // ignore_for_file: unused_field, prefer_final_fields
    
    import 'dart:async';
    import 'package:bak/Presentation/0-Base/baseviewmodel.dart';
    import '../../../Domain/models.dart';
    import '../../0-Resources/assets_manager.dart';
    
    class OnBoardingViewModel extends BaseViewModel
        with OnBoardingViewModelInputs, OnBoardingViewModelOutputs {
      StreamController _streamController = StreamController<OnBoardingViewObject>();
      List<OnBoardingPageObject> pagesDate = [
        OnBoardingPageObject(
            imagePath: ImageAssetsManager.transparentLogo,
            title: 'بكلوريتي',
            subTitle: 'subTitle'),
        OnBoardingPageObject(
            imagePath: ImageAssetsManager.idea,
            title: 'أختبر نفسك',
            subTitle: 'subTitle'),
        OnBoardingPageObject(
            imagePath: ImageAssetsManager.test,
            title: 'العنوان',
            subTitle: 'subTitle'),
        OnBoardingPageObject(
            imagePath: ImageAssetsManager.endTest,
            title: 'أختبر نفسك',
            subTitle: 'subTitle'),
        OnBoardingPageObject(
            imagePath: ImageAssetsManager.globe,
            title: 'العنوان',
            subTitle: 'subTitle'),
        OnBoardingPageObject(
            imagePath: ImageAssetsManager.onlineLearning,
            title: 'العنوان',
            subTitle: 'subTitle'),
        OnBoardingPageObject(
            imagePath: ImageAssetsManager.read,
            title: 'العنوان',
            subTitle: 'subTitle'),
      ];
      // OnBoarding ViewModel Inputs
      @override
      void dispose() {
        _streamController.close();
      }
    
      @override
      void start() {
        pagesDate;
        postData();
      }
    
      @override
      void skip() {
        inputOnBoardingViewObject.add(OnBoardingViewObject(
          currentPage: pagesDate.length - 1,
          buttonText: 'البدئ',
          pagesData: pagesDate,
        ));

  }

  @override
  void finish() {
    // TODO: implement finish
  }

  @override
  Sink get inputOnBoardingViewObject => _streamController.sink;

  @override
  Stream<OnBoardingViewObject> get outputOnBoardingViewObject =>
      _streamController.stream
          .map((onBoardingViewObject) => onBoardingViewObject);

  // Data
  void postData() {
    inputOnBoardingViewObject.add(OnBoardingViewObject(
      currentPage: 0,
      buttonText: 'تخطي',
      pagesData: pagesDate,
    ));
  }
}

// inputs means that "Orders" that our view model will receive
abstract class OnBoardingViewModelInputs {
  //functions here
  void skip();
  void finish();
  //stream controller inputs here
  Sink get inputOnBoardingViewObject;
}

abstract class OnBoardingViewModelOutputs {
  //stream controller inputs here
  Stream<OnBoardingViewObject> get outputOnBoardingViewObject;
}

// class that handle all data that comes from viewmodel to view
class OnBoardingViewObject {
  int currentPage;
  String buttonText;
  List<OnBoardingPageObject> pagesData;

  OnBoardingViewObject({
    required this.currentPage,
    required this.buttonText,
    required this.pagesData,
  });
}

i have tried two method so solve this but non of them worked..

first one:(that one make snapeshot null)

StreamController _streamController =
      StreamController<OnBoardingViewObject>.broadcast();

second one: (nothing change here)

stream: _viewModel.outputOnBoardingViewObject.asBroadcastStream(),
OMR8X
  • 1
  • use riverpod instead. it gives you better stream and future API compared to StreamBuilder and FutureBuilder – john Oct 18 '22 at 00:53

0 Answers0