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(),