Skip to content

Commit 6cdd860

Browse files
authored
Refactor image retrieval (#320)
This makes the image retrieval process not rely on a FutureBuilder. Previously we recreated Image behavior by showing a loader whilst the image loads, using a completer to detect when the image had finished load. Now we rely only on the good old state and some ifs in the build method. Also, we have split that code from the custom child code. The main widget had logics for both image and custom child modes. Now we split that into two internal widgets, the wrappers. This should resolve the following issues #316 #303
1 parent c4467a3 commit 6cdd860

File tree

4 files changed

+356
-174
lines changed

4 files changed

+356
-174
lines changed

example/lib/screens/examples/hero_example.dart

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,22 @@ class HeroExample extends StatelessWidget {
1515
context,
1616
MaterialPageRoute(
1717
builder: (context) => const HeroPhotoViewRouteWrapper(
18-
imageProvider: AssetImage("assets/large-image.jpg"),
18+
imageProvider: NetworkImage(
19+
"https://source.unsplash.com/4900x3600/?camera,paper",
20+
),
1921
),
2022
),
2123
);
2224
},
2325
child: Container(
2426
child: Hero(
2527
tag: "someTag",
26-
child: Image.asset("assets/large-image.jpg", width: 150.0),
28+
child: Image.network(
29+
"https://source.unsplash.com/4900x3600/?camera,paper",
30+
width: 350.0,
31+
loadingBuilder: (_, child, chunk) =>
32+
chunk != null ? const Text("loading") : child,
33+
),
2734
),
2835
),
2936
),
@@ -35,14 +42,12 @@ class HeroExample extends StatelessWidget {
3542
class HeroPhotoViewRouteWrapper extends StatelessWidget {
3643
const HeroPhotoViewRouteWrapper({
3744
this.imageProvider,
38-
this.loadingBuilder,
3945
this.backgroundDecoration,
4046
this.minScale,
4147
this.maxScale,
4248
});
4349

4450
final ImageProvider imageProvider;
45-
final LoadingBuilder loadingBuilder;
4651
final Decoration backgroundDecoration;
4752
final dynamic minScale;
4853
final dynamic maxScale;
@@ -55,7 +60,6 @@ class HeroPhotoViewRouteWrapper extends StatelessWidget {
5560
),
5661
child: PhotoView(
5762
imageProvider: imageProvider,
58-
loadingBuilder: loadingBuilder,
5963
backgroundDecoration: backgroundDecoration,
6064
minScale: minScale,
6165
maxScale: maxScale,

example/lib/screens/examples/network-images.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ class NetworkExamples extends StatelessWidget {
1919
MaterialPageRoute(
2020
builder: (context) => CommonExampleRouteWrapper(
2121
imageProvider: const NetworkImage(
22-
"https://source.unsplash.com/1900x3600/?camera,paper"),
22+
"https://source.unsplash.com/1900x3600/?camera,paper",
23+
),
2324
loadingBuilder: (context, event) {
2425
if (event == null) {
2526
return const Center(
@@ -40,7 +41,7 @@ class NetworkExamples extends StatelessWidget {
4041
},
4142
),
4243
ExampleButtonNode(
43-
title: "Image from the internet (with custom loader)",
44+
title: "Error image",
4445
onPressed: () {
4546
Navigator.push(
4647
context,

lib/photo_view.dart

Lines changed: 64 additions & 167 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
11
library photo_view;
22

3-
import 'dart:async';
4-
53
import 'package:flutter/material.dart';
64

75
import 'package:photo_view/src/controller/photo_view_controller.dart';
86
import 'package:photo_view/src/controller/photo_view_scalestate_controller.dart';
97
import 'package:photo_view/src/core/photo_view_core.dart';
108
import 'package:photo_view/src/photo_view_computed_scale.dart';
11-
import 'package:photo_view/src/photo_view_default_widgets.dart';
129
import 'package:photo_view/src/photo_view_scale_state.dart';
10+
import 'package:photo_view/src/photo_view_wrappers.dart';
1311
import 'package:photo_view/src/utils/photo_view_hero_attributes.dart';
14-
import 'package:photo_view/src/utils/photo_view_utils.dart';
1512

1613
export 'src/controller/photo_view_controller.dart';
1714
export 'src/controller/photo_view_scalestate_controller.dart';
@@ -259,6 +256,7 @@ class PhotoView extends StatefulWidget {
259256
this.tightMode,
260257
this.filterQuality,
261258
this.disableGestures,
259+
this.errorBuilder,
262260
}) : child = null,
263261
childSize = null,
264262
super(key: key);
@@ -292,6 +290,7 @@ class PhotoView extends StatefulWidget {
292290
this.filterQuality,
293291
this.disableGestures,
294292
}) : loadFailedChild = null,
293+
errorBuilder = null,
295294
imageProvider = null,
296295
gaplessPlayback = false,
297296
loadingBuilder = null,
@@ -306,6 +305,10 @@ class PhotoView extends StatefulWidget {
306305
final LoadingBuilder loadingBuilder;
307306

308307
/// Show loadFailedChild when the image failed to load
308+
final ImageErrorWidgetBuilder errorBuilder;
309+
310+
/// Show loadFailedChild when the image failed to load
311+
@Deprecated("Use errorBuilder instead")
309312
final Widget loadFailedChild;
310313

311314
/// Changes the background behind image, defaults to `Colors.black`.
@@ -385,74 +388,29 @@ class PhotoView extends StatefulWidget {
385388
// Useful when custom gesture detector is used in child widget.
386389
final bool disableGestures;
387390

391+
bool get _isCustomChild {
392+
return child != null;
393+
}
394+
388395
@override
389396
State<StatefulWidget> createState() {
390397
return _PhotoViewState();
391398
}
392399
}
393400

394401
class _PhotoViewState extends State<PhotoView> {
395-
Size _childSize;
396-
bool _loading;
397-
ImageChunkEvent _imageChunkEvent;
402+
// image retrieval
398403

404+
// controller
399405
bool _controlledController;
400406
PhotoViewControllerBase _controller;
401-
402407
bool _controlledScaleStateController;
403408
PhotoViewScaleStateController _scaleStateController;
404409

405-
Future<ImageInfo> _getImage() {
406-
final Completer completer = Completer<ImageInfo>();
407-
final ImageStream stream = widget.imageProvider.resolve(
408-
const ImageConfiguration(),
409-
);
410-
final listener = ImageStreamListener((
411-
ImageInfo info,
412-
bool synchronousCall,
413-
) {
414-
if (completer.isCompleted) {
415-
return;
416-
}
417-
completer.complete(info);
418-
if (mounted) {
419-
final setupCallback = () {
420-
_childSize = Size(
421-
info.image.width.toDouble(),
422-
info.image.height.toDouble(),
423-
);
424-
_loading = false;
425-
_imageChunkEvent = null;
426-
};
427-
synchronousCall ? setupCallback() : setState(setupCallback);
428-
}
429-
}, onChunk: (event) {
430-
if (mounted) {
431-
setState(() => _imageChunkEvent = event);
432-
}
433-
}, onError: (exception, stackTrace) {
434-
if (completer.isCompleted) {
435-
return;
436-
}
437-
completer.completeError(exception, stackTrace);
438-
});
439-
stream.addListener(listener);
440-
completer.future.then((_) {
441-
stream.removeListener(listener);
442-
});
443-
return completer.future;
444-
}
445-
446410
@override
447411
void initState() {
448412
super.initState();
449-
if (widget.child == null) {
450-
_getImage();
451-
} else {
452-
_childSize = widget.childSize;
453-
_loading = false;
454-
_imageChunkEvent = null;
455-
}
413+
456414
if (widget.controller == null) {
457415
_controlledController = true;
458416
_controller = PhotoViewController();
@@ -474,11 +432,6 @@ class _PhotoViewState extends State<PhotoView> {
474432

475433
@override
476434
void didUpdateWidget(PhotoView oldWidget) {
477-
if (oldWidget.childSize != widget.childSize && widget.childSize != null) {
478-
setState(() {
479-
_childSize = widget.childSize;
480-
});
481-
}
482435
if (widget.controller == null) {
483436
if (!_controlledController) {
484437
_controlledController = true;
@@ -525,114 +478,58 @@ class _PhotoViewState extends State<PhotoView> {
525478
BuildContext context,
526479
BoxConstraints constraints,
527480
) {
528-
return widget.child == null
529-
? _buildImage(context, constraints)
530-
: _buildCustomChild(context, constraints);
481+
final computedOuterSize = widget.customSize ?? constraints.biggest;
482+
483+
return widget._isCustomChild
484+
? CustomChildWrapper(
485+
child: widget.child,
486+
childSize: widget.childSize,
487+
backgroundDecoration: widget.backgroundDecoration,
488+
heroAttributes: widget.heroAttributes,
489+
scaleStateChangedCallback: widget.scaleStateChangedCallback,
490+
enableRotation: widget.enableRotation,
491+
controller: _controller,
492+
scaleStateController: _scaleStateController,
493+
maxScale: widget.maxScale,
494+
minScale: widget.minScale,
495+
initialScale: widget.initialScale,
496+
basePosition: widget.basePosition,
497+
scaleStateCycle: widget.scaleStateCycle,
498+
onTapUp: widget.onTapUp,
499+
onTapDown: widget.onTapDown,
500+
outerSize: computedOuterSize,
501+
gestureDetectorBehavior: widget.gestureDetectorBehavior,
502+
tightMode: widget.tightMode,
503+
filterQuality: widget.filterQuality,
504+
disableGestures: widget.disableGestures,
505+
)
506+
: ImageWrapper(
507+
imageProvider: widget.imageProvider,
508+
loadingBuilder: widget.loadingBuilder,
509+
loadFailedChild: widget.loadFailedChild,
510+
backgroundDecoration: widget.backgroundDecoration,
511+
gaplessPlayback: widget.gaplessPlayback,
512+
heroAttributes: widget.heroAttributes,
513+
scaleStateChangedCallback: widget.scaleStateChangedCallback,
514+
enableRotation: widget.enableRotation,
515+
controller: _controller,
516+
scaleStateController: _scaleStateController,
517+
maxScale: widget.maxScale,
518+
minScale: widget.minScale,
519+
initialScale: widget.initialScale,
520+
basePosition: widget.basePosition,
521+
scaleStateCycle: widget.scaleStateCycle,
522+
onTapUp: widget.onTapUp,
523+
onTapDown: widget.onTapDown,
524+
outerSize: computedOuterSize,
525+
gestureDetectorBehavior: widget.gestureDetectorBehavior,
526+
tightMode: widget.tightMode,
527+
filterQuality: widget.filterQuality,
528+
disableGestures: widget.disableGestures,
529+
);
531530
},
532531
);
533532
}
534-
535-
Widget _buildCustomChild(BuildContext context, BoxConstraints constraints) {
536-
final _computedOuterSize = widget.customSize ?? constraints.biggest;
537-
538-
final scaleBoundaries = ScaleBoundaries(
539-
widget.minScale ?? 0.0,
540-
widget.maxScale ?? double.infinity,
541-
widget.initialScale ?? PhotoViewComputedScale.contained,
542-
_computedOuterSize,
543-
_childSize ?? constraints.biggest,
544-
);
545-
546-
return PhotoViewCore.customChild(
547-
customChild: widget.child,
548-
backgroundDecoration: widget.backgroundDecoration,
549-
enableRotation: widget.enableRotation,
550-
heroAttributes: widget.heroAttributes,
551-
controller: _controller,
552-
scaleStateController: _scaleStateController,
553-
scaleStateCycle: widget.scaleStateCycle ?? defaultScaleStateCycle,
554-
basePosition: widget.basePosition ?? Alignment.center,
555-
scaleBoundaries: scaleBoundaries,
556-
onTapUp: widget.onTapUp,
557-
onTapDown: widget.onTapDown,
558-
gestureDetectorBehavior: widget.gestureDetectorBehavior,
559-
tightMode: widget.tightMode ?? false,
560-
filterQuality: widget.filterQuality ?? FilterQuality.none,
561-
disableGestures: widget.disableGestures ?? false,
562-
);
563-
}
564-
565-
Widget _buildImage(BuildContext context, BoxConstraints constraints) {
566-
return widget.heroAttributes == null
567-
? _buildAsync(context, constraints)
568-
: _buildSync(context, constraints);
569-
}
570-
571-
Widget _buildAsync(BuildContext context, BoxConstraints constraints) {
572-
return FutureBuilder(
573-
future: _getImage(),
574-
builder: (BuildContext context, AsyncSnapshot<ImageInfo> info) {
575-
if (info.hasError) {
576-
return _buildLoadFailed();
577-
}
578-
if (info.hasData) {
579-
return _buildWrapperImage(context, constraints);
580-
}
581-
return _buildLoading();
582-
});
583-
}
584-
585-
Widget _buildSync(BuildContext context, BoxConstraints constraints) {
586-
if (_loading == null) {
587-
return _buildLoading();
588-
}
589-
return _buildWrapperImage(context, constraints);
590-
}
591-
592-
Widget _buildWrapperImage(BuildContext context, BoxConstraints constraints) {
593-
final _computedOuterSize = widget.customSize ?? constraints.biggest;
594-
595-
final scaleBoundaries = ScaleBoundaries(
596-
widget.minScale ?? 0.0,
597-
widget.maxScale ?? double.infinity,
598-
widget.initialScale ?? PhotoViewComputedScale.contained,
599-
_computedOuterSize,
600-
_childSize,
601-
);
602-
603-
return PhotoViewCore(
604-
imageProvider: widget.imageProvider,
605-
backgroundDecoration: widget.backgroundDecoration,
606-
gaplessPlayback: widget.gaplessPlayback,
607-
enableRotation: widget.enableRotation,
608-
heroAttributes: widget.heroAttributes,
609-
basePosition: widget.basePosition ?? Alignment.center,
610-
controller: _controller,
611-
scaleStateController: _scaleStateController,
612-
scaleStateCycle: widget.scaleStateCycle ?? defaultScaleStateCycle,
613-
scaleBoundaries: scaleBoundaries,
614-
onTapUp: widget.onTapUp,
615-
onTapDown: widget.onTapDown,
616-
gestureDetectorBehavior: widget.gestureDetectorBehavior,
617-
tightMode: widget.tightMode ?? false,
618-
filterQuality: widget.filterQuality ?? FilterQuality.none,
619-
disableGestures: widget.disableGestures ?? false,
620-
);
621-
}
622-
623-
Widget _buildLoading() {
624-
if (widget.loadingBuilder != null) {
625-
return widget.loadingBuilder(context, _imageChunkEvent);
626-
}
627-
628-
return PhotoViewDefaultLoading(
629-
event: _imageChunkEvent,
630-
);
631-
}
632-
633-
Widget _buildLoadFailed() {
634-
return widget.loadFailedChild ?? PhotoViewDefaultError();
635-
}
636533
}
637534

638535
/// The default [ScaleStateCycle]
@@ -673,7 +570,7 @@ typedef PhotoViewImageTapDownCallback = Function(
673570
PhotoViewControllerValue controllerValue,
674571
);
675572

676-
/// A type definition for a callback to show a widget while a image is loading, a [ImageChunkEvent] is passed to inform progress
573+
/// A type definition for a callback to show a widget while the image is loading, a [ImageChunkEvent] is passed to inform progress
677574
typedef LoadingBuilder = Widget Function(
678575
BuildContext context,
679576
ImageChunkEvent event,

0 commit comments

Comments
 (0)