20200801のAndroidに関する記事は9件です。

【Flutterの状態管理】テーマ切替&多言語化 〜provider,BLoC,reduxの3パターンで実現してみる②

お待たせしました。プロジェクトの人員異動でバタバタしていました。
さて、前回ではproviderという実現方式でflutterの状態管理を説明しました。今回はBLoC(Business Logic Component)を利用し、アプリのテーマ(Theme)切替、多言語化を通じてFlutterの状態管理を説明します。

二、BLoCでテーマ切替&多言語化

Blocには3つのポイントがある。それは 業務ロジックユニット状態イベント

flutter_bloc: ^6.0.1

color1.png color2.png color5.png color6.png language3.png language4.png

1.テーマ切替のBloc

ハンバーガーメニューにあるテーマ色ボタンの押下により、グローバルにテーマ色を切替える。

1.1状態クラス

テーマ関連の状態量を抽象クラスに置きながら、他の状態はこれを継承し状態の細分化を実現する。

theme_state.dart
import '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.dart
import '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.dart
import '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.dart
import '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.dart
import 'package:flutter/material.dart';

@immutable
abstract class BlocLocaleEvent {}

class BlocEventSwitch2JP extends BlocLocaleEvent{}

class BlocEventSwitch2EN extends BlocLocaleEvent{}

2.3業務ロジッククラス

lang_bloc.dart
import '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.dart
import '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.dart
import '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パターンをご紹介しますので、引き続きお楽しみください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 version
Android Debug Bridge version 1.0.41
Version 30.0.3-6597393
Installed as {インストールディレクトリ}/adb.exe

端末の確認

adb devices
List 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
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Android】ライフサイクル(Lifecycle)アンチパターン 〜2020年版〜 その2

はじめに

以前、Lifecycle のアンチパターンについて最新のベターな実装を紹介しました。
【Android】ライフサイクル(Lifecycle)アンチパターン 〜2020年版〜 その1

今回は第2弾となります。
Activity(Fragment)の Lifecycle のアンチパターンについてと最新のベターな実装について紹介します。
第1弾で紹介した実装と組み合わせて使用するとよりよいと思います。

アンチパターンの実装例

onResume() で通信処理等をしたいケースがあったとします。
しかし、onResume() は、onCreate(画面生成)時以外に画面回転、バックグラウンド復帰、別の画面から戻ってくる onActivityResult() 後のケースも通知されてしまいます。その場合にありがちな実装は以下です。

MainActivity.kt
class 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.kt
class 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 → _onResume

Fragment での利用

当然ですが Fragment でも同じように使えます。

MainFragment.kt
class MainFragment : Fragment() {

    override fun onAttach(context: Context) {
        super.onAttach(context)
        lifecycleScope.launchWhenResumed {
            // 実装したい処理
        }
    }
}

lifecycleScope.launch を使ってみる

lifecycle.currentState.isAtLeast と組み合わせて使います。

MainFragment.kt
class 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 も進化していっているので便利な機能を使ってバグが少ないアプリを作りましょう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

新卒プログラマの学習ログ 〜 其の三 〜

エラー処理(CreateProcessエラー)

このエラーでドツボにハマッたので、ここに書き残しておきます。

 環境

  • Windows 10
  • Android Studio 4.0.1

概要

Android Gradle Pluginのアップデートか何かをきっかけに、どのプログラムを実行しても下記内容のエラーが表示されるようになりました。

Android Studioエラー:CreateProcessエラー

私の場合では原因はJDKのクラッシュでした。
JDK Locationで示しているディレクトリ内にjava.exeがなければ、私と同じ原因となっている可能性が高いです。

プロジェクトを開いている状態のAndroid Studio上から、FileOther SettingsDefault Project Structure... と開いてパスの先を把握し、PCの実環境で上記の内容を確認してみてください。

解決方法

JDKを再インストールすることで解決しました。
Android Studioで現在使用しているものと同じバーションを.zipファイルでダウンロードします。
https://developer.android.com/studio?hl=ja)
解凍したファイル内のJDKに現在のインストール済みAndroid StudioのJDKのパスを通せば完了です。

とりあえず。。。

メモ代わりに書いていますが、これから理解が深まれば書き足していきます。
いつか誰かのお役に立てると嬉しいですね。

初学者から一人前まで、、、

ここでは一人前のプログラマの定義を
「おおよそ自力で様々なツールを活用しながら、ある程度思い通りの実装ができること」
とします。

また、人生においてプログラミングに向き合う時間として、現時点で2000時間取り組めればある程度の基盤が備わると仮定しています。
(言語やツールに依存しない思考法や情報収集能力)

プログラミングは大学で学んで来ましたが、特に根詰めてやってきていないので、ノーカウントとして取り組みます。

現在までの学習時間
およそ360時間
どんどん更新していきます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RiverpodとFlutter Hooksを使って、カウンターアプリを作ってみよう

はじめに

この記事では、RiverpodとFlutter HooksとStateNotifierを使って、
カウンターアプリを作ってみようと思います。

今回作成したコードはこちらのリポジトリに公開しています。
https://github.com/karamage/riverpod_flutterhooks_example

スクリーンショット 2020-08-01 14.34.35.png

Flutterの状態管理パターン

みなさん、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とFlutterHooksを使ってカウンターアプリを作ってみましたので、当記事にまとめます。

Riverpodとは

Providerの作者による、Providerの進化版です。

Provider, but different

Riverpod公式ページ
https://riverpod.dev
スクリーンショット 2020-08-01 14.36.53.png

「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/98d1bc4a83265a764084

https://riverpod.dev
Riverpodの公式ページを見る限り、RiverpodとFlutter Hooksはセットで使うのが、作者の推しみたいです。
RiverpodはFlutter Hooksを外しても利用できます。

カウンターアプリを作ってみよう

Riverpod と Flutter Hooks と StateNotifierインストール

pubspec.yaml
environment:
  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.0

pubspec.yamlに上記のように書いて、ターミナルで「flutter pub get」しましょう。

flutter pub get

これでインストールは完了です。

それでは、コードを書いていきましょう。

lib/main.dart
import '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.
      ),
    );
  }
}

めちゃくちゃコードがすっきり書けて感動。

実行して、「+」を押すと数字がカウントアップすると思います。
スクリーンショット 2020-08-01 14.34.35.png

コードの説明

グローバルに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

参考リンク

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RiverpodとFlutter Hooksを使う、はじめの一歩

はじめに

この記事では、RiverpodとFlutter Hooksを使う、はじめの一歩として、
カウンターアプリを作ってみようと思います。

今回作成したコードはこちらのリポジトリに公開しています。
https://github.com/karamage/riverpod_flutterhooks_example

スクリーンショット 2020-08-01 14.34.35.png

Flutterの状態管理パターン

みなさん、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とFlutterHooksを使ってカウンターアプリを作ってみましたので、当記事にまとめます。

Riverpodとは

Providerの作者による、Providerの進化版です。

Provider, but different

Riverpod公式ページ
https://riverpod.dev
スクリーンショット 2020-08-01 14.36.53.png

「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/98d1bc4a83265a764084

https://riverpod.dev
Riverpodの公式ページを見る限り、RiverpodとFlutter Hooksはセットで使うのが、作者の推しみたいです。
RiverpodはFlutter Hooksを外しても利用できます。

カウンターアプリを作ってみよう

Riverpod と Flutter Hooks と StateNotifierインストール

pubspec.yaml
environment:
  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.0

pubspec.yamlに上記のように書いて、ターミナルで「flutter pub get」しましょう。

flutter pub get

これでインストールは完了です。

それでは、コードを書いていきましょう。

lib/main.dart
import '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.
      ),
    );
  }
}

めちゃくちゃコードがすっきり書けて感動。

実行して、「+」を押すと数字がカウントアップすると思います。
スクリーンショット 2020-08-01 14.34.35.png

コードの説明

グローバルに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

参考リンク

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ViewPager2で共通のViewModelを使う方法

はじめに

こんにちは、Androidエンジニアのどすこいです。

今回はタイトルにある通り、ViewPager2で共通のViewModelを使う方法を解説したいと思います。
タイトルだけ見て、なんのこっちゃと思うかもしれないのでまずは次の図をご覧くださいスクリーンショット 2020-08-01 15.04.28.png

こんな感じで親FragmentがViewPager2を用いて3つの子Fragmentを持っている状況があるとします。
その際、全ての子Fragmentで同じViewModelを使いたい時にどうするべきか悩んだので共有します。

環境

Android Architecture Componentを使用します。
appレベルのbuild.gradleに次の記述をしてください。

build.gradle
dependencies {
  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を生成します。

親Fragment
private val viewModel: HogeViewModel by viewModels()

そして子Fragmentで親FragmentのViewModelStoreOwnerを用いて同じViewModelを生成してあげます

子Fragment
private val viewModel: HogeViewModel by viewModels(
  { parentFragment!! }
)

こうしてあげることでそれぞれの子Fragmentで共通したViewModelが生成できますね。

まとめ

本題より前提知識や説明の方が長くなってしまいましたね(笑)

Android Architecture Componentはまだ発表されてそこまで時間が経っていないので、詳しく紹介している記事が少なかったり見つけたとしても英語でよくわからん、みたいなことが多々あって苦労するところはあると思います(少なくとも筆者はめっちゃ苦労してる)

なので今後も詳しく紹介する記事や、初心者向けの記事を書いていけたらなぁなんて思ってるので今後もよろしくお願いします!

参考にした記事

ViewModel, ViewModelProviderについて調べてみた

Activity, Fragmentを跨いでViewModelを共有する

ViewModel が画面回転しても保持される仕組み

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

データバインディングでLiveDataを評価する時はnon-nullでもnullチェックした方がいい

tl;dr

  • LiveData<Hoge>であってもLiveData#getValueの初期の戻り値はnull
  • 条件式次第ではデータバインディング時にnullチェックを忘れるとUI要素のちらつきの原因になるよ

movie.gif

ボタンがちらつく

通常非表示のボタンを、ある条件を満たす時だけ表示させるよう振る舞わせようとしていた時のことです。

データバインディングとLiveDataを使ってButtonのvisibilityを制御しようと考えました。
visibilityなら普通Booleanのデータで制御するのが普通ですが、今回はString型のデータで制御する必要がありました。
1文字以上の文字列なら表示、空文字なら非表示といった具合です。

Entity.kt
data class Entity(val value: String)
Repository.kt
class Repository {
    fun getData(): LiveData<Entity> {
        return liveData(GlobalScope.coroutineContext) {
            // 時間のかかる処理
            val data = ...

            emit(data)
        }
    }
}
MainViewModel.kt
class 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.dataMainViewModel#data)変数の取り扱いにありました。

1つずつ見ていきましょう。

MainViewModel#dataRepository#getDataから取得していますが、その中ではREST APIを叩き返ってきたデータを整形し、……といくらか時間のかかる処理を行っていました。

Repository.kt(再掲)
class Repository {
    fun getData(): LiveData<Entity> {
        return liveData(GlobalScope.coroutineContext) {
            // 時間のかかる処理
            val data = ...

            emit(data)
        }
    }
}

emit(data) が実行されるまで、MainViewModel#dataは初期状態にあるため、datagetValueはnullを返します。

それを念頭においてfragment_main.xmlをもう一度見てみましょう。

fragment_main.xml(再掲)
...

<Button
    ...
    android:visibility="@{!viewModel.data.value.empty? View.VISIBLE: View.GONE}"
    ...
/>

viewModel.datagetValueがnullを返すとき、!viewModel.data.value.emptyはどう評価されるのでしょうか?
答えは評価が中断されるです。
データバインディングで生成されるコードではgetValueのnullチェックを行っています。
nullチェックに引っかかった場合、そこで評価が中断され部分式の結果(viewModel.data.value.empty)としては初期値であるfalseを返します。
今回の条件式は頭に否定(!)が入っているのでfalseが反転してtrueになり、最終的な結果としてvisibilityはView.VISIBLEとなります。

これがちらつきの原因です。

どうすればいいか

ではどうすればいいのでしょうか?

答えは単純で、viewModel.datagetValueがnullになることを見越した条件式にすればよいのです。

修正後のfragment_main.xml
...

<Button
    ...
    android:visibility="@{(viewModel.data != null &amp;&amp; !viewModel.data.value.empty)? View.VISIBLE: View.GONE}"
    ...
/>

<!-- レイアウトファイルはxmlなので&&が&amp;&amp;にエスケープされています。 -->

こうすると生成されたコードは最初に部分式viewModel.data != nullを評価してくれます。
viewModel.datagetValueがnullを返す場合、上記部分式はfalseになるので、条件式全体としてもfalseを返すようになり、visibilityはView.GONEとなります。

振る舞いとしては、冒頭のgifの下側になります(常にボタンが非表示なのでわかりにくいですが……)

雑感

データバインディングは便利なんですが、その便利な部分を担っている自動生成コードが隠されてしまっているために落とし穴にはまりやすく、イヤーな思いをすることも多いですね。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

仮置きでゴリ押しする、イラストロジック解析アプリ

はじめに

iOS / Android で動作する解析アプリを作りました。
邪道なことは重々承知していますが、イラストロジック雑誌の難問が解けないのは悔しいので。

↓ 実行例
ss_50x50

プロジェクト

プロジェクトはこちら

仮置き

このアプリは解析に行き詰まると、まだ埋まっていないマスに仮置きして解析を続行します。

例えば、あるマスを「黒」と仮定して解析をすすめた結果、ロジックに矛盾が生じたとします。
ロジックが成り立たないということは「起点となったマスは黒であってはならない」ということがわかります。
言い換えると「起点となったマスは白である」となるわけで、仮置きした「黒」を「白」に反転確定させて解析を進めます。

仮置き中に行き詰まると、さらに仮置きをします。
もし、延々と仮置きをしていって全てのマスが矛盾なく埋まった場合は、仮置きが全て正しかったとしてロジッククリアとします。

↓ 仮置きでゴリ押しする様子(仮置きを起点としたマスはムラサキ色で表示)
ss_45x45

思ったこと

サンプル用にイラストロジックの問題をいくつか作ったのですが難しかったです。
仮置きばかり発生して、ロジック的な面白さの皆無な問題ばかり出来上がりました。

ほとんど仮置きせずに解き進められる、雑誌などの絶妙なバランスはすごいと痛感しました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む