Flutter 튜토리얼 56편: Android 플랫폼 통합
요약
핵심 요지
- 문제 정의: Flutter 앱이 Android 플랫폼에서 네이티브처럼 동작하려면 플랫폼별 설정과 통합이 필요하다.
- 핵심 주장: Android SDK 설정, 스플래시 화면, Platform Views1, 상태 복원으로 네이티브 수준의 앱을 만들 수 있다.
- 주요 근거: Android Studio와 SDK 도구를 통해 에뮬레이터와 실제 기기에서 테스트할 수 있다.
- 실무 기준: 스플래시 화면은 Android 12 SplashScreen API를 활용하고, Platform Views로 네이티브 뷰를 임베드한다.
- 한계: Platform Views는 성능 오버헤드가 있으며, Android 버전별 호환성을 고려해야 한다.
문서가 설명하는 범위
- Android 개발 환경 설정
- 스플래시 화면 구현
- Platform Views로 네이티브 뷰 임베드
- 앱 상태 복원
읽는 시간: 20분 | 난이도: 중급
참고 자료
- Set up Android development - Android 개발 환경 설정
- Adding a splash screen - 스플래시 화면
- Platform Views - 네이티브 뷰 임베드
- Restore state on Android - 상태 복원
문제 상황
Flutter 앱을 Android에서 실행하려면 개발 환경 설정이 필요합니다. 또한 앱이 시작될 때 보여줄 스플래시 화면, 네이티브 Android 뷰 통합, 시스템에 의해 종료된 앱의 상태 복원 등 플랫폼 특화 기능이 필요합니다.
Android 통합이 필요한 상황
개발 환경 → Android Studio, SDK, 에뮬레이터 설정시작 화면 → 앱 로딩 중 스플래시 화면 표시네이티브 뷰 → 지도, 웹뷰 등 기존 Android 뷰 활용상태 복원 → 시스템이 앱을 종료한 후 상태 유지문제는 다음과 같습니다.
- Android SDK와 도구 설정이 복잡하다.
- 스플래시 화면은 Android 버전별로 구현 방식이 다르다.
- 네이티브 뷰를 Flutter에 임베드하면 성능 문제가 발생할 수 있다.
- 시스템이 앱을 강제 종료하면 사용자 상태가 손실된다.
해결 방법
Flutter는 Android 플랫폼과의 긴밀한 통합을 지원합니다. Android Studio를 통한 개발 환경 설정부터 네이티브 기능 통합까지 단계별로 알아봅니다.
챕터 1: Android 개발 환경 설정
Why
NOTEFlutter 앱을 Android 기기나 에뮬레이터에서 실행하려면 Android SDK와 개발 도구가 필요합니다. Android Studio가 이 모든 것을 한 번에 설치하고 관리할 수 있게 해줍니다.
Flutter SDK → Android Studio → Android SDK → 에뮬레이터/기기
What
NOTEAndroid 개발에 필요한 구성 요소입니다.
구성 요소 역할 Android Studio IDE와 SDK 관리 도구 Android SDK Android 앱 빌드에 필요한 라이브러리 SDK Platform-Tools ADB 등 명령줄 도구 Android Emulator 가상 기기에서 앱 테스트
How
TIP1. Android Studio 설치
Android Studio 공식 사이트에서 다운로드하여 설치합니다.
2. SDK Manager에서 필요한 도구 설치
Android Studio 실행 후 Tools > SDK Manager를 엽니다.
SDK Platforms 탭에서:
- API Level 36 이상 선택
SDK Tools 탭에서 다음을 선택합니다:
- Android SDK Build-Tools
- Android SDK Command-line Tools
- Android Emulator
- Android SDK Platform-Tools
3. 라이선스 동의
Terminal window flutter doctor --android-licenses각 라이선스를 읽고 ‘y’를 입력하여 동의합니다.
4. 에뮬레이터 설정
Tools > Device Manager에서 Create Virtual Device를 클릭합니다.
1. Phone 또는 Tablet 선택2. 기기 정의 선택 (예: Pixel 6)3. 시스템 이미지 선택 (x86 또는 ARM)4. Graphics acceleration: Hardware 선택5. Finish 클릭5. 설정 확인
Terminal window flutter doctorAndroid toolchain과 Android Studio 항목이 녹색 체크로 표시되어야 합니다.
Terminal window flutter emulators && flutter devices설정된 에뮬레이터와 연결된 기기 목록이 표시됩니다.
Watch out
WARNING실제 Android 기기에서 테스트하려면 개발자 옵션을 활성화해야 합니다.
설정 → 휴대전화 정보 → 빌드 번호 7회 탭설정 → 개발자 옵션 → USB 디버깅 활성화USB로 연결 후 기기에서 디버깅 허용 팝업이 뜨면 승인합니다.
결론: Android Studio와 SDK를 설치하고 라이선스에 동의하면 Flutter 앱을 에뮬레이터나 실제 기기에서 실행할 수 있습니다.
챕터 2: 스플래시 화면 구현
Why
NOTE앱이 시작될 때 Flutter 엔진이 초기화되는 동안 빈 화면이 보이면 사용자 경험이 나빠집니다. 스플래시 화면을 설정하면 앱 로고나 브랜드 이미지를 표시하며 자연스럽게 앱을 시작할 수 있습니다.
앱 실행 → 스플래시 화면 표시 → Flutter 엔진 초기화 → 앱 화면
What
NOTEAndroid 스플래시 화면은 두 가지 방식으로 구현할 수 있습니다.
방식 지원 버전 특징 LaunchTheme 모든 버전 windowBackground로 Drawable 표시 SplashScreen API Android 12+ 아이콘 애니메이션, 배경색 등 세부 설정 Android 12 미만과 이상을 모두 지원하려면 두 가지를 함께 구현합니다.
How
TIP1. 스플래시 이미지 준비
android/app/src/main/res/drawable/폴더에 이미지를 추가합니다.launch_background.xml (기본 생성됨):
<?xml version="1.0" encoding="utf-8"?><layer-list xmlns:android="http://schemas.android.com/apk/res/android"><item android:drawable="@color/splash_background"/><item><bitmapandroid:gravity="center"android:src="@drawable/splash_logo"/></item></layer-list>2. styles.xml에 테마 정의
android/app/src/main/res/values/styles.xml:<?xml version="1.0" encoding="utf-8"?><resources><!-- 스플래시 화면 테마 --><style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar"><item name="android:windowBackground">@drawable/launch_background</item></style><!-- 일반 테마 (스플래시 후 적용) --><style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar"><item name="android:windowBackground">@drawable/normal_background</item></style></resources>3. AndroidManifest.xml 설정
<activityandroid:name=".MainActivity"android:theme="@style/LaunchTheme"android:launchMode="singleTop"android:exported="true"><!-- Flutter가 NormalTheme으로 전환하도록 설정 --><meta-dataandroid:name="io.flutter.embedding.android.NormalTheme"android:resource="@style/NormalTheme"/><intent-filter><action android:name="android.intent.action.MAIN"/><category android:name="android.intent.category.LAUNCHER"/></intent-filter></activity>4. Android 12+ SplashScreen API 사용
android/app/src/main/res/values-v31/styles.xml(Android 12용):<?xml version="1.0" encoding="utf-8"?><resources><style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar"><item name="android:windowSplashScreenBackground">@color/splash_background</item><item name="android:windowSplashScreenAnimatedIcon">@drawable/splash_logo</item></style></resources>5. 스플래시 애니메이션 제어 (선택)
MainActivity.kt:import android.os.Buildimport android.os.Bundleimport androidx.core.view.WindowCompatimport io.flutter.embedding.android.FlutterActivityclass MainActivity : FlutterActivity() {override fun onCreate(savedInstanceState: Bundle?) {WindowCompat.setDecorFitsSystemWindows(window, false)if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {// 스플래시 화면 페이드 아웃 애니메이션 비활성화splashScreen.setOnExitAnimationListener { splashScreenView ->splashScreenView.remove()}}super.onCreate(savedInstanceState)}}
Watch out
WARNINGNormalTheme의 배경색은 Flutter UI의 주요 배경색과 비슷해야 합니다. 그렇지 않으면 스플래시 화면과 앱 화면 사이에 색상 깜빡임이 발생합니다.
<!-- ❌ 스플래시는 파란색, 앱은 흰색 → 깜빡임 --><style name="LaunchTheme"><item name="android:windowBackground">@color/blue</item></style><style name="NormalTheme"><item name="android:windowBackground">@color/white</item></style><!-- ✅ 두 테마의 배경색을 비슷하게 --><style name="LaunchTheme"><item name="android:windowBackground">@color/white</item></style><style name="NormalTheme"><item name="android:windowBackground">@color/white</item></style>
결론: LaunchTheme과 NormalTheme을 설정하고 AndroidManifest.xml에서 연결하면 앱 시작 시 스플래시 화면이 표시됩니다.
챕터 3: Platform Views로 네이티브 뷰 임베드
Why
NOTE지도, 웹뷰, 광고 SDK 등 기존 Android 네이티브 뷰를 Flutter 위젯 트리에 직접 포함해야 할 때가 있습니다. Platform Views를 사용하면 네이티브 뷰를 Flutter 앱에 임베드할 수 있습니다.
Flutter 위젯 트리 ← Platform View ← Android 네이티브 뷰
What
NOTEAndroid Platform Views에는 두 가지 모드가 있습니다.
모드 특징 사용 사례 Hybrid Composition 네이티브 뷰와 Flutter 위젯 혼합 대부분의 경우 권장 Texture Layer 텍스처로 렌더링, 성능 우수 단순 뷰, 터치 불필요 시 Hybrid Composition이 기본이며 대부분의 경우 권장됩니다.
How
TIP1. 플랫폼 뷰 팩토리 생성
android/app/src/main/kotlin/.../NativeViewFactory.kt:package com.example.myappimport android.content.Contextimport android.view.Viewimport android.widget.TextViewimport io.flutter.plugin.platform.PlatformViewimport io.flutter.plugin.platform.PlatformViewFactoryimport io.flutter.plugin.common.StandardMessageCodecclass NativeViewFactory : PlatformViewFactory(StandardMessageCodec.INSTANCE) {override fun create(context: Context, viewId: Int, args: Any?): PlatformView {val creationParams = args as Map<String, Any>?return NativeView(context, viewId, creationParams)}}class NativeView(context: Context,id: Int,creationParams: Map<String, Any>?) : PlatformView {private val textView: TextView = TextView(context).apply {text = creationParams?.get("text") as? String ?: "Hello from Android!"textSize = 24f}override fun getView(): View = textViewoverride fun dispose() {}}2. MainActivity에서 팩토리 등록
MainActivity.kt:package com.example.myappimport io.flutter.embedding.android.FlutterActivityimport io.flutter.embedding.engine.FlutterEngineclass MainActivity : FlutterActivity() {override fun configureFlutterEngine(flutterEngine: FlutterEngine) {super.configureFlutterEngine(flutterEngine)flutterEngine.platformViewsController.registry.registerViewFactory("native-view", // viewType 식별자NativeViewFactory())}}3. Flutter에서 Platform View 사용
import 'package:flutter/material.dart';import 'package:flutter/services.dart';class NativeViewExample extends StatelessWidget {const NativeViewExample({super.key});@overrideWidget build(BuildContext context) {// viewType은 Android에서 등록한 것과 일치해야 함const String viewType = 'native-view';// creationParams로 초기 데이터 전달final Map<String, dynamic> creationParams = {'text': 'Flutter에서 전달한 텍스트',};return Scaffold(appBar: AppBar(title: const Text('Platform View 예제')),body: Column(children: [const Text('Flutter 위젯'),// Platform View 임베드SizedBox(height: 100,child: AndroidView(viewType: viewType,creationParams: creationParams,creationParamsCodec: const StandardMessageCodec(),),),const Text('Flutter 위젯 계속'),],),);}}Hybrid Composition 명시적 사용
AndroidView(viewType: viewType,creationParams: creationParams,creationParamsCodec: const StandardMessageCodec(),// Hybrid Composition 강제 (기본값)layoutDirection: TextDirection.ltr,)
Watch out
WARNINGPlatform Views는 성능 오버헤드가 있습니다. 스크롤 리스트 안에 여러 Platform View를 넣으면 성능이 저하됩니다.
// ❌ 리스트 아이템마다 Platform ViewListView.builder(itemCount: 100,itemBuilder: (context, index) => AndroidView(viewType: 'native-view',),)// ✅ Platform View 개수 최소화Column(children: [Expanded(child: FlutterListView()), // Flutter 위젯 사용SizedBox(height: 200,child: AndroidView(viewType: 'map-view'), // 하나의 지도만),],)또한 Platform View 위에 Flutter 위젯을 겹치면 추가 오버헤드가 발생합니다.
결론: PlatformViewFactory를 구현하고 등록하면 AndroidView 위젯으로 네이티브 뷰를 Flutter에 임베드할 수 있습니다.
챕터 4: 앱 상태 복원
Why
NOTEAndroid 시스템은 메모리가 부족하면 백그라운드 앱을 강제 종료합니다. 사용자가 다시 앱으로 돌아왔을 때 이전 상태를 복원하지 않으면 처음부터 다시 시작해야 합니다.
앱 실행 → 백그라운드 → 시스템 종료 → 앱 복귀 → 상태 복원
What
NOTEFlutter의
RestorationMixin을 사용하면 Android의 상태 저장/복원 메커니즘과 통합됩니다.
클래스 역할 RestorationMixin State 클래스에 복원 기능 추가 RestorableProperty 복원 가능한 값 래퍼 RestorationScope 복원 컨텍스트 제공
How
TIP1. MaterialApp에 restorationScopeId 설정
void main() {runApp(const MyApp());}class MyApp extends StatelessWidget {const MyApp({super.key});@overrideWidget build(BuildContext context) {return MaterialApp(// 복원 범위 ID 설정restorationScopeId: 'app',title: 'Restoration Demo',home: const CounterPage(),);}}2. State에 RestorationMixin 적용
class CounterPage extends StatefulWidget {const CounterPage({super.key});@overrideState<CounterPage> createState() => _CounterPageState();}class _CounterPageState extends State<CounterPage> with RestorationMixin {// 복원 가능한 정수 값final RestorableInt _counter = RestorableInt(0);@override// 이 위젯의 고유 복원 IDString? get restorationId => 'counter_page';@overridevoid restoreState(RestorationBucket? oldBucket, bool initialRestore) {// 복원 가능한 속성 등록registerForRestoration(_counter, 'counter');}@overridevoid dispose() {_counter.dispose();super.dispose();}void _incrementCounter() {setState(() {_counter.value++;});}@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: const Text('카운터')),body: Center(child: Text('${_counter.value}',style: Theme.of(context).textTheme.headlineMedium,),),floatingActionButton: FloatingActionButton(onPressed: _incrementCounter,child: const Icon(Icons.add),),);}}3. 다양한 RestorableProperty 사용
class _FormPageState extends State<FormPage> with RestorationMixin {// 다양한 타입의 복원 가능 속성final RestorableInt _selectedIndex = RestorableInt(0);final RestorableDouble _sliderValue = RestorableDouble(0.5);final RestorableBool _isChecked = RestorableBool(false);final RestorableString _inputText = RestorableString('');final RestorableTextEditingController _controller =RestorableTextEditingController();@overrideString? get restorationId => 'form_page';@overridevoid restoreState(RestorationBucket? oldBucket, bool initialRestore) {registerForRestoration(_selectedIndex, 'selected_index');registerForRestoration(_sliderValue, 'slider_value');registerForRestoration(_isChecked, 'is_checked');registerForRestoration(_inputText, 'input_text');registerForRestoration(_controller, 'text_controller');}@overridevoid dispose() {_selectedIndex.dispose();_sliderValue.dispose();_isChecked.dispose();_inputText.dispose();_controller.dispose();super.dispose();}@overrideWidget build(BuildContext context) {return Scaffold(body: Column(children: [TextField(controller: _controller.value),Slider(value: _sliderValue.value,onChanged: (v) => setState(() => _sliderValue.value = v),),Checkbox(value: _isChecked.value,onChanged: (v) => setState(() => _isChecked.value = v ?? false),),],),);}}4. 네비게이션 상태 복원
class MyApp extends StatelessWidget {const MyApp({super.key});@overrideWidget build(BuildContext context) {return MaterialApp(restorationScopeId: 'app',// 네비게이터 상태도 복원home: const Navigator(restorationScopeId: 'navigator',pages: [MaterialPage(child: HomePage()),],onPopPage: (route, result) => route.didPop(result),),);}}
Watch out
WARNING모든 데이터를 복원하려 하지 마세요. 상태 복원 데이터는 직렬화되어 저장되므로 큰 데이터는 성능에 영향을 줍니다.
// ❌ 대용량 데이터 복원 시도class _ListPageState extends State<ListPage> with RestorationMixin {final RestorableValue<List<Product>> _products = ...; // 수백 개 아이템}// ✅ 필요한 상태만 복원 (ID, 스크롤 위치 등)class _ListPageState extends State<ListPage> with RestorationMixin {final RestorableInt _selectedProductId = RestorableInt(-1);final RestorableDouble _scrollOffset = RestorableDouble(0);// 실제 데이터는 복원 후 다시 로드}복원 ID는 앱 전체에서 고유해야 합니다.
결론: RestorationMixin과 RestorableProperty를 사용하면 시스템이 앱을 종료해도 사용자 상태를 복원할 수 있습니다.
한계
Android 플랫폼 통합에는 몇 가지 한계가 있습니다.
- 버전 호환성: Android 버전별로 API가 다르므로 여러 버전을 지원하려면 분기 처리가 필요합니다.
- Platform Views 성능: 네이티브 뷰 임베드는 성능 오버헤드가 있으며, 많이 사용하면 프레임 드롭이 발생할 수 있습니다.
- 상태 복원 제한: 직렬화 가능한 데이터만 복원할 수 있으며, 네트워크 연결이나 파일 핸들은 복원되지 않습니다.
- 에뮬레이터 한계: 일부 하드웨어 기능(카메라, 센서 등)은 실제 기기에서만 테스트할 수 있습니다.
Footnotes
-
Platform Views(플랫폼 뷰): 네이티브 Android 또는 iOS 뷰를 Flutter 위젯 트리에 임베드하는 기능이다. ↩
공유
이 글이 도움이 되었다면 다른 사람과 공유해주세요!