Flutter - trigger navigation when Provider variable changes - flutter

I'm trying to show a splash screen on initial app startup until I have all of the data properly retrieved. The retrieval is done by a class called "ProductData" As soon as it's ready, I want to navigate from the splash page to the main screen of the app.
Unfortunately, I can't find a good way to trigger a method that runs that kind of Navigation and listens to a Provider.
This is the code that I'm using to test this idea. Specifically, I want to run the command Navigator.pushNamed(context, 'home'); when the variable shouldProceed becomes true. Unfortunately, the code below gives me the error, "setState() or markNeedsBuild() called during build."
import 'package:catalogo/firebase/ProductData.dart';
import 'package:flutter/material.dart';=
import 'package:provider/provider.dart';
class RouteSplash extends StatefulWidget {
#override
_RouteSplashState createState() => _RouteSplashState();
}
class _RouteSplashState extends State<RouteSplash> {
bool shouldProceed = false;
#override
Widget build(BuildContext context) {
shouldProceed =
Provider.of<ProductData>(context, listen: true).shouldProceed;
if (shouldProceed) {
Navigator.pushNamed(context, 'home'); <-- The error occurs when this line is hit.
} else {
return Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
}
}
Is there a better way to navigate to a page based on listening to the results of a provider?

Instead of trying to navigate to a new view what you should do is display the loading splash screen if you are still waiting for data and once that changes display your main home view, like this:
import 'package:catalogo/firebase/ProductData.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class Main extends StatefulWidget {
#override
_MainState createState() => _MainState();
}
class _MainState extends State<Main> {
bool shouldProceed = Provider.of<ProductData>(context, listen: true).shouldProceed;
#override
Widget build(BuildContext context) {
if(shouldProceed){
return Home();
}else{
return RouteSplash();
}
}
}

Use BlocListener as in this example:
BlocListener(
bloc: BlocProvider.of<DataBloc>(context),
listener: (BuildContext context, DataState state) {
if (state is Success) {
Navigator.of(context).pushNamed('/details');
}
},
child: BlocBuilder(
bloc: BlocProvider.of<DataBloc>(context),
builder: (BuildContext context, DataState state) {
if (state is Initial) {
return Text('Press the Button');
}
if (state is Loading) {
return CircularProgressIndicator();
}
if (state is Success) {
return Text('Success');
}
if (state is Failure) {
return Text('Failure');
}
},
}
)
Source: https://github.com/felangel/bloc/issues/201

I think I have a solution that does what the OP wants. If you make your splash screen to be Stateful, then you can add a PostFrameCallback. This avoids any problems with Navigator being called when build is running. Your callback can then call whatever routine Provider needs to read the data. This read data routine can be passed a further callback which contains the Navigator command.
In my solution I've added a further callback so that the splash screen is visible for at least one second (you can choose what duration you think reasonable here). Unfortunately, this creates a race condition, so I need to import the synchronized package to avoid problems.
import 'package:flutter/material.dart';
import 'package:reflect/utils/constants.dart';
import 'category_screen.dart';
import 'package:provider/provider.dart';
import 'package:reflect/data_models/app_prefs.dart';
import 'dart:async';
import 'dart:core';
import 'package:synchronized/synchronized.dart';
class LoadingScreen extends StatefulWidget {
static const id = 'LoadingScreen';
#override
_LoadingScreenState createState() => _LoadingScreenState();
}
class _LoadingScreenState extends State<LoadingScreen> {
bool readPrefsDone = false;
bool timeFinished = false;
Lock _lock = Lock();
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
Provider.of<AppPrefs>(context, listen: false).readPrefs(readDone);
Timer(Duration(seconds: 1), () {
timerDone();
});
});
}
void timerDone() async {
_lock.synchronized(() {
if (readPrefsDone) {
pushMainScreen();
}
timeFinished = true;
});
}
void readDone() {
_lock.synchronized(() {
if (timeFinished) {
pushMainScreen();
}
readPrefsDone = true;
});
}
void pushMainScreen() {
Navigator.pushReplacement(
context,
PageRouteBuilder(
pageBuilder: (context, animation, animation2) => CategoryScreen(),
transitionDuration: Duration(seconds: 1),
),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
color: Colors.white,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Hero(
tag: kFishLogoTag,
child: Image(
image: AssetImage('assets/fish_logo.png'),
),
),
SizedBox(
height: 30,
),
Text(
'Reflect',
style: TextStyle(
fontSize: 30,
color: Color(0xFF0000cc),
fontWeight: FontWeight.bold,
),
),
],
),
),
));
}
}

Anyone else facing this issue can use this code
Future.delayed(Duration.zero, () => Navigate.toView(context));
This navigates to the other screen without build errors

Related

Why flutter Navigator.of(context).push(MaterialPageRoute(()) not working

I have a small app here, i will check buildNumber of current app and compare to my remote api data, based on this condition i will show the user interfaces.
I have home and updateApp screen where home is the normal webview screen and UpdateApp is a screen where user is required to update the new version of my app.
But condition satisfies but update screen is not showing.
// ignore_for_file: prefer_const_constructors, prefer_const_literals_to_create_immutables, use_build_context_synchronously, unrelated_type_equality_checks, unused_element
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart';
import 'package:webview_test/models/app_version.dart';
import 'package:webview_test/services/remote_service.dart';
import 'package:webview_test/views/update_app.dart';
import 'package:package_info_plus/package_info_plus.dart';
void main() {
runApp(MyHomePage());
}
class MyHomePage extends StatefulWidget {
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final flutterWebViewPlugin = FlutterWebviewPlugin();
bool isLoading = true;
double webProgress = 0;
bool isLoaded = false;
List<AppVersion>? appVersions;
int buildNumber = 0;
late String packageName;
#override
#override
void initState() {
super.initState();
flutterWebViewPlugin.onProgressChanged.listen((double progress) {
setState(() {
this.webProgress = progress;
});
print("The progress is $progress");
});
getVersions();
getBuild();
}
//Fetching remote data for app versions.
getVersions() async {
appVersions = await RemoteService().getAppVersion();
if (appVersions != null) {
setState(() {
isLoaded = true;
});
}
}
//getting app information to compare remote app versions.
getBuild() async {
PackageInfo packageInfo = await PackageInfo.fromPlatform();
packageName = packageInfo.packageName;
buildNumber = int.parse(packageInfo.buildNumber);
print("build number is $buildNumber");
if (buildNumber == 1) {
print("Build number is $buildNumber");
}
}
#override
Widget build(BuildContext context) {
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
systemNavigationBarColor: Colors.white,
systemNavigationBarIconBrightness: Brightness.dark));
return MaterialApp(
home: buildNumber == 1
? proceedToUpdate(context)
: SafeArea(
child: Scaffold(
body: WillPopScope(
onWillPop: () async {
if (await flutterWebViewPlugin.canGoBack()) {
flutterWebViewPlugin.goBack();
return false;
} else {
SystemNavigator.pop();
return true;
}
},
child: Stack(
children: [
Positioned.fill(
child: Column(
children: [
webProgress < 1
? SizedBox(
height: 5,
child: LinearProgressIndicator(
value: webProgress,
color: Colors.blue,
backgroundColor: Colors.white,
),
)
: SizedBox(),
Expanded(
child: WebviewScaffold(
url: "https://google.com",
mediaPlaybackRequiresUserGesture: false,
withLocalStorage: true,
),
),
// isLoading
// ? Center(
// child: CircularProgressIndicator(),
// )
// : Stack(),
],
),
),
],
)),
),
),
);
}
proceedToUpdate(context) {
Navigator.of(context)
.push(MaterialPageRoute(builder: (context) => UpdateApp()));
}
}
Your variable context in Navigator.of(context).push(...) isn't correct.
You're trying to navigate outside build(BuildContext context), so it won't work. Function build(BuildContext context) is the place where it build your mobile interface - UI screen.
Now in your StatefulWidget MyHomePage -> initState() -> getBuild() -> _proceedToUpdate() -> Navigator.of(context).push(...). The variable context in your Navigator command is not context of your screen UI. Even though function _proceedToUpdate() can run, it cannot navigate.
You may try to show an dialog. Each dialog also has a context. You can show an dialog and then navigate to somewhere when press "OK" button. It'll success.
Good luck!
Update:
Seems like you don't want to show any dialog, therefore we need another approach. You could check the build version in main() async {}. Then pass value buildNumber to somewhere (directly pass to MyApp() or use singleton to make it more professional :D). Then you can make it like: home: _getFirstScreen()
_getFirstScreen() {
if (buildNumber == 1) return UpdateScreen();
else return MyHomePage();
}

Listening of bloc getting called multiple times

I have this code for listening to bloc on my screen.
late MyBloc myBloc;
#override
void initState() {
print('inside init state');
super.initState();
myBloc = BlocProvider.of<MyBloc>(context);
myBloc.stream.listen((state) {
if (state is MyAddCompletedState) {
print('listening to bloc');
}
}
}
If I add an event, it will print listening to bloc once. If I go to another screen and return to this same screen, it will print listening to bloc twice. It seems like the first listen I did was still active. Next, I tried to close the bloc on my dispose thinking that it would stop the first listen. So that when I come back to the screen it will have a fresh listen but it will have an error: Bad state: Cannot add new events after calling close. I tried to research about this and some mention to dispose the bloc but it doesn't have that syntax anymore. Please help on how to properly close or stop it from listening once I have change screen. Thanks!
//this is found on my screen
late MyBloc myBloc;
#override
void initState() {
print('inside init state');
super.initState();
myBloc = BlocProvider.of<MyBloc>(context);
myBloc.stream.listen((state) {
if (state is MyAddCompletedState) {
print('listening to bloc');
}
}
}
#override
void dispose() {
myBloc.close();
// myBloc.dispose(); --> I saw some tutorial to use this but it doesn't work
super.dispose();
}
This is on my main.dart:
return FutureBuilder(
future: InitFirst.instance.initialize(),
builder: (context, AsyncSnapshot snapshot) {
return MultiBlocProvider(
providers: [
BlocProvider<MyBloc>(
create: (context) => MyBloc(
authenticationRepository: authenticationRepository,
userDataRepository: userDataRepository,
),
),
...
This is the part where I trigger the event. After this event run, the stream.listen will get triggered. But it will be triggered multiple times every time I visit the my screen.
myBloc.add(MyAddEvent(
selectedName,
selectedCount);
Additional note: this event is triggering an update in Firebase which I need to check if it got completed that is why I do the stream.listen.
If the Stream used in Bloc keeps getting called when not in use, you may want to consider terminating the Stream with cancel() on your dispose() override. Try this
late MyBloc myBloc;
late StreamSubscription mSub;
#override
void initState() {
print('inside init state');
super.initState();
myBloc = BlocProvider.of<MyBloc>(context);
mSub = myBloc.stream.listen((state) {
if (state is MyAddCompletedState) {
print('listening to bloc');
}
}
}
#override
void dispose() {
mSub.cancel();
super.dispose();
}
There are several ways to solve this issue depending on the context. I try to avoid using BLoC instantiation with StatefulWidgets. And, I like to use Cubits in connection with Observers, depending on the event entering my stream. I have added most of them in the following code, which isn't all used but for you to look at as a reference. My code example eliminates the issues that you describe. I would be happy to help further if you could provide a minimum viable code.
The following code is an example that I have put together to demonstrate a possible strategy. The BLoC package website heavily inspires the code. It has the standard counter app that we are all familiar with and navigation functionality.
Please see the following code to see if it helps at all:
Please be sure to add
flutter_bloc: ^8.0.1 to your pubspec.yaml file.
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
void main() {
BlocOverrides.runZoned(
() => runApp(const MaterialApp(home: CounterPage())),
blocObserver: CounterObserver(),
);
}
class CounterObserver extends BlocObserver {
#override
void onChange(BlocBase bloc, Change change) {
super.onChange(bloc, change);
print('${bloc.runtimeType} $change');
}
#override
void onTransition(Bloc bloc, Transition transition) {
super.onTransition(bloc, transition);
print('onTransition -- bloc: ${bloc.runtimeType}, transition: $transition');
}
#override
void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
print('onError -- bloc: ${bloc.runtimeType}, error: $error');
super.onError(bloc, error, stackTrace);
}
#override
void onClose(BlocBase bloc) {
super.onClose(bloc);
print('onClose -- bloc: ${bloc.runtimeType}');
}
}
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
void decrement() => emit(state - 1);
}
class ScoreCubit extends Cubit<int> {
ScoreCubit() : super(0);
void increment() => emit(state + 1);
void decrement() => emit(state - 1);
}
class CounterPage extends StatelessWidget {
const CounterPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<CounterCubit>(create: (context) => CounterCubit()),
BlocProvider<ScoreCubit>(create: (context) => ScoreCubit()),
],
child: const CounterView(),
);
}
}
class CounterView extends StatelessWidget {
const CounterView({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Counter')),
body: Column(
children: [
Center(
child: BlocBuilder<ScoreCubit, int>(
builder: (context, score) => Text('Score: $score'),
),
),
Center(
child: BlocBuilder<CounterCubit, int>(
builder: (context, state) {
return Text('$state',
style: Theme.of(context).textTheme.headline2);
},
),
),
],
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
FloatingActionButton(
heroTag: 'FAB1',
child: const Icon(Icons.add),
onPressed: () {
// format from
context.read<CounterCubit>().increment();
context.read<ScoreCubit>().increment();
}),
const SizedBox(height: 8),
FloatingActionButton(
heroTag: 'FAB2',
child: const Icon(Icons.remove),
onPressed: () {
context.read<CounterCubit>().decrement();
},
),
const SizedBox(height: 8),
FloatingActionButton(
heroTag: 'FAB3',
child: const Icon(Icons.arrow_forward),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => Scaffold(
appBar: AppBar(title: const Text('Next Page')),
body: const Center(
child: Text('This is the next page'),
),
),
),
);
},
),
],
),
);
}
}
Like all things in Flutter, this, of course, is only one strategy of many.

In Flutter, make a HTTP request, then depending on response, update the UI or open a new page

I am developing a Flutter app where it acts as a client, connecting to a server via an API.
The app makes requests and depending on the response it progresses the state.
My question is the following: Can I make a request, and then depending on the response, either update the UI or open a new page?
I have used FutureBuilder as shown below. The problem is that the FutureBuilder requires to return a UI. In my case, if the response is OK I want to open a new page (see //todo line).
I tried using Navigator.pushReplacement but it does not really work.
Any ideas?
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:flutter/rendering.dart';
import 'model.dart';
class Start extends StatefulWidget {
final String title;
Start({Key key, #required this.title}) : super(key: key);
#override
State<StatefulWidget> createState() => new StartState();
}
class StartState extends State<Start> {
Future<StartReply> _startReply;
_makeRequest() {
setState(() {
_startReply = ...; // actual API request
});
}
#override
Widget build(BuildContext context) {
return MaterialApp(
title: widget.title,
home: Scaffold(
appBar: AppBar(
title: Text(widget.title),
leading: IconButton(
icon: Icon(Icons.arrow_back),
onPressed: () => Navigator.of(context).pop(false)
),
),
body: Center(
child: FutureBuilder(
future: _startReply,
builder: (context, snapshot) {
if(snapshot.connectionState == ConnectionState.none) {
return ElevatedButton(
onPressed: _makeRequest,
child: Text("Make request")
);
} else if(snapshot.connectionState == ConnectionState.done && snapshot.hasData) {
// todo open page here
return Text('Started!', style: TextStyle(color: Colors.green, fontStyle: FontStyle.italic));
} else if(snapshot.hasError) {
debugPrint('StartReply: ${snapshot.data}');
return Text('Error (${snapshot.error})', style: TextStyle(color: Colors.red, fontStyle: FontStyle.italic));
} else {
return CircularProgressIndicator();
}
}
)
)
)
);
}
}
Yes, you should not use a FutureBuilder if you want to do anything other than changing the UI depending on the async task. You should manage your own async. Here's some code to get you started:
class MyWidget extends StatefulWidget {
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
bool loaded;
#override
void initState() {
super.initState();
asyncInit();
}
Future<void> asyncInit() async {
final response =
await doTheNetworkRequest() //imagine that this was an http request
if (yes) {
setState(() {
loaded = true;
});
} else {
Navigator.of(context).push(...);
}
}
#override
Widget build(BuildContext context) {
return loaded == true ? Text('Loaded') : Text('Loading');
}
}

Flutter Streambuilder snapshot is null after moving to OnGenerateRoute

Back again. I refactored my code after the advice in this thread: Flutter Multiple Blocs and NamedRoutes
However, since moving my bloc from the main material app tree to the router page, the data isn't loading as snapshot is null.
The Router:
class AppRouter {
final _centresBloc = CentresBloc();
Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) {
case routes.CentreSelectScreenRoute:
return MaterialPageRoute(
builder: (_) => BlocProvider(
bloc: _centresBloc,
child: CentreSelectScreen(),
),
);
default:
return MaterialPageRoute(builder: (context) => HomeScreen());
}
and the CentreSelectScreen class itself
class _CentreSelectScreenState extends State<CentreSelectScreen> {
#override
Widget build(BuildContext context) {
final _centresBloc = BlocProvider.of<CentresBloc>(context);
return Scaffold(
body: Container(
child: StreamBuilder<List<ClimbingCentre>>(
stream: _centresBloc.centres,
builder: (context, snapshot) {
print('snapshot == ${snapshot.data}'); //is always null now
if (snapshot.hasData) {
// If there are no centres (data), display this message.
if (snapshot.data.length == 0) {
return Text('No Centres listed');
}...
The blocprovider was originally in the centreselect screen class which all worked fine, but since moving it, it's not working, and I can't seem to figure out why.
Th blog itself seems to initialise properly when the app first loads, as it is printing out all the correct information. From CentresBloc:
void getCentres() async {
// Retrieve all the centres from the database
List<ClimbingCentre> centres = await ClimbDB.db.getCentres();
// Add all of the centres to the stream so we can grab them later from our pages
_inCentres.add(centres);
print('BLOC incentres is $centres'); //this works and prints all centres when the app first loads...
}
Any help most appreciated.
EDIT Adding CentresBloc Class
import 'dart:async';
import 'package:flutterapp/data/database.dart';
import 'package:flutterapp/models/centre_model.dart';
import 'bloc_provider.dart';
class CentresBloc implements BlocBase {
final _centresController = StreamController<List<ClimbingCentre>>.broadcast();
// Input stream. Add centres to the stream using this variable.
StreamSink<List<ClimbingCentre>> get _inCentres => _centresController.sink;
// Output stream. This one will be used within our pages to display the centres.
Stream<List<ClimbingCentre>> get centres => _centresController.stream;
CentresBloc() {
// Retrieve all the climbing centres on initialization
getCentres();
}
#override
void dispose() {
_centresController.close();
}
void getCentres() async {
// Retrieve all the centres from the database
List<ClimbingCentre> centres = await ClimbDB.db.getCentres();
// Add all of the centres to the stream so we can grab them later from our pages
_inCentres.add(centres);
print('CentreBloc _incentres is: $centres'); //this prints the correct centres when the app is first loaded
}
After a new route is pushed, StreamBuilder subscribes to the same stream _centresBloc.centres, but no new events are emitted on this stream, as it only happens once - during bloc's initialization in constructor. That's because Dart's default StreamController won't send previous events/values (including the last one) to stream's new subscribers.
However, you can use BehaviorSubject from rxdart library, which is based on StreamController, but it also stores the last value emitted and sends it to any new subscriber. Subjects are also always broadcast streams and can have initial (seed) value.
Just replace this:
final _centresController = StreamController<List<ClimbingCentre>>.broadcast();
with this:
final _centresController = BehaviorSubject<List<ClimbingCentre>>();
Full working code:
import 'dart:async';
import 'package:bloc_provider/bloc_provider.dart';
import 'package:flutter/material.dart';
import 'package:rxdart/subjects.dart';
void main() => runApp(MyApp2());
class CentresBloc implements Bloc {
final _centresController = BehaviorSubject<List<String>>();
// Input stream. Add centres to the stream using this variable.
StreamSink<List<String>> get _inCentres => _centresController.sink;
// Output stream. This one will be used within our pages to display the centres.
Stream<List<String>> get centres => _centresController.stream;
CentresBloc() {
// Retrieve all the climbing centres on initialization
getCentres();
}
void dispose() {
_centresController.close();
}
void getCentres() async {
// Retrieve all the centres from the database
List<String> centres = ['LIST'];
// Add all of the centres to the stream so we can grab them later from our pages
_inCentres.add(centres);
print('CentreBloc _incentres is: $centres'); //this prints the correct centres when the app is first loaded
}
}
class AppRouter {
final _centresBloc = CentresBloc();
Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) {
case 'test':
return MaterialPageRoute(
builder: (_) => BlocProvider<CentresBloc>.fromBloc(
bloc: _centresBloc,
child: CentreSelectScreen(),
),
);
default:
return MaterialPageRoute(builder: (context) => HomeScreen());
}
}
void dispose() {
_centresBloc.dispose();
}
}
class MyApp2 extends StatefulWidget {
#override
_MyApp2State createState() => _MyApp2State();
}
class _MyApp2State extends State<MyApp2> {
final _router = AppRouter();
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
onGenerateRoute: _router.generateRoute,
);
}
#override
void dispose() {
_router.dispose();
super.dispose();
}
}
class CentreSelectScreen extends StatefulWidget {
#override
State<StatefulWidget> createState() => _CentreSelectScreenState();
}
class _CentreSelectScreenState extends State<CentreSelectScreen> {
#override
Widget build(BuildContext context) {
final _centresBloc = BlocProvider.of<CentresBloc>(context);
return Scaffold(
body: Container(
child: SafeArea(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
MaterialButton(
child: Text('push index'),
onPressed: () {
Navigator.pushReplacementNamed(context, '/');
},
),
StreamBuilder<List<String>>(
stream: _centresBloc.centres,
builder: (context, snapshot) {
print('snapshot == ${snapshot.data}'); //is always null now
if (snapshot.hasData) {
// If there are no centres (data), display this message.
if (snapshot.data.length == 0) {
return Text('No Centres listed');
} else {
return Text(snapshot.data.toString());
}
}
return Container();
}
),
],
),
),
),
),
);
}
}
class HomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Center(
child: MaterialButton(
onPressed: () => Navigator.pushReplacementNamed(context, 'test'),
child: Text('push test')
)
)
)
);
}
}

Unnecessary Widget Rebuilds While Using Selector (Provider) inside StreamBuilder

I am using a Selector which rebuilds when a data in Bloc changes. Which woks fine but when the data changes it reloads the whole tree not just the builder inside Selector.
In my case the selector is inside a StreamBuilder. I need this because the stream is connected to API. So inside the stream I am building some widget and One of them is Selector. Selector rebuilds widgets which is depended on the data from the Stream.
Here is My Code. I dont want the Stream to be called again and again. Also the Stream gets called because the build gets called every time selector widget rebuilds.
main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:provider_test/data_bloc.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MultiProvider(providers: [
ChangeNotifierProvider<DataBloc>(
create: (_) => DataBloc(),
)
], child: ProviderTest()),
);
}
}
class ProviderTest extends StatefulWidget {
#override
_ProviderTestState createState() => _ProviderTestState();
}
class _ProviderTestState extends State<ProviderTest> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: <Widget>[
Text("Outside Stream Builder"),
StreamBuilder(
stream: Provider.of<DataBloc>(context).getString(),
builder: (_, AsyncSnapshot<String> snapshot) {
if (snapshot.hasData) {
return Column(
children: <Widget>[
Text("Widget Generated by Stream Data"),
Text("Data From Strem : " + snapshot.data),
RaisedButton(
child: Text("Reload Select"),
onPressed: () {
Provider.of<DataBloc>(context, listen: false).changeValue(5);
}),
Selector<DataBloc, int>(
selector: (_, val) =>
Provider.of<DataBloc>(context, listen: false).val,
builder: (_, val, __) {
return Container(
child: Text(val.toString()),
);
}),
],
);
}
return Container();
},
)
],
),
);
}
}
bloc.dart
import 'package:flutter/foundation.dart';
class DataBloc with ChangeNotifier {
int _willChange = 0;
int get val => _willChange;
void changeValue(int val){
_willChange++;
notifyListeners();
}
Stream<String> getString() {
print("Stream Called");
return Stream.fromIterable(["one", "two", "three"]);
}
}
Also if I remove the StreamBuilder then the Selector acts like its suppose to. Why does StreamBuilder Rebuilds in this case? Is there anyway to prevent this?
Based on the code that you've shared, you can create a listener to your Stream on your initState that updates a variable that keeps the most recent version of your data, and then use that variable to populate your widgets. This way the Stream will only be subscribed to the first time the Widget loads, and not on rebuilds. I can't test it directly as I don't have your project. But please try it out.
Code example based on your code
class ProviderTest extends StatefulWidget {
#override
_ProviderTestState createState() => _ProviderTestState();
}
class _ProviderTestState extends State<ProviderTest> {
String _snapshotData;
#override
void initState() {
listenToGetString();
super.initState();
}
void listenToGetString(){
Provider.of<DataBloc>(context).getString().listen((snapshot){
setState(() {
_snapshotData = snapshot.data;
});
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: <Widget>[
Text("Outside Stream Builder"),
Column(
children: <Widget>[
Text("Widget Generated by Stream Data"),
Text("Data From Strem : " + _snapshotData),
RaisedButton(
child: Text("Reload Select"),
onPressed: () {
Provider.of<DataBloc>(context, listen: false).changeValue(5);
}
),
Selector<DataBloc, int>(
selector: (_, val) =>
Provider.of<DataBloc>(context, listen: false).val,
builder: (_, val, __) {
return Container(
child: Text(val.toString()),
);
}
),
],
)
],
),
);
}
}
I found the problem after reading this blog post here. I lacked the knowlwdge on how the Provider lib works and how its doing all the magic stuff out of Inherited widgets
The point and quote that solves this problem is. ( A quation from the blog post above)
When a Widget registers itself as a dependency of the Provider’s
InheritedWidget, that widget will be rebuilt each time a variation in
the “provided data” occurs (more precisely when the notifyListeners()
is called or when a StreamProvider’s stream emits new data or when a
FutureProvider’s future completes).
That means the variable that i am changing and the Stream that i am listning to, exists in the Same Bloc! that was the mistake. So when I change the val and call notifyListener() in a single bloc, all things reloads which is the default behaviour.
All I had to do to solve this problem is to make another Bloc and Abstract the Stream to that particular bloc(I think its a Good Practice also). Now the notifyListener() has no effect on the Stream.
data_bloc.dart
class DataBloc with ChangeNotifier {
int _willChange = 0;
String data = "";
int get val => _willChange;
void changeValue(int val){
_willChange++;
notifyListeners();
}
Future<String> getData () async {
return "Data";
}
}
stream_bloc.dart
import 'package:flutter/foundation.dart';
class StreamBloc with ChangeNotifier {
Stream<String> getString() {
print("Stream Called");
return Stream.fromIterable(["one", "two", "three"]);
}
}
And the problem is solved. Now the Stream will only be called if its invoked but not when the variable changes in the data_bloc