【Flutter】RiverPodを使ってフェードイン/フェードアウトの連続アニメーション実装してみた
はじめに
最近、Flutterをはじめたので、勉強がてら作ったアプリを記事にしてみました。
※ Flutterの前は、SwiftでiOSアプリ開発をやってました。
今回、状態管理にRiverpodを採用しております。
実装したもの
使用している主なライブラリ等
コード
一般的にModel層やData層と言われるロジック部分から書いていきます。
表示するコンテンツを管理するためのEnumを作成
まずは表示したいコンテンツ(Text)と紐づくEnumを定義しました。
Widgetに表示する内容と紐づけるために、Colorやテキストもここに定義してます。
enum LanguageType {
swift,
flutter,
kotlin,
}
extension LanguageTypeExtension on LanguageType {
Color get color {
switch (this) {
case LanguageType.swift:
return Colors.deepOrange;
case LanguageType.flutter:
return Colors.lightBlue;
case LanguageType.kotlin:
return Colors.purple;
}
}
String get text {
switch (this) {
case LanguageType.swift:
return 'Swift';
case LanguageType.flutter:
return 'Flutter';
case LanguageType.kotlin:
return 'Kotlin';
}
}
}
このLanguageType
を配列に入れて、コンテンツリストを作成し、先頭のものが画面に表示されるようにしたいです。
StateNotifierProviderを実装
まずはprovider
で管理するNotifier
を作成します。
freezedを採用した理由としては、StateNotifierProviderの解説の中で、
// StateNotifier のステート(状態)はイミュータブル(不変)である必要があります。
// ここは Freezed のようなパッケージを利用してイミュータブルにしても OK です。
とあるので、こちらも勉強がてら使ってみました。
final languageTypeListProvider =
StateNotifierProvider.autoDispose<LanguageTypeListNotifier, LanguageTypeListState>((ref) {
return LanguageTypeListNotifier();
});
abstract class LanguageTypeListState with _$LanguageTypeListState {
const factory LanguageTypeListState({
// ここでコンテンツの配列を保持する
(LanguageType.values) List<LanguageType> languageTypeList,
}) = _LanguageTypeListState;
}
class LanguageTypeListNotifier extends StateNotifier<LanguageTypeListState> {
LanguageTypeListNotifier([super._state = const LanguageTypeListState()]);
// フェードアウトしたタイミングで配列をローテションし、次に表示されるコンテンツを変更する
void rotateLanguageTypeList() {
var localLanguageTypeList = state.languageTypeList.toList();
localLanguageTypeList.add(localLanguageTypeList.removeAt(0));
state = state.copyWith(languageTypeList: localLanguageTypeList);
}
}
freezedを使っているので、クラスを作成したら、
dart run build_runner build --delete-conflicting-outputs
を実行して自動生成ファイルを作る必要があります。
var localLanguageTypeList = state.languageTypeList.toList();
localLanguageTypeList.add(localLanguageTypeList.removeAt(0));
state = state.copyWith(languageTypeList: localLanguageTypeList);
Animationの実装
class FadeInOutAnimationController extends AnimationController {
FadeInOutAnimationController({
required super.vsync,
required super.duration,
required void Function() completion,
required this.setState,
}) {
_animation.addStatusListener((status) {
if (status == AnimationStatus.completed) {
// フェードインが完了したとき、フェードアウトを開始する
reverse();
} else if (status == AnimationStatus.dismissed) {
// フェードアウトが完了したとき、completionを実行し、フェードアウトを開始する
completion();
forward();
}
});
}
final void Function(void Function()) setState;
late final Animation<double> _animation = CurvedAnimation(
parent: this,
curve: Curves.easeIn,
);
get animation => _animation;
// 再生・停止ボタンが押された時にアニメーションを制御する
void pauseOrRestartAnimation() {
setState(() {
if (isAnimating) {
stop();
} else {
// 現在のアニメーションの状態によって再生時の挙動を変える
if (status == AnimationStatus.reverse) {
reverse();
} else {
forward();
}
}
});
}
}
Widget側はsetState
するかどうかを意識せずに使いたいので、引数でsetState
を受け取ってAnimation自身で実行するようにしました。
final void Function(void Function()) setState;
Viewの実装
class FadeInOutPage extends ConsumerStatefulWidget {
const FadeInOutPage({super.key, required this.title});
final String title;
ConsumerState<ConsumerStatefulWidget> createState() => _FadeInOutPageState();
}
class _FadeInOutPageState extends ConsumerState<FadeInOutPage> with SingleTickerProviderStateMixin {
// アニメーションが終わった時に、StateNotifierProviderを通してコンテンツの入れ替えを行う
late final _fadeAnimationController = FadeInOutAnimationController(
vsync: this,
duration: const Duration(milliseconds: 900),
completion: ref.read(languageTypeListProvider.notifier).rotateLanguageTypeList,
setState: setState,
);
void initState() {
super.initState();
// 最初はコンテンツが表示されて欲しいので1.0にして停止する
_fadeAnimationController.value = 1.0;
_fadeAnimationController.stop();
}
void dispose() {
_fadeAnimationController.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Center(
child: FadeTransition(
opacity: _fadeAnimationController.animation,
child: languageText(ref.watch(languageTypeListProvider).languageTypeList.first),
),
),
// 再生と停止を行い、アニメーションの状態によってIconを差し替える
floatingActionButton: FloatingActionButton(
shape: const CircleBorder(),
onPressed: _fadeAnimationController.pauseOrRestartAnimation,
child: _fadeAnimationController.isAnimating ? const Icon(Icons.pause) : const Icon(Icons.play_arrow),
),
);
}
// 表示するコンテンツは冒頭でEnumと紐づけたからいい感じに書けた
Widget languageText(LanguageType type) {
return Text(
type.text,
style: TextStyle(fontSize: 48, color: type.color),
key: Key(type.text),
);
}
}
感想
Flutterのアニメーションの書き方は他にも色々あるみたいで、これを作るのにいくつか試して勉強になりました。
コンポーネント思考や状態管理の部分でSwiftUIに似ているなってところもありつつ、アニメーションは全然違ったのでとても新鮮でした。
Discussion