The getter '_controller' was called on null. Flutter - flutter

I'm using this package in Flutter: https://pub.dev/packages/circular_countdown_timer
However, I need to create a separate widget to separate my code a little bit. I created something like this:
import 'package:circular_countdown_timer/circular_countdown_timer.dart';
import 'package:flutter/material.dart';
class CustomOtpCard extends StatefulWidget {
final String duration;
final String accountName;
final String otp;
final Function(CountDownController) onComplete;
CustomOtpCard({
#required this.duration,
#required this.accountName,
#required this.otp,
#required this.onComplete,
});
#override
_CustomOtpCardState createState() => _CustomOtpCardState();
}
class _CustomOtpCardState extends State<CustomOtpCard> {
CountDownController _controller = CountDownController();
#override
Widget build(BuildContext context) {
return Card(
child: ListTile(
leading: CircularCountDownTimer(
duration: int.parse(widget.duration),
onComplete: widget.onComplete(_controller),
initialDuration: 0,
ringColor: Colors.grey[300],
fillColor: Colors.purple,
backgroundColor: Colors.purple[500],
width: 50.0,
height: 50.0,
),
title: Text(widget.accountName),
subtitle: Text(widget.otp),
),
);
}
}
Now, I use it like this:
return ListView.builder(
itemCount: _totpItems.length,
itemBuilder: (context, index) {
return CustomOtpCard(
duration: '30',
onComplete: (controller) {
BlocProvider.of<TotpGeneratorBloc>(context)
.add(GetTotpItemsEvent());
controller.restart();
},
accountName: _totpItems[index].accountName,
otp: _totpItems[index].otp,
);
},
);
Then the error shows up "The getter '_controller' was called on null.". Here's the image of my error:
It says, that the _controller I passed to the callback is null. What am I doing wrong? Can someone give me a hint?
Thanks!

I presume that when you call controller.restart(), _CustomOtpCardState has been disposed then _controller do not exist anymore.
You could do it this way :
First turn onComplete to a VoidCallback (equivalent to void Function()) :
final VoidCallback onComplete;
Then use it this way :
...
duration: int.parse(widget.duration),
onComplete: () {
widget.onComplete();
_controller.restart();
},
initialDuration: 0,
...
In your ListView.builder :
return CustomOtpCard(
duration: '30',
onComplete: () {
BlocProvider.of<TotpGeneratorBloc>(context)
.add(GetTotpItemsEvent());
},
accountName: _totpItems[index].accountName,
otp: _totpItems[index].otp,
);

Related

How to pass an object as an argument which may be null at the time of build?

I have the following Pop up which is essentially an AlertDialog with some data entry fields and two buttons. The widget tree is given below:
ProductPurchaseForm
class ProductPurchaseForm extends StatelessWidget {
ProductPurchaseForm({Key? key}) : super(key: key);
final _formGlobalKey = GlobalKey<FormBuilderState>();
#override
Widget build(BuildContext context) {
return AlertDialog(
scrollable: true,
content: FormBuilder(
key: _formGlobalKey,
child: Column(
children: [
const DateField(
name: 'date',
),
const TextInputField(
name: 'amount',
hintText: 'Enter the Amount',
),
FormFooter(
formKey: _formGlobalKey,
transaction: TransactionModel.productPurchase(
tDate: _formGlobalKey.currentState!.value['date'],
amount: _formGlobalKey.currentState!.value['amount'],
),
),
],
),
),
);
}
}
FormFooter
class FormFooter extends ConsumerWidget {
final TransactionModel transaction;
final GlobalKey<FormBuilderState> formKey;
const FormFooter({
Key? key,
required this.formKey,
required this.transaction,
}) : super(key: key);
#override
Widget build(BuildContext context, WidgetRef ref) {
final _databaseService = ref.watch(databaseServiceRiverpod);
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
CustomButton(
buttontext: 'Cancel',
buttonWidth: 300,
onTapFunc: () {
formKey.currentState!.reset();
VRouter.of(context).pop();
},
),
CustomButton(
buttontext: 'Add Transaction',
buttonWidth: 300,
onTapFunc: () => addTransactionFunction(
context: context,
transaction: transaction,
databaseService: _databaseService,
formKey: formKey,
),
),
],
);
}
}
addTransactionFunction
addTransactionFunction({
required BuildContext context,
required TransactionModel transaction,
required GlobalKey<FormBuilderState> formKey,
required DatabaseService databaseService,
}) async {
formKey.currentState!.save();
if (formKey.currentState!.validate()) {
final result = await databaseService.addTransaction(transaction);
formKey.currentState!.reset();
if (result.isError) {
debugPrint(result.message);
buildSnackBar(
context: context,
snackBarContent: 'ERROR: Please try again!',
);
} else {
VRouter.of(context).pop();
buildSnackBar(
context: context,
snackBarContent: 'SUCCESS: Transaction Added',
);
}
} else {
buildSnackBar(
context: context,
snackBarContent: 'Invalid Values',
);
}
}
As you can see, I am using a 3rd party package called flutter_form_builder and using a GlobalKey to get values from the FormBuilder. As I am passing the TransactionModel to the FormFooter, I use _formGlobalKey.currentState which does not have any value (or is null) at the time when the widget is first built. This is why I am getting the red screen error of Unexpected Null Value.
I know why this is happening and, technically, the TransactionModel.productPurchase is only "used" when the CustomButton is pressed and that is when I am sure that the values will NOT be null. But because of the way this widget tree is set up, the TransactionModel is referenced when the FormFooter is built rather than when it is clicked.
Is there a better way to set up my widget tree so that I can work around this null error?

Implement setstat and bind variables

I'm trying to get and display 2 variable values from another dart file, ("int myId" and "String myMenu") , these variables are updated with every "onTap" widget, my code works, but only if i do a "hot reload", i think that i need to put a "setstate" somewhere, but i'm having difficulty to implement it.
I think the problem is there, my widget text returns "null" to me, but if I hit the menu button and do a "hot reload", it's ok.
displayText.dart
import 'package:flutter/material.dart';
import './menu.dart';
class display extends StatefulWidget {
int myId;
String myMenu;
display(this.myId, this.myMenu);
#override
_displayState createState() => _displayState();
}
class _displayState extends State<display> {
Future myVarUsed() async {
//Each press on the button return the value
setState(() {
print('myIdDsiplay: ${widget.myId}'); // null
print('myMenuDisplay : ${widget.myMenu}'); // null
});
}
#override
void initState() {
super.initState();
myVarUsed();
}
#override
Widget build(BuildContext context) {
return Container(
color: Colors.blue,
height: 250,
width: 250,
child: Row(
children: [
Text('My ID is : ${widget.myId}'),
Text('My menu is : ${widget.myMenu}'),
],
),
);
}
}
This file contains the menu inside a scrollbar, each button return the ID and the name (of the button) and store it in 2 variable ("int myId" and "String myMenu") that i want to pass.
menu.dart
import 'package:flutter/material.dart';
import './mylist.dart';
import './displayText.dart';
class Menu extends StatefulWidget {
static int myId;
static String myMenu;
#override
_MenuState createState() => _MenuState();
}
class _MenuState extends State<Menu> {
Container scrollList() {
final PageController controller = PageController(initialPage: 1, keepPage: true, viewportFraction: 0.35);
return Container(
color: Colors.red,
height: 90,
child: PageView.builder(
scrollDirection: Axis.horizontal,
controller: controller,
itemCount: listdata.length,
physics: BouncingScrollPhysics(),
itemBuilder: (BuildContext context, int index) {
return Container(
child: gestureDetector_Ontap(index),
);
},
),
);
}
GestureDetector gestureDetector_Ontap(int index) {
return GestureDetector(
onTap: () {
Menu.myId = listdata[index].id;
Menu.myMenu = listdata[index].menuObj;
display(Menu.myId, Menu.myMenu);
print('myIDMenu ${Menu.myId}');
print('myMenuMenu ${Menu.myMenu}');
},
child: Container(
alignment: AlignmentDirectional.center,
child: Text(
'${listdata[index].menuObj}',
),
),
);
}
Widget build(BuildContext context) {
return Container(
child: scrollList(),
);
}
}
This file contains my list and his class
mylist.dart
class listModel {
int id;
String menuObj;
listModel(this.id, this.menuObj);
}
List listdata = [
listModel(0, 'Menu01'),
listModel(1, 'Menu02'),
listModel(2, 'Menu03'),
listModel(3, 'Menu04'),
listModel(4, 'Menu05')
];
And the container
main.dart
import 'package:flutter/material.dart';
import './menu.dart';
import './displayText.dart';
import './mylist.dart';
void main() {
runApp(MyHomePage());
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Container(
child: Column(
children: <Widget>[
Menu(),
display(Menu.myId, Menu.myMenu),
],
),
),
),
);
}
}
The problem
You're defining Menu this way:
class Menu extends StatefulWidget {
static int myId;
static String myMenu;
#override
_MenuState createState() => _MenuState();
}
When your app starts, myId and myMenu are uninitialized variables, therefore they're implicitely set to null.
Inside _MyHomePageState, you call
display(Menu.myId, Menu.myMenu)
Since you haven't initialized Menu.myId and Menu.myMenu yet, they're still null.
When you tap the GestureDetector, you initialize Menu.myId and Menu.myMenu this way:
Menu.myId = listdata[index].id;
Menu.myMenu = listdata[index].menuObj;
display(Menu.myId, Menu.myMenu);
print('myIDMenu ${Menu.myId}');
print('myMenuMenu ${Menu.myMenu}');
Now, Menu.myId and Menu.myMenu are defined to non-null values. However, this will not update the Container's display(Menu.myId, Menu.myMenu), so they'll still be null, you need to update it by yourself.
The solution
I've added comments through the code, pointing a better approach:
import 'package:flutter/material.dart';
// Avoid displaying the warning "Name types using UpperCamelCase."
class Display extends StatefulWidget {
// Make these fields final and the constructor const
final int myId;
final String myMenu;
const Display(this.myId, this.myMenu);
#override
_DisplayState createState() => _DisplayState();
}
// Avoid displaying the warning "Name types using UpperCamelCase."
class _DisplayState extends State<Display> {
// You don't need this Future nor this initState
//
// Future myVarUsed() async {
// setState(() {
// print('myIdDsiplay: ${widget.myId}'); // null
// print('myMenuDisplay : ${widget.myMenu}'); // null
// });
// }
//
// #override
// void initState() {
// super.initState();
// myVarUsed();
// }
#override
Widget build(BuildContext context) {
return Container(
color: Colors.blue,
height: 250,
width: 250,
child: Row(
children: [
Text('My ID is : ${widget.myId}'),
Text('My menu is : ${widget.myMenu}'),
],
),
);
}
}
class Menu extends StatefulWidget {
// Avoid using mutable static fields
// static int myId;
// static String myMenu;
// To simplify, you can add a onChanged callback to
// be triggered whenever you change `myId` and `myMenu`
final void Function(int myId, String myMenu) onChanged;
const Menu({this.onChanged});
#override
_MenuState createState() => _MenuState();
}
class _MenuState extends State<Menu> {
Container scrollList() {
final PageController controller = PageController(initialPage: 1, keepPage: true, viewportFraction: 0.35);
return Container(
color: Colors.red,
height: 90,
child: PageView.builder(
scrollDirection: Axis.horizontal,
controller: controller,
itemCount: listdata.length,
physics: BouncingScrollPhysics(),
itemBuilder: (BuildContext context, int index) {
return Container(
child: gestureDetectorOntap(index),
);
},
),
);
}
// Avoid displaying the warning "Name non-constant identifiers using lowerCamelCase."
GestureDetector gestureDetectorOntap(int index) {
return GestureDetector(
onTap: () {
// Make these local variables
int myId = listdata[index].id;
String myMenu = listdata[index].menuObj;
// Call the `onChanged` callback
widget.onChanged(myId, myMenu);
// This widget is being thrown away
// display(Menu.myId, Menu.myMenu);
print('myIDMenu $myId');
print('myMenuMenu $myMenu');
},
child: Container(
alignment: AlignmentDirectional.center,
child: Text(
'${listdata[index].menuObj}',
),
),
);
}
Widget build(BuildContext context) {
return Container(
child: scrollList(),
);
}
}
// Avoid the warning "Name types using UpperCamelCase."
class ListModel {
// You can make these fields final and the constructor const
final int id;
final String menuObj;
const ListModel(this.id, this.menuObj);
}
// You can make this list const to avoid modifying it unintentionally later
const List<ListModel> listdata = [
ListModel(0, 'Menu01'),
ListModel(1, 'Menu02'),
ListModel(2, 'Menu03'),
ListModel(3, 'Menu04'),
ListModel(4, 'Menu05')
];
void main() {
runApp(MyHomePage());
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
// Create fields to store the current `myId` and current `myMenu`
int myId;
String myMenu;
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Container(
child: Column(
children: <Widget>[
// Add the `onChanged` callback here, updating this widget state
Menu(
onChanged: (newMyId, newMyMenu) {
setState(() {
myId = newMyId;
myMenu = newMyMenu;
});
}
),
// Access the current values here
Display(myId, myMenu),
],
),
),
),
);
}
}

Flutter - select only single item in list view

In my app I am generating a ListView and items can be highlighted by tapping on them. That works fine and I also have a callback function that gives me the key for the just selected item. I can currently manually deselect the item by tapping on it again, but will ultimately take that functionality out.
My problem is that I want one and only one item to be selected at a time. In order to create the list I currently take some initial content in the form of a list, generate the tiles and add them to another list. I then use that list to create the ListView. My plan was on the callback from a new selection, run through the list of tiles and deselect them before highlighting the new chosen tile and carrying out the other functions. I have tried various methods to tell each tile to deselect itself but have not found any way to address each of the tiles. Currently I get the error:
Class 'OutlineTile' has no instance method 'deselect'.
Receiver: Instance of 'OutlineTile'
Tried calling: deselect()
I have tried to access a method within the tile class and to use a setter but neither worked so far. I am quite new to flutter so it could be something simple I am missing. My previous experience was with Actionscript where this system would have worked fine and I could access a method of an object (in this case the tile) easily as long s it is a public method.
I'd be happy to have another way to unselect the old item or to find a way to access a method within the tile. The challenge is to make the tiles show not highlighted without them being tapped themselves but when a different tile is tapped.
The code in my parent class is as follows:
class WorkingDraft extends StatefulWidget {
final String startType;
final String name;
final String currentContent;
final String currentID;
final List startContent;
WorkingDraft(
{this.startType,
this.name,
this.currentContent,
this.currentID,
this.startContent});
#override
_WorkingDraftState createState() => _WorkingDraftState();
}
class _WorkingDraftState extends State<WorkingDraft> {
final _formKey = GlobalKey<FormState>();
final myController = TextEditingController();
//String _startType;
String _currentContent = "";
String _name = "Draft";
List _startContent = [];
List _outLineTiles = [];
int _counter = 0;
#override
void dispose() {
// Clean up the controller when the widget is disposed.
myController.dispose();
super.dispose();
}
void initState() {
super.initState();
_currentContent = widget.currentContent;
_name = widget.name;
_startContent = widget.startContent;
_counter = 0;
_startContent.forEach((element) {
_outLineTiles.add(OutlineTile(
key: Key("myKey$_counter"),
outlineName: element[0],
myContent: element[1],
onTileSelected: clearHilights,
));
_counter++;
});
}
dynamic clearHilights(Key myKey) {
_outLineTiles.forEach((element) {
element.deselect(); // this throws an error Class 'OutlineTile' has no instance method 'deselect'.
Key _foundKey = element.key;
print("Element Key $_foundKey");
});
}
.......
and further down within the widget build scaffold:
child: ListView.builder(
itemCount: _startContent.length,
itemBuilder: (context, index) {
return _outLineTiles[index];
},
),
Then the tile class is as follows:
class OutlineTile extends StatefulWidget {
final Key key;
final String outlineName;
final Icon myIcon;
final String myContent;
final Function(Key) onTileSelected;
OutlineTile(
{this.key,
this.outlineName,
this.myIcon,
this.myContent,
this.onTileSelected});
#override
_OutlineTileState createState() => _OutlineTileState();
}
class _OutlineTileState extends State<OutlineTile> {
Color color;
Key _myKey;
#override
void initState() {
super.initState();
color = Colors.transparent;
}
bool _isSelected = false;
set isSelected(bool value) {
_isSelected = value;
print("set is selected to $_isSelected");
}
void changeSelection() {
setState(() {
_myKey = widget.key;
_isSelected = !_isSelected;
if (_isSelected) {
color = Colors.lightBlueAccent;
} else {
color = Colors.transparent;
}
});
}
void deselect() {
setState(() {
isSelected = false;
color = Colors.transparent;
});
}
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Row(
children: [
Card(
elevation: 10,
margin: EdgeInsets.fromLTRB(10.0, 6.0, 5.0, 0.0),
child: SizedBox(
width: 180,
child: Container(
color: color,
child: ListTile(
title: Text(widget.outlineName),
onTap: () {
if (widget.outlineName == "Heading") {
Text("Called Heading");
} else (widget.outlineName == "Paragraph") {
Text("Called Paragraph");
widget.onTileSelected(_myKey);
changeSelection();
},
),
........
Thanks for any help.
Amended Code sample and explanation, that builds to a complete project, from here:
Following the advice from phimath I have created a full buildable sample of the relevant part of my project.
The problem is that the tiles in my listview are more complex with several elements, many of which are buttons in their own right so whilst phimath's solution works for simple text tiles I have not been able to get it working inside my own project. My approach is trying to fundamentally do the same thing as phimath's but when I include these more complex tiles it fails to work.
This sample project is made up of three files. main.dart which simply calls the project and passes in some dummy data in the way my main project does. working_draft.dart which is the core of this issue. And outline_tile.dart which is the object that forms the tiles.
Within working draft I have a function that returns an updated list of the tiles which should show which tile is selected (and later any other changes from the other buttons). This gets called when first going to the screen. When the tile is tapped it uses a callback function to redraw the working_draft class but this seems to not redraw the list as I would expect it to. Any further guidance would be much appreciated.
The classes are:
first class is main.dart:
import 'package:flutter/material.dart';
import 'package:listexp/working_draft.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
home: WorkingDraft(
startType: "Basic",
name: "Draft",
currentID: "anID",
startContent: [
["Heading", "New Heading"],
["Paragraph", "New Text"],
["Image", "placeholder"],
["Signature", "placeholder"]
],
));
}
}
Next file is working_draft.dart:
import 'package:flutter/material.dart';
import 'package:listexp/outline_tile.dart';
class WorkingDraft extends StatefulWidget {
final String startType;
final String name;
final String currentContent;
final String currentID;
final List startContent;
final int selectedIndex;
WorkingDraft(
{this.startType,
this.name,
this.currentContent,
this.currentID,
this.startContent,
this.selectedIndex});
#override
_WorkingDraftState createState() => _WorkingDraftState();
}
class _WorkingDraftState extends State<WorkingDraft> {
int selectedIndex;
String _currentContent = "";
String _name = "Draft";
List _startContent = [];
var _outLineTiles = [];
int _counter = 0;
int _selectedIndex;
bool _isSelected;
dynamic clearHilights(int currentIndex) {
setState(() {
_selectedIndex = currentIndex;
});
}
updatedTiles() {
if (_selectedIndex == null) {
_selectedIndex = 0;
}
_currentContent = widget.currentContent;
_name = widget.name;
_startContent = widget.startContent;
_counter = 0;
_outLineTiles = [];
_startContent.forEach((element) {
_isSelected = _selectedIndex == _counter ? true : false;
_outLineTiles.add(OutlineTile(
key: Key("myKey$_counter"),
outlineName: element[0],
myContent: element[1],
myIndex: _counter,
onTileSelected: clearHilights,
isSelected: _isSelected,
));
_counter++;
});
}
#override
Widget build(BuildContext context) {
updatedTiles();
return Scaffold(
body: Center(
child: Column(children: [
SizedBox(height: 100),
Text("Outline", style: new TextStyle(fontSize: 15)),
Container(
height: 215,
width: 300,
decoration: BoxDecoration(
border: Border.all(
color: Colors.lightGreenAccent,
width: 2,
),
borderRadius: BorderRadius.circular(2),
),
child: ListView.builder(
itemCount: _startContent.length,
itemBuilder: (context, index) {
return _outLineTiles[index];
},
),
),
]),
));
}
}
and finally is outline_tile.dart
import 'package:flutter/material.dart';
class OutlineTile extends StatefulWidget {
final Key key;
final String outlineName;
final Icon myIcon;
final String myContent;
final int myIndex;
final Function(int) onTileSelected;
final bool isSelected;
OutlineTile(
{this.key,
this.outlineName,
this.myIcon,
this.myContent,
this.myIndex,
this.onTileSelected,
this.isSelected});
#override
_OutlineTileState createState() => _OutlineTileState();
}
class _OutlineTileState extends State<OutlineTile> {
Color color;
// Key _myKey;
bool _isSelected;
#override
void initState() {
super.initState();
_isSelected = widget.isSelected;
if (_isSelected == true) {
color = Colors.lightBlueAccent;
} else {
color = Colors.transparent;
}
}
void deselect() {
setState(() {
_isSelected = widget.isSelected;
if (_isSelected == true) {
color = Colors.lightBlueAccent;
} else {
color = Colors.transparent;
}
});
}
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Row(
children: [
Card(
elevation: 10,
margin: EdgeInsets.fromLTRB(10.0, 6.0, 5.0, 0.0),
child: SizedBox(
width: 180,
child: Container(
color: color,
child: ListTile(
title: Text(widget.outlineName),
onTap: () {
if (widget.outlineName == "Heading") {
Text("Called Heading");
} else if (widget.outlineName == "Paragraph") {
Text("Called Paragraph");
} else if (widget.outlineName == "Signature") {
Text("Called Signature");
} else {
Text("Called Image");
}
var _myIndex = widget.myIndex;
widget.onTileSelected(_myIndex);
deselect();
},
),
),
),
),
SizedBox(
height: 60,
child: Column(
children: [
SizedBox(
height: 20,
child: IconButton(
iconSize: 30,
icon: Icon(Icons.arrow_drop_up),
onPressed: () {
print("Move Up");
}),
),
SizedBox(height: 5),
SizedBox(
height: 20,
child: IconButton(
iconSize: 30,
icon: Icon(Icons.arrow_drop_down),
onPressed: () {
print("Move Down");
}),
),
],
),
),
SizedBox(
height: 60,
child: Column(
children: [
SizedBox(
height: 20,
child: IconButton(
iconSize: 20,
icon: Icon(Icons.add_box),
onPressed: () {
print("Add another");
}),
),
SizedBox(
height: 10,
),
SizedBox(
height: 20,
child: IconButton(
iconSize: 20,
icon: Icon(Icons.delete),
onPressed: () {
print("Delete");
}),
),
],
),
),
],
),
);
}
}
Thanks again
Instead of manually deselecting tiles, just keep track of which tile is currently selected.
I've made a simple example for you. When we click a tile, we just set the selected index to the index we clicked, and each tile looks at that to see if its the currently selected tile.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(body: Home()),
);
}
}
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
int selectedIndex;
#override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: 10,
itemBuilder: (context, index) {
return ListTile(
title: Text('Item: $index'),
tileColor: selectedIndex == index ? Colors.blue : null,
onTap: () {
setState(() {
selectedIndex = index;
});
},
);
},
);
}
}

What should I do if the nodes of a multlevel list have content that should be displayed when clicked? (Advanced ExpansionTile)

For a flutter project I needed a tree structure in which when clicking on a node, not only the entries below it are displayed, but - as with the file manager in Windows - also the content: On a smartphone as a new screen and on a tablet as an additional area to the right of the list.
Unfortunately, the standard ExpansionTile does not have this capability.
Since I'm a newcomer to Flutter, I first looked at the source code and tried to understand the most important parts (I'm still at it ;-). Then I made the following changes:
A property 'AlignOpener' now decides whether the open/close icon is displayed on the left or right.
I added the properties 'onTap' and 'onLongPress' as callback. Once one of these properties is assigned in the calling widget, the '_isInAdvancedMode' property is set to true and an IconButton is inserted as leading or trailing.
Clicking this button will open/close the directory tree. A click on the remaining part is forwarded to the calling widget via callback.
Finally, I added a property 'indentListTile' to control the indent of each layer.
If none of the properties are assigned, the AvancedExpansionTile behaves like the standard ExpansionTile.
Since I'm a newbie, there's still a lot of uncertainty as to whether the code is really correct. As far as I can see, it works, but I would be happy if experienced developers among you could check the code and make suggestions for improvements if necessary.
Maybe the code solves a problem that others also had?
Her is the code:
master_list_item.dart
import 'package:meta/meta.dart';
class ItemMaster {
ItemMaster(
{this.id,
#required this.title,
this.subtitle,
this.children = const <ItemMaster>[]});
final int id;
final String title;
final String subtitle;
final List<ItemMaster> children;
}
master_list.dart
import 'master_list_item.dart';
class MasterList {
List<ItemMaster> get items {
return _items;
}
final List<ItemMaster> _items = <ItemMaster>[
ItemMaster(
title: 'Chapter Master List',
id: 1,
children: <ItemMaster>[
ItemMaster(
title: 'Scene A0',
id: 2,
children: <ItemMaster>[
ItemMaster(title: 'Item A0.1', id: 3),
ItemMaster(title: 'Item A0.2', id: 4),
ItemMaster(title: 'Item A0.3', id: 5),
],
),
ItemMaster(title: 'Scene A1', id: 6),
ItemMaster(title: 'Scene A2', id: 7),
],
),
ItemMaster(
title: 'Chapter B',
id: 8,
children: <ItemMaster>[
ItemMaster(title: 'Scene B0', id: 9),
ItemMaster(title: 'Scene B1', id: 10),
],
),
ItemMaster(
title: 'Chapter C',
id: 11,
children: <ItemMaster>[
ItemMaster(title: 'Scene C0', id: 12),
ItemMaster(title: 'Scene C1', id: 13),
ItemMaster(
title: 'Scene C2',
id: 14,
children: <ItemMaster>[
ItemMaster(title: 'Item C2.0', id: 15),
ItemMaster(title: 'Item C2.1', id: 16),
ItemMaster(title: 'Item C2.2', id: 17),
ItemMaster(title: 'Item C2.3', id: 18),
],
),
],
),
];
}
main.dart
import 'package:flutter/material.dart';
import 'advanced_expansion_tile.dart';
import 'master_list_item.dart';
import 'master_list.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo 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> {
List<ItemMaster> items;
#override
void initState() {
// TODO: implement initState
super.initState();
items = MasterList().items;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Container(
child: ListView.builder(
itemCount: items.length,
itemBuilder: (context, int index) => MasterListEntry(items[index]),
)),
),
);
}
}
class MasterListEntry extends StatelessWidget {
const MasterListEntry(this.entry);
final ItemMaster entry;
Widget _buildTiles(ItemMaster root) {
if (root.children.isEmpty)
return ListTile(
title: Text(root.title),
onTap: () => print("onTap listTile"),
);
return AdvancedExpansionTile(
key: PageStorageKey<ItemMaster>(root),
title: Text(root.title),
children: root.children.map(_buildTiles).toList(),
onTap: () => print("onTap AdvancedExpansionTile"),
alignOpener: AlignOpener.Right,
indentListTile: 15.0,
// isInAdvancedMode: true,
);
}
#override
Widget build(BuildContext context) {
return _buildTiles(entry);
}
}
advanced_expansion_tile.dart (based on the source code from the Flutter team)
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/widgets.dart';
import 'package:flutter/material.dart';
const Duration _kExpand = Duration(milliseconds: 200);
enum AlignOpener { Left, Right }
class AdvancedExpansionTile extends StatefulWidget {
const AdvancedExpansionTile({
Key key,
this.leading,
this.trailing,
#required this.title,
this.backgroundColor,
this.onExpansionChanged,
this.onTap,
this.onLongPress,
this.alignOpener,
this.indentListTile,
this.children = const <Widget>[],
this.initiallyExpanded = false,
}) : assert(initiallyExpanded != null),
super(key: key);
/// A widget to display before the title.
///
/// Typically a [CircleAvatar] widget.
final Widget leading;
/// The primary content of the list item.
///
/// Typically a [Text] widget.
final Widget title;
/// Called when the tile expands or collapses.
///
/// When the tile starts expanding, this function is called with the value
/// true. When the tile starts collapsing, this function is called with
/// the value false.
final ValueChanged<bool> onExpansionChanged;
/// The widgets that are displayed when the tile expands.
///
/// Typically [ListTile] widgets.
final List<Widget> children;
/// The color to display behind the sublist when expanded.
final Color backgroundColor;
/// Specifies if the list tile is initially expanded (true) or collapsed (false, the default).
final bool initiallyExpanded;
/// A widget to display instead of a rotating arrow icon.
final Widget trailing;
/// A callback for onTap and onLongPress on the listTile
final GestureTapCallback onTap;
final GestureLongPressCallback onLongPress;
/// The side where the Open/Close-Icon/IconButton will be placed
final AlignOpener alignOpener;
/// indent of listTile (left)
final indentListTile;
#override
_AdvancedExpansionTileState createState() => _AdvancedExpansionTileState();
}
class _AdvancedExpansionTileState extends State<AdvancedExpansionTile>
with SingleTickerProviderStateMixin {
static final Animatable<double> _easeOutTween =
CurveTween(curve: Curves.easeOut);
static final Animatable<double> _easeInTween =
CurveTween(curve: Curves.easeIn);
static final Animatable<double> _halfTween =
Tween<double>(begin: 0.0, end: 0.5);
final ColorTween _borderColorTween = ColorTween();
final ColorTween _headerColorTween = ColorTween();
final ColorTween _iconColorTween = ColorTween();
final ColorTween _backgroundColorTween = ColorTween();
AnimationController _controller;
Animation<double> _iconTurns;
Animation<double> _heightFactor;
Animation<Color> _borderColor;
Animation<Color> _headerColor;
Animation<Color> _iconColor;
Animation<Color> _backgroundColor;
bool _isExpanded = false;
/// If set to true an IconButton will be created. This button will open/close the children
bool _isInAdvancedMode;
AlignOpener _alignOpener;
double _indentListTile;
#override
void initState() {
super.initState();
_controller = AnimationController(duration: _kExpand, vsync: this);
_heightFactor = _controller.drive(_easeInTween);
_iconTurns = _controller.drive(_halfTween.chain(_easeInTween));
_borderColor = _controller.drive(_borderColorTween.chain(_easeOutTween));
_headerColor = _controller.drive(_headerColorTween.chain(_easeInTween));
_iconColor = _controller.drive(_iconColorTween.chain(_easeInTween));
_backgroundColor =
_controller.drive(_backgroundColorTween.chain(_easeOutTween));
_isExpanded =
PageStorage.of(context)?.readState(context) ?? widget.initiallyExpanded;
if (_isExpanded) _controller.value = 1.0;
/// OnTap or onLongPress are handled in the calling widget --> AdvancedExpansionTile is in Advanced Mode
if (widget.onTap != null || widget.onLongPress != null) {
_isInAdvancedMode = true;
} else {
_isInAdvancedMode = false;
}
/// fallback to standard behaviour if aligning isn't set
_alignOpener = widget.alignOpener ?? AlignOpener.Right;
/// if no indent is set the indent will be 0.0
_indentListTile = widget.indentListTile ?? 0.0;
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
void _handleTap() {
setState(() {
_isExpanded = !_isExpanded;
if (_isExpanded) {
_controller.forward();
} else {
_controller.reverse().then<void>((void value) {
if (!mounted) return;
setState(() {
// Rebuild without widget.children.
});
});
}
PageStorage.of(context)?.writeState(context, _isExpanded);
});
if (widget.onExpansionChanged != null)
widget.onExpansionChanged(_isExpanded);
}
Widget _buildChildren(BuildContext context, Widget child) {
final Color borderSideColor = _borderColor.value ?? Colors.transparent;
return Container(
decoration: BoxDecoration(
color: _backgroundColor.value ?? Colors.transparent,
border: Border(
top: BorderSide(color: borderSideColor),
bottom: BorderSide(color: borderSideColor),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ListTileTheme.merge(
iconColor: _iconColor.value,
textColor: _headerColor.value,
child: ListTile(
onTap: () {
_isInAdvancedMode ? widget.onTap() : _handleTap();
}, // in AdvancedMode a callback will handle the gesture inside the calling widget
onLongPress: () {
_isInAdvancedMode ? widget.onLongPress() : _handleTap();
}, // in AdvancedMode a callback will handle the gesture inside the calling widget
leading: getLeading(),
title: widget.title,
trailing: getTrailing(),
),
),
ClipRect(
child: Padding(
padding: EdgeInsets.only(left: _indentListTile), // set the indent
child: Align(
heightFactor: _heightFactor.value,
child: child,
),
)),
],
),
);
}
#override
void didChangeDependencies() {
final ThemeData theme = Theme.of(context);
_borderColorTween..end = theme.dividerColor;
_headerColorTween
..begin = theme.textTheme.subhead.color
..end = theme.accentColor;
_iconColorTween
..begin = theme.unselectedWidgetColor
..end = theme.accentColor;
_backgroundColorTween..end = widget.backgroundColor;
super.didChangeDependencies();
}
#override
Widget build(BuildContext context) {
final bool closed = !_isExpanded && _controller.isDismissed;
return AnimatedBuilder(
animation: _controller.view,
builder: _buildChildren,
child: closed ? null : Column(children: widget.children),
);
}
/// A method to decide what will be shown in the leading part of the lisTile
getLeading() {
if (_alignOpener.toString() == AlignOpener.Left.toString() &&
_isInAdvancedMode == true) {
return buildIcon(); //IconButton will be created
} else if (_alignOpener.toString() == AlignOpener.Left.toString() &&
_isInAdvancedMode == false) {
return widget.leading ??
RotationTransition(
turns: _iconTurns,
child: const Icon(Icons.expand_more),
);
} else {
return widget.leading;
}
}
/// A method to decide what will be shown in the trailing part of the lisTile
getTrailing() {
if (_alignOpener.toString() == AlignOpener.Right.toString() &&
_isInAdvancedMode == true) {
return buildIcon(); //IconButton will be created
} else if (_alignOpener.toString() == AlignOpener.Right.toString() &&
_isInAdvancedMode == false) {
return widget.trailing ??
RotationTransition(
turns: _iconTurns,
child: const Icon(Icons.expand_more),
);
} else {
return widget.leading;
}
}
/// A widget to build the IconButton for the leading or trailing part of the listTile
Widget buildIcon() {
return Container(
child: RotationTransition(
turns: _iconTurns,
child: IconButton(
icon: Icon(Icons.expand_more),
onPressed: () {
_handleTap();
//toDo: open/close is working but not the animation
},
),
));
}
}

How to make widget out of a GestureDetector with a Container child?

I want to make a reusable button with a container in GestureDetector which will execute some function if I tap it and its color will become dark if I hold it. Any help, hint, tip would be very much appreciated.
I tried writing the GestureDetector in the custom widget file but it gives me errors.
When i try to extract widget on the GestureDetector it gives an Reference to an enclosing class method cannot be extracted error.
(the main page)
import 'package:flutter/material.dart';
import 'ReusableTwoLineList.dart';
import 'Text_Content.dart';
const mainTextColour = Color(0xFF212121);
const secondaryTextColour = Color(0xFF757575);
const inactiveBackgroundCardColor = Color(0xFFFFFFFF);
const activeBackgroundCardColor = Color(0xFFE5E5E5);
enum CardState {
active,
inactive,
}
class SettingsPage extends StatefulWidget {
#override
_SettingsPageState createState() => _SettingsPageState();
}
class _SettingsPageState extends State<SettingsPage> {
CardState currentCardState = CardState.inactive;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Settings'),
),
body: ListView(
children: <Widget>[
GestureDetector(
onTapDown: (TapDownDetails details) {
setState(() {
currentCardState = CardState.active;
});
},
onTapCancel: () {
setState(() {
currentCardState = CardState.inactive;
});
},
onTap: () {
setState(() {
currentCardState = CardState.inactive;
//some random function
});
},
child: ReusableTwoLineList(
mainTextColor: mainTextColour,
secondaryTextColor: secondaryTextColour,
backgroundCardColor: currentCardState == CardState.active
? activeBackgroundCardColor
: inactiveBackgroundCardColor,
cardChild: TextContent(
mainLabel: 'First Day',
secondaryLabel: 'This is the first day of the week',
),
),
),
ReusableTwoLineList(
mainTextColor: mainTextColour,
secondaryTextColor: secondaryTextColour,
cardChild: TextContent(
mainLabel: '2nd day',
secondaryLabel: 'This is the end day',
),
),
ReusableTwoLineList(
mainTextColor: mainTextColour,
secondaryTextColor: secondaryTextColour,
),
],
),
);
}
}
ReusableTwoLineList.dart (the custom widget i am trying to make)
class ReusableTwoLineList extends StatelessWidget {
ReusableTwoLineList({
#required this.mainTextColor,
#required this.secondaryTextColor,
this.backgroundCardColor,
this.cardChild,
this.onPressed,
});
final Color mainTextColor, secondaryTextColor, backgroundCardColor;
final Widget cardChild;
final Function onPressed;
#override
Widget build(BuildContext context) {
return Container(
color: backgroundCardColor,
padding: EdgeInsets.symmetric(horizontal: 16),
height: 72,
width: double.infinity,
child: cardChild,
);
}
}
This is what i want but in a custom widget so i can use it over and over.
Normal-https://i.imgur.com/lVUkMFK.png
On Pressed-https://i.imgur.com/szuD4ZN.png
You can use extract method instead of extract widget. Flutter will add everything as it is, and instead of a class you will get a reusable function.