- 投稿日:2020-07-22T21:03:52+09:00
【Android】またversionCodeインクリメントし忘れた!【Play Store】
背景
ぼく「Google Play Consoleにapkファイルアップロードしたぞー!(versionCode: 3)」
ぼく「あっ!修正しわすれてる!修正して、と。」
ぼく「expoのビルドだとBuild queue消化まで長いんだよな・・・また数十分待ちだよ」
ぼく「やっとビルドできた!アップロードだ!(versionCode: 3)GooglePlayConsole「既にversionCode:3はライブラリにアップロードされて・・・」
ぼく「ま、また数十分待たされるのー!?」
Tips
- リリース管理 > アーティファクトライブラリ
- 対象のバージョンコードのゴミ箱ボタン > 押下 => 削除!
- 投稿日:2020-07-22T18:03:10+09:00
【Flutter】Firebase を使って SNS っぽいアプリを作るために学んだこと
趣味の魚捌きの延長で、魚を捌くのが好きな人のための SNS アプリ、 "Sengyo" を作ってリリースしました。
iOS
https://apps.apple.com/us/app/id1523325680Android
https://play.google.com/store/apps/details?id=com.tsuyoshichujo.sengyoproductFirebase を始めとした今まで雰囲気で使っていた技術の勉強や OS の新機能などの実験台として何かひとつ具体的なアプリがあると良いな、というモチベーションで作ったこのアプリですが、ひとまずアプリとして最低限形になったので、まずはこのアプリを作るために Firebase の使い方で学んだことを振り返ってみたいと思います。
なお、この記事では どのようなページを見てどのような情報を得ながらアプリを作ったか という内容が中心で、「こうプログラムを書けばこう動く」というような具体的なところまでは書いていません。
「同じようなアプリを作ってみたいけど、何をどう調べてどう作り始めたら良いかイメージできない」という方の役に立てればと思って書いた記事です。
学んだこと
このアプリはアプリ本体を Flutter で、サーバーサイドを Firebase で作っています。 Firebase はさらに細かく見ると、
- Firestore
- Cloud Storage
- Firebase Authentication
という3つのプロダクトを利用しています。
この記事では、 Sengyo アプリでこれらの Firebase の各プロダクトを使うために何を見てどのような情報を得たのかをそれぞれまとめていきます。
なお、Sengyo アプリのコードは GitHub に上げてあります。必要に応じて参照してみてください。(ただし「良いコード」ではない点に注意です!)
Firestore
Firestore は文字列や数値、日時など様々な型のデータを保存することができるデータベースです。
Sengyo アプリでは、主にユーザーの入力した投稿データを保存するデータベースとしてこの Firestore を利用しています。
SQL 脳から脱却する
そんな Firestore を使う上でまず知っておく必要があるのが、 Firestore は MySQL や Oracle のような RDBMS とは違い、 JSON 形式でデータを管理する NoSQL データベース であるということです。
NoSQL データベースを使ってすでに何かプロダクトを作ったことがある方にとっては当たり前のことかもしれませんが、表形式の RDB と JSON 形式の NoSQL ではデータ構造の設計方法や考え方が全く異なります。自分は NoSQL でプロダクトを作る知見も経験もほぼなかったため、まずはそこから調べる必要がありました。
ということでまず見つけたのが↓の動画です。
The Firebase Database For SQL Developers | Youtube
この動画では SQL を知っている開発者向けに NoSQL を説明しています。Firestore に限らず「NoSQL とは」を SQL と比較する形で説明してくれているので、アプリの SQLite やバックエンドの MySQL などを触ったことのある自分にとってはちょうど良い内容でした。
詳しくは動画を観ていただければと思いますが、ここでは
- RDB ではサービスの機能に依らずデータそのものに着目して正規化する一方で、 NoSQL ではサービスが一番必要とする形式のままデータを保存すること
- 同じデータが複数箇所に保存されることを怖がる必要はないこと
- SQL を NoSQL に置き換える例
など、 SQL 脳を脱却するための考え方を得ることができました。
動画も1つ数分で終わるライトなものなので、それほど身構えずに観られるのも良い点です。
Firestore という製品を知る
NoSQL データベースというジャンルについての理解ができたら、次は Firestore というプロダクトについて知る必要があります。
これも Firebase が公式で動画を出しているので、順番に観ていきます。
Get to Know Cloud Firestore | Youtube
Firestore は NoSQL の考え方をベースに、様々な機能や設計を取り入れています。例えば
- 「ドキュメント / コレクション」ベースのデータ管理
- 配列型とコレクションの使い分け
- リアルタイム アップデート
- オフライン時のデータアクセス
- セキュリティルール
などです。
上記の動画はこれらの機能をざっと把握するのにとても役に立ちました。若干スピーカーの英語が早口なのと、所々でちょっとしたジョークが入って置いていかれることがあったものの、この内容は Firebase のドキュメントに文章としてもまとまっているため、分からない部分はそちらで補完していけばだいたい大丈夫でした。
Flutter で Firestore を扱う
NoSQL の考え方と Firestore という製品がある程度できたら、あとは Flutter アプリから使ってみるだけです。
Flutter で Firestore を扱うための
cloud_firestore
プラグインが公開されているため、これを使います。単純なデータの出し入れの方法はパッケージのサンプルコードを見ればだいたい使い方は分かるようになっています。まずは自分の Google アカウントで Firebase プロジェクトを作って README の通りに動かしてみると良いでしょう。Firebase はちょっと触ってみる分には無料で使えるようになっています。
「参照型のフィールドを
setData()
したい場合にはDocumentReference
型のインスタンスを value として渡す」など、細々としたところで README には書かれていない部分もありましたが、実際に動かしながら確認すればそれほど難しいことはなかった記憶です。なお、Sengyo アプリでは Firestore のデータを出し入れするコードは以下のあたりです。
https://github.com/chooyan-eng/Sengyo/blob/master/lib/repository/article_repository.dart
何かの参考になれば。
Cloud Storage
Cloud Storage は静的なファイルの保存場所として使える製品です。Sengyo では画像ファイルの保存に利用しています。
Cloud Storage も Flutter 向けにパッケージが公開されていますので、 Firestore と同じようにサンプルコードを見ながら動きを確認すればそれほど大きな問題はないと思います。
Cloud Storage はフォルダを作ってファイルを保存するシンプルなもので、基本的には Firebase のドキュメントとパッケージの README を読めばある程度使えるようになっています。
Sengyo アプリでは以下のあたりのソースコードが参考になるかと思います。
https://github.com/chooyan-eng/Sengyo/blob/master/lib/repository/image_file_repository.dart
Firebase Authentication (メールリンク認証)
Firebase Authentication はユーザーの認証をするための仕組みを提供してくれる製品です。これを使うことでログイン機能が簡単に、安全に実現できます。
一言に「認証」と言っても、ドキュメントを見ると分かる通りその方法は様々で、シンプルな ID/パスワード で認証する方法から電話番号、SNS認証、また匿名認証というものも用意されています。
そのため、まずはそれぞれの認証方法に必要なユーザー情報やメリット・デメリットを理解した上で、サービスの要件(想定ユーザーの使いやすさ、セキュリティ要件など)に応じて利用する方法を取捨選択していく必要があります。
Sengyo アプリでは、管理コストの問題からなるべく少ない情報のやりとりでログインを実現したいことや、まずは分かりやすく1通りだけユーザーにログイン方法を示したかったため、メールリンク認証 を採用しています。
この記事でもメールリンク認証について書いていきます。
メールリンク認証とは
メールリンク認証は、ユーザーがアプリから入力した メールアドレスに対してログイン用リンクを送信 し、メーラーで リンクをタップすればアプリが開いてログインが完了する 、という仕組みです。
ユーザーが入力されたメールアドレスの持ち主であることを根拠に本人を認証する方法で、ユーザーとしてはパスワードを入力する必要もなく手軽に安心してアカウントを作成できる、というわけです。
このあたりは、 Firebase のドキュメントを読むことである程度イメージができるかと思います。
iOS でメールリンクを使用して Firebase 認証を行う | Firebase
Android でメールリンクを使用して Firebase 認証を行う | Firebase一方で実装面では若干手順が多くなるため、ひとつひとつ何が起きているのかを整理して理解する必要があります。
メールリンク認証の流れ
まずは、ユーザーがメールアドレスを入力したあと、裏でどのようなことが起こっているのかを理解します。
同じことは Firebase のドキュメントでも説明されていますが、実際の処理の流れに加えて Firebase コンソール上の設定方法なども織り交ぜて説明されているため、ここでは事前準備などは省いて実際のサービスで何が起きているか、に着目して流れをまとめてみます。
- ユーザーのメールアドレスにログイン用のリンクが含まれたメールを送信する
- ユーザーがメール内のリンクをタップして開く。
- アプリが開かれたリンクを拾ってアプリを起動する
- リンクに含まれるリクエストパラメータとメールアドレスをセットにして Firebase へ送信する
- Firebase がリクエストパラメータとメールアドレスのセットが正しいことを検証する
- 検証の結果がアプリに通知される
ここで、3の「開かれたリンクを拾ってアプリを起動する」を実現するために、もう1つ別の Firebase 製品である Firebase Dynamic Links を利用します。そのため、まずは Firebase Dynamic Links について学ぶ必要があります。
Firebase Dynamic Links とは
アプリには、ユーザーが特定の URL を開こうとした時にブラウザではなくアプリを起動する仕組みがあります。これを、 Android では ディープリンク、iOS では Universal Link と呼んでいます。
この仕組みをアプリに取り入れるためには、アプリ側だけでなくサーバー側にも「その URL の保有者とアプリの開発者が確かに同一であること」を証明するための設定が必要であったり、アプリがインストールされていなかった場合の挙動を定める必要があったり、またそれらの設定を Android / iOS それぞれのプラットフォームの仕様に従って行わなければならないなど、様々な労力が発生します。
Firebase Dynamic Links は、そのあたりの面倒な設定を Firebase コンソール上で一括で行ってくれるサービスです。
Firebase コンソール上の "Dynamic Links" メニューから上記のような設定項目(iOS / Android それぞれについての、リンクで起動するアプリやインストールされていない場合の挙動など)、トラッキングの有無などを設定して Dynamic Link を作成するだけなのでとても便利です。
ただしこれをメールリンク認証で使う場合の設定方法についてはメールリンク認証のドキュメント内に概要だけ書かれていたものの、具体的にどの項目をどう設定すれば良いかの記述は見つからなかったため、今回は割と雰囲気と試行錯誤で設定しました。
このあたりは追って別の記事で詳しくまとめてみたいと思います。
アプリ の実装
メール認証と Dynamic Links の設定が Firebase コンソール上でできたら、次にアプリの方もいろいろ実装する必要があります。
このあたりも先ほどの Firebase のドキュメントを見ながら、 iOS は XCode から
Associated Domain
の設定などを追加、 Android はAndroidManifest.xml
ファイルにIntent-Filter
を追加して、指定したドメインのリンクが開かれたときに、ブラウザではなく自分のアプリが開かれるようにしていきます。次に、「メールリンク認証の流れ」で書いた通り、アプリのプログラムとして
- メールを送信する処理
- Dynamic Link からアプリが開かれたことを判定する処理
- Firebase Authentication へリンクとメールアドレスを送信する処理
を実装します。
これも
firebase_auth
という Firebase Authentication 全般を Flutter で扱うプラグインが公開されているため、これを使います。ただし、メールリンク認証についてはサンプルコードや README では触れられていないため、 API ドキュメントやコードを読んで適切なメソッドを見つける必要があります。
FirebaseAuth class | firebase_auth
結論から言うと、アドレスを指定してログイン用のメールを送るためには
sendSignInWithEmailLink()
を、 Dynamic Link でアプリが開かれたあと FirebaseAuth にリンクとメールアドレスを送って認証するためにはsignInWithEmailAndLink()
メソッドを利用します。さらに、 Dynamic Link からアプリが開かれた際の処理については、
firebase_dynamic_links
というプラグインが別に用意されているため、こちらを使います。firebase_dynamic_links | pub.dev
基本的な使い方や考え方は README に書いてある通りです。
注意点としては、 Dynamic Link からアプリが開かれたとき、すでに裏でアプリが実行中で初期化処理などが実行されないパターンも考慮する必要がある、ということです。この点についてはサンプルコードでも
FirebaseDynamicLinks.instance.onLink
で Dynamic Link から起動された時に呼び出されるコールバックを初回起動時に登録しておくコードが書かれていますので、参考にすると良いでしょう。また、このサンプルコードはメールリンク認証で使うことを考慮したものではないため、うまく
signInWithEmailAndLink()
メソッドとつなげてあげることや、ログイン後の処理に自分のアプリの要件に応じてつなげてあげる必要があります。このあたりはサンプルコードも少なく試行錯誤した上で実装しましたが、一応 Sengyo アプリとしては以下の
LoginBloc
クラスにまとめたような実装になっていますので、何かの参考になれれば嬉しいです。https://github.com/chooyan-eng/Sengyo/blob/master/lib/bloc/login_bloc.dart
まとめ
以上、 Sengyo アプリを作るために利用した Firestore, Cloud Storage, メールリンク認証 をそれぞれ使うために参照したものや学んだことをまとめてみました。
細かなソースコードレベルの説明や具体的な設定の手順などは最初に書いた通り省いていますので、そのうちそれぞれについては別の記事として詳しく書いてみたいと思います。特にメールリンク認証についてはそのまま参考にできる記事などが全然見つからなかったため、優先的に書きたいところです。
Firebase と Flutter は、ちょっとした個人アプリをささっと作ろうと思ったときにとても役に立つ組み合わせだと思います。
初めて触る人にとっては馴染みのないものもあるかもしれませんが(もしかしたら最近は Firebase から入るような人も多いかもしれませんが)、この記事でまとめたように参考にできる記事や動画はたくさん公開されていますので、それらをひとつずつ見ながら実際に触ってみることでとりあえずひとつのアプリとして形にできる環境が整っています。
「それっぽい SNS アプリをとりあえず作ってみる」ために、何をどれくらい知る必要があるのか、この記事を読んでイメージが伝えられれば嬉しいです。
- 投稿日:2020-07-22T17:32:23+09:00
Amazon Kinesis Video Streams WebRTC をiOSとAndroid動かしてみた
はじめに
Amazon Kinesis Video Streams に ビデオチャットサービスが加わりました。
ブラウザ(JavaScript)向けのSDKだけでなく、組み込み用途のC言語SDKや、iOS/Androidといったモバイルアプリ向けのSDKも用意されており、ビデオチャットサービスを実装することができます。
今回は iOS, Android のサンプルを動かし、iOS <-> Android アプリ間でビデオ通話をしてみようと思います。Getting Started
iOS SDK
Android SDKAWSでリソースを作成する
AndroidのSDKのREADMEの手順が分かりやすかったので、それに倣って進めていきます。
Amazon Kinesis Video Streams Android WebRTC SDK README.md
シグナリングチャンネルを作成する
Kinesis Video Streams でシグナリングチャンネルを作ります。
CognitoUserPool を作成する
CognitoUserPool を作成します。
プール名を入力します。今回は[MyUserPool]としました。プール名を入力したら[デフォルトを確認する]を選択し、次のページでデフォルト値を変えずに[プールを作成]を選択します。
新しいアプリクライアントを作成します。まずアプリクライアント名を入力します。ここでは[MyAppClient]という名前で作成しました。
アプリクライアントの作成が完了したら[アプリクライアント ID]と[アプリクライアントのシークレット]をメモします。[詳細を表示]を選択すると表示されます。
CognitoIdentityPool を作成する
CognitoIdentityPool を作成します。
[認証プロバイダー]の[Cognito]のタブを開き、[ユーザープールId]と[アプリクライアントId]に先ほどメモをした値を入力し、作成します。
Cognito_MyIdentityPoolAuth_Role
とCognito_MyIdentityPoolUnauth_Role
の2つロールが作成されます、一つはログイン済みユーザーに付与されるロールで、もう一つはログインされていないユーザーに付与されるロールです。つまりログイン済みのユーザーはkinesisvideo
にアクセスできるように以下のポリシーをアタッチします。{ "Version":"2012-10-17", "Statement":[ { "Effect":"Allow", "Action":[ "cognito-identity:*", "kinesisvideo:*" ], "Resource":[ "*" ] } ] }Android
AWSのリソースの作成ができたので、Android のサンプルアプリをビルドしていきます
- Androidのサンプルプロジェクトをダウンロードします。
$ git clone https://github.com/awslabs/amazon-kinesis-video-streams-webrtc-sdk-android.git
AndroidStudio を起動し、 Open an existing Android Studio project で先ほどダウンロードしたサンプルプロジェクトを開きます。
/res/raw/awsconfiguration.json
にメモしたIdentityPoolId
,UserPoolId
,UserPoolAppClientSecret
,UserPoolAppClientSecret
,UserPoolAppClientId
を貼り付けます。IdentityPoolId
はリージョンも含めて入力する必要があるので気をつけてください。実機にインストールし、ユーザーを作成します。入力したメールアドレスに二要素認証のコードが送られてくるので間違えずに入力します。
ユーザーの作成とログインができたら [Start Viewer]でビデオチャットを始めます。
PCで Kinesis の demo-channel を開き、Master でビデオを流すと PC と Android アプリでビデオチャットができます。
カビゴンがPCのカメラで、エヴァ初号機がAndroidのカメラで撮ってます。
ブラウザ <-> Androidアプリ間でビデオ通信ができました。iOS
基本的な設定はAndroidで既に終わっているので、設定を追加してビルドします。
$ git clone git@github.com:awslabs/amazon-kinesis-video-streams-webrtc-sdk-ios.git $ cd amazon-kinesis-video-streams-webrtc-sdk-ios $ cd Swift $ pod install # もし cocoapods が入っていなかったらインストールが必要
awsconfiguration.json
を追加。awsconfiguration.json{ "Version": "1.0", "CredentialsProvider": { "CognitoIdentity": { "Default": { "PoolId": "YOUR_IDENTITY_POOL_ID", "Region": "ap-northeast-1" } } }, "IdentityManager": { "Default": {} }, "CognitoUserPool": { "Default": { "AppClientSecret": "YOUR_APP_CLIENT_SECRET", "AppClientId": "YOUR_APP_CLIENT_ID", "PoolId": "YOUR_USER_POOL_ID", "Region": "ap-northeast-1" } } }
Constants.swift
を開いて入力let CognitoIdentityUserPoolRegion: AWSRegionType = AWSRegionType.APNortheast1 let CognitoIdentityUserPoolId = "YOUR_USER_POOL_ID" let CognitoIdentityUserPoolAppClientId = "YOUR_APP_CLIENT_ID" let CognitoIdentityUserPoolAppClientSecret = "YOUR_APP_CLIENT_SECRET" let cognitoIdentityPoolId = "YOUR_IDENTITY_POOL_ID"ログインし、ビデオ通話を開始します。
先ほど作成したユーザーを使いまわしても大丈夫です。TODO: Twilio との料金の比較
参考にさせて頂いた記事
- 投稿日:2020-07-22T14:59:59+09:00
【Flutterの状態管理】テーマ切替&多言語化 〜provider,BLoC,reduxの三つで実現してみる①
Flutterの状態管理に関する記事の中、カウンター(Counter)を弄るのは多いと思いますが、カウンターの例では少し煩雑だと感じます。
本シリーズではアプリのテーマ(Theme)切替、多言語化を通じて、provider,BLoC,redux三つの実現方式でFlutterの状態管理を説明します。
一、providerでテーマ切替&国際化
provider: ^4.3.1 (providerパッケージをpub.devから取得)
1.テーマ切替
ハンバーガーメニューにあるテーマ色ボタンの押下により、グローバルにテーマ色を切替える。
1.1.状態クラス
_themeDataはテーマやカラーに使うが、_colorIndexは選択したカラーの状態の保存に使い、本文ではテーマやカラーの状態を端末側に保存する仕組みを設けていないけど、興味ある方は実践してみてください。
changeThemeData()はコアメソッド。実行したらnotifyListeners()を通じて、グローバルにアナウンスが起こり状態がほぼ遅延なく刷新される。
theme_state.dartimport 'package:flutter/material.dart'; class ProviderThemeState extends ChangeNotifier{ ThemeData _themeData;//テーマ int _colorIndex;//色ボタン選択中の状態を記録 ProviderThemeState(this._themeData, this._colorIndex); void changeThemeData(ThemeData themeData, int colorIndex){ this._themeData = themeData; this._colorIndex = colorIndex; notifyListeners(); } int get colorIndex => this._colorIndex; ThemeData get themeData => this._themeData; }1.2.管理する必要がある部分をラップする(wrap)
MultiProviderは複数のproviderを入れることができる。widgetをwrapperだけでなく他のコンポーネントでもラップやカバーできる。
main.dartimport 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:state_dancer/provider/theme_state.dart'; import 'home_page.dart'; void main() { runApp(Wrapper(child: MyApp())); } class Wrapper extends StatelessWidget{ final Widget child; Wrapper({this.child}); @override Widget build(BuildContext context) { final initThemeData = ThemeData( primaryColor: Colors.green, ); final initIndex = 1; return MultiProvider(providers: [ ChangeNotifierProvider(create: (_) => ProviderThemeState(initThemeData, initIndex)), ],child: child,); } } class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return Consumer<ProviderThemeState>(builder: (context,state,widget) =>MaterialApp( title: "状態管理Demo", theme: state.themeData, debugShowCheckedModeBanner: false, home: HomePage(), )); } }1.3.状態の使用、メソッドの実行
Provider.of(context)よりConsumerのほうは結構細かい対象を絞れるため、widget再ビルドの消費コストが削減される。また、ConsumerはwidgetのBuildContextが不要で使えるのも一大メリット。
ボタンの押下により、changeThemeData()が呼び出されConsumerの状態の状態が更新される。
home_page.dartimport 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'provider/theme_state.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; return Scaffold( appBar: AppBar( title: Text("Flutter状態管理",style: TextStyle(color: Colors.white),), ), drawer: Drawer( child: Consumer<ProviderThemeState>(builder: (context, state, widget) { return Column( children: <Widget>[ Container( width: screenWidth, height: statusBarH + naviBarH, color: state.themeData.primaryColor, ), SizedBox(height: 10.0), Row( children: <Widget>[ Padding(padding: EdgeInsets.all(10)), Wrap(children: <Widget>[ RaisedButton( color: Colors.green, onPressed: () { state.changeThemeData(ThemeData(primaryColor: Colors.green), 1); }, child: Text("緑",style: TextStyle(color: Colors.white),), shape: CircleBorder(), ), RaisedButton( color: Colors.red, onPressed: () { state.changeThemeData(ThemeData(primaryColor: Colors.red), 2); }, child: Text("赤",style: TextStyle(color: Colors.white),), shape: CircleBorder(), ), RaisedButton( color: Colors.blue, onPressed: () { state.changeThemeData(ThemeData(primaryColor: Colors.blue), 3); }, child: Text("青",style: TextStyle(color: Colors.white),), shape: CircleBorder(), ), ],), ], ), Divider(), SizedBox(height: 10,), Row(children: <Widget>[ Padding(padding: EdgeInsets.all(10)), SizedBox(width: 15,), RaisedButton( color: state.themeData.primaryColor, onPressed: () { }, child: Text("日本語",style: TextStyle(color: Colors.white),), ), SizedBox(width: 15.0,), RaisedButton( color: state.themeData.primaryColor, onPressed: () { }, child: Text("English",style: TextStyle(color: Colors.white),), ), ],), ], ); }), ), body: ListView( children: <Widget>[ Consumer<ProviderThemeState>(builder: (context, state, widget) { return Center( child: Column( children: <Widget>[ SizedBox(height: 10.0), Container( width: 200, height: 300, color: state.themeData.primaryColor, ), SizedBox(height: 10.0), Text( "学びて思わざればすなわち罔(くら)し、\n思いて学ばざればすなわち殆(あやう)し", style: TextStyle( color: state.themeData.primaryColor, fontSize: 18.0, fontWeight: FontWeight.bold), ), SizedBox(height: 30.0), FloatingActionButton(onPressed: (){ }, backgroundColor: state.themeData.primaryColor, child: Icon(Icons.check)), ], ), ); }) ], ), ); } }2.言語切替
flutter_localizations:
sdk: flutter (多言語をpubspec.yamlで配置)ハンバーガーメニューにある言語ボタンの押下により、グローバルに言語を切替える。
2.1.言語データを準備する
language_data.dartclass LanguageData { static final EN = { "title":"Flutter State Management", "greenBtn":"Green", "redBtn":"Red", "blueBtn":"Blue", "analects":"To learn without thinking is blindness, \nto think without learning is idleness (Confucius)." }; static final JP = { "title":"Flutter状態管理", "greenBtn":"緑", "redBtn":"赤", "blueBtn":"青", "analects":"学びて思わざればすなわち罔(くら)し、\n思いて学ばざればすなわち殆(あやう)し" }; }2.2.言語データを使用するためのクラス
i18n.dartimport 'package:flutter/material.dart'; import 'language_data.dart'; class I18N { final Locale locale; I18N(this.locale); static Map<String, Map<String, String>> _localizedValues = { "en": LanguageData.EN, "ja": LanguageData.JP, }; static I18N of(BuildContext context) { return Localizations.of(context, I18N); } get title { return _localizedValues[locale.languageCode]['title']; } get greenBtn { return _localizedValues[locale.languageCode]['greenBtn']; } get redBtn { return _localizedValues[locale.languageCode]['redBtn']; } get blueBtn { return _localizedValues[locale.languageCode]['blueBtn']; } get analects { return _localizedValues[locale.languageCode]['analects']; } }2.3.多言語のデリゲートクラス
I18nDelegate.dartimport 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'i18n.dart'; class I18nDelegate extends LocalizationsDelegate<I18N> { @override bool isSupported(Locale locale) { ///サポートする言語 return ['en', 'ja'].contains(locale.languageCode); } ///現在言語環境下の文字列をロード @override Future<I18N> load(Locale locale) { return SynchronousFuture<I18N>(I18N(locale)); } @override bool shouldReload(LocalizationsDelegate<I18N> old) { return false; } //グローバル静的デリゲート static I18nDelegate delegate = I18nDelegate(); }2.4.状態クラス
localeというフィールドしか使わない
locale_state.dartimport 'package:flutter/material.dart'; class LocaleState extends ChangeNotifier{ Locale _locale;//地域 LocaleState(this._locale); factory LocaleState.jp()=> LocaleState(Locale('ja', 'JP')); factory LocaleState.en()=> LocaleState(Locale('en', 'US')); void changeLocaleState(LocaleState state){ _locale=state.locale; notifyListeners(); } Locale get locale => _locale; //言語ゲット }2.5.状態の使用、メソッドの実行
Consumer() の他に Consumer2() から Consumer6() まであり、得たい状態値の種類数によって使い分けることができる。
2.5.1.多言語化の配置
main.dartimport 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:provider/provider.dart'; import 'package:state_dancer/provider/theme_state.dart'; import 'home_page.dart'; import 'provider/locale_state.dart'; import 'provider/I18nDelegate.dart'; void main() { runApp(Wrapper(child: MyApp())); } class Wrapper extends StatelessWidget{ final Widget child; Wrapper({this.child}); @override Widget build(BuildContext context) { final initThemeData = ThemeData( primaryColor: Colors.green, ); final initIndex = 1; return MultiProvider(providers: [ ChangeNotifierProvider(create: (_) => ProviderThemeState(initThemeData, initIndex)),//themeのprovider ChangeNotifierProvider(create: (_) => LocaleState.jp()),//localeのprovider ],child: child,); } } class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return Consumer2<ProviderThemeState, LocaleState>(builder: (_,themeState,localState,__) =>MaterialApp( title: "状態管理Demo", localizationsDelegates: [GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, I18nDelegate.delegate//言語デリゲート ], locale: localState.locale, supportedLocales: [localState.locale], debugShowCheckedModeBanner: false, home: HomePage(), )); } }2.5.2.多言語化の実行
main.dartimport 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'provider/locale_state.dart'; import 'provider/theme_state.dart'; import 'provider/i18n.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; return Consumer2<ProviderThemeState, LocaleState>(builder: (_,themeState,localState,__) =>Scaffold( appBar: AppBar( backgroundColor: themeState.themeData.primaryColor, title: Text(I18N.of(context).title,style: TextStyle(color: Colors.white),), ), drawer: Drawer( child: Consumer2<ProviderThemeState,LocaleState>(builder: (_,themeState,localState,__) { return Column( children: <Widget>[ Container( width: screenWidth, height: statusBarH + naviBarH, color: themeState.themeData.primaryColor, ), SizedBox(height: 10.0), Row( children: <Widget>[ Padding(padding: EdgeInsets.all(10)), Wrap(children: <Widget>[ RaisedButton( color: Colors.green, onPressed: () { themeState.changeThemeData(ThemeData(primaryColor: Colors.green), 1); }, child: Text(I18N.of(context).greenBtn,style: TextStyle(color: Colors.white),), shape: CircleBorder(), ), RaisedButton( color: Colors.red, onPressed: () { themeState.changeThemeData(ThemeData(primaryColor: Colors.red), 2); }, child: Text(I18N.of(context).redBtn,style: TextStyle(color: Colors.white),), shape: CircleBorder(), ), RaisedButton( color: Colors.blue, onPressed: () { themeState.changeThemeData(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: themeState.themeData.primaryColor, onPressed: () { localState.changeLocaleState(LocaleState.jp()); }, child: Text("日本語",style: TextStyle(color: Colors.white),), ), SizedBox(width: 15.0,), RaisedButton( color: themeState.themeData.primaryColor, onPressed: () { localState.changeLocaleState(LocaleState.en()); }, child: Text("English",style: TextStyle(color: Colors.white),), ), ],), ], ); }), ), body: ListView( children: <Widget>[ Consumer2<ProviderThemeState,LocaleState>(builder: (_, themeState, localeState,__) { return Center( child: Column( children: <Widget>[ SizedBox(height: 10.0), Container( width: 200, height: 300, color: themeState.themeData.primaryColor, ), SizedBox(height: 10.0), Text(I18N.of(context).analects, style: TextStyle( color: themeState.themeData.primaryColor, fontSize: 18.0, fontWeight: FontWeight.bold), ), SizedBox(height: 30.0), FloatingActionButton(onPressed: (){ }, backgroundColor: themeState.themeData.primaryColor, child: Icon(Icons.check)), ], ), ); }) ], ), ), ); } }まとめ
本文ではProviderでテーマや多言語の切替という多状態を管理してみました。Flutterの状態管理はandroid, iOSと違い、多分多くの方が悩み続けていると思いますね。Flutterの状態管理について、代表的にprovider,BLoC,reduxがありますので、次回ではBLoCパターンをご紹介していきたいと思います。
- 投稿日:2020-07-22T14:48:56+09:00
【Android 9.0 Pie Java】RecyclerViewの余白部分にsetOnTouchListenerを実装する
初めに
リスト表示に便利なRecyclerViewですが、デフォルトのままだと余白部分をタッチした際にイベントを発火させることができません。
RecyclerViewの余白部分をタッチしてソフトキーボードを閉じる為に実装を模索したので共有したいと思います。実装方法
まずRecyclerViewに
android:touchscreenBlocksFocus="true"を追記します。
あとはonCreateViewやonCreatedViewでリスナーを実装するだけです。
Fragment.javarecyclerView.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { // キーボードを隠す InputMethodManager inputMethodManager = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); inputMethodManager.hideSoftInputFromWindow(v.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); // 背景にフォーカスを移す RecyclerView recyclerView = view.findViewById(R.id.recyclerView); recyclerView.requestFocus(); } });以上です。
どなたかの参考になれたら幸いです。
- 投稿日:2020-07-22T14:48:56+09:00
【Android 9.0 Pie Java】RecyclerViewの余白部分にsetOnTouchListenerを実装してソフトキーボードを閉じる
初めに
リスト表示に便利なRecyclerViewですが、デフォルトのままだと余白部分をタッチした際にイベントを発火させることができません。
RecyclerViewの余白部分をタッチしてソフトキーボードを閉じる為に実装を模索したので共有したいと思います。実装方法
まずRecyclerViewに
android:touchscreenBlocksFocus="true"を追記します。
fragment.xml<LinearLayout android:id="@+id/scrollView" android:layout_width="0dp" android:layout_height="0dp" android:isScrollContainer="false" app:layout_constraintBottom_toTopOf="@+id/footerBorder" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/headerBorder"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recyclerView" android:layout_width="match_parent" android:layout_height="match_parent" android:touchscreenBlocksFocus="true" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" /> </LinearLayout>あとはonCreateViewやonCreatedViewでリスナーを実装するだけです。
Fragment.javaRecyclerView recyclerView = view.findViewById(R.id.recyclerView); recyclerView.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { // キーボードを隠す InputMethodManager inputMethodManager = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); inputMethodManager.hideSoftInputFromWindow(v.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); // 背景にフォーカスを移す recyclerView.requestFocus(); return false; } });以上です。
どなたかの参考になれたら幸いです。
- 投稿日:2020-07-22T14:02:23+09:00
ViewModel考察
本記事を書く経緯
ViewModelをよくこんな感じに使っていましたが
class MainActivity : AppCompatActivity() { private val viewModel: MainViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { ... } }これはある程度の「魔術」だと思って、
viewModels
の中で何をやっているのか、なんでActivityのライフサイクルを超えて生存できるのかを気にしていませんでしたが、Activity間で同じViewModelを共有したい要件に出会ったので、本格的にViewModel周りを調査してみました。ViewModelの生成
まずAndroid KTXを使わないでViewModelを利用する原始的な書き方を見てみます。
class MainActivity : AppCompatActivity() { private lateinit var viewModel: MainViewModel override fun onCreate(savedInstanceState: Bundle?) { // (Deprecated) viewModel = ViewModelProviders.of(this).get(MainViewModel::class) viewModel = ViewModelProvider(this).get(MainViewModel::class) } }Android KTXが出る前まではたぶん一番こういう書き方がよく見られます。
ただ、この書き方はデフォルトで用意されているViewModelProvider.Factory
を使用する前提になっているし、登場人物はまだ出揃っていないので、解説には不向きです。もうちょっとカスタマイズした書き方を見てみます。// 2. Activityは元からViewModelStoreOwnerを実装しています class MainActivity : AppCompatActivity() { private lateinit var viewModel: MainViewModel override fun onCreate(savedInstanceState: Bundle?) { // 1. ViewModelProvider(owner: ViewModelStoreOwner, factory: ViewModelProvider.Factory) val provider = ViewModelProvider(this, MainViewModelFactory()) viewModel = provider.get(MainViewModel::class) } } // 3. 実際にViewModelを生成するFactoryクラス class MainViewModelFactory : ViewModelProvider.Factory() { @Suppress("UNCHECKED_CAST") override fun <T : ViewModel> create(modelClass: Class<T>): T { // ViewModelを手動生成する return MainViewModel() as T } } class MainViewModel : ViewModel() { ... }上記のViewModelProvider、ViewModelStoreOwner、ViewModelProvider.Factoryは
ViewModel
の取得に関わる主要人物です。一個ずつ見ていきます。ViewModelStoreOwner
public interface ViewModelStoreOwner { /** * Returns owned {@link ViewModelStore} * * @return a {@code ViewModelStore} */ @NonNull ViewModelStore getViewModelStore(); }
ViewModelStoreOwner
はViewModelStore
を返す役割を持っています。ViewModel
の保存に関わっています。
ViewModelStore
は自体一種のHashMapで、ViewModel
を保存/返す役割を持っています。
ViewModelStoreOwner
が返すViewModelStore
は毎回違うのか、それとも同じViewModelStore
を返すかはViewModelStoreOwner
の実装次第です。
ActivityはこのViewModelStoreOwner
を実装しています。回転などで再生成されたActivityでも一律前と同じViewModelStore
のインスタンスを返すように実装されているので、Activityのライフサイクルを超えてViewModel
は生存できました。ViewModelProvider.Factory
public interface Factory { /** * Creates a new instance of the given {@code Class}. * <p> * * @param modelClass a {@code Class} whose instance is requested * @param <T> The type parameter for the ViewModel. * @return a newly created ViewModel */ @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass); }
ViewModelProvider.Factory
はViewModel
の生成に関わっています。
Factory
を使わなくても例えば普通にMainViewModel()
を呼んだら生成できますが、Factory
を通すことで 生成する/しない ・ 生成のタイミング をViewModelProvider
に移譲します。ViewModelProvider
public class ViewModelProvider { public ViewModelProvider(@NonNull ViewModelStoreOwner owner, @NonNull Factory factory) { this(owner.getViewModelStore(), factory); } public ViewModelProvider(@NonNull ViewModelStore store, @NonNull Factory factory) { mFactory = factory; mViewModelStore = store; } public <T extends ViewModel> T get(@NonNull Class<T> modelClass) { // mViewModelStoreに取得しようとしているViewModelがあればそのまま返す // なければmFactoryを使ってViewModelを生成した上で、mViewModelStoreに入れてあとに返す } }
ViewModelProvider
は大体デザインパターンでいうとBuilderパターンのDirectorの役割です。Factory
とViewModelStoreOwner
を制御してViewModel
を返します。
Factory
を使ってViewModel
を生成し、ViewModelStore
に入れる作業をするし、ViewModel
がすでにViewModelStore
に入っていたら生成しないで、直接ViewModel
を返します。Activityの
ViewModelStoreOwner
の実装回転などで再生成されたActivityでも一律前と同じ
ViewModelStore
のインスタンスを返すことで、Activityのライフサイクルを超えてViewModel
は生存できました。ではActivityはどうやって同じ
ViewModelStore
を返すのか、これはComponentActivity1内のgetViewModelStore
の実装を見ればわかります。
結論を先にいうと、getLastNonConfigurationInstance
とonRetainNonConfigurationInstance
を使って実現してました。class ComponentActivity extends androidx.core.app.ComponentActivity implements ViewModelStoreOwner, ... { public ViewModelStore getViewModelStore() { if (getApplication() == null) { throw new IllegalStateException("Your activity is not yet attached to the " + "Application instance. You can't request ViewModel before onCreate call."); } if (mViewModelStore == null) { NonConfigurationInstances nc = (NonConfigurationInstances) getLastNonConfigurationInstance(); if (nc != null) { // Restore the ViewModelStore from NonConfigurationInstances mViewModelStore = nc.viewModelStore; } if (mViewModelStore == null) { mViewModelStore = new ViewModelStore(); } } return mViewModelStore; } }mViewModelStoreがまだ初期化されていなければ、
getLastNonConfigurationInstance
からViewModelStore
を取得します。この
getLastNonConfigurationInstance
はあまり馴染みがないので、調べてみました。どうもViewModelより以前からある仕組みです。public Object getLastNonConfigurationInstance() public Object onRetainNonConfigurationInstance()Activityがdestroyされる前に
onRetainNonConfigurationInstance
は呼ばれます。この関数をオーバーライドして任意のオブジェクトを返せば、次にActivityが再生成されたときにgetLastNonConfigurationInstance
から完全同一オブジェクトを取得できます(そのオブジェクトは死なないです)。ComponentActivityはこの仕組を利用して、Activityがdestroyされる前に
ViewModelStore
をonRetainNonConfigurationInstance
で一旦退避させて、Activityが再生成されてgetViewModelStore
が呼ばれたときに同じオブジェクトを返すようにしました。この
getLastNonConfigurationInstance
とonRetainNonConfigurationInstance
は、今では使われない機能らしいですが、昔はそれなりに活躍していたらしいです。だが今はComponentActivity内ではonRetainNonConfigurationInstance
をfinal
にしているので、オーバーライドさせてもらえないです。
(代わりに名前が似ているド同等機能のonRetainCustomNonConfigurationInstance
とgetLastCustomNonConfigurationInstance
がありますが、この2つはfinal
になってないのでオーバーライドできますが、Deprecatedな関数なので、あえて使う必要がないでしょう。)結論
ViewModel
の生成はFactory
の責務で、ViewModelの保存・生存期間はViewModelStoreOwner
(ViewModelStore
)の責務です。Activity間で
ViewModel
を共有したければ、共通のViewModelStore
を持てばいいです。
そのViewModelStore
はせめてActivityより長生きしないとだめなので、Activityより長いライフサイクルを持つところにそのViewModelStore
を置くしかないです。つまり手取り早く
Application
に置くことになりそうです。(もしくはDaggerでViewModelStore
のシングルトンを注入します)
- 投稿日:2020-07-22T00:01:29+09:00
大学生が1週間でFlutterアプリを学んでリリースした過程(5日目)
こんにちはシオンです。
昨日デザインがやっと決まり、コードを書き始めたところまで行きました。
ここまで来ればあとはコードを勉強しながら決めたデザインのものを形にしていくだけです。
残す課題は時間だけ、期限までに終わらせるというのが今回のチャレンジで一番大事になってくるのでここまできたら何がなんでも間に合わせます。プログラミングの進捗としては昨日FlutterでHelloWorldと表示することに成功しました。このプログラミングを学ぶ人が絶対に最初に通るLv1の状態から今日あすの2日でコードを書き切って、ラスト1日でリリースしてみせます。
少し話は変わりますが、qiitaではこういう投稿はあまり良くないようですね。
qiitaについて深く理解せずブログのような投稿をしてしまいました。この一連の投稿が終わったら改めたいと思います。ではやっていきます。
■まず画面をデザイン通りに構成する
コードを書き切ると言っても、本当にFlutterに対して無知なので闇雲に書いて行っても終わりが見えてきそうにないです。
ちなみにどのくらい無知なのかというと
現在私が持っているFlutterの知識は、
Flutterとは「フルッター」ではなく「フラッター」と読み、表記はアルファベットで構成されている。また、今日の私の朝ごはんは食パンにバターを塗ったトーストであるが、私が持つFlutterの知識と私の今日の朝食については全く関係がない。書くことがなさすぎて今日の朝食を紹介するしかないくらい無知ということです。よろしくお願いいたします。
ということで、一旦画面遷移などの処理は後回しにしてデザインの画面を形だけ再現してそのあとにカウントアップだったりの処理を書いていきたいとおもいます。
ちなみにデザインはこれです。(昨日のです。)
この画面を作っていく上で必要になってくるのは構成部品とそのレイアウトの知識です。
今回必要になってくる部品としては
・テキスト
・テキスト入力フォーム
・ボタン
・画像
この4つの部品があれば大丈夫そうです。では次にFlutterでのレイアウトの仕方を学んでいきます。
昨日Flutterでプログラミングしていく上で重要になってくるのがウィジェットというものだというのを学びました。
おそらくこのウィジェットの中に今回使う、画像だとかボタンだとかを持ったウィジェットがいるはずなので、まずはレイアウトは考えず必要な部品を
表示してみます。また3画面目から先はシンプルな構造ですぐに構成できると思うので、これは処理を書きながらにしていきます。
では1画面目と2画面目をやっていきます。
■1画面目と2画面目の外観を実装
1画面目はこんな感じに。なかなか時間がかかりました。
初めての言語でコードを書くときは勝手がわからない+関数やクラスの名前がわからない、で苦戦するのですが、その分うまく行ったときの「できた!!」っていう快感がたまらないですね。新しくゲームを始めてレベル1からスタートしている感じがしてすごく楽しいです。早くこの一週間チャレンジというチュートリアルをクリアして冒険に出かけます。
2画面目は内容入力のフォームの高さ調節が思うようにいかずこんな感じになってしまいました。
調べながらやっているのですが、まだまだFlutter自体が新しい分日本語記事が少ない印象ですかね。それでもわかりやすコードや使い方を記事にしてくださっている方が何人もいて助かりました。(本当にありがとうございます。)まだ少ししか触ってませんが、Flutterめちゃくちゃいいです。
この2画面を作っただけでもそのコードの書きやすさにびっくりしています。
まだ記事が少ないので初めてプログラミングを学ぶ人にとっては多少難しいかもしれませんが、何かしらやったことがある人にはお勧めしたいです。■まとめ
あとはこの画面に処理を記述していけばなんとか形にはなりそうです。
処理も一通り見たのですが、これくらいの画面遷移とカウントアップだけならすぐにできそうなので明日一気に仕上げてしまいます。なんとなく形になりそうな気配がしてきました。
でも残すは明日と明後日のみ。。。
果たして結果は完成するのかしないのか。残り2日間やっていきます。