π Today I Learned 13νμ°¨ - Flutter λμμ νλ μ΄μ΄ π¬
π Flutter λμμ νλ μ΄μ΄
π― νμ΅ λͺ©ν
λμμ νλ μ΄μ΄ μ±μ ꡬνν©λλ€. νΈλν°μ μ μ₯ν΄λ λμμμ μ ννκ³ μ€ννκ³ μ»¨νΈλ‘€ νλ κΈ°λ₯μ ꡬνν©λλ€.
π νμ΅ μμ
β μ¬μ μ§μ
- νλ©΄ νμ
- μκ° λ³ν λ° String ν¨λ©
π οΈ μ¬μ μ€λΉ
- κ°μ λ¨λ§μ λμμ μΆκ°
- μ΄λ―Έμ§ μΆκ°
- pubspec.yaml
- λ€μ΄ν°λΈ μ€μ
- νλ‘μ νΈ μ΄κΈ°ν
π» ꡬννκΈ°
- 첫 νλ©΄
- λ°°κ²½μ κ·ΈλΌλ°μ΄μ
- νμΌ μ ν κΈ°λ₯
- νλ μ΄ νλ©΄
- λμμ μ¬μκΈ°
- λμμ μ°λ
- λμμ 컨νΈλ‘€ λ²νΌ
- 컨νΈλ‘€λ¬ κ°μΆκΈ°
- νμμ€ν¬ν μΆκ°
π μ£Όμ λ΄μ©
β° μκ° λ³ν λ° String ν¨λ©
Durationμ κ²½μ° κΈ°κ°μ νμν΄ μ£Όλ ν΄λμ€μΈλ°, video_player
νλ¬κ·ΈμΈμ μ¬μ©νλ©΄μ νμ¬ μ€νλκ³ μλ μμμ μμΉ, μμμ μ΄ κΈΈμ΄ λ±μ Duration
ν΄λμ€λ‘ λ°ν λ°κ² λ©λλ€.
νμ§λ§ μ€μ μ¬μ©μκ° μ’μνλ λ°μ΄ν° νμ
μ΄ μλκΈ° λλ¬Έμ, 보기 μ’μ String κ°μΌλ‘ λ°κΏμ£Όλ©΄ μ’μ΅λλ€.
Duration duration = Duration(seconds: 192);
print(duration); // 0:03:12.000000
print(
'${duration.inMinutes.toString().padLeft(2, '0')}:${(duration.inSeconds % 60).toString().padLeft(2, '0')}',
); // 03:12
- inMinutes κ²ν°λ λΆ λ¨μ μκ°μ κ°μ Έμ΅λλ€.
- inSeconds κ²ν°λ μ΄ λ¨μ μκ°μ κ°μ Έμ€λ μ΄ λ¨μ μκ°μ λΆμ λͺ«μΌλ‘ λμ΄κ° κ°μ μ μΈν λλ¨Έμ§ κ°λ§ 보μ¬μ£Όλ©΄ λ©λλ€.
String
ν΄λμ€μλpadLeft
μpadRight
ν¨μκ° μ‘΄μ¬νλλ° pad ν¨μλ€μ String κΈΈμ΄λ₯Ό λ§μΆ°μ£Όλ μν μ ν©λλ€.- μ΅μ κΈΈμ΄λ₯Ό μ λ ₯νκ³ κΈΈμ΄κ° λΆμ‘±ν κ²½μ° λλ²μ§Έ λ§€κ°λ³μμ λ€μ΄κ° κ°μ μ±μ λ£μ΅λλ€.
π‘ ν:
padLeft(2, '0')
λ₯Ό μ¬μ©νλ©΄ ν μ리 μ«μλ₯Ό λ μλ¦¬λ‘ λ§μΆ° β03β, β12βμ²λΌ νμν μ μμ΅λλ€.
π¨ λ°°κ²½μ κ·ΈλΌλ°μ΄μ ꡬννκΈ°
BoxDecoration ν΄λμ€λ₯Ό μ¬μ©νλ©΄ Container
μμ ―μ λ°°κ²½μ, ν
λ리, λͺ¨μ리 λ₯κ·Ό μ λ λ± μ λ°μ μΈ λμμΈμ λ³κ²½ν μ μμ΅λλ€.
π¬ λμμ μ¬μκΈ° ꡬννκΈ°
- AspectRatioλ child λ§€κ°λ³μμ μ λ ₯λλ μμ ―μ λΉμ¨μ μ ν μ μλ μμ ―μ λλ€.
aspectRatio
λ§€κ°λ³μμ μνλ λΉμ¨μ μ λ ₯ν μ μμ΅λλ€.- λΉμ¨μ λλΉ/λμ΄λ‘ μ
λ ₯νλ©΄ λλ©° 16:9 λΉμ¨μ μ
λ ₯νκ³ μΆμ λλ
16/9
λ₯Ό μ λ ₯νλ©΄ λ©λλ€. VideoPlayerController
λ₯Ό μ μΈνλ©΄ μ λ ₯λ λμμμ λΉμ¨μvalue.aspectRatio
κ²ν°λ‘ λ°μμ¬ μ μμΌλ μ§μ λ£μ΄μ€¬μ΅λλ€.
VideoPlayerControllerμ λ€μλ μμ±μ
μμ±μ μ΄λ¦ | μ€λͺ |
---|---|
VideoPlayerController.asset |
μ± λ΄λΆ assetμμ λμμ λ‘λ |
VideoPlayerController.network |
λ€νΈμν¬ URLμμ λμμ λ‘λ |
VideoPlayerController.file |
λ‘컬 νμΌμμ λμμ λ‘λ |
ποΈ Slider μμ ― λμμκ³Ό μ°λνκΈ°
- Stack μμ ―μ κΈ°λ³Έμ μΌλ‘
children
μμ ―λ€μ μ μ€μμ μμΉμν΅λλ€. - λ§μ½ Stack λ΄λΆμ νΉμ μμΉμ μμ ―μ μμΉμν€κ³ μΆλ€λ©΄ Positioned μμ ―μ μ¬μ©ν΄μ μμΉλ₯Ό μ ν μ μμ΅λλ€.
bottom: 0
β Stackμ κ°μ₯ μλμ μμμ μμΉ- μΌμͺ½κ³Ό μ€λ₯Έμͺ½ λκΉμ§ Slider μμ ―μ΄ λμ΄λκ² νλ €κ³
left
μright
λ§€κ°λ³μ λͺ¨λμ 0 κ°μ μ£Όλ©΄ μΌμͺ½λΆν° 0ν½μ κ·Έλ¦¬κ³ μ€λ₯Έμͺ½λΆν° 0ν½μ κΉμ§ μμΉ μν€λΌλ μλ―Έκ° λλ―λ‘ Slider μμ ―μ κ°λ‘ μ μ²΄λ‘ λ릴 μ μμ΅λλ€. - Slider μμ ―μ
min
κ°μ νμ 0μ λλ€ (0μ΄λΆν° μμμ ν΄μΌνκΈ° λλ¬Έμ). - μ΅λκ°μ λμμμ μ¬μ κΈΈμ΄λ₯Ό μ΄ λ¨μλ‘ λ³ννλ©΄ λ©λλ€.
π‘ μ£Όμ λ©μλ
videoPlayerController.seekTo()
ν¨μλ λμμμ μ¬μ μμΉλ₯Ό νΉμ μμΉλ‘ μ΄λν΄ μ€λλ€.videoPlayerController!.value.position.inSeconds
κ²ν°λ₯Ό μ€ννλ©΄ νμ¬ λμμμ΄ μ€νλλ μμΉ κ°μ λ°μ μ μμ΅λλ€.
π€ Q) Playerμ Stateμ νλμ μμΉκ° μλ‘ λ€λ₯Έ μνμΈλ° μ μ΄λ κ² λλ μ μ μ νλ κ² μ’μκΉ?
νλ¬ν°μμ StatefulWidget
κ³Ό State
κ°μ²΄μ νλλ₯Ό λλμ΄ μ μνλ κ²μ μ±λ₯ μ΅μ νμ λ°μ΄ν° κ΄λ¦¬μ λͺ
νμ± λλ¬Έμ
λλ€. μ΄λ νλ¬ν° νλ μμν¬μ ν΅μ¬μ μΈ λμ λ°©μκ³Ό κΉμ κ΄λ ¨μ΄ μμ΅λλ€.
κ°λ¨ν μμ½νλ©΄, Widgetμ βμ€μ (Configuration)βμ΄κ³ Stateλ βμν(State)β κ·Έ μ체λ₯Ό μ μ₯νκΈ° λλ¬Έμ λλ€.
1οΈβ£ Widget: λΆλ³(Immutable)νλ μ€μ κ° π
CustomVideoPlayer
ν΄λμ€λ StatefulWidget
μ μμν©λλ€. νλ¬ν°μμ Widgetμ UIμ μ€κ³λλ μ€μ κ°μΌλ‘ μ·¨κΈλλ©°, ν λ² μμ±λλ©΄ κ·Έ μμ λ΄μ©μ λ³κ²½λμ§ μλ λΆλ³(immutable) κ°μ²΄μ
λλ€.
final XFile video;
: μ΄ μμ ―μ΄ μ΄λ€ λΉλμ€λ₯Ό μ¬μν΄μΌ νλμ§μ λν μ€μ μ λλ€. λΆλͺ¨ μμ ―μΌλ‘λΆν° μ λ¬λ°μ΅λλ€.final GestureTapCallback onNewVideoPressed;
: λ²νΌμ΄ λλ Έμ λ μ΄λ€ λμμ ν΄μΌ νλμ§μ λν μ€μ μ λλ€.
νλ¬ν°λ νλ©΄μ λ€μ 그릴 λ(rebuild), κΈ°μ‘΄μ μμ ―μ μμ νλ λμ μλ‘μ΄ μμ ― μΈμ€ν΄μ€λ₯Ό μμ±νμ¬ κ΅μ²΄νλ λ°©μμ μ¬μ©ν©λλ€. λ§μ½ videoPlayerController
μ²λΌ κ³μν΄μ λ³νλ μν κ°μ΄ μ΄ βμ€μ β ν΄λμ€μ μλ€λ©΄, νλ©΄μ΄ λ€μ κ·Έλ €μ§ λλ§λ€ 컨νΈλ‘€λ¬κ° μλ‘ μμ±λλ©΄μ μ΄μ μ μ¬μ μν(μ¬μ μμΉ, λ³Όλ₯¨ λ±)λ₯Ό λͺ¨λ μμ΄λ²λ¦¬κ² λ©λλ€. μ΄λ λ§€μ° λΉν¨μ¨μ μ΄κ³ μλμΉ μμ λμμ μ λ°ν©λλ€.
2οΈβ£ State: κ³μν΄μ λ³νκ³ μ μ§λλ μ€μ μν βοΈ
_CustomVideoPlayerState
ν΄λμ€λ State
λ₯Ό μμν©λλ€. μ΄ κ°μ²΄λ μμ ― νΈλ¦¬μμ ν λ² μμ±λλ©΄ μ½κ² μ¬λΌμ§μ§ μκ³ κ³μ μ μ§λ©λλ€. μμ ―(μ€κ³λ)μ΄ μλ‘κ² κ΅μ²΄λλλΌλ, νλ¬ν°λ μ΄ State
κ°μ²΄λ₯Ό μ¬μ¬μ©νμ¬ μνλ₯Ό 보쑴ν©λλ€.
VideoPlayerController? videoPlayerController;
: λΉλμ€μ μ¬μ, μ μ§ λ± μ€μ λΉλμ€λ₯Ό μ μ΄νκ³ νμ¬ μ¬μ μν(μμΉ, λ²νΌλ§ λ±)λ₯Ό λ΄κ³ μλ μ€μ μν κ°μ²΄μ λλ€. μ΄ κ°μ μ¬μ©μμ μΈν°λμ μ λ°λΌ κ³μ λ³ν΄μΌ ν©λλ€.
initState()
μμ 컨νΈλ‘€λ¬λ₯Ό μ΄κΈ°ννκ³ dispose()
μμ ν΄μ νλ λ±, μ΄ State
κ°μ²΄λ μμ λ§μ μλͺ
μ£ΌκΈ°(Lifecycle)λ₯Ό κ°μ§λ©° λ΄λΆ λ°μ΄ν°λ₯Ό κ΄λ¦¬ν©λλ€.
3οΈβ£ κ²°λ‘ : μ λλλ κ²μ΄ μ’μκ°? β
μ΄λ¬ν λΆλ¦¬ ꡬ쑰λ λ€μκ³Ό κ°μ ν΅μ¬μ μΈ μ₯μ μ κ°μ§λλ€.
-
μ±λ₯ μ΅μ ν (Performance): νλ©΄μ΄ μμ£Ό λ€μ κ·Έλ €μ Έλ(
build
λ©μλ νΈμΆ),VideoPlayerController
μ²λΌ λ¬΄κ±°μ΄ κ°μ²΄λ 볡μ‘ν μν κ°λ€μ μλ‘ μμ±λμ§ μκ³ κ·Έλλ‘ μ μ§λ©λλ€. λΆνμν μ΄κΈ°ν λΉμ©μ λ§μ μ± μ±λ₯μ ν₯μμν΅λλ€. -
μν 보쑴 (State Preservation): λΆλͺ¨ μμ ―μ ꡬ쑰 λ³κ²½ λ±μΌλ‘ μΈν΄
CustomVideoPlayer
μμ ―μ΄ μλ‘ μμ±λλλΌλ,_CustomVideoPlayerState
κ°μ²΄λ μ μ§λλ―λ‘ λΉλμ€μ μ¬μ μμΉλ μνκ° μ΄κΈ°νλμ§ μκ³ κ·Έλλ‘ λ³΄μ‘΄λ©λλ€. -
μ½λμ λͺ νμ± (Clarity): μμ ―μ μ€μ (Configuration)κ³Ό μ€μ λ΄λΆ μν(State)κ° λͺ ννκ² λΆλ¦¬λμ΄ μ½λμ μν μ μ΄ν΄νκΈ° μ½κ³ μ μ§λ³΄μκ° μ©μ΄ν΄μ§λλ€.
CustomVideoPlayer
: βμ΄ λΉλμ€ νμΌ(video
)μ κ°μ§κ³ , μ΄λ° λμ(onNewVideoPressed
)μ νλ νλ μ΄μ΄λ₯Ό λ§λ€μ΄ μ€β λΌλ μμ²μ._CustomVideoPlayerState
: μμ²μμ λ΄μ©μ λ°νμΌλ‘ λΉλμ€ μ»¨νΈλ‘€λ¬λ₯Ό λ§λ€κ³ , μ€μ λ‘ μ¬μνκ³ , νμ¬ μ¬μ μκ°μ κΈ°μ΅νλ λ± λͺ¨λ μ€μ μμ κ³Ό μνλ₯Ό κ΄λ¦¬νλ 주체.
π didUpdateWidgetμ μ΄μ©ν λμμ κ°±μ
β οΈ μ£Όμ: μλ‘μ΄ λμμμ μ νν΄λ νλ©΄μλ μλ‘ μ νν μμμ΄ μ€νλμ§ μμ΅λλ€.
- κ·Έ μ΄μ λ λμμμ μμ€λ₯Ό
videoController
λ³μλ₯Ό μΈμ€ν΄μ€ν ν λ μ μΈνλλ° νμ¬ μ½λμμvideoController
λ³μλinitState
ν¨μμμλ§ μ μΈλκΈ° λλ¬Έ- ν΄κ²°μ± :
didUpdateWidget
ν¨μλ₯Ό μ¬μ©ν΄μ μλ‘μ΄ λμμμ΄ μ νλμμ λvideoController
λ₯Ό μλ‘ μμ±νλλ‘ μ½λλ₯Ό μΆκ°
didUpdateWidgetμ μλͺ μ£ΌκΈ°
didUpdateWidget
μ μλͺ μ£ΌκΈ°λ₯Ό μ μκ°ν΄ 보면, μμ ―μ λ§€κ°λ³μμ κ°μ΄ λ³κ²½λ λ νκΈ°λκ³ μλ‘ μμ±λ©λλ€.- κ·Έλμ
didUpdateWidget
μ 첫λ²μ§Έ λ§€κ° λ³μμ μ λ ₯λλoldWidget
μ νκΈ°λλ μμ ―μ μλ―Έν©λλ€.
π‘ covariant ν€μλ:
CustomVideoPlayer
ν΄λμ€μ μμλ κ°λ νκ°ν΄μ€λλ€.
π» μ€μ΅ λ° μμ
π μμ€ μ½λ
import 'package:flutter/material.dart';
import 'package:vid_player/screen/home_screen.dart';
void main() {
runApp(MaterialApp(home: HomeScreen()));
}
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:vid_player/screen/custom_video_player.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
XFile? video;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: video == null ? renderEmpty() : renderVideo(),
);
}
Widget renderEmpty() {
return Container(
width: MediaQuery.of(context).size.width,
decoration: getBoxDecoration(),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_Logo(onTap: onNewVideoPressed),
SizedBox(height: 30.0),
_AppName(),
],
),
);
}
BoxDecoration getBoxDecoration() {
return BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFF2A3A7C), Color(0XFF000118)],
),
);
}
Widget renderVideo() {
return Center(
child: CustomVideoPlayer(
video: video!,
onNewVideoPressed: onNewVideoPressed,
),
);
}
void onNewVideoPressed() async {
final video = await ImagePicker().pickVideo(source: ImageSource.gallery);
if (video != null) {
setState(() {
this.video = video;
});
}
}
}
class _Logo extends StatelessWidget {
final GestureTapCallback onTap;
const _Logo({required this.onTap});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Image.asset('asset/img/logo.png'),
);
}
}
class _AppName extends StatelessWidget {
@override
Widget build(BuildContext context) {
final textStyle = TextStyle(
color: Colors.white,
fontSize: 30.0,
fontWeight: FontWeight.w300,
);
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('VEDIO', style: textStyle),
Text('Player', style: textStyle.copyWith(fontWeight: FontWeight.w700)),
],
);
}
}
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:vid_player/component/custom_icon_button.dart';
import 'package:video_player/video_player.dart';
class CustomVideoPlayer extends StatefulWidget {
final XFile video;
final GestureTapCallback onNewVideoPressed;
const CustomVideoPlayer({
super.key,
required this.video,
required this.onNewVideoPressed,
});
@override
State<CustomVideoPlayer> createState() => _CustomVideoPlayerState();
}
class _CustomVideoPlayerState extends State<CustomVideoPlayer> {
VideoPlayerController? videoPlayerController;
bool showControls = false;
@override
void initState() {
super.initState();
initializeController();
}
@override
void dispose() {
videoPlayerController?.removeListener(videoControllerListener);
super.dispose();
}
@override
void didUpdateWidget(covariant CustomVideoPlayer oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.video.path != widget.video.path) {
initializeController();
}
}
initializeController() async {
final videoController = VideoPlayerController.file(File(widget.video.path));
await videoController.initialize();
videoController.addListener(videoControllerListener);
setState(() {
// κ΅³μ΄ ν λΉμ setState λ΄λΆμμ νλ μ΄μ : μ΄λ κ² ν΄μΌ buildλ₯Ό λ€μ νκ² λκ³ μνλ νλ©΄μ κ·Έλ €μ£ΌκΈ° λλ¬Έμ
videoPlayerController = videoController;
});
}
void videoControllerListener() {
setState(() {});
}
@override
Widget build(BuildContext context) {
// λμμ 컨νΈλ‘€λ¬κ° μ€λΉ μ€μΌ λ
// μ΅μ΄λ‘ buildκ° λλ
if (videoPlayerController == null) {
return Center(child: CircularProgressIndicator());
}
return GestureDetector(
onTap: () {
setState(() {
showControls = !showControls;
});
},
child: AspectRatio(
// λμμ λΉμ¨μ λ°λ₯Έ νλ©΄ λ λλ§
aspectRatio: videoPlayerController!.value.aspectRatio,
child: Stack(
children: [
VideoPlayer(videoPlayerController!),
if (showControls) Container(color: Colors.black.withOpacity(0.5)),
Positioned(
bottom: 0,
right: 0,
left: 0,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0),
child: Row(
children: [
renderTimeTextFromDuration(
videoPlayerController!.value.position,
),
Expanded(
child: Slider(
value: videoPlayerController!.value.position.inSeconds
.toDouble(),
min: 0,
max: videoPlayerController!.value.duration.inSeconds
.toDouble(),
onChanged: (double val) {
videoPlayerController!.seekTo(
Duration(seconds: val.toInt()),
);
},
),
),
renderTimeTextFromDuration(
videoPlayerController!.value.duration,
),
],
),
),
),
if (showControls)
Align(
alignment: Alignment.topRight,
child: CustomIconButton(
onPressed: widget.onNewVideoPressed,
iconData: Icons.photo_camera_back,
),
),
if (showControls)
Align(
alignment: Alignment.center,
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceEvenly, // μμ μμ ―μ κ°κ²©μ λμΌνκ² μ€μ
children: [
CustomIconButton(
onPressed: onReversePressed,
iconData: Icons.rotate_left,
),
CustomIconButton(
onPressed: onPlayPressed,
iconData: videoPlayerController!.value.isPlaying
? Icons.pause
: Icons.play_arrow,
),
CustomIconButton(
onPressed: onForwardPressed,
iconData: Icons.rotate_right,
),
],
),
),
],
),
),
);
}
Widget renderTimeTextFromDuration(Duration duration) {
return Text(
'${duration.inMinutes.toString().padLeft(2, '0')}: ${(duration.inSeconds % 60).toString().padLeft(2, '0')}',
style: TextStyle(color: Colors.white),
);
}
void onReversePressed() {
final currentPosition = videoPlayerController!.value.position;
Duration position = Duration();
if (currentPosition.inSeconds > 3) {
position = currentPosition - Duration(seconds: 3);
}
videoPlayerController!.seekTo(position);
}
void onForwardPressed() {
final maxPosition = videoPlayerController!.value.duration;
final currentPosition = videoPlayerController!.value.position;
Duration position = maxPosition;
if ((maxPosition - Duration(seconds: 3)).inSeconds >
currentPosition.inSeconds) {
position = currentPosition + Duration(seconds: 3);
}
videoPlayerController!.seekTo(position);
}
void onPlayPressed() {
if (videoPlayerController!.value.isPlaying) {
videoPlayerController!.pause();
} else {
videoPlayerController!.play();
}
}
}
import 'package:flutter/material.dart';
class CustomIconButton extends StatelessWidget {
final GestureTapCallback onPressed;
final IconData iconData;
const CustomIconButton({
super.key,
required this.onPressed,
required this.iconData,
});
@override
Widget build(BuildContext context) {
return IconButton(
onPressed: onPressed,
iconSize: 30.0,
color: Colors.white,
icon: Icon(iconData),
);
}
}
π± μ€ν νλ©΄
.png)
.png)
.png)
π λ§λ¬΄λ¦¬
β μ€λ λ°°μ΄ κ²
- Duration ν΄λμ€: λμμ μ¬μ μκ°μ μ¬μ©μ μΉνμ μΈ ννλ‘ λ³ν
- VideoPlayerController: λμμ λ‘λ λ° μ¬μ μ μ΄
- AspectRatio: λμμ λΉμ¨μ λ§μΆ° νλ©΄ λ λλ§
- Slider μμ ―: λμμ μ¬μ μμΉ νμ λ° μ μ΄
- StatefulWidget vs State: μ€μ κ³Ό μνλ₯Ό λΆλ¦¬νλ μ΄μ μ μ₯μ
- didUpdateWidget: μμ ― λ§€κ°λ³μ λ³κ²½ μ μν μ λ°μ΄νΈ
π λ€μ κ³ν
- μμ ν΅ν μ± κ΅¬ν
- WebRTCμ μκ³ λΌ API νμ©
λκΈλ¨κΈ°κΈ°