Flutter 튜토리얼 36편: 플랫폼별 자동 적응
요약
핵심 요지
- 문제 정의: 같은 Flutter 코드가 Android와 iOS에서 다르게 동작해야 할 때, 플랫폼별 코드를 일일이 작성하면 복잡해진다.
- 핵심 주장: Flutter는 OS 환경 동작(스크롤, 텍스트 편집)을 자동으로 적응하고, UI 컴포넌트는
adaptive()1 생성자로 플랫폼별 위젯을 선택할 수 있다. - 주요 근거: iOS 사용자는 edge swipe로 뒤로 가기를, Android 사용자는 back 버튼을 기대한다. 스크롤 물리도 플랫폼마다 다르게 느껴져야 자연스럽다.
- 실무 기준: 네비게이션, 스크롤, 텍스트 입력은 Flutter가 자동 처리하고, Switch, Slider, AlertDialog 등은
.adaptive()생성자를 사용한다.
문서가 설명하는 범위
- 자동 적응되는 네비게이션 전환 애니메이션
- 플랫폼별 스크롤 물리와 오버스크롤 동작
- 텍스트 편집 시 플랫폼별 제스처 차이
.adaptive()생성자를 제공하는 위젯 목록- 앱 바와 하단 네비게이션 바 커스터마이징
읽는 시간: 15분 | 난이도: 중급
참고 자료
- Automatic platform adaptations - 플랫폼 자동 적응 가이드
- Additional resources - 추가 학습 자료
문제 상황
Flutter 앱을 만들었는데 Android에서는 자연스럽지만 iOS에서는 어색합니다. iOS 사용자가 화면 왼쪽 가장자리를 스와이프해도 뒤로 가기가 안 됩니다. 스크롤할 때 iOS 특유의 바운스 효과가 없어서 이상하게 느껴집니다.
// 커스텀 페이지 전환 - 플랫폼 특성 무시Navigator.push( context, MaterialPageRoute( builder: (context) => DetailPage(), ),);문제는 다음과 같습니다.
- 네비게이션 전환 애니메이션이 플랫폼에 맞지 않다.
- 스크롤 물리가 iOS/Android 고유의 느낌을 주지 않는다.
- 텍스트 선택, 복사 등 편집 동작이 익숙하지 않다.
- Switch, Slider 같은 컨트롤이 플랫폼 스타일과 다르다.
해결 방법
Flutter는 두 가지 유형의 플랫폼 적응을 구분합니다.
- OS 환경 동작: 스크롤, 텍스트 편집처럼 다르면 ‘잘못된’ 느낌을 주는 것 → 자동 적응
- OEM SDK 컨벤션: AlertDialog, Switch처럼 플랫폼 관례를 따르는 것 →
.adaptive()생성자로 선택
챕터 1: 네비게이션 전환 애니메이션
Why
NOTEAndroid와 iOS는 화면 전환 애니메이션이 다릅니다. Android는
startActivity()2 스타일로 아래에서 위로 올라오고, iOS는 오른쪽에서 왼쪽으로 슬라이드합니다.사용자가 매일 사용하는 앱들과 동일한 전환 효과가 없으면 앱이 어색하게 느껴집니다.
What
NOTEFlutter의
Navigator.push()3는 플랫폼에 따라 다른 전환 애니메이션을 적용합니다.
플랫폼 기본 전환 fullscreenDialog 전환 Android 줌(Zoom) 효과 아래에서 위로 iOS 오른쪽에서 왼쪽 슬라이드 아래에서 위로 (Present) iOS에서는
CupertinoNavigationBar4의 각 요소도 함께 애니메이션됩니다.
How
TIP기본 Navigator.push() 사용 - 자동 적응
class MyApp extends StatelessWidget {const MyApp({super.key});@overrideWidget build(BuildContext context) {return MaterialApp(home: const HomePage(),);}}class HomePage extends StatelessWidget {const HomePage({super.key});@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: const Text('Home')),body: Center(child: ElevatedButton(onPressed: () {// Flutter가 플랫폼에 맞는 전환 애니메이션 자동 적용Navigator.push(context,MaterialPageRoute(builder: (context) => const DetailPage(),),);},child: const Text('Go to Detail'),),),);}}모달 스타일 전환 (아래에서 위로)
Navigator.push(context,MaterialPageRoute(// fullscreenDialog: true로 설정하면// 두 플랫폼 모두 아래에서 위로 올라오는 전환 사용fullscreenDialog: true,builder: (context) => const SettingsPage(),),);
Watch out
WARNING뒤로 가기 동작도 자동 적응됩니다
- Android: 시스템 뒤로 가기 버튼이
Navigator.pop()호출- iOS: 화면 왼쪽 가장자리 스와이프로 뒤로 가기
iOS edge swipe를 비활성화하려면 주의가 필요합니다.
// iOS에서 swipe back 비활성화가 필요한 경우// (예: 결제 화면에서 실수로 뒤로 가기 방지)MaterialPageRoute(builder: (context) => const PaymentPage(),// 전체 화면 Dialog는 기본적으로 swipe back 비활성화fullscreenDialog: true,)
결론: Navigator.push()와 MaterialPageRoute를 사용하면 플랫폼별 전환 애니메이션이 자동으로 적용됩니다.
챕터 2: 스크롤 물리 적응
Why
NOTE스크롤은 사용자가 가장 자주 하는 동작입니다. iOS와 Android는 스크롤 물리가 완전히 다르고, 사용자는 이 차이에 매우 민감합니다.
- iOS: 관성이 크고, 속도가 천천히 올라가며, 느린 속도에서도 미끄러집니다.
- Android: 정적 마찰이 크고, 빠르게 가속되며, 더 멀리 스크롤됩니다.
What
NOTEFlutter는
ScrollPhysics5를 플랫폼에 따라 자동으로 선택합니다.
동작 Android iOS 오버스크롤 Glow 효과 (Material 색상) 바운스 (당겨지는 느낌) 모멘텀 일정함 연속 플링 시 누적 상단 복귀 없음 상태 바 탭으로 맨 위 이동
ListView,SingleChildScrollView,CustomScrollView모두 이 동작을 자동으로 따릅니다.
How
TIP기본 스크롤 - 자동 적응
class AutoAdaptiveScroll extends StatelessWidget {const AutoAdaptiveScroll({super.key});@overrideWidget build(BuildContext context) {return ListView.builder(// physics 파라미터를 지정하지 않으면// 플랫폼에 맞는 ScrollPhysics가 자동 적용itemCount: 100,itemBuilder: (context, index) {return ListTile(title: Text('Item $index'));},);}}특정 플랫폼 물리 강제 적용
// iOS 스타일 바운스를 Android에서도 사용하고 싶은 경우ListView.builder(physics: const BouncingScrollPhysics(),itemCount: 100,itemBuilder: (context, index) {return ListTile(title: Text('Item $index'));},)// Android 스타일 Clamp를 iOS에서도 사용하고 싶은 경우ListView.builder(physics: const ClampingScrollPhysics(),itemCount: 100,itemBuilder: (context, index) {return ListTile(title: Text('Item $index'));},)
Watch out
WARNINGiOS 전용 기능: 상태 바 탭으로 맨 위 이동
이 기능은
PrimaryScrollController6에 연결된 스크롤러에서만 동작합니다.// Scaffold의 body에 직접 넣은 ListView는 자동으로 동작Scaffold(body: ListView.builder(...), // 상태 바 탭 지원)// 중첩된 ListView나 여러 스크롤러가 있으면 주의Scaffold(body: Column(children: [Expanded(child: ListView.builder(...)), // primary: true 필요할 수 있음],),)
결론: ListView와 스크롤 위젯은 플랫폼에 맞는 물리 효과를 자동으로 적용합니다.
챕터 3: 텍스트 편집 동작 적응
Why
NOTE텍스트 편집은 플랫폼마다 제스처가 다릅니다. Android에서 길게 누르면 단어가 선택되지만, iOS에서 길게 누르면 커서가 이동합니다.
사용자는 이런 세부 동작에 익숙해져 있어서, 다르게 동작하면 불편함을 느낍니다.
What
NOTEFlutter의
TextField7와CupertinoTextField8는 플랫폼별 텍스트 편집 동작을 자동으로 적용합니다.
제스처 Android iOS 탭 탭한 위치에 커서 가장 가까운 단어 경계에 커서 길게 누르기 단어 선택 + 툴바 커서 위치 지정 + 툴바 길게 누른 채 드래그 선택 영역 확장 커서 이동 더블 탭 단어 선택 단어 선택 키보드 커서 이동 스페이스바 스와이프 3D Touch 플로팅 커서 텍스트 선택 툴바도 플랫폼에 맞는 디자인이 표시됩니다.
How
TIP기본 TextField 사용 - 자동 적응
class AdaptiveTextField extends StatelessWidget {const AdaptiveTextField({super.key});@overrideWidget build(BuildContext context) {return const TextField(decoration: InputDecoration(border: OutlineInputBorder(),labelText: 'Enter text',),// 텍스트 편집 제스처는 플랫폼에 맞게 자동 적응);}}맞춤법 검사도 자동 적응
TextField(// 맞춤법 검사 활성화 - 플랫폼별 맞춤법 검사 사용spellCheckConfiguration: SpellCheckConfiguration(),decoration: const InputDecoration(border: OutlineInputBorder(),labelText: 'Type with spell check',),)
결론: TextField는 탭, 길게 누르기, 드래그 등 모든 텍스트 편집 제스처를 플랫폼에 맞게 자동 적응합니다.
챕터 4: 타이포그래피와 아이콘 적응
Why
NOTEAndroid는 Roboto 폰트를, iOS는 San Francisco 폰트를 사용합니다. 아이콘도 플랫폼마다 디자인 스타일이 다릅니다.
폰트와 아이콘이 플랫폼에 맞지 않으면 앱이 이질적으로 느껴집니다.
What
NOTE폰트 자동 적응
패키지 Android iOS Material Roboto San Francisco Cupertino (폴백 폰트) San Francisco 아이콘 자동 적응
Material 패키지의 일부 아이콘은 플랫폼에 따라 다른 디자인을 표시합니다.
아이콘 Android iOS 뒤로 가기 화살표 + 막대 단순 화살표(<) 더보기 세로 점 3개 가로 점 3개
How
TIP플랫폼 적응 아이콘 사용
AppBar(leading: IconButton(// 플랫폼에 맞는 뒤로 가기 아이콘 자동 선택icon: const Icon(Icons.arrow_back),onPressed: () => Navigator.pop(context),),actions: [IconButton(// 플랫폼에 맞는 더보기 아이콘 자동 선택icon: const Icon(Icons.more_vert),onPressed: () {},),],)Icons.adaptive 사용하기
// 플랫폼 적응 아이콘 명시적 사용Icon(Icons.adaptive.arrow_back)Icon(Icons.adaptive.share)Icon(Icons.adaptive.more)
Watch out
WARNINGSan Francisco 폰트 라이선스 제한
San Francisco 폰트는 iOS, macOS, tvOS에서만 사용 가능합니다. Android에서 Cupertino 테마를 사용하면 폴백 폰트가 적용됩니다.
// Android에서 Cupertino 테마 사용 시 주의CupertinoApp(theme: const CupertinoThemeData(// Android에서는 San Francisco 대신 다른 폰트 사용됨),)
결론: Material 패키지는 폰트와 일부 아이콘을 플랫폼에 맞게 자동으로 선택합니다.
챕터 5: .adaptive() 생성자 위젯
Why
NOTESwitch, Slider, AlertDialog 같은 컨트롤은 OS와 밀접하게 통합됩니다. 사용자는 이런 컨트롤을 보고 즉시 어떻게 사용하는지 인식합니다.
플랫폼 컨벤션을 따르면 사용자가 더 빠르게 적응할 수 있습니다.
What
NOTEFlutter는
.adaptive()생성자를 제공하는 위젯이 있습니다. iOS에서는 Cupertino 버전으로, Android에서는 Material 버전으로 렌더링됩니다.
Material 위젯 Cupertino 위젯 적응 생성자 SwitchCupertinoSwitchSwitch.adaptive()SliderCupertinoSliderSlider.adaptive()CircularProgressIndicatorCupertinoActivityIndicatorCircularProgressIndicator.adaptive()RefreshProgressIndicatorCupertinoActivityIndicatorRefreshIndicator.adaptive()CheckboxCupertinoCheckboxCheckbox.adaptive()RadioCupertinoRadioRadio.adaptive()AlertDialogCupertinoAlertDialogAlertDialog.adaptive()
How
TIPSwitch.adaptive() 사용
class AdaptiveSwitchDemo extends StatefulWidget {const AdaptiveSwitchDemo({super.key});@overrideState<AdaptiveSwitchDemo> createState() => _AdaptiveSwitchDemoState();}class _AdaptiveSwitchDemoState extends State<AdaptiveSwitchDemo> {bool _value = false;@overrideWidget build(BuildContext context) {return Switch.adaptive(// iOS에서는 CupertinoSwitch로,// Android에서는 Material Switch로 렌더링value: _value,onChanged: (value) => setState(() => _value = value),);}}AlertDialog.adaptive() 사용
void _showAdaptiveDialog(BuildContext context) {showDialog(context: context,builder: (context) => AlertDialog.adaptive(// iOS에서는 CupertinoAlertDialog 스타일,// Android에서는 Material AlertDialog 스타일title: const Text('Confirm'),content: const Text('Are you sure you want to proceed?'),actions: [TextButton(onPressed: () => Navigator.pop(context),child: const Text('Cancel'),),TextButton(onPressed: () {Navigator.pop(context);// 확인 동작},child: const Text('OK'),),],),);}CircularProgressIndicator.adaptive() 사용
class LoadingIndicator extends StatelessWidget {const LoadingIndicator({super.key});@overrideWidget build(BuildContext context) {return const Center(// iOS: 스피너 스타일// Android: 원형 진행 표시기child: CircularProgressIndicator.adaptive(),);}}
결론: .adaptive() 생성자를 사용하면 플랫폼에 맞는 네이티브 스타일 컨트롤이 자동으로 선택됩니다.
챕터 6: 앱 바와 네비게이션 바 커스터마이징
Why
NOTE앱 바는 자동 적응되지 않습니다. 하지만 iOS 사용자를 위해 Apple Human Interface Guidelines 스타일에 맞게 커스터마이징할 수 있습니다.
iOS에서 Material 앱 바가 어색하게 느껴질 수 있습니다.
What
NOTETop App Bar 비교
속성 Material 3 iOS HIG 높이 64dp 44pt 그림자 스크롤 시 약간 거의 없음 타이틀 스타일 작음 크게 (Large Title) Bottom Navigation Bar 비교
속성 Material 3 iOS Tab Bar 레이블 선택 시 표시 항상 표시 선택 표시 상단 인디케이터 색상 변경
How
TIPiOS 스타일 앱 바 적용
import 'dart:io';import 'package:flutter/cupertino.dart';import 'package:flutter/material.dart';class AdaptiveAppBar extends StatelessWidget {const AdaptiveAppBar({super.key});@overrideWidget build(BuildContext context) {// iOS 텍스트 테마 매핑final cupertinoTextTheme = TextTheme(headlineMedium: const CupertinoThemeData().textTheme.navLargeTitleTextStyle.copyWith(letterSpacing: -1.5),titleLarge: const CupertinoThemeData().textTheme.navTitleTextStyle,);return MaterialApp(theme: ThemeData(// iOS에서는 Cupertino 스타일 텍스트 테마 사용textTheme: Platform.isIOS ? cupertinoTextTheme : null,),home: Scaffold(appBar: AppBar(title: const Text('Adaptive App'),// iOS 스타일 적용surfaceTintColor: Platform.isIOS ? Colors.transparent : null,shadowColor: Platform.isIOS? CupertinoColors.darkBackgroundGray: null,scrolledUnderElevation: Platform.isIOS ? 0.1 : null,toolbarHeight: Platform.isIOS ? 44 : null,),body: const Center(child: Text('Content')),),);}}플랫폼별 하단 네비게이션 바
class AdaptiveBottomNav extends StatefulWidget {const AdaptiveBottomNav({super.key});@overrideState<AdaptiveBottomNav> createState() => _AdaptiveBottomNavState();}class _AdaptiveBottomNavState extends State<AdaptiveBottomNav> {int _currentIndex = 0;final Map<String, Icon> _navigationItems = {'Home': Platform.isIOS? const Icon(CupertinoIcons.house_fill): const Icon(Icons.home),'Search': Icon(Icons.adaptive.share),};@overrideWidget build(BuildContext context) {return Scaffold(body: const Center(child: Text('Content')),bottomNavigationBar: Platform.isIOS? CupertinoTabBar(currentIndex: _currentIndex,onTap: (index) => setState(() => _currentIndex = index),items: _navigationItems.entries.map((entry) => BottomNavigationBarItem(icon: entry.value,label: entry.key,)).toList(),): NavigationBar(selectedIndex: _currentIndex,onDestinationSelected: (index) =>setState(() => _currentIndex = index),destinations: _navigationItems.entries.map((entry) => NavigationDestination(icon: entry.value,label: entry.key,)).toList(),),);}}
Watch out
WARNING앱 전체 일관성 유지
앱 바만 iOS 스타일로 바꾸면 다른 부분과 어울리지 않을 수 있습니다.
// 앱 전체적으로 일관된 스타일을 유지하세요// 앱 바만 iOS 스타일이고 나머지가 Material이면 어색함앱 바 스타일을 변경하려면 다른 UI 요소도 함께 조정하는 것을 고려하세요.
결론: 앱 바와 네비게이션 바는 Platform.isIOS 조건으로 플랫폼별 스타일을 적용할 수 있습니다.
챕터 7: 햅틱 피드백 적응
Why
NOTE터치 피드백도 플랫폼마다 다릅니다. Android에서 텍스트를 길게 눌러 선택하면 진동이 울리지만, iOS에서는 그렇지 않습니다.
What
NOTEFlutter의 Material/Cupertino 패키지는 적절한 햅틱 피드백을 자동으로 트리거합니다.
상황 Android iOS 텍스트 길게 눌러 선택 진동 없음 피커 스크롤 없음 약한 충격
How
TIP커스텀 햅틱 피드백
import 'package:flutter/services.dart';class HapticFeedbackDemo extends StatelessWidget {const HapticFeedbackDemo({super.key});@overrideWidget build(BuildContext context) {return ElevatedButton(onPressed: () {// 플랫폼에 맞는 햅틱 피드백 트리거HapticFeedback.mediumImpact();},child: const Text('Tap for haptic'),);}}
결론: 햅틱 피드백은 Material/Cupertino 위젯에서 플랫폼에 맞게 자동으로 트리거됩니다.
한계
- 자동 적응은 Android와 iOS에만 적용됩니다. 웹, 데스크톱에서는 별도 처리가 필요합니다.
.adaptive()생성자는 모든 위젯에 제공되지 않습니다. 필요하면Platform.isIOS조건문을 사용하세요.- 앱 전체의 디자인 일관성을 유지하면서 플랫폼 적응을 적용하는 것은 팀 간 합의가 필요합니다.
Footnotes
-
adaptive(적응형): 플랫폼에 따라 자동으로 다른 스타일이나 동작을 적용하는 것을 말한다. ↩
-
startActivity(): Android에서 새 Activity를 시작하는 메서드로, Flutter Navigator.push()의 Android 전환 애니메이션 기반이다. ↩
-
ScrollPhysics: 스크롤 동작의 물리 효과(마찰, 바운스 등)를 정의하는 클래스다. ↩
-
PrimaryScrollController: Scaffold 내 기본 스크롤러를 관리하며, iOS 상태 바 탭 동작과 연결된다. ↩
-
TextField: Material Design 스타일의 텍스트 입력 위젯이다. ↩
-
CupertinoTextField: iOS 스타일의 텍스트 입력 위젯이다. ↩
공유
이 글이 도움이 되었다면 다른 사람과 공유해주세요!