스터디/Flutter+Dart

Flutter Network Image (웹 이미지, 캐슁, 만료일)

Dalmangyi 2019. 9. 16.

이번 게시글에선 네트워크 이미지에 대해서 다뤄보겠습니다

급하신분은 '3.extended_image'로 이동하셔 보셔도 됩니다

 

서론

온라인에 있는 이미지를 다루다 보면 많은 문제에 부딛히게 됩니다 

 

1. 네트워크 코딩

   온라인에 있는 이미지는 해당 URL에 접속시도하고 접속하고 다운로드하고 실패할때 대처하는 코딩이 필요합니다

2. 최적화 (썸네일화,캐슁)

   이미지의 경우 다운로드해서 바로 보여주면 좋겠지만, 컴퓨터 자원의 한계로 인해서 보여주는 각 환경에 맞게  최적화를 해줘야 합니다.

   많은 최적화 방법이 있지만 그중 몇가지 예를 들면 원본 이미지 사이즈와 다르게 작은 이미지로 변경한걸 보여준다던가 (썸네일화)

   네트워크 이미지를 다운받아두었다가 다시 호출시에 다운받아 놓은 이미지를 보여주는 방법이 있습니다. (캐슁)

   (이 게시글에서 heic(이미지+동영상) 형태의 이미지를 보여주는 방법과 압축형 포맷 변환에 대해서는 다루지 않습니다.)

3. 대체 이미지 (PlaceHolder)

   이미지 용량이 크거나 네트워크 문제로 인해서 로딩이 오래 걸리는 경우,

   사용자에게 로딩중인 이미지를 보여줘야 사용자가 안심하게 됩니다.

4. 캐쉬 데이터 상세 설정 (만료일, 최대용량)

   원본 이미지를 최적화된 사이즈로 변경해서 저장할 경우, 무분별하게 계속 쌓이면 끝도 없이 저장공간을 차지하게 되서

   사용자에게 불편함을 주게 됩니다. 이걸 해결 하기 위해 만료일을 설정하기도 하고, 최대 저장할 수 있는 캐쉬 용량도

   설정해 놓으면 가장 오래된 이미지 순서대로 삭제해주는 기능도 필요합니다.

 

이미지는 우리에게 친근한 요소지만, 개발자에게 많은 부분을 신경 써야하는 요소가 되기도 합니다

 

 

 

개발시작!

웹 이미지 개발하기 전에 웹 이미지 주소를 확보해 둬야 합니다.

구글링을 좀 해보니 Picsum Photos 에서 웹 이미지를 간단히 제공해 주고 있었네요 

여기서는 다양한 이미지를 다양한 사이즈와 다양한 포맷, 다양한 방법으로 제공해 주고 있습니다. 

 

일단 흔한 jpg 포맷의 가로세로 200 사이즈인 이미지 주소를 확보해 보겠습니다

https://picsum.photos/200 이 주소를 호출하면 랜덤 id 주소를 리다이렉션 해줍니다. 

저는 https://picsum.photos/id/421/200/200 이 주소를 리다이렉션 받았습니다

id 421 jpg

 

코드는 아주 간단하게 Center > Column 안에 단계별로 Network Image를 추가할 겁니다

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Network Image',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: Text('Network Image'),
        ),
        body: MyHomePage()),
    );
  }
}

class MyHomePage extends StatefulWidget {

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

class _MyHomePageState extends State<MyHomePage> {

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          //추가할 위치
        ],
      ),
    );
  }
}

 

 

 

 

 

 

 

 

 


 

 

 

 

 

 

 

1. 네트워크 이미지를 받아오는 방법

 

 

1.1 Flutter 기본 패키지

네트워크 이미지를 불러오는 방법은 Flutter에서는 별도의 패키지 추가 없이 기본 패키지로도 간단히 구현이 가능합니다 

 

Image.network(
  'https://picsum.photos/id/421/200/200',
)

Image.network

 

 

 

 

1.2 Fade-In 

Flutter에서는 네트워크에서 이미지를 받아올 때, Fade-In 처리도 정말 간단합니다. 

단지 이름을 외워야 된다는 문제가....발생합니다. (flutter는 저같은 멍청이를 배려해주지 않습니다 ㅠㅠ)

FadeInImage.memoryNetwork(
  placeholder: kTransparentImage,
  image: 'https://picsum.photos/id/421/200/200',
);

 

kTransparentImage 를 사용하려면 pubspec.yaml에 패키지를 추가하고 dart파일에 import 해야합니다. 

dependencies:
  transparent_image: ^1.0.0
import 'package:transparent_image/transparent_image.dart';

 

fade-in network image. gif

 

 

 

 

1.3 Fade-In + PlaceHolder(Assets, GIF)

Fade-In만 사용하는 앱도 많지만, 이미지로 로딩을 보여주는 앱도 많기 때문에 한번 PlaceHolder도 불러와보겠습니다

 

1) 먼저 사전작업으로 PlaceHolder에 사용될 이미지를 구합니다.

   (loading.io 추천해요~)

https://loading.io

  

전 이녀석을 사용해 보겠습니다~

 

2) lib/assets 폴더를 만들고 gif를 추가합니다

spinner.gif가 위치한 모습

 

 

3) pubspec.yaml 에 asset을 추가합니다

flutter:
  ...
  assets:
    - lib/assets/spinner.gif

  (탭 갯수 조심, ...은 포함안하셔도 되요)

 

 

 

4) main.dart에 코딩

memoryNetwork에서 assetNetwork로 함수 이름이 바꼇어요. 

FadeInImage.assetNetwork(
  placeholder: 'lib/assets/spinner.gif',
  image: 'https://picsum.photos/id/421/200/200',
);

로딩하는 동안 spinner.gif 보여줌. gif

 

 

 

 

 


 

 

 

 

2. cached_network_image

네트워크 이미지를 캐쉬 처리하기 위해서 다양한 방법도 있지만, 

cached_network_image 패키지를 이용해서 사용하는 방법을 소개할까 합니다

 

 

 

 

 

1) 패키지 추가

pubspec.yaml 파일에 cached_network_image 패키지를 추가해주세요 

dependencies:
  cached_network_image: ^1.1.1

 

 

 

 

 

2) 코드에 패키지 import

import 'package:cached_network_image/cached_network_image.dart';

 

 

 

 

 

 

3) 캐쉬 네트워크 코드 구현

cached_network_image에서는 2가지 방식으로 지원하고 있어요. 

 

첫 번째는 CachedNetworkImage 클래스를 이용한 방법

CachedNetworkImage(
  imageUrl: 'https://picsum.photos/id/421/200/200',
  placeholder: (context, url) => new CircularProgressIndicator(),
  errorWidget: (context, url, error) => new Icon(Icons.error),
),

로딩중(PlaceHolder)와 에러상황(ErrorWidget)을 Widget 형태로 지원해 주고 있습니다.

 

두 번째는 CachedNetworkImageProvider를 이용한 방법 입니다

Image(image: new CachedNetworkImageProvider('https://picsum.photos/id/421/200/200'))

ImageProvider를 상속받고 있어서 꼭 Image로 감싸줘야 합니다.

 

 

 

 

 

4) 구동 화면 (1)

cached_network_image. gif

같은 경로의 이미지임에도 불구하고 CircularProgressIndicator 위젯이 있는 쪽이 좀 더 느리게 로딩되는것을 볼 수 있습니다. 

코드 순서를 바꿔봤지만 결과는 마찬가지로 CircularProgressIndicator 위젯이 있는쪽이 좀 더 느리더군요.

 

 

 

 

 

5) 구동화면 (2)

이미지 사이즈가 200*200 사이즈 밖에 안되다보니 캐쉬가 되던 안되던 같은 속도를 내길레 사이즈를 좀 키워봤습니다 

5000*5000으로 키운 다른 이미지(2.2MB)를 호출 해 보았습니다

처음 호출시에는 8초 정도 걸렸지만, 앱을 재실행하게 되면 2초 정도로 주는 것을 확인 할 수 있습니다.

처음엔 느리지만, 나중엔 아니란다 gif.

아무래도 이미지 사이즈를 변경한 썸네일로 저장하는게 아닌, 데이터를 그냥 저장(Cache)시키는것이다보니 다시 불러오더라도 시간이 걸리게 되나 봅니다

data폴더 안에 패키지 폴더로 가보니 cache파일이 원본 그대로 있는 것을 확인했습니다. 

물론 이름은 url을 저장할 수 없으니 url을 hash처리하여서 파일명으로 사용 되어있었습니다. 

 

 

 

 

 

 

+) 만료일 설정

CachedNetworkImage에도 BaseCacheManager를 이용하면, 얼마나 지나야 캐쉬 파일을 갱신할지를 정할 수 있습니다. 

설정하지 않아도 기본적으로 30일로 설정이 되어있습니다.

 

변경하고 싶을땐 조~큼 귀찮습니다.

먼저 패키지를 3개를 추가합니다. 

dependencies:
  flutter_cache_manager: ^1.1.1
  path_provider: ^1.3.0
  path: ^1.6.4

캐쉬 매니저에 사용되는 flutter_cache_manager와 폴더 주소를 가져와 주는 path_provider, 경로 연산을 쉽게 해주는 path가 필요합니다.

 

그리고 패키지에 맞게 import도 3개...추가

import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;

 

BaseCacheManager을 상속도 받아야 합니다.

class CustomCacheManager extends BaseCacheManager {
  static const key = "customCache";

  static CustomCacheManager _instance;

  factory CustomCacheManager() {
    if (_instance == null) {
      _instance = new CustomCacheManager._();
    }
    return _instance;
  }

  CustomCacheManager._() : super(key,
      maxAgeCacheObject: Duration(days: 7),
      maxNrOfCacheObjects: 20);

  Future<String> getFilePath() async {
    var directory = await getTemporaryDirectory();
    return p.join(directory.path, key);
  }

}

날짜를 7일로 변경하고, 캐쉬되는 갯수를 20개로 설정하였습니다.

갯수는 리스트뷰를 보여줄땐 생각보다 많이 필요해서 적절히 테스트 하면서 조정하시기 바랍니다.

위치는 임시폴더의 customCache로 설정하였습니다

 

그리고 사용하려면 CachedNetworkImage에 있는 cacheManager에 할당해 주면 됩니다

CachedNetworkImage(
  imageUrl: imageUrl,
  width: 200,
  height: 200,
  fadeInDuration: const Duration(milliseconds: 100),
  fadeOutDuration: const Duration(milliseconds: 100),
  placeholder: (context, url) => new CircularProgressIndicator(),
  errorWidget: (context, url, error) => new Icon(Icons.error),
  
  cacheManager: CustomCacheManager(),
),

 

 

 

 


 

 

 

 

 

 

3. extended_image 

지금 이 글을 쓰는 시점에서 가장 많은 기능을 가진 이미지 패키지가 아닐까 싶습니다

 

1) 패키지 설치

dependencies:
  extended_image: ^0.5.8

 

2) import

import 'package:extended_image/extended_image.dart';

 

3) 코드 

ExtendedImage.network(
  imageUrl,
  width: 200,
  height: 200,
  fit: BoxFit.fill,
  cache: true,
  border: Border.all(color: Colors.red, width: 1.0),
  shape: BoxShape.circle,
  borderRadius: BorderRadius.all(Radius.circular(30.0)),
)

네트워크 로딩과 사이즈 할당, 그리고 화면에 보여주는 scaleType(fit), 캐쉬 여부, 테두리선, 테두리모양, 테두리 두깨까지 정말 편하게 설정이 가능합니다

 

첫 호출시 걸린 시간 10초. gif

 

네트워크 환경과 성능에 따라 다르겠지만 1.7메가 이미지를 화면에 보여주는데 10초가 소모되었습니다.

 

다시 해보니 2초!. gif

다시 시도해보니 캐쉬 되어있어서 2초만에 로딩된걸 볼 수 있습니다

 

 

 

4) 상태에 따른 컨트롤

상태는 로딩중, 완료, 실패로 3가지로 나뉘게 됩니다 

이 상태마다 Widget을 반환해서 ExtendedImage.network Widget 영역을 꾸밀 수 있습니다

loadStateChanged: (ExtendedImageState state) {
  switch (state.extendedImageLoadState) {
    case LoadState.loading: 
    break;
    case LoadState.completed: 
    break;
    case LoadState.failed:
    break;
  }
},

 

loading화면

spinner.gif로 꾸미고

case LoadState.loading:
  return Image.asset(
    "lib/assets/spinner.gif",
    fit: BoxFit.fill,
  );
break;

 

completed화면

서서히 Fade시켜서 보여주고

class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin {

  AnimationController _controller;

  @override
  void initState() {
    _controller = AnimationController(
        vsync: this,
        duration: Duration(seconds: 3),
        lowerBound: 0.0,
        upperBound: 1.0);
    super.initState();
  }
  
  ...
  
}

_MyHomePageState에 with SingleTickerProviderStateMixin를 추가합니다. 

initState() 함수에 AnimationController를 초기화 해 줍니다.

case LoadState.completed:
  _controller.forward();
  return FadeTransition(
    opacity: _controller,
    child: ExtendedRawImage(
      image: state.extendedImageInfo?.image,
      width: 200,
      height: 500,
    ),
  );
break;

completed 구간은 return을 안해도 이미지가 정상적으로 나오지만 멋져보이기 위해서 FadeTransition을 추가해서 보여줍니다

 

 

failed화면

실패 아이콘이 나오고 터치하면 재시도 할 수 있게 만들어 줍니다

case LoadState.failed:
  return GestureDetector(
    child: Image.asset(
      "lib/assets/failed.png",
      fit: BoxFit.fill,
    ),
    onTap: () {
      state.reLoadImage();
    },
  );
break;

 

 

5) 상태 컨트롤 연결 후 모습

정상 URL 로딩후, 비정상 URL 로딩 gif

 

6) 이외 기능

  • URL 마다 혹은 전체 캐쉬 관리 기능 (용량, 기간, 갯수와 같은 상세 컨트롤은 안되는거 같습니다)
  • 크롭 기능
  • 줌인 기능 
  • 크롭+줌인 => 에디터 기능
  • Paint기능 (마스킹)

 

 

 


 

 

 

 

4. 마치며

이미지에 대한 글을 쓰다보니 결국엔 ExtendedImage 리뷰가 되어버렸습니다..

 

extendedImage에서는 왠만한 이미지 라이브러리 기능들이 다 포함되어있었고 

상태 관리도 아주 편한 앱이였습니다

 

한 가지 아쉬웠던것은 캐쉬 작업을 할때, 원본 이미지를 통채로 저장하고 있었고

이로 인해서 매번 로딩을 해도 파일용량이 크게 되면 기본적으로 로딩시간이 많이 소모되었던 점이 있었습니다

한번 다운로드 할땐 느리고 캐쉬로 저장하면서 썸네일 형태로도 저장되었으면 하는데 그게 지원이 안되서 아쉽더라구요.

 

 

 

댓글