Flutter streambuilder between screens - flutter

I'm new in Flutter and I implemented the bloc architecture with streambuilder.
I created 2 pages with just a button which change my background color. All of theses pages are listening a stream to change the background color but when I change on the first page, it doesn't on the second.
But I want all my application change if 1 page decide to change it
Do I need to initialize a singleton bloc that my 2 screens used it ? Because for the moment each screen initializes its own bloc
Here is an example of 1 page (the second one is the same)
class Test extends StatelessWidget {
final ColorBloc _bloc = ColorBloc();
#override
Widget build(BuildContext context) {
return StreamBuilder<Response<ColorResponse>>(
stream: _bloc.stream,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Scaffold(
appBar: AppBar(
title: Text('First Route clicked'),
),
backgroundColor: snapshot.data.data.color,
body: new Center(
child: new InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => Act2()),
);
}, // Handle your callback
child: Ink(height: 100, width: 100, color: Colors.blue),
)),
floatingActionButton: FloatingActionButton(
onPressed: () {
_bloc.changeColor(Colors.yellow);
},
child: Icon(Icons.navigation),
backgroundColor: Colors.green,
));
}
return Scaffold(
appBar: AppBar(
title: Text('First Route'),
),
body: Center(
child: new InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => Act2()),
);
}, // Handle your callback
child: Ink(height: 200, width: 200, color: Colors.red))),
floatingActionButton: FloatingActionButton(
onPressed: () {
_bloc.changeColor(Colors.yellow);
},
child: Icon(Icons.navigation),
backgroundColor: Colors.green,
));
},
);
}
}

To change the state of all screen when a bloc fires an event, you can use multiple StreamBuilder, but all of them need to listen to the bloc that fire the event. You can try these 2 ways:
Passing the bloc as parameter into the 2nd screen
class Test extends StatelessWidget {
final ColorBloc _bloc = ColorBloc();
#override
Widget build(BuildContext context) {
return StreamBuilder<Response<ColorResponse>>(
// ... other lines
body: new Center(
child: new InkWell(
onTap: () {
// Pass your bloc to the 2nd screen
Navigator.push(
context,
MaterialPageRoute(builder: (context) => Act2(bloc: _bloc)),
);
},
// ... other lines
Use package such as provider package to pass the bloc down the tree. In your first screen, you can do this:
class Test extends StatelessWidget {
final ColorBloc _bloc = ColorBloc();
#override
Widget build(BuildContext context) {
// Use Provider to provide the bloc down the widget tree
return Provider(
create: (_) => _bloc,
child: StreamBuilder<Response<ColorResponse>>(
// ... other lines
Then in the 2nd screen (which I assume is Act2()), you get the ColorBloc from the Provider:
class Act2 extends StatefulWidget {
#override
_Act2State createState() => _Act2State();
}
class _Act2State extends State<Act2> {
ColorBloc _colorBloc;
#override
void didChangeDependencies() {
// Get the bloc in the 1st page
_colorBloc = Provider.of<ColorBloc>(context);
super.didChangeDependencies();
}
#override
Widget build(BuildContext context) {
return StreamBuilder<Response<ColorResponse>>(
// Use the bloc like in the 1st page
stream: _colorBloc.stream,
builder: (context, snapshot) {
if (snapshot.hasData) {
// ... other lines
Small note: When using StreamBuilder you could initiate the value without the need to duplicate codes. Since I don't know the structure of your Response object, I'm taking Response(ColorResponse(color: Colors.green)) as the example:
// ... other lines
#override
Widget build(BuildContext context) {
return Provider(
create: (_) => _bloc,
child: StreamBuilder<Response<ColorResponse>>(
// Initiate your data here
initialData: Response(ColorResponse(color: Colors.green)),
stream: _bloc.stream,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Scaffold(
appBar: AppBar(
title: Text('First Route clicked'),
),
backgroundColor: snapshot.data.data.color,
// ... other lines
}
// Don't need to copy the above code block for the case when the data is not streamed yet
return Container(child: Center(child: CircularProgressIndicator()));
},
),
);
}

Related

How to Passing Data from Navigator Pop to Previous Page Where The Data is Used in The Widget Inside the ListView.builder

As stated in the title. How to return data to the previous page where the data is used to list widgets.
I have read this article Flutter Back button with return data or other similar articles. The code works perfectly. But there is a problem if I want to use the data returned to the widget that is in the list.\
Note that I only want to update one ListWidget, I don't want to refresh the state of the entire HomePage like the solution in this article Flutter: Refresh on Navigator pop or go back.
Here is a simple code sample to represent the problem I'm facing.
(check on ListWidget Class and SecondPage Class below)
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: HomePage(),
);
}
}
HomePage class
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Home'),
),
body: Center(
child: ListView.builder(
itemCount: 4,
itemBuilder: (_, index){
return ListWidget(number: index+1);
},
)
),
);
}
}
ListWidget Class
class ListWidget extends StatelessWidget{
ListWidget({#required this.number});
final int? number;
String? statusOpen;
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () async {
statusOpen = await Navigator.of(context, rootNavigator: true)
.push(
MaterialPageRoute(
builder: (BuildContext context) => SecondPage(),
),
);
},
child: Container(
margin: EdgeInsets.all(10),
padding: EdgeInsets.all(20),
color: Colors.amber,
child: Text(statusOpen != null ? '$number $statusOpen' : '$number Unopened'),
//
// I want to change the text here to 'has Opened' when the user returns from SecondPage
//
),
);
}
}
SecondPage Class
class SecondPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Second Page'),
),
body: Center(
child: RaisedButton(
onPressed: () {
Navigator.pop(context, 'has Opened');
// return 'has Opened' to await statusOpen variable
},
child: Text('Go Back'),
),
),
);
}
}
is there any solution to handle this?
If you make your listWidget a stateful widget, then you can get the solution where you just need to call setState when you return to your previous screen. And in this way you will be only changing your single list element and not the full screen.
sample code:
changing this line- class ListWidget extends StatefulWidget
and adding these lines -
onTap: () async {
statusOpen = await Navigator.of(context, rootNavigator: true)
.push(
MaterialPageRoute(
builder: (BuildContext context) => SecondPage(),
),
);
setState(() {
});
},
If you used the data in your listview just call setstate after Navigator.pop like below code
onTap: () async {
statusOpen = await Navigator.of(context, rootNavigator: true)
.push(
MaterialPageRoute(
builder: (BuildContext context) => SecondPage(),
),
).then((value) async {
setState(() {});
});
},

Flutter - Could not find the correct Provider

I've got an app having file structure like this: main -> auth -> home -> secret. Key codes are as below:
For main.dart:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
StreamProvider<User>.value(value: AuthService().user),
ChangeNotifierProvider(create: (context) => SecretProvider()),
],
child: MaterialApp(
title: 'My Secrets',
home: AuthScreen(),
),
);
}
}
For home.dart:
class HomeScreen extends StatelessWidget {
final AuthService _auth = AuthService();
#override
Widget build(BuildContext context) {
var secretProvider = Provider.of<SecretProvider>(context);
return ChangeNotifierProvider(
create: (context) => SecretProvider(),
child: Scaffold(
appBar: AppBar(
// some codes...
),
body: StreamBuilder<List<Secret>>(
stream: secretProvider.secrets,
builder: (context, snapshot) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 15.0),
child: ListView.separated(
// return 0 if snapshot.data is null
itemCount: snapshot.data?.length ?? 0,
itemBuilder: (context, index) {
return ListTile(
leading: Icon(Icons.web),
title: Text(snapshot.data[index].title),
trailing: Icon(Icons.edit),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SecretScreen(
secret: snapshot.data[index],
),
),
);
},
);
},
separatorBuilder: (context, index) {
return Divider();
},
),
);
},
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecretScreen()),
);
},
),
),
);
}
}
For secret.dart:
class SecretScreen extends StatefulWidget {
final Secret secret;
SecretScreen({this.secret});
#override
_SecretScreenState createState() => _SecretScreenState();
}
class _SecretScreenState extends State<SecretScreen> {
// some codes...
#override
void initState() {
final secretProvider = Provider.of<SecretProvider>(context, listen: false);
// some codes...
super.initState();
}
#override
void dispose() {
// some codes...
super.dispose();
}
#override
Widget build(BuildContext context) {
final secretProvider = Provider.of<SecretProvider>(context);
return Scaffold(
// some codes...
);
}
}
These codes worked just fine, but later on I decided to move the ChangeNotifierProvider from main.dart to home.dart due to some class instance life cycle issue. The new code is like below:
For main.dart:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
StreamProvider<User>.value(value: AuthService().user),
],
child: MaterialApp(
title: 'My Secrets',
home: AuthScreen(),
),
);
}
}
For home.dart:
class HomeScreen extends StatelessWidget {
final AuthService _auth = AuthService();
#override
Widget build(BuildContext context) {
// var secretProvider = Provider.of<SecretProvider>(context);
return ChangeNotifierProvider(
create: (context) => SecretProvider(),
child: Consumer<SecretProvider>(
builder: (context, secretProvider, child) {
return Scaffold(
appBar: AppBar(
// some codes...
),
body: StreamBuilder<List<Secret>>(
stream: secretProvider.secrets,
// stream: SecretProvider().secrets,
builder: (context, snapshot) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 15.0),
child: ListView.separated(
// return 0 if snapshot.data is null
itemCount: snapshot.data?.length ?? 0,
itemBuilder: (context, index) {
return ListTile(
leading: Icon(Icons.web),
title: Text(snapshot.data[index].title),
trailing: Icon(Icons.edit),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SecretScreen(
secret: snapshot.data[index],
),
),
);
},
);
},
separatorBuilder: (context, index) {
return Divider();
},
),
);
},
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecretScreen()),
);
},
),
);
},
),
);
}
}
Basically, I just moved the ChangeNotifierProvider to home.dart and used a Consumer to pass the context, but this time, whenever I navigate to secret screen, it prompts me error like below:
Could not find the correct Provider<SecretProvider> above this SecretScreen Widget
This likely happens because you used a `BuildContext` that does not include the provider
of your choice.
This BuildContext is really bugging me. Even if I'm having ChangeNotifierProvider one level lower than before, the SecretScreen widget should still be aware of the SecretProvider that passed on from HomeScreen because it's still the child of HomeScreen and according to my knowledge, the context should contain the SecretProvider.
You get this error because your SecretProvider instance is part of HomeScreen which is not a parent of SecretScreen.
In order, when you push a new page, this new page is not a descendent of the previous one so you can't access to inherited object with the .of(context) method.
Here the a schema representing the widget tree to explain the situation :
With a Provider on top of MaterialApp (the navigator) :
Provider
MaterialApp
HomeScreen -> push SecretScreen
SecretScreen -> Here we can acces the Provider by calling Provider.of(context) because the context can access to its ancestors
With a Provider created in HomeScreen :
MaterialApp
HomeScreen -> push SecretScreen
Provider -> The provider is part of HomeScreen
SecretScreen -> The context can't access to the Provider because it's not part of its ancestors
I hope my answer is pretty clear and will help you to understand what happens ;)

Flutter - Why setstate doesn't work on popup?

I want to change the text on popup with user interaction but it is not changing. I've tried navigator.pop(context) and relaunch show method. It correctly work but is it a correct way? And can I change value on the popup without Navigator.pop. Why doesn't it work?
Here is my code.
import 'package:flutter/material.dart';
import 'package:rflutter_alert/rflutter_alert.dart';
void main() => runApp(RatelApp());
class RatelApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: Text('RFlutter Alert by Ratel'),
),
body: Home(),
),
);
}
}
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
String val = "Deneme";
showAlertDialog(BuildContext context, Function onPressed) {
// set up the button
Widget okButton = FlatButton(
child: Text(val),
onPressed: onPressed,
);
// set up the AlertDialog
AlertDialog alert = AlertDialog(
title: Text("My title"),
content: Text("This is my message."),
actions: [
okButton,
],
);
// show the dialog
showDialog(
context: context,
builder: (BuildContext context) {
return alert;
},
);
}
#override
Widget build(BuildContext context) {
return Container(
child: RaisedButton(
child: Text("Show Popup"),
onPressed: () {
showAlertDialog(context, (){
setState(() {
val = "changed";
});
});
},
),
);
}
}
Dialogs are usually stateless and are also not a part of the state of the calling Widget so calling that setState method isn't doing anything for the dialog. To make a dialog that can have changing contents use a StatefulBuilder.
The docs have an example that shows how to use it in a dialog just like in your application.
Docs example:
await showDialog<void>(
context: context,
builder: (BuildContext context) {
int selectedRadio = 0;
return AlertDialog(
content: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Column(
mainAxisSize: MainAxisSize.min,
children: List<Widget>.generate(4, (int index) {
return Radio<int>(
value: index,
groupValue: selectedRadio,
onChanged: (int value) {
setState(() => selectedRadio = value);
},
);
}),
);
},
),
);
},
);
You need to wrap your AlertDialog with StatefulBuilder widget.

Returning to same exact place flutter navigation

Is it possible to return to the exact same place meaning state wise in flutter while using this?
Navigator.of(context).push(new MaterialPageRoute(builder: (BuildContext context) => new ConnectHome(user:widget.user))));
We have cards on the home screen "ConnectHome()" and we need to return them to the same spot.
You can copy paste run full code below
You can await Navigator.push and In Navigator.pop include UserObject()
You can see the code continue execution and print UserObject()
code snippet
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => ConnectHome()),
);
print('result ${result.name}')
...
Navigator.pop(context, UserObject("hello","world"));
working demo
full code
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(
title: 'Returning Data',
home: HomeScreen(),
));
}
class HomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Returning Data Demo'),
),
body: Center(child: SelectionButton()),
);
}
}
class SelectionButton extends StatelessWidget {
#override
Widget build(BuildContext context) {
return RaisedButton(
onPressed: () {
_navigateAndDisplaySelection(context);
},
child: Text('Pick an option, any option!'),
);
}
// A method that launches the SelectionScreen and awaits the result from
// Navigator.pop.
_navigateAndDisplaySelection(BuildContext context) async {
// Navigator.push returns a Future that completes after calling
// Navigator.pop on the Selection Screen.
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => ConnectHome()),
);
print('result ${result.name}');
// After the Selection Screen returns a result, hide any previous snackbars
// and show the new result.
Scaffold.of(context)
..removeCurrentSnackBar()
..showSnackBar(SnackBar(content: Text("${result.name}")));
}
}
class UserObject {
String name;
String id;
UserObject(this.name, this.id);
}
class ConnectHome extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Pick an option'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: RaisedButton(
onPressed: () {
// Close the screen and return "Yep!" as the result.
Navigator.pop(context, UserObject("hello","world"));
},
child: Text('Hello'),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: RaisedButton(
onPressed: () {
// Close the screen and return "Nope!" as the result.
Navigator.pop(context, UserObject("no","No"));
},
child: Text('No.'),
),
)
],
),
),
);
}
}

How solve problem Appbar icon clicked to navigate

I try to develop flutter application. In this application I used Appbar with user icon to navigate another page.
Now I am clicked that icon(person icon) it shows error.
.
It has not proper documentation though internet. I couldn't found answer. My source code is
class MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: new Text("Web Issue finder"),
actions: <Widget>[
new IconButton(
icon: Icon(Icons.person),
// tooltip: "Admin",
onPressed: (){
Navigator.push(
context,
MaterialPageRoute(builder: (_) => AdminAuth()),
);
}
)
],
),
body: new FutureBuilder(
future: loadStates(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return new ListView.builder(
itemBuilder: (context, index) {
if (index >= snapshot?.data?.length ?? 0) return null;
return new ListTile(
title: new Text("${snapshot.data[index]}"),
onTap: () {
debugPrint("${snapshot.data[index]} clicked");
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
IssueAddScreen(state: snapshot.data[index]),
),
);
},
);
},
);
} else {
return new Center(child: new CircularProgressIndicator());
}
})));
}
this is navigated class
import 'package:flutter/material.dart';
class AdminAuth extends StatelessWidget{
// final String state;
// IssueAddScreen({Key key, #required this.state}) : super(key: key);
#override
Widget build(BuildContext context) {
// TODO: implement build
return MaterialApp(
debugShowCheckedModeBanner: false,
title: "iWallet",
home: Scaffold(
appBar: AppBar(title: Text("admin auth"),),
body: Text("cvgbh"),
),
);
}
}
Still I can't fix that error I am followed some documentation and stack overflow questions.
flutter documenttation
Github question and answer
Stackoverflow question and answer
Try to use context in your builder
Navigator.push(context,MaterialPageRoute(builder: (BuildContext context){return AdminAuth();
});
The issue here is with Navigator not present in the parent context.
You are using a context for the MyApp which isn't under the navigator.
MyApp <------ context
--> MaterialApp
(--> Navigator built within MaterialApp)
--> Scaffold
--> App Bar
--> ...
to solve this - Define new class that contain MaterialApp then pass MyApp() in home: of MaterialApp.
Same for the AdminAuth.
class MyAppHome extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: MyApp(),
);
}
}
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: new AppBar(
title: new Text("Web Issue finder"),
actions: <Widget>[
new IconButton(
icon: Icon(Icons.person),
// tooltip: "Admin",
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => AdminAuth()),
);
})
],
),
body: new FutureBuilder(
future: loadStates(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return new ListView.builder(
itemBuilder: (context, index) {
if (index >= snapshot?.data?.length ?? 0) return null;
return new ListTile(
title: new Text("${snapshot.data[index]}"),
onTap: () {
debugPrint("${snapshot.data[index]} clicked");
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
IssueAddScreen(state: snapshot.data[index]),
),
);
},
);
},
);
} else {
return new Center(child: new CircularProgressIndicator());
}
}));
}
}
The problem is the one explained above.
In my own words:
The context from which you are calling "Navigator" does not contain a "Navigator".
I guess the problem is that in you code you call Scaffold before MaterialApp complete the build method and get a Navigator or something like that.
If you separate the MaterialApp and the Scaffold (like below) you solve the problem.
void main() => runApp(MaterialApp(
home: MyApp())
class MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Web Issue finder"),
actions: <Widget>[
new IconButton(
icon: Icon(Icons.person),
tooltip: "Admin",
onPressed: (){
Navigator.push(
context,
MaterialPageRoute(builder: (_) => AdminAuth()),
);
}
)
],
),
body: new FutureBuilder(
future: loadStates(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return new ListView.builder(
itemBuilder: (context, index) {
if (index >= snapshot?.data?.length ?? 0) return null;
return new ListTile(
title: new Text("${snapshot.data[index]}"),
onTap: () {
debugPrint("${snapshot.data[index]} clicked");
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
IssueAddScreen(state: snapshot.data[index]),
),
);
},
);
},
);
} else {
return new Center(child: new CircularProgressIndicator());
}
})));
There is some issue with MateriaApp context in the library.
Your code will not work. Create a different MaterialApp and then use your widget in home: property of MaterialApp.
For example:
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',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.lightBlue,
),
home: MyAppState(), //This is your MyAppState
);
}
}
Now you can remove MaterialApp widget in your MyAppState keeping only Scaffold Widget
IconButton(onPressed: () {
Navigator.of(context).push(new MaterialPageRoute(builder: (context)=> AdminAuth));
},)