- 投稿日:2020-08-12T22:02:43+09:00
Xamarin.FormsでAndroidアプリ開発② サンプルアプリ作成
前回インストールしたVisual Studioでサンプルアプリを作っていきます。
Xamarin.Formsのソリューションの立ち上げ
ソースコードの変更
まずMainpage.xamlに少しコードを追加してみます。下図のように、以下のコマンドを追加します。
<Button Text="おしてね" Clicked="OnButtonClicked" />
これで、「おしてね」と書かれ、クリックするとOnButtonClickedクラスを呼び出すボタンが作成されます。デザイン画面にします。なぜかうまく反映されないので、例えばiOSの画面にした後にAndroidの画面に戻す、などをすることで追加されたボタンが表示されます。
次にMainpage.xaml.csを開き、以下のコマンドを追加します。
private void OnButtonPressed(object sender, EventArgs e) { (sender as Button).Text = "こんにちは"; }シミュレーションの実行
関連記事
- Xamarin.FormsでAndroidアプリ開発① Visual Studioのインストール
- Xamarin.FormsでAndroidアプリ開発② サンプルアプリ作成(今日はここです)
- Xamarin.FormsでAndroidアプリ開発③ 開発したアプリをスマホで実行。躓きポイントは、共有ランライム!
- 投稿日:2020-08-12T17:33:01+09:00
Xamarin.FormsでAndroidアプリ開発① Visual Studioのインストール
前回はAndroid Studioを使ったアプリの開発を紹介しましたが、将来的にWindowsやiOSでの開発も考えると、Xamarinでの開発がお勧めです。
まだどのようなものかあまり分かっていませんが、チャレンジしていこうと思います。Visual Studioでのアプリ開発
Xamarinを使う開発ソフトとして、MicrosoftのVisual Studioを使います。開発言語はC#です。
MicrosoftはWindowsというイメージですが、最近はどんどんオープンソース化されてきていて、今はいろんなOSで動くアプリが開発できます。
少し歴史を紐解くと、.Net Framework : Windowsで動くアプリが動くフレームワーク
↓
.Net Core : Windowsだけでなく、Linux、Macで動くフレームワーク
↓
Xamarin : Android/iOSで動くフレームワーク
↓
(将来).Net 5 : Windows/Linux/Mac/Android/iOSすべてで動くフレームワーク
※現在プレビューは出ていますが、まだAndroidは使えなさそうです。2020/11登場とのことという流れで、どんどん良い時代に向かっています。
まずはVisual Studioのインストール
ということで、まず環境を用意していきます。商用利用であれば有償版が必要ですが、今回は勉強用ですので無償版のVisual Studio Communityを入れます。
- https://visualstudio.microsoft.com/ja/downloads/ にアクセス
- コミュニティをダウンロードし、exeファイルを実行
今回はインストールまでの紹介でした。
次回からアプリ開発を行っていきますので、乞うご期待。関連記事
- Xamarin.FormsでAndroidアプリ開発① Visual Studioのインストール(今日はここです)
- Xamarin.FormsでAndroidアプリ開発② サンプルアプリ作成
- Xamarin.FormsでAndroidアプリ開発③ 開発したアプリをスマホで実行。躓きポイントは、共有ランライム!
- 投稿日:2020-08-12T16:14:28+09:00
Androidアプリのアップロード鍵.keystoreの作成と管理について
Androidの署名ファイル(keystore)について個人的まとめ。
keystoreとは
今回作成するkeystoreファイルはアップロード鍵と呼ばれるもの。
アップロード鍵: Google Play アプリ署名用にアップロードする前に、App Bundle または APK への署名に使用する鍵。アップロード鍵は非公開にする必要があります。
非公開にしなければならないため、Androidプロジェクトディレクトリ配下に置いてあると、うっかりGitHubにプッシュしてしまったりと危険な香りがします。
そのためローカル(
/Users/user/.gradle
)に格納し、アプリのビルド時もそこから参照するように設定します。環境
- Windows 10 Pro
手順
/Users/user/.gradle/gradle.propertiesにパスを記述する
あらかじめ
Keys.repo
という名前でパスを定義しておきます(名前は任意)。
gradle.properties
が無い場合は作成してください。/Users/user/.gradle/gradle.propertiesKeys.repo='/Users/user/.signing'.gradle/.signingフォルダを作る
パスワード等を記述した
projectname.properties
を格納するため、.signing
というディレクトリを作成します(projectname
部分は任意)。
さらに、直下にkeystore
を格納するためのディレクトリを作成します。$ mkdir .gradle/.signing $ New-Item -Type File projectname.properties # touchと同じ $ mkdir .gradle/.signing/YourProjectName # keystoreを格納。名前は任意keystoreファイルを生成する
Android Studioを起動し、Build -> Generate Signed Bundle / APKをクリックします。
選択はAPKでもBundleでも、どちらでも構わないようです。
今回はAPKを選択し、Nextをクリックします。次に、Create new...をクリックしてkeystoreの作成先を指定します。
keystoreは作成したら
.signing
に移動させるので、プロジェクトルート直下等適当な場所で構いません。
ファイル名はrelease.keystore
としておきます。パスワードを設定し、Certificateの少なくともひとつのボックスに入力したらOKをクリックします。
そうしたらNextをクリックし、次の画面でFinishをクリックすれば、指定したディレクトリにrelease.keystore
が作成されます。keystoreを.signing/YourProjectNameに移動する
release.keystore
を.signing/YourProjectName
ディレクトリに移動します。$ mv ProjectRoot/release.keystore /Users/user/.gradle/.signing/YourProjectNameパスワードをprojectname.propertiesに記述する
release.keystore
を作成するときに設定したパスワード等を.signing/projectname.properties
に記述します。.signing/projectname.propertiesRELEASE_STORE_FILE=/YourProjectName/release.keystore RELEASE_STORE_PASS=xxxxx RELEASE_ALIAS=xxxxx RELEASE_KEY_PASS=xxxxxbuild.gradleに署名付きビルドのためのコードを記述する
appレベル
build.gradle
のandroidブロックにsigningConfig
を記述します。appレベルbuild.gradleandroid { signingConfigs { debug {} release { if (project.hasProperty("Keys.repo")) { def projectPropsFile = file(project.property("Keys.repo") + "/projectname.properties") if (projectPropsFile.exists()) { Properties props = new Properties() props.load(new FileInputStream(projectPropsFile)) storeFile file(file(project.property("Keys.repo") + props['RELEASE_STORE_FILE'])) storePassword props['RELEASE_STORE_PASS'] keyAlias props['RELEASE_ALIAS'] keyPassword props['RELEASE_KEY_PASS'] } } else { println "=======================================================" println "[ERROR] - Please configure release-compilation environment - e.g. in ~/.signing directory" println "=======================================================" } } } }以上です。
参考
ほぼ翻訳先。
gradle set absolute path value for a keystore file
他にも書き方いろいろ。
Android Studio(Gradle)でapkファイルを作成する時にstorePassword/keyAlias/keyPasswordの指定方法をいくつか検証してみた。
- 投稿日:2020-08-12T14:59:51+09:00
HMSとAppGalleryについて
背景
本番の説明の前に、まずは背景を簡単に紹介したいと思います。
去年5月以降に発売されたHuaweiのスマートフォンではGMSのサービスが使えなくなりました。
その影響で、Mate30 ProやP40シリーズなどのHuaweiのスマートフォン上でGoogle MapやChromeなどのGoogleオリジナルアプリとGMS、firebaseのSDKが使用不可になっています。(念のために説明しますが、それらのスマートフォン上で現在でもYoutube、Google Mapなどのサービスはブラウザ上であれば使用可能ですので、完全に使えないわけではありません)
その後、Huaweiは独自のHMSサービスを提供し始めました。現在、グローバルではHMSのアクティブユーザーがすでに4億人を超えていますが、日本ではまだあまり知られていないようですのでここで紹介したいと思います。HMSとは
HMSとはHuawei Mobile Serviceの略称で、Huaweiオリジナルアプリと開発者向けSDK群の総称です。
Huaweiオリジナルアプリとは、
- AppGallery(アプリストア)
- Huaweiブラウザ
- Huawei音楽
- Huaweiビデオ
- テーマ
などのHuaweiが提供しているアプリのことであり、これらのアプリはすでに170ヵ国以上に展開され、グローバルMAU(Monthly Active User)は4億人を超えています。
(引用元:https://k-tai.watch.impress.co.jp/docs/news/1256381.html)また、HMSが提供しているSDKはおもにHMS Coreと呼ばれ、現在ではバージョン5(HMS Core 5.0)まで更新されています。HMS Core 5.0は、ARエンジン、AIエンジンやCGキットなど、幅広いサービスを世界中の開発者に提供し、HMS Coreを使用したアプリ数は6万を超えています。
(参考元:https://www.agara.co.jp/article/72533)今回はSDK群のほうを重点的に紹介したいと思います。
開発者向けSDK群の紹介
HMSでは現在、主に以下の2つのSDK群を開発者に提供しています。
1. HMS Core
2. AppGallery Connect
以下にこの2つのサービスの概要をそれぞれ紹介します。HMS Core(公式サイト)
HMS Coreはアプリの開発、成長とマネタイズに必要な基本機能をカバーするSDK群で、イメージとしてはGMSのcom.google.android.gmsから始まるパッケージ群とほぼ同等であるかと思います。
例えば、以下のような対応関係があります。
GMSのサービス HMSのサービス location Location Kit maps Map Kit In-app billing In-App Purchases auth Account Kit safetynet Safety Detect 具体的には、現在HMS Coreは以下の40個以上のSDKを提供しています。
この中には、LocationやIn-App Purchasesなどの基本機能はもちろんありますが、特に気になるのはHuawei HiAI EngineやCamera KitなどのHuawei独自のSDKです。
Camera Kitを例として説明しますと、Huaweiのスマートフォンは非常にいいカメラ機能を有していますが、Camera Kitを使用することで、50倍ズームや非常に暗い環境でも鮮明に撮影できる機能などのスマートフォンのカメラ性能をフル活用できるようになります。
このように、一部のHMS CoreのSDKを使用することでHuaweiスマートフォンの素晴らしい端末性能をアプリ内で最大限に活用できるようになりますので、非常に魅力的だと思います。また、今後は定期的にHMS CoreのSDKを紹介していく予定がありますので興味のある方はぜひご覧ください。
AppGallery Connect(公式サイト)
AppGallery Connectはfirebaseと同等なサービスを提供しています。
いくつかの代表的なfirebaseのサービスとの対応関係は以下のようになります。
firebaseのサービス AppGallery Connectのサービス Crashlytics Crash Authentication Auth Service In-App Messaging App Messaging Remot Config Remote Configuration Functions Cloud Functions 現在AppGallery ConnectはAutu Service, Cloud Functions, Cloud DB, Cloud Storage, Cloud Hosting,
App Signing, Dynamic Ability, Crash, APM, Cloud Testing, Cloud Debug, Open Testing,
A/B Testing, Remote Configuration, App Linking, App Messaging, Connect API, AppGalleryKit,
Paid Apps, Device ID Serviceの合計20個のサービスを提供し、必要なSDKは全部揃っている状況だと言えるかと思います。AppGalleryの紹介
最後にAppGalleryを簡単に紹介したいと思います。
AppGalleryは上述のように、Huaweiオリジナルアプリの1つであり、Huaweiユーザー用のアプリストアのことです。
まだ日本ではあまり広く知られていないかもしれませんが、実はAppGalleryはすでにGoogle PlayとApp Storeに継ぐ世界3番目にユーザー数が多いアプリストアになっているらしいです。
AppGalleryでは、通常のアプリ以外に、QuickAppというインストール不要で直接使えるアプリも提供しています。
(引用元:https://k-tai.watch.impress.co.jp/docs/news/1256381.html)日本のAppGalleryでは現在LINE、楽天、NAVITIME、メルカリなどの身近なアプリがすでにダウンロードできるようになっています。もちろん一部のアプリがまだ載っていませんが、今後の発展に期待できるでしょう。
AppGallery用アプリを開発するには
既存のAndroidアプリは意外と簡単にAppGallery対応に改修できます。こちらの詳細についてはまた後日に紹介したいと思います。
- 投稿日:2020-08-12T14:47:32+09:00
【Android/Kotlin】フラグメントのライフサイクルまとめ
フラグメントのライフサイクル
まず、フラグメントとは「サブアクティビティ」的なポジションである。アクティビティが成り立っている上で、フラグメントは存在している。ライフサイクルについても同じような関係にあり、アクティビティのライフサイクルが破壊されれば、フラグメントのライフサイクルも破壊されてしまう。あくまでもアクティビティあってのフラグメントである、という事が重要だ。フラグメントとアクティビティのライフサイクルを表した図が以下。
参照:lifecycle:Activty/Fragment
この図は非常に分かりやすく両者のライフサイクルを理解することが出来る。今回は、この図のメインでもある、フラグメントの各コールバックについて簡単に説明をする。フラグメント生成編
これより以下はフラグメントが生成されるときに呼び出されるコールバックメソッドの説明となる。各メソッドにLogを仕込んでいるのは、最終的にライフサイクルを可視化させたいからなので、特に深い理由はない。
onAttach
フラグメントのライフサイクルにおいて、最初のメソッド。フラグメントとアクティビティ(コンテクスト)が関連付けられた時に呼ばれる。引数はアクティビティではなく、コンテクスト。他のメソッドにも共通する事であるが、フラグメントは基本的にアクティビティを参照するのではなく、コンテクストを参照する。
override fun onAttach(context: Context) { Log.d(TAG, "onAttach: called") super.onAttach(context) }onCreate
フラグメントの初期化を行う。アクティビティのonCreateと同じく、条件付きで呼ばれる(ローテーションの時は呼ばないなど)。このメソッドと同時にアクティビティのonCreateが呼ばれると誤解しがちだが実際そうでは無い。フラグメントでこのメソッドが呼ばれている間、アクティビティは絶賛作成中である。
override fun onCreate(savedInstanceState: Bundle?) { Log.d(TAG, "onCreate: called") super.onCreate(savedInstanceState) }引数にはバンドルをとる。アクティビティではonCreateにコードを書くことが多いものの、フラグメントではそうではない。アクティビティと異なりsetContentViewメソッドが無いため、レイアウトのウィジェットの参照や作成が出来ないからである。ロジックを書いていくのはまた別のメソッドである。
onCreatedView
フラグメントにビューが存在する場合、それを作り出すのがこのメソッド。Fragmentを拡張した時にデフォルトで実装される。レイアウトを膨らませる事が役割である。
override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { // Inflate the layout for this fragment Log.d(TAG, "onCreateView called") return inflater.inflate(R.layout.fragment_main, container, false) }返り値はViewであるが、ビューを持たないフラグメントの場合、nullが返される。そのためBundleはNullable。
onViewCreated
アクティビティの
onCreate
に相当するコードや、動的にフラグメントのビューを変更する時このメソッドが使われる。onCreateViewでそれを行っても構わないが、通例でonViewCreatedにて行われることが多い。override fun onViewCreated(view: View, savedInstanceState: Bundle?) { Log.d(TAG, "onViewCreated: called") super.onViewCreated(view, savedInstanceState) }onActivityCreated
このメソッドはアクティビティが完全に作成され切ったタイミングで呼ばれる。具体的には、アクティビティがonCreateを実行しきったタイミング。
override fun onActivityCreated(savedInstanceState: Bundle?) { Log.d(TAG, "onActivityCreated: called") super.onActivityCreated(savedInstanceState) }従来までこのメソッドにロジックを書くのが主流であったが、最近になって非推奨になった。onActivityCreatedに書かれていた内容はonViewCreatedに移すこと。
onVewStateRestored
フラグメントのUIに最終的な変更を加える。最終的な変更というのは、ローテーション以前のEditTextの状態など。
override fun onViewStateRestored(savedInstanceState: Bundle?) { Log.d(TAG, "onViewStateRestored: called") super.onViewStateRestored(savedInstanceState) }このメソッドのおかげで、アクティビティと異なりフラグメントは自動で状態の保存と復元を行ってくれる。具体的には、アクティビティであればローテーション後にビューの状態を復元するにはひと手間掛かっていたが、フラグメントはこのメソッドのお陰でそれを自動化してくれている。
onStart
フラグメントがユーザーにビューは表示されるものの、完全には機能していない状態の時このメソッドが呼ばれている。
override fun onStart() { Log.d(TAG, "onStart: called") super.onStart() }アクティビティのonStartと同じ感じ。
onResume
アクティビティとフラグメントの実行を完了し、ユーザーに応答している状態。
override fun onResume() { Log.d(TAG, "onResume: called") super.onResume() }このメソッドはアクティビティとフラグメントで同時に実行される。(それぞれのonResumeが実行されている)
フラグメント破壊編
これより下は、フラグメント破壊されるときのコールバックメソッドの説明である。
onPause
ユーザーが別のタスクに切り替えた時に呼ばれる。この時点でユーザーはフラグメントから離れているが、破壊はされていない。通常はここで保存すべき情報を保存しておく。
override fun onPause() { Log.d(TAG, "onPause: called") super.onPause() }このメソッドはアクティビティとフラグメントのそれぞれで同時に実行される。
onSaveInstanceState
このメソッドは必ずしもonPauseの後に呼ばれるとは限らない。基本的にはアクティビティのものと同じで、破壊編の中ならいつでも呼び出すことが可能。
override fun onSaveInstanceState(outState: Bundle) { Log.d(TAG, "onSaveInstanceState: called") super.onSaveInstanceState(outState) }このメソッドがあるという事は、onRestoreInstanceStateメソッドもあると思いがちだが、フラグメントにそれは存在しない。その代わり、引数でバンドルを受け取れる生成編のメソッドであれば、どこでも受け取ることが出来る。
onStop
このメソッドはフラグメントが停止状態になる時呼ばれる。この時フラグメントは、表示こそされていないものの、破壊までは至っていない。
override fun onStop() { Log.d(TAG, "onStop: called") super.onStop() }もしこの時点であってもユーザーが戻ってくれば、onResumeが呼ばれる。その場合でもフラグメントは状態を保持してくれている。
onDestroyView
onCreateViewによって膨らまされたビューを引きはがす時に呼ばれる。onCreateViewがnullを返したか否かに関わらず呼ばれる。
override fun onDestroyView() { Log.d(TAG, "onDestroyView: called") super.onDestroyView() }onDestroy
フラグメントが完全に使用されなくなった時に呼ばれる。フラグメントをクリーンにする。
override fun onDestroy() { Log.d(TAG, "onDestroy: called") super.onDestroy() }アクティビティのonDestroyの前に呼ばれる。
onDettach
フラグメントがアクティビティから切り離されるときに呼ばれる。
override fun onDetach() { Log.d(TAG, "onDetach: called") super.onDetach() }このメソッドもアクティビティのonDestroy前に呼び出される。
各コールバックメソッドの可視化
今回説明した各メソッドをそのままフラグメントに張り付けてやるだけで、ライフサイクルの遷移の様子がログに出力される。
同時にアクティビティのライフサイクルも可視化させてやれば、それぞれの細かい境界についても理解することが出来るのでお勧めだ。
- 投稿日:2020-08-12T11:23:39+09:00
setFragmentResultListenerを使った通知をするDialogFragmentの実装提案
androidx.fragment:fragment:1.3.0-alpha04
から新しくsetFragmentResult / setFragmentResultListener
が追加され、setTargetFragment
がdeprecatedになりました。おそらく多くの呼び出し元に通知を送る
DialogFragment
では、setTargetFragment
を使ってListenerなどで実装していたのではないでしょうか。従来のDialogFragment.ktclass MyDialogFragment : DialogFragment() { companion object { private const val REQUEST_CODE = 100 fun show(target: Fragment) = newInstance().run { setTargetFragment(target, REQUEST_CODE) show(target.requireFragmentManager(), "MyDialogFragment") } private fun newInstance() = MyDialogFragment() } private lateinit var listener: MyListener override fun onAttach(context: Context?) { super.onAttach(context) listener = targetFragment as? MyListener ?: throw IllegalArgumentException("This targetFragment is not `MyListener`") } ・・・ ・・・ ・・・ interface MyListener { fun onClickHoge() fun onDismiss() } }だいたいこんな感じかと思うんですが、
もちろんsetTargetFragment
もrequireFragmentManager
も使えないので、
以下のような感じにすると、これまでの使用感をあまり変えずに、実装できるのではないでしょうか。提案するDialogFragment.ktclass MyDialogFragment : DialogFragment() { companion object { private const val REQUEST_KEY = "request_key_my_dialog" private const val RESULT_KEY_HOGE = "result_key_hoge_my_dialog" private const val RESULT_KEY_DISMISS = "result_key_dismiss_my_dialog" fun show( target: Fragment, onClickHoge: (() -> Unit)? = null, onDismiss: (() -> Unit)? = null ) { newInstance().run { target .childFragmentManager .setFragmentResultListener(REQUEST_KEY, target.viewLifecycleOwner) { requestKey, bundle -> if (requestKey != REQUEST_KEY) return@setFragmentResultListener when { bundle.containsKey(RESULT_KEY_HOGE) -> onClickHoge?.invoke() bundle.containsKey(RESULT_KEY_DISMISS) -> onDismiss?.invoke() } } show(target.childFragmentManager, "MyDialogFragment") } } private fun newInstance() = MyDialogFragment() }まず、
setFragmentResultListener
にすることで、カスタムリスナーは必要なくなり、
onAttach
での処理も全て必要なくなります。ちなみに結果を通知する際は、例に倣って以下のように行います
// 通知したい場所で setFragmentResult(REQUEST_KEY, bundleOf(RESULT_KEY_DISMISS to ""))従来のDialogFragmentを使用する場合は、
// Fragmentで呼ぶ場合を想定 MyDialog.show(this)こんな感じでshowして、通知に関しては、Fragmentにリスナーを実装していたと思いますが、
MyDialog.show( target = this, onClickHoge = { // Hogeの処理 }, onDismiss = { // Dismissの処理 } )といった感じで、ラムダを使用して、カスタムリスナーを使用しなくても、
通知を実装することができ、とてもシンプルになります。いかがでしょうか?
何かもう少しこうしたら良いなど、ご意見あればお願いいたします。
- 投稿日:2020-08-12T09:21:30+09:00
スマホもPCの一員としてファイル同期したいじゃない?Ⅱ
おはようございます!
前回の
「Syncthingに似てる気がするけど知ってる?参考になるかもよー見てみたら??」
にスマホアプリも追従させてみたよ!って記事です。
やたらスマホ!
要するにHTTP(S)使ってフォルダを同期してくれる君というやつです。
Syncthing以外にもNearby Shareもあるじゃーんってご指摘あるな。
でも、これ強みとしてはバイナリいっこで動くし、マルチOS対応だし、、あと今あるプロキシとかHTTPエコシステムに乗っかれるのがこのツールの推しポイント
DE・KI・TA
お告げ通り機能追加しましたYO
- ディレクトリのあるフォルダでも再帰的に同期
- 前回同期時の設定をコンフィグに出して、次回はそれ読んで起動
- 同期状況を表示する
- HTTPS同期をサポート
詳しくはリポジトリ見てくださいな。releaseの.apkをもってくればインスコできます。
2が付いてないリポジトリは非互換なので注意です。
つかいたい!
- PC版を起動したら、EnterかSpaceを押すとQRコードが表示されます
- それをスマホ版アプリで読み取れば同期が開始されます
UIわからん!
①は同期ゲージです。この数字が何秒毎に同期するかになります。同期間隔が狭いとデカいファイルが転送しきれないのでその点はご注意を
②同期先を変えるときなど。QR Codeをもう一度読みましょう
③保存先フォルダを変更します
④は同期した時にサーバーに無いのに、クライアントにあるファイルを消すモードです。完全同期しておきたい時に使います。
⑤アプリを終了して設定をコンフィグファイルに書き出します
⑥は同期状況とか動作ステータスが色々でます
あとがき
GUI、転送効率化プロトコル実装、中継サーバーとかsyncthingにある機能で
実装出来てないとこはあるけど、その他の最低限は盛り込めたかな?Win、Mac以外にもラズパイとか色々持ってる人は便利だと思うので
是非使ってみてくださいな。(そんな人のニッチだよ!ってw)
- 投稿日:2020-08-12T09:05:01+09:00
GoFのデザインパターンまとめ
これは何
今まで断片的に知っていたデザインパターンを改めて勉強し直したのでそのまとめです。
23個のデザインパターン
1. 生成に関するパターン
Abstract Factory
抽象的な部品から抽象的な製品を作成する、抽象的な工場のパターン。
ある製品の製造過程を抽象化するイメージ。
あらかじめ、工場、部品、完成品に対応する抽象クラスを作成しておき、
それらすべての抽象クラスを継承した工場、部品を使って具体的な完成品を作成する。Builder
インスタンスの初期化をカプセル化するパターン。
Builderインスタンスは初期化したいインスタンスをフィールドに持ち、
外部からBuilderインスタンスのメソッド経由でインスタンスの初期化を行う。
比較的よく目にするパターン。Factory Method
インスタンスの生成が外部の状態に依存していたり、
生成が外部の状態を変化させる時に便利なパターン。
Factoryクラス内で状態を管理しておき、
Factory Method経由でインスタンスを生成するようにする。Prototype
インスタンスをコピーして新しいインスタンスを作成するパターン。
一つのクラスから多様なインスタンスを繰り返し生成する時や、
インスタンスの生成をプログラム的に行うのが困難な時に利用できる。Singleton
プログラム全体を通じて、インスタンスが一つしか生成されないように制限するパターン。
よく目にする。2. 構造に関するパターン
Adapter
データを供給するクラスと、消費するクラスの橋渡しをするパターン。
Adapterパターンはさらに、継承のパターンと委譲のパターンの二つに分けられる。
継承のパターンではAdapterクラスがデータ供給クラスと消費側に向けたのインターフェースの二つを継承する。
委譲のパターンでは、Adapterクラスは消費側に向けたインターフェースを継承し、データ供給インスタンスをメンバ変数として保持。データの取得は委譲を利用して行うというパターン。
AndroidでいうとRecyclerViewのAdapterは譲渡のAdapterパターンといえる。Bridge
クラスの継承ツリーを、機能の分岐先と実装の分岐先に分けるパターン。
あるクラスから抽象化したい処理を別の新しいクラスに抽出、委譲することで、
機能を追加したいときは元のクラスを継承、抽象化された処理を実装したい時は、
別の新しいクラスを継承することで、継承ツリー内で機能の階層と実装の階層を
分けることができる。Composite
容器と中身を同一視するパターン。
主にツリー構造のデータに対して、中間のノードと葉のノードを区別せずに扱いたい時に利用する。
AndroidでいうとViewとViewGroupがこのパターン。
ViewGroupはViewを継承しているが、Viewのリストを保持している。Decorator
中身と装飾を同一視するパターン。
あるクラスに対して、再帰的に機能を追加でき、かつ機能を追加しても同じインターフェース越しに利用できるようなパターン。
java.io.*ライブラリがこのパターンを利用しているらしい。
FileReaderやInputStreamReader、BufferedReaderなどを入れ子状にして機能を追加していくことができる?。(あまりちゃんと確認していない。)Facade
複数のクラス間の連携をまとめて、シンプルなインターフェースを提供するパターン。
特に意識しなくても、複数のクラス間の連携が複雑になっていけば自然とまとめるようにらると思う。Flyweight
インスタンスをプールして使いまわしてメモリを節約するパターン。Proxy
Proxyクラス越しにインスタンスを操作することで、制限をかけたり実際の複雑な処理をブラックボックス化したりするパターン。
Androidではbinderという分散オブジェクトの仕組みを利用して、別プロセスのインスタンスのメソッドを呼び出したりするが、それがProxyパターンになっているらしい。3. 振る舞いに関するパターン
Chain of Responsibility
問題をたらい回しにして、処理できる人に処理させるパターン。
あまり使わなさそう。Command
操作をオブジェクトにすることで、UndoやRedoなどの処理を行えるようにするパターン。
各Commandオブジェクトはexecuteメソッドを持ち、これらをMacroCommandオブジェクトが取りまとめる。Interpreter
構文解析木を作成するパターン。
各Nodeオブジェクトはparseメソッドを持ち、contextクラスからトークンを一つ一つ取得しながら構文解析木を作成する。Iterator
リストに対してループ処理を行えるようにするパターン。
hasNextとnextメソッドを実装したIteratorクラスから、要素を一つ一つ取り出していく。Mediator
複数のクラス間の連携を取りまとめるパターン。
Facadeが内部の複数のクラスを一方的に利用するのに対し、
Mediatorでは、Mediatorクラスと内部のクラスが双方向に連携する。
Androidだと、TabLayoutとViewPager2を連携するTabLayoutMediatorがこのパターンに相当する。Memento
インスタンスの状態を保存して、再生成するパターン。
Androidだと、BundleとかParcelableとかがこのパターンに相当する。Observer
リスナーを登録して、イベントの通知を監視するパターン。
Androidだと、LiveDataがこのパターンに相当する。State
ロジックが複数の状態によって切り替わるときに、状態に相当するクラスを作成し、
そのクラスの中にロジックを閉じ込めるパターン。
stateインスタンスを差し替えるだけで、ロジックの切り替えができるので、
if文が少なくなり、新しい状態が発生しても対応するクラスを作成するだけで対応できる。Strategy
ロジックを担当するクラスに特定の処理を委譲するパターン。
委譲に関する一番ベーシックなパターンのような気がする。
StateもObserverもBridgeもこのパターンの一種なような気がする。Template Method
特定の処理を抽象化して、継承する子クラスに実装を任せるパターン。
継承に関する一般的なパターンのような気がする。
abstract classは須らくこのパターンに当てはまると思う。Visitor
Visitorクラスがデーター構造(大抵の場合はツリー構造)の各ノードを渡り歩くパターン。
VisitorクラスはそれぞれのNodeクラスに対する処理を記述したメソッドを持つ。
各ノードはacceptメソッドを実装し、引数としてVisitorクラスを受け取る。
Nodeクラスはacceptメソッド内部で、自分に対応するVisitorクラスのメソッドを自分を引数として呼び出す。まとめ
デザインパターンと言いつつも、StrategyやTmeplate Methodなど普段からあまり意識せずに使っているパターンもありました。
BridgeやStateパターンは、結構いろいろな場面で利用でき、可読性やメンテナンス性も高まるので普段から意識して使うようにしていきたいです。
- 投稿日:2020-08-12T06:54:05+09:00
PreferenceScreenをネストすると外のレイアウトが崩れる
PreferenceFragmentを使った設定画面で、例えば、複数画面のPreferenceを簡単に作ろうとすると
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> <PreferenceScreen android:title="test1"> <Preference android:title="test1-1"> <Preference android:title="test1-2"> </PreferenceScreen> </PreferenceScreen>とネストをかければ複数画面を作れますが、これだとネストされたPreferenceScreen画面には、親のPreferenceScreenでデザインしたであろうレイアウト(ActionBarやbuttombarなど)が適用されません。
そこで、親のPreference要素をクリックすると別のフラグメントに置き換えるという、従来のFragmentの切り替え方でなんとかしました。SettingActivity.javapublic class SettingActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // activity_setting.xmlはfragmentを入れるレイアウトxml setContentView(R.layout.activity_setting); getFragmentManager().beginTransaction().replace(R.id.fragment_container, new UserPreferenceFragment()).commit(); } } class UserPreferenceFragment extends PreferenceFragment { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // preference_header.xmlは親の設定画面xml addPreferencesFromResource(R.xml.preference_header); Preference.OnPreferenceClickListener subpreference = new Preference.OnPreferenceClickListener() { @Override public boolean onPreferenceClick(Preference preference) { getPreferenceScreen().removeAll(); switch(preference.getKey()) { // preference_test1,2,3 は子の設定画面 case "test1": addPreferencesFromResource(R.xml.preference_test1); break; case "test2": addPreferencesFromResource(R.xml.preference_test2); break; case "test3": addPreferencesFromResource(R.xml.preference_test3); break; } return true; } }; final Preference test1 = (Preference) findPreference("test1"); final Preference test2 = (Preference) findPreference("tesst2"); final Preference test3 = (Preference) findPreference("test3"); test1.setOnPreferenceClickListener(subpreference); test2.setOnPreferenceClickListener(subpreference); test3.setOnPreferenceClickListener(subpreference); } }ホント地味にですが、setOnPreferenceClickListenerをできるだけきれいに、複数作るのに苦労しました。(ひとつずつ定義して当てはめるには量が多かったので)
参考:
- 投稿日:2020-08-12T06:51:34+09:00
BottomNavigationViewのFragmentの中に更にTabとFragmentを作る
BottomNavigationViewを一回作り、その中にTabhostとそのフラグメントを作った際、ハマったのでメモ。
主に、一番下の参考を参照しましたが、いくつかエラーが発生したため修正を入れています。
まず、BottomNavigationViewで分けた各フラグメントのうち一つです。親フラグメント.javapublic class 親フラグメント extends Fragment { public static Record newInstance() { return new Record(); } @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.record, container, false); //FragmentManagerの取得 FragmentManager mFragmentManager = getChildFragmentManager(); //xmlからFragmentTabHostを取得、idが android.R.id.tabhost である点に注意 FragmentTabHost tabHost = (FragmentTabHost)v.findViewById(android.R.id.tabhost); Log.d("tabHost", String.valueOf(tabHost)); //ContextとFragmentManagerと、FragmentがあたるViewのidを渡してセットアップ tabHost.setup(getActivity().getApplicationContext(), mFragmentManager, R.id.content); //String型の引数には任意のidを渡す //今回は2つのFragmentをFragmentTabHostから切り替えるため、2つのTabSpecを用意する TabHost.TabSpec mTabSpec1 = tabHost.newTabSpec("tab1"); TabHost.TabSpec mTabSpec2 = tabHost.newTabSpec("tab2"); //Tab上に表示する文字を渡す mTabSpec1.setIndicator("This is tab1"); mTabSpec2.setIndicator("This is tab2"); Bundle args = new Bundle(); args.putString("string", "message"); //それぞれのTabSpecにclassを対応付けるように引数を渡す //第3引数はBundleを持たせることで、Fragmentに値を渡せる。不要である場合はnullを渡す tabHost.addTab(mTabSpec1, 子フラグメント1.class, args); tabHost.addTab(mTabSpec2, 子フラグメント2.class, null); return v; } }子フラグメント1.javapublic class 子フラグメント1 extends Fragment { static 子フラグメント1 newInstance() {return new 子フラグメント1();} @Override public void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); //addTabの際にBundleを渡す場合は、Bundleから値を取得する //渡さない(nullを渡している)場合は、実装しなくてよい。そうでないとgetString("string")時にエラーが発生する Bundle args = getArguments(); String str = args.getString("string"); } @Nullable @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){ return inflater.inflate(R.layout.子フラグメントレイアウト1, null); } }子フラグメント2.javapublic class 子フラグメント2 extends Fragment { static 子フラグメント2 newInstance() {return new 子フラグメント2();} @Nullable @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){ return inflater.inflate(R.layout.子フラグメントレイアウト2, null); } }親フラグメントレイアウト.xml<?xml version="1.0" encoding="utf-8"?> <!-- FragmentTabHostのidは必ず @android:id/tabhost にする--> <android.support.v4.app.FragmentTabHost xmlns:android="http://schemas.android.com/apk/res/android" android:id="@android:id/tabhost" android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <!-- FragmentTabHost同様、idの指定あり。idは必ず @android:id/tabs にする--> <TabWidget android:id="@android:id/tabs" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_weight="0"/> <FrameLayout android:id="@android:id/tabcontent" android:layout_width="0dp" android:layout_height="0dp" android:layout_weight="0"/> <!-- contentにFragmentが追加される--> <FrameLayout android:id="@+id/content" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1"/> </LinearLayout> </android.support.v4.app.FragmentTabHost>子フラグメントレイアウト.xml(子フラグメントレイアウト1,子フラグメントレイアウト2)<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/textView3" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="子フラグメント" /> </LinearLayout>備考:
Fragment系は"android.app.Fragment~"と"android.support.v4.app.Fragment~"がありますが、どちらかに統一したほうが良いっぽいです(当たり前ですが。)
Android標準のFragmentよりsupport.v4.app.Fragmentを使うべき、という理由を徹底調査という記事を参考にして、わたしはv4を使っています。
- 投稿日:2020-08-12T06:49:10+09:00
BottomNavigationViewを少しいじってみた②
■ 各itemクリック時のviewにfragmentとxmlを使って分ける
bottomnavigationviewのitemを押したときのviewを、それぞれのレイアウトを別のものにしたかったため、fragmentとxmlでわけました。
main.javapackage com.example.yoshihiro.smartkoneco; import ... public class Main extends AppCompatActivity { private BottomNavigationView.OnNavigationItemSelectedListener mOnNavigationItemSelectedListener = new BottomNavigationView.OnNavigationItemSelectedListener() { @Override public boolean onNavigationItemSelected(@NonNull MenuItem item) { switch (item.getItemId()) { case R.id.fragment1: loadFragment1(); return true; case R.id.fragment2: loadFragment2(); return true; case R.id.fragment3: loadFragment3(); return true; case R.id.fragment4: loadFragment4(); return true; } return false; } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); if (savedInstanceState == null) { Fragment1(); } BottomNavigationView navigation = (BottomNavigationView) findViewById(R.id.navigation); disableShiftMode(navigation); navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener); } public static void disableShiftMode(BottomNavigationView view) { ... } private void loadFragment1() { Fragment1 fragment = Fragment1.newInstance(); FragmentTransaction ft = getFragmentManager().beginTransaction(); ft.replace(R.id.fragment_frame, fragment); ft.commit(); } // 以下load~ 3つで同じ } class Fragment1 extends Fragment { public static Fragment1 newInstance() { return new Fragment1(); } @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment1, container, false); } } // 以下3つfragment2~4で同じmain.xml<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context="com.example.yoshihiro.smartkoneco.Main"> <include layout="@layout/main_title" tools:layout_editor_absoluteY="-44dp" tools:layout_editor_absoluteX="-260dp" /> <FrameLayout android:id="@+id/fragment_frame" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1"> </FrameLayout> <android.support.design.widget.BottomNavigationView android:id="@+id/navigation" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="bottom" android:background="?android:attr/windowBackground" app:itemIconTint="@color/buttom_navigation" app:itemTextColor="@color/buttom_navigation" app:menu="@menu/navigation" /> </LinearLayout>time_table.xml,record.xml,room_search.xml,news.xml<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <!-- 各レイアウト --> </LinearLayout>参考:
Android using BottomNavigationView
- 投稿日:2020-08-12T06:46:37+09:00
BottomNavigationViewを少しいじってみた①
android.support.design.widget.BottomNavigationViewは公式ライブラリのひとつです。
Bottom navigation
■ アイコンとテキストの色を変更する(選択時、未選択時)
1. resのなかにcolorディレクトリを作る
2. そのcolorディレクトリの中にbuttom_navigation.xmlをつくり、次の内容を記載する
res/color/buttom_navigation.xml<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_checked="true" android:color="#FFF" /> <item android:state_pressed="true" android:color="#FFF" /> <item android:color="#969696" /> </selector>3. buttomnavigationviewの方に指定をする
res/layout/main.xml... <android.support.design.widget.BottomNavigationView android:id="@+id/navigation" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="bottom" android:background="?android:attr/windowBackground" app:itemBackground="@color/themecolor" app:itemIconTint="@color/buttom_navigation" app:itemTextColor="@color/buttom_navigation" app:menu="@menu/navigation" /> ...参考:
BottomNavigationViewのカスタマイズ■ 選択されていないタブの文字を表示させ続ける
main.java... @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); mTextMessage = (TextView) findViewById(R.id.message); BottomNavigationView navigation = (BottomNavigationView) findViewById(R.id.navigation); navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener); // no-hide menu-text BottomNavigationMenuView menuView = (BottomNavigationMenuView) navigation.getChildAt(0); for (int i = 0; i < menuView.getChildCount(); i++) { BottomNavigationItemView itemView = (BottomNavigationItemView) menuView.getChildAt(i); itemView.setShiftingMode(false); itemView.setChecked(false); // Viewの状態を反映させるために呼んでいる } }■ 全てのタブの横幅、マージンを固定する
デフォルトの状態だと、タブ(item)が4つあると、選択されたタブのwidth(横幅)が大きく広がり、ほかが狭まる。この4つの横幅を全て固定したい。
BottomNavigationViewHelper.javapublic class BottomNavigationViewHelper { public static void disableShiftMode(BottomNavigationView view) { BottomNavigationMenuView menuView = (BottomNavigationMenuView) view.getChildAt(0); try { Field shiftingMode = menuView.getClass().getDeclaredField("mShiftingMode"); shiftingMode.setAccessible(true); shiftingMode.setBoolean(menuView, false); shiftingMode.setAccessible(false); for (int i = 0; i < menuView.getChildCount(); i++) { BottomNavigationItemView item = (BottomNavigationItemView) menuView.getChildAt(i); //noinspection RestrictedApi item.setShiftingMode(false); // set once again checked value, so view will be updated //noinspection RestrictedApi item.setChecked(item.getItemData().isChecked()); } } catch (NoSuchFieldException e) { Log.e("BNVHelper", "Unable to get shift mode field", e); } catch (IllegalAccessException e) { Log.e("BNVHelper", "Unable to change value of shift mode", e); } } }main.java... mTextMessage = (TextView) findViewById(R.id.message); BottomNavigationView navigation = (BottomNavigationView) findViewById(R.id.navigation); BottomNavigationViewHelper.disableShiftMode(navigation); navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener); ...
- 投稿日:2020-08-12T05:48:56+09:00
Unit Test と Instrumented Unit Test の実行(UnitTest探求記1)
Unit Test 探求記は、unit test に関してまったりと実験しつつその過程を綴ってみるというものです。
今回のお題
Unit test と instrumented unit test を実行してみます。
Unit Test の実行(失敗例)
ここで言う unit test とは、src/test 以下のテストのことを指しています。
わざと間違えて、テストが実行されていることを確認してみます。
ExampleUnitTest.ktExampleUnitTest.kt抜粋class ExampleUnitTest { @Test fun addition_isCorrect() { // assertEquals(4, 2 + 2) // わざと間違えてみる。 assertEquals(5, 2 + 2) } }下記のようにコマンドライン上で実行します:
$ ./gradlew clean test --info com.objectfanatics.myapplication.ExampleUnitTest > addition_isCorrect FAILED java.lang.AssertionError: expected:<5> but was:<4> at org.junit.Assert.fail(Assert.java:88) at org.junit.Assert.failNotEquals(Assert.java:834) at org.junit.Assert.assertEquals(Assert.java:645) at org.junit.Assert.assertEquals(Assert.java:631) at com.objectfanatics.myapplication.ExampleUnitTest.addition_isCorrect(ExampleUnitTest.kt:18)ちゃんとテストが失敗していることを確認できました。
Instrumented Unit Test の実行(失敗例)
ここで言う instrumented unit test とは、src/androidTest 以下のテストのことを指しています。
わざと間違えて、テストが実行されていることを確認してみます。
ExampleInstrumentedTest.ktExampleInstrumentedTest.kt抜粋@RunWith(AndroidJUnit4::class) class ExampleInstrumentedTest { @Test fun useAppContext() { // Context of the app under test. val appContext = InstrumentationRegistry.getInstrumentation().targetContext // assertEquals("com.objectfanatics.chrono0020", appContext.packageName) fail("I made a mistake on purpose.") } }実機を接続た後、下記のようにコマンドライン上で実行します:
$ ./gradlew clean connectedAndroidTest > Task :app:connectedDebugAndroidTest Starting 1 tests on ASUS_X00PD - 8.0.0 com.objectfanatics.myapplication.ExampleInstrumentedTest > useAppContext[ASUS_X00PD - 8.0.0] FAILED java.lang.AssertionError: I made a mistake on purpose. at org.junit.Assert.fail(Assert.java:88) > Task :app:connectedDebugAndroidTest FAILEDちゃんとテストが失敗していることを確認できました。
依存関係を確認してみる
以下に、テスト関連の設定を抜粋し、コメントしてみます。
app/build.gradle.ktsapp/build.gradle.kts抜粋android { // JUnit 4 のテストクラス群を利用するため、AndroidJUnitRunner をデフォルトの test instrumentation runner としてセットしている。 // instrumented unit test とは実機(やエミュレータ)上で実行される unit test のこと。 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } } dependencies { // 通常の unit test から利用される JUnit 4 のクラス群への依存。※ org.junit.Test 等 testImplementation("junit:junit:4.12") // instrumented unit test から利用される JUnit 4 のクラス群への依存。※ AndroidJUnit4, ActivityScenarioRule 等 androidTestImplementation("androidx.test.ext:junit:1.1.1") // Espresso testing framework への依存。 // Espresso testing framework は UI テストのためのフレームワーク。 // @see https://developer.android.com/training/testing/ui-testing/espresso-testing androidTestImplementation("androidx.test.espresso:espresso-core:3.2.0") }
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
を外しても IDE 上で警告は出ませんが、実行時に以下のようになり、instrumented unit test が失敗します:$ ./gradlew clean connectedAndroidTest > Task :app:connectedDebugAndroidTest Starting 0 tests on ASUS_X00PD - 8.0.0 Tests on ASUS_X00PD - 8.0.0 failed: Instrumentation run failed due to 'Process crashed.' com.android.build.gradle.internal.testing.ConnectedDevice > No tests found.[ASUS_X00PD - 8.0.0] FAILED No tests found. This usually means that your test classes are not in the form that your test runner expects (e.g. don't inherit from TestCase or lack @Test annotations). > Task :app:connectedDebugAndroidTest FAILED FAILURE: Build failed with an exception.エラーの表示から依存関係の指定漏れであることを推測するのは難しそうですね、、、。
Test の実行(成功例)
わざと間違った個所を修正してテストを実行します。
ExampleUnitTest.kt
ExampleInstrumentedTest.kt$ ./gradlew clean test BUILD SUCCESSFUL in 6s 28 actionable tasks: 28 executedちゃんとテストが成功していることを確認できました。
まとめ
今回は、Unit test と instrumented unit test を実行してみました。
ソースコード
Android Studio により Empty Activity project を生成し、build.gradle を build.gradle.kts に変換したものがベースとなっています。
▼ テストに失敗するコード
source treecloneとcheckoutの例$ git clone git@github.com:beyondseeker/chrono0020.git $ cd chrono0020 $ git checkout bb13107f90c9fadac13b8a4d6b7259e222322739▼ テストに成功するコード
source treecloneとcheckoutの例$ git clone git@github.com:beyondseeker/chrono0020.git $ cd chrono0020 $ git checkout 013ee1075230625b8d5a23b12588234246252175
- 投稿日:2020-08-12T04:00:02+09:00
モバイルブラウザのキーボードの種類は制御できない
スマートフォンなどのモバイル端末のブラウザでは、input要素をフォーカスするとキーボードが出現する。
このキーボードは、「英語」「日本語 - かな」などの種類があり、日本語利用者なら切り替えながら使っているだろう。
で、input要素をフォーカスした時に出てくるこのキーボードの種類を制御したい、という要望はままあるだろう。
たとえば、英数字のみからなる何らかのシリアルコードのようなものの入力欄は、日本語ではなく英語キーボードを表示させたいだろう。
結論からいうと、キーボードの種類は制御できない。
もう少し正確に言うと、日本語ではなく英語キーボードを常に表示させるようにすることはできない。type属性で制御できるのではないか
巷の技術系ブログなどではよく書かれている。
<!-- email入力用だから、英語キーボードであるべき? --> <input type="email"> <!-- URL入力用だから、英語キーボードであるべき? --> <input type="url">たしかに、多くのモバイルブラウザは、type属性を
url
に指定することによって、それに適したキーボードを表示する機能をサポートしてはいる。
iOS Safariの場合、type="email"
のときは、スペーキーの隣に@
や.
など、メールアドレスによく使う文字キーが配置されている。(前掲のスクリーンショット画像がまさにそれだ)一見すると、求めていた挙動が実現されているように思うが、ちょっと待って欲しい。
日本語キーボードに切り替えしてみる
type="email"
をフォーカスしてキーボードを表示したら、わざと日本語に切り替えた上でinput要素のフォーカスをはずしてみよう。そして、再び同要素をフォーカスしてキーボードを表示させてみる。
このとき日本語キーボードが表示されるだろう。これはあなたの求めている挙動だろうか。
次にページをリロードしてみて、再び同要素をフォーカスする。やはり日本語キーボードが出るだろう。
同じページ内に、別要素のtype="email"
があるならば、そちらにフォーカスを当ててみよう。やはり日本語キーボードが出るはずだ。これはあなたの求めている挙動だろうか。
なんなら、他のページのtype="email"
要素でキーボードを日本語にした後、当該サイトのtype="email"
にフォーカスしてみよう。iOS Safariでは日本語キーボードが出るだろう。(Androidでは英語キーボードが出る)
何度も言うが、これはあなたが求めている挙動だろうか。1
あなたが求めているのは、当該要素をフォーカスしたときには常に英語キーボードが表示されることではないのか。2その要素に入力されるべきは英数字だけなのだから。メールアドレスはマルチバイト文字が含まれ得る
なぜこんなことになるのか。
メールアドレス入力用のtype属性値ならば、英語キーボードだけ表示されればいいのではないのか。
現に、input[type="tel"]
は数字キーボードのみ、input[type="password"]
は英語キーボードのみであり、キーボードの種類を変更することはできない。
理由は、メールアドレスの入力には英語キーボードだけでは不十分だからだ。
RFCの定義によれば、メールアドレスに使われる文字には制限があり、マルチバイト文字は含まれないらしいのだが、Gmailではマルチバイト文字を含むメールアドレスへの受送信が可能だ。
本来の規格としてNGだったとしても、Gmailがサポートしている以上、マルチバイト文字を含むメールアドレスは存在する。存在する以上は、入力できなければならない。入力するためには、日本語キーボードに切り替え可能でなければならないのだ。
そして、ひとたびユーザがtype="email"
要素で日本語キーボードに切り替えたならば、ブラウザは「ユーザーは日本語でemailを入力しようとしている」と解釈し、それ以後type="email"
フォーカス時には日本語キーボードを表示するようになると考えられる。ユーザビリティとしては筋が通っている。これは、
input[type="url"]
についても同様だ。
日本語ドメインは存在している。そうでなくても、ディレクトリ名やパラメータにマルチバイト文字が含まれることは普通にある。3
であるならば、日本語キーボードが表示されうる。inputmodeという属性もあるが
inputmode="email"
inputmode="url"
とすることで、入力キーボードを指定できるという属性値だが、これとてもtype属性と同じ挙動だ。入力値が英数字でなければならないtype属性は存在しない
ただし、パスワード
type="password"
を除く。
type="password"
は当然ながら伏字になるので、通常はパスワード以外には使えない。
というわけで、常に英語キーボードを表示させるようにする手段は、我々には与えられていない。我々はどうすべきか
諦めよ。そして、ユーザを信頼せよ。
たとえ日本語キーボードが開いたとしても、英数字で書くべきことを理解すれば、ユーザーは自らの手で英語キーボードに切り替えて入力するであろう。バリデーションと入力補助
通常は誤った値が入力された場合には、それと分かるエラー文言を表示するものだろう。ユーザに気づかせて軌道修正させればよい。
全角文字が入力された際には、JavaScriptで半角に自動変換する機能くらいは実装しておくと親切かもしれない。なぜこの記事を書いたか
この入力欄に入力する値は英数字なので、英語キーボードが出るようにします。
その方がユーザに負担がかからないので。あなたはこのように仕様を決めた手前、フォーカス時に日本語キーボードが出てしまう挙動を何とかしたいと思うかもしれない。
だが、立ち止まってよく考えてみて欲しい。これはそれほど問題なことなのか。
ユーザは自分でキーボードを切り替えることができ、適切に入力するのに何の支障もないではないか。
ユーザは自分がどうすべきか(英数字で入力)をきちんと理解できるようになっていれば、「英語キーボードを出す」などという些末な事象にとらわれる必要はどこにもない。スマホで「英語キーボードを出す」ということについて、できる、できる、と書いてある記事ばかりなので、当然実装しなければならなくなってしまう。確かに概ね期待通りにできるのだが、重箱の角をつつくようなデバッグ4には耐えられず、不具合として報告されてしまう。報告された以上は、何とか対応しようとするものの、完璧に制御することはできないのだと理解した。
このことについて触れている記事がなく、とても困ったので書いた。5
実装を担当しているあなたが、クライアントやディレクターに、この記事を見せて、ここに書いてある通り、表示するキーボードを完璧に制御することはできません。
type属性をと適当な妥結点を見出してくれることを期待する。