質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.44%
Flutter

Flutterは、iOSとAndroidのアプリを同じコードで開発するためのフレームワークです。オープンソースで開発言語はDart。双方のプラットフォームにおける高度な実行パフォーマンスと開発効率を提供することを目的としています。

Xcode

Xcodeはソフトウェア開発のための、Appleの統合開発環境です。Mac OSXに付随するかたちで配布されています。

検索

検索は、あるデータの集まりの中から 目的のデータを見つけ出すことです。

Dart

Dartは、Googleによって開発されたJavaScriptの代替となることを目的に作られた、ウェブ向けのプログラミング言語である。

API

APIはApplication Programming Interfaceの略です。APIはプログラムにリクエストされるサービスがどのように動作するかを、デベロッパーが定めたものです。

Q&A

0回答

997閲覧

flutter_hooks使用時、たまに'_element!.dirty': Bad stateというエラーが発生する。

massanmesu

総合スコア36

Flutter

Flutterは、iOSとAndroidのアプリを同じコードで開発するためのフレームワークです。オープンソースで開発言語はDart。双方のプラットフォームにおける高度な実行パフォーマンスと開発効率を提供することを目的としています。

Xcode

Xcodeはソフトウェア開発のための、Appleの統合開発環境です。Mac OSXに付随するかたちで配布されています。

検索

検索は、あるデータの集まりの中から 目的のデータを見つけ出すことです。

Dart

Dartは、Googleによって開発されたJavaScriptの代替となることを目的に作られた、ウェブ向けのプログラミング言語である。

API

APIはApplication Programming Interfaceの略です。APIはプログラムにリクエストされるサービスがどのように動作するかを、デベロッパーが定めたものです。

0グッド

0クリップ

投稿2021/11/04 06:24

編集2021/11/05 23:37

前提・実現したいこと

楽天ブックスAPIから本の情報を取得する処理を書いています。

TextFieldに本のタイトルを入力すると、候補となる本のデータをAPIから取得し、モデル化した後で本の名前を一覧表示するという処理を書いています。
また、候補の一つをタップすると本の画像を表示するようにしています。

flutter_typeaheadというパッケージを使ってTextFieldの値からレスポンスを取得、Widgetにして表示、タップした時の動作を簡潔に実装しました。

イメージ説明

実装したコード(抜粋部のみ)

モデルはfreezed、API取得は普通の関数、表示する本の情報はStateNotifierProviderで定義しています。

  • 検索バー

searchbar

1import ... 2 3// ×ボタンで入力中のテキストを消すためのコントローラー 4final textEditingControllerProvider = Provider((_) => TextEditingController()); 5 6class TitleSearchBar extends HookWidget { 7 const TitleSearchBar({ 8 Key? key, 9 }) : super(key: key); 10 11 // 候補の本をモデル化して返却 12 Future<BookList> _getBooks(String title) async { 13 const url = 14 'https://app.rakuten.co.jp/services/api/BooksBook/Search/20170404'; 15 final Map<String, Object?> queryParameters = { 16 'format': 'json', 17 'title': title, 18 'size': 0, 19 'booksGenreId': '001', 20 'hits': 5, 21 'applicationId': rakutenAPIkey 22 }; 23 final response = await Dio() 24 .get<Map<String, Object?>>(url, queryParameters: queryParameters); 25 final responseModel = ResponseFromRakuten.fromJson(response.data!); 26 final books = BookList( 27 totalCount: responseModel.hits, // ヒットした件数 28 books: responseModel.items // 個別にモデル化 29 .map( 30 (bookData) => Book.fromJson(bookData.item), 31 ) 32 .toList(growable: false), // リストにしてそれをモデル化 33 ); 34 return books; 35 } 36 37 @override 38 Widget build(BuildContext context) { 39 final _controller = useProvider(textEditingControllerProvider); 40 return Neumorphic( 41 child: TypeAheadField( 42 textFieldConfiguration: ..., 43 suggestionsBoxDecoration: ..., 44 45 // 候補を取得 46 suggestionsCallback: (title) async { 47 // 空白の時は空白のTextを返す 48 if (title.isEmpty) { 49 return [Text('')]; 50 } 51 52 // 取得 53 final bookList = await _getBooks(title); 54 55 // データの中身を返す 56 return bookList.books; 57 }, 58 59 // 候補をWidgetにして表示 60 itemBuilder: (_, bookData) { 61 // 空白の場合は空白Textを返す 62 if (bookData is Text) { 63 return Text(''); 64 } 65 66 // 型を指定しListTileとして表示 67 final book = bookData as Book; 68 return Padding( 69 padding: const EdgeInsets.all(8.0), 70 child: ListTile( 71 title: Text(book.title), 72 ), 73 ); 74 }, 75 76 errorBuilder: (_, err) => Text(err.toString()), 77 transitionBuilder: (context, suggestionsBox, _) => suggestionsBox, 78 79 // どれかタップされたら表示する本のプロバイダーshowBookProviderのStateを更新する 80 onSuggestionSelected: (bookData) { 81 if (bookData is Text) { 82 return; 83 } 84 final book = bookData as Book; 85 // 表示する本の情報を更新 86 context 87 .read(showBookProvider.notifier) 88 .changeState(book.title, book.isbn, book.largeImageUrl); 89 }, 90 ), 91 ); 92 } 93} 94
  • 本を表示するWidget
import ... class ShowBookWidget extends HookWidget { const ShowBookWidget({ Key? key, }) : super(key: key); @override Widget build(BuildContext context) { final _textTheme = Theme.of(context).textTheme; final _isLightTheme = useProvider(isLightThemeProvider); // 表示する本のプロバイダー final _book = useProvider(showBookProvider); return Neumorphic( margin: EdgeInsets.fromLTRB(0, 20, 0, 50), child: Padding( padding: ...), child: Column( mainAxisAlignment: MainAxisAlignment.spaceAround, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // 表示(画像のみ) Flexible( flex: 4, child: Image( image: NetworkImage(_book!.largeImageUrl), // 画像を指定 ), ), Flexible( flex: 1, child: ..., ), Flexible( flex: 1, child: ..., ), ], ), ), ); } }
  • 表示する本の情報を管理するプロバイダー
import '../models/freezed_models/book.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; final showBookProvider = StateNotifierProvider<ShowBookNotifier, Book?>((ref) => ShowBookNotifier()); class ShowBookNotifier extends StateNotifier<Book?> { ShowBookNotifier() : super(null); changeState(title, isbn, url) => state = Book(title: title, isbn: isbn, largeImageUrl: url); }

発生している問題・エラーメッセージ

テキストの入力と削除を繰り返しているとたまにエラーが発生し、更新ができなくなります。

[VERBOSE-2:ui_dart_state.cc(209)] Unhandled Exception: 'package:flutter_hooks/src/framework.dart': Failed assertion: line 281 pos 12: '_element!.dirty': Bad state #0 _AssertionError._doThrowNew (dart:core-patch/errors_patch.dart:47:61) #1 _AssertionError._throwNew (dart:core-patch/errors_patch.dart:36:5) #2 HookState.markMayNeedRebuild (package:flutter_hooks/src/framework.dart:281:12) #3 _ProviderHookState._mayHaveChanged (package:hooks_riverpod/src/use_provider.dart:66:5) #4 ProviderElement.listen.<anonymous closure> (package:riverpod/src/framework/base_provider.dart:589:75) #5 _runGuarded (package:riverpod/src/framework/container.dart:5:7) #6 ProviderElement.notifyMayHaveChanged (package:riverpod/src/framework/base_provider.dart:722:9) #7 ProviderElement.markDidChange (package:riverpod/src/framework/base_provider.dart:682:5) #8 ProviderStateBase.exposedValue= (package:riverpod/src/framework/base_provider.dart:904:14) #9 _StateNotifierProviderState._listener (package:riverpod/src/state_notifier_provider.dart:92:5) #10 StateNotifier.state= (package:state_notifier/state_notifier.dart:214:31) #11 ShowBookNotifier.changeState (package:library_search_app/screens/home.dart:26:7) #12 TitleSearchBar.build.<anonymous closure> (package:library_search_app/widgets/search_bar.dart:118:16) #13 _TypeAheadFieldState._initOverlayEntry.<anonymous closure>.<anonymous closure> (package:flutter_typeahead/src/flutter_typeahead.dart:851:38) #14 _SuggestionsListState.createSuggestionsWidget.<anonymous closure>.<anonymous closure> (package:flutter_typeahead/src/flutter_typeahead.dart:1250:41) #15 _InkResponseState._handleTap (package:flutter/src/material/ink_well.dart:989:21) #16 GestureRecognizer.invokeCallback (package:flutter/src/gestures/recognizer.dart:198:24) #17 TapGestureRecognizer.handleTapUp (package:flutter/src/gestures/tap.dart:608:11) #18 BaseTapGestureRecognizer._checkUp (package:flutter/src/gestures/tap.dart:296:5) #19 BaseTapGestureRecognizer.acceptGesture (package:flutter/src/gestures/tap.dart:267:7) #20 GestureArenaManager.sweep (package:flutter/src/gestures/arena.dart:157:27) #21 GestureBinding.handleEvent (package:flutter/src/gestures/binding.dart:443:20) #22 GestureBinding.dispatchEvent (package:flutter/src/gestures/binding.dart:419:22) #23 RendererBinding.dispatchEvent (package:flutter/src/rendering/binding.dart:322:11) #24 GestureBinding._handlePointerEventImmediately (package:flutter/src/gestures/binding.dart:374:7) #25 GestureBinding.handlePointerEvent (package:flutter/src/gestures/binding.dart:338:5) #26 GestureBinding._flushPointerEventQueue (package:flutter/src/gestures/binding.dart:296:7) #27 GestureBinding._handlePointerDataPacket (package:flutter/src/gestures/binding.dart:279:7) #28 _rootRunUnary (dart:async/zone.dart:1444:13) #29 _CustomZone.runUnary (dart:async/zone.dart:1335:19) #30 _CustomZone.runUnaryGuarded (dart:async/zone.dart:1244:7) #31 _invoke1 (dart:ui/hooks.dart:169:10) #32 PlatformDispatcher._dispatchPointerDataPacket (dart:ui/platform_dispatcher.dart:293:7) #33 _dispatchPointerDataPacket (dart:ui/hooks.dart:88:31)

'_element!.dirty': Bad stateが起きた場所を見てみると、package:flutter_hooks/src/framework.dartmarkMayNeedRebuildというメソッドがあり、アサート文が実行されているようです。

/// Mark the associated [HookWidget] as **potentially** needing to rebuild. /// /// As opposed to [setState], the rebuild is optional and can be cancelled right /// before `build` is called, by having [shouldRebuild] return false. void markMayNeedRebuild() { if (_element!._isOptionalRebuild != false) { _element! .._isOptionalRebuild = true .._shouldRebuildQueue.add(_Entry(shouldRebuild)) ..markNeedsBuild(); } assert(_element!.dirty, 'Bad state'); }

試したこと

何度か繰り返しましたが、何がトリガーになっているのかわかりませんでした。
エミュでも実機でも同様に起きるため実行環境は関係なさそうかもしれません。

補足情報(FW/ツールのバージョンなど)

% flutter doctor Doctor summary (to see all details, run flutter doctor -v): [✓] Flutter (Channel master, 2.6.0-12.0.pre.522, on macOS 11.6 20G165 darwin-x64, locale ja-JP) [✓] Android toolchain - develop for Android devices (Android SDK version 31.0.0) [!] Xcode - develop for iOS and macOS (Xcode 12.5.1) ! Flutter recommends a minimum Xcode version of 13.0.0. Download the latest version or update via the Mac App Store. [✓] Chrome - develop for the web [✓] Android Studio (version 2020.3) [✓] VS Code (version 1.47.3) [✓] Connected device (2 available) ! Doctor found issues in 1 category.

容量がなくてXcodeのアップデートをしてませんが、関係あるのでしょうか?

##追記(11/6)
何がトリガーになっているかわかりました。

候補を選択し、表示した後でソースコードをいじってHotReloadを実行、再度候補を選択した時にエラーが発生しています。もう一度HotReloadをすると更新した画像が表示され、エラーも消えます。

これは最初のデータが1回目のリロード実行時にStateに反映されてないことによるエラーなのかと予想しています。

アプリを使う上でソースコードはいじらないので気にしなくて良い気もしますが、原因がはっきりしないためスッキリしません。

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

まだ回答がついていません

会員登録して回答してみよう

アカウントをお持ちの方は

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.44%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問