flutter bloc how stop execution loop inside event? - flutter

I use a Provider with ChangeNotifier, but inside it I need to make a StreamController that will process the data. For example, let there be a list of sites
class ExampleProvider extends ChangeNotifier {
//list of sites for data processing
final List<String> _weblinks = [];
ExampleProvider() {
//Fill in demo data
_weblinks.add("https://stackoverflow.com/");
_weblinks.add("https://github.com");
_weblinks.add("http://microsoft.com");
//subscribe to the stream in the constructor
eventStream.listen((event) async {
if (event is StartProcessing) {
for (var i = 0; i < _weblinks.length; i++) {
//Doing some fake calculations
await Future.delayed(Duration(seconds: 3));
//add the result to the stream
dataSink.add(_weblinks[i]);
}
}
if (event is FinishProcessing) {}
});
}
final _streamController = StreamController<String>.broadcast();
Stream<String> get dataStream => _streamController.stream;
StreamSink<String> get dataSink => _streamController.sink;
final _eventController = StreamController<StreamControllerAction>.broadcast();
Stream<StreamControllerAction> get eventStream => _eventController.stream;
StreamSink<StreamControllerAction> get eventSink => _eventController.sink;
}
The problem is that the list of sites can be very large and if a data processing thread has started, then I can’t interrupt or cancel it, I cannot stop the execution of the loop, and until the list of sites ends, the thread will be executed. I also cannot use _streamController.close() because the stream is inside the provider, which is and the stream will be disabled forever, and I would like to reuse it. is there a way to interrupt the execution of a stream if the data is evaluated in a loop?
Full code
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider.value(
value: ExampleProvider(),
),
],
child: MaterialApp(
home: MyHomePage(),
));
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
StreamBuilder<String>(
initialData: null,
stream: Provider.of<ExampleProvider>(context, listen: false)
.dataStream,
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
List<Widget> children;
if (snapshot.hasError) {
children = <Widget>[Text("error")];
} else {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
children = <Widget>[
ElevatedButton(
child: Text("processing data"),
onPressed: () {
Provider.of<ExampleProvider>(context, listen: false)
.eventSink
.add(StartProcessing());
},
),
];
break;
default:
{
children = <Widget>[
Column(
children: [
ElevatedButton(
child: Text("finish"),
onPressed: () {},
),
Text(snapshot.data),
],
)
];
}
break;
}
}
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: children,
);
},
)
],
),
),
);
}
}
class ExampleProvider extends ChangeNotifier {
final List<String> _weblinks = [];
ExampleProvider() {
_weblinks.add("https://stackoverflow.com/");
_weblinks.add("https://github.com");
_weblinks.add("http://microsoft.com");
eventStream.listen((event) async {
if (event is StartProcessing) {
for (var i = 0; i < _weblinks.length; i++) {
await Future.delayed(Duration(seconds: 3));
dataSink.add(_weblinks[i]);
}
}
if (event is FinishProcessing) {}
});
}
final _streamController = StreamController<String>.broadcast();
Stream<String> get dataStream => _streamController.stream;
StreamSink<String> get dataSink => _streamController.sink;
final _eventController = StreamController<StreamControllerAction>.broadcast();
Stream<StreamControllerAction> get eventStream => _eventController.stream;
StreamSink<StreamControllerAction> get eventSink => _eventController.sink;
}
abstract class StreamControllerAction {}
class StartProcessing extends StreamControllerAction {}
class FinishProcessing extends StreamControllerAction {}

If you want to stop listening to eventStream you can save its subscription in a StreamSubscription object and then you will have access to .pause() and .resume().
When you use stream.listen() it returns a StreamSubscription you can save in a variable and use later.
You can read more about it in the documentation https://api.dart.dev/stable/2.12.4/dart-async/StreamSubscription-class.html

Related

why data from firebase is not displayed?

First, 'main.dart' calls 'homepage.dart'.
And 'homepage.dart' calls several pages in the body(the code'_pages[_index]') using bottomnavigationbar and index.
Initially, index is 0 and 'Ppage1' appears by default.
In this 'Ppage1', I brought the collection 'exhibition' from firestore.
And then called the '_buildBody'(if data is not arrived,LinearProgressIndicator will be displayed) ,
and in there i made the list 'exhibitions' using 'Exhibition'. (Previously, I had made a data model called 'Exhibition' in 'model_exhibitions.dart'.)
And in the '_buildBody', I brought the class 'BoxSlider' that is using the data from firebase.
I think the data is arrived wel since the LinearProgressIndicator is not displayed.
But only the UI using the data from firestore is not displayed.
What is problem? i can't find it
homepage.dart
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
#override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
var _index = 0;
final _pages = [
Ppage1(),
const Page2(),
const Page3(),
const Page4(),
const Page5(),
];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar( ...
Ppage1.dart
class Ppage1 extends StatefulWidget {
#override
State<Ppage1> createState() => _Ppage1State();
}
class _Ppage1State extends State<Ppage1> {
FirebaseFirestore firebaseFirestore = FirebaseFirestore.instance;
late Stream<QuerySnapshot> streamData;
#override
void initState() {
super.initState();
streamData = firebaseFirestore.collection('exhibition').snapshots();
}
Widget _fetchData(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance.collection('exhibition').snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) return LinearProgressIndicator();
return _buildBody(context, snapshot.data!.docs);
},
);
}
Widget _buildBody(BuildContext context, List<DocumentSnapshot> snapshot) {
List<Exhibition> exhibitions = snapshot.map((d) => Exhibition.fromSnapshot(d)).toList();
return SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
BoxSlider(exhibitions: exhibitions), ...
box_slider.dart
class BoxSlider extends StatefulWidget {
late final List<Exhibition> exhibitions;
BoxSlider({required this.exhibitions});
#override
State<BoxSlider> createState() => _BoxSliderState();
}
class _BoxSliderState extends State<BoxSlider> {
late List<Exhibition> exhibitions;
late List<Widget> posters;
...
#override
void initState() {
super.initState();
exhibitions = widget.exhibitions;
posters = exhibitions.map((m) => Image.asset(m.poster)).toList();
...
}
#override
Widget build(BuildContext context) {
return Container(
height: 440,
child: ListView(
scrollDirection: Axis.horizontal,
children: makeBoxImages(context, widget.exhibitions),
),
);
}
}
List<Widget> makeBoxImages(BuildContext context, List<Exhibition> exhibitions) {
List<Widget> results = [];
for (var i = 0; i < exhibitions.length; i++) {
results.add(
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InkWell(
onTap: () {Navigator.push(
context,
MaterialPageRoute(builder: (context)=>DetailScreen(exhibition: exhibitions[i])),
);
},
child: SizedBox(
height: 350,
child: Image.network(exhibitions[i].poster),
), ...
You should change the _fetchdata method to know if the data is fetched correctly:
Widget _fetchData(BuildContext context) {
return StreamBuilder<QuerySnapshot>(
stream: FirebaseFirestore.instance.collection('exhibition').snapshots(),
builder: (context, snapshot) {
//To know if the snapshot contains errors.
if (snapshot.hasError) {
return Center(
child: Text('Some error occured: ${snapshot.error.toString()}'),
);
}
//To know if the snapshot contains valid data.
if (snapshot.hasData) {
return _buildBody(context, snapshot.data!.docs);
}
//Returns a progress indicator in case that the two conditions above are not satisfied.
return LinearProgressIndicator();
},
);
}

Flutter: Bloc not navigating to another screen after succesful state update

I'm trying to navigate to another page using blocs / cubits. I have one cubit that successfully navigates to another page upon completion of a method, but for some reason, it doesn't work on another cubit, despite successful state change, and operation done on the method.
class WalletCreateDialog extends StatefulWidget {
const WalletCreateDialog({required this.mnemonic});
final String mnemonic;
#override
_WalletCreateDialogState createState() => _WalletCreateDialogState();
}
class _WalletCreateDialogState extends State<WalletCreateDialog> {
#override
void initState() {
BlocProvider.of<WalletCreateCubit>(context)
.addCreatedWalletToWalletList(widget.mnemonic);
super.initState();
}
#override
Widget build(BuildContext context) {
return BlocListener<WalletCreateCubit, WalletCreateState>(
listener: (context, state) {
if (state is WalletAdded) {
Navigator.of(context).pop();
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
content: Text(
'Wallet added! Navigating back to home screen...',
),
),
);
Navigator.of(context).pushNamedAndRemoveUntil(
WalletOverviewHomeScreen.routeName,
(route) => false,
);
}
},
child: AlertDialog(
content: Container(
height: MediaQuery.of(context).size.height * 0.08,
child: Row(
children: [
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text("Adding wallet..."),
const LoadingIndicator(),
],
),
),
],
),
),
),
);
}
}
In the line of code above, it successfully navigates to WalletOverviewHomeScreen upon successful completion of the addCreatedWalletToWalletList method.
class WalletDeleteDialog extends StatefulWidget {
const WalletDeleteDialog({required this.walletAddress});
final String walletAddress;
#override
State<WalletDeleteDialog> createState() => _WalletDeleteDialogState();
}
class _WalletDeleteDialogState extends State<WalletDeleteDialog> {
#override
void initState() {
BlocProvider.of<WalletDeleteCubit>(context)
.deleteWallet(widget.walletAddress);
super.initState();
}
#override
Widget build(BuildContext context) {
return BlocListener<WalletDeleteCubit, WalletDeleteState>(
listener: (context, state) {
if (state is WalletDeleteFinished) {
Navigator.of(context).pop();
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
content: Text(
'Wallet deleted! Navigating back to home screen...',
),
),
);
Navigator.of(context).pushNamedAndRemoveUntil(
WalletOverviewHomeScreen.routeName,
(route) => false,
);
}
},
child: AlertDialog(
content: Container(
height: MediaQuery.of(context).size.height * 0.08,
child: Row(
children: [
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text("Deleting wallet..."),
const LoadingIndicator(),
],
),
),
],
),
),
),
);
}
}
On the other hand, in the line of code above, it doesn't navigate to the same screen after completion of the method. I've already verified that the state has changed in both cubits. Additionally, hot restarting the app would actually show that what was supposed to get deleted, did actually get deleted, thus there's no issue with regards to the implementation of the deleteWallet method itself.
How can I navigate to the WalletOverviewHomeScreen after completion of the deleteWallet method?
For context, below are the state classes for the Cubits.
part of 'wallet_create_cubit.dart';
abstract class WalletCreateState extends Equatable {
const WalletCreateState();
#override
List<Object> get props => [];
}
class WalletCreateInitial extends WalletCreateState {
const WalletCreateInitial();
#override
List<Object> get props => [];
}
class WalletCreateLoading extends WalletCreateState {
const WalletCreateLoading();
#override
List<Object> get props => [];
}
class WalletCreated extends WalletCreateState {
final String mnemonic;
const WalletCreated({required this.mnemonic});
#override
List<Object> get props => [mnemonic];
}
class WalletAdding extends WalletCreateState {
const WalletAdding();
#override
List<Object> get props => [];
}
class WalletAdded extends WalletCreateState {
const WalletAdded();
#override
List<Object> get props => [];
}
part of 'wallet_delete_cubit.dart';
abstract class WalletDeleteState extends Equatable {
const WalletDeleteState();
#override
List<Object> get props => [];
}
class WalletDeleteInitial extends WalletDeleteState {
const WalletDeleteInitial();
#override
List<Object> get props => [];
}
class WalletDeleteOngoing extends WalletDeleteState {
const WalletDeleteOngoing();
#override
List<Object> get props => [];
}
class WalletDeleteFinished extends WalletDeleteState {
const WalletDeleteFinished();
#override
List<Object> get props => [];
}
remove Navigator.of(context).pop();
because you don't need it. when you use Navigator.of(context).pushNamedAndRemoveUntil
Looks like the fix was to directly copy the contents of deleteFromWallet to the deleteWallet function. That is, in the WalletDeleteCubit it went from this:
Future<void> deleteWallet(String address) async {
FlutterSecureStorage storage = FlutterSecureStorage();
emit(WalletDeleteOngoing());
deleteFromWallet(storage, address);
debugPrint("Wallet with address: $address is deleted");
emit(WalletDeleteFinished());
debugPrint('Emit WalletDeleteFinished');
}
To this:
void deleteWallet(String address) async {
FlutterSecureStorage storage = FlutterSecureStorage();
emit(WalletDeleteOngoing());
await storage.delete(
key: WalletOverviewHomeScreen.walletKey + address,
);
debugPrint("Wallet with address: $address is deleted");
emit(WalletDeleteFinished());
debugPrint('Emit WalletDeleteFinished');
}

What is the right way using the build context outside the build function

Lets say I'm enter my named route page and get the arguments in the build function.
Now my widget is state full widget and i want to make api call with the arguments in order to set the state of my widget.
I'm using future Builder to load the api when the page is loading, so i have to create Future and equal him to the api func right?
but i cant do it inside the build it will call it unlimited times, so i send it as props to an other widget but really i should create widget just in order to send my context values?
class GameScreen extends StatefulWidget {
GameScreen({Key key}) : super(key: key);
#override
_GameScreenState createState() => _GameScreenState();
}
class _GameScreenState extends State<GameScreen> {
Fixture fixture;
Future setFIxture(externalId) async {
final response =
await FixturesService().getLiveFixtureByExternalId(externalId);
setState(() {
fixture = response;
});
}
#override
Widget build(BuildContext context) {
final GameScreenArguments args = ModalRoute.of(context).settings.arguments;
Future initScreen;
initScreen = setFIxture(args.externald);
return RoutePage(
child: Loader(
future: initScreen,
succeed: Container(
height: 223,
width: double.infinity,
child: Column(
children: [
Column(
children: [
Text(""),
Row(
children: [
Text(""),
Text(""),
],
),
Column(
children: [
// TeamImage(),
Column(
children: [
Text(""),
Text(""),
],
),
// TeamImage(),
],
)
],
),
Column(
children: [
Text(""),
],
)
],
),
),
),
);
}
}
my loader widget:
class Loader extends StatefulWidget {
final Future future;
final Widget succeed;
Loader({Key key, this.future, this.succeed}) : super(key: key);
#override
_LoaderState createState() => _LoaderState();
}
class _LoaderState extends State<Loader> {
Future _getTaskAsync;
final spinkit = SpinKitFadingCircle(
color: Colors.black,
size: 40,
);
Future fetchData() async {
try {
await widget.future;
return true;
} catch (e) {
return e;
}
}
#override
void initState() {
_getTaskAsync = fetchData();
super.initState();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: _getTaskAsync,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
return widget.succeed;
} else if (snapshot.hasError) {
return Text("error");
} else {
return spinkit;
}
},
);
}
}
Yes you have to create another widget. But maybe use a dependency injection solution to make it simpler to inject objects into the widget tree (Riverpod is good i heard). Store the GameScreen args inside a shared state above in the widget tree.

How to rebuild StreamBuilder on resumed state with Flutter

I develop an app using BLoC pattern.
In my app there are 2 routes, route A and B, and both of them access same data.
A problem caused when moving the routes as below.
Move to route B from route A that shows the data.
Update the data at route B.
Back to route A.
After moving back to route A, the StreamBuilder of showing the data never updates automatically.
How can I let the StreamBuilder update on resumed state?
Here are sample codes.
routeA.dart
class RouteA extends StatefulWidget {
#override
_RouteAState createState() => _RouteAState();
}
class _RouteAState extends State<RouteA> {
#override
Widget build(BuildContext context) {
final bloc = Bloc();
return Column(
children: [
StreamBuilder( // this StreamBuilder never updates on resumed state
stream: bloc.data, // mistake, fixed. before: bloc.count
builder: (_, snapshot) => Text(
snapshot.data ?? "",
)),
RaisedButton(
child: Text("Move to route B"),
onPressed: () {
Navigator.of(context).pushNamed("routeB");
},
),
],
);
}
}
routeB.dart
class RouteB extends StatefulWidget {
#override
_RouteBState createState() => _RouteBState();
}
class _RouteBState extends State<RouteB> {
#override
Widget build(BuildContext context) {
final bloc = Bloc();
return Center(
child: RaisedButton(
child: Text("Update data"),
onPressed: () {
bloc.update.add(null);
},
),
);
}
}
bloc.dart
class Bloc {
Stream<String> data;
Sink<void> update;
Model _model;
Bloc() {
_model = Model();
final update = PublishSubject<void>();
this.update = update;
final data = BehaviorSubject<String>(seedValue: "");
this.data = data;
update.map((event) => _model.update()).listen((event) => data.sink.add(_model.getData()));
}
}
model.dart
class Model {
static Model _model;
factory Model() { // model is singleton.
_model ??= Model._();
return _model;
}
Model._();
int _data = 0;
void update() {
_data++;
}
String getData() {
return _data.toString();
}
}
StreamBuilder updates the data whenever it gets changed not when just by calling stream
RouteA
class RouteA extends StatefulWidget {
#override
_RouteAState createState() => _RouteAState();
}
class _RouteAState extends State<RouteA> {
#override
Widget build(BuildContext context) {
return Column(
children: [
StreamBuilder( // this StreamBuilder never updates on resumed state
stream: bloc.data, // mistake, fixed. before: bloc.count
builder: (_, snapshot) => Text(
snapshot.data ?? "",
)),
RaisedButton(
child: Text("Move to route B"),
onPressed: () {
Navigator.of(context).push(MaterialPageRoute(builder: (ctx) {
return RouteB();
}));
},
),
],
);
}
}
Route B
class RouteB extends StatefulWidget {
#override
_RouteBState createState() => _RouteBState();
}
class _RouteBState extends State<RouteB> {
#override
Widget build(BuildContext context) {
return Center(
child: RaisedButton(
child: Text("Update data"),
onPressed: () {
bloc.updateData();
},
),
);
}
}
Bloc
class Bloc {
final _update = PublishSubject<String>();
Model _model = Model();
Stream<String> get data => _update.stream;
void updateData() async {
_model.update();
_update.sink.add(_model.getData());
_update.stream.listen((event) {
print(event);
});
}
dispose() {
_update.close();
}
}
final bloc = Bloc();
just follow above changes, it will do the trick for you.

BlocBuilder() not being updated after BLoC yielding new state

I am new to the BLoC pattern on flutter and i'm trying to rebuild a messy flutter app using it. Currently, I intend to get a list of user's apps and display them with a ListView.builder(). The problem is that whenever the state of my AppsBloc changes, my StatelessWidget doesn't update to show the new state. I have tried:
Using MultiBlocProvider() from the main.dart instead of nesting this appsBloc inside a themeBloc that contains the whole app
Returning a list instead of a Map, even if my aux method returns a correct map
Using a StatefulWidget, using the BlocProvider() only on the ListView...
I have been reading about this problem on similar projects and the problem might be with the Equatable. However, I haven't been able to identify any error on that since I'm also new using Equatable. I have been debugging the project on VScode with a breakpoint on the yield* line, and it seems to be okay. In spite of that the widget doesn't get rebuilt: it keeps displaying the textcorresponding to the InitialState.
Moreover, the BLoC doesn't print anything on console even though all the states have an overwritten toString()
These are my 3 BLoC files:
apps_bloc.dart
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:device_apps/device_apps.dart';
import 'package:equatable/equatable.dart';
part 'apps_event.dart';
part 'apps_state.dart';
class AppsBloc extends Bloc<AppsEvent, AppsState> {
#override
AppsState get initialState => AppsInitial();
#override
Stream<AppsState> mapEventToState(AppsEvent event) async* {
yield AppsLoadInProgress();
if (event is AppsLoadRequest) {
yield* _mapAppsLoadSuccessToState();
}
}
Stream<AppsState> _mapAppsLoadSuccessToState() async* {
try {
final allApps = await DeviceApps.getInstalledApplications(
onlyAppsWithLaunchIntent: true, includeSystemApps: true);
final listaApps = allApps
..sort((a, b) =>
a.appName.toLowerCase().compareTo(b.appName.toLowerCase()));
final Map<Application, bool> res =
Map.fromIterable(listaApps, value: (e) => false);
yield AppsLoadSuccess(res);
} catch (_) {
yield AppsLoadFailure();
}
}
}
apps_event.dart
part of 'apps_bloc.dart';
abstract class AppsEvent extends Equatable {
const AppsEvent();
#override
List<Object> get props => [];
}
class AppsLoadRequest extends AppsEvent {}
apps_state.dart
part of 'apps_bloc.dart';
abstract class AppsState extends Equatable {
const AppsState();
#override
List<Object> get props => [];
}
class AppsInitial extends AppsState {
#override
String toString() => "State: AppInitial";
}
class AppsLoadInProgress extends AppsState {
#override
String toString() => "State: AppLoadInProgress";
}
class AppsLoadSuccess extends AppsState {
final Map<Application, bool> allApps;
const AppsLoadSuccess(this.allApps);
#override
List<Object> get props => [allApps];
#override
String toString() => "State: AppLoadSuccess, ${allApps.length} entries";
}
class AppsLoadFailure extends AppsState {
#override
String toString() => "State: AppLoadFailure";
}
main_screen.dart
class MainScreen extends StatelessWidget {
const MainScreen({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return TabBarView(
children: <Widget>[
HomeScreen(),
BlocProvider(
create: (BuildContext context) => AppsBloc(),
child: AppsScreen(),
)
,
],
);
}
}
apps_screen.dart
class AppsScreen extends StatelessWidget {
const AppsScreen({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
margin: EdgeInsets.fromLTRB(30, 5, 10, 0),
child: Column(children: <Widget>[
Row(
children: <Widget>[
Text("Apps"),
],
),
Row(children: <Widget>[
Container(
width: MediaQuery.of(context).size.width - 50,
height: MediaQuery.of(context).size.height - 150,
child: BlocBuilder<AppsBloc, AppsState>(
builder: (BuildContext context, AppsState state) {
if (state is AppsLoadSuccess)
return Text("LOADED");
else if (state is AppsInitial)
return GestureDetector(
onTap: () => AppsBloc().add(AppsLoadRequest()),
child: Text("INITIAL"));
else if (state is AppsLoadInProgress)
return Text("LOADING...");
else if (state is AppsLoadFailure)
return Text("LOADING FAILED");
},
),
),
])
])),
);
}
}
In GestureDetector.onTap() you create a new AppsBloc(), this is wrong. So, you need:
apps_screen.dart:
AppsBloc _appsBloc;
#override
void initState() {
super.initState();
_appsBloc = BlocProvider.of<AppsBloc>(context);
}
//...
#override
Widget build(BuildContext context) {
//...
return GestureDetector(
onTap: () => _appsBloc.add(AppsLoadRequest()),
child: Text("INITIAL")
);
//...
}
Or you can do the same even without the _appsBloc field:
BlocProvider.of<AppsBloc>(context).add(AppsLoadRequest())