- 投稿日:2020-08-01T23:23:41+09:00
【Flutterの状態管理】テーマ切替&多言語化 〜provider,BLoC,reduxの3パターンで実現してみる②
お待たせしました。プロジェクトの人員異動でバタバタしていました。
さて、前回ではproviderという実現方式でflutterの状態管理を説明しました。今回はBLoC(Business Logic Component)を利用し、アプリのテーマ(Theme)切替、多言語化を通じてFlutterの状態管理を説明します。二、BLoCでテーマ切替&多言語化
Blocには3つのポイントがある。それは 業務ロジックユニット、状態、イベント。
flutter_bloc: ^6.0.1
1.テーマ切替のBloc
ハンバーガーメニューにあるテーマ色ボタンの押下により、グローバルにテーマ色を切替える。
1.1状態クラス
テーマ関連の状態量を抽象クラスに置きながら、他の状態はこれを継承し状態の細分化を実現する。
theme_state.dartimport 'package:flutter/material.dart'; @immutable abstract class BlocThemeState { final ThemeData themeData;//テーマ final int colorIndex;//色ボタン選択中の状態を記録 BlocThemeState(this.themeData, this.colorIndex); } class InitBlocThemeState extends BlocThemeState { //初期化テーマは緑にする InitBlocThemeState(ThemeData themeData, int colorIndex) : super(ThemeData(primaryColor: Colors.green), 1); } class BlocStateImpl extends BlocThemeState { BlocStateImpl(ThemeData themeData, int colorIndex) : super(themeData, colorIndex); }1.2イベントクラス
イベントをここで定義する。パラメーターの切り替え、状態のリセットなど。
theme_event.dartimport 'package:flutter/material.dart'; @immutable abstract class BlocThemeEvent {} class BlocEventSwitchTheme extends BlocThemeEvent{ final ThemeData themeData; final int colorIndex; BlocEventSwitchTheme(this.themeData,this.colorIndex); } class BlocEventResetTheme extends BlocThemeEvent{}1.3業務ロジッククラス
このクラスはBlocの核と言っても過言ではない。イベントを通じて状態を生成する。
theme_bloc.dartimport 'package:flutter_bloc/flutter_bloc.dart'; import 'package:state_dancer/BLoC/theme_event.dart'; import 'package:state_dancer/BLoC/theme_state.dart'; class ThemeBloc extends Bloc<BlocThemeEvent,BlocThemeState> { //overrideメソッド、状態の初期化 @override ThemeBloc(BlocThemeState initialState) : super(initialState); @override //async* yield 非同期処理関数を生成(あまり使わない) 一々setState()書かなくても済むのはメリット Stream<BlocThemeState> mapEventToState(BlocThemeEvent event) async* { //テーマ切替 if(event is BlocEventSwitchTheme) { yield BlocStateImpl(event.themeData, event.colorIndex); } //テーマリセット if(event is BlocEventResetTheme) { yield InitBlocThemeState(); } } }2.多言語化のBloc
ハンバーガーメニューにある言語ボタンの押下により、グローバルに言語を切替える。
2.1状態クラス
lang_state.dartimport 'package:flutter/material.dart'; @immutable abstract class BlocLocaleState { final Locale locale; BlocLocaleState(this.locale); } class JpBlocLocaleState extends BlocLocaleState { JpBlocLocaleState() : super(Locale('ja', 'JP')); } class InitBlocLocaleState extends JpBlocLocaleState {} class EnBlocLocaleState extends BlocLocaleState { EnBlocLocaleState() : super(Locale('en', 'US')); }2.2イベントクラス
lang_event.dartimport 'package:flutter/material.dart'; @immutable abstract class BlocLocaleEvent {} class BlocEventSwitch2JP extends BlocLocaleEvent{} class BlocEventSwitch2EN extends BlocLocaleEvent{}2.3業務ロジッククラス
lang_bloc.dartimport 'package:flutter_bloc/flutter_bloc.dart'; import 'package:state_dancer/BLoC/lang_event.dart'; import 'package:state_dancer/BLoC/lang_state.dart'; class LocalBloc extends Bloc<BlocLocaleEvent,BlocLocaleState> { //初期化コンストラクター @override LocalBloc(BlocLocaleState initialState) : super(initialState); @override Stream<BlocLocaleState> mapEventToState(BlocLocaleEvent event) async* { if(event is BlocEventSwitch2JP) { yield JpBlocLocaleState(); } if(event is BlocEventSwitch2EN) { yield EnBlocLocaleState(); } } }3.Blocの使用
providerパターンと同様に、管理する必要のある状態をMultiBlocProviderの中に入れる。
main.dartimport 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_localizations/flutter_localizations.dart';//providerパターンのpubspec.yaml配置を参照 import 'package:state_dancer/BLoC/lang_state.dart'; import 'package:state_dancer/BLoC/theme_state.dart'; import 'home_page.dart'; import 'provider/I18nDelegate.dart';//providerパターン紹介時のファイルを使用 import 'BLoC/theme_bloc.dart'; import 'BLoC/lang_bloc.dart'; void main() { runApp(Wrapper(child: MyApp())); } class Wrapper extends StatelessWidget { final Widget child; Wrapper({this.child}); @override Widget build(BuildContext context) { return MultiBlocProvider(providers: [ BlocProvider<ThemeBloc>(create: (context) => ThemeBloc(InitBlocThemeState()),), BlocProvider<LocalBloc>(create: (context) => LocalBloc(InitBlocLocaleState()),), ], child: MyApp()); } } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return BlocBuilder<ThemeBloc, BlocThemeState>( builder: (_, theme) => BlocBuilder<LocalBloc, BlocLocaleState>( builder: (_, local) => MaterialApp( title: "状態管理Demo", localizationsDelegates: [ GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, I18nDelegate.delegate //言語デリゲート 前回providerパターンのファイルを利用 ], locale: local.locale, supportedLocales: [local.locale], debugShowCheckedModeBanner: false, home: HomePage(), ))); } }BlocBuilder<~Bloc, ~State>(builder: (_,~State) を通じて状態ゲット;
BlocProvider.of<~Bloc>(context).add(~Event())は状態変更のトリガーhome_page.dartimport 'package:flutter/material.dart'; import 'package:state_dancer/BLoC/lang_bloc.dart'; import 'BLoC/theme_bloc.dart'; import 'BLoC/theme_state.dart'; import 'provider/i18n.dart';//providerパターン紹介時のファイルを使用 import 'package:flutter_bloc/flutter_bloc.dart'; import 'BLoC/theme_event.dart'; import 'BLoC/lang_event.dart'; class HomePage extends StatefulWidget { @override _HomePageState createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { @override Widget build(BuildContext context) { var screenWidth = MediaQuery.of(context).size.width; var statusBarH = MediaQuery.of(context).padding.top; var naviBarH = kToolbarHeight; //BlocBuilder<ThemeBloc, BlocThemeState>(builder: (_,blocState) を通じて状態ゲット return BlocBuilder<ThemeBloc, BlocThemeState>(builder: (_,blocState) =>Scaffold( appBar: AppBar( backgroundColor: blocState.themeData.primaryColor, title: Text(I18N.of(context).title,style: TextStyle(color: Colors.white),), ), drawer: Drawer( child: BlocBuilder<ThemeBloc, BlocThemeState>(builder: (_,blocState) { return Column( children: <Widget>[ Container( width: screenWidth, height: statusBarH + naviBarH, color: blocState.themeData.primaryColor, ), SizedBox(height: 10.0), Row( children: <Widget>[ Padding(padding: EdgeInsets.all(10)), Wrap(children: <Widget>[ RaisedButton( color: Colors.green, onPressed: () { //イベントトリガー、重要 BlocProvider.of<ThemeBloc>(context).add(BlocEventSwitchTheme(ThemeData(primaryColor: Colors.green), 1)); }, child: Text(I18N.of(context).greenBtn,style: TextStyle(color: Colors.white),), shape: CircleBorder(), ), RaisedButton( color: Colors.red, onPressed: () { BlocProvider.of<ThemeBloc>(context).add(BlocEventSwitchTheme(ThemeData(primaryColor: Colors.red), 2)); }, child: Text(I18N.of(context).redBtn,style: TextStyle(color: Colors.white),), shape: CircleBorder(), ), RaisedButton( color: Colors.blue, onPressed: () { BlocProvider.of<ThemeBloc>(context).add(BlocEventSwitchTheme(ThemeData(primaryColor: Colors.blue), 3)); }, child: Text(I18N.of(context).blueBtn,style: TextStyle(color: Colors.white),), shape: CircleBorder(), ), ],), ], ), Divider(), SizedBox(height: 10,), Row(children: <Widget>[ Padding(padding: EdgeInsets.all(10)), SizedBox(width: 15,), RaisedButton( color: blocState.themeData.primaryColor, onPressed: () { BlocProvider.of<LocalBloc>(context).add(BlocEventSwitch2JP()); }, child: Text("日本語",style: TextStyle(color: Colors.white),), ), SizedBox(width: 15.0,), RaisedButton( color: blocState.themeData.primaryColor, onPressed: () { BlocProvider.of<LocalBloc>(context).add(BlocEventSwitch2EN()); }, child: Text("English",style: TextStyle(color: Colors.white),), ), ],), ], ); }), ), body: ListView( children: <Widget>[ BlocBuilder<ThemeBloc, BlocThemeState>(builder: (_,blocState) { return Center( child: Column( children: <Widget>[ SizedBox(height: 10.0), Container( width: 200, height: 300, color: blocState.themeData.primaryColor, ), SizedBox(height: 10.0), Text(I18N.of(context).analects, style: TextStyle( color: blocState.themeData.primaryColor, fontSize: 18.0, fontWeight: FontWeight.bold), ), SizedBox(height: 30.0), FloatingActionButton(onPressed: (){ }, backgroundColor: blocState.themeData.primaryColor, child: Icon(Icons.check)), ], ), ); }) ], ), ), ); } }まとめ
本文はBLocでテーマや多言語の切替という多状態を説明しました。次回ではreduxパターンをご紹介しますので、引き続きお楽しみください。
- 投稿日:2020-08-01T22:26:44+09:00
adb コマンド - Android Debug Bridge
はじめに
Android 開発に欠かせない adbコマンドについて基本的なことをまとめます。
Android の SDK には ADB(Android Debug Bridge)というツールが用意されており、
Android 開発をしていれば一度は使う機会があります。adb とは
Android Debug Bridge は adb と省略されます。
これはAndroid SDK の platform-tools に含まれるツールです。
このツールを用いると、現在利用可能なデバイス・エミュレータの列挙、シェルコマンドの発行、ファイルの転送などが行えます。
Android 端末を adb コマンドで操作できます。基本
バージョンを表示
adb versionAndroid Debug Bridge version 1.0.41 Version 30.0.3-6597393 Installed as {インストールディレクトリ}/adb.exe
端末の確認
adb devicesList of devices attached emulator-5554 device
アプリのインストール
デバイスにインストール
adb install xxxxx.apk
デバイスに上書きインストール
adb install -r xxxxx.apk複数のデバイスを接続している時は、端末を指定
adb -s {デバイス} install -r xxxxx.apk # 例 adb -s emulator-5554 install -r xxxxx.apk
- 投稿日:2020-08-01T18:18:10+09:00
【Android】ライフサイクル(Lifecycle)アンチパターン 〜2020年版〜 その2
はじめに
以前、Lifecycle のアンチパターンについて最新のベターな実装を紹介しました。
【Android】ライフサイクル(Lifecycle)アンチパターン 〜2020年版〜 その1今回は第2弾となります。
Activity(Fragment)の Lifecycle のアンチパターンについてと最新のベターな実装について紹介します。
第1弾で紹介した実装と組み合わせて使用するとよりよいと思います。アンチパターンの実装例
onResume() で通信処理等をしたいケースがあったとします。
しかし、onResume() は、onCreate(画面生成)時以外に画面回転、バックグラウンド復帰、別の画面から戻ってくる onActivityResult() 後のケースも通知されてしまいます。その場合にありがちな実装は以下です。MainActivity.ktclass MainActivity : AppCompatActivity() { var isAfterOnCreate: Boolean = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // フラグを立てる isAfterOnCreate = true } override fun onResume() { super.onResume() if (isAfterOnCreate) { isAfterOnCreate = false // onCreate() 後の onResume() で一度だけしたい処理 } } }なぜアンチパターンなのか
上記の実装で仕様は満たせます。しかし今回の例は1つの Activity で処理も少なかったので問題になりませんが、機能や画面が多くなると以下の問題が出てきます。
・画面が多い場合も同じようにフラグ管理しないといけない
・onResume() に多くの処理が集まりがちで、複雑度も上がり可読性が悪くなるベターな実装
LifecycleCoroutineScope
を使用して管理します。
※今回の実装例は Kotlin のみ使用可能で Java では使えませんので注意してください(Google公式サイト)LifecycleCoroutineScope
こちらは、名前の通り Coroutine の利用を前提とした API ですが、Coroutine を使わないケースでも十分に使えます。
Coroutine の処理で利用すればより便利です。LifecycleCoroutineScope の実装方法
MainActivity.kt// インポートが必要 import androidx.lifecycle.lifecycleScope class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) lifecycleScope.launchWhenResumed { // onCreate() 後の onResume() で一度だけしたい処理 } } }これだけで OK です!
実際使う場合は lifecycleScope.launchWhenResumed の部分をメソッド化した方が良いです。LifecycleCoroutineScope の通知
上記では launchWhenResumed を使用した例を紹介しましたが、他に数種類通知が用意されています。
仕様に合ったものを使いましょう。・lifecycleScope.launch
・lifecycleScope.launchWhenCreated
・lifecycleScope.launchWhenStarted
・lifecycleScope.launchWhenResumed応用方法やログでの動作確認
ログを確認すると動作が分かりやすいと思います。
また、通知するケースを増やしてみます。MainActivity.ktclass MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Log.d("MainActivity", "_onCreate") lifecycleScope.launchWhenResumed { Log.d("MainActivity", "_launchWhenResumed onCreate") // onCreate() 後の onResume() で一度だけしたい処理 } // ここに別の Activity への遷移する処理を追加 val button: Button = root.findViewById(R.id.button) button.setOnClickListener { startActivityForResult(Intent(this, OtherActivity::class.java), 0) } } override fun onResume() { super.onResume() Log.d("MainActivity", "_onResume") } override fun onStart() { super.onStart() Log.e("MainActivity", "_onStart") } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) Log.e("MainActivity", "_onActivityResult") //outputLog() lifecycleScope.launchWhenStarted { Log.e("MainActivity", "_launchWhenStarted _onActivityResult") } // onActivityResult() 後の onStart() で一度だけしたい処理 } }ログを確認すると以下になると思います。
・画面生成時
_onCreate → onStart → _onResume → _launchWhenResumed onCreate
・バックグラウンド復帰時
_onCreate → onStart → _onResume
・別Activityから戻ってくる場合
_onActivityResult → onStart → _launchWhenStarted _onActivityResult → _onResumeFragment での利用
当然ですが Fragment でも同じように使えます。
MainFragment.ktclass MainFragment : Fragment() { override fun onAttach(context: Context) { super.onAttach(context) lifecycleScope.launchWhenResumed { // 実装したい処理 } } }lifecycleScope.launch を使ってみる
lifecycle.currentState.isAtLeast と組み合わせて使います。
MainFragment.ktclass MainFragment : Fragment() { init { // init内でも使えます lifecycleScope.launch { when { lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED) -> { } lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED) -> { } lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) -> { } lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED) -> { } } } } }最後に
Android も進化していっているので便利な機能を使ってバグが少ないアプリを作りましょう。
- 投稿日:2020-08-01T17:28:30+09:00
新卒プログラマの学習ログ 〜 其の三 〜
エラー処理(CreateProcessエラー)
このエラーでドツボにハマッたので、ここに書き残しておきます。
環境
- Windows 10
- Android Studio 4.0.1
概要
Android Gradle Pluginのアップデートか何かをきっかけに、どのプログラムを実行しても下記内容のエラーが表示されるようになりました。
Android Studioエラー:CreateProcessエラー
私の場合では原因はJDKのクラッシュでした。
JDK Locationで示しているディレクトリ内にjava.exeがなければ、私と同じ原因となっている可能性が高いです。プロジェクトを開いている状態のAndroid Studio上から、
File
→Other Settings
→Default Project Structure...
と開いてパスの先を把握し、PCの実環境で上記の内容を確認してみてください。解決方法
JDKを再インストールすることで解決しました。
Android Studioで現在使用しているものと同じバーションを.zipファイルでダウンロードします。
(https://developer.android.com/studio?hl=ja)
解凍したファイル内のJDKに現在のインストール済みAndroid StudioのJDKのパスを通せば完了です。とりあえず。。。
メモ代わりに書いていますが、これから理解が深まれば書き足していきます。
いつか誰かのお役に立てると嬉しいですね。初学者から一人前まで、、、
ここでは一人前のプログラマの定義を
「おおよそ自力で様々なツールを活用しながら、ある程度思い通りの実装ができること」
とします。また、人生においてプログラミングに向き合う時間として、現時点で2000時間取り組めればある程度の基盤が備わると仮定しています。
(言語やツールに依存しない思考法や情報収集能力)プログラミングは大学で学んで来ましたが、特に根詰めてやってきていないので、ノーカウントとして取り組みます。
現在までの学習時間
およそ360時間
どんどん更新していきます。
- 投稿日:2020-08-01T16:03:18+09:00
RiverpodとFlutter Hooksを使って、カウンターアプリを作ってみよう
はじめに
この記事では、RiverpodとFlutter HooksとStateNotifierを使って、
カウンターアプリを作ってみようと思います。今回作成したコードはこちらのリポジトリに公開しています。
https://github.com/karamage/riverpod_flutterhooks_exampleFlutterの状態管理パターン
みなさん、Flutterの状態管理パターンは、何を使っていますでしょうか?
Flutterの状態管理パターンはいっぱいあります。
大すぎですw
- StatefulWidget
- Redux
- BLoC(Stream/InheritedWidget) + Provider
- ChangeNotifier + Provider
- StateNotifier + freezed + Provider
- StateNotifier + Flutter Hooks + Riverpod 【new】
僕は、これまでは「StateNotifier + freezed + Provider」を主に使っていました。
「StateNotifier + freezed + Provider」の使い方については、以下に記事を書きました。
https://qiita.com/karamage/items/4b1aff984b1af7541b73新たなパターン登場
そんな今日この頃ですが、状態管理パターンにキラボシのごとく輝く新星が現れました。
「RiverpodとFlutter Hooks」です。
最近、Flutter界隈では、RiverpodとFlutter Hooksが話題です。
@mono0926 さんがツイッターで、Riverpodの発言をしているのをよく拝見します。
最も取っ付きやすそうな例として、Riverpod(hooks非使用) + ChangeNotifierProviderのカウンターページ書いてみた( ´・‿・`)https://t.co/PyZlVky4zo pic.twitter.com/PS8gYI8hrt
— mono (@_mono) July 4, 2020ProviderとRiverpodの扱いの感覚の違い、いつか記事書くかもしれないけど、とりあえずライトにツイート( ´・‿・`)
— mono (@_mono) July 3, 2020
(特に2枚目が大事) pic.twitter.com/7ucPUdhdc6そこで、私もいっちょやってみようかなと思いました。
ざざっと、RiverpodとFlutterHooksを使ってカウンターアプリを作ってみましたので、当記事にまとめます。
Riverpodとは
Providerの作者による、Providerの進化版です。
Provider, but different
Riverpod公式ページ
https://riverpod.dev
「Providerってなに?」って方は、以下のページが詳しいです。
FlutterのProviderパッケージを使いこなす
https://itome.team/blog/2019/12/flutter-advent-calendar-day7/もともと使いやすかった「Provider」をさらに強化したのが「Riverpod」なのですね!
Riverpodの良いところ
Flutterに依存しなくなった(Pure Dart)
- ProviderがInheritedWidgetをラップしたものだった。
- RiverpodはInheritedWidgetを使わずゼロから構築した。
- Providerから状態を読み込む際、BuildContextが必要なくなった。
コンパイル時にエラーを検知できる
- Providerでは実行時にしかエラーが検知できなかった。
- よくあったのが、Providerから状態を読み出そうとして、「Providerが見つからない」エラー
同じ型のProviderを複数配信できる
ProviderをPrivateにできる
Widget Treeだけでなく、Model層でも利用できる
グローバル変数としてProviderを宣言できるようになった
使われていないProviderの状態をdisposeできる
Computedで、Providerの状態を使って計算した値を状態として持てる。
他にも、Family、AutoDisposeProviderなどの新機能多数
Hooks的な使い方も可能(ゆえにHooks APIとの相性がよい)
- Hooksなしでも使える
※ただし、Flutter公式はHooksを肯定的には捉えていない。今後の動向に注目。
Riverpodの悪いところ
- まだまだ開発段階。
- Hooks推しがどうなるか不明。(Flutter公式は否定的)
- これから破壊的な変更が入りそう
- プロダクトに組み込むにはまだ早い感じ
- stableされるまで待とう。(将来的には、Flutter標準に取り込まれるのを願う)
Flutter Hooks とは
https://github.com/rrousselGit/flutter_hooks
「React Hooks」のFlutter版みたいなイメージです。
参考:5分でわかるReact Hooks
https://qiita.com/Mitsuzara/items/98d1bc4a83265a764084https://riverpod.dev
Riverpodの公式ページを見る限り、RiverpodとFlutter Hooksはセットで使うのが、作者の推しみたいです。
RiverpodはFlutter Hooksを外しても利用できます。カウンターアプリを作ってみよう
Riverpod と Flutter Hooks と StateNotifierインストール
pubspec.yamlenvironment: sdk: ">=2.7.0 <3.0.0" flutter: ">= 1.17.0" dependencies: flutter: sdk: flutter flutter_hooks: ^0.11.0 hooks_riverpod: ^0.5.1 state_notifier: ^0.5.0pubspec.yamlに上記のように書いて、ターミナルで「flutter pub get」しましょう。
flutter pub getこれでインストールは完了です。
それでは、コードを書いていきましょう。
lib/main.dartimport 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:state_notifier/state_notifier.dart'; final counterProvider = StateNotifierProvider((_) => Counter()); class Counter extends StateNotifier<int> { Counter() : super(0); void increment() => state++; } void main() { runApp( ProviderScope( child: CounterApp(), ), ); } // Note: CounterApp is a HookWidget, from flutter_hooks. class CounterApp extends HookWidget { @override Widget build(BuildContext context) { final state = useProvider(counterProvider.state); final counter = useProvider(counterProvider); return MaterialApp( home: Scaffold( appBar: AppBar(title: Text('CounterApp')), body: Center( child: Text(state.toString()), ), floatingActionButton: FloatingActionButton( onPressed:() => counter.increment(), child: Icon(Icons.add), ), // This trailing comma makes auto-formatting nicer for build methods. ), ); } }めちゃくちゃコードがすっきり書けて感動。
実行して、「+」を押すと数字がカウントアップすると思います。
コードの説明
グローバルにProviderを宣言することができる
final counterProvider = StateNotifierProvider((_) => Counter());ProviderだとWidget Treeのrootのほうで、Providerを作成していたと思います。
Riverpodでは、グローバルに、Widget Treeに縛られることなく、Providerの作成が可能です。Providerにスコープを設定できる
ProviderScope( child: CounterApp(), ),ProviderScopeを使えば、Providerにアクセスできる階層をコントロールすることができます。
Hooksをつかうためには、HookWidgetを継承する
class CounterApp extends HookWidget {将来的にはclassではなく、「React Hooks」と同じように関数で書けるようになるといいですね。
そのためにもFlutte公式がHooksを認めてもらえるといいなと思います。状態を読み込むには、useProviderを使う
final state = useProvider(counterProvider.state);めっちゃ簡単でシンプルですね。
BuildContextが必要なくなったのが良い!まとめ
- Riverpodめっちゃ良い。Providerから正当進化している。使わない理由がない。
- 今後は、Providerではなく、Riverpodを使っていきたい。
- しかしながら、Hooksがどうなるかわからないので、もうしばらくは様子見。
最後までお読みいただき、ありがとうございました。
今回作成したコードはこちらのリポジトリに公開しています。
https://github.com/karamage/riverpod_flutterhooks_example参考リンク
Riverpod
https://riverpod.devFlutter Hooks
https://github.com/rrousselGit/flutter_hooksFlutter Hooks, say goodbye to StatefulWidget and reduce boilerplate code.
https://medium.com/flutter-community/flutter-hooks-say-goodbye-to-statefulwidget-and-reduce-boilerplate-code-8573d4720f9aexample counter
https://github.com/rrousselGit/river_pod/tree/master/examples/counterexample todo
https://github.com/rrousselGit/river_pod/tree/master/examples/todos
- 投稿日:2020-08-01T16:03:18+09:00
RiverpodとFlutter Hooksを使う、はじめの一歩
はじめに
この記事では、RiverpodとFlutter Hooksを使う、はじめの一歩として、
カウンターアプリを作ってみようと思います。今回作成したコードはこちらのリポジトリに公開しています。
https://github.com/karamage/riverpod_flutterhooks_exampleFlutterの状態管理パターン
みなさん、Flutterの状態管理パターンは、何を使っていますでしょうか?
Flutterの状態管理パターンはいっぱいあります。
大すぎですw
- StatefulWidget
- Redux
- BLoC(Stream/InheritedWidget) + Provider
- ChangeNotifier + Provider
- StateNotifier + freezed + Provider
- StateNotifier + Flutter Hooks + Riverpod 【new】
私は、これまでは「StateNotifier + freezed + Provider」を主に使っていました。
「StateNotifier + freezed + Provider」の使い方については、以下に記事を書きました。
https://qiita.com/karamage/items/4b1aff984b1af7541b73新たなパターン登場
そんな今日この頃ですが、状態管理パターンにキラボシのごとく輝く新星が現れました。
「RiverpodとFlutter Hooks」です。
最近、Flutter界隈では、RiverpodとFlutter Hooksが話題です。
@mono0926 さんがツイッターで、Riverpodの発言をしているのをよく拝見します。
最も取っ付きやすそうな例として、Riverpod(hooks非使用) + ChangeNotifierProviderのカウンターページ書いてみた( ´・‿・`)https://t.co/PyZlVky4zo pic.twitter.com/PS8gYI8hrt
— mono (@_mono) July 4, 2020ProviderとRiverpodの扱いの感覚の違い、いつか記事書くかもしれないけど、とりあえずライトにツイート( ´・‿・`)
— mono (@_mono) July 3, 2020
(特に2枚目が大事) pic.twitter.com/7ucPUdhdc6そこで、私もいっちょやってみようかなと思いました。
ざざっと、RiverpodとFlutterHooksを使ってカウンターアプリを作ってみましたので、当記事にまとめます。
Riverpodとは
Providerの作者による、Providerの進化版です。
Provider, but different
Riverpod公式ページ
https://riverpod.dev
「Providerってなに?」って方は、以下のページが詳しいです。
FlutterのProviderパッケージを使いこなす
https://itome.team/blog/2019/12/flutter-advent-calendar-day7/もともと使いやすかった「Provider」をさらに強化したのが「Riverpod」なのですね!
Riverpodの良いところ
Flutterに依存しなくなった(Pure Dart)
- ProviderがInheritedWidgetをラップしたものだった。
- RiverpodはInheritedWidgetを使わずゼロから構築した。
- Providerから状態を読み込む際、BuildContextが必要なくなった。
コンパイル時にエラーを検知できる
- Providerでは実行時にしかエラーが検知できなかった。
- よくあったのが、Providerから状態を読み出そうとして、「Providerが見つからない」エラー
同じ型のProviderを複数配信できる
ProviderをPrivateにできる
ProviderをWidget Tree限定でなく、Model/Domain Logic/Repositoryどこでも利用できる
グローバル変数としてProviderを宣言できるようになった
使われていないProviderの状態をdisposeできる
Computedで、Providerの状態を使って計算した値を状態として持てる。
他にも、Family、AutoDisposeProviderなどの新機能多数
Hooks的な使い方も可能(ゆえにHooks APIとの相性がよい)
- Hooksなしでも使える
※ただし、Flutter公式はHooksを肯定的には捉えていない。今後の動向に注目。
Riverpodの悪いところ
- まだまだ開発段階。
- Hooks推しがどうなるか不明。(Flutter公式は否定的)
- これから破壊的な変更が入りそう
- プロダクトに組み込むにはまだ早い感じ
- stableされるまで待とう。(将来的には、Flutter標準に取り込まれるのを願う)
Flutter Hooks とは
https://github.com/rrousselGit/flutter_hooks
「React Hooks」のFlutter版みたいなイメージです。
参考:5分でわかるReact Hooks
https://qiita.com/Mitsuzara/items/98d1bc4a83265a764084https://riverpod.dev
Riverpodの公式ページを見る限り、RiverpodとFlutter Hooksはセットで使うのが、作者の推しみたいです。
RiverpodはFlutter Hooksを外しても利用できます。カウンターアプリを作ってみよう
Riverpod と Flutter Hooks と StateNotifierインストール
pubspec.yamlenvironment: sdk: ">=2.7.0 <3.0.0" flutter: ">= 1.17.0" dependencies: flutter: sdk: flutter flutter_hooks: ^0.11.0 hooks_riverpod: ^0.5.1 state_notifier: ^0.5.0pubspec.yamlに上記のように書いて、ターミナルで「flutter pub get」しましょう。
flutter pub getこれでインストールは完了です。
それでは、コードを書いていきましょう。
lib/main.dartimport 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:state_notifier/state_notifier.dart'; final counterProvider = StateNotifierProvider((_) => Counter()); class Counter extends StateNotifier<int> { Counter() : super(0); void increment() => state++; } void main() { runApp( ProviderScope( child: CounterApp(), ), ); } // Note: CounterApp is a HookWidget, from flutter_hooks. class CounterApp extends HookWidget { @override Widget build(BuildContext context) { final state = useProvider(counterProvider.state); final counter = useProvider(counterProvider); return MaterialApp( home: Scaffold( appBar: AppBar(title: Text('CounterApp')), body: Center( child: Text(state.toString()), ), floatingActionButton: FloatingActionButton( onPressed:() => counter.increment(), child: Icon(Icons.add), ), // This trailing comma makes auto-formatting nicer for build methods. ), ); } }めちゃくちゃコードがすっきり書けて感動。
実行して、「+」を押すと数字がカウントアップすると思います。
コードの説明
グローバルにProviderを宣言することができる
final counterProvider = StateNotifierProvider((_) => Counter());ProviderだとWidget Treeのrootのほうで、Providerを作成していたと思います。
Riverpodでは、グローバルに、Widget Treeに縛られることなく、Providerの作成が可能です。Providerにスコープを設定できる
ProviderScope( child: CounterApp(), ),ProviderScopeを使えば、Providerにアクセスできる階層をコントロールすることができます。
Hooksをつかうためには、HookWidgetを継承する
class CounterApp extends HookWidget {将来的にはclassではなく、「React Hooks」と同じように関数で書けるようになるといいですね。
そのためにもFlutte公式がHooksを認めてもらえるといいなと思います。状態を読み込むには、useProviderを使う
final state = useProvider(counterProvider.state);めっちゃ簡単でシンプルですね。
BuildContextが必要なくなったのが良い!まとめ
- Riverpodめっちゃ良い。Providerから正当進化している。使わない理由がない。
- 今後は、Providerではなく、Riverpodを使っていきたい。
- しかしながら、Hooksがどうなるかわからないので、もうしばらくは様子見かなぁ。
- Hooksを外して使うのが良いかも。
- まずは個人アプリで採用してみようと思います。
最後までお読みいただき、ありがとうございました。
今回作成したコードはこちらのリポジトリに公開しています。
https://github.com/karamage/riverpod_flutterhooks_example参考リンク
Riverpod
https://riverpod.devFlutter Hooks
https://github.com/rrousselGit/flutter_hooksFlutter Hooks, say goodbye to StatefulWidget and reduce boilerplate code.
https://medium.com/flutter-community/flutter-hooks-say-goodbye-to-statefulwidget-and-reduce-boilerplate-code-8573d4720f9aexample counter
https://github.com/rrousselGit/river_pod/tree/master/examples/counterexample todo
https://github.com/rrousselGit/river_pod/tree/master/examples/todos
- 投稿日:2020-08-01T15:57:04+09:00
ViewPager2で共通のViewModelを使う方法
はじめに
こんにちは、Androidエンジニアのどすこいです。
今回はタイトルにある通り、ViewPager2で共通のViewModelを使う方法を解説したいと思います。
タイトルだけ見て、なんのこっちゃと思うかもしれないのでまずは次の図をご覧くださいこんな感じで親FragmentがViewPager2を用いて3つの子Fragmentを持っている状況があるとします。
その際、全ての子Fragmentで同じViewModelを使いたい時にどうするべきか悩んだので共有します。環境
Android Architecture Componentを使用します。
appレベルのbuild.gradleに次の記述をしてください。build.gradledependencies { implementation "androidx.viewpager2:viewpager2:1.0.0" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0" }今回説明しないこと
ViewPager2の使い方
Android Architecture ComponentのことViewModelの生成方法
まずViewModelの生成方法から解説していきます。
ActivityスコープでのViewModelの生成方法
private val viewModel: HogeViewModel by activityViewModels()これでActivityスコープでのViewModelが生成できます。
Activity内で複数ViewModelを生成しないのであればこれでいいと思います。
しかしActivityの責務が大きければ複数のViewModelを生成することが多いでしょう。
その場合、そのActivityが死なない限りViewModelも破棄されません。そうなるとメモリリークの可能性も大きくなるので、ActivityスコープでのViewModelの生成はお勧めしません。FragmentスコープでのViewModelの生成方法
private val viewModel: HogeViewModel by viewModels()これでFragmentスコープでのViewModelが生成されます。
基本的にViewModelを生成する場合はこの方法がお勧めです。画面遷移をするとFragmentが死ぬのでそれに伴いViewModelも破棄されメモリリークを起こすことはないでしょう。
ただこの方法を今回のViewPager2を使った方法で用いると何が起こるでしょうか?
子Fagment1のスコープでViewModelを生成するとします。
子Fragment1の状態をViewModelに保存します。(入力した値など)
ViewPager2を用いてFragmentを変更した場合、子Fragement2で再びViewModelを生成し子Fragment1で生成したViewModelが破棄されます。
なので子Fragment1に戻った際、先ほどの状態を子Fragment1に反映できません。それを解決すべく、ViewModelStoreとViewModelStoreOwnerという概念を使用していきます。
ViewModelStoreOwner筆者もViewModelStoreOwnerは完全に理解しているわけではないので、間違い等あればご指摘ください。
ViewModelStoreとViewModelStoreOwnerとは
ViewModelStoreはViewModelを保存してくれるクラスです。
ViewModelStoreOwnerはgetViewModelStoreというViewModelStoreを返すメソッドのみが存在しているインターフェイスです。ViewModelStoreはViewModelStoreOwner#getViewModelStore()を呼び出した時にActivityやFragmentに対応して生成されていて、既に対応したViewModelStoreが存在する場合はそれを返却するようになっています。
まずこれらを理解するためにViewModelを生成するときに使ったactivityViewModels()やviewModels()が何をやっているか見ていきましょう。
@MainThread inline fun <reified VM : ViewModel> Fragment.activityViewModels( noinline factoryProducer: (() -> Factory)? = null ) = createViewModelLazy(VM::class, { requireActivity().viewModelStore }, factoryProducer ?: { requireActivity().defaultViewModelProviderFactory }) @MainThread inline fun <reified VM : ViewModel> Fragment.viewModels( noinline ownerProducer: () -> ViewModelStoreOwner = { this }, noinline factoryProducer: (() -> Factory)? = null ) = createViewModelLazy(VM::class, { ownerProducer().viewModelStore }, factoryProducer) @MainThread fun <VM : ViewModel> Fragment.createViewModelLazy( viewModelClass: KClass<VM>, storeProducer: () -> ViewModelStore, factoryProducer: (() -> Factory)? = null ): Lazy<VM> { val factoryPromise = factoryProducer ?: { defaultViewModelProviderFactory } return ViewModelLazy(viewModelClass, storeProducer, factoryPromise) }裏側ではこんなことをしてくれてるんですね。
正直数ヶ月前までは裏側のコードを見てると頭が痛くなってましたが、最近では少し慣れてきてちょっとずつ読むようになりました(笑)まずactivityViewModels()の方はそれを呼び出したFragmentの親ActivityのViewModelStoreをcreateViewModelLazy()に渡してくれています。
viewModels()の方は少し違っていますね。
ViewModelStoreOwnerを渡さない場合は、thisを使ってViewModelsStoreをcreateViewModelLazy()に渡してくれています。このthisというのは呼び出したFragmentのことです。ViewPager2を使った場合、このViewModelStoreOwnerに親Fragmentを渡してあげれば親Fragmentが死なない限り、同じインスタンスのViewModelが使えるということになります!
ViewPager2で共通のViewModelを使う方法
さて本題です!
まずは親Fragmentで共通して使いたいViewModelを生成します。
親Fragmentprivate val viewModel: HogeViewModel by viewModels()そして子Fragmentで親FragmentのViewModelStoreOwnerを用いて同じViewModelを生成してあげます
子Fragmentprivate val viewModel: HogeViewModel by viewModels( { parentFragment!! } )こうしてあげることでそれぞれの子Fragmentで共通したViewModelが生成できますね。
まとめ
本題より前提知識や説明の方が長くなってしまいましたね(笑)
Android Architecture Componentはまだ発表されてそこまで時間が経っていないので、詳しく紹介している記事が少なかったり見つけたとしても英語でよくわからん、みたいなことが多々あって苦労するところはあると思います(少なくとも筆者はめっちゃ苦労してる)
なので今後も詳しく紹介する記事や、初心者向けの記事を書いていけたらなぁなんて思ってるので今後もよろしくお願いします!
参考にした記事
ViewModel, ViewModelProviderについて調べてみた
- 投稿日:2020-08-01T15:04:39+09:00
データバインディングでLiveDataを評価する時はnon-nullでもnullチェックした方がいい
tl;dr
LiveData<Hoge>
であってもLiveData#getValue
の初期の戻り値はnull- 条件式次第ではデータバインディング時にnullチェックを忘れるとUI要素のちらつきの原因になるよ
ボタンがちらつく
通常非表示のボタンを、ある条件を満たす時だけ表示させるよう振る舞わせようとしていた時のことです。
データバインディングとLiveDataを使ってButtonのvisibilityを制御しようと考えました。
visibilityなら普通Booleanのデータで制御するのが普通ですが、今回はString型のデータで制御する必要がありました。
1文字以上の文字列なら表示、空文字なら非表示といった具合です。Entity.ktdata class Entity(val value: String)Repository.ktclass Repository { fun getData(): LiveData<Entity> { return liveData(GlobalScope.coroutineContext) { // 時間のかかる処理 val data = ... emit(data) } } }MainViewModel.ktclass MainViewModel(repository: Repository) : ViewModel() { ... val data: LiveData<Entity> = repository.getData() ... }fragment_main.xml<data> <import type="android.view.View" /> <variable name="viewModel" type="qchr.nonnulllivedata.ui.main.MainViewModel" /> </data> ... <Button ... android:visibility="@{!viewModel.data.value.empty? View.VISIBLE: View.GONE}" ... />MainFragment.kt... private val viewModel: MainViewModel by viewModels { MainViewModel.Factory(Repository()) } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { return MainFragmentBinding.inflate(inflater, container, false).apply { lifecycleOwner = viewLifecycleOwner viewModel = this@MainFragment.viewModel }.root } ...そして、実際に空文字のデータを与えて画面を表示させてみました。
ボタンが非表示なることを期待していたところ……実際にはボタンがちらつく。
最終的にボタンは非表示になっているのですが、画面が表示された一瞬だけボタンが表示されてしまう。ちょうど、冒頭に貼ったgifの上側にあるNGボタンみたいな挙動をしてしまいます(一瞬だとgif化できなかったので表示されている時間は誇張しています)。
なぜちらついた?
なぜちらついたのでしょうか?
原因は
fragment_main.xml
における条件式内でのviewModel.data
(MainViewModel#data
)変数の取り扱いにありました。1つずつ見ていきましょう。
MainViewModel#data
はRepository#getData
から取得していますが、その中ではREST APIを叩き返ってきたデータを整形し、……といくらか時間のかかる処理を行っていました。Repository.kt(再掲)class Repository { fun getData(): LiveData<Entity> { return liveData(GlobalScope.coroutineContext) { // 時間のかかる処理 val data = ... emit(data) } } }
emit(data)
が実行されるまで、MainViewModel#data
は初期状態にあるため、data
のgetValue
はnullを返します。それを念頭において
fragment_main.xml
をもう一度見てみましょう。fragment_main.xml(再掲)... <Button ... android:visibility="@{!viewModel.data.value.empty? View.VISIBLE: View.GONE}" ... />
viewModel.data
のgetValue
がnullを返すとき、!viewModel.data.value.empty
はどう評価されるのでしょうか?
答えは評価が中断されるです。
データバインディングで生成されるコードではgetValueのnullチェックを行っています。
nullチェックに引っかかった場合、そこで評価が中断され部分式の結果(viewModel.data.value.empty
)としては初期値であるfalseを返します。
今回の条件式は頭に否定(!
)が入っているのでfalseが反転してtrueになり、最終的な結果としてvisibilityはView.VISIBLEとなります。これがちらつきの原因です。
どうすればいいか
ではどうすればいいのでしょうか?
答えは単純で、
viewModel.data
のgetValue
がnullになることを見越した条件式にすればよいのです。修正後のfragment_main.xml... <Button ... android:visibility="@{(viewModel.data != null && !viewModel.data.value.empty)? View.VISIBLE: View.GONE}" ... /> <!-- レイアウトファイルはxmlなので&&が&&にエスケープされています。 -->こうすると生成されたコードは最初に部分式
viewModel.data != null
を評価してくれます。
viewModel.data
のgetValue
がnullを返す場合、上記部分式はfalseになるので、条件式全体としてもfalseを返すようになり、visibilityはView.GONEとなります。振る舞いとしては、冒頭のgifの下側になります(常にボタンが非表示なのでわかりにくいですが……)
雑感
データバインディングは便利なんですが、その便利な部分を担っている自動生成コードが隠されてしまっているために落とし穴にはまりやすく、イヤーな思いをすることも多いですね。
- 投稿日:2020-08-01T12:55:39+09:00
仮置きでゴリ押しする、イラストロジック解析アプリ
はじめに
iOS / Android で動作する解析アプリを作りました。
邪道なことは重々承知していますが、イラストロジック雑誌の難問が解けないのは悔しいので。プロジェクト
プロジェクトはこちら。
仮置き
このアプリは解析に行き詰まると、まだ埋まっていないマスに仮置きして解析を続行します。
例えば、あるマスを「黒」と仮定して解析をすすめた結果、ロジックに矛盾が生じたとします。
ロジックが成り立たないということは「起点となったマスは黒であってはならない」ということがわかります。
言い換えると「起点となったマスは白である」となるわけで、仮置きした「黒」を「白」に反転確定させて解析を進めます。仮置き中に行き詰まると、さらに仮置きをします。
もし、延々と仮置きをしていって全てのマスが矛盾なく埋まった場合は、仮置きが全て正しかったとしてロジッククリアとします。↓ 仮置きでゴリ押しする様子(仮置きを起点としたマスはムラサキ色で表示)
思ったこと
サンプル用にイラストロジックの問題をいくつか作ったのですが難しかったです。
仮置きばかり発生して、ロジック的な面白さの皆無な問題ばかり出来上がりました。ほとんど仮置きせずに解き進められる、雑誌などの絶妙なバランスはすごいと痛感しました。