Call state class from widget in flutter - view

I think I have a flawed architecture but I'm struggling to see how/why. I'm very new to flutter, so bear with me please.
I have a map, and a drawer. I'm loading a list of coordinates in the drawer, and I'd like to do stuff on the map once I press one of those coordinates.
So my problem is that I don't know what to call where to be clean code AND working. Of course I could just expose everything to everyone but that wouldn't solve the main issue : me understanding
My map is drawn in the same place as my drawer, so there I already think I'm cheating but I think that's okay. To be honest I'm not even sure that part is really correct.
Drawing the map :
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
var _map = MapView();
var _stationService = StationService();
....
#override
Widget build(BuildContext context) {
return MaterialApp(
home: DefaultTabController(
length: 4,
child: Scaffold(
appBar: AppBar(
bottom: TabBar(
tabs: [
Tab(icon: Icon(Icons.directions_car)),
Tab(icon: Icon(Icons.map)),
Tab(icon: Icon(Icons.directions_transit)),
Tab(icon: Icon(Icons.directions_bike))
],
),
title: Text('Floctta Plus'),
),
drawer: _drawer(),
body: TabBarView(
physics: NeverScrollableScrollPhysics(),
children: [
IncrementView(),
_map,
Icon(Icons.directions_transit),
Icon(Icons.directions_bike)
],
),
),
),
);
}
In my drawer code :
ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, position) {
return ListTile(
title: new Text(snapshot.data[position].titleFR),
leading: new Icon(Icons.pin_drop),
onTap: () {
_map.GoToStation(); <<= Here, calling the MapView class
},
);
},
),
All the MapView.dart code
class MapView extends StatefulWidget {
MapView({Key key}) : super(key: key);
void GoToStation() {
print("I'm reaching this point with success!");
}
#override
_MapViewState createState() => _MapViewState();
}
class _MapViewState extends State<MapView> {
CameraPosition _initialPosition =
CameraPosition(target: LatLng(50.8267018, 4.3532732), zoom: 10.0);
Completer<GoogleMapController> _controller = Completer();
var _markers;
void AddMarkers(List<Station> stations) {
setState(() {
_markers = new List.generate(
stations.length,
(index) => Marker(
markerId: MarkerId(index.toString()),
position: new LatLng(double.parse(stations[index].latitude),
double.parse(stations[index].longitude)),
infoWindow: InfoWindow(
title: stations[index].titleFR,
snippet: stations[index].city,
),
icon: BitmapDescriptor.defaultMarker,
));
});
}
void _onMapCreated(GoogleMapController controller) {
_controller.complete(controller);
}
#override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
GoogleMap(
onMapCreated: _onMapCreated,
initialCameraPosition: _initialPosition,
markers: _markers,
myLocationEnabled: true,
),
],
);
// );
}
}
I think there is enough code.
My question is :
Im in my Mapview class now, and I'd like to call AddMarkers that is in my State class. But I can't. I'm not really sure how I'm supposed to proceed.
Should I call the state class directly from the drawer? If so, how?
Should I call the state from the view class ? If so, how?
Should I be doing something else entirely? If so, what ?

Right!
I will assume that MapView and Drawer are in different widget subtrees under Scaffold. One is the drawer, the other appears somewhere under body.
My understanding is that when someone clicks on a ListTile in the Drawer, the MapView should update.
In general, the principle you want to employ in that case is introducing a 3rd party object to mediate between the 2 components.
AWidget <--> SState <--> MapView
The most ill-advised approach of it would be to introduce a global variable (please, don't ever do, just mentioning it so that you get the idea). So chit-chat between the ListTiles and your MapView should happen through this mediator object.
Let's call this object SState. (Double SS is on purpose to differentiate from Flutter's State class). This SState will be responsible for keeping track of the params of the map building. (I'm not actually familiar with the insights of your app, so that is completely made up)
So, this indirection of action handling will mean:
tap on ListTile in drawer -> update SState object -> let Flutter know stuff happened and should update those widgets that are sensitive to that change (MapView)
Updating SState object is easy, it is just a function call/setter call to it. But how do you let Flutter know that it should update MapView? And where should you place that SState object instance so that both can reach it without a global var?
To answer that, there are a number of solutions: scoped_model, InheritedWidget, Provider, BLoC pattern. What is common in all of this is that they employ a principle called lifting state up. Basically means, that the SState object should be created in the tree above both of them.
Provider example:
class SState with ChangeNotifier {
String _someMapParam;
String get someMapParam => _someMapParam;
set someMapParam(String val) {
_someMapParam = val;
if(hasListeners) notifyListeners();
}
}
// [...] somewhere in your app
Widget build(BuildContext context) {
return ChangeNotifierProvider<SState>(
builder: (ctx) => new SState(),
child: Scaffold(
drawer: _drawer(),
body: Consumer<SState>(
builder: (_, sStateInstance, __) => Text(sStateInstance.someMapParam),
),
),
);
}
More details on provider: https://pub.dev/packages/provider

Related

Flutter: Image not updating

Im doing a school project with flutter which has a list view of cards. The card has an image, which are stored in maps.
The map is this Map<String,Image> _imgMap, the key is the name attribute on the _objectList, which is a dynamic list. I pass the image to another class called Loader, which is a stateful widget that returns a container with circularProgress indicator while the image is loading and returns the image if finished.
ListView.builder(
itemCount: _objectList.length,
itemBuilder: (context,index){
return Card(
...
Row(
children<Widget>[
Container(child: Loader(image: _imgMap[_objectList[index].name]))
]
...
Loader Class:
class Loader extends StatefulWidget {
Image image;
Loader({Key key,this.image}) : super(key : key);
#override
_LoaderState createState() => _LoaderState(image);
}
class _LoaderState extends State<Loader> {
Image _image;
bool wait=true;
_LoaderState(this._image);
void initState(){
_image.image.resolve(ImageConfiguration()).addListener(
ImageStreamListener(
(info, call) {
setState(() {
wait = false;
});
},
),
);
}
#override
Widget build(BuildContext context) {
return wait ?CircleAvatar(
backgroundColor: Colors.transparent,
radius: 50,
child: simpleCircleLoading() //this is the circleprogress widget,
): Image(image: _image.image);
}
}
It works fine the first time, shows the correct image. But after adding a search function which lets the user search by name and then updates the list with only the matching one, the resulting image is the wrong one.
void search(String value) {
if(value.isNotEmpty){
List<dynamic> result = List<dynamic>();
_backUp.forEach((obj) { //_backup is the complete list of objects, will not change
if(obj.name.toLowerCase().contains(value.toLowerCase())) {
result.add(obj);
}
});
setState(() {
_objectList = result;
});
}else {
setState(() {
_objectList = _backUp;
});
}
}
Examples:
The initial list
Search result
As you can see the information on the card is updating but the image not.The problem is somewhere in the loader class i think since replacing the Loader in the listview with a Image widget fix the error, but i want to show a loader.
Found the error, i had to pass an UniqueKey to Loader class.

How to show increment of counter in my ui in flutter? [duplicate]

This question already has an answer here:
variable won't change Text dynamically in flutter
(1 answer)
Closed 3 years ago.
I have declared a variable called ticketCounter that is getting stored in SharedPrefs. This variable is getting incremented and decremented as needed. However I wanted to display this counter on my appBar and want its value to update. Although the value is getting stored in the variable it isn't showing up.
int tickets;
void fetchTickets() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
tickets = prefs.getInt('ticketRW') ?? 0;
}
void initState() {
portraitModeOnly();
fetchTickets();
super.initState();
}
Widget build(BuildContext context) {
return Material(
child: Scaffold(
appBar: AppBar(
actions: <Widget>[
Padding(
padding: const EdgeInsets.only(top: 14, right: 14),
child: Text(
tickets.toString(),//isn't getting updated
style: TextStyle(
fontSize: 22,
fontFamily: "Netflix",
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
),
),
),
}
You should use setState() to update your state.
Change this method:
void fetchTickets() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
tickets = prefs.getInt('ticketRW') ?? 0;
}
with this:
void fetchTickets() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
setState(() {
tickets = prefs.getInt('ticketRW') ?? 0;
});
}
In Flutter, there are StatefulWidget and StatelessWidget.
Stateless widgets have an initial state and it never changes.
Stateful widgets have state objects paired with them. Widgets are UI representations of those state objects. If you have a counter variable in a state object, stateful widget will draw the value of counter. However, chaning the counter variable will not update the UI. You should tell the widget "Hey, I changed your state object, you better check it and update yourself." with setState().
This is an introduction to the state management in Flutter. I suggest you to read further here.

How can I scale/zoom in and out the widgets with only one finger using matrix gesture detector?

I am trying to scaling/zooming the widgets by tapping and dragging from certain points of widgets by only one finger using matrix gesture detector.With two finger it is working but i need it done with one finger.How can I implement it? My code is:
class MyTransformWidget extends StatefulWidget {
final Widget transformWidget;
MyTransformWidget({this.transformWidget});
#override
_MyTransformWidgetState createState() => _MyTransformWidgetState();
}
class _MyTransformWidgetState extends State<MyTransformWidget> {
#override
Widget build(BuildContext context) {
final ValueNotifier<Matrix4> notifier = ValueNotifier(Matrix4.identity());
return MatrixGestureDetector(
onMatrixUpdate: (m, tm, sm, rm) {
notifier.value = m;
},
child: AnimatedBuilder(
animation: notifier,
child: widget.transformWidget,
builder: (ctx, child) {
return Transform(transform: notifier.value, child: child);
},
),
shouldRotate: false,
shouldScale: true,
shouldTranslate: false,
);
}
}

What would be the proper way to update animation values in a Flutter animation?

So I'm trying to create an animation in Flutter that requires a different outcome every time the user presses a button.
I've implemented the following code according to the Flutter Animations tutorial and created a function to update it.
class _RoulettePageWidgetState extends State<RoulettePageWidget>
with SingleTickerProviderStateMixin {
Animation<double> _animation;
Tween<double> _tween;
AnimationController _animationController;
int position = 0;
#override
void initState() {
super.initState();
_animationController =
AnimationController(duration: Duration(seconds: 2), vsync: this);
_tween = Tween(begin: 0.0, end: 100.0);
_animation = _tween.animate(_animationController)
..addListener(() {
setState(() {});
});
}
void setNewPosition(int newPosition) {
_tween = Tween(
begin: 0.0,
end: math.pi*2/25*newPosition);
_animationController.reset();
_tween.animate(_animationController);
_animationController.forward();
}
#override
Widget build(BuildContext context) {
return Container(
child: Column(
children: <Widget>[
Center(
child: Transform.rotate(
angle: _animationController.value,
child: Icon(
Icons.arrow_upward,
size: 250.0,
),
)),
Expanded(
child: Container(),
),
RaisedButton(
child: Text('SPIN'),
onPressed: () {
setState(() {
setNewPosition(math.Random().nextInt(25));
});
},
)
],
)
);
}
}
As you can see I'm updating the _tween's begin: and end: but this doesn't seem to change the animation.
So what should I be doing to create a 'different' animation every time the users presses the button?
The general idea is to make the animations build upon each other with a random new value so for example:
first spin: 0 -> 10
second spin: 10 -> 13
third spin: 13 -> 18
... etc
So I wondered if I could update the animation, or should I create a new animation every time?
Another thing I could think of was tracking the positions and use the same animation every time (0.0 -> 100.0) to act as a percentage of the transfer.
So instead of creating a new animation from 10 -> 15 I would be doing something like:
currentValue = 10 + (15-10)/100*_animationController.value
I'm going to skip your code a bit, and focus on what you're really asking:
The general idea is to make the animations build upon each other with a random new value so for example:
first spin: 0 -> 10
second spin: 10 -> 13
third spin: 13 -> 18
... etc
With an explicit animation like this, there are three objects you are interested in:
a controller, which is a special kind of Animation that simply generates values linearly from its lower to its upper bound (both doubles, typically 0.0 and 1.0). You can control the flow of the animation - send it running forward, reverse it, stop it, or reset it.
a tween, which isn't an Animation but rather an Animatable. A tween defines the interpolation between two values, which don't even have to be numbers. It implements a transform method under the hood that takes in the current value of an animation and spits out the actual value you want to work with: another number, a color, a linear gradient, even a whole widget. This is what you should use to generate your angles of rotation.
an animation, which is the animation whose value you're actually going to work with (so this is where you'd grab values to build with). You get this by giving your tween a parent Animation to transform - this might be your controller directly but can also be some other sort of animation you've built on it (like a CurvedAnimation, which would give you easing or bouncy/elastic curves and so on). Flutter's animations are highly composable that way.
Your code is failing largely because you're not actually using the top-level animation you created in your build method and you're creating a new tween and animation every time you call setNewPosition. You can use the same tween and animation for multiple animation "cycles" - simply change the begin and end properties of the existing tween and it bubbles up to the animation. That ends up something like this:
class _RoulettePageWidgetState extends State<RoulettePageWidget>
with SingleTickerProviderStateMixin {
Animation<double> _animation;
Tween<double> _tween;
AnimationController _animationController;
math.Random _random = math.Random();
int position = 0;
double getRandomAngle() {
return math.pi * 2 / 25 * _random.nextInt(25);
}
#override
void initState() {
super.initState();
_animationController =
AnimationController(duration: Duration(seconds: 2), vsync: this);
_tween = Tween(begin: 0.0, end: getRandomAngle());
_animation = _tween.animate(_animationController)
..addListener(() {
setState(() {});
});
}
void setNewPosition() {
_tween.begin = _tween.end;
_animationController.reset();
_tween.end = getRandomAngle();
_animationController.forward();
}
#override
Widget build(BuildContext context) {
return Container(
child: Column(
children: <Widget>[
Center(
child: Transform.rotate(
angle: _animation.value,
child: Icon(
Icons.arrow_upward,
size: 250.0,
),
)),
Expanded(
child: Container(),
),
RaisedButton(
child: Text('SPIN'),
onPressed: setNewPosition,
)
],
)
);
}
}
Hope that helps!
While working, in no situation will you actually want to make these animations within your layout as explained by #filleduchaos.
This is under optimized, as you're rebuilding far more than you should for the animation. And it's a pain to write yourself.
You'll want to use the AnimatedWidget family for this. They are divided into two
kinds:
XXTransition
AnimatedXX
The first is a low layer that consumes an Animation and listens to it so that you don't need to do that ugly :
..addListener(() {
setState(() {});
});
The second handles the remaining pieces: AnimationController, TickerProvider and Tween.
This makes using animations much easier as it's almost entirely automatical.
In your case a rotation example would be as followed:
class RotationExample extends StatefulWidget {
final Widget child;
const RotationExample({
Key key,
this.child,
}) : super(key: key);
#override
RotationExampleState createState() {
return new RotationExampleState();
}
}
class RotationExampleState extends State<RotationExample> {
final _random = math.Random();
double rad = 0.0;
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _rotate,
child: AnimatedTransform(
duration: const Duration(seconds: 1),
alignment: Alignment.center,
transform: Matrix4.rotationZ(rad),
child: Container(
color: Colors.red,
height: 42.0,
width: 42.0,
),
),
);
}
void _rotate() {
setState(() {
rad = math.pi * 2 / 25 * _random.nextInt(25);
});
}
}
Easier right?
The irony is that Flutter forgot to provide an AnimatedTransform (even although we have many others !). But no worries, I made it for you!
The AnimatedTransform implementation is as followed :
class AnimatedTransform extends ImplicitlyAnimatedWidget {
final Matrix4 transform;
final AlignmentGeometry alignment;
final bool transformHitTests;
final Offset origin;
final Widget child;
const AnimatedTransform({
Key key,
#required this.transform,
#required Duration duration,
this.alignment,
this.transformHitTests = true,
this.origin,
this.child,
Curve curve = Curves.linear,
}) : assert(transform != null),
assert(duration != null),
super(
key: key,
duration: duration,
curve: curve,
);
#override
_AnimatedTransformState createState() => _AnimatedTransformState();
}
class _AnimatedTransformState
extends AnimatedWidgetBaseState<AnimatedTransform> {
Matrix4Tween _transform;
#override
void forEachTween(TweenVisitor<dynamic> visitor) {
_transform = visitor(_transform, widget.transform,
(dynamic value) => Matrix4Tween(begin: value));
}
#override
Widget build(BuildContext context) {
return Transform(
alignment: widget.alignment,
transform: _transform.evaluate(animation),
transformHitTests: widget.transformHitTests,
origin: widget.origin,
child: widget.child,
);
}
}
I will submit a pull request so that in the future you won't need this bit of code.
If you want to reverse your animation with a different path (go/back way). Try this.
In your setNewPosition function, just define new begin/end value for _tween.
void setNewPosition() {
_tween.begin = 0; //new begin int value
_tween.end = 1; //new end int value
_animationController.reverse();
}

Flutter: Hero transition + widget animation at the same time?

So, I'm having a bit of an issue with Flutter in regards to a specific animation case.
Basically, what I'm trying to do is simultaneously have both a hero transition run for a route change and a custom animation on an adjacent widget.
Broken down, I have a custom InheritedWidget at my root which is fed an app state from a StatefulWidget parent. Nested within my InheritedWidget, I have a WidgetsApp and an adjacent sibling for a custom tab navigation. The tree looks something like this:
Root Widget (Stateful)
|
|__InheritedWidget
|
|__WidgetsApp (Handles routing)
|
|__Navigation Bar (Overlay)
My issue arises when I on my WidgetsApp perform a route change which uses a Hero transition. While this is happening, I'm trying to also animate the Navigation Bar to either be shown or hidden depending on what view the user is on. But, since I'm using a bool variable on my app state to either show or hide the Navigation Bar via an animation, the SetState call there 'overwrites' the hero transition since the tree is rebuilt in the process (is what I'm thinking).
My initial thought was that the InheritedWidget would catch the app state change and only rebuild the Navigation Bar via updateShouldNotify, but alas this isn't what I'm seeing as the desired effect :(
So - has anyone tried anything similar, or have an idea as to how this could be handled gracefully? :)
I have done something similar, but unfortunately my code also contains a bunch of other stuff & this is relatively convoluted to do, so I'd have to split things out to make an example which is a bit more than I can do right now. I'll explain the general concept of what I did though. There may also be better ways of doing this.
You want to write a StatefulWidget with a State that also extends NavigatorObserver (you may be able to use a stateless widget but I don't think so). I personally put this above the navigator in the tree (i.e. it builds the navigator in its' build function), but you could most likely also have it 'beside' the navigator.
Override the didPush, didRemove, didPop etc methods from NavigatorObserver. Within each of these, call a setState and save the animation & other paramters, something like this:
class NavigationFaderState extends State<NavigationFader> with NavigatorObserver {
Animation _animation;
// whatever else you need, maybe starting/finishing opacity or position etc.
#override
void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
setState(() {
_animation = route.animation;
}
route.animation.addStatusListener((status) {
if (status = AnimationStatus.completed) {
setState(() {
_animation = null;
});
}
});
}
....
}
In your build function you'll want to check the _animation and animate based on whether it exists, and any other parameters you might want to set (i.e. a flag whether to animate, and whether the is going forward or backwards could be helpful - I believe the 'pop' animation have have started at 0 and gone to 1 the same as the push one but I could be wrong).
You can then hook up this animation to however you want to animate your navigation bar, probably using an AnimatedBuilder or hooking up the animation directly, or something. If there are any specific questions about how this all works, comment and I'll add some comments etc.
Hope that helps =)
EDIT: With full code example. For the record, I don't propose that this code is all that good, or that this is something you should do. But it is a way of solving the problem. Before using it in a real app, it would be worth testing it and probably adding some assertions to check for states etc.
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
PushListener listener = new PushListener();
#override
Widget build(BuildContext context) {
return new WidgetsApp(
locale: new Locale("en"),
navigatorObservers: [listener],
builder: (context, child) {
// this is here rather than outside the WidgetsApp so that it
// gets access to directionality, text styles, etc
return new Scaffold(
body: child,
bottomNavigationBar:
new ColorChangingNavigationBar(key: listener.navBarKey),
);
},
onGenerateRoute: (settings) {
switch (settings.name) {
case '/':
return new MaterialPageRoute(
settings: settings,
builder: (context) => Column(
children: <Widget>[
new Text(
"I have a green nav bar when you open me and blue when you come back"),
new RaisedButton(
onPressed: () {
Navigator.pushNamed(context, "/red");
},
child: new Text("Next"),
),
],
),
);
case '/red':
return new MaterialPageRoute(
settings: settings,
builder: (context) => Column(
children: <Widget>[
new Text("I have a red nav bar"),
new RaisedButton(
onPressed: () {
Navigator.pop(context);
},
)
],
),
);
}
},
color: Colors.blue,
);
}
}
class PushListener extends NavigatorObserver {
GlobalKey<ColorChangingNavigationBarState> navBarKey = new GlobalKey();
#override
void didPop(Route route, Route previousRoute) {
if (route is ModalRoute && navBarKey.currentState != null) {
var name = route.settings.name;
var color = name == "/" ? Colors.red.shade500 : Colors.blue.shade500;
var animation = new ReverseAnimation(route.animation);
print("Popping & changing color to: ${name == "/" ? "red" : "blue"}");
navBarKey.currentState.setAnimating(animation, color);
}
}
#override
void didPush(Route route, Route previousRoute) {
if (route is ModalRoute && navBarKey.currentState != null) {
var name = route.settings.name;
var color = name == "/" ? Colors.blue.shade500 : Colors.red.shade500;
print("Pushing & changing color to: ${name == "/" ? "red" : "blue"}");
var animation = route.animation;
navBarKey.currentState.setAnimating(animation, color);
}
}
#override
void didRemove(Route route, Route previousRoute) {
// probably don't need
}
#override
void didStartUserGesture() {
// might want to do if gestures are supported with whichever type of
// route you're using.
}
#override
void didStopUserGesture() {
// if you implement didStartUserGesture
}
}
class ColorChangingNavigationBar extends StatefulWidget {
final Color startColor;
ColorChangingNavigationBar(
{Key key, this.startColor = const Color.fromRGBO(0, 255, 0, 1.0)})
: super(key: key);
#override
State<StatefulWidget> createState() => new ColorChangingNavigationBarState();
}
class _ColorAnimationInfo {
final Animation animation;
final Tween<Color> colorTween;
final AnimationStatusListener statusListener;
_ColorAnimationInfo(this.animation, this.colorTween, this.statusListener);
}
class ColorChangingNavigationBarState
extends State<ColorChangingNavigationBar> {
#override
void initState() {
_toColor = widget.startColor;
super.initState();
}
Color _toColor;
_ColorAnimationInfo _colorAnimationInfo;
void setAnimating(Animation animation, Color to) {
var fromColor;
if (_colorAnimationInfo != null) {
fromColor = _colorAnimationInfo.colorTween
.lerp(_colorAnimationInfo.animation.value);
_colorAnimationInfo.animation
.removeStatusListener(_colorAnimationInfo.statusListener);
} else {
fromColor = _toColor;
}
var statusListener = (state) {
if (state == AnimationStatus.completed ||
state == AnimationStatus.dismissed) {
setState(() {
_colorAnimationInfo = null;
});
}
};
animation.addStatusListener(statusListener);
setState(() {
_toColor = to;
Tween<Color> colorTween = new ColorTween(begin: fromColor, end: to);
_colorAnimationInfo =
new _ColorAnimationInfo(animation, colorTween, statusListener);
});
}
#override
Widget build(BuildContext context) {
if (_colorAnimationInfo != null) {
return new AnimatedBuilder(
animation: _colorAnimationInfo.animation,
builder: (context, child) {
return new Container(
color: _colorAnimationInfo.colorTween
.lerp(_colorAnimationInfo.animation.value),
height: 30.0,
);
});
} else {
return new Container(
color: _toColor,
height: 30.0,
);
}
}
#override
void dispose() {
if (_colorAnimationInfo != null) {
_colorAnimationInfo.animation.removeStatusListener(_colorAnimationInfo.statusListener);
}
_colorAnimationInfo = null;
super.dispose();
}
}

Resources