ひらひら*バイク。 Flutterで状態を保存するにはどうすればよいですか?



(*フラッターという言葉の意味の1つは、フラッターすることです)







場合を把握 人生 Flutterアプリケーションでの状態の保存。 OSが再起動を決定した場合はどうなりますか。 ユーザー入力とナビゲーションはどこに行き、どのように対処するか。







免責事項:









フラッターとは



クロスプラットフォームモバイルアプリ開発のフレームワークであるFlutterは、Google I / O 17で発表されました。







FlutterはCおよびC ++で作成され、レンダリング用に独自の2Dエンジンを実装します(WebViewは使用されません)。 Reactに似たもので、開発はDart言語で行われます。 コードは1回作成され、アセンブリ中にプラットフォームごとにネイティブにコンパイルされます。







なぜフラッター?



フラッター(以下フラッターと呼びます)には利点のリストがあります:









賛否両論を避けるために、もちろん、すべてのタスクを完全に解決するのに適したツールを選択することはできません。各ツールはそのコンテキストに適しています。 フラッターは、条件付きで「典型的な」インターフェースと使用シナリオを持つ小規模プロジェクトに使用すると便利だと思います。 この場合、彼はプロセスを大幅に短縮し、ルーチン作業の必要性を排除します(2つのプラットフォームに同じコードを記述し、「iosの場合と同じようにAndroidで」しようとする)。







Flutterを使用して、問題は何ですか?



いくつかの点が心配です。 生産のために何かを急ぐ前に、それらを明確にする必要があります。









順番に始めましょう。 この記事では、ライフサイクルの問題を扱います。 たぶん、私はアンドロイド開発者の典型的な妄想を持っています。 新しい解決策を見つけたときに最初に頭に浮かぶのは、「国家の保存を保証しますか? 活動家が死んだらどうなりますか? そして、もし申請プロセスが死んだら?」







ステートフルフラッターの機能を見てみましょう



さあ、それをチェックしてください! codlabから小さなデモプロジェクトを作成します。 これは、ランダムに生成された単語をリストに表示するアプリケーションです。 スクロールすると、新しい単語が追加されるため、リストは条件付きで無限になります。 これらの単語をお気に入りに追加して、別の画面でお気に入りを表示することもできます。







[アクティビティを保持しない]チェックボックスをオンにして、開始、折りたたみ、展開、...ビンゴ! まあ、それはまったく反対ですが、私の悲観主義者は「あなたに警告しました」と言い、彼の手をこすります。







何が起こっているのか:







  1. 単語リストが再作成されます。
  2. 「お気に入り」はリセットされます。
  3. スクロール位置は保存されません。
  4. ナビゲーションは保存されません(2番目の画面に配置され、展開されると最初に表示されます)




最初の2点(単語のリストが新たに作成され、「お気に入り」がリセットされます)ですべてが整いました-これはビジネスロジックに関するもので、そのようなものがフレームワークから保存されることは誰も期待していません。 ユーザーがアプリケーションを明示的に再起動するまで単語を保存したい場合は、共有設定だけでも、どこかに保存します。







3番目のポイント(スクロール位置は保存されません) -通常のようです。 再起動時にデータがどうなるかはわかりません。ここに保存する必要はないかもしれません。 はい、RecyclerViewはスクロール位置を自動的に保存しません。







興味のために、TextField(EditTextのアナログ)で何が起こるかを確認しました。 そのような状況では、そこからのユーザー入力が消えることが判明しましたが、これは完全に悪いようです。 次に、入力を可能にする他のウィジェット(スライダー、スイッチャー、チェックボックスなど)を調べてみました。

ここでは、原則として、入力構成のロジックがわずかに異なる(Androidとは異なる)ことがわかりました。 基本的に、ウィジェットはユーザー入力を保存しません。 つまり、チェックボックスを突くと、チェックマークは表示されません。 表示するには、このために別のフィールドを作成し、ウィジェットに転送して、クリックイベントのフィールドを変更する必要があります。 フィールドの変更->レンダリングの変更。







さらにチェーンを構築すると、これらのフィールドの値が状態とともに失われると、ユーザー入力がリセットされます。







不快なニュース:これらのフィールドをメモリキャッシュ内のどこかに保存すると、プロセスが再起動すると失われます。 さらに、それらをDartのどこかに(シングルトンであってもクラスのフィールドに)保存すると、アクティビティを再起動しても失われます。

ただし、向きを変更すると、すべてが正常になります。 フラッターの内部では、1つのアクティビティに1つのビューがあり、すべてのレンダリングが行われます。 また、向きを変更しても、このアクティビティは再作成されません。







4番目のポイント(ナビゲーション保存されません) -非常に残念です。 Androidはナビゲーションを保持しますが、フラッターは発生しません。







私はグーグルに行き、それを見つけます:

a)この質問をするのは私だけではありません。人々はgithubでこれについて活発に議論しています。

b)フラッター作成者は、ナビゲーション/ステータスを保存するためにまだ何もしません。 開発者が自分でこれを処理できるようにします。







回答を引用するには:







現在、これを簡単にするために何もしていません。 この問題についてはまだ詳しく調査していません。 今のところ、手動​​で保持したい情報を保存し、アプリが復元されたときに新たに適用することをお勧めします。

トラブルの合計:









状態を維持しようとしています



これを解決する安価な方法があるかどうか見てみましょう。 概念実証が必要です。 こんにちは!







例として、私はコードラボのデモアプリケーションを苦しめ続けます。 タスク:ナビゲーションとユーザー入力を保存します。 この場合、複雑にならないように、ユーザー入力をスクロール位置にします。 さらに、生成された単語と「お気に入り」を共有設定に保存しますが、これはトピックには適用されません。説明しません。







デモを少し変更して対処しやすくしました。ランダムな単語を含むウィジェットを別のファイルに移動し、WordPairを行に置き換えました。

+コードラボで詳しく説明されているすべてのことは、ここでは繰り返しません。 基本原則、アプリケーションの構造、ウィジェットツリー、単語のリストを形成するロジックについては、こちらをご覧ください。







ユーザー入力とナビゲーションをバンドルに保存したい(アクティビティが1つしかないことに注意してください)。 当然、DartとAndroid間の通信が必要になります。 それを修正する方法を理解しましょう( ドキュメンテーション )。 フラッターサイドでは、MethodChannelを作成する必要があります。







save(String key) async { const platform = const MethodChannel('app.channel.shared.data'); platform.invokeMethod("save", /*     */); }
      
      





そして、Android側で、同じ名前のMethodChannelを作成します。







 MethodChannel(getFlutterView(), "app.channel.shared.data") .setMethodCallHandler { call, result -> if (call.method.contentEquals("save")) { //    } }
      
      





データを保存/送信する形式は何ですか? Dart側では、これは何でもよく、MethodChannelを介して任意のタイプを渡すことができます。 しかし、Android側では、バンドルに入れる均一なものを扱いたいと思います。 始めるには、json、jsonの行、バンドルの行でデータ(とにかく)を試します。







入力を保存



まず、ユーザー入力を処理しましょう。 状態に関するデータを保存する抽象クラスを作成します。







 abstract class Restorable { save(String key); Future<Restorable> restore(String key); }
      
      





特定のRestorableを特定のウィジェットにマップするには、キー引数が必要です。 つまり、ウィジェットを作成するときに、ウィジェットに一意のキーを与える必要があります。

スクロール位置を維持するための実装は次のようになります。







 class RandomWordsInput implements Restorable { double scrollPosition = -1.0; RandomWordsInput(); save(String key) async { String json = JSON.encode(this); const platform = const MethodChannel('app.channel.shared.data'); platform.invokeMethod("saveInput", {"key": key, "value": json}); } Future<RandomWordsInput> restore(String key) async { const platform = const MethodChannel('app.channel.shared.data'); String s = await platform.invokeMethod("readInput", {"key" : key}); if (s != null) { var restoredModel = new RandomWordsInput.fromJson(JSON.decode(s)); scrollPosition = restoredModel.scrollPosition; } else { _empty(); } return this; } _empty() { scrollPosition = 0.0; } }
      
      





手作業でシリアル化を記述しないために、json_annotationライブラリーを使用します。 使用方法はフラッターサイトで説明されています







アクティビティのAndroid側で、データを保存するためのフィールドを作成します。







 var savedFromFlutter: MutableMap<String, String> = mutableMapOf()
      
      





onCreateでメソッドを転送します:







 MethodChannel(getFlutterView(), "app.channel.shared.data").setMethodCallHandler { call, result -> if (call.method.contentEquals("save")) { savedModels.put(call.argument<String>("key"), call.argument<String>("value")) } else if (call.method.contentEquals("read")) { result.success(savedModels.get(call.argument<String>("key"))) } }
      
      





そして、保存/復元を行います:







 override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putParcelable("savedFromFlutter", toBundle(savedModels)); } override fun onRestoreInstanceState(savedInstanceState: Bundle) { super.onRestoreInstanceState(savedInstanceState) savedModels = fromBundle(savedInstanceState.getParcelable<Bundle>("savedFromFlutter")) }
      
      





ここで、Restoreableを使用して保存および復元するようウィジェットに教える必要があります。 この例では、RandomWordsウィジェットがあります。







 class RandomWords extends StatefulWidget { @override createState() => new RandomWordsState(); }
      
      





そして彼の状態は次のようになります。







 class RandomWordsState extends State<RandomWords> { final _suggestions = <WordPair>[]; Widget _buildSuggestions() { return new ListView.builder( padding: const EdgeInsets.all(16.0), itemBuilder: (context, i) { if (i.isOdd) return new Divider(); final index = i ~/ 2; // If you've reached the end of the available word pairings... if (index >= _suggestions.length) { // ...then generate 10 more and add them to the suggestions list. _suggestions.addAll(generateWordPairs().take(10)); } return _buildRow(_suggestions[index]); } ); } }
      
      





ウィジェットを作成するとき、キーを渡します:







 class RandomWords extends StatefulWidget { final String stateKey; RandomWords(this.stateKey); @override createState() => new RandomWordsState(); }
      
      





RandomWordsStateでは、州の下にフィールドを作成します。







 class RandomWordsState extends State<RandomWords> { RandomWordsInput input = new RandomWordsInput(); RandomWordsState() { _init(); } // … }
      
      





スクロール位置を制御するには、ScrollControllerが必要です。







 final ScrollController scrollController = new ScrollController();
      
      





_init()関数は保存された状態を読み取り、スクロールを次の位置に移動します。







 _init() async { RandomWordsInput newInput = await model.read(widget.stateKey); setState(() { input = newInput; scrollController.jumpTo(input.scrollPosition); }); }
      
      





ウィジェットを構築するための機能が次のように変更されました。







 Widget _buildSuggestions() { return new NotificationListener( onNotification: _onNotification, child: new ListView.builder( padding: const EdgeInsets.all(16.0), controller: scrollController, itemBuilder: (context, i) { // … } ),); }
      
      





_onNotification関数は、スクロール位置を更新します。







 _onNotification(Notification n) { input.scrollPosition = scrollController.position.pixels; input.save(widget.modelKey); }
      
      





このウィジェットは、次のキーで作成されます:







 class _MyHomePageState extends State<MyHomePage> { @override Widget build(BuildContext context) { return new Scaffold( appBar: new AppBar( title: new Text(widget.title), ), body: new RandomWords("list"), ); } }
      
      





これで、アクティビティの再開間でスクロールの位置が保存されます。







ナビゲーションを保存する



まず、この例では別の画面への遷移を少し書き直し、名前付きルートを使用します( ドキュメントで説明されています )。

アプリケーションを作成するときに、ルートをリストします(1つしかありません)。







 class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return new MaterialApp( title: 'Flutter Demo', theme: new ThemeData( primarySwatch: Colors.blue, ), home: new MyHomePage(title: 'Startup Name Generator'), routes: <String, WidgetBuilder>{ '/saved': (BuildContext context) => new SavedPage(title: 'Saved Suggestions'), }, ); } }
      
      





遷移履歴を保持するクラスを作成します。







 class Routes { static Queue<String> routes = new Queue<String>(); static var _firstTime = true; }
      
      





Restorableと同様に、保存と復元の方法を作成します。







 static save() async { const platform = const MethodChannel('app.channel.shared.data'); platform.invokeMethod( "saveInput", {"key": "routes", "value": JSON.encode(routes.toList())}); } static restore(BuildContext context) async { if (!_firstTime) { return; } const platform = const MethodChannel('app.channel.shared.data'); String s = await platform.invokeMethod("readInput", {"key": "routes"}); if (s != null) { routes = new Queue<String>(); routes.addAll(JSON.decode(s)); } _firstTime = false; for (String route in routes) { Navigator.of(context).pushNamed(route); } }
      
      





つまり、復元するときは、保存されたすべてのルートを取得し、それらから画面のチェーンを復元するだけです。







お気に入りを使用して画面に移動してルートを保存した場合、および戻って削除した場合は、そのまま残ります。 移行では、すべてが簡単です。これを行う関数を編集します。







 void _pushSaved() async { Routes.routes.addLast('/saved'); await Routes.save(); Navigator.of(context).pushNamed('/saved'); }
      
      





おかえりなさい。 ユーザーが「戻る」をクリックした瞬間をキャッチするには、WillPopScopeの「お気に入り」で画面上のウィジェットをラップする必要があります。 また、「戻る」を押す処理を行う関数(ここでは_onWillPop)を取得します。







 class _SavedPageState extends State<SavedPage> { @override Widget build(BuildContext context) { // … return new Scaffold( appBar: new AppBar( title: new Text('Saved Suggestions'), ), body: new WillPopScope( onWillPop: _onWillPop, child: new ListView(children: divided),), ); } Future<bool> _onWillPop() async { Routes.routes.removeLast(); await Routes.save(); return true; } }
      
      





また、変換履歴を復元する必要もあります。 メイン画面でやってみましょう:







 class _MyHomePageState extends State<MyHomePage> { @override Widget build(BuildContext context) { Routes.restore(context); // … } // … }
      
      





以上です! これで、ナビゲーションとスクロール位置の両方が保存されます。







Flutterを使用する



ナビゲーションをすぐに保存できないことは非常に奇妙だと私には思えます。 ユーザー入力を保存しながら、あなたはまだ議論することができます。 突然、クーデター時のデバイスの保存と、デバイスが破壊されたときのアクティビティの損失に満足していると誰かが思う。 私は幸せではありません。







フラッター開発者がこれで何かを決定するかどうかはまだ明らかではありませんが、githubでは非常に活発な口頭の戦いがあります。







現時点では、状態の保存を自分で行うことはかなり可能です。 もちろん、定型文が多すぎるように見えますが。







とりあえず、私はフラッターをさらに研究して、残りの疑問が飛び去るかどうかを確認したいと思っています。 そして、その適用性について決定します。








All Articles