Flutter 튜토리얼 68편: 기존 Android 앱에 Flutter 추가

요약#

핵심 요지#

  • 문제 정의: 기존 Android 앱을 Flutter로 완전히 재작성하는 것은 위험하고 시간이 많이 든다.
  • 핵심 주장: Flutter 모듈을 기존 앱에 점진적으로 통합하면 위험을 줄이면서 Flutter의 이점을 얻을 수 있다.
  • 주요 근거: Add-to-app 기능으로 Flutter를 화면 단위로 추가하고, FlutterEngine으로 성능을 최적화할 수 있다.
  • 실무 기준: AAR 또는 소스 코드 방식으로 통합하며, FlutterActivity나 FlutterFragment로 화면을 표시한다.
  • 한계: AndroidX만 지원하며, 일부 아키텍처(mips, x86)는 지원하지 않는다.

문서가 설명하는 범위#

  • Flutter 모듈 생성과 Android 프로젝트 통합
  • AAR과 소스 코드 의존성 방식 비교
  • FlutterActivity와 FlutterFragment 사용법
  • FlutterEngine 캐싱과 성능 최적화
  • Dart와 Android 간 통신 (MethodChannel)

읽는 시간: 25분 | 난이도: 고급


참고 자료#


문제 상황#

회사에서 이미 운영 중인 Android 앱이 있습니다. Flutter로 새로운 기능을 개발하고 싶지만, 앱 전체를 다시 작성할 수는 없습니다.

// 기존 Android 앱
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 수년간 쌓인 Kotlin/Java 코드...
}
}

문제는 다음과 같습니다.

  • 기존 앱을 완전히 재작성하면 리스크가 크다.
  • 점진적으로 Flutter를 도입할 방법이 필요하다.
  • 기존 코드와 Flutter 코드가 공존해야 한다.

해결 방법#

Flutter의 “Add-to-app” 기능을 사용하면 기존 앱에 Flutter를 모듈로 추가할 수 있습니다. 화면 단위로 점진적인 마이그레이션이 가능합니다.

챕터 1: Add-to-app 이해하기#

Why#

NOTE

완전한 재작성 대신 점진적 도입이 실용적입니다.

Add-to-app의 장점:

  • 기존 코드를 유지하면서 새 기능만 Flutter로 개발
  • 팀이 Flutter를 점진적으로 학습
  • 위험 분산 (문제 발생 시 롤백 가능)
  • 일부 화면만 Flutter로 전환하여 A/B 테스트 가능

What#

NOTE

Add-to-app은 두 가지 방식을 지원합니다.

Multi-engine 모드:

  • 여러 개의 독립적인 Flutter 인스턴스 실행
  • 각 인스턴스가 별도의 Dart 프로그램
  • 메모리는 더 사용하지만 완전한 격리

하이브리드 네비게이션:

  • 기존 화면과 Flutter 화면을 자유롭게 오가기
  • 예: 네이티브 홈 → Flutter 상세 → 네이티브 결제

부분 화면 통합:

  • 한 화면에 네이티브 UI와 Flutter UI 공존
  • 예: 네이티브 헤더 + Flutter 콘텐츠 영역

How#

TIP

지원되는 기능:

기능Android
Gradle 자동 빌드O
AAR 빌드O
FlutterEngine APIO
Java/Kotlin 호스트 앱O
Flutter 플러그인 사용O
Hot reload 디버깅O
다중 Flutter 인스턴스O

폴더 구조 예시:

project/
├── my_android_app/ # 기존 Android 앱
│ ├── app/
│ ├── build.gradle
│ └── settings.gradle
└── my_flutter_module/ # Flutter 모듈
├── lib/
├── pubspec.yaml
└── .android/

Watch out#

WARNING

제한 사항:

  • AndroidX 필수: 기존 앱이 AndroidX를 사용해야 합니다.
  • 아키텍처 제한: Flutter는 armeabi-v7a, arm64-v8a, x86_64만 지원합니다.
app/build.gradle.kts
android {
defaultConfig {
ndk {
// Flutter가 지원하는 아키텍처만 포함
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86_64")
}
}
}
  • 단일 Flutter 라이브러리: 여러 Flutter 라이브러리를 하나의 앱에 패키징할 수 없습니다.

챕터 2: Flutter 모듈 생성하기#

Why#

NOTE

기존 앱에 Flutter를 추가하려면 먼저 Flutter 모듈을 생성해야 합니다. 모듈은 일반 Flutter 앱과 비슷하지만, Android/iOS 프로젝트가 숨겨져 있습니다.

What#

NOTE

flutter create --template=module 명령어로 모듈을 생성합니다.

Terminal window
# 기존 Android 프로젝트와 같은 레벨에 생성
cd some/path
flutter create --template=module my_flutter_module

생성되는 구조:

my_flutter_module/
├── lib/
│ └── main.dart # Flutter 앱 진입점
├── pubspec.yaml # 의존성 설정
├── test/
└── .android/ # 숨겨진 Android 래퍼 프로젝트
├── app/
├── build.gradle
└── Flutter/
└── src/main/java/
└── io/flutter/plugins/
└── GeneratedPluginRegistrant.java

.android/ 폴더는 Git에서 제외됩니다. 필요할 때 flutter pub get으로 재생성됩니다.

How#

TIP

1단계: 모듈 생성

Terminal window
cd /path/to/projects
flutter create --template=module --org com.example my_flutter_module
cd my_flutter_module

2단계: Flutter 코드 작성

lib/main.dart
import 'package:flutter/material.dart';
void main() => runApp(const MyFlutterApp());
class MyFlutterApp extends StatelessWidget {
const MyFlutterApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Module',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Flutter 화면'),
),
body: const Center(
child: Text(
'Android에서 Flutter로!',
style: TextStyle(fontSize: 24),
),
),
);
}
}

3단계: 독립 실행 테스트

Terminal window
# .android 폴더로 이동
cd .android
# 에뮬레이터에서 실행
flutter run

Watch out#

WARNING

모듈 vs 앱의 차이:

항목모듈
android/ 폴더.android/ (숨김)android/ (표시)
독립 실행가능 (.android/ 사용)기본 동작
호스트 앱 통합주 목적불가

pubspec.yaml 설정:

# 모듈 전용 설정
flutter:
module:
androidX: true
androidPackage: com.example.my_flutter_module
iosBundleIdentifier: com.example.myFlutterModule

챕터 3: Android 프로젝트에 통합하기#

Why#

NOTE

Flutter 모듈을 생성했으면 기존 Android 프로젝트에 통합해야 합니다. 두 가지 방식이 있습니다.

  1. 소스 코드 통합: 개발 중 편리, Flutter SDK 필요
  2. AAR 통합: 배포에 적합, Flutter SDK 없이 빌드 가능

What#

NOTE

방식 1: 소스 코드 의존성 (개발용 권장)

settings.gradle.kts
// Flutter 모듈 경로 설정
setBinding(extra["gradle.settings"]!!)
evaluate(File(settingsDir.parentFile, "my_flutter_module/.android/include_flutter.groovy"))
// 또는 절대 경로 사용
val flutterProjectRoot = file("../my_flutter_module")
include(":flutter")
project(":flutter").projectDir = File(flutterProjectRoot, ".android/Flutter")
include(":app")
app/build.gradle.kts
dependencies {
implementation(project(":flutter"))
}

방식 2: AAR 의존성 (배포용 권장)

Terminal window
# Flutter 모듈에서 AAR 빌드
cd my_flutter_module
flutter build aar

이 명령어는 로컬 Maven 저장소를 생성합니다.

How#

TIP

Android Studio에서 통합 (권장):

  1. Android Studio에서 기존 Android 프로젝트 열기
  2. File > New > New Project… 선택
  3. Flutter 선택
  4. 기존 Flutter 모듈 경로 지정 또는 새로 생성
  5. Finish 클릭

수동 설정 (소스 코드 방식):

settings.gradle.kts
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
repositories {
google()
mavenCentral()
maven("https://storage.googleapis.com/download.flutter.io")
}
}
rootProject.name = "MyAndroidApp"
include(":app")
// Flutter 모듈 포함
val flutterModulePath = file("../my_flutter_module")
apply {
from("$flutterModulePath/.android/include_flutter.groovy")
}
app/build.gradle.kts
dependencies {
implementation(project(":flutter"))
}

AAR 방식 설정:

settings.gradle.kts
dependencyResolutionManagement {
repositories {
maven("https://storage.googleapis.com/download.flutter.io")
maven("../my_flutter_module/build/host/outputs/repo")
}
}
app/build.gradle.kts
android {
buildTypes {
release { }
debug { }
create("profile") {
initWith(getByName("debug"))
}
}
}
dependencies {
debugImplementation("com.example.my_flutter_module:flutter_debug:1.0")
releaseImplementation("com.example.my_flutter_module:flutter_release:1.0")
add("profileImplementation", "com.example.my_flutter_module:flutter_profile:1.0")
}

Watch out#

WARNING

빌드 타입 매칭:

Flutter는 debug, profile, release 세 가지 빌드 모드가 있습니다. Android 앱의 빌드 타입과 매칭해야 합니다.

// profile 빌드 타입 추가 필요
android {
buildTypes {
create("profile") {
initWith(getByName("debug"))
}
}
}

중국 사용자:

storage.googleapis.com 대신 미러 사이트를 사용하세요.


챕터 4: Flutter 화면 추가하기 (FlutterActivity)#

Why#

NOTE

통합이 완료되면 Flutter 화면을 표시할 수 있습니다. 가장 간단한 방법은 FlutterActivity1를 사용하는 것입니다.

FlutterActivity는 전체 화면을 Flutter로 렌더링합니다.

What#

NOTE

AndroidManifest.xml에 Activity 등록:

app/src/main/AndroidManifest.xml
<application>
<!-- 기존 Activity들 -->
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- Flutter Activity 추가 -->
<activity
android:name="io.flutter.embedding.android.FlutterActivity"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize" />
</application>

How#

TIP

기본 사용법:

MainActivity.kt
import io.flutter.embedding.android.FlutterActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<Button>(R.id.open_flutter_button).setOnClickListener {
// Flutter 화면 열기
startActivity(
FlutterActivity.createDefaultIntent(this)
)
}
}
}

특정 진입점으로 시작:

// Dart에서 특정 함수를 진입점으로 사용
startActivity(
FlutterActivity
.withNewEngine()
.initialRoute("/settings") // 초기 라우트 설정
.build(this)
)
lib/main.dart
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => const HomePage(),
'/settings': (context) => const SettingsPage(),
},
);
}
}

투명 배경 사용:

startActivity(
FlutterActivity
.withNewEngine()
.backgroundMode(FlutterActivityLaunchConfigs.BackgroundMode.transparent)
.build(this)
)

Watch out#

WARNING

성능 고려사항:

withNewEngine()은 매번 새 FlutterEngine을 생성합니다. 첫 프레임까지 시간이 걸릴 수 있습니다.

// 느림: 매번 새 엔진 생성
FlutterActivity.withNewEngine().build(this)
// 빠름: 미리 생성된 엔진 사용 (다음 챕터 참조)
FlutterActivity.withCachedEngine("my_engine_id").build(this)

테마 설정:

시작 시 흰색 화면을 방지하려면 적절한 테마를 설정하세요.

res/values/styles.xml
<style name="LaunchTheme" parent="Theme.AppCompat.NoActionBar">
<item name="android:windowBackground">@android:color/white</item>
</style>

챕터 5: FlutterFragment 사용하기#

Why#

NOTE

Activity가 아닌 Fragment로 Flutter를 표시할 수도 있습니다. Fragment를 사용하면 더 유연한 화면 구성이 가능합니다.

  • 탭 레이아웃의 한 탭에 Flutter 표시
  • ViewPager의 한 페이지에 Flutter 표시
  • 화면의 일부분에만 Flutter 표시

What#

NOTE

FlutterFragment2는 Fragment 내에서 Flutter를 렌더링합니다.

import io.flutter.embedding.android.FlutterFragment
class MyActivity : FragmentActivity() {
companion object {
private const val TAG_FLUTTER_FRAGMENT = "flutter_fragment"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_my)
// 기존 Fragment가 있는지 확인
var flutterFragment = supportFragmentManager
.findFragmentByTag(TAG_FLUTTER_FRAGMENT) as? FlutterFragment
// 없으면 새로 생성
if (flutterFragment == null) {
flutterFragment = FlutterFragment.createDefault()
supportFragmentManager
.beginTransaction()
.add(R.id.flutter_container, flutterFragment, TAG_FLUTTER_FRAGMENT)
.commit()
}
}
}

How#

TIP

레이아웃 설정:

res/layout/activity_my.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!-- 네이티브 UI -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="네이티브 헤더" />
<!-- Flutter가 렌더링될 영역 -->
<FrameLayout
android:id="@+id/flutter_container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<!-- 네이티브 UI -->
<Button
android:id="@+id/native_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="네이티브 버튼" />
</LinearLayout>

다양한 생성 방법:

// 기본 생성
val fragment = FlutterFragment.createDefault()
// 초기 라우트 지정
val fragment = FlutterFragment
.withNewEngine()
.initialRoute("/profile")
.build<FlutterFragment>()
// 캐시된 엔진 사용
val fragment = FlutterFragment
.withCachedEngine("my_engine")
.build<FlutterFragment>()
// 투명 배경
val fragment = FlutterFragment
.withNewEngine()
.transparencyMode(TransparencyMode.transparent)
.build<FlutterFragment>()

ViewPager에서 사용:

class MyPagerAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
override fun getItemCount() = 3
override fun createFragment(position: Int): Fragment {
return when (position) {
0 -> NativeFragment()
1 -> FlutterFragment.createDefault()
2 -> AnotherNativeFragment()
else -> throw IllegalArgumentException()
}
}
}

Watch out#

WARNING

Fragment 수명주기 관리:

FlutterFragment는 다른 Fragment와 마찬가지로 수명주기를 따릅니다. 화면 회전이나 백그라운드 전환 시 주의가 필요합니다.

// 설정 변경 시 Fragment 재생성 방지
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
// Flutter가 자체적으로 처리
}

백 버튼 처리:

override fun onBackPressed() {
val flutterFragment = supportFragmentManager
.findFragmentByTag(TAG_FLUTTER_FRAGMENT) as? FlutterFragment
if (flutterFragment?.onBackPressed() == true) {
// Flutter가 백 버튼 처리함
return
}
super.onBackPressed()
}

챕터 6: FlutterEngine 캐싱으로 성능 최적화#

Why#

NOTE

Flutter 화면이 처음 로드될 때 시간이 걸립니다. FlutterEngine을 미리 시작(warming up)하면 사용자 경험이 크게 향상됩니다.

시작 시간 비교:

방식첫 프레임까지 시간
새 엔진 생성~1.5초
캐시된 엔진~0.3초

What#

NOTE

FlutterEngineCache3를 사용하여 엔진을 미리 생성하고 캐싱합니다.

// Application 클래스에서 엔진 미리 생성
class MyApplication : Application() {
companion object {
const val ENGINE_ID = "my_flutter_engine"
}
lateinit var flutterEngine: FlutterEngine
override fun onCreate() {
super.onCreate()
// FlutterEngine 생성 및 워밍업
flutterEngine = FlutterEngine(this)
// Dart 코드 실행 시작
flutterEngine.dartExecutor.executeDartEntrypoint(
DartExecutor.DartEntrypoint.createDefault()
)
// 캐시에 저장
FlutterEngineCache.getInstance()
.put(ENGINE_ID, flutterEngine)
}
}

How#

TIP

Application 설정:

MyApplication.kt
import android.app.Application
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.FlutterEngineCache
import io.flutter.embedding.engine.dart.DartExecutor
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// FlutterEngine 미리 생성
val flutterEngine = FlutterEngine(this)
// Dart 진입점 실행 (main 함수 시작)
flutterEngine.dartExecutor.executeDartEntrypoint(
DartExecutor.DartEntrypoint.createDefault()
)
// 캐시에 등록
FlutterEngineCache.getInstance()
.put("my_engine_id", flutterEngine)
}
}
AndroidManifest.xml
<application
android:name=".MyApplication"
... >

캐시된 엔진 사용:

// FlutterActivity에서
startActivity(
FlutterActivity
.withCachedEngine("my_engine_id")
.build(this)
)
// FlutterFragment에서
val fragment = FlutterFragment
.withCachedEngine("my_engine_id")
.build<FlutterFragment>()

초기 라우트와 함께 사용:

// 캐시된 엔진은 이미 Dart 코드가 실행 중이므로
// initialRoute 대신 MethodChannel로 통신
flutterEngine.navigationChannel.setInitialRoute("/settings")

Watch out#

WARNING

메모리 관리:

캐시된 엔진은 메모리를 계속 사용합니다. 앱 종료 시 정리가 필요합니다.

class MyApplication : Application() {
override fun onTerminate() {
super.onTerminate()
FlutterEngineCache.getInstance()
.get("my_engine_id")
?.destroy()
}
}

다중 엔진 주의:

여러 개의 엔진을 캐싱하면 메모리 사용량이 증가합니다. 필요한 만큼만 캐싱하세요.

// 여러 엔진 캐싱 시 메모리 사용량 증가
FlutterEngineCache.getInstance().put("engine_1", engine1)
FlutterEngineCache.getInstance().put("engine_2", engine2)
// 각 엔진마다 약 40-50MB 메모리 사용

챕터 7: Android와 Dart 간 통신#

Why#

NOTE

Flutter 화면과 네이티브 Android 코드 간에 데이터를 주고받아야 할 때가 있습니다.

  • 네이티브 센서 데이터를 Flutter로 전달
  • Flutter에서 네이티브 API 호출
  • 양방향 이벤트 전파

What#

NOTE

MethodChannel4을 사용하여 양방향 통신이 가능합니다.

통신 흐름:

Android (Kotlin/Java) <--MethodChannel--> Flutter (Dart)

채널 유형:

채널용도
MethodChannel메서드 호출 (요청-응답)
EventChannel이벤트 스트림 (일방향)
BasicMessageChannel단순 메시지

How#

TIP

Dart 측 설정:

lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
static const platform = MethodChannel('com.example.app/channel');
String _batteryLevel = 'Unknown';
Future<void> _getBatteryLevel() async {
String batteryLevel;
try {
final int result = await platform.invokeMethod('getBatteryLevel');
batteryLevel = 'Battery level: $result%';
} on PlatformException catch (e) {
batteryLevel = 'Failed: ${e.message}';
}
setState(() {
_batteryLevel = batteryLevel;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('MethodChannel 예제')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(_batteryLevel),
ElevatedButton(
onPressed: _getBatteryLevel,
child: const Text('배터리 레벨 확인'),
),
],
),
),
);
}
}

Android 측 설정:

// FlutterActivity를 상속하는 경우
class MainActivity : FlutterActivity() {
private val CHANNEL = "com.example.app/channel"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
CHANNEL
).setMethodCallHandler { call, result ->
when (call.method) {
"getBatteryLevel" -> {
val batteryLevel = getBatteryLevel()
if (batteryLevel != -1) {
result.success(batteryLevel)
} else {
result.error("UNAVAILABLE", "Battery level not available.", null)
}
}
else -> result.notImplemented()
}
}
}
private fun getBatteryLevel(): Int {
val batteryManager = getSystemService(BATTERY_SERVICE) as BatteryManager
return batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
}
}

캐시된 엔진에서 MethodChannel 설정:

// Application에서 설정
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
val flutterEngine = FlutterEngine(this)
// MethodChannel 설정
MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
"com.example.app/channel"
).setMethodCallHandler { call, result ->
// 처리 로직
}
flutterEngine.dartExecutor.executeDartEntrypoint(
DartExecutor.DartEntrypoint.createDefault()
)
FlutterEngineCache.getInstance()
.put("my_engine", flutterEngine)
}
}

Watch out#

WARNING

채널 이름 규칙:

채널 이름은 고유해야 하며, 역방향 도메인 표기법을 사용합니다.

// 좋은 예
const val CHANNEL = "com.example.myapp/battery"
const val CHANNEL = "com.example.myapp/settings"
// 나쁜 예
const val CHANNEL = "channel" // 충돌 가능

스레드 주의:

MethodChannel 콜백은 메인 스레드에서 실행됩니다. 무거운 작업은 백그라운드 스레드로 이동하세요.

setMethodCallHandler { call, result ->
thread {
val data = heavyComputation()
runOnUiThread {
result.success(data)
}
}
}

한계#

이 문서는 Android add-to-app의 기초를 다룹니다. 다음 주제는 별도로 학습해야 합니다.

  • 다중 Flutter 인스턴스: FlutterEngineGroup 사용
  • 플러그인 통합: 기존 네이티브 플러그인과의 호환성
  • ProGuard 설정: 릴리스 빌드 최적화
  • 심층 디버깅: flutter attach 사용법

Footnotes#

  1. FlutterActivity: Flutter 콘텐츠를 전체 화면으로 표시하는 Android Activity이다.

  2. FlutterFragment: Android Fragment 내에서 Flutter 콘텐츠를 렌더링하여 더 유연한 화면 구성을 가능하게 한다.

  3. FlutterEngineCache: FlutterEngine 인스턴스를 캐싱하여 Flutter 화면의 시작 시간을 단축시키는 싱글톤 클래스이다.

  4. MethodChannel: Flutter(Dart)와 네이티브 코드(Kotlin/Swift) 간에 메서드를 호출하고 결과를 반환받는 양방향 통신 채널이다.

공유

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

Flutter 튜토리얼 68편: 기존 Android 앱에 Flutter 추가
https://moodturnpost.net/posts/flutter/flutter-add-to-app-android/
작성자
Moodturn
게시일
2026-01-08
Moodturn

목차