问题
In my code below, I am struggling with LifeCyrles in Flutter where I can update my State in Provider, APPARENTLY, only in didChangeDependencies
hook or in a template widget (via events hung up on buttons or so).
Alright, I don't mind that only didChangeDependencies hook works for me BUT when my logic in earlier mentioned hook depends on some class properties I am having problems with the accuracy of the class data.
I get data one step behind (since it's called before build
hook I guess).
I cannot run this logic in the build hook because it includes a request to change the state in Provider. If I try to change the state there I've got either this error:
setState() or markNeedsBuild() called during build.
or this one
The setter 'lastPage=' was called on null.
Receiver: null
Tried calling: lastPage=true
What I want to do: I've got a wrapper widget which holds three other widgets: footer, header and pageViewer. When I reach the last page I need to notify my wrapper widget about that so it reacts accordingly and hides header and footer.
I would appreciate any help here!
The focused code:
Here is the problem and must be solution
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:ui_flutter/screens/welcome/welcome_bloc.dart';
import 'package:flutter/scheduler.dart';
class _FooterState extends State<Footer> {
@override
void initState() {
super.initState();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
final WelcomeBloc _welcome = Provider.of<WelcomeBloc>(context);
_welcomeBloc = _welcome;
// this._detectLastPage();
}
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.bottomCenter,
padding: EdgeInsets.symmetric(vertical: 30.0, horizontal: 30.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
this.stepper,
this.nextArrow,
],
),
);
}
_detectLastPage() {
// Here I've got inaccurate data
print(this.widget.currentStep);
}
}
I have already tried some other hooks like Scheduler but maybe I did something wrong there.
SchedulerBinding.instance
.addPostFrameCallback((_) => this._detectLastPage());
It's called only once at the first build-up round and that's it.
I lack an Angular hook here AfterViewInit
. It would be handy here.
or Mounted
in VueJS
That's the rest of my code if you'd like to see the whole picture. If you have any suggestions on the architecture, structure or something else you are welcome. It's highly appreciated since I'm new to Flutter.
main.dart
import 'package:flutter/material.dart';
import 'package:ui_flutter/routing.dart';
import 'package:provider/provider.dart';
import 'screens/welcome/welcome_bloc.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => WelcomeBloc()),
],
child: MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
initialRoute: '/welcome',
onGenerateRoute: RouteGenerator.generateRoute,
),
);
}
}
welcome.dart (my wrapper)
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:provider/provider.dart';
import 'package:ui_flutter/screens/welcome/welcome_bloc.dart';
import './footer.dart';
import './viewWrapper.dart';
import './header.dart';
// import 'package:ui_flutter/routing.dart';
class Welcome extends StatefulWidget {
@override
_WelcomeState createState() => _WelcomeState();
}
class _WelcomeState extends State<Welcome> {
WelcomeBloc _welcomeBloc;
@override
Widget build(BuildContext context) {
final WelcomeBloc _welcome = Provider.of<WelcomeBloc>(context);
this._welcomeBloc = _welcome;
print('Welcome: _welcome.currentPage - ${this._welcomeBloc.lastPage}');
return Scaffold(
body: SafeArea(
child: Stack(
children: <Widget>[
ViewerWrapper(),
Footer(
currentStep: _welcomeBloc.currentPage,
totalSteps: 3,
activeColor: Colors.grey[800],
inactiveColor: Colors.grey[100],
),
WelcomeHeader,
],
),
),
);
}
}
welcomeBloc.dart (my state via Provider)
import 'package:flutter/material.dart';
class WelcomeBloc extends ChangeNotifier {
PageController _controller = PageController();
int _currentPage;
bool _lastPage = false;
bool get lastPage => _lastPage;
set lastPage(bool value) {
_lastPage = value;
notifyListeners();
}
int get currentPage => _currentPage;
set currentPage(int value) {
_currentPage = value;
notifyListeners();
}
get controller => _controller;
nextPage(Duration duration, Curves curve) {
controller.nextPage(duration: duration, curve: curve);
}
}
footer.dart (that's where I've problems with data at the very bottom of the code - _detectLastPage method)
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:ui_flutter/screens/welcome/welcome_bloc.dart';
import 'package:flutter/scheduler.dart';
class Footer extends StatefulWidget {
final int currentStep;
final int totalSteps;
final Color activeColor;
final Color inactiveColor;
final Duration duration;
final Function onFinal;
final Function onStart;
Footer({
this.activeColor,
this.inactiveColor,
this.currentStep,
this.totalSteps,
this.duration,
this.onFinal,
this.onStart,
}) {}
@override
_FooterState createState() => _FooterState();
}
class _FooterState extends State<Footer> {
final double radius = 10.0;
final double distance = 4.0;
Container stepper;
Container nextArrow;
bool lastPage;
WelcomeBloc _welcomeBloc;
@override
void didChangeDependencies() {
super.didChangeDependencies();
final WelcomeBloc _welcome = Provider.of<WelcomeBloc>(context);
_welcomeBloc = _welcome;
this._detectLastPage();
}
@override
Widget build(BuildContext context) {
this._makeStepper();
this._makeNextArrow();
return Container(
alignment: Alignment.bottomCenter,
padding: EdgeInsets.symmetric(vertical: 30.0, horizontal: 30.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
this.stepper,
this.nextArrow,
],
),
);
}
_makeCirle(activeColor, inactiveColor, position, currentStep) {
currentStep = currentStep == null ? 0 : currentStep - 1;
Color color = (position == currentStep) ? activeColor : inactiveColor;
return Container(
height: this.radius,
width: this.radius,
margin: EdgeInsets.only(left: this.distance, right: this.distance),
decoration: BoxDecoration(
color: color,
border: Border.all(color: activeColor, width: 2.0),
borderRadius: BorderRadius.circular(50.0)),
);
}
_makeStepper() {
List<Container> circles = List();
for (var i = 0; i < widget.totalSteps; i++) {
circles.add(
_makeCirle(this.widget.activeColor, this.widget.inactiveColor, i,
this.widget.currentStep),
);
}
this.stepper = Container(
child: Row(
children: circles,
),
);
}
_makeNextArrow() {
this.nextArrow = Container(
child: Padding(
padding: const EdgeInsets.only(right: 8.0),
child: GestureDetector(
onTap: () {
_welcomeBloc.controller.nextPage(
duration: this.widget.duration ?? Duration(milliseconds: 500),
curve: Curves.easeInOut,
);
},
child: Icon(
Icons.arrow_forward,
)),
),
);
}
_onLastPage() {
if (this.widget.onFinal != null) {
this.widget.onFinal();
}
}
_onFirstPage() {
if (this.widget.onStart != null) {
this.widget.onStart();
}
}
_detectLastPage() {
// Here I've got inaccurate data
int currentPage =
this.widget.currentStep == null ? 1 : this.widget.currentStep;
if (currentPage == 1 && this.widget.currentStep == null) {
this._onFirstPage();
} else if (currentPage == this.widget.totalSteps) {
print('lastPage detected');
setState(() {
this.lastPage = true;
});
_welcomeBloc.lastPage = true;
this._onLastPage();
} else {
setState(() {
this.lastPage = false;
});
_welcomeBloc.lastPage = false;
}
}
}
Thanks in advance!
回答1:
I am new to flutter as well, But I have learned about a few architecture patterns that have helped me build some apps.
Here is how I do it:
Create a Provider
which holds the data for you in runtime. (It can be a Bloc
in your case). Stick to one architecture, don't try to put providers and blocs in the same project. Both are used for state management and only using one would be a great practice.
Second, Register
the providers using ChangeNotificationProvider
or any other widgets which does a similar job of rebuilding the child widget when a data gets changed.
Third, Get the provider in the build method of the widget that is supposed to change when the value of the variable provider changes. This way only the concerned widget is redrawn.
For your case,
If you want to hide the header and footer once you reach the last page, you can declare a variable, let's say isLastPage
set to false
by default in your provider.
Next, wrap the widget, i.e. header
and footer
with ChangeNotificationListner
Now, let that widget decide what it has to do based on the value of isLastPage
, either hide itself or show.
I hope this helps!
回答2:
At the long run, I seem to have found Mounted
lifecycle hook in Flutter which is implemented with the help of Future.microtask
. Unlike .addPostFrameCallback
:
SchedulerBinding.instance
.addPostFrameCallback((_) => this._detectLastPage());
Which is triggered only once like InitState
(but only at the end of the build execution), Future.microtask
can be placed inside build
block and be invoked after every change and state update.
It doesn't solve the problem with the inaccurate state in didChangeDependencies
hook but provides another way to perform post-build executions.
Credits for the current solution to @Abion47
example
Future.microtask(() => this._detectLastPage());
来源:https://stackoverflow.com/questions/61598767/didchangedependencies-hook-in-flutter-widget-includes-not-accurate-data-of-the-c