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.
Related
Okay. So I'm going to show some code, and I honestly don't know WHY it doesn't work. I just feel like I'm out of my depth, and this is very frustrating.
Now this is NOT the program I'm actually working on, but a super-simple example program that should show the issue I'm having. Please do NOT ask me to put all of these things into or inside a single function or class, as that is NOT an option with my real program, so it wouldn't solve my actual issue.
so in my main.dart I have the following.
import 'package:flutter/cupertino.dart';
import 'dart:async';
import './page2.dart';
void main() => runApp(MyApp());
Page2 myPage = new Page2();
PageState myState = myPage.createState();
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return CupertinoApp(
title: 'Splash Test',
theme: CupertinoThemeData(
primaryColor: Color.fromARGB(255, 0, 0, 255),
),
home: MyHomePage(title: 'Splash Test Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool textBool = false;
void changeTest(dynamic function, context) async {
Timer.periodic(Duration (seconds: 2), (Timer t) {
myState.changeText();
counter++;
if (counter >= 10) {
t.cancel();
}
},);
Navigator.push(context, CupertinoPageRoute(builder: (context) => myPage));
}
#override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
child: Center(
child: CupertinoButton(
child: Text('To Splash'),
onPressed: () => changeTest(myState.changeText, context),
),
),
);
}
}
and in a second Dart file I have
import 'package:flutter/material.dart';
import 'package:flutter/semantics.dart';
import './main.dart';
class Page2 extends StatefulWidget {
#override
State<StatefulWidget> createState() => new PageState();
}
class PageState extends State<Page2> {
bool textChanger = false;
bool firstText = true;
Text myText() {
if (textChanger) {
Text text1 = new Text('Text One',
style: TextStyle(color: Color.fromARGB(255, 0, 0, 0)));
return text1;
} else {
Text text1 = new Text('Text Two',
style: TextStyle(color: Color.fromARGB(255, 0, 0, 0)));
return text1;
}
}
void changeText() {
if (!firstText) {
if (textChanger) {
print('Change One');
textChanger = false;
setState(() {
});
} else {
print('Change Two');
textChanger = true;
setState(() {
});
}
} else {
firstText = false;
}
}
#override
Widget build(BuildContext context) {
return new Scaffold(
body: new Container(
child: Center(
child: myText()
)
),);
}
}
Now what this program does is switch to the second page, and then stalls, and nothing happens. The timer IS getting called (I can see this through the print-screen function) And I can see that the text SHOULD be changing, as the bools are being altered properly to do so.
Expected functionality: I should be able to call the instance of the second page, and the functions on it, from my main app, and make changes to the text on that second page.
In my real app (Far more complicated, I couldn't possibly parse it down into something that would fit here) I have the same issue. (If I use the hot reload in Flutter the text DOES change in my actual app.)
So as you can see, I'm trying to communicate cross-classes and cross-functions, but either A) I'm not communicating correctly, or B) The communication is with an incorrect instance of the secondary page, and so the setState() call isn't being done on the variant that's being shown? Those are my only guesses.
You shouldn't call the createState manually. For implementing such a thing I prefer to use a stream instead, which is pretty much easy to handle.
timerStream.dart
import 'dart:async';
class TimerStream {
StreamController _streamController;
StreamSink<bool> get timerSink =>
_streamController.sink;
Stream<bool> get timerStream =>
_streamController.stream;
TimerStream() {
_streamController = StreamController<bool>();
}
dispose() {
_streamController?.close();
}
}
main.dart
import 'dart:async';
import 'package:flutter/cupertino.dart';
import './page2.dart';
import './timerStream.dart';
void main() => runApp(MyApp());
TimerStream stream = TimerStream();
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return CupertinoApp(
title: 'Splash Test',
theme: CupertinoThemeData(
primaryColor: Color.fromARGB(255, 0, 0, 255),
),
home: MyHomePage(title: 'Splash Test Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool textBool = false;
void changeTest(context) async {
Navigator.push(context, CupertinoPageRoute(builder: (context) => Page2(stream: stream,)));
Timer.periodic(Duration (seconds: 5), (Timer t) {
stream.timerSink.add(true);
});
}
#override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
child: Center(
child: CupertinoButton(
child: Text('To Splash'),
onPressed: () => changeTest(context),
),
),
);
}
}
page2.dart
import 'package:flutter/material.dart';
import 'timerStream.dart';
class Page2 extends StatefulWidget {
TimerStream stream;
Page2({this.stream});
#override
State<StatefulWidget> createState() => new PageState();
}
class PageState extends State<Page2> {
bool textChanger = false;
bool firstText = true;
Text myText() {
if (textChanger) {
Text text1 = new Text('Text One',
style: TextStyle(color: Color.fromARGB(255, 0, 0, 0)));
return text1;
} else {
Text text1 = new Text('Text Two',
style: TextStyle(color: Color.fromARGB(255, 0, 0, 0)));
return text1;
}
}
void changeText() {
if (!firstText) {
if (textChanger) {
print('Change One');
setState(() {
textChanger = false;
});
} else {
print('Change Two');
setState(() {
textChanger = true;
});
}
} else {
firstText = false;
}
}
#override
Widget build(BuildContext context) {
return new Scaffold(
body: new Container(
child: Center(
child: myText()
)
),);
}
#override
void initState() {
super.initState();
widget.stream.timerStream.listen((onData) {
changeText();
});
}
}
Note: If you want, instead of writing true to the stream you can toggle the value and use that in your page2 to change the text.
I'm trying to dynamically delete simple grid item on long press;
I've tried the most obvious way: created a list of grid data, and called setState on addition or deletion of the item.
UPD: Items works properly in the list, since it's initialisation loop moved to initState() method (just as #jnblanchard said in his comment), and don't generate new items at every build() call, but deletion is still doesn't work.
If it has more items, than can fit the screen, it deletes last row, (when enough items deleted), otherwise the following exception is thrown:
I/flutter (28074): The following assertion was thrown during performLayout():
I/flutter (28074): SliverGeometry is not valid: The "maxPaintExtent" is less than the "paintExtent".
I/flutter (28074): The maxPaintExtent is 540.0, but the paintExtent is 599.3. By definition, a sliver can't paint more
I/flutter (28074): than the maximum that it can paint!
My test code now:
main class
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:options_x_ray_informer/prototyping/TestTile.dart';
class Prototype extends StatefulWidget{
#override
_PrototypeState createState() => _PrototypeState();
}
class _PrototypeState extends State<Prototype> {
//list of grid data
List<Widget> gridItemsList = [];
#override
void initState(){
super.initState();
//----filling the list----
for(int i =0; i<10; i++){
gridItemsList.add(
TestTile(i, (){
//adding callback for long tap
delete(i);
})
);
}
}
#override
Widget build(BuildContext context) {
//----building the app----
return Scaffold(
appBar: AppBar(
title: Text("Prototype"),
actions: <Widget>[
IconButton(
icon: Icon(Icons.add),
onPressed: () {
int index = gridItemsList.length+1;
add(
new TestTile(index, (){
delete(index);
})
);
},
),
]
),
body: GridView(
gridDelegate: new SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 2),
children: gridItemsList
)
);
}
///method for adding the items
void add(Widget toAdd){
setState(() {
TestTile tile = toAdd as TestTile;
gridItemsList.add(toAdd);
print("tile number#${tile.index} added");
});
}
///method for deleting the items
void delete(int index){
setState(() {
gridItemsList.removeAt(index);
print("tile number#$index is deleted");
});
}
}
and separate widget class for grid items
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
class TestTile extends StatelessWidget{
int _index;
var _callback;
TestTile(this._index, this._callback);
get index => _index;
#override
Widget build(BuildContext context) {
return GridTile(
child: Card(
child: InkResponse(
onLongPress: _callback,
child: Center(
child:Text("data#$_index")
)
)
),
);
}
}
How can I delete an item from grid view?
p.s. the provided code is just my attempts of solving the problem - you can offer another way, if you want!
I wrote this up from the example app, it has a few things that you may find useful. Notably I abstract the list data-structure by holding the length of the list inside a stateful widget. I wrote this with a ListView but I think you could change that to a GridView without any hiccups.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.indigo,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
#override
Widget build(BuildContext context) {
return Scaffold(appBar: AppBar(
title: Text("Owl"),
actions: <Widget>[IconButton(icon: Icon(Icons.remove), onPressed: () => this.setState(() => _counter > 1 ? _counter-- : _counter = 0)), IconButton(icon: Icon(Icons.add), onPressed: () => this.setState(() => _counter++))],
),
body: ListView.builder(itemExtent: 50, itemCount: _counter, itemBuilder: (context, index) => Text(index.toString(), textAlign: TextAlign.center, style: Theme.of(context).textTheme.title))
);
}
}
Finally I've got what I wanted.
I'll leave it here for someone who might have the same problem :)
Main class:
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:options_x_ray_informer/prototyping/TestTile.dart';
class Prototype extends StatefulWidget{
#override
_PrototypeState createState() => _PrototypeState();
}
class _PrototypeState extends State<Prototype> {
//list of some data
List<Person> partyInviteList = [];
_PrototypeState(){
//filling the list
for(int i=0; i<5; i++){
partyInviteList.add(Person.generateRandomPerson());
}
print("Person ${partyInviteList.toString()}");
}
#override
Widget build(BuildContext context) {
//----building the app----
return Scaffold(
appBar: AppBar(
title: Text("Prototype"),
actions: <Widget>[
IconButton(
icon: Icon(Icons.add),
//generating an item on tap
onPressed: () {
setState(() {
partyInviteList.add(Person.generateRandomPerson());
});
},
),
]
),
body: GridView.count(
crossAxisCount: 2,
children: List.generate(partyInviteList.length, (index) {
//generating tiles with people from list
return TestTile(
partyInviteList[index], (){
setState(() {
print("person ${partyInviteList[index]} is deleted");
partyInviteList.remove(partyInviteList[index]);
});
}
);
})
)
);
}
}
///person class
class Person{
Person(this.firstName, this.lastName);
static List<String> _aviableNames = ["Bob", "Alise", "Sasha"];
static List<String> _aviableLastNames = ["Green", "Simpson", "Stain"];
String firstName;
String lastName;
///method that returns random person
static Person generateRandomPerson(){
Random rand = new Random();
String randomFirstName = _aviableNames[rand.nextInt(3)];
String randomLastName = _aviableLastNames[rand.nextInt(3)];
return Person(randomFirstName, randomLastName);
}
#override
String toString() {
return "$firstName $lastName";
}
}
Support class:
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:options_x_ray_informer/prototyping/Prototype.dart';
class TestTile extends StatelessWidget{
final Person person;
var _callback;
TestTile(this.person, this._callback);
#override
Widget build(BuildContext context) {
return GridTile(
child: Card(
child: InkResponse(
onLongPress: _callback,
child: Center(
child:Text("${person.toString()}")
)
)
),
);
}
}
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();
},
);
}
}
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
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..!!