I am trying to navigate after login inside futurebuilder. api request done successfully but navigation getting error while testing. Help me How to use futurebuilder properly.
child: ElevatedButton(
onPressed: () {
// Validate returns true if the form is valid, or false otherwise.
if (_mobileKey.currentState!.validate()) {
FutureBuilder<Loginuser>(
future: loginuser(mobileController.text.toString(),
passwordController.text.toString()),
builder: (context, snapshot) {
if (snapshot.hasData) {
context.go('/Home');
return Text(snapshot.data!.message);
} else if (snapshot.hasError) {
return Text('${snapshot.error}');
}
// By default, show a loading spinner.
return const CircularProgressIndicator();
},
);
}
context.go('/Home');
},
child: const Text('Submit')),
I tried this its not working. I am using " go_router: ^5.2.4 " for navigation
Try removing the FutureBuilder from inside the ElevatedButton instead use Promises i.e then/catch to navigate to new screen
Updated code:
child ElevatedButton(
child: Container(),
onPressed: () async {
// Remove the future builder from here
await loginuser(mobileController.text.toString(),
passwordController.text.toString())
.then((result) {
context.go('/Home'); // 👈 add your navigatin inside then block
}).onError((error, stackTrace) {
print(error);
});
TLDR: Add:
WidgetsBinding.instance.addPostFrameCallback((_) =>
context.go('/Home'));
to the FutureBuilder
I managed to reproduce the problem with this example. Let's take a look.
If you run this code:
import 'package:flutter/material.dart';
void main() => runApp(MainApp());
class MainApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(body: Test()),
);
}
}
class Test extends StatelessWidget {
const Test({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: printHello(),
builder: (context, snapshot) {
if (snapshot.hasData) {
Navigator.push(
context, MaterialPageRoute(builder: (context) => HomePage()));
return Text(snapshot.data.toString());
} else if (snapshot.hasError) {
return Text("error");
} else {
return Center(child: CircularProgressIndicator());
}
});
}
}
Future<String> printHello() async {
return Future.value("Hello");
}
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(body: const Text("home page"));
}
}
You'll see an error:
setState() or markNeedsBuild() called during build.
So, to fix the problem, you need to use WidgetsBinding.instance.addPostFrameCallback:
WidgetsBinding.instance.addPostFrameCallback((_) =>
Navigator.push(context, MaterialPageRoute(builder: (context) {
return HomePage();
})));
but for your example for go_router, add this line:
WidgetsBinding.instance.addPostFrameCallback((_) =>
context.go('/Home'));
Complete working example:
import 'package:flutter/material.dart';
void main() => runApp(MainApp());
class MainApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(body: Test()),
);
}
}
class Test extends StatelessWidget {
const Test({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: printHello(),
builder: (context, snapshot) {
if (snapshot.hasData) {
WidgetsBinding.instance.addPostFrameCallback((_) =>
Navigator.push(context, MaterialPageRoute(builder: (context) {
return HomePage();
})));
return Text(snapshot.data.toString());
} else if (snapshot.hasError) {
return Text("error");
} else {
return Center(child: CircularProgressIndicator());
}
});
}
}
Future<String> printHello() async {
return Future.value("Hello");
}
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(body: const Text("home page"));
}
}
See also
setState() or markNeedsBuild called during build
This method help you to navigate the route without FutureBuilder. see the code
onPressed: () async {
// then await the future You want to complete and then use `.then()`
//method to implement the code that you want to implement when the future is completed
await //call your future widget //
.then((result) {
print('future completed');
// Navigate here
// For errors use onError to show or check the errors.
}).onError((error, stackTrace) {
print(error);
});
}
Related
So, I have made a function in Cubit Class it is to get data from API. For now I just can get the data if I pressed a button. I wanna make the function automatically called when the page/screen is open. For your information, this page is the first page that will be launched when user open the app. Here is some of my codes.
class UsersCubit extends Cubit<UsersState> {
UsersCubit() : super(UsersInitial());
UserRepository _userRepository = UserRepository();
void getAllUsers() async{
emit(UsersLoading());
try{
ResponseUsers _data = await _userRepository.getUsers();
emit(UsersSuccess(_data));
} catch(e){
emit(UsersError(e.toString()));
}
}
}
class UsersPage extends StatefulWidget {
const UsersPage({Key? key}) : super(key: key);
#override
_UsersPageState createState() => _UsersPageState();
}
class _UsersPageState extends State<UsersPage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Users")),
body: BlocProvider(
create: (context) => UsersCubit(),
child: BlocConsumer<UsersCubit, UsersState>(
listener: (context, state) {
if(state is UsersLoading){
print("getting users ...");
} else if (state is UsersSuccess){
print(state.data.users[1].identity!.name);
} else if (state is UsersError){
print(state.errorMessage);
}
},
builder: (context, state) {
return Stack(
children: [
(state is UsersSuccess) ? listViewUsers(state.data.users) : progressBar(),
ElevatedButton(
onPressed: (){
context.read<UsersCubit>().getAllUsers();
},
child: Text("GET USERS"),
)
],
);
},
),
),
);
}
}
I have tried to call the function directly in initState but when I run the app it returns an error.
#override
void initState() {
context.read<UsersCubit>().getAllUsers();
super.initState();
}
error:
Error: Could not find the correct Provider<UsersCubit> above this UsersPage Widget
This happens because you used a `BuildContext` that does not include the provider
of your choice. There are a few common scenarios:
- You added a new provider in your `main.dart` and performed a hot-reload.
To fix, perform a hot-restart.
- The provider you are trying to read is in a different route.
Providers are "scoped". So if you insert of provider inside a route, then
other routes will not be able to access that provider.
- You used a `BuildContext` that is an ancestor of the provider you are trying to read.
Make sure that UsersPage is under your MultiProvider/Provider<UsersCubit>.
This usually happens when you are creating a provider and trying to read it immediately.
For example, instead of:
...
Widget build(BuildContext context) {
return Provider<Example>(
create: (_) => Example(),
// Will throw a ProviderNotFoundError, because `context` is associated
// to the widget that is the parent of `Provider<Example>`
child: Text(context.watch<Example>()),
),
}
...
consider using `builder` like so:
...
Widget build(BuildContext context) {
return Provider<Example>(
create: (_) => Example(),
// we use `builder` to obtain a new `BuildContext` that has access to the provider
builder: (context) {
// No longer throws
return Text(context.watch<Example>()),
}
),
}
Is there any way to solve this case?
Can you try wrapp BlocConsumer inside a Builder ?
import 'package:flutter/material.dart';
class UsersPage extends StatefulWidget {
const UsersPage({Key? key}) : super(key: key);
#override
_UsersPageState createState() => _UsersPageState();
}
class _UsersPageState extends State<UsersPage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Users")),
body: BlocProvider(
create: (context) {
final cubit = UsersCubit();
cubit.getAllUsers();
return cubit;
},
child: Builder(builder: (context) {
return BlocConsumer<UsersCubit, UsersState>(
listener: (context, state) {
if(state is UsersLoading){
print("getting users ...");
} else if (state is UsersSuccess){
print(state.data.users[1].identity!.name);
} else if (state is UsersError){
print(state.errorMessage);
}
},
builder: (context, state) {
return Stack(
children: [
(state is UsersSuccess) ? listViewUsers(state.data.users) : progressBar(),
ElevatedButton(
onPressed: (){
context.read<UsersCubit>().getAllUsers();
},
child: Text("GET USERS"),
)
],
);
},
);
}),
),
);
}
}
I have solved this case with help from #Ehsan Askari. He suggests me to provide the cubit above the MaterialApp, then I did it. Here is my code now
class AppWidget extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => UsersCubit(),
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: UsersPage(),
),
);
}
}
class UsersPage extends StatefulWidget {
const UsersPage({Key? key}) : super(key: key);
#override
_UsersPageState createState() => _UsersPageState();
}
class _UsersPageState extends State<UsersPage> {
#override
void initState() {
context.read<UsersCubit>().getAllUsers();
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Users")),
body: BlocConsumer<UsersCubit, UsersState>(
listener: (context, state) {
if(state is UsersLoading){
print("getting users ...");
} else if (state is UsersSuccess){
print(state.data.users[1].identity!.name);
} else if (state is UsersError){
print(state.errorMessage);
}
},
builder: (context, state) {
return (state is UsersSuccess) ? listViewUsers(state.data.users) : progressBar();
},
),
);
}
}
you can call it from your Blocprovider by accessing your class like this :-
BlocProvider(
create: (context) => Usercubit()..getAllUsers(),
build : (context) => Scaffold() ........
You don't need to call a function inside initState when using Bloc or cubit, just call it when creating the Cubit inside BlocProvider like this >>.
class UsersPage extends StatefulWidget {
const UsersPage({Key? key}) : super(key: key);
#override
_UsersPageState createState() => _UsersPageState();
}
class _UsersPageState extends State<UsersPage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Users")),
body: BlocProvider(
/// after create the cubit you can call the method.
create: (context) => UsersCubit()..getAllUsers();,
child: BlocConsumer<UsersCubit, UsersState>(
listener: (context, state) {
if(state is UsersLoading){
print("getting users ...");
} else if (state is UsersSuccess){
print(state.data.users[1].identity!.name);
} else if (state is UsersError){
print(state.errorMessage);
}
},
builder: (context, state) {
return Stack(
children: [
(state is UsersSuccess) ? listViewUsers(state.data.users) : progressBar(),
ElevatedButton(
onPressed: (){
context.read<UsersCubit>().getAllUsers();
},
child: Text("GET USERS"),
)
],
);
},
),
),
);
}
}
My app has a state which is computed as a Future.
For example it includes a theme color, because I want to change the color when I navigate.
I try to display a progress indicator while waiting for the data.
But I can't make it work.
Either Navigator.push is not working and the app bar is missing, or I have no progress indicator and a route error...
Here is a code snippet.
import 'package:flutter/material.dart';
void main() => runApp(Test());
class Test extends StatefulWidget {
#override
State<StatefulWidget> createState() => _TestState();
}
class _TestState extends State<Test> {
Future<Color> color = Model.getColor();
#override
Widget build(BuildContext context) {
return FutureBuilder<Color>(
future: color,
builder: (context, snapshot) {
if (snapshot.hasError) throw snapshot.error;
if (snapshot.connectionState != ConnectionState.done) {
if (false) {
// Navigation not working. App bar missing.
return Material(child: Center(child: CircularProgressIndicator()));
} else {
// Progress not working. Screen flickering.
return MaterialApp(home: _buildWait());
}
}
var app = MaterialApp(
theme: ThemeData(primaryColor: snapshot.data),
home: _buildPage(),
// ERROR: The builder for route "/" returned null.
// routes: {'/': (_) => _buildPage()},
);
return app;
},
);
}
Widget _buildPage() {
return Builder(
builder: (context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: RaisedButton(
child: Text('Push'),
onPressed: () {
setState(() {
color = Model.getColor();
});
Navigator.push(context, MaterialPageRoute(builder: (context) {
return Scaffold(appBar: AppBar());
}));
},
),
),
);
},
);
}
}
Widget _buildWait() {
return Scaffold(
appBar: AppBar(title: Text('Wait...')),
body: Center(child: CircularProgressIndicator()),
);
}
class Model {
static final _colors = [Colors.red, Colors.green, Colors.amber];
static int _index = 0;
static Future<Color> getColor() {
return Future.delayed(Duration(seconds: 2), () => _colors[_index++ % _colors.length]);
}
}
Expected result: when I push the button to navigate to the new route, it should display a progress indicator, and then the new screen with a different theme color.
Now try the following. Try to make a root widget separately, because root widget is always there. you don't want a complete UI route to persist in the memory. Also make next route as a separate widget.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Test',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: Test(),
);
}
}
class Test extends StatefulWidget {
#override
State<StatefulWidget> createState() => _TestState();
}
class _TestState extends State<Test> {
Future<Color> color = Model.getColor();
#override
Widget build(BuildContext context) {
return FutureBuilder<Color>(
future: color,
builder: (context, snapshot) {
if (snapshot.hasError) return Center(child: Text("An Error Occurred"));
if (snapshot.connectionState == ConnectionState.waiting) {
return _buildWait();
}
var app = Theme(
data: ThemeData(primaryColor: snapshot.data),
child: _buildPage(),
);
return app;
},
);
}
Widget _buildPage() {
return Scaffold(
appBar: AppBar(),
body: Center(
child: RaisedButton(
child: Text('Push'),
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (context) {
return NextRoute();
}));
},
),
),
);
}
}
Widget _buildWait() {
return Scaffold(
appBar: AppBar(title: Text('Wait...')),
body: Center(child: CircularProgressIndicator()),
);
}
class Model {
static final _colors = [Colors.red, Colors.green, Colors.amber];
static int _index = 0;
static Future<Color> getColor() {
return Future.delayed(
Duration(seconds: 2), () => _colors[_index++ % _colors.length]);
}
}
class NextRoute extends StatefulWidget {
NextRoute({Key key}) : super(key: key);
#override
_NextRouteState createState() => _NextRouteState();
}
class _NextRouteState extends State<NextRoute> {
#override
Widget build(BuildContext context) {
return FutureBuilder<Color>(
future: Model.getColor(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Center(
child: Text("An Error Occurred"),
);
}
if (snapshot.connectionState == ConnectionState.waiting) {
return _buildWait();
}
return Theme(
data: ThemeData(primaryColor: snapshot.data),
child: Scaffold(
appBar: AppBar(),
),
);
});
}
}
so I think I have found the error, actually you must have an MaterialApp inside runApp() as root.
so you can't have MaterialApp inside FutureBuilder
what you can do is make MaterialApp the root widget and have a default Home Screen and inside its build method you can have your FutureBuilder but again don't include materialApp inside it just use Scaffold directly.
EDIT :
To answer the question regarding app theme
You can have switching themes by using
theme and darkTheme in materialApp And control themeMode from Provider or any other state management approach.
MaterialApp(
title: 'Flutter Tutorials',
debugShowCheckedModeBanner: false,
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: appState.isDarkModeOn ? ThemeMode.dark : ThemeMode.light,
home: ThemeDemo(),
);
There are several ways to do it here is one more that I found custom theme app
Try this out it will work, if doesn't let me know
I think you can do as follow:
Move all Future data/FutureBuilder into different Stateless/Stateful Widget and override the theme color with Theme class
class SecondPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
var color = Theme.of(context).primaryColor;
return FutureBuilder(
future: Model.getColor(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Theme(
data: ThemeData(primaryColor: color),
child: _buildWait(),
);
} else {
color = snapshot.data;
return Theme(
data: ThemeData(primaryColor: color),
child: Scaffold(
appBar: AppBar(),
),
);
}
},
);
}
}
The first page use the local variable to store color
...
Color _primaryColor;
#override
void initState() {
_primaryColor = Theme.of(context).primaryColor;
super.initState();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(primaryColor: _primaryColor),
home: _buildPage(),
);
}
...
If you want the first page update the theme on the same time, you should use some method to share data between widget (e.g. Provider). I use the simple method to catch the custom return value
// First Page
// use "then" can get the return value from the other route
...
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (context) {
return SecondPage();
})).then((color) {
setState(() {
_primaryColor = color;
});
});
},
...
// Second Page
// WillPopScope can catch navigator pop event
...
return WillPopScope(
onWillPop: () async {
Navigator.of(context).pop(color);
return Future.value(false);
},
child: FutureBuilder(
...
If it is not necessary for you to use Routing when you try to change Theme,
I can provide a simple solution that change the theme data by Theme class
ThemeData currentTheme;
#override
void initState() {
super.initState();
currentTheme = Theme.of(context);
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: FutureBuilder<Color>(
future: color,
builder: (context, snapshot) {
Widget child;
if (snapshot.hasError) throw snapshot.error;
if (snapshot.connectionState != ConnectionState.done) {
child = Scaffold(
appBar: AppBar(),
body: const Center(child: CircularProgressIndicator()),
);
}else{
currentTheme = currentTheme.copyWith(primaryColor: snapshot.data);
child = _buildPage();
}
return Theme(
data: currentTheme,
child: child,
);
},
),
);
}
Here is the document of Themes for part of an application
I have removed the return Builder section in the _buildPage widget and it seems to work. It also shows the CircularProgressIndıcator and Wait... text in AppBar.
Here is the edited code:
Widget _buildPage() {
return Scaffold(
appBar: AppBar(),
body: Center(
child: RaisedButton(
child: Text('Push'),
onPressed: () {
setState(() {
color = Model.getColor();
});
Navigator.push(context, MaterialPageRoute(builder: (context) {
return Scaffold(appBar: AppBar());
}));
},
),
),
);
}
I'd like a FutureBuilder to wait for the user to make a selection in a route or dialog (example below) and then return that data to the builder. However, the dialog never appears.
How can I await for data to be returned from a screen or dialog using a FutureBuilder?
DartPad
import 'package:flutter/material.dart';
void main() => runApp(HomeScreen());
class HomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: FutureBuilder(
future: launch(context),
builder: (context, snapshot) {
// use result from screen or dialog in snapshot.data
return Center(child: Text(snapshot.data));
}),
),
);
}
Future launch(BuildContext context) async {
return await showDialog<void>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
actions: <Widget>[
FlatButton(
child: Text('Send Data'),
onPressed: () {
// return some data
Navigator.pop(context, 'Some data!');
},
),
],
content: Container(child: Text('')),
);
},
);
}
}
You can copy paste run full code below
Step 1: You need to use addPostFrameCallback
Step 2: move MaterialApp to upper level
Step 3: check ConnectionState
code snippet
#override
void initState() {
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {
_future = launch(context);
});
});
super.initState();
}
Future<String> launch(BuildContext context) async {
var result = await showDialog(
...
print("result $result");
return Future.value(result);
}
working demo dartpad link
full code
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: HomeScreen(),
);
}
}
class HomeScreen extends StatefulWidget {
#override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
Future<String> _future;
#override
void initState() {
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {
_future = launch(context);
});
});
super.initState();
}
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
body: FutureBuilder(
future: _future,
builder: (context, AsyncSnapshot<String> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return Text('none');
case ConnectionState.waiting:
return Center(child: CircularProgressIndicator());
case ConnectionState.active:
return Text('');
case ConnectionState.done:
if (snapshot.hasError) {
return Text(
'${snapshot.error}',
style: TextStyle(color: Colors.red),
);
} else {
return Center(child: Text("his this is ${snapshot.data}"));
}
}
}),
),
);
}
Future<String> launch(BuildContext context) async {
var result = await showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
actions: <Widget>[
FlatButton(
child: Text('Send Data'),
onPressed: () {
// return some data
Navigator.pop(context, 'Some data!');
},
),
],
content: Container(child: Text('')),
);
},
);
print("result $result");
return Future.value(result);
}
}
Thanks for reverting use the future Completer to get the data from Dialogue
create a Completer instance
var dialogueFuture = Completer();
feed the future of completer to Future builder
FutureBuilder{
future : dialogueFuture.future,
...
}
in show dialogue function complete the Future like this
var theData = await showDialogue(...)
dialogFuture.complete(theData);
see the dartpad here
I'm trying to make it that the user must be logged in when veiwing specific pages. If not it redirects them to the login page.
I have the following code, but when I add Navigator.pushReplacementNamed(context, '/login'); I get an error:
The following assertion was thrown building
FutureBuilder<Response>(dirty, state:
_FutureBuilderState<Response>#458f7):
return FutureBuilder<Response>(
future: Provider.of<LocationProvider>(context).fetchAllLocations(),
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return Text('Fetch Items');
break;
case ConnectionState.active:
return Text('is active');
break;
case ConnectionState.waiting:
return Text('Is Fetching');
break;
case ConnectionState.done:
if (snapshot.data.status == Status.ERROR)
return Text('errrrrrroooorr');
else if (snapshot.data.status == Status.UNAUTHENTICATED) {
Navigator.pushReplacementNamed(context, '/login');
return Container();
}
return MyWidget();
break;
default:
return Text('hy');
}
},
);
Also, how can I make this code reusable?
You can use addPostFrameCallback
code snippet
WidgetsBinding.instance.addPostFrameCallback((_) {
Navigator.pushReplacementNamed(context, '/login');
});
working demo
full test code
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
initialRoute: '/',
routes: {
'/': (context) => MyHomePage(
title: "demo",
),
'/login': (context) => Login(),
},
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
Future<String> _future;
Future<String> fetchAllLocations() async {
await Future.delayed(Duration(seconds: 3), () {});
Future.value("test");
}
#override
void initState() {
_future = fetchAllLocations();
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: FutureBuilder(
future: _future,
builder: (context, AsyncSnapshot<String> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return Text('none');
case ConnectionState.waiting:
return Center(child: CircularProgressIndicator());
case ConnectionState.active:
return Text('');
case ConnectionState.done:
if (snapshot.hasError) {
return Text(
'${snapshot.error}',
style: TextStyle(color: Colors.red),
);
} else {
WidgetsBinding.instance.addPostFrameCallback((_) {
Navigator.pushReplacementNamed(context, '/login');
});
return Container();
}
}
}));
}
}
class Login extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Second Screen"),
),
body: Center(
child: RaisedButton(
onPressed: () {
// Navigate back to first screen when tapped.
},
child: Text('Go back!'),
),
),
);
}
}
I have created a Flutter application with a list. On tap of an item, I am opening detail of that item.
The problem is whenever I come back from the detail screen, the list screen is reloaded. I don't want to reload the list every time.
I have used BloC architecture in this.
Below are the code snippets. Please suggest.
Thank You.
Main
void main() {
final userRepository = UserRepository();
ApiClient apiClient = ApiClient(httpClient: http.Client());
runApp(BlocProvider<AuthenticationBloc>(
builder: (context) {
return AuthenticationBloc(
userRepository: userRepository, apiClient: apiClient)
..dispatch(AppStarted());
},
child: MyApp(userRepository: userRepository),
));
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
final UserRepository userRepository;
MyApp({Key key, #required this.userRepository}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: BlocBuilder<AuthenticationBloc, AuthenticationState>(
bloc: BlocProvider.of<AuthenticationBloc>(context),
builder: (context, state) {
if (state is AuthenticationUninitialized) {
return SplashPage();
}
if (state is AuthenticationAuthenticated) {
return HomePage(userRepository: userRepository);
}
if (state is AuthenticationUnauthenticated) {
return LoginPage(userRepository: userRepository);
}
if (state is AuthenticationLoading) {
return LoadingIndicator();
}
return null;
},
),
);
}
}
List Screen
class HomePage extends StatelessWidget {
UserRepository userRepository;
HomePage({#required this.userRepository}) : super();
#override
Widget build(BuildContext context) {
ApiClient apiClient = ApiClient(httpClient: http.Client());
return Scaffold(
appBar: AppBar(
title: Text('Home'),
),
drawer: AppDrawer(userRepository),
body: BlocProvider(
builder: (context) {
return HomeBloc(apiClient);
},
child: _HomeContent(),
),
floatingActionButton: FloatingActionButton(
onPressed: () {},
child: Icon(Icons.add),
backgroundColor: Colors.amberAccent,
),
);
}
}
class _HomeContent extends StatelessWidget {
#override
Widget build(BuildContext context) {
final HomeBloc homeBloc = BlocProvider.of<HomeBloc>(context);
homeBloc.dispatch(FetchMovieList());
return BlocBuilder<HomeBloc, HomeState>(
builder: (context, state) {
if (state is MovieListLoading) {
return Center(
child: CircularProgressIndicator(),
);
}
if (state is MovieListLoaded) {
List<Movie> topRatedMovies = state.movieList;
return new ListView.builder(
itemBuilder: (BuildContext context, int index) {
return new ListTile(
title: Card(
child: Row(
children: <Widget>[
Image.network(ApiClient.IMAGE_BASE_URL +
topRatedMovies[index].poster_path),
Column(
children: <Widget>[
Text(topRatedMovies[index].title),
],
)
],
),
),
onTap: () {
_onListItemTapped(topRatedMovies[index].id, context);
},
);
},
itemCount: topRatedMovies.length,
);
}
if (state is MovieListError) {
return Center(
child: Text('Error in calling API'),
);
}
return Center(child: Text('Employee data not found'));
},
);
}
void _onListItemTapped(int movieId, BuildContext context) {
Navigator.push(context,
MaterialPageRoute(
builder: (context) => MovieDetailPage(
movieId: movieId,
)));
}
}
At anytime your build method needs to be ready for multiple build calls. If build calls are causing problem then something is probably wrong. It would be a better idea to fetch the data outside the build method to prevent unnecessary API calls.
For example you can create a Stateful Widget and in initState method you can fetch the data. After that, build method is called to prepare UI with the data. You can use a Future Builder to show progress and update UI when the data is fetched.
Example:
class MyWidget extends StatefulWidget {
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
Future _future;
Future getData() async {
// Fetch data
}
#override
void initState() {
super.initState();
_future = getData();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: _future,
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.waiting:
return Center(child: CircularProgressIndicator());
default:
if (snapshot.data.hasErrors) {
return Text('Error: ${snapshot.data.errors}');
} else {
// Data is fetched, build UI
return ListTile();
}
}
});
}
}