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분 | 난이도: 중급


참고 자료#


문제 상황#

Flutter 앱을 Android에서 실행하려면 개발 환경 설정이 필요합니다. 또한 앱이 시작될 때 보여줄 스플래시 화면, 네이티브 Android 뷰 통합, 시스템에 의해 종료된 앱의 상태 복원 등 플랫폼 특화 기능이 필요합니다.

Android 통합이 필요한 상황#

개발 환경 → Android Studio, SDK, 에뮬레이터 설정
시작 화면 → 앱 로딩 중 스플래시 화면 표시
네이티브 뷰 → 지도, 웹뷰 등 기존 Android 뷰 활용
상태 복원 → 시스템이 앱을 종료한 후 상태 유지

문제는 다음과 같습니다.

  • Android SDK와 도구 설정이 복잡하다.
  • 스플래시 화면은 Android 버전별로 구현 방식이 다르다.
  • 네이티브 뷰를 Flutter에 임베드하면 성능 문제가 발생할 수 있다.
  • 시스템이 앱을 강제 종료하면 사용자 상태가 손실된다.

해결 방법#

Flutter는 Android 플랫폼과의 긴밀한 통합을 지원합니다. Android Studio를 통한 개발 환경 설정부터 네이티브 기능 통합까지 단계별로 알아봅니다.

챕터 1: Android 개발 환경 설정#

Why#

NOTE

Flutter 앱을 Android 기기나 에뮬레이터에서 실행하려면 Android SDK와 개발 도구가 필요합니다. Android Studio가 이 모든 것을 한 번에 설치하고 관리할 수 있게 해줍니다.

Flutter SDK → Android Studio → Android SDK → 에뮬레이터/기기

What#

NOTE

Android 개발에 필요한 구성 요소입니다.

구성 요소역할
Android StudioIDE와 SDK 관리 도구
Android SDKAndroid 앱 빌드에 필요한 라이브러리
SDK Platform-ToolsADB 등 명령줄 도구
Android Emulator가상 기기에서 앱 테스트

How#

TIP

1. 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 doctor

Android 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#

NOTE

Android 스플래시 화면은 두 가지 방식으로 구현할 수 있습니다.

방식지원 버전특징
LaunchTheme모든 버전windowBackground로 Drawable 표시
SplashScreen APIAndroid 12+아이콘 애니메이션, 배경색 등 세부 설정

Android 12 미만과 이상을 모두 지원하려면 두 가지를 함께 구현합니다.

How#

TIP

1. 스플래시 이미지 준비

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>
<bitmap
android: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 설정

<activity
android:name=".MainActivity"
android:theme="@style/LaunchTheme"
android:launchMode="singleTop"
android:exported="true">
<!-- Flutter가 NormalTheme으로 전환하도록 설정 -->
<meta-data
android: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.Build
import android.os.Bundle
import androidx.core.view.WindowCompat
import io.flutter.embedding.android.FlutterActivity
class 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#

WARNING

NormalTheme의 배경색은 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#

NOTE

Android Platform Views에는 두 가지 모드가 있습니다.

모드특징사용 사례
Hybrid Composition네이티브 뷰와 Flutter 위젯 혼합대부분의 경우 권장
Texture Layer텍스처로 렌더링, 성능 우수단순 뷰, 터치 불필요 시

Hybrid Composition이 기본이며 대부분의 경우 권장됩니다.

How#

TIP

1. 플랫폼 뷰 팩토리 생성

android/app/src/main/kotlin/.../NativeViewFactory.kt:

package com.example.myapp
import android.content.Context
import android.view.View
import android.widget.TextView
import io.flutter.plugin.platform.PlatformView
import io.flutter.plugin.platform.PlatformViewFactory
import io.flutter.plugin.common.StandardMessageCodec
class 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 = textView
override fun dispose() {}
}

2. MainActivity에서 팩토리 등록

MainActivity.kt:

package com.example.myapp
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
class 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});
@override
Widget 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#

WARNING

Platform Views는 성능 오버헤드가 있습니다. 스크롤 리스트 안에 여러 Platform View를 넣으면 성능이 저하됩니다.

// ❌ 리스트 아이템마다 Platform View
ListView.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#

NOTE

Android 시스템은 메모리가 부족하면 백그라운드 앱을 강제 종료합니다. 사용자가 다시 앱으로 돌아왔을 때 이전 상태를 복원하지 않으면 처음부터 다시 시작해야 합니다.

앱 실행 → 백그라운드 → 시스템 종료 → 앱 복귀 → 상태 복원

What#

NOTE

Flutter의 RestorationMixin을 사용하면 Android의 상태 저장/복원 메커니즘과 통합됩니다.

클래스역할
RestorationMixinState 클래스에 복원 기능 추가
RestorableProperty복원 가능한 값 래퍼
RestorationScope복원 컨텍스트 제공

How#

TIP

1. MaterialApp에 restorationScopeId 설정

void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget 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});
@override
State<CounterPage> createState() => _CounterPageState();
}
class _CounterPageState extends State<CounterPage> with RestorationMixin {
// 복원 가능한 정수 값
final RestorableInt _counter = RestorableInt(0);
@override
// 이 위젯의 고유 복원 ID
String? get restorationId => 'counter_page';
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
// 복원 가능한 속성 등록
registerForRestoration(_counter, 'counter');
}
@override
void dispose() {
_counter.dispose();
super.dispose();
}
void _incrementCounter() {
setState(() {
_counter.value++;
});
}
@override
Widget 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();
@override
String? get restorationId => 'form_page';
@override
void 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');
}
@override
void dispose() {
_selectedIndex.dispose();
_sliderValue.dispose();
_isChecked.dispose();
_inputText.dispose();
_controller.dispose();
super.dispose();
}
@override
Widget 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});
@override
Widget 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는 앱 전체에서 고유해야 합니다.

결론: RestorationMixinRestorableProperty를 사용하면 시스템이 앱을 종료해도 사용자 상태를 복원할 수 있습니다.


한계#

Android 플랫폼 통합에는 몇 가지 한계가 있습니다.

  • 버전 호환성: Android 버전별로 API가 다르므로 여러 버전을 지원하려면 분기 처리가 필요합니다.
  • Platform Views 성능: 네이티브 뷰 임베드는 성능 오버헤드가 있으며, 많이 사용하면 프레임 드롭이 발생할 수 있습니다.
  • 상태 복원 제한: 직렬화 가능한 데이터만 복원할 수 있으며, 네트워크 연결이나 파일 핸들은 복원되지 않습니다.
  • 에뮬레이터 한계: 일부 하드웨어 기능(카메라, 센서 등)은 실제 기기에서만 테스트할 수 있습니다.

Footnotes#

  1. Platform Views(플랫폼 뷰): 네이티브 Android 또는 iOS 뷰를 Flutter 위젯 트리에 임베드하는 기능이다.

공유

이 글이 도움이 되었다면 다른 사람과 공유해주세요!

Flutter 튜토리얼 56편: Android 플랫폼 통합
https://moodturnpost.net/posts/flutter/flutter-platform-android/
작성자
Moodturn
게시일
2026-01-08
Moodturn

목차