Flutter change state from StatefulWidget object - dart

Like title states, how can one access the state of a StatefulWidget from the StatefulWidget.
Background:
I have a star rating widget that consists of 5 "StarWidget"s in a row. The StarWidget class is just an Icon with a detector wrapped around it (not using IconButton because it has a very large size). The StarWidget stores whether it is selected or not in a corresponding State object and accordingly displays a solid or outline icon.
In my main widget, I have access to the StatefulWidget objects, and would like to configure their states.
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
class StarRatingWidget extends StatefulWidget {
#override
_StarRatingWidgetState createState() {
return _StarRatingWidgetState();
}
}
class _StarRatingWidgetState extends State<StarRatingWidget>
implements StarSelectionInterface {
//Properties
int _currentRating = 0;
List<RatingStarWidget> starWidgets = [];
//Methods
#override
void initState() {
super.initState();
starWidgets.add(
RatingStarWidget(
starSelectionInterface: this,
starPosition: 0,
),
);
starWidgets.add(
RatingStarWidget(
starSelectionInterface: this,
starPosition: 1,
),
);
starWidgets.add(
RatingStarWidget(
starSelectionInterface: this,
starPosition: 2,
),
);
starWidgets.add(
RatingStarWidget(
starSelectionInterface: this,
starPosition: 3,
),
);
starWidgets.add(
RatingStarWidget(
starSelectionInterface: this,
starPosition: 4,
),
);
}
#override
Widget build(BuildContext buildContext) {
return Row(
children: starWidgets,
);
}
//Star Selection Interface Methods
void onStarSelected(_RatingStarWidgetState starWidgetState) {
print("listener: star selected ${starWidgetState._starPosition}");
//a new, rating has been selected, update rating
if (_currentRating != starWidgetState._starPosition) {
_currentRating = (starWidgetState._starPosition + 1);
}
//same star as rating has been selected, set rating to 0
else {
_currentRating = 0;
}
//update stars according to rating
for(int i = 1; i <= 5; i++) {
//what should I do here?!
}
}
}
class RatingStarWidget extends StatefulWidget {
//Properties
final int starPosition;
final StarSelectionInterface starSelectionInterface;
//Constructors
RatingStarWidget({this.starSelectionInterface, this.starPosition});
//Methods
#override
_RatingStarWidgetState createState() {
return _RatingStarWidgetState(starSelectionInterface, starPosition);
}
}
class _RatingStarWidgetState extends State<RatingStarWidget> {
//Properties
int _starPosition;
bool _isSelected = false;
StarSelectionInterface selectionListener;
//Constructors
_RatingStarWidgetState(this.selectionListener, this._starPosition);
//Methods
#override
Widget build(BuildContext buildContext) {
return AnimatedCrossFade(
firstChild: GestureDetector(
child: Icon(
FontAwesomeIcons.star,
size: 14,
),
onTap: () {
print("star: selected");
selectionListener.onStarSelected(this);
},
),
secondChild: GestureDetector(
child: Icon(
FontAwesomeIcons.solidStar,
size: 14,
),
onTap: () {
selectionListener.onStarSelected(this);
},
),
duration: Duration(milliseconds: 300),
crossFadeState:
_isSelected ? CrossFadeState.showSecond : CrossFadeState.showFirst,
);
}
}
class StarSelectionInterface {
void onStarSelected(_RatingStarWidgetState starWidgetState) {}
}

The Flutter way is to rebuild widgets whenever it is necessary. Don't be afraid to build widgets, they are cheap for the SDK, specially in this case for simple stars.
Accessing another widget state requires more work than just rebuilding it. To access the state you should use keys or you should add special methods in the widget itself.
In this case, where the star is rebuilt no matter what, it is even better and simpler to use plain stateless widgets because the selected state can be provided by the parent in the moment of rebuilding.
And since the state is stored in the parent widget, I think it is better no to store it as wall in each one of the individual stars.
Next is a very simple solution that follows that idea. But yes, it still rebuilds the stars.
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(body: Center(child: StarRatingWidget())),
);
}
}
class StarRatingWidget extends StatefulWidget {
#override
_StarRatingWidgetState createState() {
return _StarRatingWidgetState();
}
}
class _StarRatingWidgetState extends State<StarRatingWidget> {
int _currentRating = 0;
List<Widget> buildStars() {
List<RatingStarWidget> starWidgets = [];
for (int i = 0; i < 5; i++) {
starWidgets.add(
RatingStarWidget(
clickCallback: () => setState(() {
_currentRating = i + 1;
}),
highlighted: _currentRating > i,
),
);
}
return starWidgets;
}
#override
Widget build(BuildContext buildContext) {
return Row(
children: buildStars(),
);
}
}
class RatingStarWidget extends StatelessWidget {
//Properties
final VoidCallback clickCallback;
final bool highlighted;
//Constructors
RatingStarWidget({this.clickCallback, this.highlighted});
#override
StatelessElement createElement() {
print("Element created");
return super.createElement();
}
//Methods
#override
Widget build(BuildContext buildContext) {
return GestureDetector(
onTap: () {
clickCallback();
},
child: AnimatedCrossFade(
firstChild: Icon(
FontAwesomeIcons.star,
size: 14,
),
secondChild: Icon(
FontAwesomeIcons.solidStar,
size: 14,
),
duration: Duration(milliseconds: 300),
crossFadeState:
highlighted ? CrossFadeState.showSecond : CrossFadeState.showFirst,
),
);
}
}

I wrote my own example similar to yours. What I do here is:
Initial star rate is -1 because arrays start from 0 ;) and I create stars with position, current star rate and the callback function. We will use this callback function to update the value in the ScreenOne.
In Star widget, we have a local bool selected with default value false and we assign it a value inside the build function based on the position of the star and current rate. And we have setSelected() function which runs the callback function and updates currentRate with the value of star position.
Check the video example here.
class ScreenOne extends StatefulWidget {
#override
_ScreenOneState createState() => _ScreenOneState();
}
class _ScreenOneState extends State<ScreenOne> {
int currentRate = -1; //since array starts from 0, set non-selected as -1
List<Star> starList = []; //empty list
#override
void initState() {
super.initState();
buildStars(); //build starts here on initial load
}
Widget buildStars() {
starList = [];
for (var i = 0; i < 5; i++) {
starList.add(Star(
position: i,
current: currentRate,
updateParent: refresh, //this is callback
));
}
}
refresh(int index) {
setState(() {
currentRate = index; //update the currentRate
});
buildStars(); //build stars again
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: Text("Test page 1"),
),
body: Container(
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: starList,
),
),
),
);
}
}
class Star extends StatefulWidget {
final Function(int index) updateParent; //callback
final int position; //position of star
final int current; //current selected star from parent
const Star({Key key, this.position, this.updateParent, this.current})
: super(key: key);
#override
_StarState createState() => _StarState();
}
class _StarState extends State<Star> {
bool selected = false;
void setSelected() {
widget.updateParent(widget.position);
}
#override
Widget build(BuildContext context) {
if (widget.current >= widget.position) {
selected = true;
} else {
selected = false;
}
return GestureDetector(
child: AnimatedCrossFade(
firstChild: Icon(Icons.star_border),
secondChild: Icon(Icons.star),
crossFadeState:
selected ? CrossFadeState.showSecond : CrossFadeState.showFirst,
duration: Duration(milliseconds: 300),
),
onTap: () {
setSelected();
},
);
}
}

Related

Single Instance of Widget class is used even when different variables are coded to be used

I want to use BottomNavigationBar of Flutter so for that I have created a class called BaseWidget which will be changed as the user taps the item.
class BaseWidget extends StatefulWidget {
final String title;
BaseWidget(this.title);
_BaseWidgetState createState() => _BaseWidgetState(this.title);
}
class _BaseWidgetState extends State<BaseWidget> {
final String title;
_BaseWidgetState(this.title);
#override
Widget build(BuildContext context) {
return Center(child: Text(title));
}
}
In the above class am returning the Center widget with child as Text widget.
class HomeWidget extends StatefulWidget {
_HomeWidgetState createState() => _HomeWidgetState();
}
class _HomeWidgetState extends State<HomeWidget> {
int pageIndex = 0;
final _home = BaseWidget('Home');
final _business = BaseWidget('Business');
final _school = BaseWidget('School');
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Bottom Navigation Bar'),
),
body: choosePager(),
bottomNavigationBar: BottomNavigationBar(
currentIndex: pageIndex,
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home), title: Text('Home')),
BottomNavigationBarItem(
icon: Icon(Icons.business), title: Text('Business')),
BottomNavigationBarItem(
icon: Icon(Icons.school), title: Text('School')),
],
onTap: onTap,
),
),
);
}
void onTap(int index) {
setState(() {
this.pageIndex = index;
});
}
Widget choosePager() {
switch (pageIndex) {
case 0:
return _home;
break;
case 1:
return _business;
break;
case 2:
return _school;
break;
default:
return Text('Unknown');
break;
}
}
}
Problem 1:
Whenever user taps on the BottomNavigationBarItem the text should change to the respected string passed in the BaseWidget's constructor. But it only shows Home and the rest 2 are ignored.
Problem 2:
I am planning to replace Center widget with the ListView widget to populate the list of Schools and Businesses which will be fetched from the network API in paginated way. So I don't want to reinitialise the classes again when BottomNavigationBarItem is tapped as that would result in loss of data which is already fetched. To prevent data lose I am declaring _home, _business & _school property and using these property in choosePager() method.
There are several issues with your code:
1- The real problem is that you never rebuild the BaseWidget. You construct 3 new BaseWidgets, but you only ever call the build of the _home widget, because it's the first one returned by choosePager(). Since you don't create _home, _business, _school in the HomeWidget build, no other BaseWidget can ever get built.
2- When you don't need to store any state/variables for a widget, use a Stateless widget.
3- Don't do anything in the constructor of your State. Use initState https://docs.flutter.io/flutter/widgets/State/initState.html for that instead.
4- Create widgets using const constructors when possible.
5- Widget constructor take named parameters. One of those should be the key. Use super to call the base constructor.
With that in mind, this is what the code should look like:
class BaseWidget extends StatelessWidget {
final String title;
const BaseWidget({Key key, this.title}) : super(key: key);
#override
Widget build(BuildContext context) {
return Center(
child: Text(title),
);
}
}
class HomeWidget extends StatefulWidget {
_HomeWidgetState createState() => _HomeWidgetState();
}
class _HomeWidgetState extends State<HomeWidget> {
int pageIndex = 0;
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Bottom Navigation Bar'),
),
body: choosePager(),
bottomNavigationBar: BottomNavigationBar(
currentIndex: pageIndex,
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text('Home'),
),
BottomNavigationBarItem(
icon: Icon(Icons.business),
title: Text('Business'),
),
BottomNavigationBarItem(
icon: Icon(Icons.school),
title: Text('School'),
),
],
onTap: onTap,
),
),
);
}
void onTap(int index) {
setState(() {
pageIndex = index;
});
}
Widget choosePager() {
Widget result;
switch (pageIndex) {
case 0:
result = BaseWidget(title: 'Home');
break;
case 1:
result = BaseWidget(title: 'Business');
break;
case 2:
result = BaseWidget(title: 'School');
break;
default:
result = Text('Unknown');
break;
}
return result;
}
}
Edit: For your example, you may want to fetch some data from the network and only use the widget to display it. In that case, create a new class (not a Widget) to fetch & hold on to the data, and use the Widget only for displaying the data.
Some sample code:
/// Silly class to fetch data
class DataClass {
static int _nextDatum = 0;
int _data;
DataClass();
Future<int> fetchData() async {
await Future.delayed(Duration(
milliseconds: 2000,
));
_data = _nextDatum++;
return _data;
}
int getData() {
return _data;
}
}
class BaseClass extends StatefulWidget {
final String title;
final DataClass data;
const BaseClass({Key key, this.title, this.data}) : super(key: key);
#override
_BaseClassState createState() => _BaseClassState();
}
class _BaseClassState extends State<BaseClass> {
String title;
DataClass data;
#override
Widget build(BuildContext context) {
String dataStr = data == null ? ' - ' : '${data.getData()}';
return Center(
child: Text(
'$title: $dataStr',
),
);
}
#override
void initState() {
super.initState();
// initState gets called only ONCE
title = widget.title;
data = widget.data;
}
#override
void didUpdateWidget(BaseClass oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.data != oldWidget.data) {
data = widget.data;
}
if (widget.title != oldWidget.title) {
title = widget.title;
}
}
}
class HomeWidget extends StatefulWidget {
_HomeWidgetState createState() => _HomeWidgetState();
}
class _HomeWidgetState extends State<HomeWidget> {
int pageIndex = 0;
Map<String, DataClass> _dataMap = <String, DataClass>{};
#override
void initState() {
super.initState();
_init().then((result) {
// Since we need to rebuild the widget with the resulting data,
// make sure to use `setState`
setState(() {
_dataMap = result;
});
});
}
Future<Map<String, DataClass>> _init() async {
// this fetches the data only once
return <String, DataClass>{
'home': DataClass()..fetchData(),
'business': DataClass()..fetchData(),
'school': DataClass()..fetchData(),
};
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Bottom Navigation Bar'),
),
body: choosePager(),
bottomNavigationBar: BottomNavigationBar(
currentIndex: pageIndex,
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home),
title: Text('Home'),
),
BottomNavigationBarItem(
icon: Icon(Icons.business),
title: Text('Business'),
),
BottomNavigationBarItem(
icon: Icon(Icons.school),
title: Text('School'),
),
],
onTap: onTap,
),
),
);
}
void onTap(int index) {
setState(() {
pageIndex = index;
});
}
Widget choosePager() {
Widget result;
switch (pageIndex) {
// it doesn't matter if you create a new BaseClass() a hundred times, flutter is optimized enough to not care. The `initState()` gets called only once. You're fetching the data only once.
case 0:
result = BaseClass(
title: 'Home',
data: _dataMap['home'],
);
break;
case 1:
result = BaseClass(
title: 'Business',
data: _dataMap['business'],
);
break;
case 2:
result = BaseClass(
title: 'School',
data: _dataMap['school'],
);
break;
default:
result = Text('Unknown');
break;
}
return result;
}
}
After lot of RND I solved my problem using IndexedStack. It shows the single Child from the list of children based on the index.
It will initialise all the children when the Widget build(BuildContext context) method of the _HomeWidgetState is called. So any time you switch the tabs, the object won't be reinitialised.
Here is my full code
class BaseWidget extends StatefulWidget {
final String title;
BaseWidget(this.title);
_BaseWidgetState createState() => _BaseWidgetState();
}
class _BaseWidgetState extends State<BaseWidget> {
#override
Widget build(BuildContext context) {
return Center(child: Text(widget.title));
}
}
class HomeWidget extends StatefulWidget {
_HomeWidgetState createState() => _HomeWidgetState();
}
class _HomeWidgetState extends State<HomeWidget> {
int _pageIndex = 0;
List<Widget> _children;
#override
void initState() {
super.initState();
_children = [
BaseWidget('Home'),
BaseWidget('Business'),
BaseWidget('School')
];
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Bottom Navigation Bar'),
),
body: IndexedStack(
children: _children,
index: _pageIndex,
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _pageIndex,
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home), title: Text('Home')),
BottomNavigationBarItem(
icon: Icon(Icons.business), title: Text('Business')),
BottomNavigationBarItem(
icon: Icon(Icons.school), title: Text('School')),
],
onTap: onTap,
),
),
);
}
void onTap(int index) {
setState(() {
_pageIndex = index;
});
}
}
Please try this code, I have edited the BaseWidget class
class BaseWidget extends StatefulWidget {
final String title;
BaseWidget(this.title);
_BaseWidgetState createState() => _BaseWidgetState();
}
class _BaseWidgetState extends State<BaseWidget> {
#override
Widget build(BuildContext context) {
return Center(child: Text(widget.title));
}
}

Two widgets one re-renders the other does not

I appear to have "lost the bubble" regarding re-rendering! I'm not sure what is wrong with the current implementation of my app. It was derived with the help of a number of SO members.
What it's supposed to do: Render 9 green circles and then, one after another, render each circle in yellow in either increasing or decreasing order (Circles widget). Display the current value of count (Counter widget). In the AppBar (Home_Page widget): recognize a tap of + and increment count, recognize a tap of - and decrement count. In both cases, perform the increment/decrement in the body of a setState method. Both the Circles and the Counter widgets are expected to re-render. In this implementation, all circles participate in the green-to-yellow-to-green color changes (effectively count is ignored).
What it does: The initial rendering of the Circles and the Counter widgets display as desired. But the AppBar icons (+ and -), although recognized, do not cause a re-rendering of the Circles widget. The Counter widget does re-render its display of count. In the Circles widget is a RaisedButton that when tapped does cause the re-rendering of the Circles widget. But that button is not desired in a final implementation and is only present for testing.
What has me perplexed is that the template used for the Circles widget is the same as that used for the Counter widget. Yet they appear to execute differently.
The source code for the whole application follows. It is a single .dart file (sorry it's so long but in the past leaving something out caused questions).
Thoughts?
// ignore_for_file: camel_case_types
// ignore_for_file: constant_identifier_names
// ignore_for_file: non_constant_identifier_names
import 'package:flutter/material.dart';
import 'dart:async';
import 'dart:math';
const int NUMBER_TILES = 9;
final int CROSS_AXIS_COUNT = (sqrt(NUMBER_TILES)).toInt();
const double CROSS_AXIS_SPACING = 4.0;
const int INITIAL_COUNT = 9; // for testing; should be 1
const double MAIN_AXIS_SPACING = CROSS_AXIS_SPACING;
const int MILLISECOND_MULTIPLIER = 500;
// ************************************************************** main
void main() {
final AppState app_state = new AppState(counter: INITIAL_COUNT);
runApp(new Home_Page(app_state: app_state));
} // main
// **************************************************** class AppState
class AppState {
int counter = 0;
List<int> flash_indices = [];
bool forward = false;
AppState({this.counter}); // AppState
String toString() { // toString
return ( 'AppState{' +
'counter: $counter, ' +
'flash_indices: $flash_indices}');
} // toString
// following is a mock
void randomize_flash_indices ( ) { // randomize_flash_indices
forward = !forward;
if ( forward){
flash_indices = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, -1];
}
else {
flash_indices = [ 8, 7, 6, 5, 4, 3, 2, 1, 0, -1];
}
flash_indices.add (-1); // restore to normal colors
} // randomize_flash_indices
} // class AppState
// *************************************************** class Home_Page
class Home_Page
extends StatefulWidget {
final AppState app_state;
Home_Page({
#required this.app_state,
Key key,
}) : super(key: key);
#override
State<StatefulWidget> createState() {
return Home_Page_State();
}
} // class Home_Page
// ********************************************* class Home_Page_State
class Home_Page_State extends State<Home_Page>{
Home_Page_State();
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Periodic',
theme: new ThemeData(primarySwatch: Colors.indigo),
home: Scaffold(
appBar: AppBar(
title: Text('Periodic'),
actions: <Widget>[
IconButton(
icon: Icon(Icons.add), // increment counter
onPressed: () {
if (widget.app_state.counter < NUMBER_TILES){
setState(() {
widget.app_state.counter++;
});
}
}
),
IconButton(
icon: Icon(Icons.remove), // decrement counter
onPressed: () {
if (widget.app_state.counter > 1){
setState(() {
widget.app_state.counter--;
});
}
}
),
]
),
body: Column(
children: [
Circles (
app_state: widget.app_state,
),
Counter (
app_state: widget.app_state,
)
],
),
),
);
} // Home_Page_State build
} // class Home_Page_State
// ***************************************************** class Circles
class Circles extends StatefulWidget {
final AppState app_state;
Circles({
#required this.app_state,
Key key,
}) : super(key: key);
#override
State<StatefulWidget> createState() {
return Circles_State();
}
} // class Circles
// *********************************************** class Circles_State
class Circles_State extends State<Circles>{
Circles_State();
int flash_tile = -1;
List<GridTile> grid_tiles = <GridTile>[];
StreamController<int> tick_controller;
StreamSubscription<int> tick_listener;
Stream<int> start_ticking() { // start_ticking
tick_controller = new StreamController();
for ( int tick = 0; (tick < widget.app_state.counter); tick++ ) {
Future.delayed(Duration(milliseconds:
MILLISECOND_MULTIPLIER * tick),() {
print('start_ticking() tick: $tick');
tick_controller.add(tick);
});
}
return tick_controller.stream;
} // start_ticking
#override
void initState() { // initState
super.initState();
widget.app_state.randomize_flash_indices();
tick_listener = start_ticking().listen(on_tick);
} // initState
#override
void dispose() { // dispose
if (tick_listener != null) {
tick_listener.cancel();
tick_listener = null;
}
super.dispose();
} // dispose
on_tick(int tick) async { // on_tick
print('listen_for_tick() tick: $tick');
this.setState(() => this.flash_tile =
widget.app_state.
flash_indices[tick]);
} // on_tick
GridTile new_circle_tile( // new_circle_tile
Color tile_color,
int index) {
GridTile tile = GridTile(
child: GestureDetector(
child: Container(
decoration: BoxDecoration(
color: tile_color,
shape: BoxShape.circle,
),
),
)
);
return (tile);
} // new_circle_tile
List<GridTile> create_circle_tiles() {// create_circle_tiles
grid_tiles = new List<GridTile>();
for (int i = 0; (i < NUMBER_TILES); i++) {
Color tile_color =
( this.flash_tile == i) ?
Colors.yellow :
Colors.green;
grid_tiles.add(new_circle_tile(tile_color, i));
}
return (grid_tiles);
} // create_circle_tiles
#override // Circles_State
Widget build(BuildContext context) {
print('Circles_State Build ' +
widget.app_state.toString() +
' flash_tile: $flash_tile');
return Column(
children: [
GridView.count(
shrinkWrap: true,
crossAxisCount: CROSS_AXIS_COUNT,
childAspectRatio: 1.0,
padding: const EdgeInsets.all(4.0),
mainAxisSpacing: MAIN_AXIS_SPACING,
crossAxisSpacing: CROSS_AXIS_SPACING,
children: create_circle_tiles(),
),
RaisedButton(
child: Text("restart"),
onPressed: () {
widget.app_state.randomize_flash_indices();
tick_listener = start_ticking().listen(on_tick);
}
),
] // children
);
} // Circles_State build
} // class Circles_State
// ***************************************************** class Counter
class Counter extends StatefulWidget {
final AppState app_state;
Counter({
#required this.app_state,
Key key,
}) : super(key: key);
#override
State<StatefulWidget> createState() {
return Counter_State();
}
} // class Counter
// *********************************************** class Counter_State
class Counter_State extends State<Counter> {
Counter_State();
#override // Counter_State
Widget build(BuildContext context) {
int counter_value = widget.app_state.counter;
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: SizedBox(
width: 24.0,
child: Center(
child: Text(
'Counter $counter_value',
style: TextStyle(
color: Colors.blue,
fontWeight: FontWeight.bold,
fontSize: 24.0,
),
),
),
),
),
],
);
} // Counter_State build
} // class Counter_State
I am very disappointed with the solution I found - combining all widgets into a single class. A skeleton of the original architecture looked like:
void main(){
:
} // main()
class AppState {
:
} // class AppState
class HomePage extends StatefulWidget {
:
} // class HomePage
class HomePageState extends State<HomePage>{
:
} // class HomePageState
class Circles extends StatefulWidget {
:
} // class Circles
class CirclesState extends State<Circles>{}
:
} // class CirclesState
class Counter extends StatefulWidget {
:
} // class Counter
class CounterState extends State<Counter>{}
:
} // class CounterState
Each class could be placed into its own .dart file. However, in order to achieve the results I desired, I had to eliminate all classes except for HomePage and HomePageState. The contents of all of the other classes (variables, methods, and functions) were required to be placed into HomePageState so that, when the state was changed, the widgets would update correctly. A skeleton of the revised implementation looks like:
void main(){
:
} // main()
class HomePage extends StatefulWidget {
:
} // class HomePage
class HomePageState extends State<HomePage>{
:
void randomize_flash_indices ( ) {...
Stream<int> start_ticking() { ...
#override
void initState() { ...
#override
void dispose() { ...
on_tick(int tick) async { ...
Circles(){ ...
Counter(){ ...
#override
Widget build(BuildContext context) {...
:
} // class HomePageState
This implementation is monolithic and flies in the face of good programming (and design) (and architectural) principles. This type of software cannot be maintained in a production environment.
It appears that the architects of the Dart language missed an important point about setState() or I totally missed the correct coding practice. Since I am an experienced programmer (more than 42 years) I tend to doubt the latter (of course, because I am an experienced programmer, I recognize that I may be missing an important point about setState).
I am disappointed in flutter/dart. I had hoped for relief from Xamarin - I'm guessing that I will not get it from flutter/dart.

How to build animated headers like this GIF in Flutter?

In iOS, I wrote a somewhat complex custom UIViewController that handles transitioning between unique child controllers; most notably, a special header view at the top of each one. I'm still trying to really wrap my head around end to end architecture in Flutter, and would like some suggestions on how to accomplish this. There are two types of headers - Arc and Profile, and each one goes from an expanded to a collapsed state as the user scrolls. Additionally, navigation between any combination of type and state can have a transition defined.
Here is how it looks when used in a TabBar for example. Transitions are handled gracefully wether nested in Tab/NavigationControllers or not.
This is what I threw together, I hope it helps (click for video):
Note:
It would be better to reduce the amount of animation controllers, ideally to a single controller that controls both the header extent and the arc curvature
There is no animation for the content below the header, but I'm sure you could add that as well.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Anim playground',
theme: ThemeData(
brightness: Brightness.dark,
),
home: AnimatedPageTest(),
);
}
}
class AnimatedPageTest extends StatefulWidget {
#override
_AnimatedPageTestState createState() => _AnimatedPageTestState();
}
class _AnimatedPageTestState extends State<AnimatedPageTest> {
bool _arc = true;
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(child: AnimatedPage(
appearance: _arc ? HeaderAppearance.arc : HeaderAppearance.profile,
backgroundImage: _arc ? 'assets/earth.jpg' : 'assets/moon.jpg',
children: List.generate(30, (index) => ListTile(title: Text('index'),)),
),),
persistentFooterButtons: <Widget>[
FlatButton(
child: Text('Switch'),
onPressed: () {
setState(() {
_arc = !_arc;
});
},
)
],
);
}
}
enum HeaderAppearance { arc, profile }
double _getTargetMaxExtent(HeaderAppearance appearance) {
if (appearance == HeaderAppearance.arc) {
return 150.0;
} else {
return 75.0;
}
}
double _getTargetArcAnimationValue(HeaderAppearance appearance) {
if (appearance == HeaderAppearance.arc) {
return 1.0;
} else {
return 0.0;
}
}
class AnimatedPage extends StatefulWidget {
AnimatedPage({Key key, this.appearance, this.backgroundImage, this.children}) : super(key: key);
final HeaderAppearance appearance;
final String backgroundImage;
final List<Widget> children;
#override
_AnimatedPageState createState() => _AnimatedPageState();
}
class _AnimatedPageState extends State<AnimatedPage> with SingleTickerProviderStateMixin {
AnimationController _maxExtentAnimation;
#override
void initState() {
super.initState();
_maxExtentAnimation = AnimationController.unbounded(vsync: this, value: _getTargetMaxExtent(widget.appearance));
}
#override
void didUpdateWidget(AnimatedPage oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.appearance != oldWidget.appearance) {
_maxExtentAnimation.animateTo(
_getTargetMaxExtent(widget.appearance),
duration: Duration(milliseconds: 600),
curve: Curves.easeInOut,
);
}
}
#override
void dispose() {
_maxExtentAnimation.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _maxExtentAnimation,
builder: (context, child) {
return CustomScrollView(
slivers: <Widget>[
SliverPersistentHeader(
pinned: true,
delegate: AnimatedHeaderDelegate(
appearance: widget.appearance,
backgroundImage: widget.backgroundImage,
minExtent: 50.0,
maxExtent: _maxExtentAnimation.value,
),
),
child,
],
);
},
child: SliverList(delegate: SliverChildListDelegate(widget.children)),
);
}
}
class AnimatedHeaderDelegate extends SliverPersistentHeaderDelegate {
AnimatedHeaderDelegate({this.appearance, this.backgroundImage, this.minExtent, this.maxExtent});
final HeaderAppearance appearance;
final String backgroundImage;
#override
final double minExtent;
#override
final double maxExtent;
#override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
final shrinkRelative = shrinkOffset / (maxExtent - minExtent);
return AnimatedHeader(
appearance: appearance,
backgroundImage: backgroundImage,
curvatureMultiplier: 1.0 - shrinkRelative,
);
}
#override
bool shouldRebuild(AnimatedHeaderDelegate oldDelegate) {
return appearance != oldDelegate.appearance ||
minExtent != oldDelegate.minExtent ||
maxExtent != oldDelegate.maxExtent;
}
}
class AnimatedHeader extends StatefulWidget {
AnimatedHeader({Key key, this.appearance, this.backgroundImage, this.curvatureMultiplier}) : super(key: key);
final HeaderAppearance appearance;
final String backgroundImage;
final double curvatureMultiplier;
#override
_AnimatedHeaderState createState() => _AnimatedHeaderState();
}
class _AnimatedHeaderState extends State<AnimatedHeader> with TickerProviderStateMixin {
AnimationController _arcAnimation;
#override
void initState() {
super.initState();
_arcAnimation = AnimationController(
vsync: this,
value: _getTargetArcAnimationValue(widget.appearance),
duration: Duration(milliseconds: 600),
);
}
#override
void didUpdateWidget(AnimatedHeader oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.appearance != oldWidget.appearance) {
_arcAnimation.animateTo(_getTargetArcAnimationValue(widget.appearance));
}
}
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: CurvedAnimation(parent: _arcAnimation, curve: Curves.linear),
builder: (context, child) {
return ClipPath(
clipper: ArcClipper(
curvature: _arcAnimation.value * widget.curvatureMultiplier,
),
clipBehavior: Clip.antiAlias,
child: child,
);
},
child: Stack(
fit: StackFit.expand,
children: <Widget>[
AnimatedSwitcher(
duration: Duration(milliseconds: 600),
child: Container(
key: ValueKey(widget.backgroundImage),
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(widget.backgroundImage),
fit: BoxFit.cover,
),
),
),
),
Center(
child: Text(
'TITLE',
style: TextStyle(fontSize: 30.0, fontWeight: FontWeight.w500),
),
),
],
),
);
}
}
class ArcClipper extends CustomClipper<Path> {
ArcClipper({this.curvature});
final double curvature;
#override
Path getClip(Size size) {
if (curvature == 0.0) {
return Path()..addRect(Offset.zero & size);
} else {
return Path()
..moveTo(0.0, 0.0)
..lineTo(size.width, 0.0)
..lineTo(size.width, size.height)
..quadraticBezierTo(size.width / 2, size.height - size.height * 0.4 * curvature, 0.0, size.height)
..close();
}
}
#override
bool shouldReclip(ArcClipper oldClipper) {
return curvature != oldClipper.curvature;
}
}

How to swipe/drag 2 or more buttons in a grid of buttons using flutter

I have made a grid of buttons using flutter but now I want to swipe through 2 or more buttons in a single drag such that all the buttons through which I am dragging gets selected.
I have checked out some questions on the same and I was redirected to use gesture detector but that's not enough. I need certain properties or better a sample code such that I am able to work through it.
an example of the dragable app is http://a5.mzstatic.com/us/r30/Purple60/v4/6f/00/35/6f0035d3-1bab-fcbb-cb13-8ab46cf3c44d/screen696x696.jpeg
You can manually hit test RenderBox and extract a specific RenderObject of your choice.
We could for example add the following renderobject above our buttons:
class Foo extends SingleChildRenderObjectWidget {
final int index;
Foo({Widget child, this.index, Key key}) : super(child: child, key: key);
#override
RenderObject createRenderObject(BuildContext context) {
return _Foo()..index = index;
}
#override
void updateRenderObject(BuildContext context, _Foo renderObject) {
renderObject..index = index;
}
}
class _Foo extends RenderProxyBox {
int index;
}
Then use a Listener to extract all _Foo found under the pointer.
Here's a full application using this principle:
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: Grid(),
);
}
}
class Grid extends StatefulWidget {
#override
GridState createState() {
return new GridState();
}
}
class GridState extends State<Grid> {
final Set<int> selectedIndexes = Set<int>();
final key = GlobalKey();
final Set<_Foo> _trackTaped = Set<_Foo>();
_detectTapedItem(PointerEvent event) {
final RenderBox box = key.currentContext.findRenderObject();
final result = BoxHitTestResult();
Offset local = box.globalToLocal(event.position);
if (box.hitTest(result, position: local)) {
for (final hit in result.path) {
/// temporary variable so that the [is] allows access of [index]
final target = hit.target;
if (target is _Foo && !_trackTaped.contains(target)) {
_trackTaped.add(target);
_selectIndex(target.index);
}
}
}
}
_selectIndex(int index) {
setState(() {
selectedIndexes.add(index);
});
}
#override
Widget build(BuildContext context) {
return Listener(
onPointerDown: _detectTapedItem,
onPointerMove: _detectTapedItem,
onPointerUp: _clearSelection,
child: GridView.builder(
key: key,
itemCount: 6,
physics: NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 1.0,
crossAxisSpacing: 5.0,
mainAxisSpacing: 5.0,
),
itemBuilder: (context, index) {
return Foo(
index: index,
child: Container(
color: selectedIndexes.contains(index) ? Colors.red : Colors.blue,
),
);
},
),
);
}
void _clearSelection(PointerUpEvent event) {
_trackTaped.clear();
setState(() {
selectedIndexes.clear();
});
}
}
class Foo extends SingleChildRenderObjectWidget {
final int index;
Foo({Widget child, this.index, Key key}) : super(child: child, key: key);
#override
_Foo createRenderObject(BuildContext context) {
return _Foo()..index = index;
}
#override
void updateRenderObject(BuildContext context, _Foo renderObject) {
renderObject..index = index;
}
}
class _Foo extends RenderProxyBox {
int index;
}
I don't like this code at all, but it seems to be working
import 'package:flutter/material.dart';
class TestScaffold extends StatefulWidget {
#override
State<StatefulWidget> createState() => _TestScaffoldState();
}
List<_SquareButton> _selectedList = [];
class _TestScaffoldState extends State<TestScaffold> {
List<_SquareButton> buttons = [
_SquareButton('1'),
_SquareButton('2'),
_SquareButton('3'),
_SquareButton('4'),
_SquareButton('5'),
_SquareButton('6'),
_SquareButton('7'),
_SquareButton('8'),
_SquareButton('9'),
_SquareButton('10'),
_SquareButton('11'),
_SquareButton('12'),
_SquareButton('13'),
_SquareButton('14'),
_SquareButton('15'),
_SquareButton('16'),
];
Map<Rect, _SquareButton> positions = {};
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Test'),),
body: GestureDetector(
onPanDown: (details) {
checkGesture(details.globalPosition);
},
onPanUpdate: (details) {
checkGesture(details.globalPosition);
},
child: GridView.count(crossAxisCount: 4,
physics: NeverScrollableScrollPhysics(),
children: buttons,),)
);
}
initPositions() {
if (positions.isNotEmpty) return;
buttons.forEach((btn) {
RenderBox box = btn.bKey.currentContext.findRenderObject();
Offset start = box.localToGlobal(Offset.zero);
Rect rect = Rect.fromLTWH(start.dx, start.dy, box.size.width, box.size.height);
positions.addAll({rect: btn});
});
}
checkGesture(Offset position) {
initPositions();
positions.forEach((rect, btn) {
if (rect.contains(position)) {
if (!_selectedList.contains(btn)) {
_selectedList.add(btn);
btn.state.setState((){});
}
}
});
}
}
class _SquareButton extends StatefulWidget {
_SquareButton(this.title);
final String title;
final GlobalKey bKey = GlobalKey();
State state;
#override
State<StatefulWidget> createState() {
state = _SquareButtonState();
return state;
}
}
class _SquareButtonState extends State<_SquareButton> {
#override
Widget build(BuildContext context) {
return Padding(key: widget.bKey, padding: EdgeInsets.all(4.0), child: Container(
color: _selectedList.contains(widget) ? Colors.tealAccent : Colors.teal,
child: Text(widget.title),
alignment: Alignment.center,
),);
}
}
There is a moment.
If you enable scrolling - GestureDetector not always work on vertical movements

How to write clickable functions for slider images of image_carousel package in flutter?

I am using imagecarousel package for displaying images from the network. I want to keep onPressed function for images in the slide.
new ImageCarousel(
<ImageProvider>[
new NetworkImage('http://www.hilversum.ferraridealers.com/siteasset/ferraridealer/54f07ac8c35b6/961/420/selected/0/0/0/54f07ac8c35b6.jpg'),
new NetworkImage('http://auto.ferrari.com/en_EN/wp-content/uploads/sites/5/2017/08/ferrari-portofino-reveal-2017-featured-new.jpg'),
new NetworkImage('http://www.hilversum.ferraridealers.com/siteasset/ferraridealer/54f07ac8c35b6/961/420/selected/0/0/0/54f07ac8c35b6.jpg'),
],
interval: new Duration(seconds: 1),
)
After making some modifications to Image Carousel, I was able to implement click event (other events also possible). Here is the sample code.
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
class ImageCarousel extends StatefulWidget {
final List<ImageProvider> imageProviders;
final double height;
final TargetPlatform platform;
final Duration interval;
final TabController tabController;
final BoxFit fit;
// Images will shrink according to the value of [height]
// If you prefer to use the Material or Cupertino style activity indicator set the [platform] parameter
// Set [interval] to let the carousel loop through each photo automatically
// Pinch to zoom will be turned on by default
ImageCarousel(this.imageProviders,
{this.height = 250.0, this.platform, this.interval, this.tabController, this.fit = BoxFit.cover});
#override
State createState() => new _ImageCarouselState();
}
TabController _tabController;
class _ImageCarouselState extends State<ImageCarousel> with SingleTickerProviderStateMixin {
#override
void initState() {
super.initState();
_tabController = widget.tabController ?? new TabController(vsync: this, length: widget.imageProviders.length);
if (widget.interval != null) {
new Timer.periodic(widget.interval, (_) {
_tabController.animateTo(_tabController.index == _tabController.length - 1 ? 0 : ++_tabController.index);
});
}
}
#override
void dispose() {
_tabController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return new SizedBox(
height: widget.height,
child: new TabBarView(
controller: _tabController,
children: widget.imageProviders.map((ImageProvider provider) {
return new CarouselImageWidget(widget, provider, widget.fit, widget.height);
}).toList(),
),
);
}
}
class CarouselImageWidget extends StatefulWidget {
final ImageCarousel carousel;
final ImageProvider imageProvider;
final BoxFit fit;
final double height;
CarouselImageWidget(this.carousel, this.imageProvider, this.fit, this.height);
#override
State createState() => new _CarouselImageState();
}
class _CarouselImageState extends State<CarouselImageWidget> {
bool _loading = true;
Widget _getIndicator(TargetPlatform platform) {
if (platform == TargetPlatform.iOS) {
return new CupertinoActivityIndicator();
} else {
return new Container(
height: 40.0,
width: 40.0,
child: new CircularProgressIndicator(),
);
}
}
#override
void initState() {
super.initState();
widget.imageProvider.resolve(new ImageConfiguration()).addListener((i, b) {
if (mounted) {
setState(() {
_loading = false;
});
}
});
}
#override
Widget build(BuildContext context) {
return new Container(
height: widget.height,
child: _loading
? _getIndicator(widget.carousel.platform == null ? defaultTargetPlatform : widget.carousel.platform)
: new GestureDetector(
child: new Image(
image: widget.imageProvider,
fit: widget.fit,
),
onTap: () {
int index = int.parse(_tabController.index.toString());
switch(index){
//Implement you case here
case 0:
case 1:
case 2:
default:
print(_tabController.index.toString());
}
},
),
);
}
}
void main(){
runApp(new MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: new Text("Demo"),
),
body: new ImageCarousel(
<ImageProvider>[
new NetworkImage(
'http://wallpaper-gallery.net/images/images/images-2.jpg'),
new NetworkImage(
'http://wallpaper-gallery.net/images/images/images-10.jpg'),
new NetworkImage(
'http://wallpaper-gallery.net/images/images/images-4.jpg'),
],
interval: new Duration(seconds: 5),
)
),
));
}
Hope it helps..!!

Resources