So at the moment, I have a number represented by counter. The number is incremental and continues to change. My goal is to add commas every 3 digits to make the number more readable in my app. The only issue that I am running into is that
String number = counter.replaceAllMapped(new RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
(Match m) => '${m[1]},');
refuses to take the integer counter as an int. How do I convert int counter into String middle before running it through
String number = middle.replaceAllMapped(new RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
(Match m) => '${m[1]},');
My goal is this:
counter = 1000;
middle = "1000";
number = "1,000";
Will my current solution even work? If not, how do I change my code to meet my goal?
Update:
Here is my code:
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int counter = 0;
void _incrementCounter() {
setState(() {
if (counter > 99999999) {
//WinLandingPage();
} else if (counter >= 99000000) {
counter++;
} else if (counter > 990000) {
counter += 1000000;
} else if (counter > 9900) {
counter += 10000;
} else if (counter > 99) {
counter += 100;
} else counter++;
});
}
//BELOW is where the format is taking place, and I have no idea how to
//get this part working. This is my broken attempt.
var f = new NumberFormat("#,###,###,###", "en_US");
var number = f.format(counter);
//What am I doing wrong here?
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar (
title: new Text(widget.title),
),
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text(
'Try to get $number to 100,000,000.',
),
new Text(
'$number',
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: new FloatingActionButton(
onPressed: _incrementCounter,
backgroundColor: Colors.pinkAccent,
tooltip: 'Increment',
child: new Icon(Icons.add),
),
);
}
}
What am I doing wrong in this piece of code? I am super unfamiliar with Dart, but I am doing my best to figure it out from trial, error, and support from you guys.
You can use the NumberFormat from the intl package to do that
An example from the README.md
var f = new NumberFormat("###.0#", "en_US");
print(f.format(12.345));
==> 12.34
Your example could be
var f = new NumberFormat("#,###", "en_US");
print(f.format(1000));
==> 1,000
Related
I have a dashboard, represented by grid, that supposed to delete item on long press event (using flutter_bloc), but it deletes last item instead of selected. All debug prints show, that needed element actually removed from list, but view layer still keeps it.
My build function code:
Widget build(BuildContext context) {
double pyxelRatio = MediaQuery.of(context).devicePixelRatio;
double width = MediaQuery.of(context).size.width * pyxelRatio;
return BlocProvider(
bloc: _bloc,
child: BlocBuilder<Request, DataState>(
bloc: _bloc,
builder: (context, state) {
if (state is EmptyDataState) {
print("Uninit");
return Center(
child: CircularProgressIndicator(),
);
}
if (state is ErrorDataState) {
print("Error");
return Center(
child: Text('Something went wrong..'),
);
}
if (state is LoadedDataState) {
print("empty: ${state.contracts.isEmpty}");
if (state.contracts.isEmpty) {
return Center(
child: Text('Nothing here!'),
);
} else{
print("items count: ${state.contracts.length}");
print("-------");
for(int i = 0; i < state.contracts.length; i++){
if(state.contracts[i].isFavorite)print("fut:${state.contracts[i].name} id:${state.contracts[i].id}");
}
print("--------");
List<Widget> testList = new List<Widget>();
for(int i = 0; i < state.contracts.length; i++){
if(state.contracts[i].isFavorite) testList.add(
InkResponse(
enableFeedback: true,
onLongPress: (){
showShortToast();
DashBLOC dashBloc = BlocProvider.of<DashBLOC>(context);
dashBloc.dispatch(new UnfavRequest(state.contracts[i].id));
},
onTap: onTap,
child:DashboardCardWidget(state.contracts[i])
)
);
}
return GridView.count(
crossAxisCount: width >= 900 ? 2 : 1,
padding: const EdgeInsets.all(2.0),
children: testList
);
}
}
})
);
}
full class code and dashboard bloc
Looks like grid rebuilds itself, but don't rebuild its tiles.
How can I completely update grid widget with all its subwidgets?
p.s i've spent two days fixing it, pls help
I think you should use a GridView.builderconstructor to specify a build function which will update upon changes in the list of items, so when any update occur in your data the BlocBuilder will trigger the build function inside yourGridView.
I hope this example makes it more clear.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Test(),
);
}
}
class Test extends StatefulWidget {
#override
_TestState createState() => _TestState();
}
class _TestState extends State<Test> {
List<int> testList = List<int>();
#override
void initState() {
for (int i = 0; i < 20; i++) {
testList.add(i);
}
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
floatingActionButton: FloatingActionButton(
//Here we can remove an item from the list and using setState
//or BlocBuilder will rebuild the grid with the new list data
onPressed: () => setState(() {testList.removeLast();})
),
body: GridView.builder(
// You must specify the items count of your grid
itemCount: testList.length,
// You must use the GridDelegate to specify row item count
// and spacing between items
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 5,
childAspectRatio: 1.0,
crossAxisSpacing: 1.0,
mainAxisSpacing: 1.0,
),
// Here you can build your desired widget which will rebuild
// upon changes using setState or BlocBuilder
itemBuilder: (BuildContext context, int index) {
return Text(
testList[index].toString(),
textScaleFactor: 1.3,
);
},
),
);
}
}
Your code is always sending the last value of int i.
So instead of
for(int i = 0; i < state.contracts.length; i++){
if(state.contracts[i].isFavorite) testList.add(
InkResponse(
enableFeedback: true,
onLongPress: (){
showShortToast();
DashBLOC dashBloc = BlocProvider.of<DashBLOC>(context);
dashBloc.dispatch(new UnfavRequest(state.contracts[i].id));
},
onTap: onTap,
child:DashboardCardWidget(state.contracts[i])
)
);
Do
List<Widget> testList = new List<Widget>();
state.contracts.forEach((contract){
if(contract.isFavorite) testList.add(
InkResponse(
enableFeedback: true,
onLongPress: (){
showShortToast();
DashBLOC dashBloc = BlocProvider.of<DashBLOC>(context);
dashBloc.dispatch(new UnfavRequest(contract.id));
},
onTap: onTap,
child:DashboardCardWidget(contract)
)
));
Is it actually rebuilds? I'm just don't understand why you use the State with BLoC. Even if you use the State you should call the setState() method to update the widget with new data.
On my opinion the best solution to you will be to try to inherit your widget from StatelessWidget and call the dispatch(new UpdateRequest()); in the DashBLOC constructor.
Also always keep in mind this link about the bloc, there are lots of examples:
https://felangel.github.io/bloc/#/
just give children a key
return GridView.builder(
itemCount: children.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(3),
itemBuilder: (context, index) {
return Container(
key: ValueKey(children.length+index),
);
});
I am trying to create a quiz application and in my playQuiz, after the player has answered the correct answer then new answers would be generated. I have tried to make a function "foo" and call it so it would change the image and texts in the buttons. Still, nothing changes, this only changes when I rotate my screen. The code is short and easy to understand.
class Game extends StatelessWidget {
final ForceSelection forceSelection;
Game({Key key, #required this.forceSelection}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: forceSelection.langSelection, // TODO should work and change later
theme: new ThemeData(
primaryColor: Colors.white,
),
home: PlayQuiz(),
);
}
}
class PlayQuizState extends State<PlayQuiz> {
final ForceSelection forceSelection; // TODO Does this get forceSelection
List<String> groundForce = [ 'sotamies', 'aliupseerioppilas', 'korpraali', 'alikersantti', 'upseerioppilas', 'kersantti', 'upseerikokelas', 'ylikersantti', 'vääpeli', 'ylivääpeli', 'sotilasmestari', 'vänrikki', 'luutnantti', 'yliluutnantti', 'kapteeni', 'majuri', 'everstiluutnantti', 'eversti', 'prikaatikenraali', 'kenraalimajuri', 'kenraaliluutnantti', 'kenraali'];
List<int> randomList = Helper.randomAnswers();
static var rng = new Random();
var corrAnsIndex = rng.nextInt(3);
int points = 0;
String quizImage = "";
String debugText = "";
PlayQuizState(this.forceSelection); // TODO Does this get langSelection
void _startGame() {
quizImage = 'images/' + groundForce[randomList[corrAnsIndex]].replaceAll('ä','a') + '.jpg';
}
void _nextGame() {
setState(() {
points += 1;
randomList = Helper.randomAnswers();
corrAnsIndex = rng.nextInt(3);
quizImage = 'images/' + groundForce[randomList[corrAnsIndex]].replaceAll('ä','a') + '.jpg';
debugText = forceSelection.langSelection + randomList.toString() + " " + groundForce[randomList[corrAnsIndex]].replaceAll('ä','a');
// TODO edited debug text and next is to check if langSelection is fin
});
}
void _endGame() {
setState(() {
//TODO Ask for name and send to database
});
}
#override
Widget build(BuildContext context) {
_startGame();
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Image.asset(quizImage),
Text("randomList: " + randomList.toString()),
Text("Oikea vastaus: " +
groundForce[randomList[corrAnsIndex]] +
" corrAnsIndex: " +
corrAnsIndex.toString()),
Text(debugText + " points: " + points.toString()),
//Text("Voima valinta: " + forceSelection = Game.forceSelection),
RaisedButton(
child: Text(groundForce[randomList[0]]),
onPressed: () {
if (0 == corrAnsIndex) {
_nextGame();
//this.foo();
}
}),
RaisedButton(
child: Text(groundForce[randomList[1]]),
onPressed: () {
if (1 == corrAnsIndex) {
_nextGame();
}
}),
RaisedButton(
child: Text(groundForce[randomList[2]]),
onPressed: () {
if (2 == corrAnsIndex) {
_nextGame();
}
}),
RaisedButton(
child: Text(groundForce[randomList[3]]),
onPressed: () {
if (3 == corrAnsIndex) {
_nextGame();
}
}),
],
)),
);
}
}
class PlayQuiz extends StatefulWidget {
#override
PlayQuizState createState() => new PlayQuizState(ForceSelection("","")); //TODO how to pass vars to ""
}
You must use setState to rebuild the widget in order to update the view, also change your widget into a StatefulWidget, not StatelessWidget.
Docs: https://docs.flutter.io/flutter/widgets/State/setState.html
https://flutterdoc.com/stateful-or-stateless-widgets-42a132e529ed
I suggest you should spend more time to learn some fundamental ideas of flutter then continue your development.
I have a form with some inputs. I am using a GlobalKey<FormState> to handle submissions and validation and so on.
One of the fields is supposed to take a double input, so I validate that by trying to parse the input value to double like so :
return TextFormField(
decoration: InputDecoration(labelText: 'Price'),
keyboardType: TextInputType.number,
validator: (String value) {
double _parsedValue = double.tryParse(value);
if (_parsedValue == null) {
return "Please input a number";
}
},
onSaved: (String value) {
setState(() {
_price = double.parse(value);
});
},
);
Now that works as expected. However, if the user inputs for example 9,99 that would fail, because the parse expects 9.99 .
What I'm trying to do is, when the validator is called, I'd like to check the input string for any commas, and then if they are present, replace them with dots instead, and update the form value accordingly.
My question is - can we actually update the form state from within validators?
I think maybe what you need is a TextInputFormatter.
Here is a link to the docs https://docs.flutter.io/flutter/services/TextInputFormatter-class.html
There are pre-existing formatters you can use as a reference to convert comma's to dots.
I don't think you need to update the state in the validator. I would use only the save event to update the state. This way it gets very clear where the state is updated.
I believe nothing forbids you to update the state in the validate, but maybe it would get less organized. :)
Solution that do not exactly answer your question
I guess the best way to accomplish what you need would be using a TextInputFormatter with a WhitelistingTextInputFormatter, check it out:
Note the TextInputType.numberWithOptions(decimal: true) and that if the user pastes "-100,00" , it would become 100.0 - which for a price would be fine, but not for double values in general.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'ValidatorState',
theme: ThemeData(primarySwatch: Colors.yellow),
home: MyFormPage(),
);
}
}
class MyFormPage extends StatefulWidget {
#override
_MyFormPageState createState() => _MyFormPageState();
}
class _MyFormPageState extends State<MyFormPage> {
final _formKey = GlobalKey<FormState>();
double _price;
void _save() {
if (_formKey.currentState.validate()) {
_formKey.currentState.save();
Scaffold.of(_formKey.currentContext)
.showSnackBar(SnackBar(content: Text('New price defined! ($_price)')));
}
}
Widget _buildForm(BuildContext context) {
return Container(
padding: EdgeInsets.all(10.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
TextFormField(
inputFormatters: <TextInputFormatter>[
WhitelistingTextInputFormatter(RegExp("[0-9.]"))
],
decoration: InputDecoration(labelText: 'Price'),
keyboardType: TextInputType.numberWithOptions(decimal: true),
validator: (String value) {
double _parsedValue = double.tryParse(value);
if (_parsedValue == null) {
return "Please input a valid number";
}
if (_parsedValue == 0.0) {
return "Please input a valid price";
}
},
onSaved: (String value) {
setState(() {
_price = double.tryParse(value);
});
},
),
Text(""),
RaisedButton(
child: Text("Save"),
color: Theme.of(context).primaryColor,
textColor: Theme.of(context).primaryTextTheme.title.color,
onPressed: _save,
),
Text(""),
TextFormField(
decoration: InputDecoration(labelText: 'Copy and Paste area'),
),
],
),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Validator State"),
),
body: Form(
key:_formKey,
child: _buildForm(context),
),
);
}
}
Solution that answers your question
However, that is not exactly what you described. You want to automatically replace , to .. I would avoid doing that, as 1,234.56 would translate to 1.234.56, which is invalid. If you only strip out the commas, you end up with 1234.56 which is valid.
If you really want to do as you said, you have to use a TextEditingController and a function to normalize the text data. I've made the example below, check it out - specially the _priceController and the _parsePrice.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'ValidatorState',
theme: ThemeData(primarySwatch: Colors.yellow),
home: MyFormPage(),
);
}
}
class MyFormPage extends StatefulWidget {
#override
_MyFormPageState createState() => _MyFormPageState();
}
class _MyFormPageState extends State<MyFormPage> {
final _formKey = GlobalKey<FormState>();
TextEditingController _priceController;
double _price;
#override
void initState() {
super.initState();
_priceController = TextEditingController();
}
#override
void dispose() {
_priceController?.dispose();
super.dispose();
}
void _save() {
if (_formKey.currentState.validate()) {
_formKey.currentState.save();
Scaffold.of(_formKey.currentContext)
.showSnackBar(SnackBar(content: Text('New price defined! ($_price)')));
}
}
double _parsePrice(String text) {
var buffer = new StringBuffer();
text.runes.forEach((int rune) {
// acceptable runes are . or 0123456789
if (rune == 46 || (rune >= 48 && rune <= 57)) buffer.writeCharCode(rune);
// if we find a , we replace with a .
if (rune == 44) buffer.writeCharCode(46);
});
return double.tryParse(buffer.toString());
}
Widget _buildForm(BuildContext context) {
return Container(
padding: EdgeInsets.all(10.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
TextFormField(
controller: _priceController,
decoration: InputDecoration(labelText: 'Price'),
keyboardType: TextInputType.numberWithOptions(decimal: true),
validator: (String value) {
double _parsedValue = _parsePrice(value);
if (_parsedValue == null) {
return "Please input a valid number";
}
if (_parsedValue == 0.0) {
return "Please input a valid price";
}
},
onSaved: (String value) {
setState(() {
_price = _parsePrice(value);
_priceController.text = _price.toString();
});
},
),
Text(""),
RaisedButton(
child: Text("Save"),
color: Theme.of(context).primaryColor,
textColor: Theme.of(context).primaryTextTheme.title.color,
onPressed: _save,
),
],
),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Validator State"),
),
body: Form(
key:_formKey,
child: _buildForm(context),
),
);
}
}
hi did you get a fix for this?
I would rethink your strategy for this issue.
Maybe what you need is an observer function that is triggered when the user typing, which then looks at the comma and changes it to a dot.
TextFormField has a built in function,
onEditingCompleted and onFieldSubmitted which can run the function you have to make the check before the validate is run.
Widget build(BuildContext context) {
TextField XnumField = new TextField(
keyboardType: TextInputType.numberWithOptions(),
decoration: new InputDecoration(labelText: "X array"),
onSubmitted: (String text){
for(i = 0; i < 4; i++){
X[i] = int.parse(text);
}
},
);
Hi,
If you are looking for something like this here you go.
import 'package:flutter/material.dart';
void main() {
runApp(new MaterialApp(
home: new DemoScreen(),
));
}
class DemoScreen extends StatefulWidget {
#override
_DemoScreenState createState() => new _DemoScreenState();
}
class _DemoScreenState extends State<DemoScreen> {
List<int> _myList = new List();
TextEditingController _myController = new TextEditingController();
String _result = "";
String _inputList = "";
setSum() {
int sum = 0;
for (int i = 0; i < _myList.length; i++) {
sum += _myList[i];
if (i == 0)
_inputList = "${_myList[i]}";
else
_inputList = _inputList + " + ${_myList[i]}";
}
_result = "$sum";
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Demo App"),
),
body: new Column(
children: <Widget>[
new Container(
margin: new EdgeInsets.symmetric(vertical: 10.0),
child: new Text(
_inputList,
style: new TextStyle(fontSize: 40.0),
),
),
new Container(
margin: new EdgeInsets.symmetric(vertical: 25.0),
child: new Text(
_result,
style: new TextStyle(fontSize: 70.0),
),
),
new Container(
margin: new EdgeInsets.symmetric(horizontal: 50.0),
child: new TextField(
controller: _myController,
keyboardType: TextInputType.number,
onSubmitted: (text) {
setState(() {
_myList.add(int.parse(text));
setSum();
_myController.clear();
});
},
),
)
],
),
);
}
}
Hope it helps :)
Something like ...
import 'dart:core';
List<int> BuildIntArray(String input) {
var outList = new List<int>();
final _delimiter = ',';
final _values = input.split(_delimiter);
_values.forEach((item) {
outList.add(int.parse(item));
});
return outList;
}
Which would ...
import 'package:IntegerArray/IntegerArray.dart' as IntegerArray;
main(List<String> arguments) {
final input = "1,2,3,4,5";
final intInputValues = IntegerArray.BuildIntArray(input);
print (intInputValues);
int sum = 0;
intInputValues.forEach((item) {
sum+=item;
});
print (sum);
}
... do this ...
Observatory listening on http://127.0.0.1:64499/
[1, 2, 3, 4, 5]
15
Process finished with exit code 0
I don't see a "tryParse" to filter out non-numeric values ... but with some validation/error checking you could add to this ...
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();
}
}