24년 6월을 기준으로 작성되었습니다.
1. [13장] 영상 통화 : WebRTC, 내비게이션, 아고라 API
1-1. camera 패키지
camera 패키지를 사용하면, 카메라 기능을 활용할 수 있다.
이 장에서는 아고라 패키지를 사용하지만, 플러터 공식 패키지인 camera 패키지를 사용하는 법을 배우자.
import 'package:flutter/material.dart';
import 'package:camera/camera.dart';
late List<CameraDescription> _cameras;
Future<void> main() async {
// Flutter 앱이 실행될 준비가 됐는지 확인
WidgetsFlutterBinding.ensureInitialized();
// 핸드폰에 있는 카메라들을 가져온다
_cameras = await availableCameras();
runApp(const CameraApp());
}
class CameraApp extends StatefulWidget {
const CameraApp({super.key});
@override
State<CameraApp> createState() => _CameraAppState();
}
class _CameraAppState extends State<CameraApp> {
// 카메라를 제어할 수 있는 컨트롤러 선언
late CameraController controller;
@override
void initState() {
super.initState();
initializeCamera();
}
initializeCamera() async {
try {
// 가장 첫번째 카메라로 카메라 컨트롤러를 설정한다
controller = CameraController(_cameras[0], ResolutionPreset.max);
// 카메라 초기화
await controller.initialize();
setState(() {});
} catch (e) {
// 에러났을 때 출력
if (e is CameraException) {
switch (e.code) {
case 'CameraAccessDenied':
print('User denied camera access!');
break;
default:
print('Handle other errors.');
break;
}
}
}
}
@override
void dispose() {
// 컨트롤러 삭제
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
// 카메라 초기화 상태 확인
if (!controller.value.isInitialized) {
return Center(
child: CircularProgressIndicator()
);
}
return MaterialApp(
home: CameraPreview(controller),
);
}
}
➡️ 특이사항
1. WidgetsFlutterBinding.ensureInitialized() : material.dart에서 제공된다.
main() 함수의 첫 실행값이 runApp()이 아닐 경우, 꼭 제일 먼저 실행해줘야 한다.
2. await availableCameras() : 기기에서 사용할 수 있는 카메라들을 가져온다.
3. CameraController : 카메라를 제어할 수 잇는 컨트롤러이다.
생성자 첫 번째 파라미터는 "사용할 카메라" 를 입력한다.
두 번째 파라미터는 "해상도" 를 설정한다. 그 중 ResolutionPreset.max는 "최대 해상도" 를 의미한다.
4. CameraPreview 위젯 : 카메라를 화면에 보여줄 수 있다. 첫 번째 파라미터에 CameraController를 입력한다.
1-2. WebRTC
영상 통화 기능을 구현하려면 -> 영상과 음성 정보를 저장, 전송하기 / 클라이언트 간 연결하기 등등 다양한 기능이 필요하다. 웹 브라우저 기반으로 통신하는 "WebRTC" 라는 API가 있다.
음성 통화 / 영상 통화 / P2P 파일 공유 기능을 제공하므로 다양한 분야에 사용할 수 있다.
WebRTC를 사용하려면, 두 클라이언트 말고도 중계용 서버가 필요하다.
영어로는 시그널링 서버 (Signalling Server)라고 하는데, 직접 구현보다는 "아고라" 서비스를 이용해보자.
※ iOS 시뮬레이터는 카메라 기능을 아예 제공하지 않는다.
안드로이드 에뮬레이터는 카메라 앱에서 샘플 영상만 실행된다. 따라서 이번 프로젝트는 실제 디바이스가 1대 이상 필요하다.
1-3. 내비게이션 (Navigation)
내비게이션은 플러터에서 화면을 이동할 때 사용하는 클래스이다. 스택(Stack) 데이터 구조로 설계되어 있다.
플러터에서는 내비게이션 스택의 가장 위에 위치한 위젯을 화면에 보여준다.
Navigator 클래스에서 제공하는 메서드를 사용해서 내비게이션 스택을 사용할 수 있다.
최상위에 MaterialApp 위젯을 추가해주면, Navigator 클래스의 객체가 자동으로 생성되는데, 이 값을 이용해서 화면을 이동할 수 있다.
/**
* 영상 통화 입장 버튼
*/
class _EntryButton extends StatelessWidget {
const _EntryButton({super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
ElevatedButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => CamScreen(),
),
);
},
child: Text('입장하기'),
),
],
);
}
}
테마를 이용할 때, Theme.of(context)를 사용했던 것처럼
Navigator.of(context)를 실행하면, 위젯 트리의 가장 가까이에 있는 Navigator를 가져온다.
따라서 MaterialApp으로 최상위 위젯을 감싸주면, 앱 어디서든 Navigator를 가져올 수 있다는 의미이다.
1-4. 사전 준비
영상 통화를 구현하기 위해서 카메라, 마이크 권한 설정이 필요하다.
이번에는 "permission_handler" 패키지를 이용해서 앱 내에서 권한 요청을 하는 방법을 알아보자.
1) 아고라 API 준비
2) 네이티브 설정하기
➡️ 안드로이드에서 추가할 네이티브 권한
READ_PHONE_STATE / ACCESS_NETWORK_STATE : 네트워크 상태를 읽는다
INTERNET : 인터넷을 이용해서 영상을 스트리밍해야 하니까
RECORD_AUDIO / MODIFY_AUDIO_SETTINGS / CAMERA : 녹음과 녹화 기능 구현을 위해서
BLUETOOTH_CONNECT : 블루투스를 이용한 (무선 이어폰같은) 녹음 및 녹화 기능을 사용할 수도 있으니까
➡️ iOS에서 추가할 네이티브 권한
iOS는 카메라 권한인 "NSCameraUsageDescription" 과 마이크 권한인 "NSMicrophoneUsageDescription" 만 추가한다.
3) permission_handler 패키지를 사용해 플러터에서 권한 관리
permission_handler 패키지를 이용하면, 안드로이드와 iOS 두 플랫폼 모두에서 쉽게 권한을 관리할 수 있다.
사용법은 매우 단순하다.
패키지의 "Permission" 클래스에 존재하는 권한을 선택한 후 -> request() 함수를 실행하면 권한 요청을 할 수 있다.
값으로는 "PermissionStatus" 에 해당되는 enum 값을 받아올 수 있으며, PermissionStatus.granted 값을 반환받으면 권한이 있다는 것을 의미한다.
권한을 상황에 맞게 하나씩 요청하는 경우도 있지만, 필요한 여러 권한을 한 번에 연속적으로 요청할 때도 있다.
이 때는 요청하고 싶은 권한을 순서대로 "List" 에 넣어서, 한번에 request() 함수를 실행해주면 된다.
그리고 권한 요청에 대한 결과는 "Map 형태" 로 반환받으며, 확인하고 싶은 권한을 key로 넣어서 가져온다.
1-5. 아고라 API 사용법
가장 먼저, 아고라 API를 활성화시키는 방법은 총 3단계이다.
1) 아고라의 RtcEngine 활성화
활성화하면서, 각종 이벤트를 받을 수 있는 콜백 함수도 설정한다.
2) RtcEngine을 통해서, 사용하는 핸드폰의 카메라를 활성화
3) 미리 받아둔 아고라 API 상수값들을 사용해서, testchannel에 참여
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
import 'package:video_call/const/agora.dart';
class _CamScreenState extends State<CamScreen> {
RtcEngine? engine; // 아고라 엔진을 저장하는 변수
int? uid; // 내 ID
int? otherUid; // 상대방 아이디
/**
* 영상 통화 관련 권한 작업을 모두 실행한다
*/
Future<bool> init() async {
final resp = await [Permission.camera, Permission.microphone].request();
final cameraPermission = resp[Permission.camera];
final micPermission = resp[Permission.microphone];
if (cameraPermission != PermissionStatus.granted || micPermission != PermissionStatus.granted) {
throw '카메라 또는 마이크 권한이 없습니다!';
}
if (engine == null) {
// 엔진이 정의되지 않았으면, 새로 생성하기
engine = createAgoraRtcEngine();
// 아고라 엔진 초기화
await engine!.initialize(
// 초기화할 때 사용하는 설정
RtcEngineContext(
// 미리 상수로 저장해둔 APP ID
appId: APP_ID,
// 라이브 동영상 송출에 최적화
channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
),
);
engine!.registerEventHandler(
// 아고라 엔진에서 받을 수 있는 이벤트 값들을 등록
RtcEngineEventHandler(
onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
// 채널 접속에 성공했을 때, 실행
print('채널에 입장했습니다. uid : ${connection.localUid}');
setState(() {
this.uid = connection.localUid;
});
},
onLeaveChannel: (RtcConnection connection, RtcStats stats) {
// 채널의 퇴장했을 때, 실행
print('채널 퇴장');
setState(() {
uid = null;
});
},
onUserJoined: (RtcConnection connection, int remoteUid, int elapsed) {
// 다른 사용자가 접속했을 때, 실행
print('상대가 채널에 입장했습니다! uid : $remoteUid');
setState(() {
otherUid = remoteUid;
});
},
onUserOffline: (RtcConnection connection, int remoteUid, UserOfflineReasonType reason) {
// 다른 사용자가 채널을 나갔을 때, 실행
print('상대가 채널에서 나갔습니다! uid : $otherUid');
setState(() {
otherUid = null;
});
},
),
);
// 엔진으로 영상을 송출하겠다고 설정한다
await engine!.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
await engine!.enableVideo(); // 동영상 기능을 활성화
await engine!.startPreview(); // 카메라를 이용해 동영상을 화면에 실행한다
await engine!.joinChannel( // 채널에 입장하기
token: TEMP_TOKEN,
channelId: CHANNEL_NAME,
// 영상과 관련된 여러가지 설정을 할 수 있으나, 이 프로젝트에서는 안 쓴다
uid: 0,
options: ChannelMediaOptions(),
);
}
return true;
}
➡️ onJoinChannelSuccess
RtcConnection connection : 영상 통화 정보에 관련된 값. connectionl.localUid로는 내 ID를 가져올 수 있다.
int elapsed : joinChannel을 실행한 후, 콜백이 실행되기까지 걸린 시간
➡️ onUserJoined
int remoteUid : 통화 상대방의 고유 ID
int elapsed : 내가 채널을 들어왔을 때부터, 상대방이 들어올 때까지 걸린 시간
➡️ onUserOffline
UserOfflineReasonType reason : 통화방에서 나가게 된 이유 (직접 나감, 네트워크 끊김 등등)
➡️ engine!.joinChannel()
채널에 입장한다.
options 파라미터 : 영상 송출과 관련된 여러 옵션을 상세하게 지정할 수 있다.
uid 파라미터 : 내 고유 ID를 지정한다. 0은 자동으로 고유 ID가 배정된다.
이제 renderMainView() 와 renderSubView() 함수를 작성해서 RtcEngine에서 송수신하는 정보를 화면에 그려주면 된다.
renderSubView() : "내 핸드폰" 이 찍는 화면을 렌더링하는 함수
renderMainView() : "상대방의 핸드폰" 이 찍는 화면을 렌더링하는 함수
/**
* 내 핸드폰이 찍는 화면을 렌더링한다
*/
Widget renderSubView() {
if (uid != null) {
// AgoraVideoView 위젯을 사용하면, 동영상을 화면에 보여주는 위젯을 구현할 수 있다
return AgoraVideoView(
// VideoViewController를 파라미터로 입력하면,
// 해당 컨트롤러가 제공하는 동영상 정보를 AgoraVideoView 위젯을 통해 보여줄 수 있다
controller: VideoViewController(
rtcEngine: engine!,
// VideoCanvas에 0을 입력해서 내 영상을 보여준다
canvas: const VideoCanvas(uid: 0),
),
);
} else {
// 아직 내가 채널에 접속하지 않았다면, 로딩 화면을 보여준다
return CircularProgressIndicator();
}
}
/**
* 통화 상대 핸드폰이 찍는 화면을 렌더링한다
*/
Widget renderMainView() {
if (otherUid != null) {
// VideoViewController.remote 생성자를 이용하면,
// 상대방의 동영상을 AgoraVideoView로 그려낼 수 있다
return AgoraVideoView(
controller: VideoViewController.remote(
rtcEngine: engine!,
// uid에 상대방 ID를 입력한다
canvas: VideoCanvas(uid: otherUid),
connection: const RtcConnection(channelId: CHANNEL_NAME),
),
);
} else {
// 상대가 아직 채널에 들어오지 않았다면, 대기 메시지를 보여준다
return Center(
child: const Text(
'다른 사용자가 입장할 때까지 대기해주세요!',
textAlign: TextAlign.center,
),
);
}
}
2. [14장] 오늘도 출첵 : 구글 지도, Geolocator 패키지, 다이얼로그
2-1. Geolocator 패키지
지리와 관련된 기능을 쉽게 사용할 수 있는 패키지이다. 크게 3가지 기능이 있다.
1. 위치 서비스를 사용할 수 있는 권한이 있는지 확인하고, 권한을 요청한다.
2. 현재 GPS 위치가 바뀔 때마다, 현재 위치값을 받을 수 있는 기능을 사용한다.
3. 현재 위치와 특정 위치(회사 건물) 간의 거리를 계산한다.
1. 위치 서비스 권한 확인하기
이 과정은 2단계로 이루어진다
1-1. 기기의 위치 서비스가 활성화되어 있는지 확인한다.
1-2. 앱에서 위치 서비스 권한을 요청하고 허가 받는다.
2. 현재 위치 지속적으로 반환받기
Geolocator 패키지의 "getPositionStream()" 함수를 사용하면, 현재 위치가 변경될 때마다 그 값을 Position 클래스 형태로 주기적으로 반환받을 수 있다.
3. 두 위치 간의 거리 구하기
Geolocator 패키지의 "distanceBetween()" 함수를 실행하면, 두 위치간의 거리를 "미터" 단위로 반환받을 수 있다.
2-2. 사전 준비
1. 네이티브 코드 설정하기
구글 지도를 사용하려면, 안드로이드와 iOS 모두 네이티브 설정이 필요하다.
먼저 구글 클라우드 플랫폼에서 발급받은 API 키를 안드로이드, iOS 네이티브 파일에 등록해야하고,
안드로이드에서는 최소 버전 설정도 해야한다.
➡️ 안드로이드 설정
➡️ iOS 설정
2-3. 구현하기
1) 화면에 구글 지도를 실행하는 방법
2) 구글 지도에 마커를 생성, 원으로 영역을 표시, 현재 위치를 표시하는 방법
3) 버튼을 누르면, 현재 위치로 지도를 자동으로 이동하는 방법
1. 화면에 구글 지도를 실행하는 방법
class HomeScreen extends StatelessWidget {
static final LatLng companyLatLng = LatLng( // 지도 초기화 위치
37.5233273, // 위도
126.921252, // 경도
);
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: renderAppBar(),
body: GoogleMap( // 지도 위치 지정
initialCameraPosition: CameraPosition(
target: companyLatLng,
zoom: 16, // 확대 정도 (높을수록 크게 보인다)
),
),
);
}
LatLng 클래스 : google_maps_flutter의 클래스이다. 위도와 경도로 특정 위치를 표현할 수 있다.
GooleMap 위젯
➡️ 위치 권한 관리하기
기기 자체의 GPS 사용 권한을 확인하고 -> 앱에서 위치 서비스를 사용할 수 있는지 확인한 후 -> 위치 권한이 없다면, 권한을 재요청하는 로직을 구현한다.
/**
* 위치 권한을 관리하는 함수
*/
Future<String> checkPermission() async {
// 위치 서비스 활성화 여부 확인
final isLocationEnabled = await Geolocator.isLocationServiceEnabled();
if (!isLocationEnabled) { // 기기의 위치 서비스가 비활성화
return '스마트폰의 위치 서비스를 활성화해주세요!';
}
// 앱 내부에서 위치 권한 확인
LocationPermission checkedPermission = await Geolocator.checkPermission();
if (checkedPermission == LocationPermission.denied) { // 위치 권한 거절됨
// 위치 권한 요청
checkedPermission = await Geolocator.requestPermission();
if (checkedPermission == LocationPermission.denied) {
return '위치 권한을 허가해주세요!';
}
// 위치 권한 영구거절됨 (앱 내부에서는 재요청 불가)
if (checkedPermission == LocationPermission.deniedForever) {
return "앱의 위치 권한을 설정에서 허가해주세요!!";
}
// 위 모든 조건이 통과되면, 위치 권한 허가 완료
return '위치 권한이 허가되었습니다...';
}
}
이 함수를 어떻게 사용할지는, 앱마다 권한을 처리하는 방식과 기획에 따라 달려있다.
본 프로젝트에서는 "앱 권한이 없을 경우, 앱을 사용하지 못한다" 는 기획이라고 가정하고 코드를 작성한다.
checkPermission() 함수가 Future<String>을 반환하므로, 앞선 13장 처럼 FutureBuilder를 사용해서 UI 로직을 작성한다.
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: renderAppBar(),
body: FutureBuilder<String>(
future: checkPermission(),
builder: (context, snapshot) {
// 1. 로딩 상태
if (!snapshot.hasData && snapshot.connectionState == ConnectionState.waiting) {
return Center(
child: CircularProgressIndicator(),
);
}
// 2. 권한 허가된 상태
if (snapshot.data == '위치 권한이 허가되었습니다.') {
// 기존 위젯 UI 코드
...
}
// 3. 로딩은 되었으나, 권한이 없는 상태
return Center(
child: Text(
snapshot.data.toString(),
),
);
},
),
2. 화면에 마커 그리기 / 원으로 출근 가능 영역 표시 / 현재 위치 표시
GoogleMap 위젯 참조
3. 출근하기 기능 구현하기
다이얼로그를 사용해서 구현한다. 상황에 따라 서로 다른 다이얼로그를 렌더링해줘야 한다.
Geolocator.getCurrentPosition() : 현재 위치를 반환 받는다.
Geolocator.distanceBetween() : 위치 간의 거리를 미터 단위로 구한다.
ElevatedButton( // [출근하기] 버튼
onPressed: () async {
final curPosition = await Geolocator.getCurrentPosition(); // 현재 위치
final distance = Geolocator.distanceBetween(
curPosition.latitude, // 현재 위치 위도
curPosition.longitude, // 현재 위치 경도
companyLatLng.latitude, // 목표(회사) 위치 위도
companyLatLng.longitude, // 목표 위치 경도
);
bool canCheck = distance < 100; // 100미터 이내에 있으면, 출근 가능
showDialog(
context: context,
builder: (_) {
return AlertDialog(
title: Text('출근하기'),
// 출근 가능 여부에 따라 다른 메시지 제공
content: Text(
canCheck ? '출근 하시겠습니까?' : '출근할 수 없는 위치입니다!',
),
actions: [
TextButton(
// 취소를 누르면 false 반환
onPressed: () {
Navigator.of(context).pop(false);
},
child: Text('취소'),
),
if (canCheck) // 출근 가능한 상태일때만 [출근하기] 버튼 제공
TextButton(
// [출근하기]를 누르면 true 반환
onPressed: () {
Navigator.of(context).pop(true);
},
child: Text('출근하기'),
),
],
);
},
);
},
child: Text('출근하기!'),
),
3. [15장] 포토 스티커 : GestureDetector
3-1. 사전 준비
➡️ 안드로이드 권한 설정하기
➡️ iOS 권한 설정하기
3-2. 구현하기
12장의 동영상 플레이어에서는 ImagePicker 패키지의 pickVideo() 함수를 사용했지만,
이번에는 pickImage() 함수를 사용해서 이미지를 선택한다.
class _HomeScreenState extends State<HomeScreen> {
XFile? image; // 선택한 이미지를 저장할 변수
@override
Widget build(BuildContext context) {
}
void onPickImage() async {
// 갤러리에서 이미지 선택하기
final image = await ImagePicker().pickImage(source: ImageSource.gallery);
setState(() {
this.image = image;
});
}
}
※ [24년 6월 수정사항]
TextButton.styleFrom() 에서 primary 파라미터 대신 -> foregroundColor 파라미터를 사용한다.
StatefulWidget을 사용할 때, 같은 위젯 여러 개를 리스트로 제공하려면, 그 위젯들을 각각 구분하는 key 파라미터를 지정해야 한다. 이 값이 같으면 같은 위젯으로 인식하고, 다르면 다른 위젯으로 인식한다.
child: Stack(
fit: StackFit.expand, // 자식 위젯들의 크기를 최대로 늘려주기
children: [
Image.file(
File(image!.path),
// 이미지가 부모 위젯 크기 최대를 차지
fit: BoxFit.cover,
),
...stickers.map(
(sticker) => Center(
child: EmoticonSticker(
key: ObjectKey(sticker.id),
onTransform: onTransform,
imgPath: sticker.imgPath,
isSelected: selectedId == sticker.id,
),
),
),
],
),
3-3. ImageGallerySaver 패키지
이미지를 갤러리에 저장한다.
/**
* 수정한 이미지 저장 함수
*/
void onSaveImage() async {
RenderRepaintBoundary boundary = imgKey.currentContext!
.findRenderObject() as RenderRepaintBoundary;
ui.Image image = await boundary.toImage(); // 찾은 위젯 바운더리를 이미지로 1차 변환
// ImageGallerySaver 패키지가 이미지를 저장할 수 있도록 이미지 -> 바이트 데이터로 2차 변환
ByteData? byteData = await image.toByteData(format: ui.ImageByteFormat.png);
// 이 바이트 데이터가 8비트 정수형이어야 하기 때문에 바이트 데이터 -> Unit8List 타입으로 3차 변환
Uint8List pngBytes = byteData!.buffer.asUint8List();
await ImageGallerySaver.saveImage(pngBytes, quality: 100);
ScaffoldMessenger.of(context).showSnackBar( // 이미지 저장 후 -> Snackbar 표시
SnackBar(
content: Text('저장되었습니다!!'),
),
);
}
위에 나와있듯, 저장하고 싶은 이미지 "8비트 정수형의 바이트 데이터" 여야 한다. 그에 맞게 변환 작업이 필요하다.
특히 RepaintBoundary.toImage()가 반환하는 Image 타입은
"material.dart의 Image 위젯" 이 아니라 -> "dart:ui의 Image 클래스" 이다!!!
※ 더 나아가서 "path_provider" 패키지와 "dart:io"를 사용하면, 앱이 접근할 수 있는 폴더에 파일을 저장할 수 있다.
https://docs.flutter.dev/cookbook/persistence/reading-writing-files
4. [16장] 코팩튜브 : REST API, JSON, 유튜브 API
4-1. REST API
플러터에서 HTTP 요청을 할 때는, 주로 "http 패키지" 나 "dio 패키지" 를 사용한다.
4-2. 사전 준비
구글 클라우드 콘솔 (console.cloud.google.com)에 접속해서, 원하는 프로젝트에 "Youtube Data API V3" 도 활성화한다.
※ 안드로이드 compileSdkVersion은 34, minSdkVersion은 17로 변경했다.
4-3. 구현하기
Youtube Data API V3는 상당히 많은 기능을 제공한다.
그 중 "Search:list API" 를 사용해서 특정 채널의 동영상들을 최신 순서대로 불러오도록 한다.
위 테이블을 기반으로 HTTP 요청을 보내면, 정상 응답일 경우 다음과 같은 구조의 JSON 데이터를 받는다.
최상단의 키들은 (kind, etag, nextPageToken, pageInfo) -> 응답에 대한 정보를 보여준다.
이 프로젝트에서는 "items" 키에 집중한다. 요청한 동영상의 데이터가 모두 items 키에 리스트로 구현되어 있기 때문이다.
import 'package:cf_tube/const/api.dart';
import 'package:cf_tube/model/video_model.dart';
import 'package:dio/dio.dart';
class YoutubeRepository {
/**
* GET 메서드
*/
static Future<List<VideoModel>> getVideos() async {
final resp = await Dio().get(
YOUTUBE_API_BASE_URL, // 요청을 보낸 URL
queryParameters: { // 요청에 포함할 쿼리 파라미터들
'channelId': CF_CHANNEL_ID,
'maxResults': 50,
'key': API_KEY,
'part': 'snippet',
'order': 'date',
}
);
final listWithData = resp.data['items'].where(
(item) =>
item?['id']?['videoId'] != null && item?['snippet']?['title'] != null,
); // videoId와 title이 null이 아닌 데이터들만 남긴다
return listWithData.map<VideoModel>(
(item) => VideoModel(
id: item['id']['videoId'],
title: item['snippet']['title'],
),
).toList(); // 필터링된 데이터들을 기반으로 VideoModel 리스트를 생성한다
}
}
4-4. ScrollPhysics 클래스 간단 정리
"Scrollable" 한 위젯의 Physics(물리적 특성)을 결정하는 클래스이다. "physics" 파라미터에 삽입하면 된다.
➡️ Scollable 한 위젯들
ListView, PageView, GridView, CustomScrollView, SingleChildScrollView 등등
➡️ 대표적인 ScrollPhysics
1) AlwaysScrollableScrollPhysics : 이게 아마 디폴트
2) NeverScrollableScrollPhysics : 스크롤 잠금
3) BouncingScrollPhysics : 스크린의 Edge 부분에서 바운싱되는 효과가 추가된다.
4) PageScrollPhysics : 구역을 페이지로 나눠서 이동하기 때문에 -> 조금씩 스크롤이 불가능하다고 한다.
이외 ClampingScrollPhysics, FixedExtentScrollPhysics, RangeMaintainingScrollPhysics, ScrollPhysics 등등...
5. [17장] 일정 관리 앱 : Table Calendar
5-1. table_calendar 패키지
달력을 쉽게 구현할 수 있도록 해주는 패키지이다.
특정 날짜 선택하기 / 날짜 기간 선택하기 / 현재 화면에 보여지는 날짜 지정하기 / 일정 입력하기 등 기능을 제공한다.
그리고 매우 유연한 디자인 기능을 노출하고 있어서, 이 패키지의 거의 모든 요소를 직접 최적화할 수 잇다.
1. focusedDay : 현재 화면에 보이는 날짜(= 현재 포커스된 날짜)를 지정할 수 있다.
EX) 11월 23일로 지정했다면, 화면에 11월 달력이 보인다.
2. firstDay : 달력의 가장 첫번째 날짜. 지정된 날짜보다 이전 날짜는 조회 불가능
3. lastDay : 달력의 가장 마지막 날짜. 지정된 날짜보다 이후 날짜는 조회 불가능
4. selectedDayPredicate : 달력에 표시되는 '선택된 날짜'를, 어떤 날짜로 지정할지 로직을 작성한다.
DateTime day 인수는 현재 화면에 보이는 날짜를 하나씩 입력하게 되고, 하나의 true가 반환되면 해당 날짜를 선택하게 된다.
5. onDaySelected : 날짜가 선택됐을 때 실행되는 함수
selectedDay : 선택된 날짜
focusedDay : 날짜가 선택된 순간에 포커스되어 있는 날짜
6. onPageChanged : 달력의 페이지가 변경될 때마다 실행되는 함수
focusedDay : 달력의 페이지가 변경되면서, 새로 포커스된 날짜
7. rangeSelectionMode : 기간 선택 모드
toggledOn : 단일 날짜 대신 날짜 기간을 선택할 수 있다.
toggledOff : 그 반대이다.
8. onRangeSelected : 7번에서 rangeSelectionMode.toggledOn으로 설정됐을 때 실행되는 함수이다.
start : 선택한 기간의 시작 날짜
end : 선택한 기간의 마지막 날짜
focusedDay : 선택이 실행된 순간에 포커스된 날짜
5-2. 사전 준비
pubspec.yaml 파일의 "dependencies" 에 추가된 패키지들은 앱에 함께 패키징되지만,
"dev_dependencies" 에 추가된 패키지들은 개발할 때만 사용된다. 앱을 실행할 때 필요없는 패키지를 추가할 때 사용한다.
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.6
table_calendar: ^3.0.9 # 달력 기능
intl: ^0.18.1 # 다국어화 기능
drift: ^2.13.0 # Drift ORM(Object-Relational Mapping) 기능
sqlite3_flutter_libs: ^0.5.17 # SQLite 데이터베이스
path_provider: ^2.1.1 # 경로 관련 기능
path: ^1.8.3 # 경로 관련 기능
get_it: ^7.6.4 # 프로젝트 전역으로 의존성 주입을 가능하게 한다
dio: ^5.3.3 # 네트워크 요청을 가능하게 한다
provider: ^6.0.5 # 전역(글로벌) 상태 관리를 가능하게 한다
uuid: ^4.1.0 # UUID를 생성 기능
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.0
drift_dev: ^2.13.0 # Drift 코드 생성 기능
build_runner: ^2.4.6 # 플러터에서 코드 생성 기능을 제공한다 (Code Generation을 실행하는 명령어 지원)
5-3. 드리프트(Drift) 패키지
클래스를 이용해서 SQLite 데이터베이스를 구현할 수 있다. 직접 SQL 쿼리를 작성하지 않아도, Dart 언어로 데이터베이스 테이블과 쿼리를 구현하면, 드리프트가 자동으로 쿼리를 생성해준다.
이렇게 자동으로 코드를 작성하는 기능을, 플러터에서는 "코드 생성 (Code generation)" 이라고 부른다.
코드 생성은 데이터베이스 관련 코드가 변경될 때마다 한 번씩만 실행해주면 되기 때문에 앱과 함께 패키징될 필요가 없다.
따라서 dev_dependencies에 드리프트 코드 생성 관련 패키지들을 추가해야 한다.
5-4. 구현하기
※ [24년 6월 변경사항] 482페이지 main_calendar.dart 에러 수정
CalendarStyle에서 defaultDecoration / weekendDecoration / selectedDecoration 파라미터의
borderRadius 속성과 BoxDecoration의 shape 속성이 충돌하여 발생한 에러이다.
BoxDecoration의 shape 속성이 BoxShape.circle일 때는 borderRadius 속성을 사용할 수 없다.
따라서, CalendarStyle에서 defaultDecoration의 shape를 BoxShape.rectangle으로 변경하면 해결된다.
calendarStyle: CalendarStyle(
isTodayHighlighted: false, // 오늘 하이라이트 제거?
defaultDecoration: BoxDecoration( // 기본 날짜 스타일
shape: BoxShape.rectangle,
borderRadius: BorderRadius.circular(6.0),
color: LIGHT_GREY_COLOR,
),
weekendDecoration: BoxDecoration( // 주말 날짜 스타일
shape: BoxShape.rectangle,
borderRadius: BorderRadius.circular(6.0),
color: LIGHT_GREY_COLOR,
),
selectedDecoration: BoxDecoration( // 선택된 날짜 스타일
shape: BoxShape.rectangle,
borderRadius: BorderRadius.circular(6.0),
border: Border.all(
color: PRIMARY_COLOR,
width: 1.0,
),
),
'Dart와 Flutter > 도서 내용 정리' 카테고리의 다른 글
내용 정리 Part. 1 - [코드팩토리의 플러터 프로그래밍] (0) | 2024.06.03 |
---|---|
내용 정리 Part.2 - [풀스택 개발이 쉬워지는 다트 & 플러터] (1) | 2024.03.30 |
내용 정리 Part.1 - [풀스택 개발이 쉬워지는 다트 & 플러터] (0) | 2024.03.24 |