[Project] - [Flutter-web] 새해 말씀 뽑기


이번에 교회에서 말씀뽑기 관련한 웹앱을 만들어 달라는 요청을 받아서 만들게되었다.

화면은 크게 2개 였다.

  • 메인 화면
  • 말씀 랜덤 생성 화면

주요 기능으로는 다음과 같았다.

주요 기능


  • 랜덤으로 말씀 카드 생성 (이미지 로드)
  • 카드가 돌아가는 애니메이션
  • 이미지 저장
  • Url을 통한 웹 redirect

그래서 Flutter를 통해 SPA app을 만들어 보기로 했다.

그 중 만났던 이슈들도 기록해보겠다.

이슈사항들


  1. flutter web에서의 이미지 로드
  2. 웹사이즈 변경을 통한 rerendering에 대한 futurebuilder 이슈
  3. 이미지 저장 이슈
  4. build 이슈

Flutter web


참고로 이걸 만든 시점에서는 아직 web은 beta에서만 지원하기 때문에

공홈에서 나온 설명대로 flutter branch를 beta로 변경하여 web 설정을 enable해줘야한다.

이 부분은 https://flutter.dev/docs/get-started/web 을 따라 하도록 하자.

화면


일단 화면 구성은 flutter를 활용하게 빠르게 작성하였다.

크게 2가지 화면은 다음과 같이 구성되었다.

여기서 카드 이미지가 로드 된 후 회전하여 카드가 보이도록 하였다.

기능


1. 랜덤 말씀 카드 생성

var rng_num = new Random().nextInt(112 - 1) + 1;
image_num = rng_num.toString();
if (rng_num < 10) {
  image_num = "0" + rng_num.toString();
}

image_url = 'assets/img/card_$image_num.jpg';

간단하게 Random을 활용하여 112개 카드 갯수 안에서 (card_01~card112) 랜덤으로 가져오게 하였다.

2. 카드 회전

카드회전은 FlipCard라는 Widget을 만들어 이미지 로드 완료후 0.5초뒤에 회전 하도록 설정하였다.

class FlipCard extends StatefulWidget {
  FlipCard({Key key, this.frontWidget, this.backWidget}) : super(key: key);

  final Widget frontWidget;
  final Widget backWidget;
  @override
  _FlipCardState createState() => _FlipCardState();
}
class _FlipCardState extends State<FlipCard>
    with SingleTickerProviderStateMixin {
  Timer _timer;
  var isLoading = false;
  AnimationController controller;

  @override
  void initState() {
    controller =
        AnimationController(duration: Duration(milliseconds: 500), vsync: this);
    startAni();
    super.initState();
  }

  void startAni() async {
    _timer = new Timer(const Duration(milliseconds: 1000), () {
      controller.forward();
    });
  }

  @override
  void dispose() {
    controller.dispose();
    _timer.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        child: WidgetFlipper(
          controller: controller,
          frontWidget: widget.frontWidget,
          backWidget: widget.backWidget,
        ),
      ),
    );
  }
}

3. 이미지 저장

이걸 하면서 알게 된 사실은

모바일 웹에서 모바일폰 갤러리로 바로 이미지를 저장하게 못한다는 것이였다.

방법은 사용자에게 img가 있는 url로 이동시키고 이미지를 롱 클릭하여 스스로 저장하게 하는 방법이였다.

이미지를 바로 저장하게 하면 웹에서는 잘 동작하나 모바일 웹에서는 동작하지 않았다.

그래서 다음과 같은 방법으로 이미지 링크를 연결하였다.

void _saveImage(imageUrl) async {
    final anchor = html.document.createElement('a') as html.AnchorElement
      ..href = imageUrl
      ..style.display = 'none';
    html.document.body.children.add(anchor);

    // download
    anchor.click();

    // cleanup
    html.document.body.children.remove(anchor);
    html.Url.revokeObjectUrl(imageUrl);
}

직접 이미지를 저장하게 하는 방법은 methodChannel을 이용하는 방법이 있다.

var response = await get(imageUrl);
final DateTime now = DateTime.now();

js.context.callMethod("webSaveAs", [
  html.Blob([response.bodyBytes]),
   "$now.jpg"
]);

이렇게 하려면 index.html에 js method를 구현해주어야 한다.

...
<script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/1.3.8/FileSaver.min.js"></script>

<script>
	function webSaveAs(blob, name) {
		console.log(name);
		saveAs(blob, name);
	}
</script>
<script src="main.dart.js" type="application/javascript"></script>
...

4. Url을 이용한 redirect

이건 위의 방법을 활용하여 쉽게 구현이 가능하다 (a tag)

// 꿈교 유튜브 보내기
var youtubeUrl = 'https://www.youtube.com/channel/UCaNoaz05HCffa_61Jf_9Qng';

final anchor = html.document.createElement('a') as html.AnchorElement
                ..href = youtubeUrl
                ..style.display = 'none';

html.document.body.children.add(anchor);

anchor.click();

// cleanup
html.document.body.children.remove(anchor);
html.Url.revokeObjectUrl(youtubeUrl);

이슈 사항들


1. flutter web image

flutter에서 기본적으로 assets을 쓰려면 pubspec.yaml에서 설정해주어 사용하면 된다.

---
assets:
  - assets/

그리고 Image.assets(‘~~’)를 사용하면 되지만

flutter web에서는 load가 되지 않는다.

그 이유는 web에서 이미지를 가져오는것 또한 network처리이기 때문에다

Flutter web에서는 Image.network()를 쓰도록 하자

2. 웹사이즈 변경에 따른 rerendering 과 futurebuilder

firebase hosting을 사용함으로 인해 이미지 로드가 오랜 시간이 걸렸다.

그 이슈를 해결하고자 futurebuilder를 통해 이미지 로드 후 카드가 생성되도록 하였다.

그런데 문제는

윈도우 사이즈가 변경되면 전체 앱이 rerendering이 되고

그 rerendering에 따라 future가 다시 호출되면서 이미지를 다시 로드하였다.

해당 문제는 블로그 하나를 발견하여 해결하였다.

async 라이브러를 통해 해결했다.

...
final AsyncMemoizer _memoizer = AsyncMemoizer();
...

Future<dynamic> _fetchData(var imageUrl) {
    return this._memoizer.runOnce(() async {
      return get(imageUrl);
    });
  }

...
child: FutureBuilder(
    future: _fetchData('assets/img/card_$image_num.jpg'),
...

3. 이미지 저장 이슈

위에 잘 설명해놓았다.

4. flutter build web

web으로 build하기 위해 위의 명령어를 사용하면

build 폴더가 생성되며 build된 사항들이 적용된다.

여기서 중요한점!

assets폴더에 있는 이미지들은 직접 build 폴더 안으로 복사해서 옮겨주어야 한다!

마무리


이번 프로젝트를 통해 사이드 프로젝트를 꾸준히 해볼까 하는 생각이 들었다.

앞으로도 이런 작은 프로젝트들을 몇개 진행해서 내 project 카테고리를 채워가야겠다.