5 min read

[Flutter] Flutter를 이용한 Android와 협업후기

과연 Flutter와 Android Native 협업이 가능할까? 직접 인터페이스를 통해 통합하고, 빌드해본 결과물 공유
[Flutter] Flutter를 이용한 Android와 협업후기
Photo by krakenimages / Unsplash

결론만 말하자면

사실 이 글을 작성할까 말까 고민을 했는데 혹시 기존 네이티브 프로젝트에 Flutter를 적용을 생각하신 분들이 있을까봐 결론을 먼저 말하자면 충분히 사용해도 된다

간단하게 사용해봤다고 생각할 수 있지만 기본적인 UI적용과 로컬DB를 이용한 데이터 입/출입 네트워크를 이용하여 데이터를 가져와 화면에 표시하고, 가공하여 저장및 네이티브단으로 데이터를 넘겨줄 수 있다.

그 과정에서 필요한 추가적인 리소스는 Flutter와 네이티브간의 인터페이스를 정의하는 일인데 만약 독단적인 Flutter화면이 필요하다면 더더욱 적용할 가치가 있다고 생각한다.
기존의 네이티브의 설계및 개발과정에 비해 단순해진 작업으로 효율이 높아지는것을 느꼈다.

thumbnail

해커톤 참여

22년 08월 26일 금요일 서울시에서 진행하는 해커톤에 참여하였다. 고등학생 이후 처음 참여하는 해커톤인데 연차를 사용하고, 참가하였다. 이전에 사이드 프로젝트를 같이 했던 디자이너분이 연락을 해주셔서 처음 본 분들과 팀을 맺고 진행하였다.
주제는 제목처럼 서울시에있는 IoT 데이터를 활용하어 시간안에 앱을 제작하여 발표하고, 시상하는 방식으로 진행되었다.

다행히 잘 만들어졌고, 발표또한 무사히 잘 마쳐서 우수상 수상과 함께 200만원의 상금을 받았다(아직 안들어왔다)

01

Flutter를 이용한 협업

최근에 Flutter를 많이 사용하고, 활용하려고 노력하고 있다. 개발을 진행하는 개발자 분에게 양해를 구하고 내 파트는 Flutter로 UI를 작업하여 라이브러리 형태로 임포트하여 바로 사용할 수 있도록 만들겠다고 했다. 저번 외주작업을 진행할 때에는 .AAR 파일을 만들어서 진행했지만 이번에는 속도가 중요할것 같아 build시 나오는 라이브러리 repo를 Github에 업로드하고, 업데이트될 때마다 Pull받아 바로 적용할 수 있도록 하였다.
기본적인 연동 방법은 Flutter 웹 사이트의 가이드를 따르면 금방 적용된다.

Flutter UI 작업

아래 이미지는 내가 작업을 진행했던 화면이다. Native안드로이드로도 충분히 작업을 진행할 수 있었지만 Flutter를 이용하는것이 UI를 개발하는것이 더 빠를것이라고 판단했다.

디자이너를 위하여

이미지에서 보이는것처럼 CollapsingToolbarLayout를 이용하면 쉽게 AppBar가 스크롤 되면서 가려지고, Title이 중앙에 위치하게 작업을 진행하면 되는데 Flutter에서는 조금 달랐기 때문에 코드를 수정했다.
비슷한 기능으로 SliverAppBar를 이용하고, 애니메이션을 이용하여 비슷하게 보이도록 개발을 진행하였다. SliverAppBar를 이용하면 충분히 비슷하게 만들 수 있지만 디자이너가 열심히 만든 디자인을 그대로 구현하기 위해서 찾아보았다.
역시 StackOverflow에 답이 나와있다. StackOverflow에 있는 코드를 수정하여 사용하기로 했다.

https://stackoverflow.com/questions/63231817/custom-flexiblespacebar-widget

SliverAppBarFlexible위젯을 이용하여 Flexible 위젯의 height를 이용하여 opacity를 계산하여 이미지를 보이게 함과 동시에 아래 주석처리되어있는 opacityReverse를 적용하여 스크롤이 되는 순간 숨겨져있는 AppBar가 보이게 만들었다.
상세 코드는 아래에서 확인할 수 있다.

https://gist.github.com/lowapple/6f80e4549192617782c085737f15d3f0

import 'package:flutter/material.dart';
import 'dart:math' as math;

import 'package:flutter/services.dart';

class ParkingHeader extends StatelessWidget {
  final String parkingTitle;
  final List<String> parkingTags;
  final String parkingAddress;

  const ParkingHeader(
      {Key? key,
      required this.parkingTitle,
      required this.parkingTags,
      required this.parkingAddress})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, c) {
        final statusHeight = MediaQuery.of(context).viewPadding.top;
        final settings = context
            .dependOnInheritedWidgetOfExactType<FlexibleSpaceBarSettings>()!;
        final deltaExtent = settings.maxExtent - settings.minExtent;
        final t =
            (1.0 - (settings.currentExtent - settings.minExtent) / deltaExtent)
                .clamp(0.0, 1.0);
        final fadeStart = math.max(0.0, 1.0 - kToolbarHeight / deltaExtent);
        const fadeEnd = 1.0;
        final opacity = 1.0 - Interval(fadeStart, fadeEnd).transform(t);
        // final opacityReverse = 1.0 - opacity;
        // final c =
        //     (255 - (255 * Interval(fadeStart, fadeEnd).transform(t))).toInt();
        // final cc = Color.fromARGB(255, c, c, c);
        return Stack(
          children: [
            Container(
              color: Colors.white,
              height: 280,
              width: double.infinity,
              child: Opacity(
                opacity: opacity,
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Flexible(
                      child: Container(
                        color: Colors.blue,
                        width: double.infinity,
                        child: Image.asset(
                          'assets/parking_image.png',
                          fit: BoxFit.cover,
                        ),
                      ),
                    ),
                    ...
                  ],
                ),
              ),
            ),
            ...
          ],
        );
      },
    );
  }
}

사실 위 부분이 제일 시간이 오래 걸렸다. 중간에 다시 Native로 할까 했는데 생각보다 보이는 부분만 교묘하게 수정하여 실제 구현된 것처럼 보이게 하는 작업이 잘 맞아 떨어져서 해결이 가능했다.