Related
I was struct in a problem that i have to download pdf or images etc from network and store them in local storage.i am trying path provider plugin with GetApplicationDocumentDirectory its successfully storing files but not showing in device folder. how to create a directory and store files such as images pdfs etc that are visible to users. how could i achieve that.
thanks for help in advance
You can write to the device external storage as shown in the below example code by creating the folder
Hope it helps
class PDFDownloader extends StatefulWidget {
final String extension;
final String url;
final String fileName;
PDFDownloader(this.url, this.fileName,[this.extension='pdf']);
#override
_DownloadAppState createState() => new _DownloadAppState();
}
class _DownloadAppState extends State<PDFDownloader> {
bool downloading = false;
String _message;
var progressString = "";
Future<Directory> _externalDocumentsDirectory;
#override
void initState() {
//downloadFile();
checkPer();
// _bannerAd = createBannerAd()..load();
super.initState();
}
void checkPer() async {
await new Future.delayed(new Duration(seconds: 1));
bool checkResult = await SimplePermissions.checkPermission(
Permission.WriteExternalStorage);
if (!checkResult) {
var status = await SimplePermissions.requestPermission(
Permission.WriteExternalStorage);
//print("permission request result is " + resReq.toString());
if (status == PermissionStatus.authorized) {
await downloadFile();
}
} else {
await downloadFile();
}
}
#override
Widget build(BuildContext context) {
var scaffold= Scaffold(
appBar: AppBar(
title: Text("Download"),
),
body: Center(
child: downloading
? Container(
height: 120.0,
width: 200.0,
child: Card(
color: Colors.black,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
CircularProgressIndicator(),
SizedBox(
height: 20.0,
),
Text(
"Downloading File: $progressString",
style: TextStyle(
color: Colors.white,
),
)
],
),
),
)
: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(_message ?? 'Please wait!!'),
SizedBox(
height: 10.0,
),
new RaisedButton(
textColor: Colors.white,
color: Colors.blueAccent,
shape: new RoundedRectangleBorder(
borderRadius: new BorderRadius.circular(10.0)),
onPressed: () {
Navigator.pop(context);
},
child: Text('Close'),
)
],
),
),
);
return WillPopScope(
onWillPop: _onWillPop,
child: scaffold,
);
}
Future<bool> _onWillPop() {
return new Future.value(!downloading);
}
Future<void> downloadFile() async {
var dio = new Dio();
var dir = await getExternalStorageDirectory();
var knockDir =
await new Directory('${dir.path}/iLearn').create(recursive: true);
print(widget.url);
await dio.download(widget.url, '${knockDir.path}/${widget.fileName}.${widget.extension}',
onProgress: (rec, total) {
//print("Rec: $rec , Total: $total");
if (mounted) {
setState(() {
downloading = true;
progressString = ((rec / total) * 100).toStringAsFixed(0) + "%";
});
}
});
if (mounted) {
setState(() {
downloading = false;
progressString = "Completed";
_message = "File is downloaded to your SD card 'iLearn' folder!";
});
}
print("Download completed");
}
}
To create a Directory of App in Internal Storage use this snippet:
Directory directory = await getExternalStorageDirectory();
String fileName =
"xyz.pdf";
String newPath = "";
print(directory);
List<String> paths = directory.path.split("/");
for (int x = 1; x < paths.length; x++) {
String folder = paths[x];
if (folder != "Android") {
newPath += "/" + folder;
} else {
break;
}
}
newPath = newPath + "/YourAppName";
directory = Directory(newPath);
if (!await directory.exists()) {
await directory.create(recursive: true);
}
if (await directory.exists()) {
final File file = File(directory.path + "/$fileName");
// your logic for saving the file.
}
For a Detailed Explanation check out this Medium Article by Retroportal Studio:
https://retroportalstudio.medium.com/saving-files-to-application-folder-and-gallery-in-flutter-e9be2ebee92a
Hello I am working on a instagram clone using flutter and for the feed I want the images to show up in a horizontal card carousel view for the posts of from the user you follow
here is the current feed code:
import 'package:flutter/material.dart';
import 'image_post.dart';
import 'dart:async';
import 'package:async/async.dart';
import 'main.dart';
import 'dart:io';
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
class Feed extends StatefulWidget {
_Feed createState() => new _Feed();
}
class _Feed extends State<Feed> {
List<ImagePost> feedData;
#override
void initState() {
super.initState();
this._loadFeed();
}
buildFeed() {
if (feedData != null) {
return new ListView(
children: feedData,
);
} else {
return new Container(
alignment: FractionalOffset.center,
child: new CircularProgressIndicator());
}
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: const Text('InstaGroove',
style: const TextStyle(
fontFamily: "Billabong", color: Colors.black, fontSize: 35.0)),
centerTitle: true,
backgroundColor: Colors.white,
),
body: new RefreshIndicator(
onRefresh: _refresh,
child: buildFeed(),
),
);
}
Future<Null> _refresh() async {
await _getFeed();
setState(() {
});
return;
}
_loadFeed() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String json = prefs.getString("feed");
if (json != null) {
List<Map<String, dynamic>> data =
jsonDecode(json).cast<Map<String, dynamic>>();
List<ImagePost> listOfPosts = _generateFeed(data);
setState(() {
feedData = listOfPosts;
});
} else {
_getFeed();
}
}
_getFeed() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String userId = googleSignIn.currentUser.id.toString();
var url =
'https://us-central1-mp-rps.cloudfunctions.net/getFeed?uid=' + userId;
var httpClient = new HttpClient();
List<ImagePost> listOfPosts;
String result;
try {
var request = await httpClient.getUrl(Uri.parse(url));
var response = await request.close();
if (response.statusCode == HttpStatus.OK) {
String json = await response.transform(utf8.decoder).join();
prefs.setString("feed", json);
List<Map<String, dynamic>> data =
jsonDecode(json).cast<Map<String, dynamic>>();
listOfPosts = _generateFeed(data);
} else {
result =
'Error getting a feed:\nHttp status ${response.statusCode}';
}
} catch (exception) {
result =
'Failed invoking the getFeed function. Exception: $exception';
}
print(result);
setState(() {
feedData = listOfPosts;
});
}
List<ImagePost> _generateFeed(List<Map<String, dynamic>> feedData) {
List<ImagePost> listOfPosts = [];
for (var postData in feedData) {
listOfPosts.add(new ImagePost.fromJSON(postData));
}
return listOfPosts;
}
}
I've done some research on google and youtube and I can't figure out how to add the card carousel to the feed part of my app. I'm still fairly new to flutter so any help would be amazing! thanks in advance!! :)
Try this code. This is a sample image carousel screen. For now, there is hardcoded image list used, you can create dynamic list of Image.network() based on image list from api call.
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class TestImageCarousel extends StatefulWidget {
#override
State<StatefulWidget> createState() {
// TODO: implement createState
return _TestImageCarousel();
}
}
class _TestImageCarousel extends State<TestImageCarousel> {
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Material(
child: Stack(
children: <Widget>[
PageView(
onPageChanged: (id) {
//code to handle page change
},
scrollDirection: Axis.horizontal,
children: getImageList(),
),
],
));
}
}
/*
Get image list to populate in carousel
*/
getImageList() {
return <Widget>[
Padding(
padding: EdgeInsets.only(left: 1, right: 1),
child: Image.network("https://www.gstatic.com/webp/gallery/1.jpg",
fit: BoxFit.fill),
),
Padding(
padding: EdgeInsets.only(left: 1, right: 1),
child: Image.network("https://www.gstatic.com/webp/gallery/2.jpg",
fit: BoxFit.fill),
),
Padding(
padding: EdgeInsets.only(left: 1, right: 1),
child: Image.network("https://www.gstatic.com/webp/gallery/3.jpg",
fit: BoxFit.fill),
),
Padding(
padding: EdgeInsets.only(left: 1, right: 1),
child: Image.network("https://www.gstatic.com/webp/gallery/4.jpg",
fit: BoxFit.fill),
)
];
}
Now in this you can make a network call to get feed image list and bind list to Image.network("src").You can also add a page controller to get automatic carousel effects. Add this images in InkWell() to get onTap listeners.
In the following class, I am trying to set _isFavorited to the value that I get from the SharedPreference. However, I guess the widget is already build by the time I get the value from Sharedpreference. How can I set the value from sharedpreference and then display my widget?
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter/material.dart';
class FavoriteWidget extends StatefulWidget {
final dish_name;
final dish_pic;
#override
_FavoriteWidgetState createState() => _FavoriteWidgetState();
FavoriteWidget(this.dish_name, this.dish_pic,{Key key})
: super(key: key);
}
class _FavoriteWidgetState extends State<FavoriteWidget> {
bool _isFavorited = false;
#override
void initState() {
// TODO: implement initState
super.initState();
isDishFavorited(this.context);
}
isDishFavorited(BuildContext context) async{
SharedPreferences prefs = await SharedPreferences.getInstance();
if(prefs.getString(widget.dish_name) != null){
//rebuilding the context to display Icons.favorite
build(context);
_isFavorited = true; <== the widget is built before this is set to true.
}
debugPrint("isfavorite inside method is" + _isFavorited.toString());
}
// #docregion _toggleFavorite
void _toggleFavorite() {
setState(() {
if (_isFavorited) {
//Remove from favorites
_removeFromFavorites();
_isFavorited = false;
debugPrint("Removed from favorites");
} else {
//Add to favorites
_addToFavorites();
_isFavorited = true;
debugPrint("Added to favorites");
}
});
}
#override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: (_isFavorited
? Icon(Icons.favorite, size: 35, color: Colors.green)
: Icon(Icons.favorite_border, size: 35, color: Colors.green)),
color: Colors.red[500],
onPressed: _toggleFavorite,
),
Container(
margin: const EdgeInsets.only(top: 8.0),
child: Text(
"FAVORITE",
style: TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.w400,
color: Colors.white,
),
),
),
],
);
}
}
What I tried to do was to rebuild the widget after setting is_Favorited to true but that didn't work either. So, I am not sure how else to make this work. Any ideas?
you need to fix the isDishFavorited() function in your code
isDishFavorited(BuildContext context) async{
SharedPreferences prefs = await SharedPreferences.getInstance();
if(prefs.getString(widget.dish_name) != null){
setState(() {
_isFavorited = true;
});
}
debugPrint("isfavorite inside method is" + _isFavorited.toString());
}
setState will rebuild your widget after you fetch the data fromt the shared preferences
So I have a login page and when user pressed login, the middleware makes API calls and after the response is received the reducer changes the app state (isLogged in is set to true). How do I go the the next page using Navigator. When I try to do it, the error is thrown that the setResult() was called during building.
When the state changes, the widget tree is rebuilt so the navigator doesn't get appropriate context. How do I wait for the tree to be rebuilt and then call the Navigator.of(context).push(Route).
import 'package:flutter/material.dart';
import 'package:vattendance_flutter/common/loading_status.dart';
import 'package:vattendance_flutter/common/ui/buttons/primary_button.dart';
import 'package:vattendance_flutter/common/ui/loading/primary_circular_progress.dart';
import 'package:vattendance_flutter/common/ui/text_fields.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:vattendance_flutter/redux/app/app_state.dart';
import 'package:redux/redux.dart';
import 'package:vattendance_flutter/redux/login/login_actions.dart';
import 'package:vattendance_flutter/ui/home/home_page.dart';
class LoginPage extends StatefulWidget {
#override
State<StatefulWidget> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
_ViewModel _viewModel;
TextEditingController _usernameController;
TextEditingController _passwordController;
GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
#override
void initState() {
super.initState();
_usernameController = TextEditingController();
_passwordController = TextEditingController();
}
#override
Widget build(BuildContext context) {
return StoreConnector<AppState, _ViewModel>(
converter: (store) => _ViewModel.fromStore(store),
builder: (context, viewModel) {
_viewModel = viewModel;
_stateDependentSetUp();
return StoreBuilder<AppState>(
rebuildOnChange: true,
onDidChange: (store) {},
builder: (context, viewModel) {
return Scaffold(
key: scaffoldKey,
resizeToAvoidBottomPadding: false,
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'VAttendance',
style: TextStyle(
fontSize: 40.0,
fontWeight: FontWeight.w300,
),
),
Padding(
padding: EdgeInsets.only(
top: 40.0,
left: 16.0,
right: 16.0,
),
child: PrimaryTextField(
controller: _usernameController,
hintText: "Username",
),
),
Padding(
padding: EdgeInsets.only(
top: 40.0,
left: 16.0,
right: 16.0,
),
child: PrimaryTextField(
controller: _passwordController,
hintText: "Password",
obsecureText: true,
),
),
Container(
margin: EdgeInsets.only(top: 40.0),
child: _getLoginButtonOrProgress(),
),
],
),
);
},
);
},
);
}
_stateDependentSetUp() {
var loadingData = _viewModel.loadingData;
if (_viewModel.loadingData.status == LoadingStatus.ERROR) {
scaffoldKey.currentState.showSnackBar(
SnackBar(content: Text(loadingData.message ?? "Error")));
} else if (_viewModel.loadingData.status == LoadingStatus.SUCCESS) {
scaffoldKey.currentState
.showSnackBar(SnackBar(content: Text("Welcome to VAttendance.")));
_startHome();
}
}
_getLoginButtonOrProgress() {
return _viewModel.loadingData.status == LoadingStatus.LOADING
? PrimaryCircularProgress(
progressColor: Colors.blue,
)
: PrimaryButton(
text: "Login",
onPressed: () {
FocusScope.of(context).requestFocus(FocusNode());
_viewModel.onLoginPressed(
username: _usernameController.text,
password: _passwordController.text,
);
});
}
_startHome() {
Navigator.of(scaffoldKey.currentContext).pushReplacement(
MaterialPageRoute(
builder: (context) {
return HomePage();
},
),
);
}
}
class _ViewModel {
final bool isLoggedIn;
final LoadingData loadingData;
final Function({String username, String password, Function onError})
onLoginPressed;
_ViewModel({
this.loadingData,
this.onLoginPressed,
this.isLoggedIn,
});
factory _ViewModel.fromStore(Store<AppState> store) {
return _ViewModel(
isLoggedIn: store.state.loginState.isLoggedIn,
loadingData: store.state.loginState.loadingData,
onLoginPressed: ({String username, String password, Function onError}) {
store.dispatch(LoginAction(username: username, password: password));
},
);
}
}
You can add a Completer to your LoginAction so it will have 3 fields: username, password and completer.
When you pass this completer to your LoginAction:
Completer _initCompleter() {
return new Completer()
..future.then((_) => Navigator.of(context).pushNamed("\someRoute"));
}
Completer will wait until the Future gets there.
In the middleware you can invoke complete() method to tell the widget to go to next screen.
This is inside middleware logic:
authService.loginInWithEmail(action.email, action.password).then(
(user) {
store.dispatch(new UserProvidedAction(user));
action.completer?.complete();
},
);
You can also use this to handle errors, just add to completer's future onError parameter and then in the middleware you can call completeError().
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();
}
}