20200812のAndroidに関する記事は14件です。

Xamarin.FormsでAndroidアプリ開発② サンプルアプリ作成

前回インストールしたVisual Studioでサンプルアプリを作っていきます。

Xamarin.Formsのソリューションの立ち上げ

  1. まずVisualStudioを立ち上げると以下の画面が出てきます。「新しいプロジェクトの作成」をクリックします。
    image.png

  2. 色々出てきますが、モバイルアプリ(Xamarin.Forms)を選択します。
    image.png

  3. 適当に場所と名前を設定します。
    image.png

  4. 今回は空白を選択します。あ、Windows(UWP)にも対応するためには別途インストールしないといけなかったのですね。。こちらはまた別の記事で説明しようと思います。
    image.png

  5. Androidライセンスの同意画面が出てきますので、(内容確認の上)同意します。
    image.png

  6. ソリューションエクスプローラーからMainPage.xamlをダブルクリックします。こちらがアプリの画面になります。
    image.png

  7. ソースコードの右上にあるimage.pngをクリックすると視覚的にアプリ画面を見ることができます。

  8. image.pngをクリックするとソースコードに戻ります。

ソースコードの変更

まずMainpage.xamlに少しコードを追加してみます。下図のように、以下のコマンドを追加します。
<Button Text="おしてね" Clicked="OnButtonClicked" />
image.png
これで、「おしてね」と書かれ、クリックするとOnButtonClickedクラスを呼び出すボタンが作成されます。

デザイン画面にします。なぜかうまく反映されないので、例えばiOSの画面にした後にAndroidの画面に戻す、などをすることで追加されたボタンが表示されます。
image.png

次にMainpage.xaml.csを開き、以下のコマンドを追加します。

private void OnButtonPressed(object sender, EventArgs e)
{
    (sender as Button).Text = "こんにちは";
}

image.png

シミュレーションの実行

  1. Android Emulatorをクリックします。
    image.png

  2. Createします。(Pixel2のAndroid9です。ちょっと古いですね。。)
    image.png

  3. 同意します。
    image.png

  4. 少し時間がかかりましたが、シミュレータの作成が完了しました。「開始」をクリックします。
    image.png

  5. 画面が立ち上がった後もう一度Visual Studioに戻ると、Pixel2でデバッグできるようになっているので、こちらをクリックします。
    image.png

  6. 出てきました!「おしてね」をクリックしてみます。
    image.png

  7. 無事「こんにちは」に変更され、アプリが動いていることが確認できました。
    image.png

関連記事

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

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を入れます。

  1. https://visualstudio.microsoft.com/ja/downloads/ にアクセス
  2. コミュニティをダウンロードし、exeファイルを実行 image.png
  3. 続行します。
    image.png

  4. .NETによるモバイル開発にチェックを入れインストールします。(かなり時間がかかります)
    image.png

  5. 今は「後で行う」で。
    image.png

  6. 「Visual Studioの開始」でインストールは完了です。
    image.png

今回はインストールまでの紹介でした。
次回からアプリ開発を行っていきますので、乞うご期待。

関連記事

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

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.properties
Keys.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をクリックします。

generate_nav.png

選択はAPKでもBundleでも、どちらでも構わないようです。
今回はAPKを選択し、Nextをクリックします。

次に、Create new...をクリックしてkeystoreの作成先を指定します。

generate_nav2.png

keystoreは作成したら.signingに移動させるので、プロジェクトルート直下等適当な場所で構いません。
ファイル名はrelease.keystoreとしておきます。

generate_nav3.png

パスワードを設定し、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.properties
RELEASE_STORE_FILE=/YourProjectName/release.keystore
RELEASE_STORE_PASS=xxxxx
RELEASE_ALIAS=xxxxx
RELEASE_KEY_PASS=xxxxx

build.gradleに署名付きビルドのためのコードを記述する

appレベルbuild.gradleのandroidブロックにsigningConfigを記述します。

appレベルbuild.gradle
android {
  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の指定方法をいくつか検証してみた。

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

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を提供しています。
HMS Core

この中には、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というインストール不要で直接使えるアプリも提供しています。
AppGallery
(引用元:https://k-tai.watch.impress.co.jp/docs/news/1256381.html)

日本のAppGalleryでは現在LINE、楽天、NAVITIME、メルカリなどの身近なアプリがすでにダウンロードできるようになっています。もちろん一部のアプリがまだ載っていませんが、今後の発展に期待できるでしょう。
AppGalleryのスクリーンショット

AppGallery用アプリを開発するには

既存のAndroidアプリは意外と簡単にAppGallery対応に改修できます。こちらの詳細についてはまた後日に紹介したいと思います。

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

【Android/Kotlin】フラグメントのライフサイクルまとめ

フラグメントのライフサイクル

 まず、フラグメントとは「サブアクティビティ」的なポジションである。アクティビティが成り立っている上で、フラグメントは存在している。ライフサイクルについても同じような関係にあり、アクティビティのライフサイクルが破壊されれば、フラグメントのライフサイクルも破壊されてしまう。あくまでもアクティビティあってのフラグメントである、という事が重要だ。フラグメントとアクティビティのライフサイクルを表した図が以下。
complete_android_fragment_lifecycle (1).png
参照: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前に呼び出される。

各コールバックメソッドの可視化

 今回説明した各メソッドをそのままフラグメントに張り付けてやるだけで、ライフサイクルの遷移の様子がログに出力される。
キャプチャ.PNG
同時にアクティビティのライフサイクルも可視化させてやれば、それぞれの細かい境界についても理解することが出来るのでお勧めだ。

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

setFragmentResultListenerを使った通知をするDialogFragmentの実装提案

androidx.fragment:fragment:1.3.0-alpha04から新しく setFragmentResult / setFragmentResultListenerが追加され、setTargetFragmentがdeprecatedになりました。

おそらく多くの呼び出し元に通知を送るDialogFragmentでは、setTargetFragmentを使ってListenerなどで実装していたのではないでしょうか。

従来のDialogFragment.kt
class 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()
    }
}

だいたいこんな感じかと思うんですが、
もちろんsetTargetFragmentrequireFragmentManagerも使えないので、
以下のような感じにすると、これまでの使用感をあまり変えずに、実装できるのではないでしょうか。

提案するDialogFragment.kt
class 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の処理
    }
)

といった感じで、ラムダを使用して、カスタムリスナーを使用しなくても、
通知を実装することができ、とてもシンプルになります。

いかがでしょうか?
何かもう少しこうしたら良いなど、ご意見あればお願いいたします。

参考:
setFragmentResultを使ったFragment間のデータ受け渡し
フラグメント間でデータを渡す

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

スマホもPCの一員としてファイル同期したいじゃない?Ⅱ

おはようございます!

前回の続きです。

前回の

Syncthingに似てる気がするけど知ってる?参考になるかもよー見てみたら??」

にスマホアプリも追従させてみたよ!って記事です。

やたらスマホ!

要するにHTTP(S)使ってフォルダを同期してくれる君というやつです。
Syncthing以外にもNearby Shareもあるじゃーんってご指摘あるな。
でも、これ強みとしてはバイナリいっこで動くし、マルチOS対応だし、、

あと今あるプロキシとかHTTPエコシステムに乗っかれるのがこのツールの推しポイント

DE・KI・TA

お告げ通り機能追加しましたYO

  • ディレクトリのあるフォルダでも再帰的に同期
  • 前回同期時の設定をコンフィグに出して、次回はそれ読んで起動
  • 同期状況を表示する
  • HTTPS同期をサポート

詳しくはリポジトリ見てくださいな。releaseの.apkをもってくればインスコできます。

2が付いてないリポジトリは非互換なので注意です。

つかいたい!

  • PC版を起動したら、EnterかSpaceを押すとQRコードが表示されます

1.png

  • それをスマホ版アプリで読み取れば同期が開始されます

UIわからん!

screen.png

①は同期ゲージです。この数字が何秒毎に同期するかになります。同期間隔が狭いとデカいファイルが転送しきれないのでその点はご注意を

②同期先を変えるときなど。QR Codeをもう一度読みましょう

③保存先フォルダを変更します

④は同期した時にサーバーに無いのに、クライアントにあるファイルを消すモードです。完全同期しておきたい時に使います。

⑤アプリを終了して設定をコンフィグファイルに書き出します

⑥は同期状況とか動作ステータスが色々でます

あとがき

GUI、転送効率化プロトコル実装、中継サーバーとかsyncthingにある機能で
実装出来てないとこはあるけど、その他の最低限は盛り込めたかな?

Win、Mac以外にもラズパイとか色々持ってる人は便利だと思うので
是非使ってみてくださいな。(そんな人のニッチだよ!ってw)

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

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パターンは、結構いろいろな場面で利用でき、可読性やメンテナンス性も高まるので普段から意識して使うようにしていきたいです。

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

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.java
public 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をできるだけきれいに、複数作るのに苦労しました。(ひとつずつ定義して当てはめるには量が多かったので)

参考:

PREFERENCEACTIVITYからINTENTを呼び出す

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

BottomNavigationViewのFragmentの中に更にTabとFragmentを作る

BottomNavigationViewを一回作り、その中にTabhostとそのフラグメントを作った際、ハマったのでメモ。

主に、一番下の参考を参照しましたが、いくつかエラーが発生したため修正を入れています。
まず、BottomNavigationViewで分けた各フラグメントのうち一つです。

親フラグメント.java
public 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.java
public 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.java
public 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を使っています。

参考:

FragmentTabHost覚書

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

BottomNavigationViewを少しいじってみた②

■ 各itemクリック時のviewにfragmentとxmlを使って分ける

bottomnavigationviewのitemを押したときのviewを、それぞれのレイアウトを別のものにしたかったため、fragmentとxmlでわけました。

main.java
package 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

Start fragment in BottomNavigationView

Bottom Navigation Views

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

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つの横幅を全て固定したい。

参考:
Android android.support.design.widget.BottomNavigationView with 4 items not equally width [duplicate]

BottomNavigationViewHelper.java
public 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);
...
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unit Test と Instrumented Unit Test の実行(UnitTest探求記1)

Unit Test 探求記は、unit test に関してまったりと実験しつつその過程を綴ってみるというものです。

今回のお題

Unit test と instrumented unit test を実行してみます。

Unit Test の実行(失敗例)

ここで言う unit test とは、src/test 以下のテストのことを指しています。

わざと間違えて、テストが実行されていることを確認してみます。
ExampleUnitTest.kt

ExampleUnitTest.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.kt

ExampleInstrumentedTest.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.kts

app/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 tree

cloneとcheckoutの例
$ git clone git@github.com:beyondseeker/chrono0020.git
$ cd chrono0020
$ git checkout bb13107f90c9fadac13b8a4d6b7259e222322739

▼ テストに成功するコード
source tree

cloneとcheckoutの例
$ git clone git@github.com:beyondseeker/chrono0020.git
$ cd chrono0020
$ git checkout 013ee1075230625b8d5a23b12588234246252175
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

モバイルブラウザのキーボードの種類は制御できない

スマートフォンなどのモバイル端末のブラウザでは、input要素をフォーカスするとキーボードが出現する。
このキーボードは、「英語」「日本語 - かな」などの種類があり、日本語利用者なら切り替えながら使っているだろう。

で、input要素をフォーカスした時に出てくるこのキーボードの種類を制御したい、という要望はままあるだろう。
たとえば、英数字のみからなる何らかのシリアルコードのようなものの入力欄は、日本語ではなく英語キーボードを表示させたいだろう。
結論からいうと、キーボードの種類は制御できない。
もう少し正確に言うと、日本語ではなく英語キーボードを常に表示させるようにすることはできない。

type属性で制御できるのではないか

巷の技術系ブログなどではよく書かれている。

<!-- email入力用だから、英語キーボードであるべき? -->
<input type="email">

<!-- URL入力用だから、英語キーボードであるべき? -->
<input type="url">

たしかに、多くのモバイルブラウザは、type属性をemailurlに指定することによって、それに適したキーボードを表示する機能をサポートしてはいる。
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属性をemailにしておけば、初回フォーカス時には(100%ではないですが)概ね英語キーボードで開くようですので、このくらいの対応で手を打ちましょう。

と適当な妥結点を見出してくれることを期待する。


  1. type="url"でも同様。 

  2. もちろん、ユーザ手動で英語キーボードに戻しておけば、再度フォーカス時には英語キーボードが出る。 

  3. Wikipediaを見よ。 

  4. これがデバッガーの仕事なので当然である。 

  5. 問題の性質上当然なのだが、検索しても英語の記事は全く出てこない。 

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