- 投稿日:2020-08-03T22:09:51+09:00
道案内ボランティア向けのアプリ開発プロジェクト
1 はじめに
みなさんはじめまして。塚原卜伝と申します。
仕事ではC,C++,pythonを扱っています。
仕事の合間に外国人道案内ボランティア団体であるask me!での活動に参加しています。
ask me! HP : https://askme.ne.jp/
Twitter : https://twitter.com/askmeJAPAN
Facebook : https://www.facebook.com/weareaskme
Instagram :https://www.instagram.com/we.are.askme/?hl=ja2020年の3月にask me!での活動の効率化のためにAndroid&iOSアプリ(ask me! app)をリリースしました。
今回初めての個人開発で大変な所も多かったですが、楽しい所もありました。
今回は自分がどのように「ask me! app」を作っていったのかをまとめたいと思います。
2 ask me!の活動紹介
ask me! appはask me!での活動がベースになって作られたものなので、まずは活動内容の説明をしたいと思います。
活動中は困っていそうな外国人に積極的に話しかけていき、彼らの困っている内容を聞き出します(例:おすすめの寿司のお店はどこ?など)。
外国人の方を目的地までご案内いたしますが、我々はプロではないので、あくまでも自分たちの知っている範囲でorGoogle やスマートフォンで調べて分かる範囲でお伝えしています。
3 アプリを作成しようと思ったきっかけ
きっかけとしては2つあります。
一つ目は人数集計作業の簡素化のためです。
活動中に案内した方々の国籍や人数等は下記のシートに記入しています。
一番最後に活動で関わった人達の合計人数を大陸別に集計するのですが、この作業を簡素化することを目指しました。
2つ目は、活動中に撮影した写真を参加者の間で共有するツールが必要だったためです。
今までは参加者同士でLINEのアカウント交換をしてLINE上で写真の共有を行っていましたが、LINEに頼らなくても写真を共有できるツールが必要でした。
以上の2点からask me!のオリジナルのアプリを開発することにしました!
4 UI
開発したアプリの主なUIを下記に示します。
①ログイン画面
②活動する前に参加者や今日の目標等を登録する画面
③統計シート入力画面
④案内した合計人数を確認できる画面
⑤サイドバーメニュ
⑥写真共有機能画面
5 アプリの機能説明
①ログイン画面
ask me! appはask me!の活動参加者が使うことを想定していますので、一番最初にログイン画面を設けています。②活動する前に参加者や本日の目標等を登録する画面
統計シートでは活動前に参加者の名前や本日の活動目標を記載していましたが、それをスマホアプリでも記入できるようにする画面です。③統計シート入力画面
活動中に案内した外国人の国籍や人数を記入する画面です。④案内した合計人数を確認できる画面
活動中に案内した人数一覧を確認できる画面です。⑤サイドバーメニュ
ask me!のHPやSNSおよび写真共有機能画面へのリンクを貼っています。⑥写真共有機能画面
活動中に撮影した写真はこの画面で参加者と共有します。6 技術選定
ask me! app作成にはFlutterを利用しました。理由は同じソースコードでiOSとAndroidの開発が同時にできるからです。
バックエンドにはFirebaseを利用しました。
7 技術習得方法
私はFlutter初心者だったため、まずは本で勉強しました。Flutterに関する本は少ないのですが、下記の本で勉強しました。
Android/iOSクロス開発フレームワーク:https://books.rakuten.co.jp/rk/8f657c363ada3ef9bd62b9abafc2fa28/
全部読んでいると非常に時間がかかるので、最初の2章までを読みました。2章までを読むだけでも、基本的な内容はわかると思います。
なお、Flutterで分からないことを知らべる際は英語の記事が多いので、英単語を入力して調べるといいとおもいます。
実現したい機能を英語で入力するとサンプルコードが出てくるのでそれを見て勉強していました。
(例:写真共有機能なら「Flutter photo share」で検索してみる、など)8 まとめ
今回はask me! appの簡単な紹介をさせていただきました。
次回以降は各画面についてソースコード例と共にどのように実装したのかを記載したいと思います。
また、興味があれば読んでいただけると幸いです(次回は8月下旬に投稿予定です)。
アプリは下記から
Android: https://play.google.com/store/apps/details?id=com.askme.flutter_app4
iOS : https://apps.apple.com/us/app/ask-me/id1481671421?l=ja&ls=1
- 投稿日:2020-08-03T22:02:38+09:00
coroutines x Retrofitで CallAdapterを使いAPIエラーレスポンスをデコードする
はじめに
APIのエラーレスポンスの型が統一されている場合, エラーレスポンスの型を定義し, デコードを共通化して行いたくなります.
今回はRetrofitのCallAdapter
を使用して, APIエラー時の挙動を(HttpException
ではなく) Custom Exceptionを吐くよう変更する方法をメモしておきます.公式のサンプルにも同じ目的の
CallAdapter
がありますが, coroutinesの場合必要な定義が少し変わるのでメモしておきます.CallAdapter.Factory#getの処理
suspend関数を使う場合,
CallAdapter.Factory#get
でreturnType
がCall
であることを確認する必要があります.class ErrorHandlingCallAdapterFactory : CallAdapter.Factory() { override fun get( returnType: Type, annotations: Array<Annotation>, retrofit: Retrofit ): CallAdapter<*, *>? { // 返り値がCall<..>である場合のみ適用する. // 注: 「nullを返す」 == 「このAdapterはこの関数に適用できない」を表す if (getRawType(returnType) != Call::class.java) { return null } require(returnType is ParameterizedType) { "Call return type must be parameterized as Call<Foo> or Call<? extends Foo>" } ... } }coroutinesを使用する場合, 返り値の型は
Call
で括らず,suspend fun sampleApi(): Hoge
のように直接定義すると思います.
そのため一瞬解せない気持ちになりそうですが, この時内部では以下のような処理が行われています(参考).
- もしsuspend関数であれば,
Hoge
をCall<Hoge>
に変換する.Call<Hoge>
に対応できるCallAdapter.Factory
を探索するこれらから, suspend関数の
returnType
はHoge
ではなくCall<Hoge>
が与えられます. そのため, suspend関数に対応してCallAdapter
を設定したい場合は,returnType
がCall<..>
である時にインスタンスを返すように実装します.CallAdapterの型
suspend関数を使う場合,
Call
を返すAdapterを適用します.class Adapter<T> : CallAdapter<T, Call<T>> { override fun responseType(): Type = ... // Call<T>を受け取り, Call<T>を返すAdapter. override fun adapt(call: Call<T>): Call<T> = .. }前節と同様に, retrofit内部では以下のような処理がされています(参考).
CallAdapter#adopt
の返り値としてCall<Hoge>
を受け取る- それを
Contiuation
(suspend関数) を介してHoge
に変換して返すそのため, 「(
Call<T>
を受け取り, )Call<T>
を返す」型であるCallAdapter<T, Call<T>>
が正しい型となります.Executor
suspend関数を使う場合, 通常callbackをメインスレッドで行う必要がないため,
Retrofit#callbackExecutor
は使用しません.
メインスレッド以外を設定していたり, 特別な事情がある場合は使用した方が良さそうです.サンプル
以下がコード例です. Custom Exceptionにデコードできないケースではデフォルトと同じ挙動をさせるような実装になっています.
class ErrorHandlingCallAdapterFactory : CallAdapter.Factory() { override fun get( returnType: Type, annotations: Array<Annotation>, retrofit: Retrofit ): CallAdapter<*, *>? { if (getRawType(returnType) != Call::class.java) { return null } require(returnType is ParameterizedType) { "Call return type must be parameterized as Call<Foo> or Call<? extends Foo>" } return Adapter<Any>(getParameterUpperBound(0, returnType)) } private class Adapter<T>(private val responseType: Type) : CallAdapter<T, Call<T>> { override fun responseType(): Type = responseType override fun adapt(call: Call<T>): Call<T> = ErrorHandlingCall(call) } } class ErrorHandlingCall<T>( private val delegate: Call<T> ) : Call<T> by delegate { override fun enqueue(callback: Callback<T>) = delegate.enqueue(object : Callback<T> by callback { override fun onResponse(call: Call<T>, response: Response<S>) { if (response.isSuccessful) { callback.onResponse(this@ErrorHandlingCall, response) return } val errorBody = response.errorBody() if (errorBody == null || errorBody.contentLength() == 0L) { callback.onResponse(this@ErrorHandlingCall, response) return } try { // Custom Exceptionにデコード・変換する val exception = convertToCustomException(response) callback.onFailure(this@ErrorHandlingCall, exception) } catch (ex: Exception) { callback.onResponse(this@ErrorHandlingCall, response) } } }) }これを
Retrofit.Builder
に渡せば完成です.interface Service { @GET("..sample..") suspend fun sample(): Sample } Retrofit.Builder() .addCallAdapterFactory(ErrorHandlingCallAdapterFactory()) .build() .create(Service::class.java)参考
- 投稿日:2020-08-03T20:53:50+09:00
ChannelTalkのiOS、Androdアプリへの導入方法
MAMORIOにおけるチャットツールの利用とChannelTalkへの移行
落とし物防止タグのMAMORIOはIoTタグとアプリを連携させる複雑さの問題からユーザーからの問い合わせが多く、2018年頃からサポートの体験の質を向上させるためにIntercomというチャットツールを導入していた。
しかし、米国製で主に欧米や中南米諸国をマーケットとしているIntercomは基本的に英語話者向けで管理コンソールもIntercom社員とのやりとりも英語であり、また(便利そうな)自然言語処理を用いたチャットボット機能も一向に日本語対応する気配がなかったため、韓国製でBotやマーケティング機能のコストが安く、かつ日本語圏を優先してくれるChannelTalkへ移行したところオペレーターによるサポート負荷を半減させることに成功した。
以下は弊社オペレーターが書いた記事: https://note.com/tanakosan0508/n/n99af82341ba0
導入方法
導入ガイド: https://developers.channel.io/docs
SDKを使用するには以下2つを行う必要がある。
また注意点としては、Androidでは以下のようにgradleに追加するだけでサクッとパッケージをインストールできるのだが、
dependencies{ //Cannel Talk implementation 'com.zoyi.channel:plugin-android:7.1.3' implementation 'com.zoyi.channel:plugin-android-fcm:7.1.3' }iOSの場合、弊社のようにCarthageで管理を行っていると以下のようにChannelIOが依存している大量のライブラリを追加する必要がある。
利用方法
ChannelTalkの動作には以下のサイクルがある。
1. initalize
ChannelTalkがアプリの状態を監視できるようにする(やり忘れてもコンパイルは通りその後のコードも動くが、バックグラウンド時のプッシュ通知によるチャットの応答などが一切機能しなくなるので注意)。
iOS
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. ChannelIO.initialize(application) return true }Andorid
public class MyApplication extends Application { @Override public void onCreate() { super.onCreate(); ChannelIO.initialize(this); // TODO : Your code } }2. 遠隔プッシュ通知登録
ChannelTalkのサーバーにデバイスをプッシュ通知の受け手として登録する
これをおこなわないとユーザーがバックグラウンドにしてしまったあとに返信を行ってもプッシュ通知が届かない。iOS
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { ChannelIO.initPushToken(deviceToken) }Android
くわしくはこちらpublic class MyActivity extends AppCompatActivity { @Override public void onCreate() { super.onCreate(); // TODO : Your code // ... // ... ChannelIO.handlePushNotification(this); } }3. bootとshow
bootはユーザーの情報をチャネルトークに渡してユーザー情報を更新し、セッションを貼る処理である。
bootはチャットコミュニケーションを行うためには必ず行わなければならず、viewの描画が終わったタイミングでbootを行ったあと、コールバックでshowコマンドによりランチャーを表示することが推奨されている。 (iOSの場合viewDidAppear)iOS
let settings = ChannelPluginSettings() settings.pluginKey = "YOUR_PLUGIN_KEY" settings.memberId = "memberId" let profile = Profile() profile.set(name: "Jason") ChannelIO.boot(with: settings, profile: profile) { (status, guest) in if status == .success { ChannelIO.show(animated: true) } else { //異常処理 } }なお、Profileは非必須でログイン前の匿名ユーザーともチャットを行うことができる。
let settings = ChannelPluginSettings() settings.pluginKey = "YOUR_PLUGIN_KEY" ChannelIO.boot(with: settings, profile: profile) { (status, guest) in print(guest) //=> nil }Android
ChannelPluginSettings settings = new ChannelPluginSettings(YOUR_PLUGIN_KEY); settings.setUserId(uniqueUserId); Profile profile = Profile.create() .setName(userName) .setEmail(userEmail) .setProperty("HomePage", "www.zoyi.co"); // If you register the listener, it receives the boot status. // Booting without user information ChannelIO.boot(ChannelPluginSettings settings, OnBootListener listener);4. hide
iOSではチャネルトークのランチャーと返信のバナーはWindowの最前面に表示されるので、特定のページのみでチャットを表示し他のページで表示したくない時は明示的にhideし隠さなければならない。
iOS
override func viewWillDisappear(_ animated: Bool) { ChannelIO.hide(animated: true) }5. shutdown
ChannelTalkは一度bootした後コネクションを貼ってメッセージを待ち続けるため、ユーザーがそのアカウントでのアプリの使用をやめる際は明示的にshowdownコマンドを実行しなければならない。
iOS
func logout() { ChannelIO.shutdown(); }メリット
1. 接客チャットの料金が訪問者数5000人超で月9000円
ChannelTalkも接続したユーザー数による従量課金だが、上限が5001人で終わる。
2. 日本語の接客botを作成できる
ChannelTalkを導入する最大のメリットは、選択肢式の接客ボットを日本語で複数種類手軽に作成できることである。
MAMORIOで日本語のbotを導入した結果、オペレーターへ直接話しかけるユーザーの割合が半減しサポートチームの負荷を激減させることに成功した。
- 投稿日:2020-08-03T13:31:33+09:00
オープンソースのAndroid(AOSP)にGooglePlayStoreを入れる
Step1 端末にあったファイルをダウンロードする
PlayStoreを利用するには、以下の3つのパッケージが必要となります。
- GoogleServicesFramework
- Phonesky(GooglePlayストア)
- PrebuiltGmsCore(GooglePlay開発者サービス)
これらはOpen GAPPSからダウンロードすることができます。
手持ちのAndroidに合わせてPlatform,Androidを選択しましょう。
Variantはpicoあたりで大丈夫です。CPUを確認するコマンド(armeabi-v7aであれば32bitARMです)
adb shell getprop ro.product.cpu.abi
バージョンを確認するコマンド
adb shell getprop ro.build.version.release
Step2 必要な.apkファイルを揃える
ダウンロードしたファイルを解凍すると、Coreフォルダ内に以下の3つのファイルがあると思います。
- gmscore-arm.tar.lz
- gsfcore-all.tar.lz
- vending-arm.tar.lz
これらをlzipで展開します。
tar --lzip -xvf [ファイル名]
または
lzip -d -c [ファイル名] | tar xvf -
展開すると、nodpiフォルダ内に.apkファイルがあるので、それらを別フォルダにまとめておきましょう。
Step3 システムアプリとしてインストールする
1) システムへの書き込み権限を与える
通常の方法ではインストールができないので、手動でインストール作業をします。
まずどこのパーティションが/systemに割り当てられているかを確認します。
adb shell cat /proc/mounts | grep system
すると以下のように表示されると思います。
/dev/block/mmcblk2p5 on /system type ext4 (ro,seclabel,relatime,data=ordered)
書き込み権限がないようであれば(roと書いてある)、書き込み権限をつけてマウントしなおします。
adb shell mount -o rw,remount /system
2) フォルダを作成し、パッケージを入れる
/system/priv-appに、以下の3つのフォルダを作成します。
- GoogleServicesFramework
- Phonesky
- PrebuiltGmsCore
adb shell mkdir /system/priv-app/GoogleServicesFramework adb shell mkdir /system/priv-app/Phonesky adb shell mkdir /system/priv-app/PrebuiltGmsCoreStep2で抽出したパッケージをこのフォルダに入れていきます。
adb push PrebuiltGmsCore.apk /system/priv-app/PrebuiltGmsCore/ adb push GoogleServicesFramework.apk /system/priv-app/GoogleServicesFramework/ adb push Phonesky.apk /system/priv-app/Phonesky/Phonesky.apkフォルダとファイルの権限を絞ります。
adb shell chmod 755 /system/priv-app/GoogleServicesFramework adb shell chmod 755 /system/priv-app/Phonesky adb shell chmod 755 /system/priv-app/PrebuiltGmsCore adb shell chmod 644 /system/priv-app/GoogleServicesFramework/GoogleServicesFramework.apk adb shell chmod 644 /system/priv-app/Phonesky/Phonesky.apk adb shell chmod 644 /system/priv-app/PrebuiltGmsCore/PrebuiltGmsCore.apkStep4 端末での設定
最後に端末を再起動すると、インストールされたアプリが確認できると思います。
adb reboot
あとは端末上で、先ほどインストールしたアプリに権限をつけていきます。
Settings>Apps(設定>アプリ)に移動し、右上のメニューからシステムアプリを表示させましょう。
以下のものがインストールされていると思います。
- Google Play Store(Google Play ストア)
- Google Play Services(Google Play開発者サービス)
- Google Services Framework
それぞれのアプリの権限を全て許可させれば終了です。
その他
場合によっては、GoogleLoginService(Googleアカウントマネージャー)が必要となるかもしれません。
この場合Step2でgsflogin-all.tar.lzを展開し、あとは同様の手順を辿るとよいでしょう。このドキュメントは以下のサイトを参考にしました。
Install Google App on AOSP Build
Install Google Play Store on Android 7.1.1(API 25) emulatorちなみに、GMS搭載のライセンスはGoogleへの申請が必要となります。
- 投稿日:2020-08-03T08:34:07+09:00
Androidアプリ開発探求記(その4)
概要
今回は git の .ignore に関して、探求してみたいと思います。
やること
Android Studio で作成したデフォルトの .ignore ファイルに対して、以下のことをしてみようと思います。
- 現在の実験コード1上にフィルタリング対象が存在する項目に対してコメントを追加
- 現在の実験コード上にフィルタリング対象が存在しない項目を削除
※ 自明と思われるものについては例外的に .ignore に含めていこうと思います。2
背景
.ignore に関してはネット上に様々な情報が存在し、.ignore.io のような便利な自動生成ツールも存在します。
しかし、状況によっては必要となるグレーゾーンのファイルまでバッサリ切り捨ててしまっているため、弊害もあります。
例えば、開発プロジェクトでスペルチェック用辞書を共有する方法では辞書ファイルは idea/dictionaries フォルダ以下に配置されますが、.gitignore.io [AndroidStudio] ではバッサリ切り捨てられてしまっています。
実際問題としては、Android Studio 上での辞書共有にはいろいろと欠点もあるため、デフォルトでバッサリ切り捨ててしまったほうが安全だと思います。しかし、最初から切り捨ててしまうと、存在自体を気づけないという弊害があり、これは探求記的には許容できません。
ということで、今後 .ignore に関しては、全項目を把握していこうと思います。
Android Project
Android Studio で作成したデフォルトの .ignore ファイルは2つあります。
◆ Root Project
/build◆ Sub Project
*.iml .gradle /local.properties /.idea/caches /.idea/libraries /.idea/modules.xml /.idea/workspace.xml /.idea/navEditor.xml /.idea/assetWizardSettings.xml .DS_Store /build /captures .externalNativeBuild .cxx.gitignore を削除してみる
.gitignore を削除し、Reimport gradle project, Rebuild Project を行い、差分を確認してみます。
$ git status On branch comment_on_dot_ignore_files Changes to be committed: (use "git restore --staged <file>..." to unstage) deleted: .gitignore deleted: app/.gitignore Untracked files: (use "git add <file>..." to include in what will be committed) .gradle/ .idea/caches/ .idea/libraries/ .idea/modules.xml .idea/modules/ .idea/workspace.xml app/build/ local.properties必須の項目だけ復活させる
現状でフィルタが必要、もしくは自明の項目だけを復活させてみます。
### Android Studio ### # IDEAのモジュール定義ファイル。自動生成されるものなので、commit されるべきではない。 *.iml # IDEローカルのキャッシュ情報だと思われる。commit されるべきではない。 /.idea/caches # IDE上で利用されるライブラリの情報だと思われる。自動制裁される情報であり、共有の必要もないので、commit されるべきではない。 /.idea/libraries # IDE上のモジュール構成情報だと思われる。IDEの利用状況により異なる情報であり、共有の必要もないので、commit されるべきではない。 /.idea/modules.xml # IDEローカルの情報なので commit されるべきではない。 /.idea/workspace.xml # This file is automatically generated by Android Studio. This file should *NOT* be checked into Version Control Systems, as it contains information specific to your local configuration. /local.properties ### Gradle ### # gradle の work 領域。gradle により自動生成されるものなので commit されるべきではない。 .gradle # gradle の build 成果物用フォルダ。build により自動生成されるものなので commit されるべきではない。 /build ### MAC ### # MAC OS上の不可視ファイル。commit されるべきではない。 .DS_Store削除された項目
/.idea/navEditor.xml /.idea/assetWizardSettings.xml /captures .externalNativeBuild .cxxまとめ
今回は、Android Studio で作成したデフォルトの .ignore ファイルに対して、現時点で必須なものだけに対してコメントを付与し、そうでないものは削除してみました。
今後も同様の対応をしていこうと思います。
.DS_Store
など ↩
- 投稿日:2020-08-03T06:51:27+09:00
Androidアプリ開発探求記(その3)
概要
前回利用した Android project を起動したら、なにやら .idea/gradle.xml が変更されていました。
タイムスタンプを確認したところ、どうやら Android Studio でプロジェクトを開いたタイミングで変更が入ったっぽいです。
<component name="GradleMigrationSettings" migrationVersion="1" />対応
特に問題なさそうなので、普通に commit しました。
diff: GitHub
- 投稿日:2020-08-03T01:00:11+09:00
【Flutterの状態管理】テーマ切替&多言語化 〜provider,BLoC,reduxの3パターンで実現してみる③
いよいよ本シリーズの最終篇になります。
本篇は、アプリのテーマ(Theme)切替、多言語化を通じてFlutterの状態管理のreduxパターンを説明します。三、reduxでテーマ切替&多言語化
flutter_redux: ^0.6.0
Blocと同じのようにreduxにも3つのポイントがある。それは 状態State、アクションAction、変化させる者Reducer。
reduxは封建制とある程度似ている。天子が、土地を諸侯に分与し、諸侯はまたそれぞれ領内の管理を大夫に委託する。各々が責任を取って行動する。reduxにおいて、全ての状態はstoreによって集中管理し、天子AppStateから階層的に分配していく。
1.redux状態、アクション、変化させる者の準備
天子
appState_redux.dartimport 'themeState_redux.dart'; import 'localState_redux.dart'; class AppState { final ReduxThemeState themeState;//テーマ状態 final ReduxLocaleState localeState;//言語状態 AppState({this.themeState, this.localeState}); factory AppState.initial()=> AppState( themeState: ReduxThemeState.initial(), localeState: ReduxLocaleState.initial() ); } //集中的に状態を分配 AppState appReducer(AppState prev, dynamic action)=> AppState( themeState:themeDataReducer(prev.themeState, action), localeState: localReducer(prev.localeState, action),);諸侯① theme
themeState_redux.dartimport 'package:flutter/material.dart'; import 'package:redux/redux.dart'; //テーマ状態の切り替え class ReduxThemeState extends ChangeNotifier { ThemeData themeData; int colorIndex; ReduxThemeState(this.themeData,this.colorIndex); factory ReduxThemeState.initial()=> ReduxThemeState(ThemeData(primaryColor: Colors.green),1); } //テーマアクションの切り替え class ActionSwitchTheme { final ThemeData themeData; final int colorIndex; ActionSwitchTheme(this.themeData,this.colorIndex); } //変化させる者の切り替え var themeDataReducer = TypedReducer<ReduxThemeState, ActionSwitchTheme>((state, action) => ReduxThemeState(action.themeData,action.colorIndex));諸侯② local
localState_redux.dartimport 'package:flutter/material.dart'; import 'package:redux/redux.dart'; //言語状態の切り替え class ReduxLocaleState{ Locale locale; ReduxLocaleState(this.locale); factory ReduxLocaleState. initial()=> ReduxLocaleState(Locale('ja', 'JP')); } //言語アクションの切り替え class ActionSwitchLocal { final Locale locale; ActionSwitchLocal(this.locale); factory ActionSwitchLocal.ja()=> ActionSwitchLocal(Locale('ja', 'JP')); factory ActionSwitchLocal.en()=> ActionSwitchLocal(Locale('en', 'US')); } //変化させる者の切り替え var localReducer = TypedReducer<ReduxLocaleState, ActionSwitchLocal>((state, action) => ReduxLocaleState(action.locale,));2.reduxプロパティーの使用
main.dartimport 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; //providerパターンのpubspec.yaml配置を参照 import 'home_page.dart'; import 'provider/I18nDelegate.dart'; //providerパターン紹介時のファイル使用 import 'package:flutter_redux/flutter_redux.dart'; import 'package:redux/redux.dart'; import 'redux/appState_redux.dart'; void main() { runApp(Wrapper(child: MyApp())); } class Wrapper extends StatelessWidget { final Widget child; Wrapper({this.child}); @override Widget build(BuildContext context) { //StoreProviderによってラップする、storeプロパティーでAppStateを配置 return StoreProvider( store: Store<AppState>(appReducer, initialState: AppState.initial()), child: MyApp()); } } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return StoreBuilder<AppState>(builder: (context,store) => MaterialApp( title: "状態管理Demo", localizationsDelegates: [ GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, I18nDelegate.delegate //言語デリゲート シリーズ初編のproviderパターンのファイルを利用 ], locale: store.state.localeState.locale, supportedLocales: [store.state.localeState.locale], debugShowCheckedModeBanner: false, home: HomePage(), )); } }状態の切り替えは store.dispatch(~action) を使う
home_page.dartimport 'package:flutter/material.dart'; import 'provider/i18n.dart'; //providerパターン紹介時のファイル使用 import 'package:flutter_redux/flutter_redux.dart'; import 'redux/appState_redux.dart'; import 'redux/themeState_redux.dart'; import 'redux/localState_redux.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; //StoreBuilder<AppState>(builder: (context,store) を通じて状態ゲット return StoreBuilder<AppState>( builder: (context, store) => Scaffold( appBar: AppBar( backgroundColor: store.state.themeState.themeData.primaryColor, title: Text( I18N.of(context).title, style: TextStyle(color: Colors.white), ), ), drawer: Drawer( child: Column( children: <Widget>[ Container( width: screenWidth, height: statusBarH + naviBarH, color: store.state.themeState.themeData.primaryColor, ), SizedBox(height: 10.0), Row( children: <Widget>[ Padding(padding: EdgeInsets.all(10)), Wrap( children: <Widget>[ RaisedButton( color: Colors.green, onPressed: () { //イベントトリガー、重要 store.dispatch(ActionSwitchTheme( ThemeData(primaryColor: Colors.green), 1)); }, child: Text( I18N.of(context).greenBtn, style: TextStyle(color: Colors.white), ), shape: CircleBorder(), ), RaisedButton( color: Colors.red, onPressed: () { store.dispatch(ActionSwitchTheme( ThemeData(primaryColor: Colors.red), 2)); }, child: Text( I18N.of(context).redBtn, style: TextStyle(color: Colors.white), ), shape: CircleBorder(), ), RaisedButton( color: Colors.blue, onPressed: () { store.dispatch(ActionSwitchTheme( 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: store.state.themeState.themeData.primaryColor, onPressed: () { store.dispatch(ActionSwitchLocal.ja()); }, child: Text( "日本語", style: TextStyle(color: Colors.white), ), ), SizedBox( width: 15.0, ), RaisedButton( color: store.state.themeState.themeData.primaryColor, onPressed: () { store.dispatch(ActionSwitchLocal.en()); }, child: Text( "English", style: TextStyle(color: Colors.white), ), ), ], ), ], ), ), body: ListView( children: <Widget>[ Center( child: Column( children: <Widget>[ SizedBox(height: 10.0), Container( width: 200, height: 300, color: store.state.themeState.themeData.primaryColor, ), SizedBox(height: 10.0), Text( I18N.of(context).analects, style: TextStyle( color: store.state.themeState.themeData.primaryColor, fontSize: 18.0, fontWeight: FontWeight.bold), ), SizedBox(height: 30.0), FloatingActionButton( onPressed: () {}, backgroundColor: store.state.themeState.themeData.primaryColor, child: Icon(Icons.check)), ], ), ) ], ), ), ); } }まとめ
いかがでしたか。本篇をもってflutterの状態管理シリーズを完了とします。総じて言えば、3パターンともに構造的に、実現方式的に似ているようなところがあります。providerはflutter公式が推奨するパターンとして使っても無難でしょう。Streamに対する理解を深めれば、Blocの使用は少し上級感が感じられます。reduxはコード量が少なく、構造もはっきりしており、個人的には好きです。
どのパターンを採用するのかは正解がなく、プロジェクトのニーズに合わせて決めておけば良いでしょう。また、状態を変更する必要のあるwidgetがそれほど無ければ、基礎のsetState()というapiを使っては分かりやすいでしょう。