How to use a Positioned widget in an AppBar - dart

From what I've read this should be possible, but I can't quite get it to work. I have a Stack inside the bottom of an appBar, there's a Positioned list inside of the Stack. Everything seems to be positioned as expected, but the appBar is cropping the list, so the list isn't displaying on top if the appBar and contents of the body.
I'm new to Flutter, but in the world of HTML I'd have an absolutely positioned list and the appBar would be fixed with a z-index higher than the body allowing for the layered effect.
I've tried a bunch of variations, but it just seems the appBar wants to crop it's children. Any help would be appreciated.
Here's a pic of what I'm trying to emulate:
Here's a snippet of code:
return new Scaffold(
appBar: new AppBar(
title: new Row(
children: <Widget>[
new Padding(
padding: new EdgeInsets.only(
right: 10.0,
),
child: new Icon(Icons.shopping_basket),
),
new Text(appTitle)
],
),
bottom: new PreferredSize(
preferredSize: const Size.fromHeight(30.0),
child: new Padding(
padding: new EdgeInsets.only(
bottom: 10.0,
left: 10.0,
right: 10.0,
),
child: new AutoCompleteInput(
key: new ObjectKey('$completionList'),
completionList: completionList,
hintText: 'Add Item',
onSubmit: _addListItem,
),
),
),
),
Update #1
Widget build(BuildContext ctx) {
final OverlayEntry _entry = new OverlayEntry(
builder: (BuildContext context) => const Text('hi')
);
Overlay.of(ctx, debugRequiredFor: widget).insert(_entry);
return new Row(

You won't be able to use a Positioned widget to absolutely position something outside of a clip. (The AppBar requires this clip to follow the material spec, so it likely won't change).
If you need to position something "outside" of the bounds of the widget it is built from, then you need an Overlay. The overlay itself is created by the navigator in MaterialApp, so you can push new elements into it. Some other widgets which use the Overlay are tooltips and popup menus, so you can look at their implementations for more inspiration if you'd like.
final OverlayEntry entry = new OverlayEntry(builder: (BuildContext context) => /* ... */)
Overlay.of(context, debugRequiredFor: widget).insert(_entry);

I have never tested this, but the AppBar has a flexibleSpace property that takes a widget as a parameter. This widget is placed in a space in-between the top of the AppBar (where the title is) and the bottom of the AppBar. If you place your widget in this space instead of the bottom of the AppBar (which should only be used for widgets such as TabBars) your app might work correctly.
Another possible solution is to place your list elements in a DropdownButton instead of in a Stack.
You can find more information on the AppBar here.
EDIT: You might also consider using the Scaffold body to display the suggestions while search is in use.
Also, you may find the source code for the PopupMenuButton useful to solve your problem (since it works in a similar way as your suggestion box). Here is a snippet:
void showButtonMenu() {
final RenderBox button = context.findRenderObject();
final RenderBox overlay = Overlay.of(context).context.findRenderObject();
final RelativeRect position = new RelativeRect.fromRect(
new Rect.fromPoints(
button.localToGlobal(Offset.zero, ancestor: overlay),
button.localToGlobal(button.size.bottomRight(Offset.zero), ancestor: overlay),
),
Offset.zero & overlay.size,
);
showMenu<T>(
context: context,
elevation: widget.elevation,
items: widget.itemBuilder(context),
initialValue: widget.initialValue,
position: position,
)
.then<void>((T newValue) {
if (!mounted)
return null;
if (newValue == null) {
if (widget.onCanceled != null)
widget.onCanceled();
return null;
}
if (widget.onSelected != null)
widget.onSelected(newValue);
});
}

Created an example file that demonstrates what I came up with (at least what's related to this question). Hopefully it saves others from any unnecessary headaches.
import 'dart:async';
import 'package:flutter/material.dart';
String appTitle = 'Overlay Example';
class _CustomDelegate extends SingleChildLayoutDelegate {
final Offset target;
final double verticalOffset;
_CustomDelegate({
#required this.target,
#required this.verticalOffset,
}) : assert(target != null),
assert(verticalOffset != null);
#override
BoxConstraints getConstraintsForChild(BoxConstraints constraints) => constraints.loosen();
#override
Offset getPositionForChild(Size size, Size childSize) {
return positionDependentBox(
size: size,
childSize: childSize,
target: target,
verticalOffset: verticalOffset,
preferBelow: true,
);
}
#override
bool shouldRelayout(_CustomDelegate oldDelegate) {
return
target != oldDelegate.target
|| verticalOffset != oldDelegate.verticalOffset;
}
}
class _CustomOverlay extends StatelessWidget {
final Widget child;
final Offset target;
const _CustomOverlay({
Key key,
this.child,
this.target,
}) : super(key: key);
#override
Widget build(BuildContext context) {
double borderWidth = 2.0;
Color borderColor = Theme.of(context).accentColor;
return new Positioned.fill(
child: new IgnorePointer(
ignoring: false,
child: new CustomSingleChildLayout(
delegate: new _CustomDelegate(
target: target,
verticalOffset: -5.0,
),
child: new Padding(
padding: const EdgeInsets.symmetric(horizontal: 10.0),
child: new ConstrainedBox(
constraints: new BoxConstraints(
maxHeight: 100.0,
),
child: new Container(
decoration: new BoxDecoration(
color: Colors.white,
border: new Border(
right: new BorderSide(color: borderColor, width: borderWidth),
bottom: new BorderSide(color: borderColor, width: borderWidth),
left: new BorderSide(color: borderColor, width: borderWidth),
),
),
child: child,
),
),
),
),
),
);
}
}
class _CustomInputState extends State<_CustomInput> {
TextEditingController _inputController = new TextEditingController();
FocusNode _focus = new FocusNode();
List<String> _listItems;
OverlayState _overlay;
OverlayEntry _entry;
bool _entryIsVisible = false;
StreamSubscription _sub;
void _toggleEntry(show) {
if(_overlay.mounted && _entry != null){
if(show){
_overlay.insert(_entry);
_entryIsVisible = true;
}
else{
_entry.remove();
_entryIsVisible = false;
}
}
else {
_entryIsVisible = false;
}
}
void _handleFocus(){
if(_focus.hasFocus){
_inputController.addListener(_handleInput);
print('Added input handler');
_handleInput();
}
else{
_inputController.removeListener(_handleInput);
print('Removed input handler');
}
}
void _handleInput() {
String newVal = _inputController.text;
if(widget.parentStream != null && _sub == null){
_sub = widget.parentStream.listen(_handleStream);
print('Added stream listener');
}
if(_overlay == null){
final RenderBox bounds = context.findRenderObject();
final Offset target = bounds.localToGlobal(bounds.size.bottomCenter(Offset.zero));
_entry = new OverlayEntry(builder: (BuildContext context){
return new _CustomOverlay(
target: target,
child: new Material(
child: new ListView.builder(
padding: const EdgeInsets.all(0.0),
itemBuilder: (BuildContext context, int ndx) {
String label = _listItems[ndx];
return new ListTile(
title: new Text(label),
onTap: () {
print('Chose: $label');
_handleSubmit(label);
},
);
},
itemCount: _listItems.length,
),
),
);
});
_overlay = Overlay.of(context, debugRequiredFor: widget);
}
setState(() {
// This can be used if the listItems get updated, which won't happen in
// this example, but I figured it was useful info.
if(!_entryIsVisible && _listItems.length > 0){
_toggleEntry(true);
}else if(_entryIsVisible && _listItems.length == 0){
_toggleEntry(false);
}else{
_entry.markNeedsBuild();
}
});
}
void _exitInput(){
if(_sub != null){
_sub.cancel();
_sub = null;
print('Removed stream listener');
}
// Blur the input
FocusScope.of(context).requestFocus(new FocusNode());
// hide the list
_toggleEntry(false);
}
void _handleSubmit(newVal) {
// Set to selected value
_inputController.text = newVal;
_exitInput();
}
void _handleStream(ev) {
print('Input Stream : $ev');
switch(ev){
case 'TAP_UP':
_exitInput();
break;
}
}
#override
void initState() {
super.initState();
_focus.addListener(_handleFocus);
_listItems = widget.listItems;
}
#override
void dispose() {
_inputController.removeListener(_handleInput);
_inputController.dispose();
if(mounted){
if(_sub != null) _sub.cancel();
if(_entryIsVisible){
_entry.remove();
_entryIsVisible = false;
}
if(_overlay != null && _overlay.mounted) _overlay.dispose();
}
super.dispose();
}
#override
Widget build(BuildContext ctx) {
return new Row(
children: <Widget>[
new Expanded(
child: new TextField(
autocorrect: true,
focusNode: _focus,
controller: _inputController,
decoration: new InputDecoration(
border: new OutlineInputBorder(
borderRadius: const BorderRadius.all(
const Radius.circular(5.0),
),
borderSide: new BorderSide(
color: Colors.black,
width: 1.0,
),
),
contentPadding: new EdgeInsets.all(10.0),
filled: true,
fillColor: Colors.white,
),
onSubmitted: _handleSubmit,
),
),
]
);
}
}
class _CustomInput extends StatefulWidget {
final List<String> listItems;
final Stream parentStream;
_CustomInput({
Key key,
this.listItems,
this.parentStream,
}): super(key: key);
#override
State createState() => new _CustomInputState();
}
class HomeState extends State<Home> {
List<String> _overlayItems = [
'Item 01',
'Item 02',
'Item 03',
];
StreamController _eventDispatcher = new StreamController.broadcast();
Stream get _stream => _eventDispatcher.stream;
_onTapUp(TapUpDetails details) {
_eventDispatcher.add('TAP_UP');
}
#override
void initState() {
super.initState();
}
#override
void dispose(){
super.dispose();
_eventDispatcher.close();
}
#override
Widget build(BuildContext context){
return new GestureDetector(
onTapUp: _onTapUp,
child: new Scaffold(
appBar: new AppBar(
title: new Row(
children: <Widget>[
new Padding(
padding: new EdgeInsets.only(
right: 10.0,
),
child: new Icon(Icons.layers),
),
new Text(appTitle)
],
),
bottom: new PreferredSize(
preferredSize: const Size.fromHeight(30.0),
child: new Padding(
padding: new EdgeInsets.only(
bottom: 10.0,
left: 10.0,
right: 10.0,
),
child: new _CustomInput(
key: new ObjectKey('$_overlayItems'),
listItems: _overlayItems,
parentStream: _stream,
),
),
),
),
body: const Text('Body content'),
),
);
}
}
class Home extends StatefulWidget {
#override
State createState() => new HomeState();
}
void main() => runApp(new MaterialApp(
title: appTitle,
home: new Home(),
));

I think the AppBar has a limited space. and placing a list in a AppBar is a bad practice.

Related

Dynamically changing ListView of images using CachedNetworkImage

Essentially I want to do this but instead of updating a single image, I want to update a list of images. I have a ListView of images based off a List<String> of image URLs. See below:
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
void main() => runApp(new MyApp());
class MyApp extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return new MyAppState();
}
}
class MyAppState extends State<MyApp> {
double padding = 5.0;
String one = 'https://SOME_URL_HERE.com';
String two = 'https://SOME_URL_HERE.com';
String three = 'https://SOME_URL_HERE.com';
String four = 'https://SOME_URL_HERE.com';
List urls;
#override
Widget build(BuildContext context) {
urls = List();
urls.add(one);
urls.add(two);
return new MaterialApp(
debugShowCheckedModeBanner: false,
home: new Scaffold(
appBar: new AppBar(
title: new Text("Image list test"),
),
body: showBody(),
),
);
}
Widget showBody() {
return Padding(
padding: EdgeInsets.only(top: 15.0, left: 10.0, right: 10.0),
child: ListView(
children: <Widget>[
showItemImages(),
RaisedButton(
child: Text("Change image"),
onPressed: () {
setState(() {
urls = List();
urls.add(three);
urls.add(four);
});
},
),
Container(
child: Text('urls length: ${urls.length}'),
),
],
),
);
}
Widget showItemImages() {
return Padding(
padding: EdgeInsets.all(padding),
child: Container(
margin: EdgeInsets.symmetric(vertical: 20.0),
height: 200,
child: urls.length > 0 //item.images.length > 0
? getImagesListView(context)
: Container(child: Text('No images yet'))),
);
}
getImagesListView(BuildContext context) {
return ListView.builder(
shrinkWrap: true,
scrollDirection: Axis.horizontal,
itemCount: urls.length,
itemBuilder: (BuildContext context, int index) {
return new Container(
width: 160.0,
child: _sizedContainer(
new CachedNetworkImage(
key: new ValueKey<String>(urls[index]),
imageUrl: urls[index],
//placeholder: (context, url) => new CircularProgressIndicator(),
),
),
);
},
);
}
Widget _sizedContainer(Widget child) {
return new SizedBox(
width: 300.0,
height: 150.0,
child: new Center(
child: child,
),
);
}
}
When I push the button, it should clear the list and add two new elements to it but it is not working
The following 3 lines:
urls = List();
urls.add(one);
urls.add(two);
should not be put inside #override widget build method. putting them there means that you re-initialize urls every time you setstate, you should put these lines inside #override initState() method.

setState from parent widget doesn't update value for child

I have a StatefulWidget where there is a ListView holding several childs widget.
One of the child is a GridView containing some items.
What I would want to achieve is to rebuild this GridView child when a button is pressed from the Parent widget. The button is located in the bottomNavigationBar in the Parent widget.
However, when I pressed the button, it should go to the _resetFilter() method, which works. But the setState() doesn't seem to update the GridView build() method inside Child widget.
class ParentState extends State<Parent> {
// removed for brevity
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(...),
bottomNavigationBar: BottomAppBar(
child: new Row(
children: <Widget>[
Padding(
padding: EdgeInsets.symmetric(vertical: 10.0, horizontal: 5.0),
child: SizedBox(
onPressed: () {
_resetFilter();
},
)
),
],
),
),
body: Container(
child: Form(
key: _formKey,
child: ListView(
children: <Widget>[
Column(
children: <Widget>[
Container(
child: Column(
children: <Widget>[
Container(...), // this works
Column(...),
Container(...), // this works
Container(
child: GridView.count(
// ...
children:
List.generate(oriSkills.length, (int i) {
bool isSkillExist = false;
if (_selectedSkills.contains(rc.titleCase)) {
isSkillExist = true;
} else {
isSkillExist = false;
}
return Child( // this doesn't work
id: oriSkills[i]['id'],
name: oriSkills[i]['description'],
skillSelect: isSkillExist, // this boolean showed correct value from the above logic
onChange: onSkillChange,
);
}),
),
),
],
),
)
],
)
],
)),
),
);
}
void _resetFilter() {
setState(() {
_theValue = 0.0;
searchC.text = "";
_selectedSkills = []; // this is the variable that I'd like the GridView to recreate from.
});
}
}
I tried to print one of the field name inside Child widget, but it always showing the old value instead of the new one.
Even after presing the button, it does passing correct value to ChildState.
class ChildState extends State<Child> {
final String name;
final MyCallbackFunction onChange;
bool skillSelect;
double size = 60.0;
ChildState({this.name, this.skillSelect, this.onChange});
#override
void initState() {
super.initState();
}
#override
void dispose() {
super.dispose();
}
void setSkillLevel() {
setState(() {
if (skillSelect) {
skillSelect = false;
onChange(name, false);
} else {
skillSelect = true;
onChange(name, true);
}
});
}
Color _jobSkillSelect(bool select) {
print(select); // always print old state instead of new state
return select ? Color(MyColor.skillLvlOne) : Color(MyColor.skillDefault);
}
#override
Widget build(BuildContext context) {
return Container(
child: Column(children: <Widget>[
InkResponse(
onTap: setSkillLevel,
child: Container(
height: size,
width: size,
decoration: BoxDecoration(
image: DecorationImage(
colorFilter: ColorFilter.mode(_jobSkillSelect(skillSelect), BlendMode.color),
),
),
)),
]));
}
}
How can I update the Child widget to have the updated value from the Parent widget after reset button is pressed?
You might want to pass the values to the actual Child class. Not to its state.
The class is whats rebuilding once your parent rebuilds. So the new values will be reflected.
So your Child implementation should look something like this (don't forget to replace the onChange Type to your custom Function.
class Child extends StatefulWidget {
final String name;
final Function(void) onChange;
final bool skillSelect;
final double size;
final Function(bool) onSkillLevelChanged;
const Child({Key key, this.name, this.onChange, this.skillSelect, this.size, this.onSkillLevelChanged}) : super(key: key);
#override
_ChildState createState() => _ChildState();
}
class _ChildState extends State<Child> {
Color _jobSkillSelect(bool select) {
print(select); // always print old state instead of new state
return select ? Color(MyColor.skillLvlOne) : Color(MyColor.skillDefault);
}
#override
Widget build(BuildContext context) {
return Container(
child: Column(
children: <Widget>[
InkResponse(
onTap: () {
if (widget.onSkillLevelChanged != null) {
widget.onSkillLevelChanged(!widget.skillSelect);
}
},
child: Container(
height: widget.size,
width: widget.size,
decoration: BoxDecoration(
image: DecorationImage(
colorFilter: ColorFilter.mode(_jobSkillSelect(widget.skillSelect), BlendMode.color),
),
),
)),
],
),
);
}
}
In this case the Child ist not responsible anymore for managing its skillSelect property. It simply calls a Function on its parent. The parent then builds with a new skillSelect boolean.
So you might use this child like this:
return Child( // this doesn't work
id: oriSkills[i]['id'],
name: oriSkills[i]['description'],
skillSelect: oriSkills[i]['isSkillExist'],
onChange: onSkillChange,
onSkillLevelChanged: (newSkillLevel) {
setState(() {
oriSkills[i]['isSkillExist'] = newSkillLevel;
});
},
);

Flutter setState is reverting

When I setState and add an image to the _images array, it appears to have added, but then it quickly reverts:
This form is loosely following Brian Egan's redux architecture example:
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
class Note {
final String comments;
final List<String> images;
Note({
this.comments,
this.images,
});
}
class AddNote extends StatefulWidget {
final Note note;
final bool isEditing;
AddNote({
this.note,
this.isEditing,
});
#override
_AddNoteState createState() => _AddNoteState();
}
class _AddNoteState extends State<AddNote> {
static final _scaffoldKey = GlobalKey<ScaffoldState>();
static final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
List<String> _images;
String _comments;
Note get _note => widget.note;
bool get _isEditing => widget.isEditing;
#override
Widget build(BuildContext context) {
_images = _note.images;
_comments = _note.comments;
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
title: Text(
_isEditing ? "Edit Note" : "Create Note",
),
),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
children: [
_photoPickerField(),
_notesField(),
],
),
),
),
);
}
Widget _photoPickerField() {
return GestureDetector(
onTap: _selectPicture,
child: Row(
children: <Widget>[
Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey, width: 1,),
borderRadius: BorderRadius.all(const Radius.circular(10)),
),
child: SizedBox(child: Icon(Icons.camera_alt), width: 110, height: 110,)
),
] + _imagesRowItems(),
),
);
}
List<Widget> _imagesRowItems() {
return _images.map((image) {
return SizedBox(
height: 110,
width: 110,
child: Image.file(File(image), height: 110, width: 110, fit: BoxFit.cover),
);
}).toList();
}
Future _selectPicture() async {
return ImagePicker.pickImage(source: ImageSource.gallery)
.then((file) {
setState(() {
_images.add(file.path);
});
});
}
Widget _notesField() {
return TextFormField(
maxLines: 2,
keyboardType: TextInputType.multiline,
initialValue: _comments,
onSaved: (String value) => _comments = value,
);
}
}
Note that the comments field keeps its state without issue. How can I add to the images array in a way that will maintain its new state?
Your problem is that you're setting variables inside the build() method of the Widget state, but the build method is called every time you call setState() because your variables have changed, so it resets the images and comments.
To fix it, you should initialize your variables in the initState() method, like this:
class _AddNoteState extends State<AddNote> {
...
#override
void initState() {
super.initState();
_images = _note.images;
_comments = _note.comments;
}
}
And remove them from the build() method.

Maintain state for StatefulWidget during build in Flutter

I have a list of stateful widgets where the user can add, remove, and interact with items in the list. Removing items from the list causes subsequent items in the list to rebuild as they shift to fill the deleted row. This results in a loss of state data for these widgets - though they should remain unaltered other than their location on the screen. I want to be able to maintain state for the remaining items in the list even as their position changes.
Below is a simplified version of my app which consists primarily of a list of StatefulWidgets. The user can add items to the list ("tasks" in my app) via the floating action button or remove them by swiping. Any item in the list can be highlighted by tapping the item, which changes the state of the background color of the item. If multiple items are highlighted in the list, and an item (other than the last item in the list) is removed, the items that shift to replace the removed item lose their state data (i.e. the background color resets to transparent). I suspect this is because _taskList rebuilds since I call setState() to update the display after a task is removed. I want to know if there is a clean way to maintain state data for the remaining tasks after a task is removed from _taskList.
void main() => runApp(new TimeTrackApp());
class TimeTrackApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Time Tracker',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new TimeTrackHome(title: 'Task List'),
);
}
}
class TimeTrackHome extends StatefulWidget {
TimeTrackHome({Key key, this.title}) : super(key: key);
final String title;
#override
_TimeTrackHomeState createState() => new _TimeTrackHomeState();
}
class _TimeTrackHomeState extends State<TimeTrackHome> {
TextEditingController _textController;
List<TaskItem> _taskList = new List<TaskItem>();
void _addTaskDialog() async {
_textController = TextEditingController();
await showDialog(
context: context,
builder: (_) => new AlertDialog(
title: new Text("Add A New Task"),
content: new TextField(
controller: _textController,
decoration: InputDecoration(
border: InputBorder.none, hintText: 'Enter the task name'),
),
actions: <Widget>[
new FlatButton(
onPressed: () => Navigator.pop(context),
child: const Text("CANCEL")),
new FlatButton(
onPressed: (() {
Navigator.pop(context);
_addTask(_textController.text);
}),
child: const Text("ADD"))
],
));
}
void _addTask(String title) {
setState(() {
// add the new task
_taskList.add(TaskItem(
name: title,
));
});
}
#override
void initState() {
_taskList = List<TaskItem>();
super.initState();
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Align(
alignment: Alignment.topCenter,
child: ListView.builder(
padding: EdgeInsets.all(0.0),
itemExtent: 60.0,
itemCount: _taskList.length,
itemBuilder: (BuildContext context, int index) {
if (index < _taskList.length) {
return Dismissible(
key: ObjectKey(_taskList[index]),
onDismissed: (direction) {
if(this.mounted) {
setState(() {
_taskList.removeAt(index);
});
}
},
child: _taskList[index],
);
}
}),
),
floatingActionButton: new FloatingActionButton(
onPressed: _addTaskDialog,
tooltip: 'Click to add a new task',
child: new Icon(Icons.add),
),
);
}
}
class TaskItem extends StatefulWidget {
final String name;
TaskItem({Key key, this.name}) : super(key: key);
TaskItem.from(TaskItem other) : name = other.name;
#override
State<StatefulWidget> createState() => new _TaskState();
}
class _TaskState extends State<TaskItem> {
static final _taskFont =
const TextStyle(fontSize: 26.0, fontWeight: FontWeight.bold);
Color _color = Colors.transparent;
void _highlightTask() {
setState(() {
if(_color == Colors.transparent) {
_color = Colors.greenAccent;
}
else {
_color = Colors.transparent;
}
});
}
#override
Widget build(BuildContext context) {
return Column(children: <Widget>[
Material(
color: _color,
child: ListTile(
title: Text(
widget.name,
style: _taskFont,
textAlign: TextAlign.center,
),
onTap: () {
_highlightTask();
},
),
),
Divider(
height: 0.0,
),
]);
}
}
I ended up solving the problem by creating an intermediate class which contains a reference to the StatefulWidget and transferred over all the state variables. The State class accesses the state variables through a reference to the intermediate class. The higher level widget that contained and managed a List of the StatefulWidget now access the StatefulWidget through this intermediate class. I'm not entirely confident in the "correctness" of my solution as I haven't found any other examples of this, so I am still open to suggestions.
My intermediate class is as follows:
class TaskItemData {
// StatefulWidget reference
TaskItem widget;
Color _color = Colors.transparent;
TaskItemData({String name: "",}) {
_color = Colors.transparent;
widget = TaskItem(name: name, stateData: this,);
}
}
My StatefulWidget and its corresponding State classes are nearly unchanged, except that the state variables no longer reside in the State class. I also added a reference to the intermediate class inside my StatefulWidget which gets initialized in the constructor. Previous uses of state variables in my State class now get accessed through the reference to the intermediate class. The modified StatefulWidget and State classes is as follows:
class TaskItem extends StatefulWidget {
final String name;
// intermediate class reference
final TaskItemData stateData;
TaskItem({Key key, this.name, this.stateData}) : super(key: key);
#override
State<StatefulWidget> createState() => new _TaskItemState();
}
class _TaskItemState extends State<TaskItem> {
static final _taskFont =
const TextStyle(fontSize: 26.0, fontWeight: FontWeight.bold);
void _highlightTask() {
setState(() {
if(widget.stateData._color == Colors.transparent) {
widget.stateData._color = Colors.greenAccent;
}
else {
widget.stateData._color = Colors.transparent;
}
});
}
#override
Widget build(BuildContext context) {
return Column(children: <Widget>[
Material(
color: widget.stateData._color,
child: ListTile(
title: Text(
widget.name,
style: _taskFont,
textAlign: TextAlign.center,
),
onTap: () {
_highlightTask();
},
),
),
Divider(
height: 0.0,
),
]);
}
}
The widget containing the List of TaskItem objects has been replaced with a List of TaskItemData. The ListViewBuilder child now accesses the TaskItem widget through the intermediate class (i.e. child: _taskList[index], has changed to child: _taskList[index].widget,). It is as follows:
class _TimeTrackHomeState extends State<TimeTrackHome> {
TextEditingController _textController;
List<TaskItemData> _taskList = new List<TaskItemData>();
void _addTaskDialog() async {
_textController = TextEditingController();
await showDialog(
context: context,
builder: (_) => new AlertDialog(
title: new Text("Add A New Task"),
content: new TextField(
controller: _textController,
decoration: InputDecoration(
border: InputBorder.none, hintText: 'Enter the task name'),
),
actions: <Widget>[
new FlatButton(
onPressed: () => Navigator.pop(context),
child: const Text("CANCEL")),
new FlatButton(
onPressed: (() {
Navigator.pop(context);
_addTask(_textController.text);
}),
child: const Text("ADD"))
],
));
}
void _addTask(String title) {
setState(() {
// add the new task
_taskList.add(TaskItemData(
name: title,
));
});
}
#override
void initState() {
_taskList = List<TaskItemData>();
super.initState();
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Align(
alignment: Alignment.topCenter,
child: ListView.builder(
padding: EdgeInsets.all(0.0),
itemExtent: 60.0,
itemCount: _taskList.length,
itemBuilder: (BuildContext context, int index) {
if (index < _taskList.length) {
return Dismissible(
key: ObjectKey(_taskList[index]),
onDismissed: (direction) {
if(this.mounted) {
setState(() {
_taskList.removeAt(index);
});
}
},
child: _taskList[index].widget,
);
}
}),
),
floatingActionButton: new FloatingActionButton(
onPressed: _addTaskDialog,
tooltip: 'Click to add a new task',
child: new Icon(Icons.add),
),
);
}
}

flutter: CircleAvatar with fallback text

I'm learning Flutter and would like to make a Widget just like the built-in CircleAvatar. However, I would like the behaviour to be
specify both an Image (NetworkImage) and initials (ie, BB)
while the image isn't loaded, show the initials
if the image does load, show the image and remove the initials
The following code sort of works, but when used in the Chat demo it falls apart as multiple MyAvatars are added.
Breakpointing on initState shows that it is always called with the first message text that is entered - not what I expected.
It also flickers as images "reload". It appears that the widgets are being reused in a way I don't understand.
class MyAvatar extends StatefulWidget {
NetworkImage image;
MyAvatar({this.text}) {
debugPrint("MyAvatar " + this.text);
if (text.contains('fun')) {
this.image = new NetworkImage("https://cdn3.iconfinder.com/data/icons/minicons-for-web-sites/24/minicons2-14-512.png");
}
}
final String text;
#override
MyAvatarState createState() {
return new MyAvatarState();
}
}
class MyAvatarState extends State<MyAvatar> {
bool showImage = false;
#override
initState() {
super.initState();
if (widget.image != null) {
var completer = widget.image.load(widget.image);
completer.addListener((info, sync) {
setState(() {
showImage = true;
});
});
}
}
#override
Widget build(BuildContext context) {
return !showImage ? new CircleAvatar(radius: 40.0, child: new Text(widget.text[0]))
: new CircleAvatar(radius: 40.0, backgroundImage: widget.image);
}
}
I'm still having trouble - full code
import 'package:flutter/material.dart';
// Modify the ChatScreen class definition to extend StatefulWidget.
class ChatScreen extends StatefulWidget { //modified
ChatScreen() {
debugPrint("ChatScreen - called on hot reload");
}
#override //new
State createState() {
debugPrint("NOT on hot reload");
return new ChatScreenState();
} //new
}
// Add the ChatScreenState class definition in main.dart.
class ChatScreenState extends State<ChatScreen> {
final List<ChatMessage> _messages = <ChatMessage>[];
final TextEditingController _textController = new TextEditingController(); //new
ChatScreenState() {
debugPrint("ChatScreenState - not called on hot reload");
}
#override //new
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(title: new Text("Friendlychat")),
body: new Column( //modified
children: <Widget>[ //new
new Flexible( //new
child: new ListView.builder( //new
padding: new EdgeInsets.all(8.0), //new
reverse: true, //new
itemBuilder: (_, int index) => _messages[index], //new
itemCount: _messages.length, //new
) //new
), //new
new Divider(height: 1.0), //new
new Container( //new
decoration: new BoxDecoration(
color: Theme.of(context).cardColor), //new
child: _buildTextComposer(), //modified
), //new
] //new
), //new
);
}
Widget _buildTextComposer() {
return new IconTheme(
data: new IconThemeData(color: Theme
.of(context)
.accentColor),
child:
new Container(
margin: const EdgeInsets.symmetric(horizontal: 8.0),
child: new Row(
children: <Widget>[
new Container( //new
margin: new EdgeInsets.symmetric(horizontal: 4.0), //new
child: new IconButton( //new
icon: new Icon(Icons.send),
onPressed: () =>
_handleSubmitted(_textController.text)), //new
),
new Flexible(
child: new TextField(
controller: _textController,
onSubmitted: _handleSubmitted,
decoration: new InputDecoration.collapsed(
hintText: "Send a message"),
)
),
])
)
);
}
void _handleSubmitted(String text) {
_textController.clear();
ChatMessage message = new ChatMessage(text: text);
setState(() {
_messages.insert(0, message);
});
}
}
const String _name = "Hardcoded Name";
class ChatMessage extends StatelessWidget {
ChatMessage({this.text, this.image, this.useImage});
final String text;
final NetworkImage image;
final Map useImage;
#override
Widget build(BuildContext context) {
var use = true; //useImage != null && useImage['use'];
var image = new NetworkImage("https://cdn3.iconfinder.com/data/icons/minicons-for-web-sites/24/minicons2-14-512.png");
if (text.contains('bad')) {
image = new NetworkImage("https://cdn3.iconfinder.com/data/icons/minicons-for-web-sites/24/minicons2-14-512.pngz");
}
return new Container(
margin: const EdgeInsets.symmetric(vertical: 10.0),
child: new Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
new Container(
margin: const EdgeInsets.only(right: 16.0),
child : new CustomCircleAvatar(initials: text[0], myImage: image)
),
new Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
new Text(_name, style: Theme.of(context).textTheme.subhead),
new Container(
margin: const EdgeInsets.only(top: 5.0),
child: new Text(text),
),
],
),
],
),
);
}
}
class CustomCircleAvatar extends StatefulWidget {
NetworkImage myImage;
String initials;
CustomCircleAvatar({this.myImage, this.initials}) {
debugPrint(initials);
}
#override
_CustomCircleAvatarState createState() => new _CustomCircleAvatarState();
}
class _CustomCircleAvatarState extends State<CustomCircleAvatar>{
bool _checkLoading = true;
#override
void initState() {
if (widget.myImage != null) {
widget.myImage.resolve(new ImageConfiguration()).addListener((image, sync) {
if (mounted && image != null) {
setState(() {
_checkLoading = false;
});
}
});
}
}
#override
Widget build(BuildContext context) {
return _checkLoading == true ? new CircleAvatar(child: new Text(widget.initials))
: new CircleAvatar(backgroundImage: widget.myImage);
}
}
Enter 'fun' as a message, then 'bad' as the second -
image
The idea is that depending on what you enter, different images might load (or not). In the 'failed to load' case, the initials should remain.
You can achieve this functionality by adding a listener to ImageStream that you can obtain from ImageConfiguration,
Here, I am feeding the same data to my ListView you can of course customize this yourself by adding a List of images and initials as a field in any class and use ListView.builder instead to be able to loop on them by index.
class CustomCircleAvatar extends StatefulWidget {
NetworkImage myImage;
String initials;
CustomCircleAvatar({this.myImage, this.initials});
#override
_CustomCircleAvatarState createState() => new _CustomCircleAvatarState();
}
class _CustomCircleAvatarState extends State<CustomCircleAvatar>{
bool _checkLoading = true;
#override
void initState() {
widget.myImage.resolve(new ImageConfiguration()).addListener((_, __) {
if (mounted) {
setState(() {
_checkLoading = false;
});
}
});
}
#override
Widget build(BuildContext context) {
return _checkLoading == true ? new CircleAvatar(
child: new Text(widget.initials)) : new CircleAvatar(
backgroundImage: widget.myImage,);
}
}
Now you can use it like this:
void main() {
runApp(new MaterialApp (home: new MyApp()));
}
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => new _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(title: new Text("Custom Circle Avatar"),),
body: new ListView(children: new List.generate(20, (int index) {
return new Container(
height: 100.0,
width: 100.0,
child: new CustomCircleAvatar(myImage: new NetworkImage(
"https://www.doginni.cz/front_path/images/dog_circle.png"),
initials: "Dog",
),
);
}),),
);
}
}
This works really well and easy. Use the CachetNetworkImage and build the appropriate CircleAvatar.
return CachedNetworkImage(
httpHeaders: headers,
imageUrl: general.HOST + 'api/media/v2/' + id,
imageBuilder: (context, imageProvider) => new CircleAvatar(
radius: radius,
backgroundImage: imageProvider,
backgroundColor: backgroundColor),
errorWidget: (context, url, error) => CircleAvatar(
backgroundColor: backgroundColor,
radius: radius,
child: new Text(initials, style: textStyle,)),
);
The answer from #aziza was really the only one I could find on the topic for a while and it took me a while to read it and understand. I tried implementing it and there were some issues though I did get it to work eventually. I think I have a more readable (for me at least!)/up to date answer that might help someone stumbling upon this question:
class FallBackAvatar extends StatefulWidget {
final AssetImage image;
final String initials;
final TextStyle textStyle;
final Color circleBackground;
FallBackAvatar({#required this.image, #required this.initials, #required this.circleBackground, #required this.textStyle});
#override
_FallBackAvatarState createState() => _FallBackAvatarState();
}
class _FallBackAvatarState extends State<FallBackAvatar> {
bool _checkLoading = true;
#override
initState() {
super.initState();
// Add listeners to this class
ImageStreamListener listener = ImageStreamListener(_setImage, onError: _setError);
widget.image.resolve(ImageConfiguration()).addListener(listener);
}
void _setImage(ImageInfo image, bool sync) {
setState(() => _checkLoading = false);
//DO NOT DISPOSE IF IT WILL REBUILD (e.g. Sliver/Builder ListView)
dispose();
}
void _setError(dynamic dyn, StackTrace st) {
setState(() => _checkLoading = true);
dispose();
}
#override
Widget build(BuildContext context) {
return _checkLoading == true ? new CircleAvatar(
backgroundColor: widget.circleBackground,
child: new Text(widget.initials, style: widget.textStyle)) : new CircleAvatar(
backgroundImage: widget.image,
backgroundColor: widget.circleBackground,);
}
}
A couple of points, I'm manually disposing because I know after this there should be no more rebuilds (did you get the image? good! no more rebuilds unless you are part of a sliver or something OR did the image fail to load? well that's it then - no more rebuilds). This also handles the error case where the AssetImage (in my case, its Asset image but you could use any kind of image provider) is not there for whatever reason.
Second edit, because I have personal problems best left out of this answer. So I noticed that there was a slight delay in loading the profile images (like a second). But then the images came flooding in. Didn't like that transition so here is one with an AnimatedSwitcher:
class FallBackAvatar extends StatefulWidget {
final AssetImage image;
final String initials;
final TextStyle textStyle;
final Color circleBackground;
final double radius;
final int msAnimationDuration;
FallBackAvatar({#required this.image, #required this.initials, #required this.circleBackground, #required this.textStyle, #required this.radius, this.msAnimationDuration});
#override
_FallBackAvatarState createState() => _FallBackAvatarState();
}
class _FallBackAvatarState extends State<FallBackAvatar> {
bool _imgSuccess = false;
#override
initState() {
super.initState();
// Add listeners to this class
ImageStreamListener listener = ImageStreamListener(_setImage, onError: _setError);
widget.image.resolve(ImageConfiguration()).addListener(listener);
}
void _setImage(ImageInfo image, bool sync) {
setState(() => _imgSuccess = true);
}
void _setError(dynamic dyn, StackTrace st) {
setState(() => _imgSuccess = false);
dispose();
}
Widget _fallBackAvatar() {
return Container(
height: widget.radius*2,
width: widget.radius*2,
decoration: BoxDecoration(
color: widget.circleBackground,
borderRadius: BorderRadius.all(Radius.circular(widget.radius))
),
child: Center(child: Text(widget.initials, style: widget.textStyle))
);
}
Widget _avatarImage() {
return CircleAvatar(
backgroundImage: widget.image,
backgroundColor: widget.circleBackground
);
}
#override
Widget build(BuildContext context) {
return AnimatedSwitcher(
duration: Duration(milliseconds: widget.msAnimationDuration ?? 500),
child: _imgSuccess ? _avatarImage() : _fallBackAvatar(),
);
}
}
Actually the code can be even simpler:
if you want to put a text when the image is unavailable you should simply use foregroundImage instead of backgroundImage.
The text will displayed by default, when the image is loaded it will cover the text without having to deal with image loading status etc.
If you need to know if the image had an error you can intercept it with onForegroundImageError.
Example function:
Widget CircleAvatarTest(
{String? imageUrl,
String? text,
double radius = 35,
Color? backgroundColor}) {
return CircleAvatar(
radius: radius,
child: (text != null)
? Center(
child: Text(text,
style: TextStyle(
color: Colors.white,
fontSize: radius * 2 / text.length - 10,
)),
)
: null,
foregroundImage: imageUrl == null ? null : NetworkImage(imageUrl),
backgroundColor: backgroundColor,
//onForegroundImageError: (e,trace){/*....*/},
);
}
Here is the sample with stacked architecture where fallback is person icon.
ViewBuilder and ViewModel are just extended widgets from stacked architecture alternatives. #swidget is functional widget. You can achieve the same functionality via StatefulWidget.
#swidget
Widget avatarView({String userId, double radius = 24}) =>
ViewBuilder<AvatarViewModel>(
viewModelBuilder: () => AvatarViewModel(),
builder: (model) => CircleAvatar(
radius: radius,
backgroundColor: CColors.blackThird,
backgroundImage: NetworkImage(
Config.photoUrl + userId ?? userService.id,
),
child: model.isFailed ? Icon(EvaIcons.person, size: radius) : null,
onBackgroundImageError: (e, _) => model.isFailed = e != null,
),
);
class AvatarViewModel extends ViewModel {
bool _isFailed = false;
bool get isFailed => _isFailed;
set isFailed(bool isFailed) {
_isFailed = isFailed;
notifyListeners();
}
}

Resources