【Flutter】youtube_player_flutterというパッケージのサンプルをriverpod, state_notifier, freezedで書き直す
サンプルlib内にはmain.dartとvideo_list.dartの二つがあり今回はmain.dartの方を書き直す。
状態管理の部分を外部に出しただけです。
ウィジットの部分は↓(HookWidget内にfinal以外の定義をするのはよくない?)
import 'dart:developer';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:mytube/provider/youtube_player_provider.dart';
import 'package:mytube/view/video_list.dart';
import 'package:youtube_player_flutter/youtube_player_flutter.dart';
class MyYoutubePlayerPage extends HookWidget {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey();
final ypsp = useProvider(youtubePlayerStateProvider);
final yps = useProvider(youtubePlayerStateProvider.state);
TextEditingController _idController = TextEditingController();
@override
Widget build(BuildContext context) {
useEffect(() {
Future.microtask(() => ypsp.initState());
return () {
ypsp.dispose();
_idController.dispose();
};
}, []);
return YoutubePlayerBuilder(
onExitFullScreen: () {
// The player forces portraitUp after exiting fullscreen. This overrides the behaviour.
SystemChrome.setPreferredOrientations(DeviceOrientation.values);
},
player: YoutubePlayer(
controller: yps.youtubePlayerController,
showVideoProgressIndicator: true,
progressIndicatorColor: Colors.blueAccent,
topActions: <Widget>[
const SizedBox(width: 8.0),
Expanded(
child: Text(
yps.youtubePlayerController.metadata.title,
style: const TextStyle(
color: Colors.white,
fontSize: 18.0,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
IconButton(
icon: const Icon(
Icons.settings,
color: Colors.white,
size: 25.0,
),
onPressed: () {
log('Settings Tapped!');
},
),
],
onReady: () {
ypsp.onReady();
},
onEnded: (data) {
yps.youtubePlayerController.load(
yps.ids[(yps.ids.indexOf(data.videoId) + 1) % yps.ids.length]);
_showSnackBar('Next Video Started!');
},
),
builder: (context, player) => Scaffold(
key: _scaffoldKey,
appBar: AppBar(
leading: Padding(
padding: const EdgeInsets.only(left: 12.0),
child: Image.asset(
'images/ypf.png',
fit: BoxFit.fitWidth,
),
),
title: const Text(
'Youtube Player Flutter',
style: TextStyle(color: Colors.white),
),
actions: [
IconButton(
icon: const Icon(Icons.video_library),
onPressed: () => Navigator.push(
context,
CupertinoPageRoute(
builder: (context) => VideoList(),
),
),
),
],
),
body: ListView(
children: [
player,
Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_space,
_text('Title', yps.videoMetaData.title),
_space,
_text('Channel', yps.videoMetaData.author),
_space,
_text('Video Id', yps.videoMetaData.videoId),
_space,
Row(
children: [
_text(
'Playback Quality',
yps.youtubePlayerController.value.playbackQuality,
),
const Spacer(),
_text(
'Playback Rate',
'${yps.youtubePlayerController.value.playbackRate}x ',
),
],
),
_space,
TextField(
enabled: yps.isPlayerReady,
controller: _idController,
decoration: InputDecoration(
border: InputBorder.none,
hintText: 'Enter youtube \<video id\> or \<link\>',
fillColor: Colors.blueAccent.withAlpha(20),
filled: true,
hintStyle: const TextStyle(
fontWeight: FontWeight.w300,
color: Colors.blueAccent,
),
suffixIcon: IconButton(
icon: const Icon(Icons.clear),
onPressed: () => _idController.clear(),
),
),
),
_space,
Row(
children: [
_loadCueButton('LOAD', context),
const SizedBox(width: 10.0),
_loadCueButton('CUE', context),
],
),
_space,
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
icon: const Icon(Icons.skip_previous),
onPressed: yps.isPlayerReady
? () => yps.youtubePlayerController.load(yps.ids[
(yps.ids.indexOf(yps.youtubePlayerController
.metadata.videoId) -
1) %
yps.ids.length])
: null,
),
IconButton(
icon: Icon(
yps.youtubePlayerController.value.isPlaying
? Icons.pause
: Icons.play_arrow,
),
onPressed: yps.isPlayerReady
? () {
yps.youtubePlayerController.value.isPlaying
? yps.youtubePlayerController.pause()
: yps.youtubePlayerController.play();
// setState(() {});
}
: null,
),
IconButton(
icon: Icon(
yps.muted ? Icons.volume_off : Icons.volume_up),
onPressed: yps.isPlayerReady
? () {
yps.muted
? yps.youtubePlayerController.unMute()
: yps.youtubePlayerController.mute();
ypsp.changeMuted();
}
: null,
),
FullScreenButton(
controller: yps.youtubePlayerController,
color: Colors.blueAccent,
),
IconButton(
icon: const Icon(Icons.skip_next),
onPressed: yps.isPlayerReady
? () => yps.youtubePlayerController.load(yps.ids[
(yps.ids.indexOf(yps.youtubePlayerController
.metadata.videoId) +
1) %
yps.ids.length])
: null,
),
],
),
_space,
Row(
children: <Widget>[
const Text(
"Volume",
style: TextStyle(fontWeight: FontWeight.w300),
),
Expanded(
child: Slider(
inactiveColor: Colors.transparent,
value: yps.volume,
min: 0.0,
max: 100.0,
divisions: 10,
label: '${(yps.volume).round()}',
onChanged: yps.isPlayerReady
? (value) {
ypsp.setVolume(value);
yps.youtubePlayerController
.setVolume(yps.volume.round());
}
: null,
),
),
],
),
_space,
AnimatedContainer(
duration: const Duration(milliseconds: 800),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20.0),
color: _getStateColor(yps.playerState),
),
padding: const EdgeInsets.all(8.0),
child: Text(
yps.playerState.toString(),
style: const TextStyle(
fontWeight: FontWeight.w300,
color: Colors.white,
),
textAlign: TextAlign.center,
),
),
],
),
),
],
),
),
);
}
Widget _text(String title, String value) {
return RichText(
text: TextSpan(
text: '$title : ',
style: const TextStyle(
color: Colors.blueAccent,
fontWeight: FontWeight.bold,
),
children: [
TextSpan(
text: value ?? '',
style: const TextStyle(
color: Colors.blueAccent,
fontWeight: FontWeight.w300,
),
),
],
),
);
}
Color _getStateColor(PlayerState state) {
switch (state) {
case PlayerState.unknown:
return Colors.grey[700];
case PlayerState.unStarted:
return Colors.pink;
case PlayerState.ended:
return Colors.red;
case PlayerState.playing:
return Colors.blueAccent;
case PlayerState.paused:
return Colors.orange;
case PlayerState.buffering:
return Colors.yellow;
case PlayerState.cued:
return Colors.blue[900];
default:
return Colors.blue;
}
}
Widget get _space => const SizedBox(height: 10);
Widget _loadCueButton(String action, BuildContext context) {
return Expanded(
child: MaterialButton(
color: Colors.blueAccent,
onPressed: yps.isPlayerReady
? () {
if (_idController.text.isNotEmpty) {
var id = YoutubePlayer.convertUrlToId(
_idController.text,
);
if (action == 'LOAD') yps.youtubePlayerController.load(id);
if (action == 'CUE') yps.youtubePlayerController.cue(id);
FocusScope.of(context).requestFocus(FocusNode());
} else {
_showSnackBar('Source can\'t be empty!');
}
}
: null,
disabledColor: Colors.grey,
disabledTextColor: Colors.black,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 14.0),
child: Text(
action,
style: const TextStyle(
fontSize: 18.0,
color: Colors.white,
fontWeight: FontWeight.w300,
),
textAlign: TextAlign.center,
),
),
),
);
}
void _showSnackBar(String message) {
_scaffoldKey.currentState.showSnackBar(
SnackBar(
content: Text(
message,
textAlign: TextAlign.center,
style: const TextStyle(
fontWeight: FontWeight.w300,
fontSize: 16.0,
),
),
backgroundColor: Colors.blueAccent,
behavior: SnackBarBehavior.floating,
elevation: 1.0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(50.0),
),
),
);
}
}
stateは
import 'package:flutter/foundation.dart'; // *.freezed.dartで必要なのでimportしておく
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:youtube_player_flutter/youtube_player_flutter.dart';
part 'youtube_player.freezed.dart';
@freezed
abstract class YoutubePlayerState with _$YoutubePlayerState {
const factory YoutubePlayerState({
PlayerState playerState,
YoutubeMetaData videoMetaData,
YoutubePlayerController youtubePlayerController,
@Default([]) List<String> ids,
@Default(100) double volume,
@Default(false) bool muted,
@Default(false) bool isPlayerReady,
}) = _YoutubePlayerState;
}
providerは
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/all.dart';
import 'package:kikutube/entity/youtube_player.dart';
import 'package:state_notifier/state_notifier.dart';
import 'package:youtube_player_flutter/youtube_player_flutter.dart';
final youtubePlayerStateProvider =
StateNotifierProvider((_) => YoutubePlayerStateProvider());
class YoutubePlayerStateProvider extends StateNotifier<YoutubePlayerState> {
YoutubePlayerStateProvider() : super(const YoutubePlayerState());
void initState() {
state = state.copyWith(ids: [
'nPt8bK2gbaU',
'gQDByCdjUXw',
'iLnmTe5Q2Qw',
'_WoCV4c6XOE',
'KmzdUe0RSJo',
'6jZDSSZZxjQ',
'p2lYr3vM_1w',
'7QUtEmBT_-w',
'34_PXCzGw1M',
]);
state = state.copyWith(
youtubePlayerController: YoutubePlayerController(
initialVideoId: state.ids.first,
flags: const YoutubePlayerFlags(
mute: false,
autoPlay: true,
disableDragSeek: false,
loop: false,
isLive: false,
forceHD: false,
enableCaption: true,
),
)..addListener(listener));
state = state.copyWith(videoMetaData: const YoutubeMetaData());
state = state.copyWith(playerState: PlayerState.unknown);
}
void listener() {
if (state.isPlayerReady &&
mounted &&
!state.youtubePlayerController.value.isFullScreen) {
state = state.copyWith(
playerState: state.youtubePlayerController.value.playerState);
state =
state.copyWith(videoMetaData: state.youtubePlayerController.metadata);
}
}
void deactivate() {
// Pauses video while navigating to next page.
state.youtubePlayerController.pause();
}
void dispose() {
state.youtubePlayerController.dispose();
}
void onReady() {
state = state.copyWith(isPlayerReady: true);
}
void changeMuted() {
state = state.copyWith(muted: !state.muted);
}
void setVolume(value) {
state = state.copyWith(volume: value);
}
}
あ