- 投稿日:2021-06-03T23:54:38+09:00
[iOS・Android] Apple Silicon搭載 Macの環境構築トラップまとめ
Apple Silicon(M1 chip)を搭載したMac(以下 M1 Mac)に、iOS・Androidのアプリ開発環境を整備した際に遭遇したトラップをまとめます。 便利なコマンド uname -m 実行中のシェルが、どのアーキテクチャで実行されているかを確認できる arch -x86_64 実行したいコマンド x86_64アーキテクチャでコマンドを実行する(arch -arch x86_64 実行したいコマンドと同じ) arch -arm64 実行したいコマンド arm64でコマンドを実行する(arm64eでも同じっぽい?) iOS・Android共通 Homebrewを使うなら、インストール先は /opt/homebrew のままにしておく M1 MacにHomebrewをインストールすると、brew installを実行した時の保存先は /opt/homebrew/bin/* になる。 なお、Rosetta 2を使用してシェルを実行している場合は、/opt/homebrew・/usr/localどちらにインストールするかを選択するプロンプトがあったはずだが、ここはデフォルト設定にしておく。 iOS rbenv等は使わない、システムのRubyを使う rbenvを使うと、CocoaPodsを動作させることができない システムのRubyを使用する CocoaPodsが依存するffiがarm64に対応していないことが原因 M1 macでpod installができない。sudo gem install ffiをしても治らない(rbenvが原因) じきに対応するとは思うが……。 CocoaPodsが動かない場合 CocoaPodsが依存するffiのバージョンが古い可能性がある ターミナルアプリ(iTerm2なども)を、Rosetta 2で開くように設定する bundle update ffiまたはgem update ffiを実行する M1 Macでpod installを実行する 完全なApple Silicon環境は、まだ遠いようだ…。 CocoaPodsで導入したライブラリが原因でシミュレータ向けビルドができない場合 これはM1 Macというよりは、Xcode 12なのかもしれないけど……。 いわゆる以下のエラーの問題。 building for iOS Simulator, but linking in object file built for iOS, for architecture arm64 Intelのときは発生しなかったのに、M1にしたら発生し始めるプロジェクトにも遭遇した。以下のリンクなどをよく参考にさせてもらって、どうにか対応した。 プロジェクトによっては、アプリのターゲット、もしくは、プロジェクト自体の方でも「Excluded Architectures」の設定をしないとビルドできないこともあった。 MintはMINT_PATHとMINT_LINK_PATHの変数を指定して実行する mint実行前に MINT_PATH と MINT_LINK_PATH をセットしておく。このパスは、アクセス権限のあるディレクトリでなければならない。 上記変数を指定しない場合、mintでインストールするパッケージは、デフォルトの /usr/local/lib/mint にインストールしようとする。しかしBig Surでは(M1 Macでは?)、このディレクトリのオーナーがrootに変わっているため、インストールすることができなくなっている。 https://github.com/MakeAWishFoundation/SwiftyMocky/issues/279 https://github.com/yonaskolb/Mint/issues/188 対策として、上のリンクにあるように /usr/local/lib/mint のオーナーを変更する方法もあるが、あまり安全とは言えない。そのため、 MINT_PATHやMINT_LINK_PATHは、自身の管理できるディレクトリ(例えばプロジェクトのディレクトリ等)に置くのが良さそうだ。 例 $ MINT_PATH=${PROJECT_ROOT}/mint/lib MINT_LINK_PATH=${PROJECT_ROOT}/mint/bin `which mint` bootstrap --link Carthageは特に問題なく動いた Carthageもなにかトラップがあるだろうと構えていたが、思いの外、問題なく通過できた。 Fastlaneも特に問題なく動いた 今のところ大丈夫そう。 Node.jsは未チェック Firebase App Distributionをローカルで実行している人には気になるところ。今後、動作チェックしたら加筆しておこう。 Android インストール時、Intel向けのツールはインストールに失敗する 標準インストールでなく、カスタムインストールを選ぶなら、 EmulatorやIntel HAXMのチェックを外すだけで良さそう。 エミュレータはプレビュー版を使う arm-v8a用のイメージがプレビューで公開されている。AVD Managerでエミュレータを新規で作成するときに、arm64-v8a用のイメージを選択できるようになっている。 しかし、2021年5月公開のrev.3のエミュレータイメージでは、起動してもずっとオフラインになる問題がある。Android Studioでビルドができても、デバッグができない。 rev. 2のイメージをダウンロードして対応しよう。 M1 Macは、iOS開発でトラップが多い Androidは比較的スムーズに環境構築ができたが、iOSについては、トラップがめちゃくちゃ多かった……。特に、Ruby周りとCocoaPods周りにトラップが多い。 とはいえ、M1 Macがリリースされてから半年以上になるので、このあたりのトラップに対する解決策はネット上にたくさんある。おかげで安心してM1に移行していけそうだ。 そういえば、これを読んでいる人の中には、OSSだけでなく外部ベンダーが開発したクローズドなライブラリを使っている人もいるだろう。そういったライブラリが引き金になってビルドできない場合もあるようなので、早めにM1 Macで検証しておくのが良いだろう……。
- 投稿日:2021-06-03T19:53:56+09:00
Androidにプリインストールされてるアプリをadbコマンドで無効化した。
adb uninstallでスマホを奇麗にしてやった。 使用者に不要なアプリ入れないでもらえますかね・・。 ここで書いてる内容 Win端末にadbコマンドができるように設定 Androidの開発者モードのOn adbコマンドでアプリを削除(無効化) adbコマンドで削除したアプリを復活 経緯 GPSでナビ運用していたサブスマホがGPS誤検知するようになった。 なので新しいスマホを買うことにした。 性能はそこそこ欲しいけどそんなに予算はない 色々検討した結果、SoftbankのRedmi note 9t 5gが破格だったので買ってみた。 起動してみると画面いっぱいに要らないアプリが。。使いたいアプリを探すのが手間!! ということで、要らないアプリを一掃することにした。 必要なモノ Win端末 Android端末 100均で購入したTypeA-Cケーブル 環境設定 作業したのと違うパソコンで書いてるので画像はほぼない。 Win端末にSDK Platform-Toolsをインストール インストールといっても、パスを通すだけ。 SDK Platform-Toolsの赤枠のをクリックしてダウンロード。 zipを展開して、任意の場所に配置。 配置したパスをコピーしておく。 環境変数にパスを登録するので、スタート>設定>環境変数を編集 新規>パスを入力してOK Winキー+R>cmdと入力してEnterでコマンドプロンプトを起動して adb.exe version でバージョン情報が出たらOK(出なかったらググって解決してください。) Android端末の設定 Android端末で開発者モードをON。 開発者モードは端末によって出し方が違うようだけど、Redmi note(MIUI)は、 設定>デバイス情報>MIUIバージョン連打>追加設定の中に開発者向けオプションが表示された。 開発者モード内のUSBデバッグをONに設定する。 Win端末とAndroidを接続すると、このパソコンでUSBデバッグ許可する?って表示されるので許可する。 接続状態の確認。 いきなり作業するのは怖いので、対象の一覧を表示するコマンドを実行する。 adb.exe shell pm list package 端末にインストールされているアプリがずらずらっと出てくるはず。(出なかったら略) ちなみに、コマンドは実行する前にググってどんなコマンドなのか確認しましょう。 これで作業準備完了。 対象の確認と削除コマンドの準備 要らないアプリを目視で確認>Google Playストアで検索 例えばChromeなら https://play.google.com/store/apps/details?id=com.android.chrome&hl=ja&gl=US このようなURLが出てきた。必要なのはcom.android.chromeこの部分だけ。 不要なアプリを片っ端から調べてSakuraEditorに貼り付けていく。 突然出てきたSakuraEditorはダウンロードしといてください。。編集できればなんでもいいです。 要らないアプリのリストアップが終わったら以下をコピーする。0の後ろの半角スペースを忘れずにコピーしてほしい。 adb.exe shell pm uninstall -k --user 0 SakuraEditorの1行目、左端にカーソルがある状態で「ALT+↓」をリストの一番下まで行ってから 「CTRL+V」をすると こんな感じで準備完了。 いざ削除 とりあえず1行目だけコピーして、実行してスマホからアプリがなくなったことを確認する。 問題ないようなら2行目以降をコピーして実行して一掃! やったー!消えた! というか、邪魔なものを削除(無効化)する機能を消すなよ、と。 うっかり間違えて消しちゃった! 安心してください。復旧できます。 削除(無効化)したアプリの復旧は以下のコマンドでできるらしい。 たぶん、2番目のコマンドの方が良さそう。試してないけど。 adb.exe shell cmd package install-existing com.android.chrome adb.exe shell pm enable --user 0 com.android.chrome おわりに 元々サブスマホはsimなし運用していたんですが、先日の楽天モバイル0円キャンペーンでつい導入しちゃいまして。 楽天はband3と18(18はauから借りてるだけらしい)。 band3は遠くまで届くけど障害物に弱く、band18は障害物に強い特性を持っているらしい。 私の元サブスマホはband18非対応ということで、屋内に入ると圏外になりまくるって状態でした。 → 原因1? bandの電波塔にはそれぞれ固有のIDが割り当てられているらしく、恐らくそのIDを元にgpsの位置を算出しているんだと思われます。 ただ楽天だけは同じIDの電波塔が3か4個か存在するらしく、私の元サブスマホは古いものでそんなことを知る由もなく。 → 原因2? それらが原因でgpsの異常につながったのでは?と邪推してます。 現にsim抜いた元サブスマホは元気にgps表示していますから・・
- 投稿日:2021-06-03T17:32:53+09:00
Androidでgoogle-services.jsonを置かずにFCMからのPushの受信
FCM(Firebase Cloud Messaging)をつかったPush通知の際、 通常、google-services.jsonをappフォルダ以下に配置して実装しているかと思います。 google-services.jsonはFCMのアカウント(およびプロジェクト)に紐づくjsonファイルとなっており、 基本的に1つのFCMアカウントからのメッセージしか受診できません。 同じアプリでユーザーによって送信元のFCMのアカウントを切りかえたいという話があったので その調査のメモ書きになります。 前提事項 前提事項として、google-services.jsonを配置した状態でFCMのPush通知が届いている環境であることを想定しています。 Push通知が届かない場合の問題の切り分けとして、単純に設定不足なのか 今回の設定が効かなかったのかを区別するため上記前提となっております。 なので、google-services.jsonありの状態との差分の説明になります。 google-services pluginのコメントアウト google-services.jsonを探しにいくgoogle-servicesのpluginの読み込みを止めます。 こちらがあるままだと、google-services.jsonを取り除くと google-services.jsonが見つからない旨のエラーになります。 google-services.jsonから必要な情報を取得 後でgoogle-services pluginが行っているgoogle-services.jsonファイルの取得の代わりを実装しますので google-services.jsonから[プロジェクトID]、[アプリケーションID]、[APIキー]をコピーするなりして保存しておいてください。 後で使います。 google-services.json { "project_info": { "project_number": "XXX", "project_id": "[プロジェクトID]", "storage_bucket": "XXX" }, "client": [ { "client_info": { "mobilesdk_app_id": "[アプリケーションID]", "android_client_info": { "package_name": "XXX" } }, "oauth_client": [ { "client_id": "XXX", "client_type": 3 } ], "api_key": [ { "current_key": "[APIキー]" } ], "services": { "appinvite_service": { "other_platform_oauth_client": [ { "client_id": "XXX", "client_type": 3 } ] } } } ], "configuration_version": "1" } google-services.jsonの削除 google-services.jsonを探しに行く仕組みを止めて必要な情報を取得したので、 google-services.jsonファイルをapp以下から取り除きます。 appフォルダ以下にあるgoogle-services.jsonをフォルダを開いて削除します。 FireBaseの初期化の実装 FireBaseの初期化 var options = FirebaseOptions.Builder() .setProjectId("[プロジェクトID]") .setApplicationId("[アプリケーションID]") .setApiKey("[APIキー]") .build() FirebaseApp.initializeApp(this, options) (ソースはkotlinのプログラムになってます。) 上記を最初に起動するActivityのonCreateなどに実装すればpushが届くかと思います。 状況に合わせて[プロジェクトID]、[アプリケーションID]、[APIキー]を変えれば、 FCMのアカウントの切替などができるかと思います。 ※アプリ内の操作をトリガーに上記を呼び出して切り替えをすると、 消したはずのFCMアカウントからもPushが届いたりするので 初回インストール時点での切替にした用が良さげです。 余談 FirebaseApp.initializeApp(this, options)は呼ばないでおくと 起動後しばらくしてExceptionが発生するようです。 操作後にFCMの初期化をしたい場合は、 最初に FirebaseApp.initializeApp(this) を呼び出しておくとExceptionは回避できました。 参考 下記Firebaseのドキュメントを元に試した内容になります。
- 投稿日:2021-06-03T17:32:17+09:00
【Kotlin研修2日目】可変リストデータのリストへの紐付けとダイアログ表示
Adapterクラス アクティビティで記述したリストデータをリスト形式ビュー(ListView/Spinner)に紐づけるクラス。 リスト形式ビューに反映するリストデータを管理し、リスト形式ビューの各Itemに当てはめる役割を担う。 主なAdapterインタフェース インタフェース リストデータ型 ArrayAdapter Array/MutableList SimpleAdapter MutableList<MutableMap<String, *>>XML/JSONデータの解析結果を格納 SimpleCursorAdaptor CursorオブジェクトAndroid端末内DBに対するSELECT文の結果を格納 可変リストデータとListViewの紐づけ 可変のリストデータをListViewに紐づける手順は以下の通り。 可変のリストデータを用意 リストデータを基にAdapterオブジェクトを生成 ListViewのadapterプロパティに、2.で生成したAdapterオブジェクトを代入 また、可変のリストデータをリスト形式ビューに反映する場合は、 XMLでstrings.xmlに記述せず、Kotlinで.ktファイルに記述する必要がある。 リストデータをもつArrayAdapterオブジェクトの生成 定義 参考: ArrayAdapter ArrayAdapter(context: Context, resource: Int, objects: Array<T>) // パラメータ // context: コンテキストとなるアクティビティオブジェクト // resource: リスト形式ビューの各Itemのレイアウトを表現するR値 // objects: リスト形式ビューに反映するリストデータ サンプルコード MainActivity.kt // リストデータの定義 val menuList = mutableListOf("item1", "item2", "item3", ...) // 定義したリストデータをもつAdapterオブジェクトを生成 val adapter = ArrayAdapter( this@MainActivity, android.R.layout.simple_list_item_1, menuList ) R値 resフォルダ内のファイルや、ファイル中に記述されたリソースを識別するためのRクラスの定数。 Javaのint型定数によって表現される。 android.R Android SDKで用意されたリソースを識別するクラス。 android.Rクラスをインポート文で宣言すると、同名であるアプリ内のRクラスが読み込まれなくなるため、 android.RクラスのR値を利用する際はインポートせず、android.R.<R値>のように記述する。 ListViewに対するArrayAdapterオブジェクトのセット ListViewのadapterプロパティに、生成したArrayAdapterオブジェクトを代入する。 サンプルコード MainActivity.kt // Adapterがリストデータを紐づけるオブジェクト(=ListView)の定義 val lvMenu = findViewById<ListView>(R.id.lvMenu) // ListViewにAdapterオブジェクトをセット(adapterプロパティにAdapterオブジェクトを代入) lvMenu.adapter = adapter ダイアログ(Dialog) 参考: ダイアログ ユーザに対して処理続行の確認や注意喚起、追加情報の入力を求めるポップアップウィンドウ。 AlertDialogクラスによって定義される。 ダイアログの画面構成 ダイアログの最小構成 ダイアログを構成するための最小限の部品は以下の2つ。 コンテンツエリア アクションボタン x1(Positive Button) ダイアログの生成 ダイアログ生成クラスの作成 java/<PackageName>配下にKotlin Class/Fileを作成し、作成した.ktファイルに記述する。 ダイアログは汎用性が高いため、privateなメンバクラスでなくトップレベルクラスで宣言する。 java/com.example.listview2/OrderConfirmDialogFragment.kt import androidx.appcompat.app.AlertDialog import androidx.fragment.app.DialogFragment // ダイアログを生成するクラス(DialogFragment()を継承) class OrderConfirmDialogFragment: DialogFragment() { // ダイアログの生成処理 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { Android X Androidアプリ開発で利用するSDKのサポートライブラリ(=機能拡張を目的に追加されたクラス群)を、Google社がまとめたライブラリ。 なお、Android Studioは、Android Xライブラリを標準で利用している。 API 28以降は基本的にAndroid Xライブラリを継承元にする。 ビルダーの生成 参考: AlartDialog.Builder 定義 AlartDialog.Builder(context: Context!) // パラメータ // context: ダイアログを表示するアクティビティオブジェクト(=コンテキスト) サンプルコード java/com.example.listview2/OrderConfirmDialogFragment.kt // ダイアログを生成するクラス(DialogFragment()を継承) class OrderConfirmDialogFragment: DialogFragment() { // ダイアログの生成処理 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { // Dialogオブジェクトの定義 // activity: OrderConfirmDialogFragment()オブジェクトを呼び出すNullableなアクティビティ // ?: セーフコール演算子(nullでない場合に代入する値を"?.let {...}"のブロック{}末尾に記述) // <- ※ブロック{}内では、nullチェック対象は"it"で置換される // また、nullチェック対象がnullである場合は代入されない val dialog = activity?.let { // AlertDialog.Builderオブジェクトの生成 // AlertDialog.Builder(context: Context!) // context: ダイアログを表示するアクティビティオブジェクト(=コンテキスト) // it: let関数ブロック内における、nullチェック対象(=activity)の変数 val builder = AlertDialog.Builder(it) ... } ... } } ビルダー(Builder) 複数のプロパティをもつクラスのオブジェクトを、プロパティをコンストラクタで指定せずに生成するデザインパターン。 ダイアログの表示設定 タイトル // R値を指定して定義する場合 AlartDialog.Builder(Context!).setTitle(titleId: Int) // 文字列を指定して定義する場合 AlartDialog.Builder(Context!).setTitle(title: CharSequence!) コンテンツ // R値を指定して定義する場合 AlartDialog.Builder(Context!).setMessage(messageId: Int) // 文字列を指定して定義する場合 AlartDialog.Builder(Context!).setMessage(message: CharSequence!) アクションボタン // Positive Button // R値を指定して定義する場合 AlartDialog.Builder(Context!).setPositiveButton( textId: Int, listener: DialogInterface.OnClickListener! ) // 文字列を指定して定義する場合 AlartDialog.Builder(Context!).setPositiveButton( text: CharSequence!, listener: DialogInterface.OnClickListener! ) // Negative Button(Positive Button同様、文字列の指定も可能) // R値を指定して定義する場合 AlartDialog.Builder(Context!).setNegativeButton( textId: Int, listener: DialogInterface.OnClickListener! ) // Neutral Button(同上) // R値を指定して定義する場合 AlartDialog.Builder(Context!).setNeutralButton( textId: Int, listener: DialogInterface.OnClickListener! ) // パラメータ // textId, text: ボタンに表示する文字列 // listener: "タップ"イベントを検知するリスナクラスのインスタンス サンプルコード java/com.example.listview2/OrderConfirmDialogFragment.kt // R値を指定してダイアログのタイトルを定義 builder.setTitle(R.string.dialog_title) // R値を指定してダイアログのコンテンツを定義 builder.setMessage(R.string.dialog_msg) // R値を指定してダイアログのアクションボタンを定義 // Positive Button builder.setPositiveButton( R.string.dialog_btn_ok, DialogButtonClickListener() ) // Negative Button builder.setNegativeButton( R.string.dialog_btn_ng, DialogButtonClickListener() ) // Neutral Button builder.setNeutralButton( R.string.dialog_btn_nu, DialogButtonClickListener() ) ダイアログオブジェクトの生成 サンプルコード java/com.example.listview2/OrderConfirmDialogFragment.kt class OrderConfirmDialogFragment: DialogFragment() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val dialog = activity?.let { val builder = AlertDialog.Builder(it) ... // Dialogオブジェクトの生成 // -> 返却型はAlertDialog型 builder.create() } // 生成したDialogオブジェクトを返却 // ?:: エルビス演算子(nullである場合の処理を":"以降に記述) // <- let関数ブロック{}はactivityがnullでない場合の処理であり、 // activityがnullである場合はdialogもnullになる return dialog ?: throw IllegalStateException("Activity is null.") } } ダイアログ(Dialog)のリスナクラスの定義 参考: DialogInterface サンプルコード java/com.example.listview2/OrderConfirmDialogFragment.kt class OrderConfirmDialogFragment: DialogFragment() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { ... } // DialogのAction Buttonの"タップ"イベントを検知するリスナクラス(リスナ) // DialogInterface: 各イベントのリスナインタフェースを子にするインタフェース // OnClickListener: "タップ"イベントを検知するメンバインタフェース private inner class DialogButtonClickListener: DialogInterface.OnClickListener { // "タップ"イベント検知時の処理(イベントハンドラ) override fun onClick(dialog: DialogInterface?, which: Int) { ... } } } アクションボタンに応じたイベントハンドラ 定義 DialogInterface?.OnClickListener.onClick( dialog: DialogInterface!, which: Int ): Unit // パラメータ // dialog: "タップ"を受け取るダイアログ // which: "タップ"されるボタン アクションボタン(=DialogInterface)の定数値 ボタン 定数 Positive DialogInterface.BUTTON_POSITIVE値: -1 Negative DialogInterface.BUTTON_NEGATIVE値: -2 Neutral DialogInterface.BUTTON_NEUTRAL値: -3 サンプルコード java/com.example.listview2/OrderConfirmDialogFragment.kt override fun onClick(dialog: DialogInterface?, which: Int) { // "タップ"されたボタンに応じた分岐処理 when(which) { // Positive Button DialogInterface.BUTTON_POSITIVE -> ... // Negative Button DialogInterface.BUTTON_NEGATIVE -> ... // Neutral Button DialogInterface.BUTTON_NEUTRAL -> ... } ... } ダイアログの表示 定義 MainActivity.kt open fun show( @NonNull manager: FragmentManager, @Nullable tag: String? ): Unit // パラメータ // manager: FragmentManagerオブジェクト(=supportFragmentManagerプロパティ) // tag: ダイアログを識別するための文字列タグ(任意) サンプルコード MainActivity.kt class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { ... } ... // ListViewのItemの"タップ"イベントを検知するリスナクラス(リスナ) private inner class ListItemClickListener: AdapterView.OnItemClickListener { // "タップ"イベント検知時の処理(イベントハンドラ) override fun onItemClick(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { // ダイアログ生成クラスのインスタンス化(実体化) val dialogFragment = OrderConfirmDialogFragment() // 生成したダイアログの表示 dialogFragment.show(supportFragmentManager, "OrderConfirmDialogFragment") } } }
- 投稿日:2021-06-03T15:44:46+09:00
Compose By Exampleを読んでのまとめ
概要 今回は、Google社で提供されているComposeのpathwayの一環として、Compose By Exampleを視聴した内容をまとめていきたいと思います。 参考リンク:https://www.youtube.com/watch?v=DDd6IOlH3io Jetpack Composeとは? まず初めに、Jetpack Composeには以下のような定義があります。 モダンな宣言的UIツールキット Kotlinに基づいている Unbundled(個別に独立している)である また、Compoeの一つの大きなポイントとして"Make the easy things easy and the hard things possible" というのがあり、動画内でも何度も語られていました。 Theming, Layout, Animation サンプル Github: https://github.com/android/compose-samples/tree/34a75fb3672622a3fb0e6a78adc88bbc2886c28f Composeのサンプルとして、何種類かデモアプリが上記のリポジトリで提供されています。 ※ビルドが失敗する場合は、Dependencies.ktの以下のラインを const val androidGradlePlugin = "com.android.tools.build:gradle:7.0.0-beta01" このように変更してみてください↓ const val androidGradlePlugin = "com.android.tools.build:gradle:7.1.0-alpha01" JetChat&Jetsurvey JetchatとJetsurveyプロジェクトは、TextInputやStateManagementなどの基本となるような要素が含まれており、Compose学習の入門用に推薦されているプロジェクトです。 Jetcaster AdancedThemingやAnimationなどが組み込まれた、より複雑なUIのサンプルプロジェクトです。 Owl MaterialDesignやAnimationなどに、フォーカスしたサンプルプロジェクトです。 Theming(テーマ) Jetpack ComposeではMaterialDesignに特化した実装が組み込まれています。、JetPackColorからダークテーマ対応までは "Make the easy things easy" と定義されています。 Themeに関して、color、typography、shapeの3つの引数を定義することによって、任意にカスタマイズすることも可能です。 MaterialTheme( colors = ... typography = ... shapes = ... ) { } サンプルプロジェクトのOwlではこのビルダー関数を使用して、各画面ごとにテーマを作成しています。カスタムThemeを使用することによって、DarkThemeにも柔軟に対応することが可能になっています。 例)https://github.com/android/compose-samples/blob/34a75fb3672622a3fb0e6a78adc88bbc2886c28f/Owl/app/src/main/java/com/example/owl/ui/theme/Theme.kt#L48 Color Composeでは、下記のMaterial color stytemをモデリングしたクラスが用意されています。 コードで表すとこちら↓ val colors = Colors( primary = ... primaryVariant = ... Material Themeでは、Colorsクラスを継承した、ベースラインのカラーパレットから作るデフォルトのビルダー関数もいくつか提供されており、色だけを変更したい場合などに用いることができます。 val lightColors = lightColors( primary = ... secondary = ... Typography 同様に、Material ThemeでデフォルトのTypographyクラスが定義されており、ビルダー関数を用いてカスタマイズするための上書きもできます。 デフォルトで使用する場合↓ kotlin val typography = Typography() TextStyleを用いたカスタマイズの場合↓ val customTypography = Typography( h1 = TextStyle( fontFamily = Rubik, fontSize = 96.sp, fontWeight = FontWeight.Bold, lineHeight = 120.sp, ) h2 = ... subtitle = ... body = ... 図式化したものがこちら↓ Shape Composeでは、small, medium, largeの3種類のサイズのコンポーネントを定義することによって、デフォルトまたはカスタムのShapeを作成することができます。それぞれのサイズの例として、ボタン、カード及びシートなどが挙げられます。 val shapes = Shapes( small = ... medium = ... large = ... 角丸にしたい場合↓(サイズとパーセントの両方でしていできます) small = RoundedCornerShape(size = 4.dp), または small = RoundedCornerShape(percent = 50), 左上の角をカットしたい場合。 small = CutCornerShape(topLeft = 16.dp) 推奨されるアプリでのTheme適応方法 以下のサンプルコードのように、Compasable関数の中に内包する事により、コード上で横断して使えるようにすることができます。 @Compose fun YelloTheme( content: @Composable () -> Unit ) { MaterialTheme( colors = YellowLightColors, typography = OwlTypography, ... ) } なにもThemeが設定されていないコンテンツに対し YellowThemeを適用した結果↓ 以下のように記述することで、画面の一部分だけ違うテーマを適応することもできます。 fun CourseDetails(...) { PinkTheme { ... BlueTheme { RelatedCourses(...) } } } 関連ページの部分だけ、Themeを変更した例↓ テーマの要素について MaterialThemeの要素には、それぞれ型安全にアクセスして利用できます。 Text( text = ... style = MaterialTheme.typography.subtitle1, color = MaterialTheme.colors.(ドットのあとはシステムが自動で補完してくれます) ) 色だけをコピーして利用することもできるので、色のためのハードコーディングを防止し、複数のテーマをサポートすることにも役立ちます。 val background = MaterialTheme.colors.onSurface.copy( alpha = 0.2f ) Surface(color = background) {...} Smart Default Surfaceコンポーネントを例として挙げた時、バックグラウンドの色を設定するとWrapされているコンテンツの色が自動でつくSmart Defaultが適用されます。例えば以下のサンプルではcolor = primaryと設定しているので、ラッピングされているコンテンツの色は自動的にprimary色が適用されます。 同様にComposableなコンポーネントでもこの仕組は適用されます(Floating Action Buttonなど)。 Surface(color = MaterialTheme.colors.primary) { // ここでのデフォルトカラーは `onPrimary` になる。 Text(...) Button(...) } @Composable fun FloatingActionButton(color = MaterialTheme.colors.secondary) { // ここでのデフォルトカラーは `secondary` になる。 } ダークテーマ対応 isSystemInDarkTheme()でダークモードの判別をし、対応したカラーリストを渡すだけで完了です! すごく便利! @Composable fun PinkTheme( darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit ) { if (darkTheme) PinkDarkColors else PinkLightColors ... (おまけ) 判別式では、どのようなことが行われているか調査してみました。 isSystemInDarkTheme()関数の中では、下記のような判定が行われています。 @Composable @ReadOnlyComposable fun isSystemInDarkTheme(): Boolean { val uiMode = LocalConfiguration.current.uiMode return (uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES } Dynamic Theme(ダイナミックテーマ) ここからの内容は、 Make the easy things easy and the hard things possible の the hard things possibleに関わってくる部分の内容になります。 ダイナミックテーマのサンプルとしてJetcasterが用意されており、一番手前に表示されているアルバムに対応した色を取得して、それを動的にテーマに適用しています。 例。 既存のPaletteライブラリを使って画像からdominantColorを取得して、それを使っている。 これでアニメーションもできる。(軽く書いているけどすごい。。) (動画内のコードと現状のコードでは、異なる場合があります) val currentImage = ... val palette = // Paletteライブラリを使って画像からpaletteを取得 val dominantColor = // paletteからdominant colorを取得 val colors = MaterialTheme.colors.copy( primary = animate(dominantColor), ) MaterialTheme(colors = colors) { content() } Layout Colum: コンポーネントを縦に配置する Row: コンポーネントを横に配置する Stack: コンポーネント同士を上に積み重ねる ConstraintLayout: おなじみの制約でレイアウト Modifierでクリックやpadding、toggleable、verticalScroll()、zoomable()などのイベントも付与することが可能です。 カスタムレイアウトを作るには @Composable fun Layout()の使い方。 これを作るにはどうすればよいか。Layoutブロック使うとカスタムレイアウトを作れる https://youtu.be/DDd6IOlH3io?t=869 より 後日補足 Animation(アニメーション) シンプルなアニメーション val radius = animate(if (selected) 28.dp else 0.dp) val shape = RoundedCornerShape(topLeft = radius) Surface( shape = shape ... Transition 今後Android Studioにアニメーションのinspectorが追加される 今後canaryのリリースで、以下のようにアニメーションをキーフレームごとで確認できる機能が追加される予定です。 https://www.youtube.com/watch?v=DDd6IOlH3io より アニメーションのスクショ&テストができる clockTestRuleにアクセスし、時間軸を操作することにより、スクショなどの比較などが可能です。また、それを用いてのアニメーションテストもできます。 private fun compareTimeScreenshot(timeMs: Long, goldenName: String) { // Start with a paused clock composeTestRule.clockTestRule.pauseClock() // Start the unit under test showAnimatedCircle() // Advance clock (keeping it paused) composeTestRule.clockTestRule.advanceClock(timeMs) // 時間を操作する // Take screenshot and compare with golden image in androidTest/assets assertScreenshotMatchesGolden(goldenName, onRoot()) // 保存されている画像とスクショを比較する }
- 投稿日:2021-06-03T15:39:37+09:00
Android の Spinner を簡単に使う
はじめに 数年ぶりに Android のアプリを書こうとしたら、Android Studio は(以前は Eclipse でしたが)随分高機能化したようでしたが、UI 開発周りの IDE での補助が 25 年以上前の MFC にも劣るように感じられました(個人の感想です)。 Android2.x 時代から私が(作って)使っているユーティリティクラスのようなコードのデザインパターンを検索しても見かけなかったので(よく探せばあるかもしれませんが)、ここでは Spinner のユーティリティクラスを紹介します。 Spinnerとは? Win32 のコンボボックス(ドロップダウンリスト)に相当するUIです。 https://developer.android.com/guide/topics/ui/controls/spinner?hl=ja 使い方 クラスのソースコードは最後に付加しておきますので、まずは使い方を説明します。 まずは以下のような準備をします。 private UIDialog.SimpleSpinner m_smplSpin ; // 表示項目 String[] spinNameArray = { "リスト1", "リスト2", "リスト3", ... } ; // 対象のスピナー Spinner spinner = view.findViewById( R.id.対象のスピナー) ; // 準備 int numSpinner = 1 ; // ← "リスト2" を初めに選択する m_smplSpin = new UIDialog.SimpleSpinner ( spinner, numSpinner, spinNameArray , null) ; spinner は findViewById でも、binding でも、何か好きな方法で取得してください。 そして、値を取得したいタイミングで int indexSelected = m_smplSpin.getSelectionIndex() ; を呼び出すと indexSelected に選択された番号(0~)が取得できます。 選択されていないときは -1 になります。 もし、番号が 0 からの連番でない場合には int[] spinNumArray = { 0, -1, 1, ... } ; int numSpinner = 1 ; // ← "リスト3" を初めに選択する m_smplSpin = new UIDialog.SimpleSpinner ( spinner, numSpinner, spinNameArray, spinNumArray ) ; のように、int[] を渡しておくといい感じにやってくれて、結果は同様に getSelectionIndex 又は、 int numSelected = m_smplSpin.getSelectionNum( -2 ) ; で取得します。引数は選択されていないときに返却する値を指定します。 getSelectionIndex は 0~ の連番、getSelectionNum は対応する任意の値を返します。 選択が変更されたときに処理をしたい場合には結局リスナを派生することになりますが、以下のようにします。 m_smplSpin = new UIDialog.SimpleSpinner ( spinner, numSpinner, spinNameArray, spinNumArray new UIDialog.SimpleSpinnerListener() { @Override public void onSelected( SimpleSpinner spinner, int index ) { // 何かの処理... } } } ; これでも一応、値が連番でないときや、選択されていない状態を index に -1 で受け取れるなど、コードの簡略化が出来るなどの利点は一応あります。 大抵の UI 要素はシンプルなデフォルトの挙動で十分なことが多いと思いますのでコードを簡略化できます。 ソース 以下がソースです。 UIDialog と言うクラスの中に入っていますが外に出しても問題無いと思います。 (実際には他にもいろいろ入っていますが、ここでは省略して Spinner 部分だけ記載します) public class UIDialog { // スピナー通知 public interface SimpleSpinnerListener { public abstract void onSelected( SimpleSpinner spinner, int index ) ; } // スピナーアイテム・ユーティリティ public static class SimpleSpinner implements AdapterView.OnItemSelectedListener { private Spinner m_spinner = null ; private int m_selection = 0 ; private String[] m_aTextArray = null ; private int[] m_aNumArray = null ; private SimpleSpinnerListener m_listener = null ; public SimpleSpinner ( Spinner spinner, int iFirstNum, String[] aTextArray, int[] aNumArray ) { m_aTextArray = aTextArray ; m_aNumArray = aNumArray ; setupSpinner ( spinner, iFirstNum, android.R.layout.simple_spinner_item, android.R.layout.simple_spinner_dropdown_item ) ; } public SimpleSpinner ( Spinner spinner, int iFirstNum, String[] aTextArray, int[] aNumArray, SimpleSpinnerListener listener ) { m_aTextArray = aTextArray ; m_aNumArray = aNumArray ; m_listener = listener ; setupSpinner ( spinner, iFirstNum, android.R.layout.simple_spinner_item, android.R.layout.simple_spinner_dropdown_item ) ; } public SimpleSpinner ( Spinner spinner, int iFirstNum, String[] aTextArray, int[] aNumArray, int resSpinnerItem, SimpleSpinnerListener listener ) { m_aTextArray = aTextArray ; m_aNumArray = aNumArray ; m_listener = listener ; setupSpinner ( spinner, iFirstNum, resSpinnerItem, android.R.layout.simple_spinner_dropdown_item ) ; } public SimpleSpinner ( Spinner spinner, int iFirstNum, String[] aTextArray, int[] aNumArray, int resSpinnerItem, int resDropdownItem, SimpleSpinnerListener listener ) { m_aTextArray = aTextArray ; m_aNumArray = aNumArray ; m_listener = listener ; setupSpinner( spinner, iFirstNum, resSpinnerItem, resDropdownItem ) ; } public void setupSpinner ( Spinner spinner, int iFirstNum, int resSpinnerItem, int resDropdownItem ) { m_spinner = spinner ; m_selection = numToSelection( iFirstNum ) ; ArrayAdapter<String> adpStyleList = new ArrayAdapter ( EntisGLS.getMainActivity(), resSpinnerItem, m_aTextArray ) ; adpStyleList.setDropDownViewResource( resDropdownItem ) ; spinner.setAdapter( adpStyleList ) ; if ( (m_selection >= 0) && (m_selection < m_aTextArray.length) ) { spinner.setSelection( m_selection ) ; } spinner.setOnItemSelectedListener( this ) ; } protected int numToSelection( int num ) { if ( m_aNumArray == null ) { return num ; } for ( int i = 0; i < m_aNumArray.length; i ++ ) { if ( m_aNumArray[i] == num ) { return i ; } } return -1 ; } public Spinner getSpinner() { return m_spinner ; } public int getSelectionIndex() { return m_selection ; } public int getSelectionNum( int ifNonSelected ) { if ( m_aNumArray == null ) { return m_selection ; } if ( m_selection >= 0 ) { return m_aNumArray[m_selection] ; } return ifNonSelected ; } @Override public void onItemSelected ( AdapterView<?> parent, View view, int position, long id ) { if ( (position >= 0) && (position < m_aTextArray.length) ) { m_selection = position ; if ( m_listener != null ) { m_listener.onSelected( this, m_selection ) ; } } } @Override public void onNothingSelected (AdapterView<?> parent) { m_selection = -1 ; if ( m_listener != null ) { m_listener.onSelected( this, m_selection ) ; } } } }
- 投稿日:2021-06-03T14:47:26+09:00
viewModelScope.launch { } の Job を公開したくない件
発端 viewModelScope.launch { } を使うときにネストが大きくなるのを避けたいなどの理由で、以下のように = を使って書くことがあります。 fun buy(item: Item): Job = viewModelScope.launch { // ← return type は Job // do something } しかしながら、不用意に Job を公開することになるのが若干気になります(気にならない人は気にならないかもしれない)。 かといって = を使わないと若干不格好な感じがします。 fun buy(item: Item) { viewModelScope.launch { // ネストが大きくなり、本質的なコードがかすんでしまう // do something } } どうすればいいか考える F# という関数型言語には ignore というものがあって、名前の通り関数の返り値を無視することができます。これを Kotlin の世界でも使うことができれば上記の問題を解決できそうです。 let add x y = x + y add 2 3 |> ignore // 返り値 を無視する Kotlin で F# の ignore 相当の関数を作る アイデアはとても簡単です。何もせず Unit を返すだけの拡張関数を作ります。 fun <T> T.ignore() = Unit 解決 先程のignore()を viewModelScope.launch { } の後ろに書いてあげると... できました ? こうすればネストを最小限にしつつ Job を公開せずに済みます。 fun buy(item: String): Unit = viewModelScope.launch { // ← return type が Unit になった! // do something }.ignore() しかも最後に ignore() をつけるかどうかで Job を公開するかどうかを選択することもできていい感じですね!
- 投稿日:2021-06-03T10:30:11+09:00
【Kotlin研修2日目】トーストとListViewへのリスナ定義
トースト 参考: トースト Toastオブジェクトのインスタンス化 定義 Toast.makeText(context: Context, text: CharSequence, duration: Int) // パラメータ // context: トーストを表示させるアクティビティオブジェクト(=Activityインスタンス) // text: トーストで表示する文字列 // duration: トーストを表示する時間 サンプルコード MainActivity.kt val show = "test" val toast = Toast.makeText(this@MainActivity, show, Toast.LENGTH_LONG) 説明 Contextクラス Activityクラスの親クラス。 context thisは自身のインスタンスを指すが、コンパイルエラーや誤作動を避けるためにthis@アクティビティクラス名という記述を行う。 また、Contextクラスが保持するapplicationContextプロパティも利用できるが、コンテキストの指定が不明瞭になるため非推奨。 duration トーストを表示する時間を、Toastクラスの定数を使って指定する。 Toastクラスの定数は以下の2種類。 定数 内容 Toast.LENGTH_LONG 長い Toast.LENGTH_SHORT 短い Toastオブジェクトの表示 サンプルコード toast.show() リスト形式のビュー(ListView, Spinner)のリスナクラス MainActivity.kt class MainActivity: AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { ... } // ListViewのItemをタップした場合のリスナクラス(リスナ) // AdapterView: ListView, Spinnerの親クラス // OnItemClickListener: "タップ"イベントを検知するAdapterViewのメンバインタフェース private inner class ListItemClickListener: AdapterView.OnItemClickListener { // タップ時の処理(イベントハンドラ) // view: (タップされた)ビュー(=ListViewのItem(=TextView)) // position: (タップされた)ItemのIndex // id: (SimpleCursorAdapterを使用する場合)DBの主キー // (それ以外の場合)positionと同じ値 override fun onItemClick(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { // ListViewのItem(TextView)の値をIndex番号から取得, String型に変換 // parent: AdapterView // getItemAtPosition(): 指定したIndexのtext値を取得(返り値はAny型) val item = parent?.getItemAtPosition(position) as String // トーストで表示する文字列 val show = "you chose: " + item // Toast.makeText(context:text:duration:): Toastオブジェクトのインスタンス化 // context: トーストを表示させるアクティビティオブジェクト(=Activityインスタンス) // text: トーストで表示する文字列 // duration: トーストを表示する時間 val toast = Toast.makeText(this@MainActivity, show, Toast.LENGTH_LONG) // トーストの表示 toast.show() } } リスナのセット セッタがリスナプロパティのセッタでない場合 参考: 研修1日目 setOnClickListener()メソッドは、onClickListenerプロパティのセッタではないため、 onClickListenerプロパティにリスナを直接代入(=アクセス)し、リスナとして定義することができない。 MainActivity.kt // リスナとするオブジェクトの定義 val btClick = findViewById<Button>(R.id.btClick) // リスナクラスのインスタンス化(実体化) val listener = HelloListener() // オブジェクトをリスナとして設定 btClick.setOnClickListener(listener) セッタがリスナプロパティのセッタである場合 setOnItemClickListener()メソッドは、onItemClickListenerプロパティのセッタであるため、 onItemClickListenerプロパティにリスナを直接代入することで、自動的にセッタを呼び出し、リスナとして定義することができる。 MainActivity.kt // リスナとするオブジェクトの定義 val lvMenu = findViewById<ListView>(R.id.lvMenu) // 本来はこっち // リスナクラスのインスタンス化(実体化) val listener = ListItemClickListener() // オブジェクトをリスナとして設定 lvMenu.setOnItemClickListener(listener) // 上記の条件を満たす場合(直接リスナプロパティにリスナをセット) lvMenu.onItemClickListener = ListItemClickListener() 使い分け 上記条件に該当する場合は、Android StudioがUse property access syntaxとプロパティへのアクセスで記述するように提案してくれるので、最初から意図的にセッタを呼び出してリスナを定義する。
- 投稿日:2021-06-03T09:41:43+09:00
fvm インストール
公式Document https://fvm.app/docs/getting_started/installation に従って brew tap leoafarias/fvm brew install fvm を実行したがエラー Error: Can't create update lock in /usr/local/var/homebrew/locks! Fix permissions by running: sudo chown -R $(whoami) /usr/local/var/homebrew ==> Tapping leoafarias/fvm fatal: could not create leading directories of '/usr/local/Homebrew/Library/Taps/leoafarias/homebrew-fvm': Permission denied Error: Failure while executing; `git clone https://github.com/leoafarias/homebrew-fvm /usr/local/Homebrew/Library/Taps/leoafarias/homebrew-fvm --origin=origin` exited with 128. このエラーはこの対応を行った sudo chown -R $(whoami) $(brew --prefix)/* 次にCommand Line Tools のバージョンに関するエラーメッセージ Error: Your Command Line Tools are too outdated. Update them from Software Update in System Preferences or run: softwareupdate --all --install --force If that doesn't show you any updates, run: sudo rm -rf /Library/Developer/CommandLineTools sudo xcode-select --install Alternatively, manually download them from: https://developer.apple.com/download/more/. You should download the Command Line Tools for Xcode 12.5. Error: Your Command Line Tools (CLT) does not support macOS 11. It is either outdated or was modified. Please update your Command Line Tools (CLT) or delete it if no updates are available. Update them from Software Update in System Preferences or run: softwareupdate --all --install --force If that doesn't show you any updates, run: sudo rm -rf /Library/Developer/CommandLineTools sudo xcode-select --install Alternatively, manually download them from: https://developer.apple.com/download/more/. You should download the Command Line Tools for Xcode 12.5. 素直にCommand Line Tools for Xcode 12.5. を入れて再実行(Xcode12.5も) brew install fvm fvmのインストールが成功したらfvmからflutterをインストール 利用可能なバージョン fvm releases 1.22.6 と 2.2.1 をインストール fvm install 1.22.6 fvm install 2.2.1 インストールされていることを確認 fvm list 指定のバージョンをグローバルのデフォルトに設定 fvm use 1.22.6 global (fvmの1系では --global だったような気がするので変わった?) PATHにデフォルトのflutterを追加するときは以下を追加 $HOME/fvm/default/bin
- 投稿日:2021-06-03T01:20:53+09:00
文献管理のすすめ
最近困ったことがあります 最近困ったことが起こりました.なんとMendeleyのiosアプリとandroidアプリの提供が廃止されました. それまで行っていた * Mendeleyで文献管理 * Goodnoteで文献に書き込み * Google driveで書き込んだ文献を保存 を行っていたのですが,その素敵環境がなくなりました. その他の環境を求めていた僕にとっては環境を変える転機が来ました. Paperpileを使って見よう Paperpileはchromeの拡張機能として使える文献管理ツールです. 一ヶ月で$3かかるのが少し気になりますが,Endnoteなどは採用していない大学も多く,導入するのに今後のキャリアを考えると少し不安なので,Paperpileを使用することにしました. さらに追加情報として,最近,Paperpileのiosアプリができました. これを使えば前の環境を大幅に崩さずに行けるはず まだ書き途中なので,更新します・・・・(2021/06/03)