Flutter - ynjch97/YNJCH_WIKI GitHub Wiki
- ์ฐธ๊ณ : https://nomadcoders.co/flutter-for-beginners
- Dart ์ธ์ด์ Flutter ํ๋ ์์ํฌ ์ฌ์ฉ
- ๋ฉํฐ ํ๋ซํผ ์ง์
- ์น ์ฌ์ดํธ ์ดํ๋ฆฌ์ผ์ด์
- iOS, Android ์ดํ๋ฆฌ์ผ์ด์
- Mac OS, Windows, Linux
- IoT ์ ๊ฐ์ ์๋ฒ ๋๋ ์ดํ๋ฆฌ์ผ์ด์
- https://flutter.dev/showcase
- ์ฑ : Tiktok, Tencent, Toyota, Wonderous ๋ฑ
- ์น : IโO Photo Booth, flokk, pinball ๋ฑ
- Swift(iOS), Java(Android) ๋ก ๋ค์ดํฐ๋ธ ์ฑ ๊ฐ๋ฐ ์, OS ์ ์ง์ ์ ์ผ๋ก ๋ํํ๊ฒ ๋จ (๋ค์ดํฐ๋ธ ํ๋ ์์ํฌ ๋์ ๋ฐฉ์)
- Flutter/Dart ์ฝ๋๋ ์ด์์ฒด์ ์ ์ง์ ์ํตํ์ง ์์
- https://docs.flutter.dev/resources/architectural-overview
- Engine ์ ์ดํ๋ฆฌ์ผ์ด์ ๋ด๋ถ์ ๋ฃ๊ณ , Dart ์ฝ๋๋ฅผ ์ปดํ์ผํจ
- C or C++ ๋ก ๋ง๋ค์ด์ง Engine ์ด ํ๋ฉด ์์ ์์๋ค์ ๊ทธ๋ ค์ค (ex. Unity ์์ง)
- ์ฑ ์คํ ์, Flutter ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ๋ถ๋ฌ์ค๊ณ ์ฝ๋๋ฅผ ๋์์์ผ ๋ชจ๋ UI ๋ฅผ ๋ ๋๋ง ํด์ค
- Flutter ๋ก ํต์ ํ ์ ์๋ ๊ฒ๋ค์ด ๋ง์์ง (ํ๋ฉด ์ ๋ชจ๋ ํฝ์ ์ ์กฐ์ > ์ ๋๋ฉ์ด์ /๋ด๋น๊ฒ์ด์ ๋ฑ์ ํต์ )
- ๊ฒ์ ์์ง์ด ํ๋ฉด์ ๋ชจ๋ ํฝ์ ์ ๋ค๋ฃจ๋ ๊ฒ์ฒ๋ผ ๋ชจ๋ ๊ฒ์ ๊ทธ๋ ค์ค (โด ๋ ๋๋ง ์ ํธ์คํธ ํ๋ซํผ ์ฌ์ฉ X)
- ๋ ์์ ์์ง์ ๊ฐ์ง๋ฉฐ, ์ด๊ฒ์ผ๋ก ๋ ๋๋งํจ > ๋ค์ดํฐ๋ธ ์ฑ๋ค์ ์ปดํฌ๋ํธ, ์์ ฏ๋ค์ ๊ฐ์ง ์ ์์
- ์ ์ ๊ฐ Mac OS, Windows, iOS, Android ์ดํ๋ฆฌ์ผ์ด์ ์คํ ์, ์์ง์ ๊ฐ๋์ํค๋ 'runner' ํ๋ก์ ํธ ์คํ
- Embedder : ํธ์คํธ ํ๋ซํผ ์์์ ์์ง์ ๊ฐ๋์ํค๋ ์ญํ (ํน์ ํ๋ซํผ์ ํนํ)
- Flutter
- ์ธ๋ฐํ ๋์์ธ ์๊ตฌ์ฌํญ์ด ์๊ฑฐ๋ 100% ์ปค์คํฐ๋ง์ด์งํ๊ณ ์ถ์ ๊ฒฝ์ฐ
- ์ธ๋ถ ํจํค์ง์ ์์กดํ์ง ์๊ณ ๊ณ ์์ค์ ์ ๋๋ฉ์ด์ ์ ๊ตฌํํ๊ณ ์ถ์ ๊ฒฝ์ฐ
- React Native
- ๋ค์ดํฐ๋ธ ์ฑ ์ด์์ฒด์ ์์์ ๊ฐ๋ฅํ ์์ ฏ์ ์ฌ์ฉํด์ผ ํ๋ ๊ฒฝ์ฐ
- ๋์์ธ์ด iOS ํน์ Android ์ฑ์ฒ๋ผ ๋ณด์ด๊ฒ๋ ๋ง๋ค๊ณ ์ถ์ ๊ฒฝ์ฐ
- ์ฐธ๊ณ : Flutter SDK ์ค์น
- ์ค์น ๋จ๊ณ 1. SDK ์ค์น (Flutter, Dart) > 2. ์๋ฎฌ๋ ์ดํฐ ์ค์น
- https://docs.flutter.dev/get-started/install > Windows
- SDK ์ค์น๊ฐ ์ ํ๋์ด์ผ ํจ
- Windows Flutter SDK ์ค์น
- https://docs.flutter.dev/release/archive?tab=windows
-
flutter_windows_3.3.8-stable.zip
ํ์ผ๋ก ์ค์น, path ์ค์
- Mac OS Flutter SDK ์ค์น
- https://docs.flutter.dev/release/archive?tab=macos
-
flutter_macos_3.19.6-stable.zip
ํ์ผ๋ก ์ค์น, path ์ค์ - M1 Mac ์ธ์ง ํ์ธํ์ฌ ๋ง๋ .zip ํ์ผ ๋ค์ด๋ก๋
- ๋ ๋ค ์ค์นํ๋ ๊ฒ์ด ๋ฒ๊ฑฐ๋กญ๋ค๋ฉด ์๋์ ๊ฐ์ด ์งํ
- Chocolatey : ์ฝ์์ ์ด์ฉํ Windows ํจํค์ง ์ค์น ๋งค๋์ (.zip ๋ค์ด๋ก๋ ๋ฐ path ์ค์ ํ์ X)
- https://chocolatey.org/install#individual
- PowerShell ์ค์น ๋ฐ ๊ด๋ฆฌ์ ๊ถํ ์คํ (administrative shell)
# Get-ExecutionPolicy -> Restricted ๋ฐํ ์ ๊ทธ ์๋ ๋ช
๋ น์ด๋ค ์
๋ ฅ
PS C:\Users\YNJCH> Get-ExecutionPolicy
Unrestricted
PS C:\Users\YNJCH> Set-ExecutionPolicy AllSigned
PS C:\Users\YNJCH> Set-ExecutionPolicy Bypass -Scope Process
PS C:\Users\YNJCH> Get-ExecutionPolicy
Bypass
# ์ ๋ช
๋ น์ด ์ดํ ์คํ
PS C:\Users\YNJCH> Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
๊ฒฝ๊ณ : 'choco' was found at 'C:\ProgramData\chocolatey\bin\choco.exe'.
๊ฒฝ๊ณ : An existing Chocolatey installation was detected. Installation will not continue. This script will not overwrite
existing installations.
If there is no Chocolatey installation at 'C:\ProgramData\chocolatey', delete the folder and attempt the installation
again.
Please use choco upgrade chocolatey to handle upgrades of Chocolatey itself.
If the existing installation is not functional or a prior installation did not complete, follow these steps:
- Backup the files at the path listed above so you can restore your previous installation if needed.
- Remove the existing installation manually.
- Rerun this installation script.
- Reinstall any packages previously installed, if needed (refer to the lib folder in the backup).
Once installation is completed, the backup folder is no longer needed and can be deleted.
- Chocolatey ์ค์น ํ, choco- ๋ช ๋ น์ด ์ฌ์ฉ ๊ฐ๋ฅ
PS C:\Users\YNJCH> choco install flutter
Chocolatey v1.1.0
Installing the following packages:
flutter
By installing, you accept licenses for the packages.
Progress: Downloading flutter 3.19.6... 100%
flutter v3.19.6 [Approved]
flutter package files install completed. Performing other installation steps.
The package flutter wants to run 'chocolateyinstall.ps1'.
Note: If you don't run this script, the installation will fail.
Note: To confirm automatically next time, use '-y' or consider:
choco feature enable -n allowGlobalConfirmation
Do you want to run the script?([Y]es/[A]ll - yes to all/[N]o/[P]rint): y
Downloading flutter
from 'https://storage.googleapis.com/flutter_infra_release/releases/stable/windows/flutter_windows_3.19.6-stable.zip'
Progress: 100% - Completed download of C:\Users\YNJCH\AppData\Local\Temp\flutter\3.19.6\flutter_windows_3.19.6-stable.zip (955.15 MB).
Download of flutter_windows_3.19.6-stable.zip (955.15 MB) completed.
Hashes match.
Extracting C:\Users\YNJCH\AppData\Local\Temp\flutter\3.19.6\flutter_windows_3.19.6-stable.zip to C:\tools...
C:\tools
PATH environment variable does not have C:\tools\flutter\bin in it. Adding...
Environment Vars (like PATH) have changed. Close/reopen your shell to
see the changes (or in powershell/cmd.exe just type `refreshenv`).
The install of flutter was successful.
Software installed to 'C:\tools'
Chocolatey installed 1/1 packages.
See the log for details (C:\ProgramData\chocolatey\logs\chocolatey.log).
- Homebrew : Mac OS ์ฉ ํจํค์ง ๊ด๋ฆฌ์ (Node, Python ๋ฑ ์ค์น๋ ๊ฐ๋ฅ)
- https://brew.sh/
brew install --cask flutter
- Chocolatey, Homebrew ๊ด๊ณ ์์ด ์ฝ์์ ์๋ ๋ช ๋ น์ด ์ ๋ ฅํ์ฌ ํ์ธ
PS C:\Users\YNJCH> flutter
Manage your Flutter app development.
Common commands:
(์๋ต)
PS C:\Users\YNJCH> flutter --version
Flutter 3.19.6 โข channel stable โข https://github.com/flutter/flutter.git
Framework โข revision 54e66469a9 (5 weeks ago) โข 2024-04-17 13:08:03 -0700
Engine โข revision c4cd48e186
Tools โข Dart 3.3.4 โข DevTools 2.31.1
- Windows ์ฌ์ฉ์ ๊ธฐ์ค์ผ๋ก
Windows
,Web
,Android
์ธ ๊ฐ์ง ์ค ์ ํ - Web ๊ฐ๋ฐ > ์ด๋ฏธ ์น ๋ธ๋ผ์ฐ์ ๊ฐ ์๊ธฐ ๋๋ฌธ์ ๋ณ๋๋ก ํ ์ผ X
- Android ๊ฐ๋ฐ > https://docs.flutter.dev/get-started/install/windows/mobile ํ์ธ
- Android Studio ์ค์นํด์ผ ํจ
- ํด๋ํฐ ์ฐ๊ฒฐํ์ฌ ์คํ or ์ ๋ฎฌ๋ ์ดํฐ ์คํ
- https://developer.android.com/
- Setup Wizard
- Install Type
Standard
- Install the following components ํ์ธ
- Install Type
Android SDK Platform, API 34.0.0
Android SDK Command-line Tools
Android SDK Build-Tools
Android SDK Platform-Tools
Android Emulator
- Android Studio ์คํ >
Plugins
>SDK Manager
ํ์ธ ๊ฐ๋ฅ (ํ๋ก์ ํธ ์ด๊ธฐ >Tools
์์๋ ํ์ธ ๊ฐ๋ฅ) - Android Studio > ํ๋ก์ ํธ ์ด๊ธฐ >
Tools
>Device Manager
-
Create Virtual Device
ํด๋ฆญ > ๊ธฐ์ข ์ ํ - Next >
x86 Images
> ์ํ๋ ์ด๋ฏธ์ง์ ์ค๋ฅธ์ชฝ ๋ค์ด๋ก๋ ์์ด์ฝ ํด๋ฆญ > SDK Quickfix ์ค์น ๋ํ์์ ํ์ธ - AVD(Android Virtual Device) ์ด๋ฆ ๋ณ๊ฒฝ
-
Show Advanced Settings
>Emulated Performance
>Graphics
>Hardware - GLES 2.0.
(ํ๋์จ์ด ๊ฐ์์ด ๊ฐ๋ฅํด์ง๊ณ ๋ ๋๋ง ์ฑ๋ฅ์ด ํฅ์๋จ)
-
- PowerShell >
Run Flutter doctor
-
Android์ฉ์ผ๋ก ๊ฐ๋ฐํ๊ธฐ๋ก ์ ํํ์ผ๋ฏ๋ก ๋ชจ๋ ๊ตฌ์ฑ ์์๊ฐ ํ์ํ์ง๋ ์์ต๋๋ค. ์ด ๊ฐ์ด๋๋ฅผ ๋ฐ๋ฅธ ๊ฒฝ์ฐ ๋ช ๋ น ๊ฒฐ๊ณผ๋ ๋ค์๊ณผ ์ ์ฌํฉ๋๋ค.
์ ๊ฐ์ ๊ฒฐ๊ณผ์ด๋ฏ๋ก ํจ์ค
-
PS C:\Users\YNJCH> flutter doctor
Found an existing Pub cache at C:\Users\YNJCH\AppData\Local\Pub\Cache.
It can be repaired by running `dart pub cache repair`.
It can be reset by running `dart pub cache clean`.
Doctor summary (to see all details, run flutter doctor -v):
[โ] Flutter (Channel stable, 3.19.6, on Microsoft Windows [Version 10.0.22631.3447], locale ko-KR)
[โ] Windows Version (Installed version of Windows is version 10 or higher)
[!] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
X cmdline-tools component is missing
Run `path/to/sdkmanager --install "cmdline-tools;latest"`
See https://developer.android.com/studio/command-line for more details.
X Android license status unknown.
Run `flutter doctor --android-licenses` to accept the SDK licenses.
See https://flutter.dev/docs/get-started/install/windows#android-setup for more details.
[โ] Chrome - develop for the web
[X] Visual Studio - develop Windows apps
X Visual Studio not installed; this is necessary to develop Windows apps.
Download at https://visualstudio.microsoft.com/downloads/.
Please install the "Desktop development with C++" workload, including all of its default components
[โ] Android Studio (version 2023.2)
[โ] VS Code (version 1.76.0)
[โ] Connected device (4 available)
[โ] Network resources
! Doctor found issues in 2 categories.
- Wi-Fi ๋ก ๋ก์ปฌ ๋๋ฐ์ด์ค์์ ์คํํ๊ธฐ
- ์๋จ ํ๋ก์ ํธ๋ช
๋๋กญ๋ค์ด >
Pair Devices Using Wi-Fi
- https://developer.android.com/studio/run/device?hl=ko#wireless
- ์๋จ ํ๋ก์ ํธ๋ช
๋๋กญ๋ค์ด >
- ๋ณ๋ ํ๋ก๊ทธ๋จ ์์ด ์คํ ๊ฐ๋ฅ
- DartPad ์์
counter
์ํ ํ์ธ (https://dartpad.dev/?sample=counter)
- PowerShell > Flutter Project ์์ฑ
PS C:\Users\YNJCH> cd C:\Users\YNJCH\Flutter
PS C:\Users\YNJCH\Flutter> flutter create toonflix
Creating project toonflix...
Resolving dependencies in toonflix...
Got dependencies in toonflix.
Wrote 129 files.
All done!
You can find general documentation for Flutter at: https://docs.flutter.dev/
Detailed API documentation is available at: https://api.flutter.dev/
If you prefer video documentation, consider: https://www.youtube.com/c/flutterdev
In order to run your application, type:
$ cd toonflix
$ flutter run
Your application code is in toonflix\lib\main.dart.
- ์คํํ๊ธฐ (๋ธ๋ผ์ฐ์ ์ ํ > http://localhost:11542/)
- Flutter ๋ฅผ console ์์ ์คํ์ํจ ๊ฒ์ด๋ฏ๋ก ์ ์ ์คํ๋ ๊ฒ ํ์ธ ํ ์ค์ง
PS C:\Users\YNJCH\Flutter> cd toonflix
PS C:\Users\YNJCH\Flutter\toonflix> flutter run
# ์์ ๊ฐ์ด ์คํํ ์ ์์
- Flutter ํ์ผ >
C:\Users\YNJCH\Flutter\toonflix\lib\main.dart
-
linux
,windows
,macos
,web
,android
,ios
ํด๋ : ๊ฐ๊ฐ ๊ฐ๋ฐ์ ์ํ ์ค์ ํ์ผ ๆ
- PowerShell ๋ก VSCode ์ด๊ธฐ :
PS C:\Users\YNJCH\Flutter\toonflix> code .
-
Extension
>Dart
,Flutter
์ค์น
-
- ํ๋จ์
Dart DevTools
์กด์ฌ ํ์ธ,Windows(windows-64)
ํ์ธ- ํด๋ฆญํ์ฌ ์คํํ ์ ์๋ ์๋ฎฌ๋ ์ดํฐ ํ์ธ
-
Chrome(web-javascript)
์ ํ orAndroid Emulator
์ ํ - ์๋ก์ด ์๋ฎฌ๋ ์ดํฐ ์ถ๊ฐ ์
avdmanager is missing from the Android SDK
์ค๋ฅ ๋ฐ์ ํด๊ฒฐ
-
Start Debbugging
> ์๋ฎฌ๋ ์ดํฐ์์ ์คํ๋๋ ๊ฒ ํ์ธ
-
Widget Inspector
ํ์ธ ๊ฐ๋ฅ -
main.dart
> ์์ ๋ณ๊ฒฝํ์ฌ ๋ฐ์๋๋ ๊ฒ ํ์ธ
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
backgroundColor: Theme.of(context).colorScheme.inverseSurface,
-
Flutter Docs Widget : https://docs.flutter.dev/ui/widgets
- Animation and Motion, Input, Layout ๋ฑ
-
pub.dev : https://pub.dev/
-
Samsung Android USB Driver : https://developer.samsung.com/android-usb-driver
-
UI CHALLENGE ์ฐธ๊ณ ์ฌ์ดํธ : https://dribbble.com/shots/19858341-Financial-Mobile-IOS-App
-
POMODORO APP ์ฐธ๊ณ ์ฌ์ดํธ : https://www.behance.net/gallery/98918603/POMO-UIKIT
-
WEBTOON APP ์ฐธ๊ณ ์ฌ์ดํธ
- ์นํฐ ์ ๋ณด ์กฐํ API : https://webtoon-crawler.nomadcoders.workers.dev/
- API ๊ฐ๋ฐ ๋ฐฉ๋ฒ : https://developers.cloudflare.com/workers/runtime-apis/html-rewriter/ (Cloudflare Workers application)
-
Tiktok ํด๋ก ์ฝ๋ฉ : https://nomadcoders.co/tiktok-clone/lobby
- ๊ฐ์ ๊ธฐ์ค ์คํฌ๋ฆฐ์ท : https://nomadcoders.co/downloads/tiktok.zip
-
Google Fonts : https://fonts.google.com/
-
Font Awesome : https://fontawesome.com/icons
-
CSS Framework (Size ๊ธฐ์คํ) : https://tailwindcss.com/
-
pub.dev : Dart, Flutter ๊ณต์ ํจํค์ง ๋ณด๊ด์
- Node.js ์ npm, Python ์ PyPI ์ ๋น์ทํ ๊ฐ๋
- HTTP ๊ด๋ จ ํจํค์ง : https://pub.dev/packages/http
A composable, Future-based library for making HTTP requests.
- PLATFORM > ANDROID, IOS, LINUX, MACOS, WEB, WINDOWS
- ์ค์น ๋ฐฉ๋ฒ : command line ์
๋ ฅ,
pubspec.yaml
$ dart pub add http # Dart ์ค์น
$ flutter pub add http # Flutter ์ค์น
dependencies:
flutter:
sdk: flutter
http: ^1.2.1 # ๋ฒ์ ์ ๋ณด ๊ธฐ์ฌ
-
pubspec.yaml
: VSCode ์์ ์ ์ฅ ์ฆ์ ๋ฐ๋ก ํจํค์ง ์ค์น (์๋์ ๊ฐ์ด ๋ฒํผ์ ๋๋ฌ ์ค์นํ ์๋ ์์)
- Font Awesome :๋ฌด๋ฃ ์์ด์ฝ (๋ธ๋๋ ๋ก๊ณ ๆ)
-
https://pub.dev/packages/font_awesome_flutter ํจํค์ง ์ค์น
- nomadcoders ๊ฐ์ ๊ธฐ์ค ์ค์น ์,
pubspec.yaml
>font_awesome_flutter: 10.3.0
์์ฑ
- nomadcoders ๊ฐ์ ๊ธฐ์ค ์ค์น ์,
-
FaIcon(FontAwesomeIcons.amazon)
- ์์ด์ฝ ์ด๋ฆ์ FontAwesome ์ฌ์ดํธ์์ ๊ฒ์ํ์ฌ ์ฌ์ฉ
- Mobbin : https://mobbin.com/
- UXArchive : https://uxarchive.com/
- WWIT : https://wwit.design/
- ์ ์์ด๋ณผ : https://uibowl.io/
- Refero : https://refero.design/
- Land-book : https://land-book.com/
- https://m3.material.io/
- Flutter Framework ๋ material design ์ฌ์์ ๋ฐ๋ฆ
- material design 3 ์ฌ์ฉ ์,
useMaterial3
์ฌ์ฉ
return MaterialApp(
theme: ThemeData(
useMaterial3: true,
-
Error Lens
Extension ์ค์น > ์๋ฌ ๋ด์ฉ์ ๋ฐ๋ก ๋ณผ ์ ์๊ฒ ํด์ค
- Widget ๊ตฌ์กฐ ํ์
-
Toggle select widget mode
: ์๋ฎฌ๋ ์ดํฐ์์ Widget ์ ํ ๊ฐ๋ฅ -
Overlay guidelines to assist with fixing layout issues
: ๊ฐ์ด๋๋ผ์ธ ํ์ธ ๊ฐ๋ฅ - Widget Tree : tree ๊ตฌ์กฐ๋ก ํ์ ๊ฐ๋ฅ
- Layout Explorer : ์ต์ ๊ฐ์ ๋ณ๊ฒฝํ์ฌ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ๊ฐ๋ฅ (์์ค ๋ณ๊ฒฝ X)
- Widget Details Tree : ๋ชจ๋ ์์ฑ๊ฐ ํ์ธ ๊ฐ๋ฅ
- ์ ํํ ๋ผ์ธ ์ข์ธก์ ๋
ธ๋ ์ ๊ตฌ ํด๋ฆญ (or
Ctrl
+.
) - ํน์ Widget ์ฝ๋ ๋ธ๋ก์ ๋ค๋ฅธ Widget ํ์๋ก ์ด๋ ์ >
Wrap with ~
ํด๋ฆญ-
Wrap with widget...
: ์ง์ ์ ๋ ฅํ์ฌ ์ฌ์ฉํ ์ ์๋๋ก ํ๋ง ์ ๊ณต -
Remove this widget
: ๊ฐ์ธ๊ณ ์๋ Widget ์ ๊ฑฐ ๊ฐ๋ฅ
-
- ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ Widget ์์ฑ ์ >
Extract Widget
ํด๋ฆญ
- ์ค๋งํธํฐ์ ์ฐ๊ฒฐํ๋ ค๋ฉด, PC์
Samsung Android USB Driver
์ค์น - ์ค๋งํธํฐ ์ค์ > ํด๋์ ํ ์ ๋ณด > ์ํํธ์จ์ด ์ ๋ณด > ๋น๋๋ฒํธ 5-6ํ ํด๋ฆญ > ๊ฐ๋ฐ์ ๋ชจ๋ ํ์ฑํ
- ์ค์ > ๊ฐ๋ฐ์ ์ต์
> USB ๋๋ฒ๊น
ON
- ์ค๋งํธํฐ์ด ๊ธฐ๊ธฐ ๋ชฉ๋ก์ ๋จ๋ฉด ์ฑ๊ณต > Run
- UI ์์ overflow ๋ฐ์ ์ ์๋์ ๊ฐ์ด ์๋ด๋จ
- ํ๋ฉด์ด overflow ๋๋ ์ด์ ๋ ์คํฌ๋กค์ด ๋ถ๊ฐํ๊ธฐ ๋๋ฌธ
-
SingleChildScrollView
Widget ์ฌ์ฉ
-
Widget build(BuildContext context) { ...
-
theme : ์ฑ์ ๋ชจ๋ ์คํ์ผ์ ํ ๊ณณ์์ ์ง์ ํ ์ ์๋ ๊ธฐ๋ฅ ์ ๊ณต
- ์์, ํฌ๊ธฐ, ๊ธ์ ๊ตต๊ธฐ ๋ฑ์ผ ๋ณ์๋ก ์ฌ์ฉ
- ์ ํ๋ฆฌ์ผ์ด์ ์ ์ํ ์คํ์ผ ์ํธ ์ง์
-
MaterialApp
>theme: ThemeData.dark()
์ ๊ฐ์ด ์ฌ์ฉ
- Flutter ๋ ๋๋ง ์์ ์์) Container > Row > Column > Container > Text
-
Text
๊ฐ ๋ถ๋ชจ ์์ ์ ๋ณดMaterialApp
์ ์ ๊ทผํ๋ ค๋ฉด =>context
์ฌ์ฉ
-
-
context :
Text
์ด์ ์ ๋ชจ๋ ์์ ์์๋ค์ ๋ํ ์ ๋ณด๋ฅผ ๊ฐ์ง๊ณ ์์ (= ์์ ฏ ํธ๋ฆฌ์ ๋ํ ์ ๋ณด)
- ํธ๋ํฐ ๋ฐฉํฅ ์ ํ์ ์ํ Widget
-
builder: (context, orientation) {}
-
orientation
: Orientation.portrait or Orientation.landscape ๋ฅผ ๋ฆฌํด
-
- ์์ if ๋ฌธ์ ์ด์ฉํ์ฌ Widget list ๋๋ Widget ์ ๋ฐฐ์นํ๋๋ก ํจ
if (orientation == Orientation.portrait) ...[
AuthButton(
text: 'Use email & password',
),
Gaps.v16,
AuthButton(
text: 'Continue with Google',
),
],
- ์ฑ ์์ ์ ์ ๋ฐ๊พธ๊ณ ์ถ์ state ๊ฐ ์๋ค๋ฉด engine ์์ฒด์ engine-widget ์ฐ๊ฒฐ์ ํ์คํ ์ด๊ธฐํํด์ผ ํจ
-
WidgetsFlutterBinding : This is the glue that binds the framework to the Flutter engine.
-
WidgetsFlutterBinding.ensureInitialized();
: GestureBinding, SchedulerBinding, ServicesBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding ๋ฅผ ๋ชจ๋ ์ด๊ธฐํ
-
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await SystemChrome.setPreferredOrientations(
[DeviceOrientation.portraitUp], // ์ธ๋ก ๋ชจ๋๋ก ๊ณ ์
);
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle.light); // ๋คํฌ ๋ชจ๋
runApp(const TikTokApp());
}
- Stateless Widget :
build()
๋ฉ์๋๋ฅผ ํตํด UI ๋ฅผ ์ถ๋ ฅํ๋ ์ญํ ๋ง ํจ
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: Scaffold(
backgroundColor: Color(0xFFF4EDDB),
body: Container(),
));
}
}
-
Scaffold
- navigation bar, body, navbar ์ฌ์ฉ
- Text ๋ฐฉํฅ, ์ฌ์ด์ฆ ๋ฑ์ ์ค์
-
st
์๋์์ฑFlutter Stateless Widget
๋ก Widget ํ์ผ ๊ตฌ์กฐ ์์ฑ ๊ฐ๋ฅ - ๋ชจ๋ Widget ์ ๊ฐ์์ build ๋ฉ์๋๊ฐ ์คํ๋์ด์ผ ํจ ->
@override ~ build()
- ๋
ธ๋ ์ ๊ตฌ Code Action >
Create constructor for final fields
๋ก ์์ฑ์ ํจ์ ์๋ ์์ฑ
class Button extends StatelessWidget {
final String text;
final Color bgColor;
final Color textColor;
const Button({super.key, required this.text, required this.bgColor, required this.textColor,});
-
_blackColor
: ์์ฃผ ์ฌ์ฉํ๋ ๊ฐ์ ์์ํ ๋ณ์๋ก ๋ฑ๋ก (_
๋ฅผ ๋ถ์ฌ private ํ์ ์ผ๋ก ์ฌ์ฉ) -
isInverted
: bool ๊ฐ์ผ๋ก ๋ฐ์์ ๋ถ๊ธฐ ์ฒ๋ฆฌ์ ํ์ฉ
final String name, code, amount;
final bool isInverted; // ์์ ๋ฐ์ ์ฌ๋ถ
final _blackColor = const Color(0xFF1F2123);
(์ค๋ต)
color: isInverted ? _blackColor : Colors.white,
-
<div>
์ ๊ฐ์ ์ญํ ์ ํ๋ Widget - child ๋ฅผ ๊ฐ์ง๋ ๋จ์ํ box ๋ก,๋ฒํผ ๋์์ธ ์ ์ฌ์ฉํ๊ฒ ๋จ
- ์๋ฏธ ์์ด ์ฌ์ฉ ์
Unnecessary instance of 'Container'
๋ฌธ๊ตฌ ๋ธ > ์คํ์ผ ์ง์ ํ์
-
Transform.scale
: ์์ด์ฝ์ด overflow ๋๋๋ก scale ์ค์ ํ์ -
Transform.translate
: ์์ด์ฝ์ด ์ด๋๋๋๋ก offset ์ค์ ํ์- ํตํ ์์ด์ฝ ์์น ์กฐ์ , ์นด๋ ๊ฒน์ณ ๋ณด์ด๋๋ก ์์น ์กฐ์ ๋ฑ
Transform.scale(
scale: 2.2,
child: Transform.translate(
offset: const Offset(-5, 12),
-
Row
,Column
: ๊ฐ๋ก, ์ธ๋ก๋ก ์ฐจ๋ก๋๋ก ๋ฐฐ์น -
Stack
: ์์ ฏ์ ์์ ์์ ์ ์๊ฒ ํจ (๊ฒน์ณ์ง๋๋ก)-
alignment
์์ฑ์ผ๋ก ๋ฐฐ์นํ ์ ์์
-
child: Stack(
alignment: Alignment.center,
children: [
Align(
alignment: Alignment.centerLeft,
child: icon,
),
Expanded(
child: Text(
text,
),
),
],
- Stateful Widget : ๋ฐ์ดํฐ๊ฐ ๋ณ๊ฒฝ๋จ์ ๋ฐ๋ผ ์ค์๊ฐ์ผ๋ก UI ์ ๋ฐ์๋จ
- ์๋ ๋ ๊ฐ์ ๋ถ๋ถ์ผ๋ก ๋๋์ด์ง (ํ๋จ ์์ค ํ์ธ)
- Stateless Widget ๊ทธ ์์ฒด
- State : Widget ์ ๋ค์ด๊ฐ ๋ฐ์ดํฐ์ UI ๋ฅผ ์ ์ฅ
-
StatelessWidget
> Code Action >Convert to StatefulWidget
์ผ๋ก ์์ ๊ฐ๋ฅ
class App extends StatefulWidget {
const App({super.key});
@override
State<App> createState() => _AppState();
}
class _AppState extends State<App> {
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: Scaffold(
backgroundColor: Color(0xFFF4EDDB),
body: Container(),
));
}
}
- State ์ ๋ฐ์ดํฐ๋ฅผ ๋ฐ๊ฟ ๋, UI ๊ฐ ์๋ก๊ณ ์นจ ๋๋ฉด์ ์ต์ ๋ฐ์ดํฐ๋ฅผ ๋ณด์ฌ์ค
- ๋ฐ์ดํฐ = class property
- State ๋
build()
ํจ์๊ฐ ํ์-
initState()
,dispose()
๋ฑ์ ํจ์๋ ์กด์ฌ (ํ์ X)
-
- State ํด๋์ค์๊ฒ ๋ฐ์ดํฐ๊ฐ ๋ณ๊ฒฝ๋์๋ค๊ณ ์๋ฆฌ๋ ํจ์
- State ์๊ฒ ์๋ก์ด ๋ฐ์ดํฐ๊ฐ ์์์ ์๋ฆฌ๊ณ ์ค์ค๋ก ์๋ก๊ณ ์นจํ๊ฒ ํจ
- ํจ์ ๋ด๋ถ์ ์์ฑํ๊ฒ ๋จ
- ์ํ๋ฅผ ์ด๊ธฐํ ํ๊ธฐ ์ํ ๋ฉ์๋ (Initialize)
- ๋๋ถ๋ถ ๋ณ์(Variable) ์ ์ธ์ผ๋ก ์ด๊ธฐํ ํ๊ธฐ ๋๋ฌธ์ ์ ์ฐ์ง ์์
- ๋ถ๋ชจ ์์์ ์์กดํ๋ ๋ฐ์ดํฐ ์ด๊ธฐํ ์ ์ฌ์ฉ
- ๋ฐ์ดํฐ ์ด๊ธฐํ, API ์์ ์ ๋ฐ์ดํธ ์ ์ฌ์ฉ
-
initState()
๋build()
๋ณด๋ค ๋จผ์ ํธ์ถ๋์ด์ผ ํจ
- ์์ ฏ์ด ์คํฌ๋ฆฐ์์ ์ ๊ฑฐ๋ ๋ ํธ์ถ๋๋ ๋ฉ์๋
- API ์ ๋ฐ์ดํธ, ์ด๋ฒคํธ ๋ฆฌ์ค๋๋ก๋ถํฐ ๊ตฌ๋ ์ทจ์ ์ ์ฌ์ฉ
- ์์ ฏ์ด ์์ ฏ ํธ๋ฆฌ์์ ์ ๊ฑฐ๋๊ธฐ ์ ์ ๋ฌด์ธ๊ฐ ์ทจ์ํ ๋
-
IconButton(onPressed: onClicked, icon: icon)
-
onPressed
: ํด๋ฆญํ ๋๋ง๋ค ์คํ๋ ํจ์๋ฅผ ํ ๋น - ์๋ฌด๊ฒ๋ ํ ๋นํ์ง ์์ ๋๋
onPressed: () {},
๋ก ์์ฑ
-
- ์คํ๋ ํจ์๋
_AppState
๋ด๋ถ์ ์์ฑ
class _AppState extends State<App> {
int counter = 0;
void onClicked() {
setState(() {
counter += 1;
});
}
(์ค๋ต)
child: IconButton(
onPressed: onClicked,
(์ค๋ต)
Text(
'$counter',
),
import 'package:http/http.dart' as http;
- HTTP ๊ด๋ จ ํจํค์ง ์ค์น ํ import ->
as ALIAS
๋ก ์ด๋ฆ ์ง์ ํ์ฌ ์ฌ์ฉ ๊ฐ๋ฅ
- return ๊ฐ :
Future<Response>
-
Future
: ๋ฏธ๋์ ๋ฐ์ ๊ฐ์ ํ์ - ๋์ค์ ์๋ฃ๋ ๊ฐ์ด์ง๋ง, ์๋ฃ๋๋ฉด
Response
๋ฅผ ๋ฐํํ๋ค๋ ๋ป
-
- async(๋น๋๊ธฐ) programming ์ฒ๋ฆฌ
-
http.get()
ํจ์์ API ์์ฒญ์ด ์ฒ๋ฆฌ๋๊ณ ์๋ต์ด ๋ฐํ๋ ๋๊น์ง ๊ธฐ๋ค๋ฆผ- ์๋ฒ๊ฐ ์๋ตํ ๋๊น์ง ํ๋ก๊ทธ๋จ์ด ๊ธฐ๋ค๋ฆฌ๊ฒ ๋จ
- ๋น๋๊ธฐ ํจ์(asynchronous function) ๋ด์์๋ง ์ฌ์ฉ ๊ฐ๋ฅ
- ๋ณดํต
Future
ํ์ ์ ์ฌ์ฉ๋จ
- ๋ณดํต
void getTodaysToons() async {
final url = Uri.parse('$baseUrl/$today');
final response = await http.get(url);
}
- ํจ์ ์คํ ์๋ฃ๋ ๋๊น์ง ๊ธฐ๋ค๋ฆฌ๊ธฐ ๋๋ฌธ์
Future
๊ฐ ์๋Response
ํ์ ์ผ๋ก ๋ฐ๋ก ๋ฐํ
- API ํต์ ์ผ๋ก ๋ฐ์ ๋ฐ์ดํฐ
response.body
๋ String ํ์
[
{
"id": "208",
"rating": "9.91",
"date": "24.05.12"
},
{
"id": "207",
"rating": "9.96",
"date": "24.05.05"
},
]
- ์ด ํ
์คํธ๋ฅผ ํด๋์ค๋ค์ List ํํ๋ก ๋ณํํ๋ ์์
ํ์ :
List<dynamic>
- String to JSON :
jsonDecode(response.body)
- return ๊ฐ์ด
dynamic
ํ์ ์ด๋ฏ๋ก ์ด๋ค ํ์ ์ด๋ ์์ฉํ ์ ์๊ฒ ๋จ
- String to JSON :
final List<dynamic> webtoons = jsonDecode(response.body);
for (var webtoon in webtoons) {
print(webtoon); // webtoon.runtimeType = _JsonMap
}
- named constructor(์ด๋ฆ์ด ์๋ ํด๋์ค ์์ฑ์) ๋ก ์ด๊ธฐํํ๊ธฐ
- Java ์ VO ๊ฐ์ฒด ์์ฑ ๊ณผ์ ๊ณผ ๋์ผ (
WebtoonModel
์์ฑ)
- Java ์ VO ๊ฐ์ฒด ์์ฑ ๊ณผ์ ๊ณผ ๋์ผ (
WebtoonModel.fromJson(Map<String, dynamic> json)
: id = json['id'],
title = json['title'],
thumb = json['thumb'];
-
WebtoonModel
ํด๋์ค๋ก ๋งคํํ๊ธฐ
List<WebtoonModel> webtoonInstances = [];
final List<dynamic> webtoons = jsonDecode(response.body);
for (var webtoon in webtoons) {
webtoonInstances.add(WebtoonModel.fromJson(webtoon));
}
- API ํต์ ์ ํตํด ๋ถ๋ฌ์จ ๋ฐ์ดํฐ๋ฅผ ๋ณด์ฌ์ฃผ๋ ๋ฐฉ๋ฒ์ ๋ ๊ฐ์ง
-
StatefulWidget
์์async
,await
์ฌ์ฉ(์ ์ฌ์ฉํ์ง ์์) -
webtoons
,isLoading
๋ณ์ ์์ฑ -
isLoading
์ํ ๋ณํ ๋ฐinitState
,setState
์ฌ์ฉ
class _HomeScreenState extends State<HomeScreen> {
List<WebtoonModel> webtoons = [];
bool isLoading = true;
void waitForWebtoons() async {
webtoons = await ApiService.getTodaysToons();
isLoading = false;
setState(() {});
}
@override
void initState() {
super.initState();
waitForWebtoons();
}
-
await
๋ฅผ ์ฌ์ฉํ ํ์๊ฐ ์์ - StatelessWidget ์์
FutureBuilder
์ฌ์ฉ-
future
: Future ๊ฐ์ ์ ๋ฌ -
builder
: UI ๋ฅผ ๊ทธ๋ ค์ฃผ๋ ํจ์ (BuildContext
,Snapshot
์ ์ ๋ฌ)
-
-
Snapshot
: Future ์ ์ํ๋ฅผ ์ ์ ์์ (๋ฐ์ดํฐ๋ฅผ ๋ฐ์๋์ง, ์ค๋ฅ ๋ฐ์ํ๋์ง)
final Future<List<WebtoonModel>> webtoons = ApiService.getTodaysToons();
(์ค๋ต)
body: FutureBuilder(
future: webtoons,
builder: (context, snapshot) {
if (snapshot.hasData) {
return const Text('there is data');
}
return const Text('loading...');
},
),
-
snapshot.hasData
: false ์ด๋ฉด ๋ก๋ฉ ์ค UI, true ์ด๋ฉด if ๋ฌธ ๋ด UI ๋ก ์ ํ๋จ -
snapshot.data
: API ํธ์ถ ํ ๋ฐ์ ๋ฐ์ดํฐ
-
getToonById()
๋ ํ๋ผ๋ฏธํฐ๋กwebtoon.id
๋ฅผ ๋ฐ์- StatelessWidget ์์
webtoonDetail
ํ๋กํผํฐ๋ฅผ ์ด๊ธฐํ ์,webtoon
ํ๋กํผํฐ์ ์ ๊ทผ ๋ถ๊ฐ
- StatelessWidget ์์
class DetailScreen extends StatefulWidget {
final WebtoonModel webtoon;
(์ค๋ต)
}
class _DetailScreenState extends State<DetailScreen> {
//final Future<WebtoonDetailModel> webtoonDetail = ApiService.getToonById(widget.webtoon.id);
late Future<WebtoonDetailModel> webtoonDetail; // initState() ์์ define ์์
@override
void initState() {
super.initState();
webtoonDetail = ApiService.getToonById(widget.webtoon.id);
}
(์ค๋ต)
- State ์ build method ๊ฐ StatefulWidget ์ data ๋ฅผ ๋ฐ์์ค๋ ๋ฒ
-
DetailScreen
>webtoon
๋ฐ์ดํฐ๋ฅผ_DetailScreenState
ํด๋์ค์์ ์ฌ์ฉ ->widget
์ ๋ถ์ -
late
์ฒ๋ฆฌํ ํinitState()
์์widget.webtoon.id
๋ก ์ฌ์ฉ
-
- ์ด๊ธฐํํ๊ณ ์ถ์ ๊ฐ์ด ์์ง๋ง, constructor ์์ ๋ถ๊ฐ๋ฅํ ๊ฒฝ์ฐ
late
์ฌ์ฉ initState()
๋ ํญ์build()
๋ณด๋ค ๋จผ์ , ๋จ ํ ๋ฒ๋ง ์คํ๋๋ค๋ ํน์ง
- ๋ง์ ์์ ๋ฐ์ดํฐ๋ฅผ ์ฐ์์ ์ผ๋ก ๋์ดํ ๋ ์ฌ์ฉ
- ์๋์ผ๋ก Scroll View ๋ ๊ฐ์ง (Overflow X)
return ListView(
children: [
for (var webtoon in snapshot.data!) Text(webtoon.title)
],
);
- ํ๋ฒ์ ๋ชจ๋ ์์ดํ ๋ก๋ฉํ๋ ๋ฐฉ์
- ์ธ์คํ๊ทธ๋จ
- ํ์๋ผ์ธ์ ๋ชจ๋ ์ฌ์ง์ ํ๋ฒ์ ๋ก๋ฉํ์ง ์์
- ์ฌ์ฉ์๊ฐ ๋ณด๊ณ ์๋ ์ฌ์ง์ด๋ ์น์ ๋ง ๋ก๋ฉํด์ผ ํจ
-
ListView
: ๋ฉ๋ชจ๋ฆฌ ๋ฌธ์ ๋ฐ์ -
ListView.builder
: ๋ง์ ์์ดํ ์ ์ต์ ํ -> ์ฌ์ฉ์๊ฐ ๋ณด๊ณ ์๋ ์์ดํ ๋ง build (๋๋จธ์ง๋ ๋ฉ๋ชจ๋ฆฌ์์ ์ญ์ )-
itemCount
: ๋ช ๊ฐ์ ์์ดํ ์ build ํ ์ง ์ค์ (์ ์ฒด ๊ฐ์ ์ค์ -> ํ๋ฉด์ ๋ณด์ด๋ ๊ฒ๋ง ๋จผ์ build ๋จ)
-
return ListView.builder(
scrollDirection: Axis.vertical, // ์คํฌ๋กค ๋ฐฉํฅ
itemCount: snapshot.data!.length,
itemBuilder: (context, index) {
var webtoon = snapshot.data![index];
return Text('$index ${webtoon.title}');
},
);
-
ListView.builder
+separatorBuilder
๋ก ๋ฆฌ์คํธ ์์ดํ ์ฌ์ด ๋ ๋๋ง ๋ Widget ์ค์
-
*.dart
๋ก ๋ง๋ ํด๋์ค๋ ๋จ์ํ Widget ์ ๊ฐ๋ - ์์ ฏ ์ ํ ์ ๋๋ฉ์ด์ ํจ๊ณผ, ๋ด๋น๊ฒ์ด์ ๋ฐ ๋ฑ์ด ์ค์
- ์ด๋ฒคํธ ๋ฐ์์ ๊ฐ์งํ๊ธฐ ์ํ ์์ ฏ
-
onTap
: ์ ์ ๊ฐ ๋ฒํผ์ ํด๋ฆญํ์ ๋
-
return GestureDetector(
onTap: () {
print('objects');
},
child: Column(
(์๋ต)
- ํ์ด์ง ์ด๋์ด ํ์ํ ๋ ์ฌ์ฉ
-
Navigator.push(context, route)
- Navigator ๋ก ์๋ก์ด route ๋ฅผ push
-
route
: StatelessWidget ์ ์ ๋๋ฉ์ด์ ํจ๊ณผ๋ก ๊ฐ์ธ, ์คํฌ๋ฆฐ์ฒ๋ผ ๋ณด์ด๋๋ก ํจ
-
Navigator.push(context, MaterialPageRoute(builder: builder))
-
builder
: route ๋ฅผ ๋ง๋๋ ํจ์ - StatelessWidget ์์ ฏ์ ๋ ๋๋ง
-
- ๊ธฐ์กด ํ๋ฉด์์ ์ด๋๋๋ฏ๋ก,
Scaffold
๋ ๋๋ง ํ์
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
DetailScreen(webtoon: webtoon);
},
),
);
-
MaterialPageRoute
๋์PageRouteBuilder
์ฌ์ฉํด๋ ๋จ
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) {
return DetailScreen(webtoon: webtoon);
},
fullscreenDialog: true,
),
);
- default : ์์ผ๋ก ์ค์์ดํ ๋๋ฉด์ ์ ํ์ด์ง ๋ฑ์ฅ
- ์ข์ธก ์๋จ ์์ด์ฝ์ด
โ
๋ก ๋ธ
- ์ข์ธก ์๋จ ์์ด์ฝ์ด
-
fullscreenDialog: true
- ์ข์ธก ์๋จ ์์ด์ฝ์
X
, ์ ๋๋ฉ์ด์ ํจ๊ณผ๋ ๋ณ๊ฒฝ๋จ
- ์ข์ธก ์๋จ ์์ด์ฝ์
- ํ๋ฉด ์ ํ ์ ์ ๋๋ฉ์ด์ ์ ๊ณต
// webtoon_widget.dart > ๋ชฉ๋ก ํ์ด์ง์ ์ด๋ฏธ์ง
Hero(
tag: webtoon.id,
child: Container(
// detail_screen.dart > ์์ธ ํ์ด์ง์ ์ด๋ฏธ์ง
Hero(
tag: webtoon.id,
child: Container(
- ํ๋ฉด ์ ํ ์, ์๋ก์ด ์ด๋ฏธ์ง๋ก ๋ฎ์ด๋ฒ๋ฆฌ๋ ๊ฒ ์๋ ๊ธฐ์กด ์ด๋ฏธ์ง๋ฅผ ์์ง์ด๋ ํจ๊ณผ ์ฃผ๊ธฐ
- ๋ ๊ฐ ํ๋ฉด์ ๊ฐ๊ฐ ์ฌ์ฉ, ๊ฐ๊ฐ์ ๊ฐ์ ํ๊ทธ๋ฅผ ์ค
- Flutter ์์ ๋ธ๋ผ์ฐ์ ์ด๊ธฐ
- ํจํค์ง ์ค์น : https://pub.dev/packages/url_launcher
-
pubspec.yaml
>url_launcher: ^6.2.6
์ ๋ ฅ - Readme ํญ ํ์ธ > iOS >
Info.plist
ํ์ผ ์์ - Readme ํญ ํ์ธ > Android >
AndroidManifest.xml
ํ์ผ ์์ -
sms
,tel
์ ๊ฐ๊ฐ ๋ฌธ์, ์ ํ ๊ด๋ จ์ด๋ฏ๋กhttps
๋ฅผ ์ถ๊ฐํด์ผ ํจ
-
onButtonTap() async {
final url = Uri.parse("https://www.naver.com");
await launchUrl(url);
}
- ํธ๋ํฐ ์ ์ฅ์์ ๋ฐ์ดํฐ๋ฅผ ๋ด์ ์ ์์
- ex. ํํธ ๋ฒํผ์ ๋๋ฌ ํธ๋ํฐ ์ ์ฅ์์ ์ข์์ ํ ์นํฐ ์ ์ฅ
- ํจํค์ง ์ค์น : https://pub.dev/packages/shared_preferences
-
pubspec.yaml
>shared_preferences: ^2.2.3
์ ๋ ฅ - Readme ํญ > ์๋ ๋ฐฉ์ ํ์ธ
-
-
SharedPreferences
์ด๊ธฐํ ํ์ >async
,await
ํ์
// SharedPreferences.getInstance()
final SharedPreferences prefs = await SharedPreferences.getInstance();
// initState() ํ์ฉ ์ late ๋ณ์ ์ง์
late SharedPreferences prefs;
Future initPrefs() async {
prefs = await SharedPreferences.getInstance();
}
@override
void initState() {
super.initState();
initPrefs();
}
- ๊ฐ์ ์ฝ๊ฑฐ๋ ์ ์ฅํ ์ ์์
await prefs.setInt('counter', 10);
final String? action = prefs.getString('action');
- Code, Method ๋ฑ์ผ๋ก Textfield ์ ๊ฐ์ ์์ ฏ์ ์ปจํธ๋กคํ ์ ์๊ฒ ํด์ค
-
StatefulWidget
>State
ํด๋์ค ์์ Controller ๋ฅผ ์ ์ธํ์ฌ ์ฌ์ฉ
-
class _UsernameScreenState extends State<UsernameScreen> {
final TextEditingController _usernameController = TextEditingController();
@override
void initState() {
super.initState();
_usernameController.addListener(() {
print(_usernameController.text); // ์
๋ ฅ๊ฐ์ ๋ณด์ฌ์ค
});
}
(์๋ต)
TextField(
controller: _usernameController,
-
initState()
>addListener
: Textfield ์ ํ ์คํธ ๋ณํ ๋ฑ์ ๊ฐ์งํ๊ธฐ ์ํด ํ์
- AnimatedContainer
- decoration ์ด ๋ณ๊ฒฝ๋์์ ๋ ์ ๋๋ฉ์ด์ ํจ๊ณผ๊ฐ ์ ์ฉ๋จ
- AnimatedContainer ์ ํจ๊ณผ๊ฐ ์์์๊ฒ๊น์ง ์ํฅ์ ์ฃผ์ง ์์
- ํ์ ์์ฑ๊ฐ :
duration: const Duration(milliseconds: 300),
- AnimatedDefaultTextStyle
- text ๊ฐ ๋ณ๊ฒฝ๋์์ ๋ ์ ๋๋ฉ์ด์ ํจ๊ณผ๊ฐ ์ ์ฉ๋จ
- ํ์ ์์ฑ๊ฐ :
duration
,style
child: AnimatedDefaultTextStyle(
// style : ์ ๋๋ฉ์ด์
ํ ์ํค๊ณ ์ถ์ ๋์
style: TextStyle(
color: _username.isEmpty
? Colors.grey.shade400
: Colors.white,
fontWeight: FontWeight.w600,
),
duration: const Duration(milliseconds: 300),
child: const Text("Next"),
- Widget ์ด ์๋ Controller ๋ฅผ ์ฌ์ฉํ๋ ๋ฐฉ์
late final AnimationController _animationController;
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
lowerBound: 1.0, // ํฌ๊ธฐ ์ง์
upperBound: 1.5, // ํฌ๊ธฐ ์ง์
value: 1.5, // default ํฌ๊ธฐ
duration: _animationDuration,
);
_animationController.addListener(() {
setState(() {});
});
}
-
_animationController.reverse()
์ํ- 1.5 => 1.0 ์ผ๋ก ๊ฐ์ด ๋ฐ๋๊ฒ ๋๋๋ฐ, build() ๋ 1.5, 1.0 ์ผ ๋๋ง ์ฌ์ํ ๋จ
- build() ๊ฐ ๊ฐ์ด ๋ฐ๋๋ ๊ฒ์ ๊ฐ์งํ๊ฒ ํ๋ ค๋ฉด?
_animationController
์ ์ด๋ฒคํธ ๋ฆฌ์ค๋ ์ถ๊ฐ - ๋ชจ๋ ๋จ๊ณ์์ build ๋ฉ์๋๋ฅผ ์คํํ๊ธฐ ์ํด
addListener
๋ด๋ถ์setState()
์ถ๊ฐ
child: Transform.scale(
scale: _animationController.value,
-
scale
: ์ฌ์ด์ฆ ์กฐ์ ์ ์ํด AnimationController ์ฌ์ฉ
-
14-1-1. ๋์ ์ฌ์ฉ ๊ฐ๋ฅํ ๋ฐฉ๋ฒ (
_animationController.addListener( ~
์ฃผ์ ์ฒ๋ฆฌ) -
AnimatedBuilder
๊ฐ ์ ๋๋ฉ์ด์ ์ ๋ณํ๋ฅผ ๊ฐ์งํ๊ณ , builder ๊ฐ ์ต์ ๊ฐ์ผ๋ก return ํ๋๋ก ํจ
child: AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return Transform.scale(
scale: _animationController.value,
child: child,
);
},
- Widget ์ด ์ฌ๋ผ์ง๋ฉด Controller ๋ ๋ฉ๋ชจ๋ฆฌ์์ ์ง์์ผ ํจ (Listener ๊ฐ ์๋ํ๋ ์ํ)
@override
void dispose() {
// _usernameController ์ ์ฐ๊ด๋ ์ด๋ฒคํธ ๋ฆฌ์ค๋๋ฅผ ๋ชจ๋ ์ง์
_usernameController.dispose();
super.dispose();
}
-
super.dispose();
๋ ๋ชจ๋ ๊ฒ์ ๋ค์,super.initState();
๋ ๋ชจ๋ ๊ฒ์ ์์ ์ ์ธํ๋ ๊ฒ์ ๊ถ์ฅ
-
controller: _emailController
: ์์ ฏ์ ์ปจํธ๋กคํ๊ธฐ ์ํด Controller ์ถ๊ฐ -
onSubmitted: (value) => _onSubmit
: ํค๋ณด๋์ Done ํด๋ฆญ ์์๋_onSubmit
์ด ์๋ํ๋๋ก + value ์ ๋ ฅ๊ฐ์ ์ ๊ณต -
onEditingComplete: () => _onSubmit
: ํค๋ณด๋์ Done ํด๋ฆญ ์์๋_onSubmit
์ด ์๋ํ๋๋ก + ๋งค๊ฐ๋ณ์ ์์ด ์คํ -
keyboardType: TextInputType.emailAddress
: ํค๋ณด๋ ํ์ ์ง์ -
autocorrect: false
: ์๋์์ฑ ๋๊ธฐ -
hintText: "Email"
: ํํธ ๋ฌธ์์ด -
errorText: _isEmailValid()
: ์๋ฌ ๋ฉ์์ง ํ์ (์์ผ๋ฉด ์๋ฌ ํ์๋์ง ์์)
- ์
๋ ฅ ํ๋๊ฐ ์๋ ์๋ฌด ๊ณต๊ฐ์ด๋ ํญํ ๊ฒฝ์ฐ, ํค๋ณด๋๊ฐ ์ฌ๋ผ์ง๋๋ก ํด์ผํจ
- focus ๋ ๊ฒ์ ๋ชจ๋ unfocus ์ํค๊ธฐ
void _onScaffoldTap() {
FocusScope.of(context).unfocus();
}
- ํจ์์
()
๋ฅผ ๋ถ์ด๋ฉด ์ฆ์ ์คํ๋จ-
_isEmailVaild()
: ์ฆ์ ์คํ๋จ
-
- ๋ถ์ด์ง ์์ผ๋ฉด ์ด๋ฒคํธ ๋ฐ์ ์์๋ง Flutter ๊ฐ add
-
_onNextTap
: ์ ์ ๊ฐ ํญํ ์๊ฐ์๋ง()
๋ฅผ ๋ถ์ด๊ฒ ๋จ
-
- ๊ณ ์ ์๋ณ์ ์ญํ ์ ํ๋ Global Key ํ์
- Form ์ State ์ ์ ๊ทผ ๊ฐ๋ฅ / Method Trigger ์คํ ๊ฐ๋ฅ
- Controller ๋ฅผ ์ด์ฉํด ์ถ์ ํ ํ์๊ฐ ์์
class _LoginFormScreenState extends State<LoginFormScreen> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
(์๋ต)
child: Form(
key: _formKey,
-
validate()
: ๊ฐ ํ๋์ ์ ํจ์ฑ ๊ฒ์ฌ ์ํ (return bool) -
save()
: ๋ชจ๋ ํ ์คํธ ์ ๋ ฅ์ onSaved ์ฝ๋ฐฑ ํจ์ ์คํ
void _onSubmitTap() {
if (_formKey.currentState != null) {
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
}
}
}
-
validator
:String?
์ ์ ํจ์ฑ ๊ฒ์ฌ ๊ฒฐ๊ณผ, ์๋ด ๋ฉ์์งString?
์ ๋ฐํ (null ์ผ ์๋ ์์) -
onSaved
:_formKey.currentState!.save();
์คํ ์ ์ฝ๋ฐฑ ํจ์-
newValue
: ์ ์ฅ๋ ์๊ฐ์ ์ ๋ ฅ๊ฐ
-
TextFormField(
decoration: const InputDecoration( ~ ),
validator: _chkTextField,
onSaved: (newValue) {
if (newValue != null) formData['email'] = newValue;
},
),
-
_chkTextField()
์คํ ๊ฒฐ๊ณผ ๋ฆฌํด๊ฐ์ด null ์ด๋ฉด,onSaved
์ ์ํดformData
Map ์ ์ ๋ ฅ๊ฐ์ด ์ธํ ๋จ
-
NavTab
: Custom Widget ์
return Scaffold(
// screens[_selectedIndex] ๋ก๋ ์์ฑ ๊ฐ๋ฅ
body: screens.elementAt(_selectedIndex),
bottomNavigationBar: BottomAppBar(
child: Row(
children: [
NavTab(
icon: FontAwesomeIcons.house,
isSelected: _selectedIndex == 0,
onTap: () => _onNavTap(0),
),
(x4 ๋ฐ๋ณต)
],
),
),
);
-
body
: ๊ต์ฒดํ Widget ์ ๋ฐฐ์ด index-
screens.elementAt(_selectedIndex)
: NavigationBar ์ ํ๋ ํ๋ฉด๋ง์ ๋ณด์ฌ์ฃผ๋๋ก build, ์ ํ๋์ง ์์ ์ด์ ํ๋ฉด์ Flutter ๊ฐ ์์ ๊ฒ ๋จ - Tab Navigation ์ ์๋ ์๋ฆฌ๋ฅผ ๊ธฐ์ตํจ
-
-
bottomNavigationBar
>onTap
: ํ์ฌ index_selectedIndex
๊ฐ์ ๋ณ๊ฒฝ
-
StfScreen
Widget ์ NavigationBar ๊ฐ ํญ๋ง๋ค ์ฌ์ฉํ๊ณ ์๋ ๊ฒฝ์ฐ, ๊ฐ state ๋ฅผ ํผ๋ํ๊ฒ ๋จ -
StfScreen( key: GlobalKey() )
: key ํ๋ผ๋ฏธํฐ๋ฅผ ์ฌ์ฉํ์ฌ, ์๋ก ๋ค๋ฅธ Widget ์ธ ๊ฒ์ฒ๋ผ ๋ ๋๋งํจ- ๋ค์ ํญ์ ๋์์ค๋ฉด ์๋ก build ํ๊ฒ ๋จ (ํ๋ฉด์ ์๋ก ๊ทธ๋ฆผ)
final screens = [
StfScreen(key: GlobalKey()),
StfScreen(key: GlobalKey()),
StfScreen(key: GlobalKey()),
StfScreen(key: GlobalKey())
];
- ๋ณด๊ณ ์์๋ ์์์ ๋ค์ fetch ํ์ง ์๊ณ , ์ฌ์ฉ์์ ์คํฌ๋กค ์์น๋ ๊ธฐ์ตํ ์ ์์ด์ผ ํจ
-
screens.elementAt(0)
:screens
์ 0๋ฒ์งธ ํ๋ฉด๋ง ๋ ๋๋ง
-
-
Offstage
: Widget ์ด ์๋ณด์ด๊ฒ ํ๋ฉด์ ๊ณ์ ์กด์ฌํ๊ฒ ํด์ค-
Creates a widget that visually hides its child.
(์ค์ ๋ก build ๋์ด render ๋๊ณ ์์ผ๋, child ๋ฅผ ๋ณด์ด์ง ์๊ฒ ์จ๊น) -
offstage
: bool ๊ฐ์ผ๋ก ํ๋ฉด์ ๋ณด์ฌ์ค์ง ๊ฒฐ์
-
body: Stack(
children: [
Offstage(
offstage: _selectedIndex != 0,
child: StfScreen(), // GlobalKey ๋ฏธ์ฌ์ฉ
),
],
),
- ์ ์ ๊ฐ ํ๋ฉด์ ๋ณด๊ณ ์๋์ง ํ์ธ ๊ฐ๋ฅ
- ์ฌ์ฉ์๊ฐ ํ๋ฉด์ ๋๊ฐ๋ฉด, ์์์ ๋ฉ์ถ๊ณ ๋ชจ๋ fetching ์ ์ค๋จ -> ๋์์์ ๋ ์ฌ๊ฐํ๋๋ก ํจ
- ํ ํ๋ฉด์์ ๋๋ฌด ๋ง์ resource ์ฌ์ฉ ์, Widget ์ด ์ฌ๋ผ์ง์ง ์๊ณ ์ฑ์ด ๋๋ ค์ง ์ ์์
- https://pub.dev/packages/video_player ํจํค์ง ์ฌ์ฉ
-
pubspec.yaml
-
video_player: 2.4.10
๋ค์ด๋ก๋ - ์๋์ ๊ฐ์ด assets ํด๋ ๋ํ ์ถ๊ฐํ ์ ์์
-
assets:
- assets/videos/
- https://pub.dev/packages/visibility_detector ํจํค์ง ์ฌ์ฉ
-
pubspec.yaml
>visibility_detector: 0.3.3
๋ค์ด๋ก๋ - ์คํฌ๋กค์ ๋ด๋ฆฌ๋ ์์ค์ ์์์ด ์ฌ์๋๋ ๋ฌธ์ ํด๊ฒฐ
- ์คํฌ๋กค์ ์์ ํ ๋ด๋ฆฐ ๋ค์ ์์์ด ์ฌ์๋๋๋ก ๋ณ๊ฒฝํ๊ธฐ ์ํด ์ฌ์ฉ
- ํ ๋ฒ์ ํ๋์ ๋น๋์ค๋ง ์ฌ์ํ๊ธฐ ์ํด
return VisibilityDetector(
key: Key("${widget.index}"),
onVisibilityChanged: (info) {
print("${widget.index} is ${info.visibleFraction * 100} Visible");
},
child: Stack(
-
Key("${widget.index}")
: ์ด์ ํ๋ฉด์์ ๋ฐ์ index ๊ฐ์ Key ๋ก -
onVisibilityChanged
>info.visibleFraction
: Widget ์ด ์ผ๋ง๋ ๋ณด์ด๋์ง ๋ํ๋ด๋ 0 ์ด์ 1 ์ดํ ๋ฒ์์ ์ (%)
-
slivers
: ์ฌ์ฉ์๊ฐ ์คํฌ๋กคํ ์ ์๋ ๊ตฌ์ญ (ํน์ ํ sliver widget ๋ชฉ๋ก์ ๋ฃ์) - Sliver* ์์ ๋ ๋ค๋ฅธ Sliver* ๋ฅผ ์ฌ์ฉํ ์ ์์
- ์๋๋ก ์คํฌ๋กคํด๋ ๋ณผ ์ ์๋ ์๋จ ์ ๋ชฉ ๋ฐ
-
snap
,floating
,stretch
,pinned
: SliverAppBar ๊ฐ ์จ๊ฒจ์ง๊ฑฐ๋ ๋ณด์ฌ์ง๋ ๋ฐฉ์ -
collapsedHeight
: ํ๋ฉด์ ์ธ์ด์ฌ๋ฆฌ๋ฉด 80px๊น์ง ์ค์ด๋ค์๋ค๊ฐ ์ฌ๋ผ์ง
return CustomScrollView(
slivers: [
SliverAppBar(
snap: true, // ์ด์ง๋ง ์ฌ๋ผ๊ฐ๋ appbar ์ ์ฒด๊ฐ ๋ฐ๋ก ๋ณด์ฌ์ง
floating: true, // ์คํฌ๋กค์ ๋ด๋ ธ๋ค๊ฐ ์ฌ๋ผ๊ฐ๋ฉด ๋ค์ ๋ณด์ฌ์ง
stretch: true,
pinned: true, // FlexibleSpaceBar ๋ฅผ ๋ณผ ์ ์๊ฒ ์ ์ง
collapsedHeight : 80,
expandedHeight: 200,
flexibleSpace: FlexibleSpaceBar(
stretchModes: const [
StretchMode.blurBackground,
StretchMode.zoomBackground,
StretchMode.fadeTitle,
],
background: Image.asset(
"assets/images/kota.jpg",
fit: BoxFit.cover,
),
title: const Text("Hello!"),
titlePadding:
const EdgeInsets.symmetric(horizontal: 0, vertical: 10),
centerTitle: true,
),
-
childCount
: ๋ชฉ๋ก ์์ดํ ๊ฐ์ -
itemExtent
: ์์ดํ ์ ํฌ๊ธฐ (height)
SliverFixedExtentList(
delegate: SliverChildBuilderDelegate(
childCount: 20, // 20๊ฐ ์ ํ
(context, index) => Container(
color: Colors.amber[100 * (index % 9)],
child: Align(
alignment: Alignment.center,
child: Text("Item $index"),
),
),
),
itemExtent: 100, // item ์ height
),
-
childCount
: ๊ทธ๋ฆฌ๋ ์์ดํ ๊ฐ์
SliverGrid(
delegate: SliverChildBuilderDelegate(
childCount: 50, // 50๊ฐ ์ ํ
(context, index) => Container(
color: Colors.blue[100 * (index % 9)],
child: Align(
alignment: Alignment.center,
child: Text("Item $index"),
),
),
),
// ํ์ฉ๋ ๋งํผ์ ๋ฌดํํ grid ๋ฅผ ์์ฑ
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 100,
mainAxisSpacing: Sizes.size20,
crossAxisSpacing: Sizes.size20,
childAspectRatio: 1,
),
),
- ์คํฌ๋กค์ ๋ด๋ฆฌ๋ ์ค ์ด ์์ญ์ ๊ฑธ๋ฆฌ๋ฉด
pinned
์ ์ํดCustomDelegate()
์์ญ์ด ๊ณ ์ ๋จ
SliverPersistentHeader(
delegate: CustomDelegate(),
pinned: true,
),
-
CustomDelegate
ํด๋์ค๋SliverPersistentHeaderDelegate
๋ฅผ extends ํด์ ์ฌ์ฉํด์ผ ํจ
class CustomDelegate extends SliverPersistentHeaderDelegate {
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return Container(
color: Colors.lightGreen,
// FractionallySizedBox : ๋ถ๋ชจ๋ก๋ถํฐ ์ต๋ํ ๋ง์ ๊ณต๊ฐ์ ์ฐจ์ง
child: const FractionallySizedBox(
heightFactor: 1, // 100% (maxExtent)
child: Center(
child: Text(
'SliverPersistentHeader!!!!!',
style: TextStyle(
color: Colors.white,
),
),
),
),
);
}
@override
double get maxExtent => 150; // ์ต๊ณ ๋์ด
@override
double get minExtent => 80; // ์ต์ ๋์ด
// SliverPersistentHeader ๊ฐ ๋ณด์ฌ์ ธ์ผ ๋๋์ง ์๋ ค์ฃผ๋ method
@override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
// maxExtent, minExtent ๋ณ๊ฒฝ ์์๋ true ๋ฐํ
// build ์์ ์์ ํ ๋ค๋ฅธ widget tree ๋ฆฌํด ์ false ๋ฐํ
return false;
}
}
- ์ผ๋ฐ์ ์ธ Flutter Widget ์ render ํ ๋ ์ฌ์ฉ
SliverToBoxAdapter(
child: Column(
children: [
CircleAvatar(
backgroundColor: Colors.red,
radius: 20,
)
],
),
),
- ์ฌ๋ฌ ๊ฐ์ ์คํฌ๋กค ๊ฐ๋ฅํ View ๋ค์ ๋ฃ์ ์ ์๊ฒ ํด์ค
- ๋ชจ๋ scroll position ๋ค์ ์ฐ๊ฒฐํด์ค
-
SliverAppBar
,TabBar
๋ฅผ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ
-
-
pubspec.yaml
>go_router: 6.0.2
์ ๋ ฅ- 12. ํ๋ฉด ์ ํ ๋ณด๋ค ๋ณต์กํ๊ณ ๋ค์ํ ํ์ฉ์ด ๊ฐ๋ฅํ ๋ฐฉ๋ฒ์ ์ฌ์ฉ
- ๋ฉํฐ ํ๋ซํผ ํ๊ฒฝ์์ ์ฌ์ฉํ๊ธฐ ์ ํฉ (
Navigator.pushNamed
: ๋ธ๋ผ์ฐ์ ์์ ์ฌ์ฉ ์ ์์ผ๋ก ๊ฐ๊ธฐ ๋ฒํผ ์ง์ X, ๋น์ถ์ฒ) -
/video/1
๊ณผ ๊ฐ์ด ํ๋ผ๋ฏธํฐ๋ ์ฌ์ฉํ ์ ์๋๋ก ํจ
- ์๋์ ๊ฐ์ด ํด๋์ค๋ฅผ ์์ฑํ์ฌ ์ฌ์ฉํจ
- routes ๋ ์ค์ฒฉํ์ฌ ์ฌ์ฉํ ์ ์์ (nested routes)
-
users/:id/settings
๊ฐ์ ๊ฒฝ์ฐ ๋ชจ๋ route ๋ฅผ ์ ์ง ์๊ณ ์ค์ฒฉ์์ผ ํด๊ฒฐ
-
final router = GoRouter(
routes: [
GoRoute(
path: SignUpScreen.routeURL, // String ๊ฐ
builder: (context, state) => const SignUpScreen(),
routes: [
GoRoute(
path: UsernameScreen.routeURL, // ์ด๋๋ ์ฌ๋์ ์ ๊ฑฐ
builder: (context, state) => const UsernameScreen(),
),
],
),
GoRoute(
path: "/users/:username", // parameter ๋ฅผ ๋ฐ์์ด
builder: (context, state) {
final username = state.params['username'];
return UserProfileScreen(username: username!);
},
),
],
);
-
main.dart
>MaterialApp.router
์ฌ์ฉ,routerConfig
์์ฑ๊ฐ์ผ๋ก ์ฐ๊ฒฐ
return MaterialApp.router(
routerConfig: router,
-
path
๋์name
์ฌ์ฉ ๊ฐ๋ฅ
// router.dart
GoRoute(
name: UsernameScreen.routeName,
path: UsernameScreen.routeURL,
// sign_up_screen.dart
context.pushNamed(UsernameScreen.routeName);
context.goNamed(UsernameScreen.routeName);
- go_router ํจํค์ง๊ฐ context ๋ฅผ ํ์ฅ์ํด
-
context.push(LoginScreen.routeURL);
,context.pop();
- ์์ผ๋ก, ๋ค๋ก๊ฐ๊ธฐ ๋ฒํผ์ผ๋ก ์๋ค๊ฐ๋คํ ์ ์์ (Stack ๊ตฌ์กฐ)
-
context.go(LoginScreen.routeURL);
- Stack ์ ๊ด๋ จ๋ ๊ฒ์ ์ ๋ถ ๋ฌด์ (Screen, Navigation)
- Stack ์ ๊ด๊ณ ์์ด ๋ณ๋์ ์์น๋ก ์ด๋์ํด (๋ค๋ก๊ฐ๊ธฐ ํ ์ ์์)
- back ๋ฒํผ์ด ํ์ ์์ ๋ ์ฌ์ฉ
-
:username
: Path Parameter -
~/users/ynjch?show=likes
:show
๋ Query Parameter
GoRoute(
path: "/users/:username", // parameter ๋ฅผ ๋ฐ์์ด
builder: (context, state) {
final username = state.params['username'];
final tab = state.params['show'];
return UserProfileScreen(username: username!, tab: tab!);
},
)
- URL ์ ๋ด์ง ์๊ณ ๋ฐ์ดํฐ๋ฅผ ์ ๋ฌํ ์ ์์
-
extra
: ์ด๋ค ํํ์ ๋ฐ์ดํฐ๋ ์ ๋ฌ ๊ฐ๋ฅ
context.push(
EmailScreen.routeURL,
extra: EmailScreenArgs(username: _username),
);
-
router.dart
> ๋ค์๊ณผ ๊ฐ์ด extra ๋ฅผ ๋ฐ์์ด
GoRoute(
path: EmailScreen.routeURL,
builder: (context, state) {
final args = state.extra as EmailScreenArgs;
return EmailScreen(
username: args.username,
);
},
),
- ํฐ๋ฏธ๋ >
flutter pub add camera
์ ๋ ฅํ์ฌ camera ํจํค์ง ๋ค์ด๋ก๋ (camera: ^0.11.0+1
) - Android >
Change the minimum Android sdk version to 21 (or higher) in your android/app/build.gradle file.
-
~\tiktok_clone\android\app\build.gradle
>minSdkVersion 21
๋ก ๋์ฒด
-
- iOS >
Add two rows to the ios/Runner/Info.plist
-
ios/Runner/Info.plist
> ์๋ ๋ด์ฉ ์ถ๊ฐ
-
<key>NSCameraUsageDescription</key>
<string>your usage description here</string>
<key>NSMicrophoneUsageDescription</key>
<string>your usage description here</string>
- ๋ ์ข์ camera ํจํค์ง ์ถ์ฒ : https://pub.dev/packages/camerawesome
- ๊ถํ ๊ด๋ จ ํจํค์ง ์ค์น : https://pub.dev/packages/permission_handler
-
pubspec.yaml
>permission_handler: ^10.2.0
์ ๋ ฅ
final cameraPermission = await Permission.camera.request();
final micPermission = await Permission.microphone.request();
- ๋ฏธ๋์ด ํ์ผ ์ ์ฅ ํจํค์ง ์ค์น : https://pub.dev/packages/gallery_saver
-
pubspec.yaml
>gallery_saver: 2.3.2
์ ๋ ฅ - ์ฌ์ฉ ์ ๊ถํ ์์ฒญ ํ์ (Readme ํญ ํ์ธ)
- Android :
~\tiktok_clone\android\app\src\main\AndroidManifest.xml
์ค์
- Android :
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application
android:label="tiktok_clone"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
(์๋ต)
- iOS :
~\tiktok_clone\ios\Runner\Info.plist
์ค์
<key>NSPhotoLibraryUsageDescription</key>
<string>This app required NSPhotoLibraryUsageDescription permission</string>
- Application ์ด ์ ๊ฑฐ๋์์ ๋(๋นํ์ฑํ), ์ ์ ๊ฐ ์ฑ์ ๋ ๋ฌ์ ๋ ์๋์ผ๋ก ๊ฐ์งํด์ผ ํจ
-
https://docs.flutter.dev/release/breaking-changes/add-applifecyclestate-hidden
-
with WidgetsBindingObserver
์ถ๊ฐ
-
-
initState()
>WidgetsBinding.instance.addObserver(this);
- ์ ์ ๊ฐ Application ์์ ๋ฒ์ด๋๋ฉด ์ ์ ์๋๋ก ํจ
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.inactive) {
_cameraController.dispose();
} else if (state == AppLifecycleState.resumed) {
initCamera();
}
}
-
didChangeAppLifecycleState()
: ์์คํ ์ด ์ฑ์ background ๋ก ๋ณด๋ด๊ฑฐ๋ ๋ค์ ๋์์ฌ ๋ ํธ์ถ
- https://pub.dev/packages/image_picker ํจํค์ง ๋ค์ด๋ก๋
-
pubspec.yaml
>image_picker: 0.8.6+1
์ ๋ ฅ-
~\tiktok_clone\ios\Runner\Info.plist
> iOS ๊ถํ ์ค์ ํ์ - Android ๋ ๋ณ๋ ์ค์ ํ ํ์ ์์
-
<key>NSCameraUsageDescription</key>
<string>your usage description here</string>
<key>NSMicrophoneUsageDescription</key>
<string>your usage description here</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>This app required NSPhotoLibraryUsageDescription permission</string>
final video = await ImagePicker().pickVideo(source: ImageSource.gallery);
- State ๊ด๋ฆฌ
- ์ฑ์ ๋ฐ์ดํฐ๋ฅผ ์ฒด๊ณ์ ์ผ๋ก ์ ๋ฆฌ, ๋ฐ์ดํฐ์ ๋ณ๊ฒฝ์ ๊ฐ์ง, ๋ฐ์ดํฐ๋ฅผ ๋ณ๊ฒฝํ๋ ๋ฐฉ์
- ์์ ฏ๊ณผ ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฆฌํ๋ ๋ชฉ์ ์ผ๋ก ์ฌ์ฉ
-
InheritedWidget
: ์ํ๋ฅผ ๊ด๋ฆฌํ๋ ๊ฐ์ฅ ์ฌ์ด ๋ฐฉ๋ฒ - ์์ ฏ ํธ๋ฆฌ ๋งจ ์์์ ์ ์๋ ๊ฐ๋ ์ ๊ทผ ๊ฐ๋ฅ (๋ถ๋ชจ/์์ ๊ฐ ์ ๋ฌํ ํ์ X)
- ํฌ๊ณ ์๊ตฌ์ฌํญ์ด ๋ง์ ์ฑ์๋ ์ฌ์ฉํ์ง ์๋ ๊ฒ์ ์ถ์ฒ
class VideoConfig extends InheritedWidget {
(์๋ต)
final bool autoMute = false;
static VideoConfig of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<VideoConfig>()!;
}
@override
bool updateShouldNotify(covariant InheritedWidget oldWidget) {
return true;
}
}
-
updateShouldNotify
- ์์ ฏ์ rebuild ํ ์ง ๋ง์ง๋ฅผ ์ ํ ์ ์๊ฒ ํด์ค (์์ํ๋ ์์ ฏ๋ค๋ rebuild ํด์ผํ ์๋ ์์)
- ์ด ์์ ฏ์ ๋ค์ render ํ๋ฉด,
oldWidget
์ state ๋ฅผ ๊ฐ์ง ์ ์์ - ์ด ์์ ฏ์ ์์ํ๋ ์์ ฏ๋ค์๊ฒ ์๋ ค์ค ๊ฒ์ธ๊ฐ๋ฅผ ๊ฒฐ์
-
dependOnInheritedWidgetOfExactType
- VideoConfig ๋ก ์ ๊ทผํ ์ ์๋ ์ง์ ์ ์ธ ๋งํฌ ํ์
- dependOnInheritedWidgetOfExactType : VideoConfig ๋ผ๋ ์ด๋ฆ์ InheritedWidget ์ ๊ฐ์ ธ์ค๋๋ก ํจ
- of Constructor ์์ฑํ์ฌ ๊ฐ๋จํ๊ฒ ์ ๊ทผํ๊ฒ ํจ (ํ๋ฉด๋ง๋ค ๊ธธ๊ฒ ์์ฑํ ํ์ X)
theme: ThemeData(
primaryColor: const Color(0xFFE9435A),
-
ThemeData.of(context)
,MediaQuery.of(context)
๋dependOnInheritedWidgetOfExactType
์ฌ์ฉ
- ํ๋ฉด์ด 10๊ฐ ๋ฏธ๋ง + API ์์ ๋ฐ์์ฌ ๊ฒ์ด ๋ง๊ฑฐ๋, ๋ฉ์๋, ๋ฐ์ดํฐ๊ฐ ๋ง์ ๋ ์ถ์ฒ
class VideoConfig extends ChangeNotifier {
bool autoMute = true;
void toggleAutoMute() {
autoMute = !autoMute;
notifyListeners();
}
}
-
notifyListeners()
: ํน์ ๋ฐ์ดํฐ ๊ฐ์ด ๋ณ๊ฒฝ๋์๋ค๊ณ ์๋ ค์ฃผ๊ธฐ ์ํด ์ฌ์ฉ
AnimatedBuilder(
animation: videoConfig,
builder: (context, child) => SwitchListTile.adaptive(
value: videoConfig.autoMute,
onChanged: (value) => videoConfig.toggleAutoMute(),
-
AnimatedBuilder
-
ChangeNotifier
์ ๊ฐ์ด ์ฐ์ => ๋ฐ์ดํฐ๊ฐ ๋ณด์ฌ์ง๋ ํ๋ฉด์์ ์ฌ์ฉ - ์ ๋๋ฉ์ด์ ๋ฟ๋ง ์๋๋ผ ๊ฐ ๋ณ๊ฒฝ ์๋ฆผ์๋ ์ฌ์ฉ๋จ
- ๋ฐ์ดํฐ ๋ณ๊ฒฝ ์ ์ด ๋ถ๋ถ๋ง ์๋ก rebuild ๋๊ธฐ ๋๋ฌธ์
InheritedWidget
๋ณด๋ค ์ ์ฉํจ
-
bool _autoMute = videoConfig.autoMute;
@override
void initState() {
super.initState();
videoConfig.addListener(() {
setState(() {
_autoMute = videoConfig.autoMute;
});
});
}
-
notifyListeners()
๋ฅผ ๋ฃ๊ธฐ ์ํดinitState()
์ ๋ฆฌ์ค๋ ์ถ๊ฐ - ๊ฐ์ด ๋ณ๊ฒฝ๋ ๋๋ง๋ค ์ ๋ฐ์ดํธ๋ฅผ ๋ฐ์
final videoConfig = ValueNotifier(true);
- ๊ฐ์ด ํ๋๊ณ true, false ๋ฑ ๊ฐ๋จํ ๊ตฌ๋ถ๋ง ํ์ํ ๊ฒฝ์ฐ
ChangeNotifier
๋ณด๋ค ์งง์ ์ฝ๋๋ก ์ฌ์ฉ ๊ฐ๋ฅ
ValueListenableBuilder(
valueListenable: videoConfig,
builder: (context, value, child) => SwitchListTile.adaptive(
value: videoConfig.value,
onChanged: (value) => videoConfig.value = !videoConfig.value,
-
AnimatedBuilder
๋์ValueListenableBuilder
์ฌ์ฉ ๊ฐ๋ฅ
- https://pub.dev/packages/provider ํจํค์ง ๋ค์ด๋ก๋
-
pubspec.yaml
>provider: 6.0.5
์ค์น
class VideoConfig extends ChangeNotifier {
bool isMuted = false;
void toggleIsMuted() {
isMuted = !isMuted;
notifyListeners();
}
}
-
wrapper around InheritedWidget
->InheritedWidget
๋ฅผ ์ฝ๊ฒ ์ฌ์ฉํ๊ณ ์ฌ์ฌ์ฉํ ์ ์๋๋ก ํจ - ์ฌ์ฉ์๊ฐ ๋ณด๋ UI ์์ ฏ๋ง ์๋ ์ฑ์ ๋ง๋ค ์ ์์ => ๊ธฐ๋ฅ์
ChangeNotifier
๋ก ๋ถ๋ฆฌ
Widget build(BuildContext context) {
// VideoConfig ChangeNotifier ๋ฅผ ์ฑ ์ ์ฒด์ ์ ๊ณต
return ChangeNotifierProvider(
create: (context) => VideoConfig(),
child: MaterialApp.router(
-
Provider
: The most basic form of provider. It takes a value and exposes it, whatever the value is. -
ChangeNotifierProvider
: A specification of ListenableProvider for ChangeNotifier. It will automatically callChangeNotifier.dispose
when needed.
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(
create: (context) => VideoConfig(),
),
],
child: MaterialApp.router(
- Provider ๊ฐ ๋ง์ ๋๋
MultiProvider
์์ ๋ฆฌ์คํธ๋ก ๊ธฐ์ฌ
SwitchListTile.adaptive(
value: context.watch<VideoConfig>().isMuted,
onChanged: (value) => context.read<VideoConfig>().toggleIsMuted(),
- ๊ฐ์ ธ๋ค ์ธ ๋๋
context.watch<VideoConfig>()
๋ก ์์ฑ-
watch
: ๋ณ๊ฒฝ์ฌํญ์ด ์์ผ๋ฉด rebuild (ํ๋กํผํฐ ๊ฐ์ ์ฌ์ฉ) -
read
: ๋ฉ์๋ ์ ๊ทผ ์ ์ฌ์ฉ
-
- ChangeNotifier, Provider ์ ์๋ง๋ ์ํคํ ์ฒ๋ก ์ ์
- Model, View, View Model ๋ก ๊ตฌ์ฑ๋จ
- View : UI ํํ, ์ฌ์ฉ์ ์ ๋ ฅ
- Model : ๋ฐ์ดํฐ
- View Model : ํ๋ฉด๊ณผ ๋ฐ์ดํฐ๋ฅผ ์ฐ๊ฒฐ => ํ๋ฉด์ผ๋ก๋ถํฐ ์ด๋ฒคํธ๋ฅผ ๋ฐ์ ๋ฐ์ดํฐ๋ฅผ ๋ณ๊ฒฝํ๊ณ ์ด๋ฅผ ํ๋ฉด์ ์๋ ค์ค (ChangeNotifier)
- Repository : ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๊ณ ๊ฐ์ ธ์ค๋ ์ญํ
- ๊ธฐ๊ธฐ ์ ์ฅ์์ ์ ์ฅ => ๋ฐ์ดํฐ๋ฅผ ๋์คํฌ์ ์ ์ฅ(persist)ํ๊ณ , ๋์คํฌ์์ ๊ฐ์ ธ์ด(read)
- Firebase ์ ํต์
- https://pub.dev/packages/shared_preferences ํจํค์ง ๋ค์ด๋ก๋
-
pubspec.yaml
>shared_preferences: ^2.0.17
์ ๋ ฅ
class PlaybackConfigModel {
bool muted;
PlaybackConfigModel({
required this.muted,
});
}
-
SharedPreferences
์ด์ฉํ์ฌ ๊ธฐ๊ธฐ ๋ด ์ ์ฅ์์ ๋ฐ์ดํฐ ์ ์ฅ
class PlaybackConfigRepository {
static const String _muted = "muted";
final SharedPreferences _preferences;
PlaybackConfigRepository(this._preferences);
// ์์๊ฑฐ ๊ด๋ จ ๋ฐ์ดํฐ ๋์คํฌ์ ์ ์ฅ
Future<void> setMuted(bool value) async {
_preferences.setBool(_muted, value);
}
// ์์๊ฑฐ ๊ด๋ จ ๋ฐ์ดํฐ ๋์คํฌ์์ ์ฝ๊ธฐ
bool isMuted() {
return _preferences.getBool(_muted) ?? false;
}
}
-
PlaybackConfigViewModel
ํด๋์ค construct ์์ ์ Repository ์๊ตฌ
class PlaybackConfigViewModel extends ChangeNotifier {
final PlaybackConfigRepository _repository;
late final PlaybackConfigModel _model = PlaybackConfigModel(
muted: _repository.isMuted(),
autoplay: _repository.isAutoplay(),
);
PlaybackConfigViewModel(this._repository);
// _model ์ private ํ๊ฒ ๋๊ณ getter ์ฌ์ฉ
bool get muted => _model.muted;
void setMuted(bool value) {
_repository.setMuted(value); // ๋์คํฌ์ ์ ์ฅ(persist)
_model.muted = value; // Model ์์
notifyListeners(); // ๋ฐ์ดํฐ๋ฅผ listen ํ๊ณ ์๋ screen ๋ค์๊ฒ ์ ๋ฌ
}
}
-
main.dart
-
PlaybackConfigViewModel
์ฌ์ฉ์ ์ํด Repository ๋ฅผ ๋ณด๋ด์ค์ผ ํจ - Repository ์ฌ์ฉ์ ์ํด
SharedPreferences
์ Instance ์ ์ ๊ทผ์ด ํ์
-
// Provider ์ด๊ธฐํ
final preferences = await SharedPreferences.getInstance();
final repository = PlaybackConfigRepository(preferences);
runApp(MultiProvider(
providers: [
ChangeNotifierProvider(
create: (context) => PlaybackConfigViewModel(repository),
),
],
child: const TikTokApp(),
));
- View ๋ก์ง๊ณผ Business ๋ก์ง์ ๊ฐ๊ฐ ๋ค๋ฅธ ๊ณณ์ ์์นํ๊ฒ ๋ง๋ค์ด์ค
- dependency ์ฃผ์ ํ์ฌ Provider ๋ค์ ์ด๋์์๋ ์ฝ์ ์ ์๊ฒ ํด์ค
- ์ฌ๋ฌ ๊ฐ์ Provider ๊ฐ ๋์ผํ type ์ ๊ฐ์ ๋ ธ์ถํ ์ ์์ (๋ณต์ ๊ฐ๋ฅ)
- ์์ ฏ ํธ๋ฆฌ ๋ฐ์ ์์นํ ์ ์์ (์์ ๋ฐ์ ์์ ฏ ํธ๋ฆฌ๋ฅผ ์ฌ์ฉํ ํ์ X)
- Provider ์ ํ์์ / Provider ์ ์ ์ฌ
- ๊ฐ์ฒด๋ฅผ ์บ์ฑ(cache)ํ๊ณ ์ข ๋ฃ(dispose)ํจ
- ๊ทธ๋ฌํ ๊ฐ์ฒด๋ค์ ์์ ฏ์ด listen ํ ์ ์๋ ๋ฐฉ๋ฒ ์ ๊ณต
- Provider ๊ฐ ๊ฐ์ง ๋ฌธ์ ์ ๋ณด์ (Provider ๊ฐ์ ๊ฒฐํฉ์ ๊ฐ์ํ)
- ๊ณต์ ๋ฌธ์ ์ฐธ๊ณ : https://riverpod.dev/docs/introduction/getting_started
- https://pub.dev/packages/riverpod ํจํค์ง ๋ค์ด๋ก๋
- Getting started >
flutter_riverpod: ^2.1.3
์ ๋ ฅ
- VSCode > Extensions >
Flutter Riverpod Snippets
install (shortcut ์ ๊ณต)
- View Model ์ Provider ๋ฅผ extend ํด์ผ ํจ
- NotifierProvider, AsyncNotifierProvider
- Firebase ์ด์ฉ ์, FutureProvider
-
PlaybackConfigViewModel
> Notifier
class PlaybackConfigViewModel extends Notifier<PlaybackConfigModel> {
final PlaybackConfigRepository _repository;
PlaybackConfigViewModel(this._repository);
@override
PlaybackConfigModel build() {
// PlaybackConfigViewModel build => ํ๋ฉด์ด ๊ฐ๊ฒ ๋๋ ์ด๊ธฐ ๋ฐ์ดํฐ๋ฅผ return
return PlaybackConfigModel(
muted: _repository.isMuted(),
);
}
void setMuted(bool value) {
_repository.setMuted(value);
// ์ํ ๋ณ๊ฒฝํ๋ฉด ๋ชจ๋ listener ๋ค์ด ์๋์ผ๋ก ํต์ง ๋ฐ์
state = PlaybackConfigModel(muted: value);
}
}
- Notifier ์์ expose ํ ๋ฐ์ดํฐ๋ฅผ ๋ฃ์ (ํ๋ฉด์ด listen ํ๊ธฐ๋ฅผ ์ํ๋ ๋ฐ์ดํฐ)
- Notifier ๋
State
๋ฅผ ๊ฐ์ง => Notifier ๋ด ์ด๋์๋ ๋ฐ์ดํฐ์ ์ ๊ทผ ๊ฐ๋ฅ -
State
: ์ฌ์ฉ์์๊ฒ ๋ ธ์ถ์ํค๊ณ ์ถ์ ๋ฐ์ดํฐ - ๋ชจ๋ธ ๊ฐ์ฒด ์ ๊ทผ ๋ฐฉ์ => state.muted (๋ฐ์ดํฐ๊ฐ ๋ณ๊ฒฝ๋์ง ์์)
- ๋ชจ๋ธ ๊ฐ์ฒด ๋ฐ์ดํฐ ๋ณ๊ฒฝ => State ๋ฅผ ๋์ฒดํด์ผ ํจ
state = PlaybackConfigModel(muted: value, autoplay: state.autoplay);
- Notifier ๋
final playbackConfigProvider =
NotifierProvider<PlaybackConfigViewModel, PlaybackConfigModel>(
() => throw UnimplementedError(),
// SharedPreferences ์ด ํ์ํด์ PlaybackConfigViewModel(repository) ๊ฐ ํ์ํ๋ฐ, ์ฌ๊ธฐ์ repository ๋ฅผ ์ธ ์ ์์ด์ ์๋ฌ๋ฅผ ๋ด๊ณ , main.dart ์์ ๋ฐ์์ด
);
- NotifierProvider<๋ฐ์ดํฐ ๋ณํ๋ฅผ ํต์ง, Provider ๊ฐ expose ํ ๋ฐ์ดํฐ>
final preferences = await SharedPreferences.getInstance();
final repository = PlaybackConfigRepository(preferences);
// 22.1 Riverpod ์ฌ์ฉ ๊ฐ๋ฅํ ํ๊ฒฝ์ผ๋ก ์ค์
runApp(
ProviderScope(overrides: [
playbackConfigProvider.overrideWith(
() => PlaybackConfigViewModel(repository),
),
], child: const TikTokApp()),
);
- Riverpod >
ConsumerWidget
-
WidgetRef ref
: Provide ๋ฅผ ๊ฐ์ ธ์ค๊ณ , ์ฝ์ ์ ์๋ ๋ ํผ๋ฐ์ค -
ref.watch
: ๋ณํ๋ฅผ listen ํ๊ธฐ ์ํจ /ref.read
: ํ ๋ฒ ์ฝ๋ ๊ฒ-
ref
: ViewModel > Notifier ๋ด์์๋ ์ฌ์ฉํ ์ ์์
-
-
@override
Widget build(BuildContext context, WidgetRef ref) {
(์๋ต)
SwitchListTile.adaptive(
value: ref.watch(playbackConfigProvider).muted,
onChanged: (value) =>
ref.read(playbackConfigProvider.notifier).setMuted(value),
title: const Text("Mute video"),
),
- Riverpod >
ConsumerStatefulWidget
-
ConsumerState
์ฌ์ฉํด์ผ ํ๋ฉฐ,ref
๋ ์ด๋์์๋ ์ด์ฉ ๊ฐ๋ฅ
-
// StatefulWidget
class VideoPost extends StatefulWidget {
@override
State<VideoPost> createState() => _VideoPostState();
}
class _VideoPostState extends State<VideoPost> {}
// ConsumerStatefulWidget
class VideoPost extends ConsumerStatefulWidget {
@override
VideoPostState createState() => VideoPostState();
}
class VideoPostState extends ConsumerState<VideoPost> {}
-
TimelineViewModel
> AsyncNotifier
class TimelineViewModel extends AsyncNotifier<List<VideoModel>> {
List<VideoModel> _list = [VideoModel(title: "First video")];
@override
FutureOr<List<VideoModel>> build() async {
await Future.delayed(const Duration(seconds: 3));
return _list;
}
void uploadVideo() async {
state = const AsyncValue.loading(); // ๊ฐ์ ๋ก loading state ๊ฐ ๋๋๋ก ํจ
await Future.delayed(const Duration(seconds: 2));
final newVideo = VideoModel(title: "${DateTime.now()}");
_list = [..._list, newVideo];
state = AsyncValue.data(_list);
}
}
final timelineProvider =
AsyncNotifierProvider<TimelineViewModel, List<VideoModel>>(
() => TimelineViewModel(),
);
-
FutureOr
: Future ๋๋ ๋ชจ๋ธ์ return -
state = AsyncValue.data(_list);
: AsyncNotifier ์์์ State ๋ฅผ ๋์ฒดํ ๋
-
when()
์ ์ด์ฉํ์ฌ ๊ฐ๊ธฐ ๋ค๋ฅธ state ๋ฅผ ์ํ callback ์ ๊ณต (async ๊ฐ์ด๋ฏ๋ก)return ref.watch(timelineProvider).when(data: data, error: error, loading: loading)
- ๊ฐ state ์ ๋ํ ์์ ฏ์ ์ธํ
(์๋ต)
data: (videos) => PageView.builder( // videos : timelineProvider ๊ฐ expose ํ๋ ๋ฐ์ดํฐ
controller: _pageController,
onPageChanged: _onPageChanged,
itemCount: videos.length,
itemBuilder: (context, index) => VideoPost(onVideoFinished: _onVideoFinished, index: index),
),
- state ์ ๋ฐ๋ฅธ ๋ถ๊ธฐ ์ฒ๋ฆฌ
IconButton(
onPressed: _onUploadPressed,
icon: ref.watch(timelineProvider).isLoading // state ์กฐํ
? const CircularProgressIndicator()
: const FaIcon(FontAwesomeIcons.cloudArrowUp),
),
-
router.dart
>GoRouter
๋ฅผProvider
๋ก ๊ฐ์ธ์ ์ฌ์ฉ
final routerProvider = Provider((ref) {
GoRouter(
-
main.dart
>ConsumerWidget
์ผ๋ก ๋ณ๊ฒฝํ์ฌref.watch
์ฌ์ฉ
class TikTokApp extends ConsumerWidget {
const TikTokApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return MaterialApp.router(
routerConfig: ref.watch(routerProvider),
- https://firebase.google.com/?hl=ko
- ๋ฐฑ์๋์์ ์ฌ์ฉํ ์ ์๋ ์ฌ๋ฌ๊ฐ์ง ์๋ฃจ์
์ ๊ณต (๋ฐฑ์๋๋ฅผ ๋ง๋ค ํ์ X)
- Firestore DB ์ ๊ณต
- ํด๋ผ์ฐ๋ ์ ์ฅ์ ์ ๊ณต (๋น๋์ค, ์ ์ ์๋ฐํ ๋ฑ)
- Authentication, ์์ ๋ฏธ๋์ด ์ธ์ฆ, Cookie, Token, ๋ณด์ ๋ฑ ์ ๊ณต
- ์ฑ ๋ฐฐํฌ, ํต๊ณ๋ถ์, ํ ์คํธ, ์๋ฆผ์ฐฝ, ๋์ ๋งํฌ ๋ฑ
- ์๊ธ์ ์ ๋ณด : https://firebase.google.com/pricing?authuser=0&hl=ko
- ํ๋กํ ํ์
์ ์, ์คํํธ์
์์ ์ฌ์ฉํ๊ธฐ ์ ํฉ
- SQL ์ ์ฌ์ฉํ ์ ์๊ณ , Firebase ์์ ์ ๊ณตํ๋ DB ๋ฅผ ์จ์ผ ํจ
- Firebase CLI reference > https://firebase.google.com/docs/cli?hl=ko
- Windows > binary ํ์ผ์ ๋ฐ์์ ์ค์น, console ์์
firebase
command ์ ๋ ฅ ๊ฐ๋ฅํ์ง ํ์ธ-
firebase-tools-instant-win.exe
์คํ > ์ค์น ๊ณผ์ ์์ ๋ก๊ทธ์ธ ์งํ > "Firebase CLI Login Successful"
-
> firebase --version
13.11.2
- macOS >
curl -sL https://firebase.tools | bash
์ ๋ ฅ
- https://firebase.google.com/docs/flutter/setup?hl=ko&platform=ios
- Firebase CLI : Firebase ์ ์ง์ ์ํต์ ํ๊ธฐ ์ํด ํ์
- VSCode > Terminal >
dart pub global activate flutterfire_cli
- VSCode > Terminal >
PS C:\Users\> dart pub global activate flutterfire_cli
+ ansi_styles 0.3.2+1s... (1.0s)
(์๋ต)
+ test_api 0.7.2
You can fix that by adding that directory to your system's "Path" environment variable.
A web search for "configure windows path" will show you how.
Activated flutterfire_cli 1.0.0.
Warning: Pub installs executables into C:\Users\YNJCH\AppData\Local\Pub\Cache\bin, which is not on your path.
You can fix that by adding that directory to your system's "Path" environment variable.
A web search for "configure windows path" will show you how.
Activated flutterfire_cli 1.0.0.
- ํ๊ฒฝ ๋ณ์ ํธ์ง > Path > ์ ๋ช
๋ น์ด ์ํ ๊ฒฐ๊ณผ๋ก ๋์จ ๊ฒฝ๋ก ์ถ๊ฐ (
C:\Users\YNJCH\AppData\Local\Pub\Cache\bin
) - flutterfire : Flutter ์ฑ์์ Firebase ๋ฅผ ์ค์ ํ๊ธฐ ์ํด ํ์
- VSCode > Terminal >
flutterfire configure
- ์ฌ๊ธฐ์ CommandNotFoundException ๋ฐ์ ์, ์๋ 26-1-2. ๋ด์ฉ ๋จผ์ ์ํ
- VSCode > Terminal >
PS C:\Users\YNJCH\Flutter\tiktok_clone> flutterfire configure
i Found 2 Firebase projects.
โ Select a Firebase project to configure your Flutter application with ยท tiktokclone-3260b (TikTokClone)
โ Which platforms should your configuration support (use arrow keys & space to select)? ยท web, ios, android
i Firebase android app com.example.tiktok_clone is not registered on Firebase project tiktokclone-3260b.
i Registered a new Firebase android app on Firebase project tiktokclone-3260b.
i Firebase ios app com.example.tiktokClone is not registered on Firebase project tiktokclone-3260b.
i Registered a new Firebase ios app on Firebase project tiktokclone-3260b.
i Firebase web app tiktok_clone (web) is not registered on Firebase project tiktokclone-3260b.
i Registered a new Firebase web app on Firebase project tiktokclone-3260b.
Firebase configuration file lib\firebase_options.dart generated successfully with the following Firebase apps:
Platform Firebase App Id
web 1:[์ซ์]:web:[์ฝ๋] // ~\tiktok_clone\firebase.json ์ ์ฐ์ฌ์๋ ๋ด์ฉ ํ์ธ!
android 1:[์ซ์]:android:[์ฝ๋]
ios 1:[์ซ์]:ios:[์ฝ๋]
Learn more about using this file and next steps from the documentation:
> https://firebase.google.com/docs/flutter/setup
-
C:\tools\flutter\bin
>cache
ํด๋ ์ญ์ > cmd ์ฐฝ์์flutter doctor
์ํ - PowerShell ๊ด๋ฆฌ์ ๊ถํ ์คํ > dart sdk ํ์ผ ์
๊ทธ๋ ์ด๋
choco upgrade dart-sdk
-
~\tiktok_clone\android\app\build.gradle
>minSdkVersion 23
๋ก ๋์ฒด
-
PS C:\Users\YNJCH> choco upgrade dart-sdk
Chocolatey v1.1.0
2 validations performed. 1 success(es), 1 warning(s), and 0 error(s).
Validation Warnings:
- A pending system reboot request has been detected, however, this is
being ignored due to the current Chocolatey configuration. If you
want to halt when this occurs, then either set the global feature
using:
choco feature enable -name=exitOnRebootDetected
or pass the option --exit-when-reboot-detected.
Upgrading the following packages:
dart-sdk
By upgrading, you accept licenses for the packages.
dart-sdk is not installed. Installing...
Progress: Downloading dart-sdk 3.4.3... 100%
Progress: Downloading dart-sdk 3.4.3... 100%
dart-sdk v3.4.3 [Approved]
- PowerShell ๊ด๋ฆฌ์ ๊ถํ ์คํ >
npm install -g firebase-tools
PS C:\Users\YNJCH> npm install -g firebase-tools
npm WARN deprecated [email protected]: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
npm WARN deprecated [email protected]: Rimraf versions prior to v4 are no longer supported
npm WARN deprecated [email protected]: Glob versions prior to v9 are no longer supported
added 620 packages in 27s
66 packages are looking for funding
run `npm fund` for details
PS C:\Users\YNJCH> firebase --version
13.11.2
PS C:\Users\YNJCH> firebase login
Already logged in as [๋ก๊ทธ์ธ ์ด๋ฉ์ผ ์ฃผ์]
- ์ฝ์๋ก ์ด๋ > https://console.firebase.google.com/
- ํ๋ก์ ํธ ๋ง๋ค๊ธฐ >
TikTokClone
ํ๋ก์ ํธ ๊ธฐ๋ณธ ์ค์ ์ผ๋ก ์์ฑ
- ํ๋ก์ ํธ ๋ง๋ค๊ธฐ >
- PC ๋ค์ ์์ํ๋ ๋์์...
- https://firebase.google.com/docs/flutter/setup?hl=ko&platform=ios
- Firebase DB ๊ฒฝ๋ก : https://console.firebase.google.com/project/tiktokclone-3260b
-
flutterfire configure
์งํ ํ,-
~\tiktok_clone\lib\firebase_options.dart
ํ์ผ ์์ฑ๋ ๊ฒ ํ์ธ
-
- VSCode > Terminal > ์๋ ๋ช
๋ น์ด ์์๋๋ก ์
๋ ฅ
- Firebase ํ๋ฌ๊ทธ์ธ์ ์ถ๊ฐ/์ ๊ฑฐํ ๋๋ง๋ค
flutterfire configure
์คํํด์ฃผ์ด์ผ ํจ
- Firebase ํ๋ฌ๊ทธ์ธ์ ์ถ๊ฐ/์ ๊ฑฐํ ๋๋ง๋ค
flutter pub add firebase_core # core ํ๋ฌ๊ทธ์ธ์ ์ค์น
flutterfire configure # Flutter ์ฑ์ Firebase ๊ตฌ์ฑ์ด ์ต์ ์ํ์ธ์ง ํ์ธ
- ํ๋ฌ๊ทธ์ธ ์ถ๊ฐ ์ค์น (4๋จ๊ณ: Firebase ํ๋ฌ๊ทธ์ธ ์ถ๊ฐ)
# flutter pub add [PLUGIN_NAME] ๋ก ์ค์น ์ํ => flutterfire configure ์ํ
flutter pub add firebase_auth
flutter pub add cloud_firestore
flutterfire configure
-
main.dart
์์ ์ด๊ธฐํ
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
- Firebase ์ด๊ธฐํ + FirebaseAuth.instance ์์ฑ => ๋ฐ๋ก Firebase ์ ์ํตํ ์ ์์
class AuthenticationRepository {
final FirebaseAuth _firebaseAuth = FirebaseAuth.instance;
bool get isLoggedIn => user != null;
User? get user => _firebaseAuth.currentUser; // ๋ก๊ทธ์ธํ ์ฌ์ฉ์๋ Nullable
}
final authRepo = Provider((ref) => AuthenticationRepository());
-
GoRouter
>redirect
์์ฑ์ ์ด์ฉํด ๋ก๊ทธ์ธ ์ฌ๋ถ ํ์ธ ํ ํ์ด์ง ๋ฆฌํด
final routerProvider = Provider((ref) {
return GoRouter(
redirect: (context, state) {
final isLoggedIn = ref.read(authRepo).isLoggedIn;
if (!isLoggedIn) { // ํ์๊ฐ์
, ๋ก๊ทธ์ธ ํ์ด์ง ์ ์ธ ์ ๊ทผ ๋ถ๊ฐํ๋๋ก
if (state.subloc != SignUpScreen.routeURL &&
state.subloc != LoginScreen.routeURL) {
return SignUpScreen.routeURL;
}
}
return null;
},
- Firebase console > Authentication > Get Started
- ๋ก๊ทธ์ธ ๋ฐฉ์ ์ ํํ๊ธฐ > Email/Password > Enable ํ ์ ์ฅ
-
signup_view_model.dart
>AsyncNotifier
view_model ์์ฑ- ๊ณ์ ์์ฑ ์ ๋ก๋ฉ ํ๋ฉด ๋ณด์ฌ์ฃผ๊ณ , ๊ณ์ ์์ฑ์ ํธ๋ฆฌ๊ฑฐ (ํ์๊ฐ์ )
class SignUpViewModel extends AsyncNotifier<void> {
late final AuthenticationRepository _authRepo;
@override
FutureOr<void> build() {
_authRepo = ref.read(authRepo);
}
Future<void> signUp() async {
state = const AsyncValue.loading();
final form = ref.read(signUpForm);
state = await AsyncValue.guard(
() async => await _authRepo.signUp(form["email"], form["password"]),
);
}
}
final signUpForm = StateProvider((ref) => {});
final signUpProvider = AsyncNotifierProvider<SignUpViewModel, void>(
() => SignUpViewModel(),
);
- AsyncValue ์ํ ๊ด๋ จ
-
AsyncValue.loading()
: ๋ก๋ฉ ์ค -
AsyncValue.data(null)
: ๋ก๋ฉ ์ํ ์ ๊ฑฐ -
AsyncValue.guard()
: ์ฝ๋ ์คํ ํ ์๋ฌ ๋ฐ์ ์ ๊ทธ ์๋ฌ๋ฅผ state ์ ๋ฆฌํดํ๊ณ , ์ ์ ์คํ ์ ๊ฒฐ๊ณผ๊ฐ์ state ์ ๋ฆฌํด-
(state.error as FirebaseException).message
๋ก ์๋ฌ ๋ฉ์ธ์ง ํ์ธ ๊ฐ๋ฅ
-
-
-
signUpForm
: ์ฌ์ฉ์๊ฐ ์ ๋ ฅํ ์ ๋ณด๋ฅผ ๋ด๋ ์ญํ-
StateProvider
=> ๋ฐ๊นฅ์์ ์์ ํ ์ ์๋ value ๋ฅผ expose ํ๊ฒ ํจ
-
-
signUpProvider
: state ํ์ธ /signUp()
๋ฑ ๋ฉ์๋ ์คํ
ref.read(signUpForm.notifier).state = {"email": _email};
-
email_screen.dart
> ์ด๋ฉ์ผ ์ฃผ์ ์ ๋ณด๋ฅผsignUpForm
state ์ ์ ๋ฌ
final state = ref.read(signUpForm.notifier).state;
ref.read(signUpForm.notifier).state = {...state, "password": _password};
-
password_screen.dart
> ๊ธฐ์กด state ์ password ๊ฐ ์ ๋ฌ- print ๊ฒฐ๊ณผ :
I/flutter ( 4427): {email: [์ด๋ฉ์ผ], password: [ํจ์ค์๋]}
- print ๊ฒฐ๊ณผ :
void _onNextTap() {
ref.read(signUpProvider.notifier).signUp();
}
-
birthday_screen.dart
>signUp()
์คํ- ๋ฒํผ ํ์ฑํ >
disabled: ref.watch(signUpProvider).isLoading,
- ๋ฒํผ ํ์ฑํ >
- UI์ ๋ฐฑ์๋ ๊ฐ์ ์ค์๊ฐ ์ฐ๊ฒฐ
- ์ฌ์ฉ์ ์ธ์ฆ ์ํ๊ฐ ๋ฐ๋ ๋ ์ฑ์ ์ค์๊ฐ์ผ๋ก ์๋ก๊ณ ์นจ
-
StreamProvider
: ๋ณํ๋ฅผ ๋ฐ๋ก ๊ฐ์งํ ์ ์์ => ์ ์ ์ธ์ฆ ์ํ ๋ณ๊ฒฝ์ ๊ฐ์ง (ex. ๋ก๊ทธ์ธ/๋ก๊ทธ์์)
class AuthenticationRepository {
final FirebaseAuth _firebaseAuth = FirebaseAuth.instance;
(์๋ต)
Stream<User?> authStateChanges() => _firebaseAuth.authStateChanges();
}
final authStateStream = StreamProvider((ref) {
final repo = ref.read(authRepo);
return repo.authStateChanges();
});
- View >
ref.watch(authStateStream);
: Stream ๋ณํ๊ฐ ์์ผ๋ฉด ๋ฐ๋ก rebuild ๋๋ฉฐ, router ์initialLocation
๋ก ๋์๊ฐ
-
https://console.firebase.google.com/project/tiktokclone-3260b/authentication/providers
- Firebase console > Authentication > Github > Enable ํ์ฑํ
- ์ฝ๋ฐฑ URL ๋ณต์ฌ
https://tiktokclone-3260b.firebaseapp.com/__/auth/handler
- Github Register a new OAuth application > https://github.com/settings/applications/new
- Application name :
TikTokClone
- Homepage URL :
http://temp.com
(ํ์ฌ ์๋ฌด๊ฑฐ๋ ์๊ด X) - Authorization callback URL :
https://tiktokclone-3260b.firebaseapp.com/__/auth/handler
-
Register application
ํด๋ฆญ
- Application name :
- Client ID ํ์ธ,
Generate a new client secret
ํด๋ฆญํ์ฌ Client secrets ํ์ธ - Firebase console > Authentication > Github ์ด์ด์ ์์
- Client ID :
Ov23li855FeRNA2vsRJV
- Client secrets :
1f81dbd16227abc6bf14dbac251480268a1d0870
- Client ID :
- Firebase Docs > https://firebase.google.com/docs/auth/flutter/federated-auth?hl=ko
- GitHub ๋ก๊ทธ์ธ ๊ด๋ จ ๋ด์ฉ ํ์ธ
- iOS : iOS ๊ฐ์ด๋๋ฅผ ๋ฐ๋ผ custom URL scheme ์ถ๊ฐ (https://nomadcoders.co/tiktok-clone/lectures/4333)
- VSCode > Terminal > App ์ signature ๋ฅผ ์์ฑ (Firebase ๊ฐ ์ด๋ฅผ ์์์ผ Authorization Client ๋ฅผ ๋ง๋ค์ด ์ค ์ ์์)
PS C:\Users\YNJCH\Flutter\tiktok_clone\android> ./gradlew signinReport
Starting a Gradle Daemon, 1 incompatible Daemon could not be reused, use --status for details
> Task :app:signingReport
Variant: debug
Config: debug
Store: C:\Users\YNJCH\.android\debug.keystore
Alias: AndroidDebugKey
MD5: B4:80:06:00:30:25:53:E8:1C:2B:F3:58:41:71:B1:02
SHA1: C9:FD:F1:9A:FC:30:57:E5:5F:D6:C0:96:81:18:37:36:8B:9F:CC:79
SHA-256: CA:E3:44:17:BE:6F:60:8C:08:54:BA:0F:A6:ED:BB:F2:C3:0F:5E:73:30:40:A5:8E:79:F7:E7:29:E9:27:0B:4C
Valid until: 2054๋
4์ 26์ผ ์ผ์์ผ
----------
Variant: release
Variant: profile
Variant: debugAndroidTest
... (์๋ต)
BUILD SUCCESSFUL in 12s
16 actionable tasks: 12 executed, 4 up-to-date
- Firebase console > Project Settings(ํ๋ก์ ํธ ์ค์ ) >
tiktok_clone (android)
>Add fingerprint
- https://console.firebase.google.com/project/tiktokclone-3260b/settings/general/android:com.example.tiktok_clone
- ๋ณต์ฌํ
SHA1
๋ด์ฉ ๋ถ์ฌ๋ฃ๊ธฐ - ์ด๊ฒ์ Debug ์ ์ฉ์ด๋ฏ๋ก ์ค์ ๋ฐฐํฌ ์์๋
Release > Setup > App Integrity
์์ ์ค์ !
- Firebase ์ NoSQL ๋ฐ์ดํฐ๋ฒ ์ด์ค
- ๋ฐ์ดํฐ ์ค๋ณต์ด ๋ฐ์ํ ์ ์๊ณ , ์ ๋ฐ์ดํธ ๋ฐ์ ์ ์ฌ๋ฌ ๊ณณ์์ ํด๋น ๋ฐ์ดํฐ ์ ๋ฐ์ดํธ ํด์ฃผ์ด์ผ ํจ
- Firestore : https://console.firebase.google.com/project/tiktokclone-3260b/firestore
- Create Database > ๋ณด์ ๊ท์น(ํ ์คํธ ๋ชจ๋) / ์๋ฒ ์์น(์ ์ ์ ๊ฐ๊น์ด ์ง์ญ) ์ ํ > Create
-
Start Collection
๋ก ํ ์ด๋ธ ์์ฑํ๊ธฐ- Collection ID :
users
- Document ID :
Auto generate ID
ํด๋ฆญํ์ฌ ์๋ ์์ฑ (user id
๊ฐ๋ ) - Field :
name
,bio
,link
๋ชจ๋ String ํ์ ์ผ๋ก ์์ฑ
- Collection ID :
- ์์ฑ๋
users
๋ด์ ์ Collection ์์ฑ ๊ฐ๋ฅ- Collection ID :
likes
- Document ID :
Auto generate ID
ํด๋ฆญํ์ฌ ์๋ ์์ฑ (video likes
๊ฐ๋ ) - Field :
video
๋ฅผ Reference ํ์ ์ผ๋ก ์์ฑ, ์ฐธ์กฐ ๊ฒฝ๋ก ์ค์ (videos/124
)
- Collection ID :
- ๋ฐ์ดํฐ๋ Dart ํด๋์ค๊ฐ ์๋ JSON ํ์ ์ผ๋ก ๋ณด๋ด์ผ ํจ
-
Map<String, String>
ํํ๋ก ๋ณ๊ฒฝํ์ฌ Repository ์์ Insertawait _db.collection("users").doc(user.uid).set(user.toJson());
Map<String, String> toJson() {
return {
"uid": uid,
"email": email,
"name": name,
"bio": bio,
"link": link,
};
}
- ๋ฐ์ดํฐ๋ฅผ ๋ฐ์ ๋๋ JSON ํ์ ์ Dart ํด๋์ค๋ก ๋ณํ
UserProfileModel.fromJson(Map<String, dynamic> json)
: uid = json["uid"],
email = json["email"],
name = json["name"],
bio = json["bio"],
link = json["link"],
birthday = json["birthday"];
- ์ด๋ Permission ์๋ฌ ๋ฐ์ ์, Firestore Rules ํ์ธ
match /{document=**} {
allow read, write: if request.time < timestamp.date(2025, 7, 17);
}
-
flutter pub add firebase_storage
์ค์น๋์ด์ผ ํจ - Storage : https://console.firebase.google.com/project/tiktokclone-3260b/storage
- Start > ๋ณด์ ๊ท์น(ํ ์คํธ ๋ชจ๋) > Done
- ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ด์
ref.watch()
๋ฅผ ํ๋กํ ์ด๋ฏธ์ง ๋ณ๊ฒฝ์๋ ํ์ฉํ๋ค๋ฉด ์ด๋ฏธ์ง ๋ณ๊ฒฝ ์, ํ๋ฉด ์ ์ฒด๊ฐ ๋ก๋ฉ๋ ๊ฒ์-
CircleAvatar
๋ถ๋ถ๋ง ๋ก๋ฉ๋ ์ ์๋๋ก ํ๋์ View ๋ก ๋ถ๋ฆฌํด์ผ ํจ
-
@override
Widget build(BuildContext context) {
return ref.watch(userProvider).when(
data: (data) => SafeArea(
(์๋ต)
CircleAvatar(
radius: 50,
foregroundImage: const NetworkImage(
"https://avatars.githubusercontent.com/u/69029517",
),
child: Text(data.name),
),
- Firebase Storage ์์๋ ๋๋ถ๋ถ reference ๋ก ์๋
- reference : ์ฑ๊ณผ Firebase ํด๋์์ link ๊ฐ์ ๊ฒ
Future<void> uploadAvatar(File file, String fileName) async {
final fileRef = _storage.ref().child("avatar/$fileName");
await fileRef.putFile(file);
}
-
_storage.ref()
: Storage Bucket -
child()
: reference ์ ๋ํ ์๋ ๊ฒฝ๋ก์ reference ๋ฅผ ๋ฆฌํด
- Google ๋ฐฑ์๋์ ์ด๋ค ์ฝ๋๋ ๋ฐฐํฌํ ์ ์๊ฒ ํด์ค (๋ฐฑ์๋์์ ์ด๋ฒคํธ๋ฅผ ๊ด์ฐฐ, ์ปค์คํ
ํจ์ ์คํ)
- AWS, Cloudflare, Google Cloud ๋ฑ๊ณผ ๊ฐ์ ๊ธฐ๋ฅ
- Authentication, Database, Storage ์ ๋ณํ ๋ฐ์ ์ ์คํํ ์ปค์คํ ์ฝ๋ ์์ฑ ๊ฐ๋ฅ
- ์๋ ์ฝ๋๋ก ์ค์นํ๋ ๊ณผ์ ์ด ํ์ํ๋ ์ฐ์ ์คํตํ์์
- Firebase ๋๊ตฌ ์ค์น ์ฝ๋ :
$ npm install -g firebase-tools
- ํ๋ก์ ํธ ์์ :
$ firebase init
- ํจ์ ๋ฐฐํฌ :
$ firebase deploy
- Firebase ๋๊ตฌ ์ค์น ์ฝ๋ :
-
https://console.firebase.google.com/project/tiktokclone-3260b/functions ์์ํ๊ธฐ
- ์ข ๋์ ์๊ธ์ ๋ก ์ ๊ทธ๋ ์ด๋ ํ์
- VSCode > Terminal > ์๋ ๋ช
๋ น์ด ์
๋ ฅ
- Firebase ํ๋ฌ๊ทธ์ธ์ ์ถ๊ฐ/์ ๊ฑฐํ ๋๋ง๋ค
flutterfire configure
์คํํด์ฃผ์ด์ผ ํจ
- Firebase ํ๋ฌ๊ทธ์ธ์ ์ถ๊ฐ/์ ๊ฑฐํ ๋๋ง๋ค
flutter pub add cloud_functions # Cloud Functions ํ๋ฌ๊ทธ์ธ์ ์ค์น
- Node.js ์ค์น ์ฌ๋ถ ํ์ธ
- Cloud Functions ๋ Typescript ๋ก ์์ฑ๋๋ฉฐ, Typescript ๋ Node.js ์์์ ๋์
C:\Users\YNJCH>node -v
v20.10.0
- VSCode > Terminal > ์๋ ๋ช
๋ น์ด ์
๋ ฅ
- ๊ธฐ์กด ํ๋ก์ ํธ ์ ํ
Use an existing project
>tiktokclone-3260b (TikTokClone)
-
TypeScript
์ ํ ๋ฐESLint
๋ ์ฌ์ฉํ์ง ์์
- ๊ธฐ์กด ํ๋ก์ ํธ ์ ํ
firebase init functions
? Please select an option: Use an existing project
? Select a default Firebase project for this directory: tiktokclone-3260b (TikTokClone)
? What language would you like to use to write Cloud Functions? TypeScript
? Do you want to use ESLint to catch probable bugs and enforce style? No
+ Firebase initialization complete!
-
functions
ํด๋ ->~\functions\src\index.ts
ํ์ผ ์์ฑ๋ ๊ฒ ํ์ธ- ๋ฐฑ์๋์ ๋ค์ด๊ฐ ์ฝ๋๋ฅผ ์์ฑํ๊ฒ ๋จ
- ์์ฑ, ์์ , ์ญ์ ๋ฅผ listen ํ ์ ์์
- ์์ ์
๋ก๋ ํ์ ๋, ์ด๋ฅผ ๊ด์ฐฐํ๋ ์ปค์คํ
์ฝ๋๋ฅผ ์์ฑ
- ์ฌ์ฉ์๋ ์์์ ์ฌ๋ฆฌ๊ธฐ๋ง ํ๋ฉด ๋ฐฑ๊ทธ๋ผ์ด๋์์ ๋ชจ๋ ๋์ํ ๊ฒ์
- ์ฌ์ฉ์๊ฐ ๋น๋์ค document ์ ๋ก๋ ํ๋ ๊ฒ์ ๊ด์ฐฐ -> ํ์ผ ๋ค์ด๋ก๋, ์ธ๋ค์ผ ์ถ์ถ -> Storage bucket ์ ์ธ๋ค์ผ ํ์ผ์ ์ ๋ก๋ -> ์ธ๋ค์ผ property ๋ฅผ ์ถ๊ฐํด ์์ ์ ๋ฐ์ดํธ
- Firebase ์ admin ์ import (Firebase ์ ๊ด๋ฆฌ์ ๋๊ตฌ ์กด์ฌ)
-
~\tiktok_clone\functions\package.json
์firebase-admin
,firebase-functions
์๋ ๊ฒ ํ์ธ
"dependencies": {
"firebase-admin": "^12.1.0",
"firebase-functions": "^5.0.0"
},
-
~\tiktok_clone\functions\src\index.ts
์๋์ ๊ฐ์ด ์ ๋ ฅ
import * as functions from "firebase-functions";
import * as admin from "firebase-admin";
// admin ์ด๊ธฐํ
admin.initializeApp();
/**์ ์์์ด ์๋์ง listen -> ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ถ๊ฐ -> ์ถ๊ฐ์ ์ธ ์ ๋ณด๋ฅผ ์์์ ์ถ๊ฐ
* document() : listen ํ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ฒฝ๋ก ์
๋ ฅ
* {videoId} : ๋ณ์์ฒ๋ผ ์๋ -> onCreate ๋ก ๊ฐ์ด ์์ฑ๋ ๋ ์๋ฆผ์ ๋ฐ์
* onCreate() : snapshot(์์ฑ๋ document), context(์ด๋ค document ๊ฐ ์์ฑ๋์๋์ง videoId ์ ๋ณด ์ป์ ์ ์์) ํ๋ผ๋ฏธํฐ
*/
export const onVideoCreated = functions.firestore
.document("videos/{videoId}")
.onCreate(async (snapshot, context) => {
// snapshot : ๋ฐฉ๊ธ ๋ง๋ค์ด์ง ์์์ ์๋ฏธ (ref ๋ก document ์ ์ ๊ทผ ๊ฐ๋ฅ)
snapshot.ref.update({ "hello": "from functions" });
});
- VSCode > Terminal > ์๋ ๋ช ๋ น์ด ์ ๋ ฅ
PS C:\Users\YNJCH\Flutter\tiktok_clone> firebase deploy --only functions
(์๋ต)
+ Deploy complete!
- ์ค๋ฅ ๋ฐ์ ์ ์๋์ ๊ฐ์ด ํด๊ฒฐ (~never read ๋ด์ฉ๋ง ์ฃผ์ํ์๋๋ ํด๊ฒฐ๋จ)
-
src/index.ts:10:1 - error TS6133: 'onRequest' is declared but its value is never read.
- ๊ด๋ จ ๋ด์ฉ ์ฃผ์ ์ฒ๋ฆฌ :
// import {onRequest} from "firebase-functions/v2/https";
- ๊ด๋ จ ๋ด์ฉ ์ฃผ์ ์ฒ๋ฆฌ :
- ๋
ธ๋ ๊ด๋ จ ์ค๋ฅ ๋ฐ์ ์, ๋
ธ๋ ๋ฒ์ ์ผ์น์ํค๊ธฐ :
"node": "18"
->"node": "20"
-
Error: HTTP Error: 500, Failed to generate source upload url for 1st Gen function projects/tiktokclone-3260b/locations/us-central1
- region ์ถ๊ฐํ๊ธฐ :
functions.region('asia-northeast3').firestore.document('videos/{videoId}')
- region ์ถ๊ฐํ๊ธฐ :
-
.document("videos/{videoId}")
: ์๋ก์ด document ๊ฐ ์์ฑ๋๋ฉด -
.update({ "hello": "from functions" })
: hello ๋ผ๋ ํ๋ ์ถ๊ฐํ๋๋ก ํจ