投稿

MVVM と Fat View(その 2)

10 ヶ月前の記事「 LVMC (a.k.a. MVVM: ViewModel + DataBinding) と Fat View 」でも、Activity からコードを追い出すのに夢中になっているうちにいつの間にやら Fat View になってしまった話をしたが、最近また同じ症状に陥ってしまった。 今回の場合は、「Activity からコードを追い出す」という状況ではなかったせいもあって、当初気付かなかったのだ。 ImageView に Glide を使ってネットワークから画像を読み込む機能を追加した自前のライブラリーモジュールを作っていて、ViewModel から DataBinding を通じて url 文字列さえ与えれば、ImageView が url から読み込んで表示する。そんな風な設計にしていたのだが、「ライブラリーだし、色々なプロジェクトで使い回す場合に、少しでもコードを省けるように」と、Glide を利用したネットワークからの読み込み処理の部分を、ImageView を継承したカスタム ImageView に追加で盛り込んだ。 しかし、正解としては、ViewModel から DataBinding を通じて与えるのは、url ではなく、Bitmap の方が良い。というのも、そうすれば、ImageView は、それがネットワーク上の画像なのか、ローカルにあるリソースなのか、を意識せず、ただ、layout.xml の app:bitmap="@{viewModel.bitmap}" を見て反応すればいいだけである。url 文字列ならダウンロードする、ローカルなら setImageResource() する、などと処理を分岐させる必要がないのである。 当初 url で実装した時には、わざわざ loadImageUrl() なる専用のメソッド・プロパティを用意して動作するように実装したが、後でローカルの画像を使おうとした場合に setImageResource() と使い分けなけばならないことに気付いて、url 文字列ではなく Bitmap をデータバインドして ViewModel と連絡すべきだと気付いたのである。 もちろん、そうなると、ViewModel 側で Glide を使ってネットワークから画像をダウンロードする部分

Android Studio Wi-Fi デバッグ

イメージ
巷では既に出回っている情報かと思ったが、基本的に「ADB コマンドを使ってムニャムニャする」ものばかりだったので、そうではなくて「コマンド不要の Android Studio 上の操作一発で可能」な方法があるので、紹介する。 ADB Wi-Fi プラグインを使う Preferences の Plugins から ADB Wi-Fi プラグイン をインストールすると、右側のタブに ADB Wi-Fi が追加されるので、この中から、USB で有線接続したデバイスを選んで、Connect するだけ。一旦繋がれば、USB ケーブルを外してよい。 ※もちろん、PC と端末は同じ Wi-Fi ネットワークに接続している必要がある。

Android API Threshold

item min note Dark Theme 29 Adaptive Icons 26 Java 8: OffsetDateTime 26 Java 8: Instant 26 AnimatedStateListDrawable 21 AnimatedVectorDrawable 21

ACR1251DI-NTTCom と macOS Big Sur

イメージ
そろそろ確定申告の時期を迎えて、e-Tax に関して、 M1 Mac の IC カードリーダーの対応が鬼門だ と国税庁が注意を喚起した件について、あちこちで記事になっている。 自分は M1 Mac ではないのだが、そもそも Big Sur にアップデートしてしまったので、その影響もあるのかどうか、既にアップデートしてしまってから気になってきた。 Mac で e-Tax を安心してできるようにと、わざわざ NTT コミュニケーションズのフラッグシップモデルである ACR1251DI-NTTCom を入手したくらいなので、それが迂闊な OS アップデートで差し障りが生じてしまうというのはちょっと残念過ぎる。JPKI(公的個人認証サービス)によると Big Sur はサポート対象外 である。不安が煽られる。 早速、ACR1251DI-NTTCom を MacBook に接続し、JPKI の利用者クライアントソフトを立ち上げてみた。 問題なく立ち上がる。動作チェックをしてみる。 動作チェックも無事終了する。念のため、マイナンバーカードの電子証明書を読み取らせてみたが、正常に機能した。 M1 Mac はともかく、どうやら Big Sur については(ACR1251DI-NTTCom に関する限り)問題ないようだ。

Inkscape on macOS Big Sur

イメージ
迂闊にも macOS Big Sur にアップデートしてしまって、元からインストールしていた MacPorts 版 Inkscape が使えなくなっていることに気付いた。改めてインストールし直そうとしたが…… sudo port install inkscape poppler のビルドエラー しかし、poppler のビルドの失敗で先に進めなくなった。結構、アチコチで報告されている現象のようだ。 poppler のエラーについては、 #61901 poppler 20.12.1: Failed to build (macOS 11.1 Big Sur) の報告がドンピシャで、正解に辿り着けた。 sudo port -f uninstall cctools sudo port -v install cctools +xcode cctools のデフォルトでのインストールの仕方に問題があるらしく、一旦アンインストールして、+xcode オプションを付けてインストールし直せばよい。これで poppler のビルドが通るので、inkscape のインストールがその先に進み、インストールできた。 ただし、注意点があり、従来のように inkscape コマンドから X11 が自動で立ち上がらないので、先に手動で X11 を立ち上げた状態で inkscape コマンドを実行する必要がある。

GIMP on macOS Big Sur

イメージ
迂闊にも macOS Big Sur にアップデートしてしまって、元からインストールしていた GIMP が重くて使い物にならないことに気付いた( GIMP 公式 でも認識されている問題)。改めて最新版をダウンロードしてきてインストールしても改善せず。 そこで代わりに MacPorts 版の GIMP をインストールしようと思いついた。 sudo port install gimp poppler のビルドエラー しかし、poppler のビルドの失敗で先に進めなくなった。結構、アチコチで報告されている現象のようだ。 poppler のエラーについては、 #61901 poppler 20.12.1: Failed to build (macOS 11.1 Big Sur) の報告がドンピシャで、正解に辿り着けた。 sudo port -f uninstall cctools sudo port -v install cctools +xcode cctools のデフォルトでのインストールの仕方に問題があるらしく、一旦アンインストールして、+xcode オプションを付けてインストールし直せばよい。これで poppler のビルドが通るので、GIMP のインストールがその先に進むようになる、 MacPorts 版だからなのかはわからないが、これは X11 アプリとして動く。そのため、macOS の UI との連携性はイマイチで、フォルダー(Documents / Downloads / Desktop が駄目。Pictures なら ok)を開いたりすることができない。一応、画像ファイルから開くアプリを選んで GIMP を立ち上げることはできるが、一旦立ち上がった GIMP からファイルを開くことができないので、2 枚目以降の画像をレイヤーで開いて合わせて利用するようなことができない。究極的には、公式版の Big Sur 対応の改善を待つしかない。

Model-ViewModel-View の最近のイメージ

イメージ
以前にも MVVM について 記事にしたことがあった が、最近のイメージについて整理してみる。 基本的には、Model-ViewModel-View の 3 段構成である点については前の記事の通りなのだが、それぞれの部分において、抽象的ではなくより具体的な性質の違いが見えてきた。絵にすると次のような感じ: Model Model 部分はミキサーに材料を入れてジュースにするようなイメージで、最終的にひとつの結果の出力に到る。Perl や Python 等で手続志向のコンソールプログラミングをする時のようなイメージ。Web スクレイピング等で、データを加工するその複雑な処理過程をプログラミングすることになるが、時間の流れとしては一方向で、またデータオブジェクトも、成果物としては一つである(ミキサーの絵のように、入力される側の要素は、複数でも構わない)。 流れ的には一直線であるが、データの加工工程そのものに重要性がある。 ViewModel ViewModel 部分は時間の流れ的には一方向ではあるものの、Model に生じた更新を伝播させて複数の View 用の LiveData オブジェクトに帰着させる流れを定義する。ここでは伝播が分岐したりし、さらに、オブジェクト同士の依存関係から、前後関係に留意しなければならないので、Model 部分よりは安易ではない。 例えば、View 側にさらすための LiveData オブジェクトが、あるリストとインデックスとそこからリストとインデックスに依存して取り出す一つの要素だったとする。何も考えずにバラバラにそれぞれを LiveData として定義しただけだと、インデックスが最後尾にあった場合に、リストが伸び縮みすると、要素を取り出す時に IndexOutOfBound エラーを引き起してしまう。そのため、まずはリストを更新し、インデックスが新しいリストのサイズに対して問題ないかをチェックして必要な場合は修正し、それからリストとインデックスに従って要素を取り出すという順序になるようにコードを記述する必要がある。つまり、1) リスト 2) インデックス 3) 取り出す要素 という 3 者の先後は必ず守らなければならないものであることがわかる。 View ViewModel で非同期的な部分は吸収できてい

Material Slider 試用

イメージ
いつの間にやら、Material コンポーネントに Slider が追加されていた。以前は、Material デザインの公式サイトにコンセプトとしてのみ掲げられていて、実装が存在しておらず、結局 GitHub で適当なサードパーティライブラリーを探してきて利用するしかなかったが、今や Google 純正の Slider があるのなら、これを使うに越したことはないと思う。早速、使い勝手を見るために、10 分そこらでサンプルを作って試してみた。 サンプルアプリ build.gradle (:app) には依存関係として implementation 'com.google.android.material:material:1.2.1' を記述している。 値の実体としては 0 ~ 1.0f で受け取っており、TextView にはその値をそのまま反映している。吹き出しに表示されるラベルは値をそのまま使用せず、LabelFormatter で値を変換して 0 ~ 100 のインデックスとして表示している。 MainActivity.Java package com.scaredeer.materialslider; import android.os.Bundle; import android.widget.Button; import android.widget.TextView; import androidx.appcompat.app.AppCompatActivity; import com.google.android.material.slider.Slider; public class MainActivity extends AppCompatActivity { private TextView textView; private static final int MAX_INDEX = 100; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.lay

LiveData の observer 側の onChanged() がトリガーされるタイミング

source に対する setValue() がトリガーとなる 漠然とサンプルを参考にして LiveData を使い始めてきたので、複数の LiveData の処理がされるタイミングの前後関係が意図した通りにならずに困ってしまった。 MediatorLiveData や Transformations.map / Transformations.switchMap では lambda 表現で onChanged() を定義するが、この onChanged() の処理が、監視先の source の変更よりも先になってしまう現象が発生し、頭を抱えてしまった。 実際のところ、MediatorLiveData や Transformations.* で潜在的に行っていることにおいて、MutableLiveData が使われており、本来的に、source の変更が observer 側に伝えられるタイミングは、setValue() / postValue() であると明記されている。 いずれの場合でも、setValue() または postValue() を呼び出すことによってオブザーバーがトリガーされ、UI が更新されます。 公式ガイド「 LiveData オブジェクトを更新する 」 つまり、Transformations.* では source 側 onChanged() の末尾に隠蔽されているので曖昧に理解してしまっていたが、MediatorLiveData の onChanged() の場合はその中で source 自身が setValue() / postValue() された直後に、observer 側の onChanged() が実行されることになる。そのため、source 側が MediatorLiveData の場合に、必ずしも source 側の変更が先にならないといった現象が発生していたのである。「source が先、observer が後」ではなく、「source の setValue() / postValue() が先、observer の onChanged() が後」というのが正確だったわけである。 active でないと observe 自体が有効にならない もう一つハマったのが、source が更新されてい

LiveData と MutableLiveData の使い分け

イメージ
observe 関係の場合は、できるだけ ViewModel と View(Activity 含む)の関係を疎にして、一方通行の関係がいいと思う。つまり、ViewModel 内部では MutableLiveData 扱いのデータを LiveData にして外側、つまり View 側に対してさらす形である。 private MutableLiveData<T> mData; public LiveData<T> getData() { return mData; } 一方、user action を ViewModel に入力する場合はどうするのがいいか? この場合は、user action の影響を受ける LiveData は、View/Activity からの入力で変化することが当然であるから、上の observe の場合のように MutableLiveData を getter で隠蔽することは冗長である。なので、public な MutableLiveData を getter を介さずに直接さらしてしまえばいいと思う。 public MutableLiveData<T> mData; そうして、当該 MutableLiveData に関する処理を、無理矢理 ViewModel に置かずに、素直に、View/Activity 内で処理を記述するのが、 実は 適切だと思う。 というのも、user action によって処理を分けたいのは、View/Activity 側の都合であって、ViewModel 側の場合ではないケースが考えられるからである。通常、user action を受け付けるのは、Activity が active すなわち、onResume から onPause の間のライフサイクルにおいてである。なので、その間のみ処理を行って、それ以外のタイミングでは処理を停止しているような形にするのが望ましい。そうなると、それらの処理の主となるコードは、Activity 側の各ライフサイクルに応じたメソッドに配置して、処理の結果変更を受ける変数のみ、MutableLiveData として ViewModel 内に所属させておくのがストレートなコード表現になるわけである。 サンプ