├── README.md ├── example ├── model │ └── Account.dart └── view │ ├── AccountDetail.dart │ ├── AccountList.dart │ └── AccountNodeData.dart ├── lib └── src │ ├── BasicPage.dart │ ├── CustomExpansionTile.dart │ ├── CustomListTile.dart │ ├── Global.dart │ ├── SearchBar.dart │ └── TreeView.dart └── pubspec.yaml /README.md: -------------------------------------------------------------------------------- 1 | # piggy treeview 2 | 3 | A Treeview component for Flutter, featuring expand/collapse all, search, hilite, checkbox. 4 | 5 | ## Getting Started 6 | 7 | How to use? 8 | 1. Import TreeView.dart. 9 | 2. Derive TreeNodeData to publish your own data. 10 | 3. See AccountList.dart for the example detail. 11 | 12 | 13 | -------------------------------------------------------------------------------- /example/model/Account.dart: -------------------------------------------------------------------------------- 1 | class Account{ 2 | String username; 3 | String email; 4 | String phone; 5 | String type; 6 | String id; 7 | String parent_id; 8 | String full_path; 9 | 10 | Account(this.username, this.email, this.phone, this.type, this.id, this.parent_id, this.full_path ); 11 | } -------------------------------------------------------------------------------- /example/view/AccountDetail.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:piggy/common/BasicPage.dart'; 3 | import 'package:piggy/common/Global.dart'; 4 | import 'package:validator/validator.dart'; 5 | import 'AccountNodeData.dart'; 6 | 7 | class AccountDetail extends BasicPage { 8 | AccountNodeData _account; 9 | 10 | AccountNodeData get currentAccount => _account; 11 | 12 | AccountDetail(String title, {AccountNodeData account}) 13 | : super(title, appBar: true, enableFAB: false) { 14 | _account = account; 15 | } 16 | 17 | @override 18 | AccountDetailState createState() => new AccountDetailState(); 19 | } 20 | 21 | class AccountDetailState extends BasicPageState { 22 | final GlobalKey _formKey = new GlobalKey(); 23 | bool _autovalidate = false; 24 | 25 | @override 26 | void initState() { 27 | super.initState(); 28 | } 29 | 30 | @override 31 | Widget buildContent(BuildContext context) { 32 | AccountDetail theWidget = widget as AccountDetail; 33 | 34 | Widget requiredHint = new Container( 35 | padding: const EdgeInsets.only(top: 5.0, bottom: 5.0), 36 | child: new Text('* means the field is required', 37 | style: Theme 38 | .of(context) 39 | .textTheme 40 | .caption 41 | .copyWith(color: Colors.indigo))); 42 | 43 | Widget inputUserName = new TextFormField( 44 | decoration: new InputDecoration( 45 | hintText: "Type in account name", 46 | hintStyle: new TextStyle(color: Colors.black26), 47 | labelText: "Account Name *", 48 | labelStyle: new TextStyle(color: Colors.black), 49 | errorStyle: new TextStyle(color: Colors.red), 50 | ), 51 | initialValue: theWidget.currentAccount.data.username, 52 | onSaved: (String value) { 53 | theWidget.currentAccount.data.username = value; 54 | }, 55 | validator: null, 56 | ); 57 | 58 | Widget inputEmail = new TextFormField( 59 | decoration: new InputDecoration( 60 | hintText: "Email", 61 | hintStyle: new TextStyle(color: Colors.black26), 62 | labelText: "Email", 63 | labelStyle: new TextStyle(color: Colors.black), 64 | errorStyle: new TextStyle(color: Colors.red), 65 | ), 66 | initialValue: theWidget.currentAccount.data.email, 67 | onSaved: (String value) { 68 | theWidget.currentAccount.data.email = value; 69 | }, 70 | validator: null, 71 | ); 72 | 73 | Widget inputPhone = new TextFormField( 74 | decoration: new InputDecoration( 75 | hintText: "Contact number", 76 | hintStyle: new TextStyle(color: Colors.black26), 77 | labelText: "Contact Number *", 78 | labelStyle: new TextStyle(color: Colors.black), 79 | errorStyle: new TextStyle(color: Colors.red), 80 | ), 81 | initialValue: theWidget.currentAccount.data.phone, 82 | onSaved: (String value) { 83 | theWidget.currentAccount.data.phone = value; 84 | }, 85 | validator: null, 86 | ); 87 | 88 | Widget _getDropDownType() { 89 | return new Row( 90 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 91 | children: [ 92 | const Text("Account type"), 93 | new DropdownButton( 94 | value: isNull(theWidget.currentAccount.data.type) 95 | ? "" 96 | : theWidget.currentAccount.data.type, 97 | onChanged: (String newValue) { 98 | setState(() { 99 | theWidget.currentAccount.data.type = newValue; 100 | }); 101 | }, 102 | items: ["master", "slaver", "root"].map((String value) { 103 | return new DropdownMenuItem( 104 | value: value, 105 | child: new Text(value == 'master' || value == "root" 106 | ? "Master" 107 | : 'Slaver'), 108 | ); 109 | }).toList(), 110 | ), 111 | ], 112 | ); 113 | } 114 | 115 | Widget inputType = _getDropDownType(); 116 | 117 | Widget _getDropDownParent() { 118 | return new Row( 119 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 120 | children: [ 121 | const Text("Direct master"), 122 | new DropdownButton( 123 | value: isNull(theWidget.currentAccount.data.parent_id) 124 | ? "" 125 | : theWidget.currentAccount.data.parent_id, 126 | onChanged: (String newValue) { 127 | setState(() { 128 | theWidget.currentAccount.data.parent_id = newValue; 129 | }); 130 | }, 131 | items: [ 132 | isNull(theWidget.currentAccount.data.parent_id) 133 | ? "" 134 | : theWidget.currentAccount.data.parent_id 135 | ].map((String value) { 136 | return new DropdownMenuItem( 137 | value: value, 138 | child: new Text(value), 139 | ); 140 | }).toList(), 141 | ), 142 | ], 143 | ); 144 | } 145 | 146 | Widget inputParent = _getDropDownParent(); 147 | 148 | Widget cmdButtons = new Padding( 149 | padding: new EdgeInsets.symmetric(vertical: Constants.VERTICAL_PADDING), 150 | child: 151 | new Row(mainAxisAlignment: MainAxisAlignment.end, children: [ 152 | new RaisedButton(child: const Text("Save"), onPressed: _onDone), 153 | const SizedBox( 154 | width: 12.0, 155 | ), 156 | new RaisedButton(child: const Text("Cancel"), onPressed: _onCancel) 157 | ]), 158 | ); 159 | 160 | return new Container( 161 | padding: new EdgeInsets.symmetric( 162 | vertical: Constants.VERTICAL_PADDING_FORM, 163 | horizontal: Constants.HORIZONTAL_PADDING_FORM), 164 | child: new Form( 165 | key: _formKey, 166 | autovalidate: _autovalidate, 167 | child: new Scrollbar( 168 | child: new ListView(children: [ 169 | requiredHint, 170 | inputUserName, 171 | inputEmail, 172 | inputPhone, 173 | inputType, 174 | inputParent, 175 | cmdButtons 176 | ])))); 177 | } 178 | 179 | // Event handlers 180 | _onDone() { 181 | Navigator.pop(context); 182 | } 183 | 184 | _onCancel() { 185 | Navigator.pop(context); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /example/view/AccountList.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:piggy/common/BasicPage.dart'; 4 | import 'package:piggy/common/Global.dart'; 5 | import 'package:piggy/common/TreeView.dart'; 6 | import 'package:piggy/model/Account.dart'; 7 | import 'AccountNodeData.dart'; 8 | import 'AccountDetail.dart'; 9 | import 'package:piggy/common/SearchBar.dart'; 10 | 11 | class AccountList extends BasicPage { 12 | AccountList(String title, {Key key}) 13 | : super(title, 14 | key: key, 15 | actions: [ 16 | ActionTypes.delete, 17 | ActionTypes.expandAll, 18 | ActionTypes.collapseAll 19 | ], 20 | enableFAB: true, 21 | appBar: true); 22 | 23 | @override 24 | AccountListState createState() => new AccountListState(); 25 | } 26 | 27 | class AccountListState extends BasicPageState { 28 | List _lstAccount; 29 | 30 | //Callback as highlighting a node 31 | void _onHiliteNode(dynamic node) { 32 | assert(null != node); 33 | 34 | bool isExist = _mapHilitedNodes.containsKey(node.id); 35 | if (isExist) 36 | // flip/flop hilited state 37 | _mapHilitedNodes.remove(node.id); 38 | else if (_mapHilitedNodes.length > 0) { 39 | _mapHilitedNodes.forEach((key, value) { 40 | value.hilited = false; 41 | }); 42 | _treeComponent.broadcast(_mapHilitedNodes); 43 | 44 | _mapHilitedNodes.clear(); 45 | // add it 46 | // node id is actually the full path 47 | _mapHilitedNodes[node.id] = node; 48 | } else 49 | _mapHilitedNodes[node.id] = node; 50 | 51 | // print("On hilte node call back:${_mapHilitedNodes.length}"); 52 | } 53 | 54 | //Callback as selecting a node by ticking the respective checkbox 55 | void _onSelectNode(dynamic node) { 56 | assert(null != node); 57 | setState(() { 58 | bool isExist = _mapSelectNodes.containsKey(node.id); 59 | if (node.isChecked) 60 | // add it 61 | // node id is actually the full path 62 | _mapSelectNodes[node.id] = node; 63 | else if (isExist) _mapSelectNodes.remove(node.id); 64 | 65 | widget.enableDelete = _mapSelectNodes.length > 0 ? true : false; 66 | }); 67 | 68 | print("On select node call back:${_mapSelectNodes.length}"); 69 | } 70 | 71 | void _onCollapseAll() { 72 | _treeComponent.expandIt = false; 73 | _treeComponent.toggle(_treeComponent.expandIt); 74 | } 75 | 76 | void _onExpandAll() { 77 | _treeComponent.expandIt = true; 78 | _treeComponent.toggle(_treeComponent.expandIt); 79 | } 80 | 81 | void _onAddNew() { 82 | Util.alert(context); 83 | } 84 | 85 | void _onDelete() { 86 | Util.alert(context, content: "Delete"); 87 | } 88 | 89 | void _onSearch(String textToSearch) { 90 | setState(() { 91 | widget.isLoading = true; 92 | }); 93 | 94 | Iterable foundNodes = this | textToSearch; 95 | 96 | // Turn off hilited nodes 97 | if (_mapHilitedNodes.length > 0) { 98 | _mapHilitedNodes.forEach((key, value) { 99 | value.hilited = false; 100 | }); 101 | _treeComponent.broadcast(_mapHilitedNodes); 102 | } 103 | // Hilite new found nodes 104 | _mapHilitedNodes.clear(); 105 | foundNodes?.forEach((element) { 106 | _mapHilitedNodes[element.id] = element; 107 | }); 108 | 109 | _treeComponent.broadcast(_mapHilitedNodes); 110 | 111 | if (_mapHilitedNodes.length > 0) _onExpandAll(); 112 | 113 | setState(() { 114 | widget.isLoading = false; 115 | }); 116 | } 117 | 118 | void _onEdit(AccountNodeData node) { 119 | setState(() { 120 | print(node.toString()); 121 | }); 122 | _toggleFAB(); 123 | 124 | Navigator.push( 125 | context, 126 | new MaterialPageRoute( 127 | builder: (context) => new AccountDetail( 128 | Constants.TITLE_ACCOUNT_DETAIL_PAGE, 129 | account: node), 130 | fullscreenDialog: true)); 131 | _toggleFAB(); 132 | } 133 | 134 | void _toggleFAB() { 135 | setState(() => widget.enableFAB = !widget.enableFAB); 136 | } 137 | 138 | void initListItems() { 139 | _lstAccount = [ 140 | new Account( 141 | "Root", "root@gmail.com", "0988877766", "root", "0", "", "\$0"), 142 | new Account("Master1", "master1@gmail.com", "0988877766", "master", "1", 143 | "0", "\$0\$1"), 144 | new Account("Master11", "master11@gmail.com", "0988877766", "master", 145 | "11", "1", "\$0\%1\$11"), 146 | new Account("Master12", "master12@gmail.com", "0988877766", "master", 147 | "12", "1", "\$0\$1\$12"), 148 | new Account("Slaver13", "slaver13@gmail.com", "0988877766", "slaver", 149 | "13", "1", "\$0\$1\$13"), 150 | new Account("Master2", "master2@gmail.com", "0988877766", "master", "2", 151 | "0", "\$0\$2"), 152 | new Account("Slaver21", "slaver21@gmail.com", "0988877766", "slaver", 153 | "21", "2", "\$0\$2\$21"), 154 | new Account("Slaver22", "slaver22@gmail.com", "0988877766", "slaver", 155 | "22", "2", "\$0\$2\$22"), 156 | new Account("Master23", "master23@gmail.com", "0988877766", "master", 157 | "23", "2", "\$0\$2\$23"), 158 | new Account("Master30", "master30@gmail.com", "0988877766", "master", 159 | "30", "23", "\$0\$2\$23\$30"), 160 | ]; 161 | } 162 | 163 | @override 164 | void initState() { 165 | super.initState(); 166 | 167 | initListItems(); 168 | 169 | // Call API to get the list of shops 170 | Future future = _getAccountList(); 171 | 172 | future.then((value) { 173 | _treeComponent = new TreeView.multipleRoots(_lstTreeNode, 174 | header: new Heading( 175 | key: new Key("PiggyHeader"), 176 | searchCallback: _onSearch, 177 | )); 178 | _treeComponent.expandIt = false; 179 | _treeComponent.onSelectNode = _onSelectNode; 180 | _treeComponent.onEditNode = _onEdit; 181 | _treeComponent.onHiliteNode = _onHiliteNode; 182 | }) 183 | ..catchError((error) => print(error)); 184 | 185 | _displayLongPressGuide(); 186 | } 187 | 188 | Future _displayLongPressGuide() async { 189 | showInSnackBar("Press long on an account to edit."); 190 | } 191 | 192 | @override 193 | void didUpdateWidget(covariant AccountList oldWidget) { 194 | super.didUpdateWidget(oldWidget); 195 | if (_onDelete != null && widget.onDelete == null) 196 | widget.onDelete = _onDelete; 197 | if (_onExpandAll != null && widget.onExpandAll == null) 198 | widget.onExpandAll = _onExpandAll; 199 | if (_onCollapseAll != null && widget.onCollapseAll == null) 200 | widget.onCollapseAll = _onCollapseAll; 201 | if (_onAddNew != null && widget.onFABressed == null) 202 | widget.onFABressed = _onAddNew; 203 | if (_lstTreeNode.length > 0) widget.enableExpandCollap = true; 204 | } 205 | 206 | @override 207 | Widget buildContent(BuildContext context) { 208 | return new Container( 209 | child: new Scrollbar(child: _treeComponent), 210 | ); 211 | } 212 | 213 | TreeView _treeComponent; 214 | List _lstTreeNode; 215 | 216 | Map _mapSelectNodes = {}; 217 | Map get selectedNodes => _mapSelectNodes; 218 | 219 | Map _mapHilitedNodes = {}; 220 | Map get hilitedNodes => _mapHilitedNodes; 221 | 222 | _AccountMap _mapAccount; 223 | // API calls 224 | Future _getAccountList() async { 225 | setState(() => widget.isLoading = true); 226 | 227 | _mapAccount = new _AccountMap(_lstAccount); 228 | 229 | _processTreeData(); 230 | 231 | setState(() => widget.isLoading = false); 232 | } 233 | 234 | void _processTreeData() { 235 | // Make sure the list is empty 236 | if (_lstTreeNode != null) { 237 | _lstTreeNode.clear(); 238 | _lstTreeNode = null; 239 | } 240 | _lstTreeNode = []; 241 | 242 | // Get all roots first 243 | _mapAccount.interMap?.forEach((String key, Account account) { 244 | if (account.parent_id == null || account.parent_id.isEmpty) { 245 | // root _lstTreeNode: e.g. "$1" 246 | AccountNodeData root = new AccountNodeData.root(account); 247 | _lstTreeNode.add(root); 248 | } 249 | }); 250 | 251 | void _continueBuildTree(AccountNodeData node) { 252 | List lstAccountNodeDataTemp = []; 253 | 254 | Iterable children = _mapAccount ^ node.id; 255 | 256 | children?.forEach((Account account) { 257 | var theChild = node.createChild( 258 | account.username, account.email, account.full_path, account); 259 | lstAccountNodeDataTemp.add(theChild); 260 | }); 261 | 262 | // Recursively build tree. 263 | lstAccountNodeDataTemp.forEach((node) => _continueBuildTree(node)); 264 | } 265 | 266 | _lstTreeNode.forEach((node) => _continueBuildTree(node)); 267 | } 268 | 269 | // Util 270 | // Search the tree for the text 271 | Iterable operator |(String textToSearch) sync* { 272 | if (_lstTreeNode?.length > 0) { 273 | for (AccountNodeData account in _lstTreeNode) { 274 | String cat = 275 | "${account.data.username} ${account.data.email} ${account.data.type} ${account.data.phone}"; 276 | if (cat.toLowerCase().indexOf(textToSearch.toLowerCase()) > -1) { 277 | account.hilited = true; 278 | yield account; 279 | } 280 | if (account.hasChildren) 281 | for (AccountNodeData acc in account.children) 282 | yield* acc | textToSearch; 283 | } 284 | } else 285 | yield null; 286 | } 287 | } 288 | 289 | class _AccountMap { 290 | Map _mAccount; 291 | 292 | _AccountMap(List lstAccount) { 293 | if (lstAccount?.length > 0) { 294 | _mAccount = new Map.fromIterable(lstAccount, 295 | key: (Account account) => account.full_path); 296 | } 297 | } 298 | // Return all the direct children of this key 299 | Iterable operator ^(String thisKey) sync* { 300 | if (_mAccount?.length > 0) { 301 | if (_mAccount[thisKey] == null) yield null; 302 | String pattern = "$thisKey\$"; 303 | for (Account account in _mAccount.values) { 304 | if (account.full_path.replaceFirst(pattern, '') == account.id) 305 | yield account; 306 | } 307 | } else 308 | yield null; 309 | } 310 | 311 | Account operator [](String key) { 312 | if (_mAccount?.length > 0) 313 | return _mAccount[key]; 314 | else 315 | return null; 316 | } 317 | 318 | void operator []=(String key, Account value) { 319 | if (_mAccount != null) _mAccount[key] = value; 320 | } 321 | 322 | void clear() { 323 | if (_mAccount != null) { 324 | _mAccount.clear(); 325 | _mAccount = null; 326 | } 327 | } 328 | 329 | void remove(String key) { 330 | if (_mAccount != null) _mAccount.remove(key); 331 | } 332 | 333 | int get length => _mAccount?.length; 334 | 335 | Map get interMap => _mAccount; 336 | } 337 | -------------------------------------------------------------------------------- /example/view/AccountNodeData.dart: -------------------------------------------------------------------------------- 1 | import 'package:piggy/common/TreeView.dart' show TreeNodeData; 2 | import 'package:piggy/model/Account.dart'; 3 | 4 | class AccountNodeData extends TreeNodeData { 5 | AccountNodeData.root(Account data) 6 | : super.root(data.username, data.email, data.full_path, data); 7 | 8 | AccountNodeData.node(Account data, AccountNodeData parent) 9 | : super.node(parent, data.username, data.email, data.full_path, data); 10 | 11 | @override 12 | AccountNodeData createChild( 13 | String title, String subTitle, String id, Account data, 14 | [bool expanded = false]) { 15 | var child = new AccountNodeData.node(data, this); 16 | return child; 17 | } 18 | 19 | @override 20 | String get title => data.username; 21 | 22 | @override 23 | String get subTitle => 24 | "${data.email} ${data.phone}\n${data.type=='root' || data.type=='master'?"Agent":"Player"}\n"; 25 | 26 | @override 27 | String toString() { 28 | return "${super.toString()}" 29 | "\nfullpath:${data.full_path} belongs to:${data.parent_id}"; 30 | } 31 | 32 | @override 33 | Iterable operator |(String textToSearch) sync* { 34 | String cat = "${data.username} ${data.email} ${data.type} ${data.phone}"; 35 | if (cat.toLowerCase().indexOf(textToSearch.toLowerCase()) > -1) { 36 | hilited = true; 37 | yield this; 38 | } 39 | if (hasChildren) 40 | for (AccountNodeData account in children) yield* account | textToSearch; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/src/BasicPage.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:piggy/common/Global.dart'; 3 | 4 | typedef dynamic ActionCallBack(dynamic inputParam); 5 | 6 | enum ActionTypes { search, delete, manage, expandAll, collapseAll } 7 | 8 | class BasicPage extends StatefulWidget { 9 | final String _title; 10 | bool _isLoading = false; 11 | bool _appBar; 12 | bool _enableFAB; 13 | bool _enableExpanCollap; 14 | bool _enableDelete; 15 | bool _enableSearch; 16 | List _actions; 17 | 18 | VoidCallback onFABressed; 19 | VoidCallback onSearch; 20 | VoidCallback onDelete; 21 | VoidCallback onManage; 22 | VoidCallback onExpandAll = null; 23 | VoidCallback onCollapseAll = null; 24 | 25 | // Properties 26 | String get title => _title; 27 | bool get isLoading => _isLoading; 28 | void set isLoading(bool value) => _isLoading = value; 29 | 30 | bool get enableFAB => _enableFAB; 31 | void set enableFAB(bool value) => _enableFAB = value; 32 | 33 | bool get enableExpandCollap => _enableExpanCollap; 34 | void set enableExpandCollap(bool value) => _enableExpanCollap = value; 35 | 36 | bool get enableSearch => _enableSearch; 37 | void set enableSearch(bool value) => _enableSearch = value; 38 | 39 | bool get enableDelete => _enableDelete; 40 | void set enableDelete(bool value) => _enableDelete = value; 41 | 42 | BasicPage(this._title, 43 | {Key key, 44 | List actions, 45 | appBar = false, 46 | enableFAB = true, 47 | enableExpanCollap = false, 48 | enableSearch = true, 49 | enableDelete = false}) 50 | : super(key: key) { 51 | _enableFAB = enableFAB; 52 | _appBar = appBar; 53 | _actions = actions; 54 | _enableExpanCollap = enableExpanCollap; 55 | _enableDelete = enableDelete; 56 | _enableSearch = enableSearch; 57 | } 58 | 59 | @override 60 | State createState() => new BasicPageState(); 61 | } 62 | 63 | class BasicPageState extends State { 64 | final GlobalKey _scaffoldKey = new GlobalKey(); 65 | // Utils 66 | void showInSnackBar(String value) { 67 | _scaffoldKey.currentState 68 | .showSnackBar(new SnackBar(content: new Text(value))); 69 | } 70 | 71 | List _buildActions() { 72 | List lstActions = []; 73 | int nIndex = 0; 74 | if (widget._actions != null) 75 | for (ActionTypes action in widget._actions) { 76 | if (action == ActionTypes.delete) 77 | lstActions.add(new IconButton( 78 | icon: const Icon(Icons.delete), 79 | tooltip: 'Delete', 80 | onPressed: widget.enableDelete ? widget.onDelete : null, 81 | )); 82 | else if (action == ActionTypes.search) 83 | lstActions.add(new IconButton( 84 | icon: const Icon(Icons.search), 85 | tooltip: 'Search', 86 | onPressed: widget.enableSearch ? widget.onSearch : null)); 87 | else if (action == ActionTypes.manage) 88 | lstActions.add(new IconButton( 89 | icon: const Icon(Icons.people_outline), 90 | tooltip: 'Manage people', 91 | onPressed: widget.onManage)); 92 | else if ((action == ActionTypes.expandAll || 93 | action == ActionTypes.collapseAll) && 94 | nIndex == 0) { 95 | lstActions.add(new PopupMenuButton( 96 | itemBuilder: (BuildContext context) => 97 | >[ 98 | new PopupMenuItem( 99 | enabled: widget.enableExpandCollap, 100 | value: ActionTypes.expandAll, 101 | child: const Text('Expand all')), 102 | new PopupMenuItem( 103 | enabled: widget.enableExpandCollap, 104 | value: ActionTypes.collapseAll, 105 | child: const Text('Collapse all')) 106 | ], 107 | onSelected: (ActionTypes doIt) { 108 | switch (doIt) { 109 | case ActionTypes.collapseAll: 110 | if (widget.onCollapseAll != null) widget.onCollapseAll(); 111 | break; 112 | case ActionTypes.expandAll: 113 | if (widget.onExpandAll != null) widget.onExpandAll(); 114 | break; 115 | default: 116 | break; 117 | } 118 | })); 119 | nIndex++; 120 | } 121 | } 122 | return lstActions; 123 | } 124 | 125 | @override 126 | Widget build(BuildContext context) { 127 | Widget content = buildContent(context); 128 | 129 | final ThemeData theme = Theme.of(context); 130 | final TextStyle titleStyle = theme.textTheme.subhead.copyWith( 131 | fontStyle: FontStyle.italic, color: theme.textTheme.caption.color); 132 | 133 | Widget loadingIndicatior = widget.isLoading 134 | ? new Column( 135 | children: [ 136 | const LinearProgressIndicator(), 137 | new Text("Wait for a moment...", style: titleStyle) 138 | ], 139 | ) 140 | : new Container(); 141 | 142 | Widget stack = new SizedBox.expand( 143 | child: new Stack( 144 | children: [ 145 | new Positioned.fill(child: content), 146 | new Positioned( 147 | bottom: Constants.VIEW_PADDING * 1.5, 148 | left: Constants.VIEW_PADDING * 5.2, 149 | right: Constants.VIEW_PADDING * 5.2, 150 | child: loadingIndicatior) 151 | ], 152 | )); 153 | 154 | Widget wrapperContainer; 155 | wrapperContainer = new Scaffold( 156 | appBar: widget._appBar 157 | ? new AppBar( 158 | title: new Text(widget.title), actions: _buildActions()) 159 | : null, 160 | key: _scaffoldKey, 161 | floatingActionButton: widget._enableFAB 162 | ? new FloatingActionButton( 163 | onPressed: widget.onFABressed, child: const Icon(Icons.add)) 164 | : null, 165 | body: new Container(child: stack)); 166 | 167 | return wrapperContainer; 168 | } 169 | 170 | Widget buildContent(BuildContext context) { 171 | return null; 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /lib/src/CustomExpansionTile.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/foundation.dart'; 6 | import 'package:flutter/widgets.dart'; 7 | import 'package:flutter/material.dart'; 8 | import 'package:piggy/common/CustomListTile.dart'; 9 | 10 | const Duration _kExpand = const Duration(milliseconds: 200); 11 | 12 | /// A single-line [ListTile] with a trailing button that expands or collapses 13 | /// the tile to reveal or hide the [children]. 14 | /// 15 | /// This widget is typically used with [ListView] to create an 16 | /// "expand / collapse" list entry. When used with scrolling widgets like 17 | /// [ListView], a unique [PageStorageKey] must be specified to enable the 18 | /// [ExpansionTile] to save and restore its expanded state when it is scrolled 19 | /// in and out of view. 20 | /// 21 | /// See also: 22 | /// 23 | /// * [ListTile], useful for creating expansion tile [children] when the 24 | /// expansion tile represents a sublist. 25 | /// * The "Expand/collapse" section of 26 | /// . 27 | class CustomExpansionTile extends StatefulWidget { 28 | /// Creates a single-line [ListTile] with a trailing button that expands or collapses 29 | /// the tile to reveal or hide the [children]. The [initiallyExpanded] property must 30 | /// be non-null. 31 | CustomExpansionTile({ 32 | Key key, 33 | this.leading, 34 | @required this.title, 35 | this.backgroundColor, 36 | this.onExpansionChanged, 37 | this.children: const [], 38 | this.trailing, 39 | this.initiallyExpanded: false, 40 | }) 41 | : assert(initiallyExpanded != null), 42 | super(key: key); 43 | 44 | /// A widget to display before the title. 45 | /// 46 | /// Typically a [CircleAvatar] widget. 47 | final Widget leading; 48 | 49 | /// The primary content of the list item. 50 | /// 51 | /// Typically a [Text] widget. 52 | final Widget title; 53 | 54 | /// Called when the tile expands or collapses. 55 | /// 56 | /// When the tile starts expanding, this function is called with the value 57 | /// true. When the tile starts collapsing, this function is called with 58 | /// the value false. 59 | final ValueChanged onExpansionChanged; 60 | 61 | /// The widgets that are displayed when the tile expands. 62 | /// 63 | /// Typically [ListTile] widgets. 64 | final List children; 65 | 66 | /// The color to display behind the sublist when expanded. 67 | final Color backgroundColor; 68 | 69 | /// A widget to display instead of a rotating arrow icon. 70 | final Widget trailing; 71 | 72 | /// Specifies if the list tile is initially expanded (true) or collapsed (false, the default). 73 | final bool initiallyExpanded; 74 | 75 | _CustomExpansionTileState _state; 76 | 77 | void toggle(bool bExpand) => _state.toggle(bExpand); 78 | 79 | @override 80 | _CustomExpansionTileState createState() { 81 | _state = new _CustomExpansionTileState(); 82 | return _state; 83 | } 84 | } 85 | 86 | class _CustomExpansionTileState extends State 87 | with SingleTickerProviderStateMixin { 88 | AnimationController _controller; 89 | CurvedAnimation _easeOutAnimation; 90 | CurvedAnimation _easeInAnimation; 91 | ColorTween _borderColor; 92 | ColorTween _headerColor; 93 | ColorTween _iconColor; 94 | ColorTween _backgroundColor; 95 | Animation _iconTurns; 96 | 97 | bool _isExpanded = false; 98 | 99 | @override 100 | void initState() { 101 | super.initState(); 102 | _controller = new AnimationController(duration: _kExpand, vsync: this); 103 | _easeOutAnimation = 104 | new CurvedAnimation(parent: _controller, curve: Curves.easeOut); 105 | _easeInAnimation = 106 | new CurvedAnimation(parent: _controller, curve: Curves.easeIn); 107 | _borderColor = new ColorTween(); 108 | _headerColor = new ColorTween(); 109 | _iconColor = new ColorTween(); 110 | _iconTurns = 111 | new Tween(begin: 0.0, end: 0.5).animate(_easeInAnimation); 112 | _backgroundColor = new ColorTween(); 113 | 114 | _isExpanded = widget 115 | .initiallyExpanded; //PageStorage.of(context)?.readState(context) ?? 116 | if (_isExpanded) _controller.value = 1.0; 117 | } 118 | 119 | @override 120 | void dispose() { 121 | _controller.dispose(); 122 | super.dispose(); 123 | } 124 | 125 | void toggle(bool bExpand) { 126 | if (mounted) 127 | setState(() { 128 | _isExpanded = bExpand; 129 | if (_isExpanded) 130 | _controller.forward(); 131 | else 132 | _controller.reverse().then((Null value) { 133 | setState(() { 134 | // Rebuild without widget.children. 135 | }); 136 | }); 137 | PageStorage.of(context)?.writeState(context, _isExpanded); 138 | }); 139 | if (widget.onExpansionChanged != null) 140 | widget.onExpansionChanged(_isExpanded); 141 | } 142 | 143 | void _handleTap() { 144 | setState(() { 145 | _isExpanded = !_isExpanded; 146 | if (_isExpanded) 147 | _controller.forward(); 148 | else 149 | _controller.reverse().then((Null value) { 150 | setState(() { 151 | // Rebuild without widget.children. 152 | }); 153 | }); 154 | PageStorage.of(context)?.writeState(context, _isExpanded); 155 | }); 156 | if (widget.onExpansionChanged != null) 157 | widget.onExpansionChanged(_isExpanded); 158 | } 159 | 160 | Widget _buildChildren(BuildContext context, Widget child) { 161 | final Color borderSideColor = 162 | _borderColor.evaluate(_easeOutAnimation) ?? Colors.transparent; 163 | final Color titleColor = _headerColor.evaluate(_easeInAnimation); 164 | 165 | return new Container( 166 | // decoration: new BoxDecoration( 167 | // color: _backgroundColor.evaluate(_easeOutAnimation) ?? 168 | // Colors.transparent, 169 | // border: new Border( 170 | // top: new BorderSide(color: borderSideColor), 171 | // bottom: new BorderSide(color: borderSideColor), 172 | // )), 173 | child: new Column( 174 | mainAxisSize: MainAxisSize.min, 175 | children: [ 176 | IconTheme.merge( 177 | data: 178 | new IconThemeData(color: _iconColor.evaluate(_easeInAnimation)), 179 | child: new CustomListTile( 180 | onTap: _handleTap, 181 | leading: widget.leading ?? 182 | new RotationTransition( 183 | turns: _iconTurns, 184 | child: const Icon(Icons.expand_more), 185 | ), 186 | title: new DefaultTextStyle( 187 | style: Theme 188 | .of(context) 189 | .textTheme 190 | .subhead 191 | .copyWith(color: titleColor), 192 | child: widget.title, 193 | ), 194 | trailing: widget.trailing), 195 | ), 196 | new ClipRect( 197 | child: new Align( 198 | heightFactor: _easeInAnimation.value, 199 | child: child, 200 | ), 201 | ), 202 | ], 203 | ), 204 | ); 205 | } 206 | 207 | @override 208 | Widget build(BuildContext context) { 209 | final ThemeData theme = Theme.of(context); 210 | _borderColor.end = theme.dividerColor; 211 | _headerColor 212 | ..begin = theme.textTheme.subhead.color 213 | ..end = theme.accentColor; 214 | _iconColor 215 | ..begin = theme.unselectedWidgetColor 216 | ..end = theme.accentColor; 217 | _backgroundColor.end = widget.backgroundColor; 218 | 219 | final bool closed = !_isExpanded && _controller.isDismissed; 220 | return new AnimatedBuilder( 221 | animation: _controller.view, 222 | builder: _buildChildren, 223 | child: closed ? null : new Column(children: widget.children), 224 | ); 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /lib/src/CustomListTile.dart: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | import 'package:flutter/foundation.dart'; 6 | import 'package:flutter/widgets.dart'; 7 | 8 | import 'package:flutter/material.dart'; 9 | //import 'constants.dart'; 10 | //import 'debug.dart'; 11 | //import 'ink_well.dart'; 12 | //import 'theme.dart'; 13 | 14 | /// Defines the title font used for [CustomListTile] descendants of a [CustomListTileTheme]. 15 | /// 16 | /// List tiles that appear in a [Drawer] use the theme's [TextTheme.body2] 17 | /// text style, which is a little smaller than the theme's [TextTheme.subhead] 18 | /// text style, which is used by default. 19 | enum CustomListTileStyle { 20 | /// Use a title font that's appropriate for a [CustomListTile] in a list. 21 | list, 22 | 23 | /// Use a title font that's appropriate for a [CustomListTile] that appears in a [Drawer]. 24 | drawer, 25 | } 26 | 27 | /// An inherited widget that defines color and style parameters for [CustomListTile]s 28 | /// in this widget's subtree. 29 | /// 30 | /// Values specified here are used for [CustomListTile] properties that are not given 31 | /// an explicit non-null value. 32 | /// 33 | /// The [Drawer] widget specifies a tile theme for its children which sets 34 | /// [style] to [CustomListTileStyle.drawer]. 35 | class CustomListTileTheme extends InheritedWidget { 36 | /// Creates a list tile theme that controls the color and style parameters for 37 | /// [CustomListTile]s. 38 | const CustomListTileTheme({ 39 | Key key, 40 | this.dense: false, 41 | this.style: CustomListTileStyle.list, 42 | this.selectedColor, 43 | this.iconColor, 44 | this.textColor, 45 | Widget child, 46 | }) 47 | : super(key: key, child: child); 48 | 49 | /// Creates a list tile theme that controls the color and style parameters for 50 | /// [CustomListTile]s, and merges in the current list tile theme, if any. 51 | /// 52 | /// The [child] argument must not be null. 53 | static Widget merge({ 54 | Key key, 55 | bool dense, 56 | CustomListTileStyle style, 57 | Color selectedColor, 58 | Color iconColor, 59 | Color textColor, 60 | @required Widget child, 61 | }) { 62 | assert(child != null); 63 | return new Builder( 64 | builder: (BuildContext context) { 65 | final CustomListTileTheme parent = CustomListTileTheme.of(context); 66 | return new CustomListTileTheme( 67 | key: key, 68 | dense: dense ?? parent.dense, 69 | style: style ?? parent.style, 70 | selectedColor: selectedColor ?? parent.selectedColor, 71 | iconColor: iconColor ?? parent.iconColor, 72 | textColor: textColor ?? parent.textColor, 73 | child: child, 74 | ); 75 | }, 76 | ); 77 | } 78 | 79 | /// If true then [CustomListTile]s will have the vertically dense layout. 80 | final bool dense; 81 | 82 | /// If specified, [style] defines the font used for [CustomListTile] titles. 83 | final CustomListTileStyle style; 84 | 85 | /// If specified, the color used for icons and text when a [CustomListTile] is selected. 86 | final Color selectedColor; 87 | 88 | /// If specified, the icon color used for enabled [CustomListTile]s that are not selected. 89 | final Color iconColor; 90 | 91 | /// If specified, the text color used for enabled [CustomListTile]s that are not selected. 92 | final Color textColor; 93 | 94 | /// The closest instance of this class that encloses the given context. 95 | /// 96 | /// Typical usage is as follows: 97 | /// 98 | /// ```dart 99 | /// CustomListTileTheme theme = CustomListTileTheme.of(context); 100 | /// ``` 101 | static CustomListTileTheme of(BuildContext context) { 102 | final CustomListTileTheme result = 103 | context.inheritFromWidgetOfExactType(CustomListTileTheme); 104 | return result ?? const CustomListTileTheme(); 105 | } 106 | 107 | @override 108 | bool updateShouldNotify(CustomListTileTheme oldTheme) { 109 | return dense != oldTheme.dense || 110 | style != oldTheme.style || 111 | selectedColor != oldTheme.selectedColor || 112 | iconColor != oldTheme.iconColor || 113 | textColor != oldTheme.textColor; 114 | } 115 | } 116 | 117 | /// Where to place the control in widgets that use [CustomListTile] to position a 118 | /// control next to a label. 119 | /// 120 | /// See also: 121 | /// 122 | /// * [CheckboxCustomListTile], which combines a [CustomListTile] with a [Checkbox]. 123 | /// * [RadioCustomListTile], which combines a [CustomListTile] with a [Radio] button. 124 | enum CustomListTileControlAffinity { 125 | /// Position the control on the leading edge, and the secondary widget, if 126 | /// any, on the trailing edge. 127 | leading, 128 | 129 | /// Position the control on the trailing edge, and the secondary widget, if 130 | /// any, on the leading edge. 131 | trailing, 132 | 133 | /// Position the control relative to the text in the fashion that is typical 134 | /// for the current platform, and place the secondary widget on the opposite 135 | /// side. 136 | platform, 137 | } 138 | 139 | /// A single fixed-height row that typically contains some text as well as 140 | /// a leading or trailing icon. 141 | /// 142 | /// A list tile contains one to three lines of text optionally flanked by icons or 143 | /// other widgets, such as check boxes. The icons (or other widgets) for the 144 | /// tile are defined with the [leading] and [trailing] parameters. The first 145 | /// line of text is not optional and is specified with [title]. The value of 146 | /// [subtitle], which _is_ optional, will occupy the space allocated for an 147 | /// additional line of text, or two lines if [isThreeLine] is true. If [dense] 148 | /// is true then the overall height of this tile and the size of the 149 | /// [DefaultTextStyle]s that wrap the [title] and [subtitle] widget are reduced. 150 | /// 151 | /// List tiles are always a fixed height (which height depends on how 152 | /// [isThreeLine], [dense], and [subtitle] are configured); they do not grow in 153 | /// height based on their contents. If you are looking for a widget that allows 154 | /// for arbitrary layout in a row, consider [Row]. 155 | /// 156 | /// List tiles are typically used in [ListView]s, or arranged in [Column]s in 157 | /// [Drawer]s and [Card]s. 158 | /// 159 | /// Requires one of its ancestors to be a [Material] widget. 160 | /// 161 | /// ## Sample code 162 | /// 163 | /// Here is a simple tile with an icon and some text. 164 | /// 165 | /// ```dart 166 | /// new CustomListTile( 167 | /// leading: const Icon(Icons.event_seat), 168 | /// title: const Text('The seat for the narrator'), 169 | /// ) 170 | /// ``` 171 | /// 172 | /// Tiles can be much more elaborate. Here is a tile which can be tapped, but 173 | /// which is disabled when the `_act` variable is not 2. When the tile is 174 | /// tapped, the whole row has an ink splash effect (see [InkWell]). 175 | /// 176 | /// ```dart 177 | /// int _act = 1; 178 | /// // ... 179 | /// new CustomListTile( 180 | /// leading: const Icon(Icons.flight_land), 181 | /// title: const Text('Trix\'s airplane'), 182 | /// subtitle: _act != 2 ? const Text('The airplane is only in Act II.') : null, 183 | /// enabled: _act == 2, 184 | /// onTap: () { /* react to the tile being tapped */ } 185 | /// ) 186 | /// ``` 187 | /// 188 | /// See also: 189 | /// 190 | /// * [CustomListTileTheme], which defines visual properties for [CustomListTile]s. 191 | /// * [ListView], which can display an arbitrary number of [CustomListTile]s 192 | /// in a scrolling list. 193 | /// * [CircleAvatar], which shows an icon representing a person and is often 194 | /// used as the [leading] element of a CustomListTile. 195 | /// * [Card], which can be used with [Column] to show a few [CustomListTile]s. 196 | /// * [Divider], which can be used to separate [CustomListTile]s. 197 | /// * [CustomListTile.divideTiles], a utility for inserting [Divider]s in between [CustomListTile]s. 198 | /// * [CheckboxCustomListTile], [RadioCustomListTile], and [SwitchCustomListTile], widgets 199 | /// that combine [CustomListTile] with other controls. 200 | /// * 201 | class CustomListTile extends StatelessWidget { 202 | /// Creates a list tile. 203 | /// 204 | /// If [isThreeLine] is true, then [subtitle] must not be null. 205 | /// 206 | /// Requires one of its ancestors to be a [Material] widget. 207 | const CustomListTile({ 208 | Key key, 209 | this.leading, 210 | this.title, 211 | this.subtitle, 212 | this.trailing, 213 | this.isThreeLine: false, 214 | this.dense, 215 | this.enabled: true, 216 | this.onTap, 217 | this.onLongPress, 218 | this.selected: false, 219 | }) 220 | : assert(isThreeLine != null), 221 | assert(enabled != null), 222 | assert(selected != null), 223 | assert(!isThreeLine || subtitle != null), 224 | super(key: key); 225 | 226 | /// A widget to display before the title. 227 | /// 228 | /// Typically an [Icon] or a [CircleAvatar] widget. 229 | final Widget leading; 230 | 231 | /// The primary content of the list tile. 232 | /// 233 | /// Typically a [Text] widget. 234 | final Widget title; 235 | 236 | /// Additional content displayed below the title. 237 | /// 238 | /// Typically a [Text] widget. 239 | final Widget subtitle; 240 | 241 | /// A widget to display after the title. 242 | /// 243 | /// Typically an [Icon] widget. 244 | final Widget trailing; 245 | 246 | /// Whether this list tile is intended to display three lines of text. 247 | /// 248 | /// If false, the list tile is treated as having one line if the subtitle is 249 | /// null and treated as having two lines if the subtitle is non-null. 250 | final bool isThreeLine; 251 | 252 | /// Whether this list tile is part of a vertically dense list. 253 | /// 254 | /// If this property is null then its value is based on [CustomListTileTheme.dense]. 255 | final bool dense; 256 | 257 | /// Whether this list tile is interactive. 258 | /// 259 | /// If false, this list tile is styled with the disabled color from the 260 | /// current [Theme] and the [onTap] and [onLongPress] callbacks are 261 | /// inoperative. 262 | final bool enabled; 263 | 264 | /// Called when the user taps this list tile. 265 | /// 266 | /// Inoperative if [enabled] is false. 267 | final GestureTapCallback onTap; 268 | 269 | /// Called when the user long-presses on this list tile. 270 | /// 271 | /// Inoperative if [enabled] is false. 272 | final GestureLongPressCallback onLongPress; 273 | 274 | /// If this tile is also [enabled] then icons and text are rendered with the same color. 275 | /// 276 | /// By default the selected color is the theme's primary color. The selected color 277 | /// can be overridden with a [CustomListTileTheme]. 278 | final bool selected; 279 | 280 | /// Add a one pixel border in between each tile. If color isn't specified the 281 | /// [ThemeData.dividerColor] of the context's [Theme] is used. 282 | /// 283 | /// See also: 284 | /// 285 | /// * [Divider], which you can use to obtain this effect manually. 286 | static Iterable divideTiles( 287 | {BuildContext context, 288 | @required Iterable tiles, 289 | Color color}) sync* { 290 | assert(tiles != null); 291 | assert(color != null || context != null); 292 | 293 | final Color dividerColor = color ?? Theme.of(context).dividerColor; 294 | final Iterator iterator = tiles.iterator; 295 | final bool isNotEmpty = iterator.moveNext(); 296 | 297 | Widget tile = iterator.current; 298 | while (iterator.moveNext()) { 299 | yield new DecoratedBox( 300 | position: DecorationPosition.foreground, 301 | decoration: new BoxDecoration( 302 | border: new Border( 303 | bottom: new BorderSide(color: dividerColor, width: 0.0), 304 | ), 305 | ), 306 | child: tile, 307 | ); 308 | tile = iterator.current; 309 | } 310 | if (isNotEmpty) yield tile; 311 | } 312 | 313 | Color _iconColor(ThemeData theme, CustomListTileTheme tileTheme) { 314 | if (!enabled) return theme.disabledColor; 315 | 316 | if (selected && tileTheme?.selectedColor != null) 317 | return tileTheme.selectedColor; 318 | 319 | if (!selected && tileTheme?.iconColor != null) return tileTheme.iconColor; 320 | 321 | switch (theme.brightness) { 322 | case Brightness.light: 323 | return selected ? theme.primaryColor : Colors.black45; 324 | case Brightness.dark: 325 | return selected 326 | ? theme.accentColor 327 | : null; // null - use current icon theme color 328 | } 329 | assert(theme.brightness != null); 330 | return null; 331 | } 332 | 333 | Color _textColor( 334 | ThemeData theme, CustomListTileTheme tileTheme, Color defaultColor) { 335 | if (!enabled) return theme.disabledColor; 336 | 337 | if (selected && tileTheme?.selectedColor != null) 338 | return tileTheme.selectedColor; 339 | 340 | if (!selected && tileTheme?.textColor != null) return tileTheme.textColor; 341 | 342 | if (selected) { 343 | switch (theme.brightness) { 344 | case Brightness.light: 345 | return theme.primaryColor; 346 | case Brightness.dark: 347 | return theme.accentColor; 348 | } 349 | } 350 | return defaultColor; 351 | } 352 | 353 | bool _denseLayout(CustomListTileTheme tileTheme) { 354 | return dense != null ? dense : (tileTheme?.dense ?? false); 355 | } 356 | 357 | TextStyle _titleTextStyle(ThemeData theme, CustomListTileTheme tileTheme) { 358 | TextStyle style; 359 | if (tileTheme != null) { 360 | switch (tileTheme.style) { 361 | case CustomListTileStyle.drawer: 362 | style = theme.textTheme.body2; 363 | break; 364 | case CustomListTileStyle.list: 365 | style = theme.textTheme.subhead; 366 | break; 367 | } 368 | } else { 369 | style = theme.textTheme.subhead; 370 | } 371 | final Color color = _textColor(theme, tileTheme, style.color); 372 | return _denseLayout(tileTheme) 373 | ? style.copyWith(fontSize: 13.0, color: color) 374 | : style.copyWith(color: color); 375 | } 376 | 377 | TextStyle _subtitleTextStyle(ThemeData theme, CustomListTileTheme tileTheme) { 378 | final TextStyle style = theme.textTheme.body1; 379 | final Color color = 380 | _textColor(theme, tileTheme, theme.textTheme.caption.color); 381 | return _denseLayout(tileTheme) 382 | ? style.copyWith(color: color, fontSize: 12.0) 383 | : style.copyWith(color: color); 384 | } 385 | 386 | @override 387 | Widget build(BuildContext context) { 388 | assert(debugCheckHasMaterial(context)); 389 | final ThemeData theme = Theme.of(context); 390 | final CustomListTileTheme tileTheme = CustomListTileTheme.of(context); 391 | 392 | final bool isTwoLine = !isThreeLine && subtitle != null; 393 | final bool isOneLine = !isThreeLine && !isTwoLine; 394 | double tileHeight; 395 | if (isOneLine) 396 | tileHeight = _denseLayout(tileTheme) ? 48.0 : 56.0; 397 | else if (isTwoLine) 398 | tileHeight = _denseLayout(tileTheme) ? 60.0 : 72.0; 399 | else 400 | tileHeight = _denseLayout(tileTheme) ? 76.0 : 88.0; 401 | 402 | // Overall, the list tile is a Row() with these children. 403 | final List children = []; 404 | 405 | IconThemeData iconThemeData; 406 | if (leading != null || trailing != null) 407 | iconThemeData = new IconThemeData(color: _iconColor(theme, tileTheme)); 408 | 409 | if (leading != null) { 410 | children.add(IconTheme.merge( 411 | data: iconThemeData, 412 | child: new Container( 413 | // margin: const EdgeInsetsDirectional.only(end: 16.0), 414 | width: 30.0, 415 | alignment: AlignmentDirectional.centerStart, 416 | child: leading, 417 | ), 418 | )); 419 | } 420 | 421 | final Widget primaryLine = new AnimatedDefaultTextStyle( 422 | style: _titleTextStyle(theme, tileTheme), 423 | duration: kThemeChangeDuration, 424 | child: title ?? new Container()); 425 | Widget center = primaryLine; 426 | if (subtitle != null && (isTwoLine || isThreeLine)) { 427 | center = new Column( 428 | mainAxisSize: MainAxisSize.min, 429 | crossAxisAlignment: CrossAxisAlignment.start, 430 | children: [ 431 | primaryLine, 432 | new AnimatedDefaultTextStyle( 433 | style: _subtitleTextStyle(theme, tileTheme), 434 | duration: kThemeChangeDuration, 435 | child: subtitle, 436 | ), 437 | ], 438 | ); 439 | } 440 | children.add(new Expanded( 441 | child: center, 442 | )); 443 | 444 | if (trailing != null) { 445 | children.add(IconTheme.merge( 446 | data: iconThemeData, 447 | child: new Container( 448 | margin: const EdgeInsetsDirectional.only(start: 16.0), 449 | alignment: AlignmentDirectional.centerEnd, 450 | child: trailing, 451 | ), 452 | )); 453 | } 454 | 455 | return new InkWell( 456 | onTap: enabled ? onTap : null, 457 | onLongPress: enabled ? onLongPress : null, 458 | child: new ConstrainedBox( 459 | constraints: new BoxConstraints(minHeight: tileHeight), 460 | child: new Padding( 461 | padding: const EdgeInsets.symmetric(horizontal: 0.0), 462 | child: new UnconstrainedBox( 463 | constrainedAxis: Axis.horizontal, 464 | child: new Row(children: children), 465 | ), 466 | )), 467 | ); 468 | } 469 | } 470 | -------------------------------------------------------------------------------- /lib/src/Global.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:flutter/material.dart'; 3 | 4 | 5 | class Constants { 6 | 7 | static const VIEW_PADDING = 15.0; 8 | static const LIST_ITEM_INDENT = 16.0; 9 | static const VERTICAL_PADDING = 15.0; 10 | static const HORIZONTAL_PADDING = 48.0; 11 | static const VERTICAL_PADDING_FORM = 10.0; 12 | static const HORIZONTAL_PADDING_FORM = 8.0; 13 | } 14 | class Util { 15 | static String getKey() { 16 | List strings = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; 17 | strings.shuffle(); 18 | String randId = strings.join(""); 19 | return randId; 20 | } 21 | 22 | static Future alert(BuildContext context, 23 | {String title = "Notification", 24 | String content = "Detail", 25 | bool allowCancel = false}) async { 26 | var alert = new AlertDialog( 27 | title: new Text(title), 28 | content: new Text(content), 29 | actions: [ 30 | new FlatButton( 31 | child: const Text('OK'), 32 | onPressed: () { 33 | Navigator.of(context).pop(true); 34 | }, 35 | ), 36 | allowCancel 37 | ? new FlatButton( 38 | child: const Text('Cancel'), 39 | onPressed: () { 40 | Navigator.of(context).pop(false); 41 | }, 42 | ) 43 | : new Container() 44 | ], 45 | ); 46 | 47 | return await showDialog( 48 | context: context, child: alert, barrierDismissible: false); 49 | } 50 | } 51 | // Stack emulator 52 | class StackEmul { 53 | List _internalList; 54 | 55 | StackEmul({List list}) { 56 | if (list != null) 57 | _internalList = list; 58 | else 59 | _internalList = []; 60 | } 61 | 62 | void push(T object) { 63 | assert(_internalList != null); 64 | _internalList.add(object); 65 | } 66 | 67 | T pop() { 68 | assert(_internalList != null); 69 | if(_internalList.length>0) 70 | return _internalList.removeLast(); 71 | else 72 | return null; 73 | } 74 | 75 | int get count=> _internalList?.length; 76 | } 77 | 78 | 79 | -------------------------------------------------------------------------------- /lib/src/SearchBar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:validator/validator.dart'; 3 | 4 | typedef void SearchCallback(String stringToSearch); 5 | 6 | class SearchBar extends StatefulWidget { 7 | SearchCallback _onSearch; 8 | 9 | SearchBar({SearchCallback onSearch}) { 10 | _onSearch = onSearch; 11 | } 12 | 13 | @override 14 | SearchBarState createState() => new SearchBarState(); 15 | } 16 | 17 | class SearchBarState extends State { 18 | bool _isSearching = false; 19 | final GlobalKey> _textsearchFieldKey = 20 | new GlobalKey>(); 21 | final GlobalKey _formKey = new GlobalKey(); 22 | String _searchValue = ""; 23 | FocusNode _inputFocus = new FocusNode(); 24 | 25 | // Event handlers 26 | _onSearch() { 27 | FocusScope.of(context).requestFocus(new FocusNode()); 28 | final FormState form = _formKey.currentState; 29 | form.save(); 30 | // print("Search: '$_searchValue'"); 31 | 32 | if (!isNull(_searchValue)) 33 | setState(() { 34 | _isSearching = true; 35 | if (widget._onSearch != null) widget._onSearch(_searchValue); 36 | }); 37 | } 38 | 39 | _onDelete() { 40 | final FormFieldState _textsearchField = 41 | _textsearchFieldKey.currentState; 42 | _textsearchField.reset(); 43 | 44 | FocusScope.of(context).requestFocus(_inputFocus); 45 | setState(() { 46 | _isSearching = false; 47 | _searchValue = ""; 48 | }); 49 | } 50 | 51 | @override 52 | Widget build(BuildContext context) { 53 | return new SizedBox( 54 | height: 40.0, 55 | child: new Form( 56 | key: _formKey, 57 | child: new Row( 58 | mainAxisSize: MainAxisSize.min, 59 | children: [ 60 | new SizedBox( 61 | width: 180.0, 62 | child: new TextFormField( 63 | focusNode: _inputFocus, 64 | key: _textsearchFieldKey, 65 | style: new TextStyle(color: Colors.black), 66 | decoration: new InputDecoration( 67 | hintText: "Enter...", 68 | hintStyle: new TextStyle(color: Colors.black), 69 | ), 70 | onSaved: (String value) { 71 | _searchValue = value; 72 | }, 73 | ), 74 | ), 75 | new Expanded( 76 | child: _isSearching 77 | ? new IconButton( 78 | icon: new Icon(Icons.clear), 79 | onPressed: _onDelete, 80 | ) 81 | : new IconButton( 82 | icon: new Icon(Icons.search), 83 | onPressed: _onSearch, 84 | )) 85 | ], 86 | ), 87 | )); 88 | } 89 | } 90 | 91 | class _HeadingLayout extends MultiChildLayoutDelegate { 92 | _HeadingLayout(); 93 | 94 | static final String searchBar = 'searchBar'; 95 | 96 | @override 97 | void performLayout(Size size) { 98 | const double marginX = 16.0; 99 | const double marginY = 5.0; 100 | 101 | final double maxHeaderWidth = 200.0; 102 | final BoxConstraints headerBoxConstraints = 103 | new BoxConstraints(maxWidth: maxHeaderWidth); 104 | final Size headerSize = layoutChild(searchBar, headerBoxConstraints); 105 | 106 | final double searchbarX = (size.width - headerSize.width) / 2.0; 107 | positionChild(searchBar, new Offset(searchbarX, marginY)); 108 | } 109 | 110 | @override 111 | bool shouldRelayout(_HeadingLayout oldDelegate) => false; 112 | } 113 | 114 | class Heading extends StatelessWidget { 115 | SearchCallback _searchCallback; 116 | 117 | Heading({Key key, SearchCallback searchCallback}) : super(key: key) { 118 | _searchCallback = searchCallback; 119 | } 120 | 121 | @override 122 | Widget build(BuildContext context) { 123 | final Size screenSize = MediaQuery.of(context).size; 124 | final ThemeData theme = Theme.of(context); 125 | return new MergeSemantics( 126 | child: new SizedBox( 127 | height: screenSize.width > screenSize.height 128 | ? (screenSize.height - kToolbarHeight) * 0.10 129 | : (screenSize.height - kToolbarHeight) * 0.08, 130 | child: new Container( 131 | decoration: new BoxDecoration( 132 | color: Theme.of(context).accentColor.withOpacity(0.015), 133 | border: 134 | new Border(bottom: new BorderSide(color: theme.dividerColor)), 135 | ), 136 | child: new CustomMultiChildLayout( 137 | delegate: new _HeadingLayout(), 138 | children: [ 139 | new LayoutId( 140 | id: _HeadingLayout.searchBar, 141 | child: new SearchBar( 142 | onSearch: _searchCallback, 143 | ), 144 | ), 145 | ], 146 | ), 147 | ), 148 | ), 149 | ); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /lib/src/TreeView.dart: -------------------------------------------------------------------------------- 1 | library piggy_treeview; 2 | 3 | import 'package:flutter/material.dart'; 4 | import './CustomExpansionTile.dart'; 5 | import './CustomListTile.dart'; 6 | import './Global.dart'; 7 | 8 | typedef void NodeCallback(T node); 9 | typedef void EditNodeCallback(T node); 10 | typedef void ManageCallback(T node); 11 | 12 | /// 13 | /// T: any data passed to the node. 14 | /// 15 | 16 | class TreeNodeData { 17 | T _data; 18 | Widget _bindedWidget; 19 | 20 | String _id; 21 | String _title; 22 | String _subtitle; 23 | 24 | TreeNodeData _parent; 25 | List> _children = []; 26 | 27 | bool _hidden; 28 | bool _expanded; 29 | bool _checked; 30 | bool _hilited; 31 | 32 | // Constructors 33 | TreeNodeData(); 34 | 35 | TreeNodeData.root(this._title, this._subtitle, this._id, 36 | [this._data, 37 | this._checked = false, 38 | this._expanded = true, 39 | this._hidden = false, 40 | this._hilited = false]) { 41 | _checkInit(); 42 | } 43 | 44 | TreeNodeData.node(this._parent, this._title, this._subtitle, this._id, 45 | [this._data, 46 | this._checked = false, 47 | this._expanded = false, 48 | this._hidden = false, 49 | this._hilited = false]) { 50 | _checkInit(); 51 | _parent._children.add(this); 52 | } 53 | 54 | void _checkInit() { 55 | if (this._checked == null) this._checked = false; 56 | if (this._expanded == null) this._expanded = false; 57 | if (this._hidden == null) this._hidden = false; 58 | if (this._hilited == null) this._hilited = false; 59 | } 60 | 61 | // Properties 62 | T get data => _data; 63 | 64 | set data(T nodeData) => _data = nodeData; 65 | 66 | String get id => _id; 67 | 68 | String get title => _title; 69 | 70 | String get subTitle => _subtitle; 71 | 72 | bool get isRoot => _parent == null; 73 | 74 | bool get isLeaf => !hasChildren; 75 | 76 | bool get hasChildren => _children.isNotEmpty; 77 | 78 | bool get isExpanded => _expanded; 79 | 80 | bool get isChecked => _checked; 81 | 82 | bool get isHidden => _hidden; 83 | 84 | bool get isHilited => _hilited; 85 | 86 | set checked(bool checked) => _checked = checked; 87 | 88 | set expanded(bool expanded) => _expanded = expanded; 89 | 90 | set hidden(bool hidden) => _hidden = hidden; 91 | 92 | set hilited(bool hilite) => _hilited = hilite; 93 | 94 | set bindedWidget(Widget widget) => _bindedWidget = widget; 95 | 96 | List> get children => _children; 97 | 98 | TreeNodeData get parent => _parent; 99 | 100 | Widget get bindedWidget => _bindedWidget; 101 | 102 | // Methods 103 | TreeNodeData getNodeByID(String id) { 104 | if (this.id == id) return this; 105 | 106 | for (var node in _children) { 107 | var found = node.getNodeByID(id); 108 | if (found != null) return found; 109 | } 110 | 111 | return null; 112 | } 113 | 114 | List> getAllSubNodes([bool addThisNode = true]) { 115 | List> all = []; 116 | 117 | if (addThisNode) all.add(this); 118 | 119 | _addAllSubNodes(all); 120 | 121 | return all; 122 | } 123 | 124 | void _addAllSubNodes(List> all) { 125 | for (var node in _children) { 126 | all.add(node); 127 | node._addAllSubNodes(all); 128 | } 129 | } 130 | 131 | TreeNodeData getRoot() { 132 | TreeNodeData cursor = this; 133 | do { 134 | if (cursor.isRoot) return cursor; 135 | cursor = cursor.parent; 136 | } while (cursor != null); 137 | 138 | throw new StateError("Cấu trúc cây không hợp lệ"); 139 | } 140 | 141 | TreeNodeData createChild(String title, String subTitle, String id, T data, 142 | [bool expanded = false]) { 143 | var child = 144 | new TreeNodeData.node(this, title, subTitle, id, data, expanded); 145 | return child; 146 | } 147 | 148 | String toString() => 149 | "title: id=$title id:$id subTittle:$subTitle isExpanded:$isExpanded checked: $isChecked"; 150 | } 151 | 152 | class TreeNode extends StatefulWidget { 153 | TreeView _treeComponent; 154 | TreeNodeData _nodeData; 155 | NodeCallback onSelectNode; 156 | NodeCallback onHiliteNode; 157 | EditNodeCallback onEditNode; 158 | ManageCallback onManage; 159 | // Constructor 160 | TreeNode(this._treeComponent, this._nodeData, {Key key}) : super(key: key); 161 | TreeNodeState _state; 162 | 163 | void toggle(bool bExpand) => _state?.toggle(bExpand); 164 | 165 | void broadcast(Map map) => _state?.broadcast(map); 166 | 167 | void openIt() => _state?.openIt(); 168 | 169 | @override 170 | TreeNodeState createState() { 171 | _state = new TreeNodeState(); 172 | return _state; 173 | } 174 | } 175 | 176 | class TreeNodeState extends State { 177 | CustomExpansionTile _wExpand; 178 | List _lstNodes; 179 | 180 | void openIt() { 181 | _wExpand?.toggle(true); 182 | } 183 | 184 | void toggle(bool bExpand) { 185 | _wExpand?.toggle(bExpand); 186 | if (_lstNodes != null) 187 | for (TreeNode node in _lstNodes) { 188 | node?.toggle(bExpand); 189 | } 190 | } 191 | 192 | void broadcast(Map map) { 193 | TreeNodeData tempNode = map[widget._nodeData.id]; 194 | if (tempNode != null && mounted) 195 | setState(() => widget._nodeData.hilited = tempNode.isHilited); 196 | _lstNodes?.forEach((node) => node.broadcast(map)); 197 | } 198 | 199 | @override 200 | void initState() { 201 | super.initState(); 202 | _lstNodes = widget._nodeData.hasChildren 203 | ? widget._nodeData.children.map((TreeNodeData subNode) { 204 | TreeNode treeNode = new TreeNode(widget._treeComponent, subNode, 205 | key: new Key(Util.getKey())); 206 | treeNode.onSelectNode = widget.onSelectNode; 207 | treeNode.onEditNode = widget.onEditNode; 208 | treeNode.onManage = widget.onManage; 209 | treeNode.onHiliteNode = widget.onHiliteNode; 210 | // Make sure to store a reference to the associated widget 211 | subNode.bindedWidget = treeNode; 212 | return treeNode; 213 | }).toList() 214 | : null; 215 | } 216 | 217 | @override 218 | void dispose() { 219 | super.dispose(); 220 | _wExpand = null; 221 | _lstNodes?.clear(); 222 | _lstNodes = null; 223 | } 224 | 225 | @override 226 | Widget build(BuildContext context) { 227 | return _buildNode(); 228 | } 229 | 230 | Widget _buildNode() { 231 | var currentNode = widget._nodeData; 232 | if (currentNode.isLeaf) { 233 | return buildNormalNode(currentNode); 234 | } else { 235 | return buildExpandedNode(currentNode); 236 | } 237 | } 238 | 239 | // Handlers 240 | void selectNodes(TreeNodeData currentNode, bool value) { 241 | currentNode.checked = value; 242 | 243 | if (widget.onSelectNode != null) widget.onSelectNode(currentNode); 244 | 245 | if (currentNode.hasChildren) { 246 | for (var node in currentNode.children) { 247 | selectNodes(node, value); 248 | } 249 | } 250 | } 251 | 252 | /// The subclass has to override the two methods below to display a node 253 | Widget buildNormalNode(TreeNodeData currentNode) { 254 | return new Container( 255 | padding: new EdgeInsets.only(left: Constants.LIST_ITEM_INDENT), 256 | child: new CustomListTile( 257 | key: new Key(Util.getKey()), 258 | leading: new Checkbox( 259 | value: currentNode.isChecked, 260 | onChanged: (bool value) { 261 | setState(() => currentNode.checked = value); 262 | if (widget.onSelectNode != null) 263 | widget.onSelectNode(currentNode); 264 | }), 265 | title: new Text( 266 | currentNode.title, 267 | ), 268 | subtitle: new Text(currentNode.subTitle), 269 | selected: currentNode.isHilited, 270 | onTap: () { 271 | setState(() => currentNode.hilited = !currentNode.isHilited); 272 | if (widget.onHiliteNode != null) widget.onHiliteNode(currentNode); 273 | }, 274 | onLongPress: () { 275 | setState(() { 276 | if (widget.onEditNode != null) widget.onEditNode(currentNode); 277 | }); 278 | }, 279 | )); 280 | } 281 | 282 | Widget buildExpandedNode(TreeNodeData currentNode) { 283 | _wExpand = new CustomExpansionTile( 284 | initiallyExpanded: currentNode.parent == null 285 | ? currentNode.isExpanded 286 | : widget._treeComponent.expandIt, 287 | onExpansionChanged: (bool value) { 288 | currentNode.expanded = value; 289 | }, 290 | key: new Key(Util.getKey()), 291 | title: new CustomListTile( 292 | key: new Key(currentNode.id), 293 | leading: new Checkbox( 294 | value: currentNode.isChecked, 295 | onChanged: (bool value) { 296 | setState(() { 297 | selectNodes(currentNode, value); 298 | }); 299 | }), 300 | title: new Text(currentNode.title), 301 | subtitle: new Text(currentNode.subTitle), 302 | selected: currentNode.isHilited, 303 | onTap: () { 304 | if (mounted) { 305 | setState(() => currentNode.hilited = !currentNode.isHilited); 306 | if (widget.onHiliteNode != null) widget.onHiliteNode(currentNode); 307 | } 308 | }, 309 | onLongPress: () { 310 | setState(() { 311 | if (widget.onEditNode != null) widget.onEditNode(currentNode); 312 | }); 313 | }, 314 | ), 315 | backgroundColor: Theme.of(context).accentColor.withOpacity(0.015), 316 | children: [ 317 | new Container( 318 | padding: 319 | new EdgeInsets.only(left: Constants.LIST_ITEM_INDENT * 2), 320 | child: new Column( 321 | mainAxisSize: MainAxisSize.min, children: _lstNodes)) 322 | ]); 323 | return _wExpand; 324 | } 325 | } 326 | 327 | class TreeView extends StatefulWidget { 328 | List _roots; 329 | Widget _header; 330 | bool expandIt; 331 | 332 | NodeCallback onSelectNode; 333 | NodeCallback onHiliteNode; 334 | EditNodeCallback onEditNode; 335 | ManageCallback onManage; 336 | 337 | // Constructors 338 | TreeView(TreeNodeData roots) { 339 | _roots = [roots]; 340 | } 341 | 342 | TreeView.multipleRoots(this._roots, {Widget header}) { 343 | _header = header; 344 | } 345 | 346 | // Methods 347 | TreeNodeData getNodeByID(String id) { 348 | for (var root in _roots) { 349 | var found = root.getNodeByID(id); 350 | if (found != null) return found; 351 | } 352 | return null; 353 | } 354 | 355 | TreeViewState _state; 356 | 357 | void toggle(bool bExpand) => _state?.toggle(bExpand); 358 | 359 | void broadcast(Map map) => _state?.broadcast(map); 360 | 361 | void expandOnTo(Map map) => _state?.expandOnTo(map); 362 | 363 | // Overrides 364 | @override 365 | TreeViewState createState() { 366 | _state = new TreeViewState(); 367 | return _state; 368 | } 369 | } 370 | 371 | class TreeViewState extends State { 372 | List _lstNodes; 373 | 374 | void toggle(bool bExpand) { 375 | for (TreeNode node in _lstNodes) { 376 | node?.toggle(bExpand); 377 | } 378 | } 379 | 380 | void broadcast(Map map) { 381 | _lstNodes?.forEach((node) { 382 | TreeNodeData tempNode = map[node._nodeData.id]; 383 | if (tempNode != null) 384 | setState(() => node._nodeData.hilited = tempNode.isHilited); 385 | node.broadcast(map); 386 | }); 387 | } 388 | 389 | void expandOnTo(Map map) { 390 | _openIt(TreeNodeData node) { 391 | // Try to expand the tree to this node 392 | TreeNodeData parent = node.parent; 393 | StackEmul stackWidget = new StackEmul(); 394 | while (parent != null) { 395 | TreeNode parentWidget = parent.bindedWidget as TreeNode; 396 | assert(parentWidget != null); 397 | stackWidget.push(parentWidget); 398 | parent = parent.parent; 399 | } 400 | // Now expand the tree the top-down way 401 | TreeNode openWidget = stackWidget.pop(); 402 | while (openWidget != null) { 403 | openWidget.openIt(); 404 | openWidget = stackWidget.pop(); 405 | } 406 | } 407 | 408 | map.forEach((key, node) { 409 | _openIt(node); 410 | node.hilited = true; 411 | }); 412 | broadcast(map); 413 | } 414 | 415 | @override 416 | void initState() { 417 | super.initState(); 418 | _lstNodes = widget._roots?.length > 0 419 | ? widget._roots.map((TreeNodeData root) { 420 | TreeNode treeNode = 421 | new TreeNode(widget, root, key: new Key(Util.getKey())); 422 | treeNode.onSelectNode = widget.onSelectNode; 423 | treeNode.onEditNode = widget.onEditNode; 424 | treeNode.onManage = widget.onManage; 425 | treeNode.onHiliteNode = widget.onHiliteNode; 426 | // Make sure to store a reference to the associated widget 427 | root.bindedWidget = treeNode; 428 | return treeNode; 429 | }).toList() 430 | : null; 431 | } 432 | 433 | @override 434 | void dispose() { 435 | super.dispose(); 436 | _lstNodes?.clear(); 437 | _lstNodes = null; 438 | } 439 | 440 | @override 441 | Widget build(BuildContext context) { 442 | List _lstWidget = []; 443 | if (widget._header != null) _lstWidget.add(widget._header); 444 | 445 | _lstWidget.addAll(_lstNodes); 446 | 447 | return new ListView(children: _lstWidget); 448 | } 449 | } 450 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: piggy 2 | description: A piggy client for turk server. 3 | 4 | dependencies: 5 | flutter: 6 | sdk: flutter 7 | 8 | # The following adds the Cupertino Icons font to your application. 9 | # Use with the CupertinoIcons class for iOS style icons. 10 | cupertino_icons: ^0.1.0 11 | shared_preferences: "^0.4.3" 12 | validators: ^1.0.0+1 13 | jaguar_serializer: "^2.2.6" 14 | intl: 0.15.7 15 | 16 | dev_dependencies: 17 | flutter_test: 18 | sdk: flutter 19 | 20 | environment: 21 | sdk: ">=2.0.0 <3.0.0" 22 | 23 | 24 | # For information on the generic Dart part of this file, see the 25 | # following page: https://www.dartlang.org/tools/pub/pubspec 26 | 27 | # The following section is specific to Flutter. 28 | flutter: 29 | 30 | # The following line ensures that the Material Icons font is 31 | # included with your application, so that you can use the icons in 32 | # the material Icons class. 33 | uses-material-design: true 34 | 35 | 36 | # To add assets to your application, add an assets section, like this: 37 | # assets: 38 | # - images/a_dot_burr.jpeg 39 | # - images/a_dot_ham.jpeg 40 | 41 | # An image asset can refer to one or more resolution-specific "variants", see 42 | # https://flutter.io/assets-and-images/#resolution-aware. 43 | 44 | # For details regarding adding assets from package dependencies, see 45 | # https://flutter.io/assets-and-images/#from-packages 46 | 47 | # To add custom fonts to your application, add a fonts section here, 48 | # in this "flutter" section. Each entry in this list should have a 49 | # "family" key with the font family name, and a "fonts" key with a 50 | # list giving the asset and other descriptors for the font. For 51 | # example: 52 | # fonts: 53 | # - family: Schyler 54 | # fonts: 55 | # - asset: fonts/Schyler-Regular.ttf 56 | # - asset: fonts/Schyler-Italic.ttf 57 | # style: italic 58 | # - family: Trajan Pro 59 | # fonts: 60 | # - asset: fonts/TrajanPro.ttf 61 | # - asset: fonts/TrajanPro_Bold.ttf 62 | # weight: 700 63 | # 64 | # For details regarding fonts from package dependencies, 65 | # see https://flutter.io/custom-fonts/#from-packages 66 | --------------------------------------------------------------------------------