Flutter图片证书过期加载适配

前言:首次写博客文章,如有侵权,望告知。我会下架文章。

最近开始Android+Flutter混合开发。边摸索边开发。

最近遇到一个实际开发的遇到的bug。公司有部分图片在过期的地址里。所以要做适配。

一贯的做法,先找度娘,找了各路大神的处理方式。但是现在网上同样情况的博客相对还少。

不过找到了一个参考价值较高的:https://www.jianshu.com/p/a09532ad4b3d,这是最接近我遇到的情况。

而且文章写得很好也很详细。估计由于flutter版本不同所以导致我按上面的去操作时也未能有实现的效果。

我在这文章的基础上。添加了一些改动然后实现了证书过期的适配。注:我的flutter 版本是v1.9.1+hotfix.6-pre.1。

关键代码:

       //解决证书校验通不过的问题  参考 https://www.jianshu.com/p/a09532ad4b3d
      _httpClient.badCertificateCallback = (X509Certificate cert,String host,int port){
        return true;
      };

过程:

1.参考常用图片加载的代码。FadeInImage.assetNetwork。

Text("FadeInImage正常的图片地址"),
FadeInImage.assetNetwork(
  placeholder: 'images/cherry.png', // 本地展位图
  image: 'http://n.sinaimg.cn/photo/transform/700/w1000h500/20200511/fe11-itmiwry9590007.jpg',
  width: 200,
  height: 120,
),

点进看源码详情,看看传入的image(图片地址)的去向。在构造方法中可以看到传入了一个NetworkImage类中,官方类见名知意的就是网络图片,即可初步理解类里面"可能是"具体的下载实现。点进去再看具体详情:

abstract class NetworkImage extends ImageProvider<NetworkImage> {
  /// Creates an object that fetches the image at the given URL.
  ///
  /// The arguments [url] and [scale] must not be null.
  const factory NetworkImage(String url, { double scale, Map<String, String> headers }) = network_image.NetworkImage;

  /// The URL from which the image will be fetched.
  String get url; 

  /// The scale to place in the [ImageInfo] object of the image.
  double get scale;

  /// The HTTP headers that will be used with [HttpClient.get] to fetch image from network.
  ///
  /// When running flutter on the web, headers are not used.
  Map<String, String> get headers;

  @override
  ImageStreamCompleter load(NetworkImage key);
}

这个类里可以直观看到的实现没有,这是个abstract 抽象类。其成员(接口或者方法皆无具体实现),然后看构造方法区域发现有一个更进一步地方:network_image.NetworkImage。Go!点进去有惊喜。即进入了NetworkImage类,注意该类是前一个抽象类NetworkImage的子类即具体实现类。这里就应该我们想找找的具体下载实现的代码接近了。先看构造方法,传入了url很好理解这就是图片地址了,在该类中搜索url。找出与网络下载相关的地方。可定位到_loadAsync()方法:

 Future<ui.Codec> _loadAsync(
    NetworkImage key,
    StreamController<ImageChunkEvent> chunkEvents,
  ) async {
    try {
      assert(key == this);
      // todo 插入  关键代码
      _httpClient.badCertificateCallback = (X509Certificate cert,String host,int port){
        return true;
      };

      final Uri resolved = Uri.base.resolve(key.url);
      final HttpClientRequest request = await _httpClient.getUrl(resolved);
      headers?.forEach((String name, String value) {
        request.headers.add(name, value);
      });
      final HttpClientResponse response = await request.close();
      if (response.statusCode != HttpStatus.ok)
        throw image_provider.NetworkImageLoadException(statusCode: response.statusCode, uri: resolved);

      final Uint8List bytes = await consolidateHttpClientResponseBytes(
        response,
        onBytesReceived: (int cumulative, int total) {
          chunkEvents.add(ImageChunkEvent(
            cumulativeBytesLoaded: cumulative,
            expectedTotalBytes: total,
          ));
        },
      );
      if (bytes.lengthInBytes == 0)
        throw Exception('NetworkImage is an empty file: $resolved');

      return PaintingBinding.instance.instantiateImageCodec(bytes);
    } finally {
      chunkEvents.close();
    }
  }

这里就是具体的下载实现了。然后参考之前的大神博客。我们要想办法重重写一下这里的代码插入关键代码已达到适配证书过期图片的目的。

根据参考文章。建议是参考源码重写一个 FadeInImage类命名随意【参考源码,东改改西改改弄成我们自己想要的东西。恰巧我之前为了实现一个时间轴的效果自己通过参考源码弄过一个圆点边框(改天也弄个博客记录一下)】,我这用FadeInImageWithoutAuth。说是参考源码其实就是复制粘贴改导包改关联。然后我们就能针对性改动部分代码:在自己copy出来代码中的_loadAsync插入关键代码。

2.重写

过程大概就是:
①将FadeInImage的代码全copy到FadeInImageWithoutAuth,然后改导包、关联。然后发现这里改不到_loadAsync的代码;
②所以要构造方法中的NetworkImage,重复①如新建一个NetworkImageWithoutAuth类复制NetworkImage代码。还是改不到_loadAsync的代码;
③找构造方法中的network_image.NetworkImage。新建一个NetworkNoAuthImage复制network_image_io.dart文件下的NetworkImage代码。有_loadAsync的代码,可以改了。
最后补上三个重现类的代码:
FadeInImageWithoutAuth篇幅过长已删除注释,如需要参考原来的FadeInImage的

class FadeInImageWithoutAuth extends prefix0.StatelessWidget {
  const FadeInImageWithoutAuth({
    Key key,
    @required this.placeholder,
    @required this.image,
    this.excludeFromSemantics = false,
    this.imageSemanticLabel,
    this.fadeOutDuration = const Duration(milliseconds: 300),
    this.fadeOutCurve = Curves.easeOut,
    this.fadeInDuration = const Duration(milliseconds: 700),
    this.fadeInCurve = Curves.easeIn,
    this.width,
    this.height,
    this.fit,
    this.alignment = Alignment.center,
    this.repeat = ImageRepeat.noRepeat,
    this.matchTextDirection = false,
  }) : assert(placeholder != null),
        assert(image != null),
        assert(fadeOutDuration != null),
        assert(fadeOutCurve != null),
        assert(fadeInDuration != null),
        assert(fadeInCurve != null),
        assert(alignment != null),
        assert(repeat != null),
        assert(matchTextDirection != null),
        super(key: key);
  
  FadeInImageWithoutAuth.memoryNetwork({
    Key key,
    @required Uint8List placeholder,
    @required String image,
    double placeholderScale = 1.0,
    double imageScale = 1.0,
    this.excludeFromSemantics = false,
    this.imageSemanticLabel,
    this.fadeOutDuration = const Duration(milliseconds: 300),
    this.fadeOutCurve = Curves.easeOut,
    this.fadeInDuration = const Duration(milliseconds: 700),
    this.fadeInCurve = Curves.easeIn,
    this.width,
    this.height,
    this.fit,
    this.alignment = Alignment.center,
    this.repeat = ImageRepeat.noRepeat,
    this.matchTextDirection = false,
  }) : assert(placeholder != null),
        assert(image != null),
        assert(placeholderScale != null),
        assert(imageScale != null),
        assert(fadeOutDuration != null),
        assert(fadeOutCurve != null),
        assert(fadeInDuration != null),
        assert(fadeInCurve != null),
        assert(alignment != null),
        assert(repeat != null),
        assert(matchTextDirection != null),
        placeholder = MemoryImage(placeholder, scale: placeholderScale),
        image =  img_provider.NetworkImageWithoutAuth(image, scale: imageScale),
        super(key: key);

  
  FadeInImageWithoutAuth.assetNetwork({
    Key key,
    @required String placeholder,
    @required String image,
    AssetBundle bundle,
    double placeholderScale,
    double imageScale = 1.0,
    this.excludeFromSemantics = false,
    this.imageSemanticLabel,
    this.fadeOutDuration = const Duration(milliseconds: 300),
    this.fadeOutCurve = Curves.easeOut,
    this.fadeInDuration = const Duration(milliseconds: 700),
    this.fadeInCurve = Curves.easeIn,
    this.width,
    this.height,
    this.fit,
    this.alignment = Alignment.center,
    this.repeat = ImageRepeat.noRepeat,
    this.matchTextDirection = false,
  }) : assert(placeholder != null),
        assert(image != null),
        placeholder = placeholderScale != null
            ? ExactAssetImage(placeholder, bundle: bundle, scale: placeholderScale)
            : AssetImage(placeholder, bundle: bundle),
        assert(imageScale != null),
        assert(fadeOutDuration != null),
        assert(fadeOutCurve != null),
        assert(fadeInDuration != null),
        assert(fadeInCurve != null),
        assert(alignment != null),
        assert(repeat != null),
        assert(matchTextDirection != null),
        image = img_provider.NetworkImageWithoutAuth(image, scale: imageScale),
        super(key: key);

  final ImageProvider placeholder;

  final  ImageProvider image;

  final Duration fadeOutDuration;

  final Curve fadeOutCurve;

  final Duration fadeInDuration;

  final Curve fadeInCurve;
  
  final double width;

  final double height;
  
  final BoxFit fit;

  final AlignmentGeometry alignment;
  
  final ImageRepeat repeat;

  final bool matchTextDirection;

  final bool excludeFromSemantics;
  
  final String imageSemanticLabel;

  Image _image({
    @required  ImageProvider image,
    ImageFrameBuilder frameBuilder,
  }) {
    assert(image != null);
    return Image(
      image: image,
      frameBuilder: frameBuilder,
      width: width,
      height: height,
      fit: fit,
      alignment: alignment,
      repeat: repeat,
      matchTextDirection: matchTextDirection,
      gaplessPlayback: true,
      excludeFromSemantics: true,
    );
  }

  @override
  Widget build(BuildContext context) {
    Widget result = _image(
      image: image,
      frameBuilder: (BuildContext context, Widget child, int frame, bool wasSynchronouslyLoaded) {
        if (wasSynchronouslyLoaded)
          return child;
        return _AnimatedFadeOutFadeIn(
          target: child,
          placeholder: _image(image: placeholder),
          isTargetLoaded: frame != null,
          fadeInDuration: fadeInDuration,
          fadeOutDuration: fadeOutDuration,
          fadeInCurve: fadeInCurve,
          fadeOutCurve: fadeOutCurve,
        );
      },
    );

    if (!excludeFromSemantics) {
      result = Semantics(
        container: imageSemanticLabel != null,
        image: true,
        label: imageSemanticLabel ?? '',
        child: result,
      );
    }

    return result;
  }

}

class _AnimatedFadeOutFadeIn extends ImplicitlyAnimatedWidget {
  const _AnimatedFadeOutFadeIn({
    Key key,
    @required this.target,
    @required this.placeholder,
    @required this.isTargetLoaded,
    @required this.fadeOutDuration,
    @required this.fadeOutCurve,
    @required this.fadeInDuration,
    @required this.fadeInCurve,
  }) : assert(target != null),
        assert(placeholder != null),
        assert(isTargetLoaded != null),
        assert(fadeOutDuration != null),
        assert(fadeOutCurve != null),
        assert(fadeInDuration != null),
        assert(fadeInCurve != null),
        super(key: key, duration: fadeInDuration + fadeOutDuration);

  final Widget target;
  final Widget placeholder;
  final bool isTargetLoaded;
  final Duration fadeInDuration;
  final Duration fadeOutDuration;
  final Curve fadeInCurve;
  final Curve fadeOutCurve;

  @override
  _AnimatedFadeOutFadeInState createState() => _AnimatedFadeOutFadeInState();
}

class _AnimatedFadeOutFadeInState extends ImplicitlyAnimatedWidgetState<_AnimatedFadeOutFadeIn> {
  Tween<double> _targetOpacity;
  Tween<double> _placeholderOpacity;
  Animation<double> _targetOpacityAnimation;
  Animation<double> _placeholderOpacityAnimation;

  @override
  void forEachTween(TweenVisitor<dynamic> visitor) {
    _targetOpacity = visitor(
      _targetOpacity,
      widget.isTargetLoaded ? 1.0 : 0.0,
          (dynamic value) => Tween<double>(begin: value),
    );
    _placeholderOpacity = visitor(
      _placeholderOpacity,
      widget.isTargetLoaded ? 0.0 : 1.0,
          (dynamic value) => Tween<double>(begin: value),
    );
  }

  @override
  void didUpdateTweens() {
    _placeholderOpacityAnimation = animation.drive(TweenSequence<double>(<TweenSequenceItem<double>>[
      TweenSequenceItem<double>(
        tween: _placeholderOpacity.chain(CurveTween(curve: widget.fadeOutCurve)),
        weight: widget.fadeOutDuration.inMilliseconds.toDouble(),
      ),
      TweenSequenceItem<double>(
        tween: ConstantTween<double>(0),
        weight: widget.fadeInDuration.inMilliseconds.toDouble(),
      ),
    ]));
    _targetOpacityAnimation = animation.drive(TweenSequence<double>(<TweenSequenceItem<double>>[
      TweenSequenceItem<double>(
        tween: ConstantTween<double>(0),
        weight: widget.fadeOutDuration.inMilliseconds.toDouble(),
      ),
      TweenSequenceItem<double>(
        tween: _targetOpacity.chain(CurveTween(curve: widget.fadeInCurve)),
        weight: widget.fadeInDuration.inMilliseconds.toDouble(),
      ),
    ]));
    if (!widget.isTargetLoaded && _isValid(_placeholderOpacity) && _isValid(_targetOpacity)) {
      // Jump (don't fade) back to the placeholder image, so as to be ready
      // for the full animation when the new target image becomes ready.
      controller.value = controller.upperBound;
    }
  }

  bool _isValid(Tween<double> tween) {
    return tween.begin != null && tween.end != null;
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      fit: StackFit.passthrough,
      alignment: AlignmentDirectional.center,
      // Text direction is irrelevant here since we're using center alignment,
      // but it allows the Stack to avoid a call to Directionality.of()
      textDirection: TextDirection.ltr,
      children: <Widget>[
        FadeTransition(
          opacity: _targetOpacityAnimation,
          child: widget.target,
        ),
        FadeTransition(
          opacity: _placeholderOpacityAnimation,
          child: widget.placeholder,
        ),
      ],
    );
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(DiagnosticsProperty<Animation<double>>('targetOpacity', _targetOpacityAnimation));
    properties.add(DiagnosticsProperty<Animation<double>>('placeholderOpacity', _placeholderOpacityAnimation));
  }
}

NetworkImageWithoutAuth

abstract class NetworkImageWithoutAuth extends ImageProvider<NetworkImageWithoutAuth> {
  /// Creates an object that fetches the image at the given URL.
  ///
  /// The arguments [url] and [scale] must not be null.
  const factory NetworkImageWithoutAuth(String url, { double scale, Map<String, String> headers }) = network_image.NetworkNoAuthImage;

  /// The URL from which the image will be fetched.
  String get url;

  /// The scale to place in the [ImageInfo] object of the image.
  double get scale;

  /// The HTTP headers that will be used with [HttpClient.get] to fetch image from network.
  ///
  /// When running flutter on the web, headers are not used.
  Map<String, String> get headers;

  @override
  ImageStreamCompleter load(NetworkImageWithoutAuth key);
}

**NetworkNoAuthImage **

import 'dart:async';
import 'dart:io';
import 'dart:typed_data';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

import 'img_provider.dart';
import 'dart:ui' as ui;
import 'img_provider.dart' as img_provider;
/**
 * author walke
 * date: 2020/5/20
 * des:
 */
/// The dart:io implemenation of [NetworkImageWithoutAuth].
 class NetworkNoAuthImage extends ImageProvider<img_provider.NetworkImageWithoutAuth> implements img_provider.NetworkImageWithoutAuth {
  /// Creates an object that fetches the image at the given URL.
  ///
  /// The arguments [url] and [scale] must not be null.
  const NetworkNoAuthImage(this.url, { this.scale = 1.0, this.headers })
      : assert(url != null),
        assert(scale != null);

  @override
  final String url;

  @override
  final double scale;

  @override
  final Map<String, String> headers;

  @override
  Future<NetworkNoAuthImage> obtainKey(ImageConfiguration configuration) {
    return SynchronousFuture<NetworkNoAuthImage>(this);
  }

  @override
  ImageStreamCompleter load(NetworkImageWithoutAuth key) {
    // Ownership of this controller is handed off to [_loadAsync]; it is that
    // method's responsibility to close the controller's stream when the image
    // has been loaded or an error is thrown.
    final StreamController<ImageChunkEvent> chunkEvents = StreamController<ImageChunkEvent>();

    return MultiFrameImageStreamCompleter(
      codec: _loadAsync(key, chunkEvents),
      chunkEvents: chunkEvents.stream,
      scale: key.scale,
      informationCollector: () {
        return <DiagnosticsNode>[
          DiagnosticsProperty<ImageProvider>('Image provider', this),
          DiagnosticsProperty<img_provider.NetworkImageWithoutAuth>('Image key', key),
        ];
      },
    );
  }

  // Do not access this field directly; use [_httpClient] instead.
  // We set `autoUncompress` to false to ensure that we can trust the value of
  // the `Content-Length` HTTP header. We automatically uncompress the content
  // in our call to [consolidateHttpClientResponseBytes].
  static final HttpClient _sharedHttpClient = HttpClient()..autoUncompress = false;

  static HttpClient get _httpClient {
    HttpClient client = _sharedHttpClient;
    assert(() {
      if (debugNetworkImageHttpClientProvider != null)
        client = debugNetworkImageHttpClientProvider();
      return true;
    }());
    return client;
  }

  Future<ui.Codec> _loadAsync(
      NetworkNoAuthImage key,
      StreamController<ImageChunkEvent> chunkEvents,
      ) async {
    try {
      assert(key == this);
      //解决证书校验通不过的问题  参考 https://www.jianshu.com/p/a09532ad4b3d
      _httpClient.badCertificateCallback = (X509Certificate cert,String host,int port){
        return true;
      };
      final Uri resolved = Uri.base.resolve(key.url);
      final HttpClientRequest request = await _httpClient.getUrl(resolved);
      headers?.forEach((String name, String value) {
        request.headers.add(name, value);
      });
      final HttpClientResponse response = await request.close();
      if (response.statusCode != HttpStatus.ok)
        throw NetworkImageLoadException(statusCode: response.statusCode, uri: resolved);

      final Uint8List bytes = await consolidateHttpClientResponseBytes(
        response,
        onBytesReceived: (int cumulative, int total) {
          chunkEvents.add(ImageChunkEvent(
            cumulativeBytesLoaded: cumulative,
            expectedTotalBytes: total,
          ));
        },
      );
      if (bytes.lengthInBytes == 0)
        throw Exception('NetworkImage is an empty file: $resolved');

      return PaintingBinding.instance.instantiateImageCodec(bytes);
    } finally {
      chunkEvents.close();
    }
  }

  @override
  bool operator ==(dynamic other) {
    if (other.runtimeType != runtimeType)
      return false;
    final NetworkImageWithoutAuth typedOther = other;
    return url == typedOther.url
        && scale == typedOther.scale;
  }

  @override
  int get hashCode => ui.hashValues(url, scale);

  @override
  String toString() => '$runtimeType("$url", scale: $scale)';

}

使用:

 FadeInImage.assetNetwork(
                placeholder: 'images/cherry.png',
                image: 'https://image-.xxxxxxxxxx.jpg', // 公司地址就不贴出来了
                width: 200,
                height: 120,
 ),

PS:包结构:

flutter_boke.png

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。