Flutter - How can I call setState when a future builder has completed? - flutter

I have a widget which is getting data from one of my other classes(booruHandler), I am using the search function for the future of a future builder but I need it to be called multiple times but only once the future builder has completed, I am using a similar future builder in another widget and getting it to call again using setState to increment widget.pageNum when an action happens. Is there a way to make it so the setState gets called when the future builder has finished building? I tried putting setState inside the futurebuilder and making it call if the connectionState is done but that doesn't work
class SnatcherProgressPage extends StatefulWidget {
String tags,amount,timeout;
int pageNum=0;
int count=0;
SnatcherProgressPage(this.tags,this.amount,this.timeout);
#override
_SnatcherProgressPageState createState() => _SnatcherProgressPageState();
}
class _SnatcherProgressPageState extends State<SnatcherProgressPage> {
static int limit, count;
#override
void initState() {
// TODO: implement initState
super.initState();
if (int.parse(widget.amount) <= 100){limit = int.parse(widget.amount);} else {limit = 100;}
}
BooruHandler booruHandler = new GelbooruHandler("https://gelbooru.com", limit);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Snatching"),
),
body: FutureBuilder(
future: booruHandler.Search(widget.tags,widget.pageNum),
builder: (context, AsyncSnapshot snapshot){
switch(snapshot.connectionState){
case ConnectionState.active:
return Text("Snatching");
break;
case ConnectionState.done:
if (snapshot.data.length < int.parse(widget.amount)){
// Inc pagenum to get more data
// Call the writer function on all of the data
}
return Text(snapshot.data.length.toString());
break;
case ConnectionState.waiting:
return CircularProgressIndicator();
break;
case ConnectionState.none:
return Text("hmmmmmm");
break;
}
return Text("hmmmmmm");
},
),
);
}
}

very nice example could be like this:
final navigatorKey = GlobalKey<NavigatorState>();
void main() => runApp(
MaterialApp(
home: HomePage(),
navigatorKey: navigatorKey, // Setting a global key for navigator
),
);
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: SafeArea(
child: Center(
child: Text('test')
)
),
floatingActionButton: FloatingActionButton(
onPressed: showMyDialog, // Calling the function without providing it BuildContext
),
);
}
}
void showMyDialog() {
showDialog(
context: navigatorKey.currentContext,
builder: (context) => Center(
child: Material(
color: Colors.transparent,
child: Text('Hello'),
),
)
);
}
showMyDialog() could be used everywhere including static Utility classes, db connection classes etc...

Call back pageNum when updating
class SnatcherProgressPage extends StatefulWidget {
final NumberCallBack onPageNumUpdated;
}
// ...
setState((){
onPageNumUpdated(/* the number */);
});
// ...
typedef NumberCallBack = void Function(int number);
Or use state management

Related

How to modify a variable using async method before it use in widget build?

I have a variable named userName,which depends on databse query,so async is a must.
My older code can be concluded liks this
class IndexScreen extends StatefulWidget {
#override
_IndexScreenState createState() => _IndexScreenState();
}
//use database query function
Future<void> initUser() async{
UserTable().getUserInfo(curUserEmail).then((value)=>null);
}
//show page
class _IndexScreenState extends State<IndexScreen> {
#override
Widget build(BuildContext context) {
initUser().then((value){
final theme = Theme.of(context);
return WillPopScope(
onWillPop: () =>router.navigateTo(context, '/welcome'),
child: SafeArea(
child: Scaffold(
//The static global variable is used in Body in other files
body: Body()
),
),
);
});
}
}
It warns that miss return,I dont knwo how to amend my code.
Thanks!!
You can achive this by using the FutureBuilder widget. Please refer the code below.
class IndexScreen extends StatefulWidget {
#override
_IndexScreenState createState() => _IndexScreenState();
}
//use database query function
Future<Map> initUser() async {
final data =
await UserTable().getUserInfo(curUserEmail);
return data;
}
//show page
class _IndexScreenState extends State<IndexScreen> {
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: initUser(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
final theme = Theme.of(context);
return WillPopScope(
onWillPop: () => router.navigateTo(context, '/welcome'),
child: SafeArea(
child: Scaffold(
body: Body(),
),
),
);
} else {
// Returns empty container untill the data is loaded
Container();
}
},
);
}
}

Getx argumentsbeing cleared after using showDialog() in Flutter

The used Getx Arguments are cleared after the showDialog method is executed.
_someMethod (BuildContext context) async {
print(Get.arguments['myVariable'].toString()); // Value is available at this stage
await showDialog(
context: context,
builder: (context) => new AlertDialog(
//Simple logic to select between two buttons
); // get some Confirmation to execute some logic
print(Get.arguments['myVariable'].toString()); // Variable is lost and an error is thrown
Also I would like to know how to use Getx to show snackbars without losing the previous arguments as above.
One way to do this is to duplicate the data into a variable inside the controller and make a use from it instead of directly using it from the Get.arguments, so when the widget tree rebuild, the state are kept.
Example
class MyController extends GetxController {
final myArgument = ''.obs;
#override
void onInit() {
myArgument(Get.arguments['myVariable'] as String);
super.onInit();
}
}
class MyView extends GetView<MyController> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Expanded(
child: Center(child: Obx(() => Text(controller.myArgument()))),
),
);
}
}
UPDATE
Since you are looking for solution without page transition, another way to achieve that is to make a function in the Controller or directly assign in from the UI. Like so...
class MyController extends GetxController {
final myArgument = 'empty'.obs;
}
class MyView extends GetView<MyController> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Expanded(
child: ElevatedButton(
onPressed: () => _someMethod(context),
child: Obx(() => Text(controller.myArgument())),
),
),
);
}
void _someMethod(BuildContext context) async {
// store it in the state.
controller.myArgument(Get.arguments['myVariable'] as String);
await showDialog(
context: context,
builder: (context) => new AlertDialog(...),
);
print(controller.myArgument()); // This should work
}
}
UPDATE 2 (If you don't use GetView)
class MyController extends GetxController {
final myArgument = 'empty'.obs;
}
class MyView extends StatelessWidget {
final controller = Get.put(MyController());
#override
Widget build(BuildContext context) {
return Scaffold(
body: Expanded(
child: ElevatedButton(
onPressed: () => _someMethod(context),
child: Obx(() => Text(controller.myArgument())),
),
),
);
}
void _someMethod(BuildContext context) async {
// store it in the state.
controller.myArgument(Get.arguments['myVariable'] as String);
await showDialog(
context: context,
builder: (context) => new AlertDialog(...),
);
print(controller.myArgument()); // This should work
}
}
UPDATE 3 (NOT RECOMMENDED)
If you really really really want to avoid using Controller at any cost, you can assign it to a normal variable in a StatefulWidget, although I do not recommend this approach since it was considered bad practice and violates the goal of the framework itself and might confuse your team in the future.
class MyPage extends StatefulWidget {
const MyPage({ Key? key }) : super(key: key);
#override
_MyPageState createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
String _myArgument = 'empty';
#override
Widget build(BuildContext context) {
return Scaffold(
body: Expanded(
child: ElevatedButton(
onPressed: () => _someMethod(context),
child: Text(_myArgument),
),
),
);
}
void _someMethod(BuildContext context) async {
// store it in the state.
setState(() {
_myArgument = Get.arguments['myVariable'] as String;
});
await showDialog(
context: context,
builder: (context) => new AlertDialog(...),
);
print(_myArgument); // This should work
}
}

Flutter FutureProvider Execute build twice

I've changed from Statefulwidget using initState to fetch the data and Futurebuilder to load it to Futureprovider. But it seems like Futureprovider is execute build method twice, while my previous approach executed it once. Is this behaviour normal?
class ReportsPage extends StatelessWidget {
const ReportsPage({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return FutureProvider<List<ReportModel>>(
create: (_) async => ReportsProvider().loadReportData(1),
initialData: null,
catchError: (_, __) => null,
child: const ReportWidg()
);
}
}
class ReportWidg extends StatelessWidget {
const ReportWidg();
#override
Widget build(BuildContext context) {
print("Execute Build");
final reportList = Provider.of<List<ReportModel>>(context);
if (reportList == null) {
return Center(child: CircularProgressIndicator());
} else if (reportList.isEmpty) {
return Center(child: Text("Det finns inga rapporter."));
}
print(reportList.length);
return Container();
}
}
Im relativly new to flutter but I think its because StatelessWidget is #immutable, which means whenever something changes it needs to rebuild itself.
At first build there is async calling made and ReportWidg() is rendered.
Then this line final reportList = Provider.of<List<ReportModel>>(context); get new fetched data as result of async function therefore immutable widget needs to rebuild itself because it cannot be "changed".
In object-oriented and functional programming, an immutable object
(unchangeable object) is an object whose state cannot be modified
after it is created. ... This is in contrast to a mutable object
(changeable object), which can be modified after it is created
or am I wrong ?
I suspect your FutureProvider should be hoisted to a single instantiation, like placed into a global variable outside any build() methods. This will of course cache the result, so you can set it up for rebuild by having the value depend on other Providers being watch()ed or via FutureProvider.family.
You can copy paste run full code below
Yes. it's normal
First time Execute Build reportList is null and show CircularProgressIndicator()
Second time Execute Build reportList has data and show data
If you set listen: false , final reportList = Provider.of<List<ReportModel>>(context, listen: false);
You get only one Execute Build and the screen will always show CircularProgressIndicator()
In working demo simulate 5 seconds network delay so you can see CircularProgressIndicator() then show ListView
You can reference https://codetober.com/flutter-provider-examples/
code snippet
Widget build(BuildContext context) {
print("Execute Build");
final reportList = Provider.of<List<ReportModel>>(context);
print("reportList ${reportList.toString()}");
if (reportList == null) {
print("reportList is null");
return Center(child: CircularProgressIndicator());
} else if (reportList.isEmpty) {
return Center(child: Text("Empty"));
}
return Scaffold(
body: ListView.builder(
itemCount: reportList.length,
working demo
full code
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class ReportModel {
String title;
ReportModel({this.title});
}
class ReportsProvider with ChangeNotifier {
Future<List<ReportModel>> loadReportData(int no) async {
await Future.delayed(Duration(seconds: 5), () {});
return Future.value([
ReportModel(title: "1"),
ReportModel(title: "2"),
ReportModel(title: "3")
]);
}
}
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: ReportsPage(),
);
}
}
class ReportsPage extends StatelessWidget {
const ReportsPage({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return FutureProvider<List<ReportModel>>(
create: (_) async => ReportsProvider().loadReportData(1),
initialData: null,
catchError: (_, __) => null,
child: const ReportWidg());
}
}
class ReportWidg extends StatelessWidget {
const ReportWidg();
#override
Widget build(BuildContext context) {
print("Execute Build");
final reportList = Provider.of<List<ReportModel>>(context);
print("reportList ${reportList.toString()}");
if (reportList == null) {
print("reportList is null");
return Center(child: CircularProgressIndicator());
} else if (reportList.isEmpty) {
return Center(child: Text("Empty"));
}
return Scaffold(
body: ListView.builder(
itemCount: reportList.length,
itemBuilder: (context, index) {
return Card(
elevation: 6.0,
child: Padding(
padding: const EdgeInsets.only(
top: 6.0, bottom: 6.0, left: 8.0, right: 8.0),
child: Text(reportList[index].title.toString()),
));
}),
);
}
}
In your case you should use Consumer, i.e.
FutureProvider<List<ReportModel>(
create: (_) => ...,
child: Consumer<List<ReportModel>(
builder: (_, reportList, __) {
return reportList == null ?
CircularProgressIndicator() :
ReportWidg(reportList);
}
),
),
But in this case you must to refactor your ReportWidg.

Using Navigator with a bottomNavigationBar

What's the correct way of setting up navigation architecture named routes while using a bottomNavigationBar?
Here's my current setup but I feel there's a better way of doing it:
main.dart:
onGenerateRoute: (settings) {
return MaterialPageRoute(
settings: settings,
builder: (context) {
switch (settings.name) {
case NamedRoutes.splashScreen:
return SplashScreen();
case NamedRoutes.login:
return LoginPage();
case NamedRoutes.mainApp:
return NavigatorSetup();
default:
throw Exception('Invalid route: ${settings.name}');
}
});
navigatorSetup.dart:
IndexedStack(
index: Provider.of<RoutesProvider>(context).selectedViewIndex,
children: [FirstMain(), SecondMain(), ThirdMain(), FourthMain()],
), bottomNavigationBar...
in each main files there is the following setup
class FirstMain extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Navigator(
key: Provider.of<RoutesProvider>(context).homeKey,
onGenerateRoute: (settings) {
return MaterialPageRoute(
settings: settings,
builder: (context) {
switch (settings.name) {
case '/':
case NamedRoutes.mainPage:
return MainPage();
case NamedRoutes.singleMainPage:
return SingleMainPage();
default:
throw Exception('Invalid route: ${settings.name}');
}
},
);
},
);
}
}
Then my routes provider looks like this:
class RoutesProvider extends ChangeNotifier {
int _selectedViewIndex = 0;
get selectedViewIndex => _selectedViewIndex;
set selectedViewIndex(int newIndex) {
_selectedViewIndex = newIndex;
notifyListeners();
}
GlobalKey _mainKey = GlobalKey<NavigatorState>();
GlobalKey _homeKey = GlobalKey();
GlobalKey _secondKey = GlobalKey();
GlobalKey _thirdKey = GlobalKey();
GlobalKey _fourthKey = GlobalKey();
get mainKey => _mainKey;
get homeKey => _homeKey;
get secondKey => _secondKey;
get thirdKey => _thirdKey;
get fourthKey => _fourthKey;
}
The way I'm currently changing routes when on another page of the indexedStack
final RoutesProvider routesProvider = Provider.of<RoutesProvider>(context, listen: false);
final GlobalKey thirdKey = routesProvider.thirdKey;
routesProvider.selectedViewIndex = 2;
Navigator.pushReplacementNamed(thirdKey.currentContext, NamedRoutes.third);
The better way to navigate
Creating a route_generator
import 'package:flutter/material.dart';
import 'package:routing_prep/main.dart';
class RouteGenerator {
static Route<dynamic> generateRoute(RouteSettings settings) {
// Getting arguments passed in while calling Navigator.pushNamed
final args = settings.arguments;
switch (settings.name) {
case '/':
return MaterialPageRoute(builder: (_) => FirstPage());
case SecondPage.routeName:
// Validation of correct data type
if (args is String) {
return MaterialPageRoute(
builder: (_) => SecondPage(
data: args,
),
);
}
// If args is not of the correct type, return an error page.
// You can also throw an exception while in development.
return _errorRoute();
default:
// If there is no such named route in the switch statement, e.g. /third
return _errorRoute();
}
}
static Route<dynamic> _errorRoute() {
return MaterialPageRoute(builder: (_) {
return Scaffold(
appBar: AppBar(
title: Text('Error'),
),
body: Center(
child: Text('ERROR'),
),
);
});
}
}
As you can see, you've moved from having bits of routing logic everywhere around your codebase, to a single place for this logic - in the RouteGenerator. Now, the only navigation code which will remain in your widgets will be the one pushing named routes with a navigator.
Before you can run and test the app, there's still a bit of a setup to do for this RouteGenerator to function.
main.dart
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
...
// Initially display FirstPage
initialRoute: '/',
onGenerateRoute: RouteGenerator.generateRoute,
);
}
}
class FirstPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
...
RaisedButton(
child: Text('Go to second'),
onPressed: () {
// Pushing a named route
Navigator.of(context).pushNamed(
SecondPage.routeName,
arguments: 'Hello there from the first page!',
);
},
)
...
}
}
class SecondPage extends StatelessWidget {
static const routeName = "/second";
// This is a String for the sake of an example.
// You can use any type you want.
final String data;
SecondPage({
Key key,
#required this.data,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Routing App'),
),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
'Second Page',
style: TextStyle(fontSize: 50),
),
Text(
data,
style: TextStyle(fontSize: 20),
),
],
),
),
);
}
}

Flutter - How catch back button(hardware) event?

I created Flutter app for Android. This my user case:
I'm on the widgetA.
I'm click button and and go to the widgetB. (A->B)
I'm click hardware back button ant return to widgetA.
Now I'm on widgetA and I need any event so that I can update my WidgetA.
Any advices?
You could do something similar to what is called on Android startActivityForResult().
If you are on WidgetA push a new WidgetB and wait for a result.
From WidgetB detect when the widget will pop and pop manually sending the result.
WidgetA:
class WidgetA extends StatefulWidget {
#override
State<StatefulWidget> createState() => WidgetAState();
}
class WidgetAState extends State<WidgetA> {
Future<String> _result;
Future<String> _startWidgetForResult(BuildContext context) async {
String result = await Navigator.push(
context,
MaterialPageRoute(
builder: (BuildContext context) => WidgetB(context: context),
),
);
return result;
}
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
child: Icon(Icons.subdirectory_arrow_right),
onPressed: () => _result = _startWidgetForResult(context),
),
body: FutureBuilder(
future: _result,
builder: (context, snapshot) {
return Center(child: Text('${snapshot.data}'));
},
),
);
}
}
WidgetB:
class WidgetB extends StatelessWidget {
final BuildContext context;
WidgetB({this.context});
Future<bool> _onWillPop() async {
Navigator.of(context).pop("New data");
return false;
}
#override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: _onWillPop,
child: Scaffold(
floatingActionButton: FloatingActionButton(
child: Icon(Icons.subdirectory_arrow_left),
onPressed: () => Navigator.of(context).pop("New data"),
),
body: Center(child: Text('Press back to send new data')),
),
);
}
}
I guess you can wrap your widgetB in WillPopScope widget. Or you can set a key press handler with SystemChannels.keyEvent.setMessageHandler(...), but this isn't a recommended way to go.
You can use RouteObserver for that
https://api.flutter.dev/flutter/widgets/RouteObserver-class.html