はじめに
Riverpodで非同期処理を扱う際、画面遷移のタイミングによっては
UnmountedRefException が発生することがあります。
本記事では、この問題の原因と対処方法について解説します。
ユースケース
本記事では、以下のような典型的な画面構成を例に説明します。
- 一覧画面
- 登録画面
実現したい動作は次の通りです。
- 登録画面でデータを登録
- 一覧画面に戻る
- 一覧画面が最新の状態に更新される
問題があるコード
まずは問題が発生するコードを見ていきます。
状態管理はMVVM + Riverpodを想定しています。
(UIコードは省略)
一覧画面のViewModel
/// サンプル一覧画面の状態モデルクラス
@freezed
abstract class SampleListPageState with _$SampleListPageState {
/// コンストラクタ
const factory SampleListPageState({required List<String> sampleList}) =
_SampleListPageState;
}
/// サンプル一覧画面のVM
@riverpod
class SampleListPageViewModel extends _$SampleListPageViewModel {
/// 初期化
@override
Future<SampleListPageState> build() async {
// APIでサンプル一覧を取得
final sampleList = await sampleRepository.listSample();
return SampleListPageState(sampleList: sampleList);
}
}
登録画面のViewModel
/// サンプル登録画面の状態モデルクラス
@freezed
abstract class SampleCreatePageState with _$SampleCreatePageState {
/// コンストラクタ
const factory SampleCreatePageState({
String? inputData,
@Default(false) bool isCreating,
}) = _SampleCreatePageState;
}
/// サンプル登録画面のVM
@riverpod
class SampleCreatePageViewModel extends _$SampleCreatePageViewModel {
/// 初期化
@override
SampleCreatePageState build() {
return SampleCreatePageState();
}
/// 入力値を更新
void updateInputData({required String newInputData}) {
state = state.copyWith(inputData: newInputData);
}
/// サンプル登録
///
/// 戻り値は登録結果(true: 成功 false: 失敗)
Future<bool> create() async {
if (state.isCreating) {
return false;
}
final inputData = state.inputData;
if (inputData == null) {
return false;
}
state = state.copyWith(isCreating: true);
try {
// サンプルデータを登録
await sampleRepository.createSample(inputData: inputData);
// 登録できたらサンプル一覧画面を更新する
ref.invalidate(sampleListPageViewModelProvider);
return true;
} on Exception catch (e) {
print(e);
return false;
} finally {
state = state.copyWith(isCreating: false);
}
}
}
問題の本質(UnmountedRefException)
このコードには次の問題があります。
-
非同期処理中に画面遷移が発生する
-
Providerが破棄される(autoDispose)
-
破棄後にstate更新しようとして例外発生
await sampleRepository.createSample(inputData: inputData);
この処理中に画面遷移が起きると、Providerは破棄されます。
その後の
state = state.copyWith(isCreating: false);
で、破棄済みのstateにアクセスしてしまい
UnmountedRefException が発生します。
以下の部分です。
Future<bool> create() async {
...
state = state.copyWith(isCreating: true);
try {
// ** この処理中に画面遷移を行うとUnmountedRefExceptionが出る
await sampleRepository.createSample(inputData: inputData);
ref.invalidate(sampleListPageViewModelProvider);
return true;
} on Exception catch (e) {
print(e);
return false;
} finally {
state = state.copyWith(isCreating: false);
}
}
自動破棄しないように設定も可能です。
riverpod_generatorを使っている場合は、下記のようのkeepAliveをtrueにすると参照がなくなっても保持されます。
/// サンプル登録画面のVM
@Riverpod(keepAlive: true)
class SampleCreatePageViewModel extends _$SampleCreatePageViewModel {
注意点として、参照が保持されるので、画面遷移して再度同じ画面に遷移しても表示が更新されません。
なので、どのタイミングで更新が必要か考え、更新処理を実装する必要があります。
解決①:ref.mountedでクラッシュを防ぐ
非同期処理後にProviderがまだ有効か確認することで、例外を防げます。
/// サンプル登録
///
/// 戻り値は登録結果(true: 成功 false: 失敗)
Future<bool> create() async {
...
state = state.copyWith(isCreating: true);
try {
// サンプルデータを登録
await sampleRepository.createSample(inputData: inputData);
// 登録できたらサンプル一覧画面を更新する
+ if(ref.mounted) {
ref.invalidate(sampleListPageViewModelProvider);
+ }
return true;
} on Exception catch (e) {
print(e);
return false;
} finally {
+ if(ref.mounted) {
state = state.copyWith(isCreating: false);
+ }
}
}
しかし、新たな問題が発生する
ref.mountedを追加したことで、新たな問題が発生します。
/// サンプル登録
///
/// 戻り値は登録結果(true: 成功 false: 失敗)
Future<bool> create() async {
...
state = state.copyWith(isCreating: true);
try {
await sampleRepository.createSample(inputData: inputData);
// **createSample中に別画面に遷移すると参照が破棄されるので、
// **ref.mountedがfalseになり、一覧画面が更新されない
if(ref.mounted) {
ref.invalidate(sampleListPageViewModelProvider);
}
...
}
解決②:ref.keepAliveでライフサイクルを制御
この問題は ref.keepAlive() で解決できます。
ref.keepAlive()では一時的に破棄されるのを防ぐ役割があります。
/// サンプル登録
///
/// 戻り値は登録結果(true: 成功 false: 失敗)
Future<bool> create() async {
if (state.isCreating) {
return false;
}
final inputData = state.inputData;
if (inputData == null) {
return false;
}
+ final link = ref.keepAlive();
state = state.copyWith(isCreating: true);
try {
// サンプルデータを登録
await sampleRepository.createSample(inputData: inputData);
// 登録できたらサンプル一覧画面を更新する
if(ref.mounted) {
ref.invalidate(sampleListPageViewModelProvider);
}
return true;
} on Exception catch (e) {
print(e);
return false;
} finally {
if(ref.mounted) {
state = state.copyWith(isCreating: false);
}
+ link.close();
}
}
keepAliveのポイント
-
ref.keepAlive() で破棄を防ぐ
-
link.close() で通常のライフサイクルに戻す
-
必要な期間だけ生存させるのが重要
記事全体のポイント
- 非同期処理の後に、refやstateを参照する場合、必ずref.mountedで確認する
- 非同期処理中に破棄されるのを防ぐにはref.keepAlive()を使用する

