pushing new page when state changes is causing weird behaviour - flutter

im new to flutter. I am trying to push to a new page whenever my qrcode reader detects a qrcode. however upon detecting the qrcode, infinite pages are being pushed. Can anyone provide me with some advice or sample code that pushes to a new page whenever the state changes?
class _QRViewExampleState extends State<QRViewExample> {
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
_QRViewExampleState({this.state});
var state = false;
QRViewController controller;
#override
Widget build(BuildContext context) {
if (state == true) {
WidgetsBinding.instance.addPostFrameCallback((_) {
Navigator.of(context).push(
MaterialPageRoute(builder: (ctx) => Menu()),
);
// Navigation
});
}
return Scaffold(
...
);
}
void _onQRViewCreated(QRViewController controller) {
this.controller = controller;
controller.scannedDataStream.listen((scanData) {
setState(() {
state = true;
});
});
}

first of all, this line of code:
WidgetsBinding.instance.addPostFrameCallback((_) {});
usually used when you want run statement after build finished, and called inside initState(). in your case i think you don't need it.
i recommend you to call navigator inside your QRViewController listener:
_onQRViewCreated(QRViewController controller) {
this.controller = controller;
controller.scannedDataStream.listen((scanData) {
Navigator.of(context).push(
MaterialPageRoute(builder: (ctx) => Menu(scanData)),
);
});
}

Related

How can i reload my page every time i am on it on my flutter app?

Assume that I'm on page-A now. I navigate to page-B. When I pop the page-B and come back to page-A, currently nothing happens. How can I reload page-A and load the new API data from the init state of page-A? Any Ideas?
first main page
void refreshData() {
id++;
}
FutureOr onGoBack(dynamic value) {
refreshData();
setState(() {});
}
void navigateSecondPage() {
Route route = MaterialPageRoute(builder: (context) => SecondPage());
Navigator.push(context, route).then(onGoBack);
}
second page
RaisedButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('Go Back'),
),
more details check here
From the explanation that you have described, so when you are popping the page.
This below Code will be on the second page.
Navigator.of(context).pop(true);
so the true parameter can be any thing which ever data that you want to send.
And then when you are pushing from one page to another this will be the code.
this is on the first page.
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => const PageOne()),
);
so if you print the result you will get the bool value that you send from the second page.
And based on bool you can hit the api. if the bool true make an api call.
Let me know if this works.
There are one more solutions for this situtation.
İf you want to trigger initState again
You can use pushAndRemoveUntil method for navigation. ( if you use only push method this is not remove previous page on the stack)
You can use key
You can set any state manegement pattern.( not for only trigger initState again)
There are 2 ways:
Using await
await Navigator.push(context, MaterialPageRoute(builder: (context){
return PageB();
}));
///
/// REFRESH DATA (or) MAKE API CALL HERE
Passing fetchData constructor to pageB and call it on dispose of pageB
class PageA {
void _fetchData() {}
Future<void> goToPageB(BuildContext context) async {
await Navigator.push(context, MaterialPageRoute(builder: (context) {
return PageB(onFetchData: _fetchData);
}));
}
}
class PageB extends StatefulWidget {
const PageB({Key? key, this.onFetchData}) : super(key: key);
final VoidCallback? onFetchData;
#override
State<PageB> createState() => _PageBState();
}
class _PageBState extends State<PageB> {
#override
void dispose() {
widget.onFetchData?.call();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Container();
}
}

Flutter : make splash screen load all api's data

I have flutter app need to load some api's data in splash screen which contain animation.
class _SplashPageState extends State<SplashPage> {
#override
void initState() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
var auth = Provider.of<AuthProvider>(context, listen: false);
auth.chekingAuthVariables();
var loading = Provider.of<LoadingProvider>(context, listen: false);
loading.getBrands();
loading.getVideos();
var catsProv = Provider.of<CatProviders>(context, listen: false);
catsProv.getCategoryList();
});
super.initState();
}
#override
Widget build(BuildContext context) {
return AnimatedSplashScreen(
backgroundColor: ColorManager.tabBottonNonActive,
duration: 3000,
splash: 'assets/splash.png',
nextScreen: const MainTabPage(),
splashTransition: SplashTransition.slideTransition,
);
}
}
the duration of animation is 3 seconds, sometimes when the net is slow the api's data dont load which make a problem for loading some screens.
How can I make the duration of animation finish after loading all api's data?
void loadAPIData() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
var auth = Provider.of<AuthProvider>(context, listen: false);
auth.chekingAuthVariables();
var loading = Provider.of<LoadingProvider>(context, listen: false);
loading.getBrands();
loading.getVideos();
var catsProv = Provider.of<CatProviders>(context, listen: false);
catsProv.getCategoryList();
// run the animation indefinitely and use navigate after retrieving all data
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const SecondRoute()),
);
});
}
I noticed you are making API calls without using await, which is quite odd considering you are loading data from an asynchronous source.

ChangeNotifier shows error when navigate to next page

I'm new to flutter and building an app using ChangeNotifier and provider for an MVVM design pattern. When a login page loads I want to check it's already logged in or not. if it's already logged in, then navigate to the next page. But it show's following error.
My ChangeNotifier Binding code from splash screen
_moveToNextScreen() async {
await Navigator.pushReplacement(context,
MaterialPageRoute(
builder: (context) =>
ChangeNotifierProvider(
create: (_) => LoginViewModel(),
child: LoginScreen(),
),
));
}
Login Screen code
class _LoginScreenState extends State<LoginScreen> {
LoginViewModel _loginViewModel;
//controller for login
TextEditingController _usernameController = TextEditingController();
TextEditingController _passwordController = TextEditingController();
Size size; // calculate screen size
#override
void initState() {
_loginViewModel = Provider.of<LoginViewModel>(context, listen: false);
_loginViewModel.loggedInOrNot();
checkLoginStatus();
super.initState();
}
checkLoginStatus() async {
print('vcal - ${_loginViewModel.getIsLoggedIn}');
if (_loginViewModel.getIsLoggedIn == true) {
//_moveToNextScreen();
}
}
#override
Widget build(BuildContext context) {
_loginViewModel = Provider.of<LoginViewModel>(context);
size = MediaQuery.of(context).size;
return Scaffold(
backgroundColor: green,
//IP screen
// Asset Tracker- Text
body: _loginScreen(),
);
}
my LoginViewModel.dart
class LoginViewModel extends ChangeNotifier{
LoginRepository _loginRepository = LoginRepository();
bool _isLoading = false;
bool _isLoggedIn = false;
void loggedInOrNot() async {
_isLoggedIn = !_isLoggedIn;
//await _loginRepository.checkToken();
notifyListeners();
}
bool get getIsLoggedIn => _isLoggedIn;
bool get getIsLoading => _isLoading;
}
How can i resolve this issue?
You should access a provider like this
Provider.of<LoginViewModel>(context, listen: false).getIsLoggedIn;
and Wrap with Consumer widget with top of the ui class widget tree
Consumer is rebuilds your UI every state changes.
For more info about provider & consumer I recommend you to their documentation find it from here
It is maybe because when you go to the next page you create new ChangeNotifierProvider. You have to place it above these two Widgets, e.g. above the MaterialApp/CupertinoApp

Flutter setState not updating child element

I have an InkWell which uses onTap to perform some actions. When the button is tapped, I like an indicator to be shown (in case the action is long-running). However, the setState in the InkWell does not trigger its children to be re-rendered. The code is as follows:
class PrimaryButtonState extends State<PrimaryButton> {
bool _apiCall;
Widget getWidget() {
if(_apiCall) {
return new CircularProgressIndicator();
} else {
return Text(
widget.label,
);
}
}
#override
Widget build(BuildContext context) {
final List<Color> colors = //omitted
return InkWell(
child: Container(
decoration: // omitted
child: getWidget(), // not updated when _apiCall changes !!!!!
),
onTap: () {
setState(() {
_apiCall = true;
});
widget.onTab(context);
setState(() {
_apiCall = false;
});
}
);
}
}
How can I solve this that getWidget returns the correct widget dependent on _apiCall?
EDIT:
The widget.onTap contains the following:
void performLogin(BuildContext context) {
final String userName = _userName.text.trim();
final String password = _password.text.trim();
UserService.get().loginUser(userName, password).then((val) {
Navigator.push(
context, MaterialPageRoute(builder: (context) => MainLayout()));
}).catchError((e) {
// omitted
});
}
it is passed with the widget:
class PrimaryButton extends StatefulWidget {
final bool isPrimary;
final String label;
final Function(BuildContext context) onTab;
PrimaryButton(this.label, this.isPrimary, this.onTab);
#override
State<StatefulWidget> createState() => PrimaryButtonState();
}
My main concern is, that the given onTap method should not know it is "bound" to a UI widget and therefore should not setState. Also, as this is a general button implementation I like it to be interchangeable (therefore, onTap is not hardcoded)
It looks like your problem is because you are calling setState() twice in your onTap() function. Since onTap() is not an async function it will set _apiCall = true in the first setState, then immediately run widget.onTab(context) and then immediately perform the second setState() to set _apiCall = false so you never see the loading widget.
To fix this you will need to make your onTab function an async function and await for a value in your onTap function for your InkWell:
onTap: () async {
setState(() {
_apiCall = true;
});
await widget.onTab(context);
setState(() {
_apiCall = false;
});
}
This will also let you use the results of your onTab function to show errors or other functionality if needed.
If you are unsure how to use async functions and futures here is a good guide on it that goes over this exact kind of use case.

Reload widget in flutter

I have an API that returns content and I put this content in a GridView.builder to allow pagination.
I have architected the page in such a way that I have a FutureBuilder on a stateless widget and when the snapshot is done I then pass the snapshot data to a stateful widget to build the grid.
It is all working fine, however I want now to implement a functionality that allows me to reload the widget by placing a reload icon when snapshot has error and on click reloading widget. How can I accomplish this?
The following is my FutureBuilder on my Stateless widget:
return new FutureBuilder<List<Things>>(
future: apiCall(),
builder: (context, snapshot) {
if (snapshots.hasError)
return //Reload Icon
switch (snapshots.connectionState) {
case ConnectionState.waiting:
return Center(child: CircularProgressIndicator());
case ConnectionState.done:
return StatefulWidhet(things: snapshot.data);
default:
}
});
}
You'll need to lift the state up. The whole loading concept is abstracted by the FutureBuilder, but because you don't want to do one-time-loading, that's not the right abstraction layer for you. That means, you'll need to implement the "waiting for the future to complete and then build stuff" yourself in order to be able to trigger the loading repeatedly.
For example, you could put everything in a StatefulWidget and have isLoading, data and error properties and set these correctly.
Because this is probably a recurring task, you could even create a widget to handle that for you:
import 'package:flutter/material.dart';
class Reloader<T> extends StatefulWidget {
final Future<T> Function() loader;
final Widget Function(BuildContext context, T data) dataBuilder;
final Widget Function(BuildContext context, dynamic error) errorBuilder;
const Reloader({
Key key,
this.loader,
this.dataBuilder,
this.errorBuilder,
}) : super(key: key);
#override
State<StatefulWidget> createState() => ReloaderState<T>();
static of(BuildContext context) =>
context.ancestorStateOfType(TypeMatcher<ReloaderState>());
}
class ReloaderState<T> extends State<Reloader<T>> {
bool isLoading = false;
T data;
dynamic error;
#override
void initState() {
super.initState();
reload();
}
Future<void> reload() async {
setState(() {
isLoading = true;
data = null;
error = null;
});
try {
data = await widget.loader();
} catch (error) {
this.error = error;
} finally {
setState(() => isLoading = false);
}
}
#override
Widget build(BuildContext context) {
if (isLoading) {
return Center(child: CircularProgressIndicator());
}
return (data != null)
? widget.dataBuilder(context, data)
: widget.errorBuilder(context, error);
}
}
Then, you can just do
Reloader(
loader: apiCall,
dataBuilder: (context, data) {
return DataWidget(things: data);
},
errorBuilder: (context, error) {
return ...
RaisedButton(
onPressed: () => Reloader.of(context).reload(),
child: Text(reload),
),
...;
},
)
Also, I wrote a package for that case which has some more features built-in and uses a controller-based architecture instead of searching the state through Reload.of(context): flutter_cached
With it, you could just do the following:
In a state, create a CacheController (although you don't need to cache things):
var controller = CacheController(
fetcher: apiCall,
saveToCache: () {},
loadFromCache: () {
throw 'There is no cache!';
},
),
Then, you could use that controller to build a CachedBuilder in the build method:
CachedBuilder(
controller: controller,
errorScreenBuilder: (context, error) => ...,
builder: (context, items) => ...,
...
),
When the reload button is pressed, you can simply call controller.fetch(). And you'll also get some cool things like pull-to-refresh on top.