Flutter Text To Speech: Speak several strings subsequently with different SpeechRates - dart

I have a problem with the Flutter Text To Speech package.
When clicking on a FloatingActionButton I would like to speak/play several Strings (with different Speechrates) subsequently. However, when doing so, I can only hear the last string that I have passed onto the function and not the first one.
As you can see in the code below, I have tried to make use of the asynchronus programming (async / await).
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:flutter_tts/flutter_tts.dart';
class SpeakerClass extends StatefulWidget{
#override
State<StatefulWidget> createState() {
// TODO: implement createState
return _SpeakerClassState();
}
}
class _SpeakerClassState extends State<SpeakerClass>{
String text1 = 'eins';
String text2 = 'zwei';
String text3 = 'drei';
String text4 = 'vier';
String currentTtsString;
double ttsSpeechRate1 = 0.5;
double ttsSpeechRate2 = 1.0;
double currentSpeechRate;
Future playTtsString1() async {
currentTtsString = text1;
currentSpeechRate = ttsSpeechRate1;
await runTextToSpeech(currentTtsString, currentSpeechRate);
return null;
}
Future playTtsString2() async {
currentTtsString = text2;
currentSpeechRate = ttsSpeechRate2;
await runTextToSpeech(currentTtsString, currentSpeechRate);
return null;
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: FloatingActionButton (
backgroundColor: Colors.blue,
child: Icon(Icons.volume_up, color: Colors.white),
onPressed: () async {
await playTtsString1();
await playTtsString2();
},
)
);
}
}
Future<void> runTextToSpeech(String currentTtsString, double currentSpeechRate) async {
FlutterTts flutterTts;
flutterTts = new FlutterTts();
await flutterTts.setLanguage("en-GB");
await flutterTts.setVolume(1.0);
await flutterTts.setPitch(1.0);
await flutterTts.isLanguageAvailable("en-GB");
await flutterTts.setSpeechRate(currentSpeechRate);
await flutterTts.speak(currentTtsString);
}
When pressing the FloatingActionButton I expect the program to first carry out the function playTtsString1 ("eins" with a speed of 0.5) and afterwards the function playTtsString2 ("zwei" with a speed of 1).
However, somehow I can only hear the program saying "zwei". I guess the program is not waiting for the first function "playTtsString1" to be finished and already carries out the second function "playTtsString2". I would really appreciate any help on this matter!

import 'package:flutter/material.dart';
import 'dart:async';
import 'package:flutter_tts/flutter_tts.dart';
class SpeakerClass extends StatefulWidget{
#override
State<StatefulWidget> createState() {
// TODO: implement createState
return _SpeakerClassState();
}
}
class _SpeakerClassState extends State<SpeakerClass>{
String text1 = 'eins';
String text2 = 'zwei';
String text3 = 'drei';
String text4 = 'vier';
String currentTtsString;
double ttsSpeechRate1 = 0.5;
double ttsSpeechRate2 = 1.0;
double currentSpeechRate;
FlutterTts flutterTts;
bool bolSpeaking = false;
Future playTtsString1() async {
bolSpeaking = true;
currentTtsString = text1;
currentSpeechRate = ttsSpeechRate1;
await runTextToSpeech(currentTtsString, currentSpeechRate);
return null;
}
Future playTtsString2() async {
bolSpeaking = true;
currentTtsString = text2;
currentSpeechRate = ttsSpeechRate2;
await runTextToSpeech(currentTtsString, currentSpeechRate);
return null;
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: FloatingActionButton (
backgroundColor: Colors.blue,
child: Icon(Icons.volume_up, color: Colors.white),
onPressed: () async {
// Play String 1
await playTtsString1();
// Check String 1 Finish
new Future.delayed(new Duration(milliseconds: 100), () async {
// loop until bolSpeaking = false
while (bolSpeaking) {
await Thread.sleep(100);
}
// play String 2
await playTtsString2();
}
},
)
);
}
}
Future<void> runTextToSpeech(String currentTtsString, double currentSpeechRate) async {
flutterTts = new FlutterTts();
await flutterTts.setLanguage("en-GB");
await flutterTts.setVolume(1.0);
await flutterTts.setPitch(1.0);
await flutterTts.isLanguageAvailable("en-GB");
await flutterTts.setSpeechRate(currentSpeechRate);
flutterTts.setCompletionHandler(() {
setState(() {
// The following code(s) will be called when the TTS finishes speaking
bolSpeaking = false;
});
});
flutterTts.speak(currentTtsString);
}

This should now work with the latest flutter_tts version.
You simply need to set awaitSpeakCompletion before the speaking happens.
You can update your run method like so:
Future<void> runTextToSpeech(String currentTtsString, double currentSpeechRate) async {
FlutterTts flutterTts;
flutterTts = new FlutterTts();
await flutterTts.awaitSpeakCompletion(true);
await flutterTts.setLanguage("en-GB");
await flutterTts.setVolume(1.0);
await flutterTts.setPitch(1.0);
await flutterTts.isLanguageAvailable("en-GB");
await flutterTts.setSpeechRate(currentSpeechRate);
await flutterTts.speak(currentTtsString);
}

Related

StateError (Bad state: No element) on IOS only

This error does not occur on Android or web but only on IOS. It seem very trivial but I can't figure out what's wrong.
import 'dart:developer';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'dart:convert';
import 'package:qr_code_scanner/qr_code_scanner.dart';
class ScanQrPage extends StatefulWidget {
#override
_ScanQrPageState createState() => _ScanQrPageState();
}
class _ScanQrPageState extends State<ScanQrPage> {
final qrKey = GlobalKey();
late QRViewController qrViewController;
late Barcode barcode;
// In order to get hot reload to work we need to pause the camera if the platform
// is android, or resume the camera if the platform is iOS.
#override
void reassemble() {
super.reassemble();
if (Platform.isAndroid) {
qrViewController.pauseCamera();
} else if (Platform.isIOS) {
qrViewController.resumeCamera();
}
}
#override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () {
Navigator.of(context).pop("");
return new Future(() => true);
},
child: Scaffold(
body: Stack(
children: [
buildQrView(context),
],
),
),
);
}
Widget buildQrView(BuildContext context) {
return QRView(
onQRViewCreated: onQRViewCreated,
key: qrKey,
overlay: QrScannerOverlayShape(
cutOutSize: MediaQuery.of(context).size.width * 0.8),
onPermissionSet: (ctrl, p) => _onPermissionSet(context, ctrl, p),
);
}
void _onPermissionSet(BuildContext context, QRViewController ctrl, bool p) {
log('${DateTime.now().toIso8601String()}_onPermissionSet $p');
if (!p) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('no Permission')),
);
}
}
#override
void dispose() {
qrViewController.dispose();
super.dispose();
}
void onQRViewCreated(QRViewController qrViewController) {
setState(() {
this.qrViewController = qrViewController;
});
qrViewController.scannedDataStream.listen((event) {
setState(() {
this.barcode = event;
if (Platform.isAndroid) {
qrViewController.pauseCamera();
} else if (Platform.isIOS) {
qrViewController.resumeCamera();
}
String rawData = event.code;
Uri data = Uri.dataFromString(rawData);
String para1 = data.queryParameters["buy"] ??
""; //get parameter with attribute "para1"
Codec<String, String> stringToBase64 = utf8.fuse(base64);
if (para1 != "") {
placer = stringToBase64.decode(para1);
}
WidgetsBinding.instance?.addPostFrameCallback((timeStamp) {
Navigator.pop(context, placer);
});
});
});
}
}
I've tried all the solutions with the same error found on stackoverflow (addPostFrameCallback and Future(Duration.zero)) but none of them are exactly the same and does not seem to fix my problem.
I don't think I have having the same issue as any of the other questions.
The exception is happening on the Navigator.pop(context, placer);
Does anyone have any idea how to overcome this?
Why does this only happen on IOS?

Flutter integrating Hive database with Riverpod

There is very easy way to use Hive key-value database on StatefulWidgets, for example:
class HookDemo extends StatefulWidget {
#override
_HookDemoState createState() => _HookDemoState();
}
class _HookDemoState extends State<HookDemo> {
Box user;
#override
void initState() {
super.initState();
user = Hive.box<User>('user');
}
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
final _u = User()
..nameFamily = 'myname'
..mobileNumber = '123456789';
_user!.add(_u);
_u.save();
},
child: Icon(Icons.add),
),
...
);
}
}
here we defined Box user property and inside initState we implemented what's user such as user = Hive.box<User>('user');
after that we can use user without any problem and getting already opened error
now in this current application we used HookWidget and when we want to use Hive we get error as box already opened
main.dart:
Future<void> initHiveDriver() async {
final appDocumentDirectory = await path_provider.getApplicationDocumentsDirectory();
await Hive.initFlutter(appDocumentDirectory.path);
await Hive.openBox<UserAdapter>('user');
}
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
///...
initHiveDriver();
runApp(
ProviderScope(observers: [
Logger()
],
child: MyApp()),
);
}
how can i create a provider for Hive with Riverpod and use it into HookWidget?
I am using Hive with Riverpod like this.
I am using a named constructor so I can await the openBox call.
final hiveProvider = FutureProvider<HiveDB>((_) => HiveDB.create());
class HiveDB {
var _userBox;
HiveDB._create() {}
static Future<HiveDB> create() async {
final component = HiveDB._create();
await component._init();
return component;
}
_init() async {
Hive.registerAdapter(UserAdapter());
this._userBox = await Hive.openBox<User>('user');
}
storeUser(User user) {
this._userBox.put('user', user);
}
User getUser() {
return this._userBox.get('user');
}
}
Use in a ConsumerWidget:
class SomeWidget extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final provider = ref.watch(hiveProvider).data?.value;
...
}
}

Flutter don't update UI from iOS using setState() method

I am invoking a method from native Swift code by using the platform channel like this:
channel.invokeMethod(METHOD_NAME, arguments: STRING_ARGUMENT)
in my Flutter class I handle the respective method call by using a callback
platform.setMethodCallHandler(_receiveFromHost);
The setMethodCallHandler() requires the callback to return a Future and I set state in _receiveFromHost method in Dart.
Problem is that my UI is not updated when Swift invokes the callback. Why?
call.method catch method string, state is set, but UI don't update.
video
This is my code in Swift:
let flutterEngine = (UIApplication.shared.delegate as! AppDelegate).flutterEngine
let flutterViewController = FlutterViewController(engine: flutterEngine!, nibName: nil, bundle: nil)
let channel = FlutterMethodChannel(name: "flutter_apple_pay", binaryMessenger: flutterViewController.binaryMessenger)
channel.invokeMethod("sendDidFinishAdding", arguments: "payment success")
And this is my code in Flutter:
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:flutter/services.dart';
import 'dart:convert';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new HomePage(),
);
}
}
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: const Text("Native Code from Dart"),
),
body: new MyHomePage(title: ""),
);
}
}
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> {
String _state = "Waiting results...";
static const platform = const MethodChannel("flutter_apple_pay");
_MyHomePageState() {
platform.setMethodCallHandler(_receiveFromHost);
}
// #override
// void initState() {
// platform.setMethodCallHandler(_receiveFromHost);
// super.initState();
// }
Future<dynamic> _receiveFromHost(MethodCall call) async {
List certificatesBase64Encoded;
String nonceBase64Encoded;
String nonceSignatureBase64Encoded;
String state;
try {
print(call.method);
if (call.method == "sendGenerateRequestWithData") {
final String data = call.arguments;
final jData = jsonDecode(data);
certificatesBase64Encoded = jData['certificatesBase64Encoded'];
nonceBase64Encoded = jData['nonceBase64Encoded'];
nonceSignatureBase64Encoded = jData['nonceSignatureBase64Encoded'];
print(certificatesBase64Encoded);
print(nonceBase64Encoded);
print(nonceSignatureBase64Encoded);
state = "sendGenerateRequestWithData";
platform.invokeMethod("SendTokenizationData", "TokenizationDataString");
} else if (call.method == "sendDidFinishAdding") {
final String result = call.arguments;
print(result);
state = result;
}
} on PlatformException catch (e) {
print(e);
}
setState(() {
_state = state;
});
print(_state);
}
Future<void> _checkCardState() async {
String cardState;
const primaryAccountIdentifiers = ["a", "b"];
try {
final String result = await platform.invokeMethod(
"checkCardState", primaryAccountIdentifiers);
cardState = result;
} on PlatformException catch (e) {
cardState = "Failed to Invoke: '${e.message}'.";
}
setState(() {
_state = cardState;
});
}
Future<void> _startApplePay() async {
const _cardNetwork = "cn/AMEX";
const _cardHolderName = "pero peric";
const _primaryAccountIdentifier = "a";
const _primaryAccountSuffix = "1234";
const _localizedDescription = "AMEX";
Map<String, dynamic> resultMap = Map();
resultMap['cardNetwork'] = _cardNetwork;
resultMap['cardHolderName'] = _cardHolderName;
resultMap['primaryAccountIdentifier'] = _primaryAccountIdentifier;
resultMap['primaryAccountSuffix'] = _primaryAccountSuffix;
resultMap['localizedDescription'] = _localizedDescription;
platform.invokeMethod("startApplePay", resultMap);
}
#override
Widget build(BuildContext context) {
return Material(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
child: Text('Check Card State'),
onPressed: _checkCardState,
),
ElevatedButton(
child: Text('Start Apple Pay'),
onPressed: _startApplePay,
),
Text(_state),
],
),
),
);
}
}
Thanks!

Flutter get logged user data to other screen view

I have API.dart like this to authenticating a user, log in and log out
class Api {
static FirebaseAuth _auth = FirebaseAuth.instance;
static GoogleSignIn _googleSignIn = GoogleSignIn();
FirebaseUser firebaseUser;
Api(FirebaseUser user) {
this.firebaseUser = user;
}
static Future<FBApi> signInWithGoogle() async {
final GoogleSignInAccount googleUser = await _googleSignIn.signIn();
final GoogleSignInAuthentication googleAuth = await googleUser.authentication;
final FirebaseUser user = await _auth.signInWithGoogle(
accessToken: googleAuth.accessToken,
idToken: googleAuth.idToken,
);
assert(user.email != null);
assert(user.displayName != null);
assert(await user.getIdToken() != null);
final FirebaseUser currentUser = await _auth.currentUser();
assert(user.uid == currentUser.uid);
// print('photoURL api ' + user.photoUrl);
return Api(user);
}
static Future<void> signOut() async {
await _auth.signOut().then((_) {
print("***** log out...what the hell?");
_googleSignIn.signOut();
});
}
}
I've have a cloud function to create new user to database cloud firestore.
And in view account settings, I want to update user information like displayName, photoUrl into firestore. How I get current user in my account setting view.
class Settings extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(
'ACCOUNT',
style: TextStyle(color: primaryColor, fontWeight: FontWeight.bold),
),
centerTitle: true,
),
body: new SettingsScreen(),
);
}
}
class SettingsScreen extends StatefulWidget {
#override
State createState() => new SettingsScreenState();
}
class SettingsScreenState extends State<SettingsScreen> {
TextEditingController controllerNickname;
SharedPreferences prefs;
String id;
String nickName;
String photoUrl;
bool isLoading = false;
File avatarImageFile;
final FocusNode focusNodeNickname = new FocusNode();
#override
void initState() {
super.initState();
readLocal();
}
void readLocal() async {
prefs = await SharedPreferences.getInstance();
id = prefs.getString('id') ?? '';
nickName = prefs.getString('nickName') ?? '';
photoUrl = prefs.getString('photoUrl') ?? '';
controllerNickname = new TextEditingController(text: nickName);
// Force refresh input
setState(() {});
}
Future getImage() async {
File image = await ImagePicker.pickImage(source: ImageSource.gallery);
if (image != null) {
setState(() {
avatarImageFile = image;
isLoading = true;
});
}
uploadFile();
}
Future uploadFile() async {
String fileName = id;
StorageReference reference = FirebaseStorage.instance.ref().child(fileName);
StorageUploadTask uploadTask = reference.putFile(avatarImageFile);
StorageTaskSnapshot storageTaskSnapshot;
uploadTask.onComplete.then((value) {
if (value.error == null) {
storageTaskSnapshot = value;
storageTaskSnapshot.ref.getDownloadURL().then((downloadUrl) {
photoUrl = downloadUrl;
Firestore.instance
.collection('users')
.document(id)
.updateData({'displayName': nickName, 'photoUrl': photoUrl}).then((data) async {
await prefs.setString('photoUrl', photoUrl);
setState(() {
isLoading = false;
});
Fluttertoast.showToast(msg: "Upload success");
}).catchError((err) {
setState(() {
isLoading = false;
});
Fluttertoast.showToast(msg: err.toString());
});
}, onError: (err) {
setState(() {
isLoading = false;
});
Fluttertoast.showToast(msg: 'This file is not an image');
});
} else {
setState(() {
isLoading = false;
});
Fluttertoast.showToast(msg: 'This file is not an image');
}
}, onError: (err) {
setState(() {
isLoading = false;
});
Fluttertoast.showToast(msg: err.toString());
});
}
void handleUpdateData() {
focusNodeNickname.unfocus();
setState(() {
isLoading = true;
});
Firestore.instance
.collection('users')
.document(id)
.updateData({'displayName': nickName, 'photoUrl': photoUrl}).then((data) async {
await prefs.setString('nickname', nickName);
await prefs.setString('photoUrl', photoUrl);
setState(() {
isLoading = false;
});
Fluttertoast.showToast(msg: "Update success");
}).catchError((err) {
setState(() {
isLoading = false;
});
Fluttertoast.showToast(msg: err.toString());
});
}
#override
Widget build(BuildContext context) {
...
You can do something like this FirebaseAuth.instance.currentUser()
This returns the current user if any. Otherwise it returns null

Drawing over an image downloaded from remote server

I need to do the following:
Download a PNG resource from a server
Draw several rectangles over that image with different color depending on state
Display that image in Zoomable Image View
I have a working code in an Android app using Canvas, but I cannot figure out how to do that with Flutter.
Here is the code that downloads the resource:
static Future<File> getImageFromUrl(String url) async {
final directory = await getApplicationDocumentsDirectory();
final file = File("$directory/${_getSHA(url)}.png");
if (await file.exists()) {
// Returns the cached file
} else {
final response = await http.get(url);
if (response.statusCode >= 200 && response.statusCode < 300) {
await file.writeAsBytes(response.bodyBytes);
} else {
return null;
}
}
return file;
}
What should I do next? I tried using PictureRecorder and Canvas, but I cannot find a way to draw image from the file on those canvas and then convert it to Image because I cannot extract width and height from the file.
EDIT:
Below is the Android code equivalent that I would like to implement in Flutter.
// Here we have a bitmap from a file
Bitmap mapBitmap = getBitmap();
Canvas mapCanvas = new Canvas(mapBitmap);
mapDrawable.setBounds(0, 0, mapCanvas.getWidth(), mapCanvas.getHeight());
mapDrawable.draw(mapCanvas);
canvasWidth = mapCanvas.getWidth();
canvasHeight = mapCanvas.getHeight();
Paint paint = new Paint();
for (java.util.Map.Entry<String, MapObject> entry : this.mapObjects.entrySet()) {
MapObject mapObject = entry.getValue();
paint.setColor(getContext().getResources().getColor(mapObject.getBackgroundColor()));
paint.setAlpha(100);
mapCanvas.drawRect((int) (mapObject.getPosX() * scaleX),
(int) (mapObject.getPosY() * scaleY),
(int) ((mapObject.getPosX() + mapObject.getWidth()) * scaleX),
(int) ((mapObject.getPosY() + mapObject.getHeight()) * scaleY),
paint);
}
photoView.setImageBitmap(mapBitmap);
I finally managed to solve the issue!
I created a renderer that creates a composite image (background from the remote resource and adds rectangles in the foreground).
The renderer:
class MapRenderer {
ui.Image _mapBackgroundImage;
Future<ui.Codec> renderMap(String url, List<Sensor> sensors) async {
await _loadMapBackground(url);
var renderedMapImage = await _updateSensors(sensors);
var byteD = await renderedMapImage.toByteData(
format: ui.ImageByteFormat.png);
return ui.instantiateImageCodec(Uint8List.view(byteD.buffer));
}
Future<ui.Image> _updateSensors(List<Sensor> sensors) async {
ui.PictureRecorder recorder = ui.PictureRecorder();
Canvas c = Canvas(recorder);
var paint = ui.Paint();
c.drawImage(_mapBackgroundImage, ui.Offset(0.0, 0.0), paint);
for (Sensor s in sensors) {
paint.color = (s.availability ? CustomColors.npSensorFree : CustomColors
.npSensorOccupied);
c.drawRect(
ui.Rect.fromPoints(ui.Offset(s.posX, s.posY),
ui.Offset(s.posX + s.width, s.posY + s.height)),
paint,
);
}
return recorder
.endRecording()
.toImage(_mapBackgroundImage.width, _mapBackgroundImage.height);
}
Future<void> _loadMapBackground(String url) async {
var imageBytes = await _getLocalCopyOrLoadFromUrl(url);
if (imageBytes != null) {
_mapBackgroundImage = await _getImageFromBytes(imageBytes);
} else {
return null;
}
}
Future<ui.Image> _getImageFromBytes(Uint8List bytes) async {
var imageCodec = await ui.instantiateImageCodec(bytes);
var frame = await imageCodec.getNextFrame();
return frame.image;
}
Future<Uint8List> _getLocalCopyOrLoadFromUrl(String url) async {
final directory = await getApplicationDocumentsDirectory();
final file = File("${directory.path}/${_getSHA(url)}.png");
if (await file.exists()) {
return await file.readAsBytes();
} else {
Uint8List resourceBytes = await _loadFromUrl(url);
if (resourceBytes != null) {
await file.writeAsBytes(resourceBytes);
return resourceBytes;
} else {
return null;
}
}
}
Future<Uint8List> _loadFromUrl(String url) async {
final response = await http.get(url);
if (response.statusCode >= 200 && response.statusCode < 300) {
return response.bodyBytes;
} else {
return null;
}
}
String _getSHA(String sth) {
var bytes = utf8.encode(sth);
var digest = sha1.convert(bytes);
return digest.toString();
}
void dispose() {
_mapBackgroundImage.dispose();
}
}
And to supply the image to the ZoomableImage I created a custom ImageProvider:
class MapImageProvider extends ImageProvider<MapImageProvider> {
final String url;
final List<Sensor> sensors;
final MapRenderer mapRenderer = MapRenderer();
MapImageProvider(this.url, this.sensors);
#override
ImageStreamCompleter load(MapImageProvider key) {
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key),
scale: 1.0,
informationCollector: (StringBuffer information) {
information.writeln('Image provider: $this');
information.write('Image key: $key');
});
}
Future<ui.Codec> _loadAsync(MapImageProvider key) async {
assert(key == this);
return await mapRenderer.renderMap(url, sensors);
}
#override
bool operator ==(Object other) =>
identical(this, other) ||
other is MapImageProvider &&
runtimeType == other.runtimeType &&
url == other.url;
#override
int get hashCode => url.hashCode;
#override
String toString() => '$runtimeType("$url")';
#override
Future<MapImageProvider> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture<MapImageProvider>(this);
}
}
If anybody knows a better way to convert an Image to Codec or to even skip this step, please comment (MapRenderer.renderMap function).
In general to simply display an image from the internet you can use the Image.network constructor. If you want to further customize the interaction, for example showing rectangles based on its loading state, you can use the Image class and pass a NetworkImage to its constructor. The NetworkImage allows you to listen to loading and error events.
To draw above the Image I would simply suggest using the Stack Widget.
If you wanna add zooming functionality to the image, you should consider using the zoomable_image or photo_view package to replace the Image in the code below.
Also, if caching is necessary you can use the CachedNetworkImageProvider from the cached_network_image package.
The example below shows a yellow rectangle on a loading image, a green rectangle on a fully loaded image and a red rectangle if the loading crashed. It is a full application, you can copy & paste it in your IDE and try it out.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Network Image Download',
theme: ThemeData(),
home: MainPage(),
);
}
}
class MainPage extends StatefulWidget {
#override
State<StatefulWidget> createState() => MainPageState();
}
class MainPageState extends State<MainPage> {
ImageProvider provider;
bool loaded;
bool error;
#override
void initState() {
super.initState();
loaded = false;
error = false;
provider = NetworkImage('https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png');
provider.resolve(ImageConfiguration()).addListener((_, __) {
setState(() {
loaded = true;
});
}, onError: (_, __) {
setState(() {
error = true;
});
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Stack(
alignment: Alignment.center,
children: <Widget>[
Image(image: provider),
Container(
width: 75.0,
height: 75.0,
color: colorByState(),
)
],
),
),
);
}
Color colorByState() {
if (error) {
return Colors.red;
} else if (loaded) {
return Colors.green;
} else {
return Colors.yellow;
}
}
}

Resources