20200207のAndroidに関する記事は8件です。

【Android】Jetpack、AndroidX、Architecture Componentsなどの用語や関係性を整理する

Android の Architecture Components を使ってみたくて情報を調べていたら、「Jetpack」「AndroidX」などの用語も合わせてよく見かけた。Android 開発経験が1年に満たない自分には、まだこれらの意味や関係性がピンと来ていなかったので、この機会に一通り調べて整理してみた。

※自分なりに解釈したものなので、もし間違っているところがあればコメントなどをください:pray:

Android Jetpackとは?

image.png

https://developer.android.com/jetpack

Jetpack は、高品質の Android アプリをデベロッパーが簡単に作成するための一連のライブラリ、ツール、およびガイダンスです。

Android Jetpack とは、Google 公式の「Android 開発のベストプラクティス集」のようなもの。「思想」と言ってもいいかもしれない。「思想」にとどまらず、実際にそのベストプラクティスを実現するためのツールやライブラリも提供されている:pray:

Jetpackの構成要素(コンポーネント)

image.png

https://developer.android.com/jetpack#android-jetpack-components

Jetpack という思想に含まれる構成要素は、上記のスクリーンショットからもわかるように色々ある。大きく4種類に分類される。

  • 基盤 (Foundation) Kotlin向けの拡張、テストなど。
  • アーキテクチャ (Architecture) Jetpackの目玉。ViewModelやLiveData、Navigationなど、実践的なアプリでよく使われるパターンが公式にライブラリ化されている。
  • 動作 (Behavior) Androidが提供するサービス(カメラ、通知など)へのより良いアクセス方法。
  • UI UI向けの便利機能。アニメーション、絵文字、フラグメントなど。

「Architecture Components」は、↑の2番目のコンポーネント群のこと。Architecture Component の詳細は後述する。

AndroidXとは?

https://developer.android.com/jetpack/androidx

AndroidX は Android チームが Jetpack 内でのライブラリの開発、テスト、パッケージ、バージョン管理、リリースに使用しているオープンソース プロジェクトです。

AndroidX とは、Jetpack という「思想」のリファレンス実装のようなもの、と解釈した。androidx.* という名前空間のライブラリ群で構成されていている。

たとえば、Jetpack の一部にナビゲーションというコンポーネントがあるが、AndroidX での実装が androidx.navigation ライブラリとして提供されている。

Android のプラットフォーム API (android.*) を基盤にしているが、それとは独立したバージョニングになっている。

Jetpack、AndroidX、プラットフォーム API の関係は以下のようになる(と考えられる)。

Android Jetpack: 思想

↓ リファレンス実装

AndroidX: ライブラリ(androidx.*)

↓ 基盤

Android プラットフォーム API (android.*)

Android Architecture Components

Android アーキテクチャ コンポーネントは、堅牢でテストとメンテナンスが簡単なアプリの設計を支援するライブラリのコレクションです。

https://developer.android.com/topic/libraries/architecture/

Jetpack の一部で、おそらく一番の目玉が Arcitecture Components。関心の分離、ビューモデル、データバインディングなど、「良い」アプリで実践されているアーキテクチャのパターンがまとめられている。

Jetpack の一部なので、Architecture Components 自体は「考え方」の集合。以下のページを見るとわかりやすい。
https://developer.android.com/jetpack/docs/guide

とはいえ考え方だけでなく、それを実現するための実装にもちゃんと触れられている。

Architecture Componentsの始め方

本筋とは若干逸れるが、Architecture Component についてもう少し掘り下げて調べた時の参考資料も載せておく。

まとめ

  • Android Jetpackが一番大きな概念で、Android開発のベストプラクティス集のようなもの。
  • Jetpackはいくつかのコンポーネント群で構成されていて、そのうちの1つのコンポーネント群が Architecture Component。
  • AndroidXは、Jetpackの思想に則って実際にアプリを作るためのライブラリで、いわば「Jetpackのリファレンス実装」のようなものと考えられる。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Androidの強制アップデートでよく使うアプリバージョン比較方法

現在のバージョンと強制アップデートのバージョンを比較して、処理をするということはよくあります。

重大なバグがあった場合に「このバージョン以上は〜する」みたいなときに使うあれです。

/**
 * バージョン
 *
 * @property major メジャーバージョン
 * @property minor マイナーバージョン
 * @property patch パッチバージョン
 */
class Version(val major: Int, val minor: Int, val patch: Int) : Comparable<Version> {

    /**
     * コンストラクタ
     */
    constructor(major: Int) : this(major, 0, 0)

    /**
     * コンストラクタ
     */
    constructor(major: Int, minor: Int) : this(major, minor, 0)

    /**
     * バージョン(Int)
     */
    private val version = versionOf(major, minor, patch)

    /**
     * バージョン変換
     *
     * @param major メジャーバージョン
     * @param minor マイナーバージョン
     * @param patch パッチバージョン
     * @return Int
     */
    private fun versionOf(major: Int, minor: Int, patch: Int): Int {
        return major.shl(16) + minor.shl(8) + patch
    }

    override fun toString(): String = "$major.$minor.$patch"

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        val otherVersion = (other as? Version) ?: return false
        return this.version == otherVersion.version
    }

    override fun hashCode(): Int = version

    override fun compareTo(other: Version): Int = version - other.version

    companion object {

        /**
         * 文字列のバージョンから生成
         *
         * @param version バージョン(String)
         * @return [Version]
         */
        @Throws(IllegalArgumentException::class, NumberFormatException::class)
        fun from(version: String): Version {
            val parts = version.split(Regex("\\."))
            return when (parts.count()) {
                1 -> Version(parts[0].toInt())
                2 -> Version(parts[0].toInt(), parts[1].toInt())
                3 -> Version(parts[0].toInt(), parts[1].toInt(), parts[2].toInt())
                else -> throw IllegalArgumentException("This is invalid version format. $version")
            }
        }
    }
}

使う側

if (BuildConfig.VERSION_NAME < Version("1.1.0")) {
    // 強制アップデート表示
}

おまけ

こんなけバージョン比較方法を紹介しといてあれなんですが

最近は in-app-updates という機能が追加されました。

ただこれはアプリ内で、アップデートを促進するための機能です。

アップデート画面を閉じることができるので 強制アップデートではないです。

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

[Android] TextView の textScaleX を自動調整する

何を作ったか

限られたスペースしか使えない状況で TextView に長い文字列を表示させようとして、途中で文字列が切れてしまい見た目がよろしくない経験は多々あるでしょう。解決策はいろいろあるでしょうが、ここでは textScaleX と呼ばれる TextView の属性に注目します。表示する文字の横方向への伸縮倍率を表す値であり、1より大きい値なら文字列を伸ばし、逆に1より小さな値なら文字列を収縮させます。TextView の最大横幅に収まり切らない長さの文字列を表示するときに、この textScaleX の値を自動で調節して文字列の長さを圧縮する View を作りました。クラス名は ExpandableTextView とでも呼びましょう。

デモ画像

分かりやすさのため、ExpandableTextView の背景を灰色にしてあります。
expandableTextView_demo.gif

何が出来るか

  • View の最大横幅に合わせて表示する文字列を横方向へ伸縮する(textScaleX = (0.0, 1.0])
  • onLayout() や setText() で状況が変化するたびに自動で調整する
  • textScaleX の下限値を用意して、文字列を最小倍率まで圧縮しても収まらない場合は代替文字列に置換する(末尾を削って"…"に置換)

View 横幅の自動調整の挙動

LayoutParam の layout_width に関して場合分け。XMLで静的に定義する android:layout_width の値のことです。

  • wrap_content
    • 文字列の幅 > View の最大幅:文字列を収縮(textScaleX < 1)させる
    • 文字列の幅 <= View の最大幅:文字列長さ(textScaleX = 1)に View 幅を合わせる
    • ただし、View の横幅の最大値 android:maxWidth を上限とする
  • それ以外:LayoutParam から計算される値で横幅を決定し、この長さを超える文字列は横方向に縮小させて収める

ソースコード

Github に上げておきます。

実装の説明

SDK version = 26 で確認

TextView を継承する

楽に実装したいので TextView を継承して弄り回します。いくつかのメソッドをオーバーライドして振る舞いを改変。

  • setText(CharSequence,BufferType)
    いくつかオーバロードが存在しますが setText(char[],int,int) 以外はすべてここを経由して呼び出されます。残念ながら setText(char[],int,int) は final でオーバーライドできないので、諦めて対象外とします。
  • setMaxWidth(int)
    親クラスのメンバ mMaxWidth が private なので、この setter を通じて指定される値を自身でも記憶しておきます。
  • setLines(int)
    長い文字列を伸縮しつつ1行で表示するための View ですから、行数は1に強制します。
  • setTextScaleX(float)
    View 側で自動で調節するのでユーザには弄らせないように変更。

表示する文字列の制御

許される最大横幅に対し、必要に応じて文字列を伸縮させたり代替文字列に置換する制御を用意

ExpandableTextView#updateText
private Paint mTextPaint;           // 文字を描画するオブジェクト
private CharSequence mDisplayedText // 実際に表示している文字列

private void updateText(CharSequence text, BufferType type, int widthSize){
    mTextPaint.setTextScaleX(1f);
    float length = mTextPaint.measureText(text, 0, text.length());
    int padding = getCompoundPaddingLeft() + getCompoundPaddingRight();
    if ( length + padding > widthSize ){
        float scale = (widthSize - padding) / length;
        text =  modifyText(scale, text, widthSize - padding);
    }
    mDisplayedText = text;
    super.setText(text, type);
}

/**
 * 指定された最大幅に合うように文字列を横方向へ収縮させる.
 * 収縮率が{@link #mMinTextScaleX}を下回る場合は、その最小比率でちょうど最大幅に合うような代替文字列に置換する
 * @param scale     rough value, with which text width can be adjusted to maxLength
 * @param text      raw text
 * @param maxLength max length of text
 * @return modified text
 */
private CharSequence modifyText(float scale, CharSequence text, float maxLength){
    // ソースコード参照
}

具体的に文字列を収縮したり、代替文字列に置換するのは modifyText() 以下になります。注意すべき点として、Paint#measureText で測定される文字列の長さは、

  • Paint#setTextScaleX で指定した比率とは比例しない
  • 文字列の文字数 String#length とは比例しない

ですから、目的の横幅になる倍率 textScaleX を探索するために、適当な当たりをつけて倍率を少しずつ変化させ最善の値を採択します。同様に、最大横幅に文字列が収まる代替文字列を決定するために、1文字ずつ末端から削りながら探索する、という残念な実装方法になってしまいました。コードは汚いのでここには載せません。

View#onMeasure(int,int) で View の横幅を制御する

親の View がこの View を配置するとき、その大きさを計測するために呼び出されるメソッドです。onMeasure()の役割に関しては [Qiita]onMeasureとonLayoutについて理解する が詳しいです。今回興味があるのは横幅に関してのみであり、width に関する measureSpec に要求を追加します。onMeasure() をすべて自前で実装するのは面倒なので、横幅以外はそのまま super#onMeasure() に渡して親クラスに丸投げします。

ExpandableTextView#onMeasure
private Paint mTextPaint;           // 文字を描画するオブジェクト
private CharSequence mCurrentText;  // 表示したい文字列
private CharSequence mDisplayedText;// 実際に表示している文字列
private int mMaxWidth;              // 最大の横幅
private int mRawWidthMeasureSpec;   // 親から指定されたこのViewの大きさに関する要求

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
    // 幅に関してのみ要求を追加 
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);

    CharSequence text = mCurrentText;
    mTextPaint.setTextScaleX(1f);
    float length = mTextPaint.measureText(text, 0, text.length());

    int padding = getCompoundPaddingLeft() + getCompoundPaddingRight();
    int requestedWidth = 0;

    switch ( widthMode ){
        case MeasureSpec.EXACTLY:
            // 表示する文字列に関わらず指定された横幅で固定
            requestedWidth = widthSize;
            // 固定された横幅に合わせて表示する文字列を調整
            updateText(mCurrentText, BufferType.NORMAL, widthSize);
            break;
        case MeasureSpec.AT_MOST:
            int max = Math.min(widthSize, mMaxWidth);
            if ( length + padding > max ){
                // 最大の横幅に収まり切らないなら圧縮
                requestedWidth = max;
                float scale = (max - padding) / length;
                CharSequence modified = modifyText(scale, text, max - padding);
                if ( !mDisplayedText.equals(modified) ){
                    mDisplayedText = modified;
                    super.setText(modified, BufferType.NORMAL);
                }
            }else{
                // 収まるなら文字列の幅にViewの横幅を合わせる
                requestedWidth = (int)Math.ceil(length + padding);
            }
            break;
        case MeasureSpec.UNSPECIFIED:
            // 指定がないなら勝手に決める
            requestedWidth = (int)Math.ceil(length + padding);
            mTextPaint.setTextScaleX(1f);
            mDisplayedText = text;
            break;
    }

    mRawWidthMeasureSpec = widthMeasureSpec;

    // 横幅は固定値として要求を追加する
    int calcWidthMeasureSpec = MeasureSpec.makeMeasureSpec(requestedWidth, MeasureSpec.EXACTLY);

    // onMeasure()の細かい実装は親に任せる
    super.onMeasure(calcWidthMeasureSpec, heightMeasureSpec);
}

表示する文字列・環境の変化に対応する

setText() で表示したい文字列が変化した場合などでも意図したとおり振る舞うための実装。ここで重要な点は TextView が layout() を呼び出すタイミング。通常の TextView はその大きさが中身に依存する場合(LayoutParamでwrap_contentを指定するなどして onMeasure()で MeasureSpec.AT_MOST が指定される場合)、

  • setText() で表示する文字列が変化する
  • setMaxWidth() で最大横幅が変化する
  • setTextScaleX() で textScaleX が変化する

のタイミングで layout() を呼び出し、View の大きさを再計算します。今回は上記に加えて、

  • setMinTextScaleX() で textScaleX の最小値が変化する

のタイミングでも layout() を呼び出すように実装します。

以上は View の大きさが中身に依存する場合の話です。親から MeasureSpec.EXACTLY で横幅が指定されている場合は、updateText() で表示する文字列・伸縮倍率を固定幅に合わせて調節します。

ExpandableTextView
private CharSequence mCurrentText;  // 表示したい文字列
private CharSequence mDisplayedText // 実際に表示している文字列
private int mMaxWidth;              // 最大の横幅
private int mRawWidthMeasureSpec;   // 親から指定されたこのViewの大きさに関する要求
private float mMinTextScaleX;       // textScalex の最小値

@Override
public void setMaxWidth(int width){
    // super#mMaxWidth は private だから、自身でも記録しておく
    if ( width <= 0 ) return;
    if ( width != mMaxWidth ){
        mMaxWidth = width;
        mRequestMeasure = true;
        super.setMaxWidth(width);
        // maxWidth はこのViewの横幅が中身に依存する設定(wrap_contentなど)の場合のみ有効
        // そのような場合では親クラスが layout() を呼び出す
    }
}

/**
 * 文字列の横方向のScaleの最小値を指定する.
 * @param scale in range of (0,1]
 * @see TextPaint#setTextScaleX(float)
 */
public void setMinTextScaleX(float scale){
    if ( scale <= 0 || scale > 1 ) return;
    if ( mMinTextScaleX != scale ){
        mMinTextScaleX = scale;
        if ( MeasureSpec.getMode(mRawWidthMeasureSpec) == MeasureSpec.EXACTLY ){
            // layout() する必要なし
            updateText(mCurrentText, BufferType.NORMAL, MeasureSpec.getSize(mRawWidthMeasureSpec));
        }else{
            // layout() して大きさを再計算
            requestLayout();
        }
    }
}


//#setText(char[], int, int) <- finalでオーバーライド不可
//           以外はここを経由している
@Override
public void setText(CharSequence text, BufferType type){
    if ( text == null ) return;
    if ( mCurrentText != null && mCurrentText.equals(text) ) return;

    mCurrentText = text;

    if ( MeasureSpec.getMode(mRawWidthMeasureSpec) == MeasureSpec.EXACTLY ){
        // 親クラスはlayout()しないし、する必要もない
        updateText(text, type, MeasureSpec.getSize(mRawWidthMeasureSpec));
    }else{
        // 親クラスがlayout()する
        super.setText(text, type);
    }
}

2つの setTextScaleX() の落とし穴

今回の実装では、TextView#getPaint() で取得した文字描画に使う Paint オブジェクトに setTextScaleX() で文字列の伸縮倍率を指定しています。getPaint() には Paint のプロパティを弄るなと注意書きがありますが(原文:Use this only to consult the Paint's properties and not to change them.)、textScaleX に関しては以下のような実装箇所あり。

TextView
public void setTextScaleX(float size) {
    if (size != mTextPaint.getTextScaleX()) {
        mUserSetTextScaleX = true;
        mTextPaint.setTextScaleX(size);
        if (mLayout != null) {
            nullLayouts();
            requestLayout();
            invalidate();
        }
    }
}

private void setText(CharSequence text, BufferType type, boolean notifyBefore, int oldlen) {
    /* 中略 */
    if (!mUserSetTextScaleX) mTextPaint.setTextScaleX(1.0f);   
    /* 中略 */
}

TextView の方の setTextScaleX() から指定しないと textScaleX = 1.0f で上書きされてしまう!

はじめ嵌りました。解決策:一度でも TextView#setTextScaleX() を呼び出せばいい。

ExpandableTextView
public class ExpandableTextView extends AppCompatTextView{

    public ExpandableTextView(Context context, AttributeSet attr, int defaultAttr){
        super(context, attr, defaultAttr);

        /* 中略 */

        // 現在の値と異なる値を指定すればいいので適当に getTextScaleX() / 2f
        super.setTextScaleX(getTextScaleX() / 2f);
    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

YouTube Android Player APIのYouTubePlayerはYouTube公式アプリをキルすると道連れにされる

この記事は

YouTube Android Player APIの謎挙動に振り回されて得た知見を共有する記事です。

問題内容

以下のようなYouTube動画を全画面で流すシンプルなアプリを用意します。

SamplePlayerActivity.kt
class SamplePlayerActivity : YouTubeBaseActivity() {
    private val apiKey = "hogehoge" // 実際には公式で発行されたAPIkeyが必要です
    private val youtubeId = "H_oGi8uuDpA" // 流したい動画ID

    private val videoListener = object : YouTubePlayer.OnInitializedListener { // 初期化リスナーを定義
        override fun onInitializationSuccess(provider: YouTubePlayer.Provider?, player: YouTubePlayer?, wasRestored: Boolean) {
            if(!wasRestored) player?.loadVideo(youtubeId) // restoreされてなければplayerに動画をロードさせる
        }

        override fun onInitializationFailure(provider: YouTubePlayer.Provider?, error: YouTubeInitializationResult?) {
            // 初期化失敗したときの処理、今回は不要
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.player_activity)

        playerView.initialize(apiKey, videoListener) // YouTubePlayerViewを初期化

    }
}
player_activity.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >

    <com.google.android.youtube.player.YouTubePlayerView
        android:id="@+id/playerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        />

</FrameLayout>

動画を表示した状態でYouTubeロゴや関連動画をタップしてYouTube公式アプリに遷移後、YouTube公式アプリのほうをキルして戻ってくると、動画プレイヤーが消え黒い画面が残った状態になります。

problem.gif

YouTubeBaseActivityの他にYouTubePlayerFragment, YouTubePlayerSupportFragmentで実装しても同様の現象が起きます。

何故こうなるのか

どうやら公式アプリをキルするとサンプルアプリ側のYouTubePlayerがrelease()されるようです。
試しにサンプルアプリに戻ってきたところで再度player?.loadvideo(youtubeId)をする処理を追加してみたところ、
Caused by: java.lang.IllegalStateException: This YouTubePlayer has been released
でアプリが終了します。

そもそもこのYouTubePlayer、公式アプリが無効化されていると初期化時にエラーが発生して動画再生できません。

Screenshot_1581070238.png

このことから推察するに、YouTubePlayerは公式アプリの何らかの処理を裏でやりとりして動画再生しており、それがアプリキルによってやりとりが遮断され、結果releaseされたと考えられます。
誰だこんなAPI設計したのは

ちなみに、YouTubePlayerがreleaseされたかどうかの判定は提供されてないため、releaseされたのをトリガーにインスタンスを再生成したりリロードするなどの対応はできません。
誰だこんなAPI(ry

対応策

動画再生するときはIntent投げて公式アプリにやってもらいましょう。

もしくは、特殊ケースかつ軽微な問題なので気にしないのもありです。

どうしても自分のアプリ内で動画再生したい、かつこの問題も解決したい場合は、YouTubeStandalonePlayerを使うと回避できることは確認できています。
(理由は不明…)

ただこのYouTubeStandalonePlayer、カスタマイズ性がほぼ皆無でめちゃくちゃ使いづらいです。(再生中のフルスクリーンの切り替えもできない)
動画再生は公式に任せろというYouTubeさんからの強いメッセージを感じます。

最後に

YouTubeStandalonePlayer以外でも解決できる方法をご存知の方がもしいらっしゃればぜひ教えていただきたいです。

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

【Android/Kotlin】SearchView(検索バー)とちょっと仲良くなって入力文字列を取得できるようになった

概要

Androidアプリで、リスト内検索機能を実装したかったが、検索バーとは面識がなかった。
そこで、検索バーと少し遊んで基本的な使い方を知った。設置方法や、入力した文字列を取得する際に使えそうなメソッドを備忘録的にまとめる。
not_iconified_searchbar.png

画面に検索バーを設置する

xmlSearchViewを記述すれば検索UIを設置できる。

<SearchView
    android:id="@+id/search"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:iconifiedByDefault="false"
    android:queryHint="検索" />

iconifiedByDefaultfalseにして、常に検索の入力欄が表示されている状態にする。デフォルトはtrueで、trueの場合平時はアイコンのみ表示され、アイコンをクリックすると検索バーが出てくる。例えばアプリバーに設置する場合などはtrueにすると良いだろう。

iconifiedByDefault: false
not_iconified_searchbar.png

iconifiedByDefault: true
iconified_searchbar.png

また、queryHintにヒント文字列を設定しておくと、未入力状態の検索バーにヒントが表示される。

入力された文字列を取得する

setOnQueryTextListener()

「入力欄の文字列が変わったとき」と「検索ボタンが押されたとき」のイベントリスナーをセットする。

SearchView.OnQueryTextListener

  • onQueryTextChange(newText: String): Boolean
    ユーザーによって文字列が変更されたときに呼ばれる(一文字や予測変換一つ位の単位)。
    newTextはそのとき入力されている文字列。
  • onQueryTextSubmit(query: String): Boolean
    ユーザーがクエリを送信(検索ボタンをクリック)したときに呼ばれる。
    queryは送信された文字列。

例えば、検索結果をリアルタイムに表示したい場合はonQueryTextChange()に、検索ボタンを押したときに検索結果を表示する(こちらはほぼ100%やると思うが)場合はonQueryTextSubmit()に処理を記述すれば良い。

searchBar.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
    override fun onQueryTextChange(newText: String): Boolean {
        // text changed
        return false
    }
    override fun onQueryTextSubmit(query: String): Boolean {
        // submit button pressed
        return false
    }
})

検索ボタン

文字列を確定させたときはReturnボタンが検索ボタンになる。
submit_button.png

また、setSubmitButtonEnabled()で検索バーの入力欄の端に検索ボタンを表示するかどうか(真偽値)を設定でき、trueで検索ボタンが表示される。
submit_button_enabled.png

setOnQueryTextFocusChangeListener()

「検索バーのフォーカスが変わったとき」のイベントリスナーをセットする。

View.OnFocusChangeListener

  • onFocusChange(v: View, hasFocus: Boolean)
    ビューのフォーカス状態が変わったときに呼ばれる。

例えば、検索バーにフォーカスが当たっている時のみ検索キャンセルボタンを表示する、検索バー以外にフォーカスが移ったときに入力欄をリセットする、などの使い方ができる。

getQuery()

現在入力されている文字列を取得する。
例えば、独自に設置したボタンを押したときに、入力されている文字列を取得するという使い方ができる。

まとめ

  • xmlにSearchViewを記述すれば、検索バーを設置できる。このとき、バー表示にするためにiconifiedByDefaultfalseにする。
  • setOnQueryTextListener()OnQueryTextListenerをセットして、「入力欄の文字列が変わったとき」や「検索ボタンが押されたとき」の処理を設定できる。
  • setOnQueryTextFocusChangeListener()OnFocusChangeListenerをセットして、「検索バーのフォーカスが変わったとき」の処理を設定できる。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Android (Flutter) で Android dependency 'xxx' has different version というエラーが出たときの対処

状況

Flutter のバージョンを上げたらビルドできなくなりました。

Android Studio: 3.5.3
Flutter: 1.12.13+hotfix.7

やったこと

端的に言うと、次の2点です。

  • 依存ライブラリのアップデート
  • Gradleのアップデート
  • guava の問題の解消

以下、詳細です。

依存ライブラリのアップデート

具体的には下のようなエラーが出ていました。

Launching lib/main.dart on Android SDK built for x86 in debug mode...
Running Gradle task 'assembleDebug'...

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':app:preDebugBuild'.
> Android dependency 'androidx.lifecycle:lifecycle-viewmodel' has different version for the compile (2.0.0) and runtime (2.1.0) classpath. You should manually set the same version via DependencyResolution

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.

* Get more help at https://help.gradle.org

BUILD FAILED in 6s

なにやらバージョンが違うらしいので android/app/build.gradle を確認してみると、下図のようにいくつかのライブラリのバージョン古いと警告されていました。

Screenshot_1.png

この警告に従って、バージョンを最新に更新しました。
$kotlin_versionandroid/build.gradleext.kotlin_version で定義されている値を更新します。

念のため File > Invalidate Cache / Restart... を実行しました。

Gradleのアップデート

再度ビルドをすると、下のようなエラーに変わりました。

FAILURE: Build failed with an exception.

* Where:
Build file '/path/to/project/android/app/build.gradle' line: 24

* What went wrong:
A problem occurred evaluating project ':app'.
> Failed to apply plugin [id 'com.android.application']
   > Minimum supported Gradle version is 5.4.1. Current version is 4.10.2. If using the gradle wrapper, try editing the distributionUrl in /path/to/project/android/gradle/wrapper/gradle-wrapper.properties to gradle-5.4.1-all.zip

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.

* Get more help at https://help.gradle.org

BUILD FAILED in 10s
Finished with error: Gradle task assembleDebug failed with exit code 1

Gradle のバージョンが古いと言っています。
ドキュメントによると、5.4.15.6.4 にアップグレードすれば良さそうです。
https://developer.android.com/studio/releases/gradle-plugin#updating-gradle

android/gradle/wrapper/gradle-wrapper.properties を開き、バージョンを書き換え、再度ビルドします。

gradle-wrapper.properties
#distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip
# ↓
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip

guavaの問題の解消

今度は guava まわりの依存解決?がうまくいっていないようで、下記のエラーが出ました。

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':app:checkDebugDuplicateClasses'.
> 1 exception was raised by workers:
  java.lang.RuntimeException: java.lang.RuntimeException: Duplicate class com.google.common.util.concurrent.ListenableFuture found in modules guava-20.0.jar (com.google.guava:guava:20.0) and listenablefuture-1.0.jar (com.google.guava:listenablefuture:1.0)

  Go to the documentation to learn how to <a href="d.android.com/r/tools/classpath-sync-errors">Fix dependency resolution errors</a>.


* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.

* Get more help at https://help.gradle.org

BUILD FAILED in 2s
Finished with error: Gradle task assembleDebug failed with exit code 1

下記記事を参考に、android/app/build.gradledependencies の前に configurations を追加しました。
https://stackoverflow.com/questions/25792398/gradle-transitive-dependency-exclusion-is-not-working-as-expected-how-do-i-get

app/build.gradle
configurations {
    all*.exclude group: 'com.google.guava', module: 'listenablefuture'
}

以上で、従来通りビルドが通るようになりました。

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

Windows の Hyper-V を無効化して Android 開発用に HAXM へ切り替える

はじめに

これまで Hyper-V で仮想マシンを利用していた Windows PC で Android アプリ開発を行うため、折角なので HAXM (Intel Hardware Accelerated Execution Manager) を利用して、Android エミュレータを高速化することにしました。

いろいろと調べてみると、下記制約などがあるとのこと。

  1. CPU が Intel 製であること( Ryzen などの AMD 製 CPU では利用できない)

    → Intel Core i5 なので OK

  2. CPU が仮想化支援機能 (VT-x) をサポートしていること

    → こちらも Intel Core i5 なので OK

  3. BIOS や UEFI で仮想化支援機能 (VT-x) が有効化されていること

    → Hyper-V を利用していたからOK

  4. Windows が仮想化支援機能 (VT-x) を認識していること

    → こちらも Hyper-V を利用していたからOK

  5. Microsoft Hyper-V が有効化されていないこと

    → なんですと!

Microsoft の情報を見てみると、下記文章がありました。

エミュレーターのパフォーマンスのためのハードウェア高速化 (Hyper-V と HAXM)

Windows で最適なエクスペリエンスを実現するには、HAXM を使用して Android エミュレーターを高速化することをお勧めします。 コンピューターで HAXM を使用できない場合は、Windows ハイパーバイザー プラットフォーム (WHPX) を使用できます。

Android Emulator のトラブルシューティング

Intel HAXM および Microsoft HYPER-V の両方を同時にアクティブ化することはできません。 残念ながら、コンピューターを再起動せずに Hyper-V と HAXM を切り替える方法はありません。

Windows 10 April 2018 Update (ビルド 1803) 以降を使用している場合は、Android Emulator のバージョン 27.2.7 以降で (HAXM ではなく) Hyper-V を使用してハードウェアを高速化できるため、Hyper-V を無効にする必要はありません。

ざっくり整理してみました。

  • Windows 10 April 2018 Update (ビルド 1803) 以降を使用している場合は、 Hyper-V と Windows Hypervisor Platform (WHPX) で高速化できる。
  • ただし HAXM には劣るため、Hyper-V を無効化して HAXM での高速化がおススメ。

というわけで、Hyper-V を無効化し、HAXM を有効化していきます。

ちなみに、Hyper-V を無効化せずに HAXM をインストールすると下記のようなエラーが発生します。

This computer does not support Intel Virtualization Technology (VT-x) or it is being exclusively used by Hyper-V. HAXM cannot be installed. 
Please ensure Hyper-V is disabled in Windows Features, or refer to the Intel HAXM documentation for more information.

環境

  • Windows 10 Pro x64 1903
  • Android Studio 3.5.3

Hyper-V を無効化する

  1. 「コントロール パネル」 → 「プログラムと機能」 → 「Windowsの機能の有効化または無効化」で、「Hyper-V」のチェックボックスをはずして「OK」をクリックします。
    01.jpg

  2. 念のために「Windowsの機能の有効化または無効化」で「Windows ハイパーバイザープラットフォーム」のチェックボックスもはずれていることを確認しておきます。
    02.jpg

  3. Windows を再起動します。

    この再起動を必ず行わないと無効化が完了しないので注意すること。

  4. 下記コマンドの実行でも無効化できます。

> Disable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V-Hypervisor

Hyper-V を無効化できない場合は Device Guard と Credential Guard を無効化する

Hyper-V の無効化が失敗する場合は、下記手順で Device Guard を無効化します。

  1. ファイル名を指定して実行で「gpedit.msc」を実行し、「ローカルグループポリシーエディター」を実行します。

  2. 「ローカルグループポリシーエディター」で、コンピューターの構成 → 管理用テンプレート → システム → Device Guard の「仮想化ベースのセキュリティを有効にする」を、無効または未構成に設定します。
    03.jpg

どうしても同居させておきたい場合

  1. 必要に応じてHyper-Vの有効と無効を切り替えます。

  2. 有効化(必ず再起動する)

> bcdedit /set hypervisorlaunchtype auto
  1. 無効化(必ず再起動する)
> bcdedit /set hypervisorlaunchtype off

HAXM をインストールして有効化する

  1. Android Studio を利用している場合は、SDK Manager の SDK Tools タブで「Intel x86 Emulator Accelerator (HAXM installer)」を選択してインストールを行います。

  2. Intel の Web サイト(intel/haxm)から Windows 版の最新のインストーラーをダウンロードしてインストールを行うことも可能です。

  3. メモリーサイズの割り当てでは、無理なく余裕のあるメモリーサイズを設定しておきます。

  4. エミュレーターで動作させるシステムイメージを、SDK Manager の SDK Platform タブで選択してダウンロードします。

    ARM ベースのシステムイメージは高速化されないため、必ず x86 ベースのシステムイメージを選択する必要があります。

HAXM のインストール状況を確認する

HAXM がインストールされている場合は、下記コマンドでHAXMプロセスの状態を確認できます。

> sc query intelhaxm

AVD を作成する

  1. Android Studio の AVD Manager を起動し、「Create Virtual Device」をクリックして、必要な構成のデバイスを選択します。

  2. システムイメージの選択では、x86 Images タブでシステムイメージを選択し、名前などを設定して Android 仮想デバイスを作成します。

    CPU/ABI が x86 ベースであることを確認します。

    04.png

AVD を起動して設定する

  1. 作成した Android 仮想デバイスの「▶」をクリックして起動します。

  2. しばらく待つと Android が起動するので、いくつか設定を行っておきます。

  3. Settingsアプリ → System → Languages & input → Languages → Add a language → 「日本語」を追加し、「日本語」を1番上に移動

  4. 設定アプリ → システム → 言語と入力 → 仮想キーボード → GBoard → 言語 → 「日本語」を追加し、「QWERTY」を選択

  5. 設定アプリ → システム → 日付と時刻 → タイムゾーンの選択 → タイムゾーンの自動設定 = OFF → タイムゾーンの選択 → タイムゾーン → 「東京」を選択

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

【Android-Codelabs】9/36 〜 DataBindingの使い方 〜

2019年にGoogleから,Web上でAndroidを学べるコースが提供されました.(Android Kotlin Fundamentals Course)
この記事では,そのコースを実際にやってみてアウトプットしてみるといった内容です.
何かツッコミなどあれば編集リクエストかコメントかTwitterでいただければ修正いたします :pray:

今回学ぶこと

・DataBindingとはなにか
・DataBindingの使い方
・DataBindingを使うと,findViewById()よりも効率的なビューの呼び出しができるということ

これまでは,MainActivity.ktでビューを参照する場合,findViewById()関数を使っていました.
シンプルなアプリであればこれでも良かったのですが,複雑なビュー階層がある場合,findViewById()はルートから探索を開始して,目的のビューが見つかるまで探索を続けるので,アプリの速度が低下します.

そんな問題を解決するのがDataBindingという技術です.
まず,各ビューへの参照を含むオブジェクトを作成することです.
アプリのバインディングオブジェクトが作成されると,ビュー階層を探索したりするこなく,バインディングオブジェクトを介してビューや他のデータにアクセスすることができます.

2-4-2.jpeg

DataBindingには次のメリットがあります.

findViewById()に比べてコードが短く,読みやすく,保守しやすいこと
・データとビューが明確に分離されていること
・各ビューを取得するために,ビュー階層を1度だけ全て探索すること
(ただし,これはユーザーがアプリを操作している時ではなくアプリの起動時に行われます.)
・ビューにアクセスするための,TypeSafeを取得すること
(TypeSafeとは,コンパイラがコンパイル中に,型を検証し変数に間違った型を割り当てようとすると,エラーが渡されることです)

目指す成果物

ここでは,以前作ったAboutMeというアプリに,DataBindingという技術を実装していきます.
なので,完成したら見た目は同じです.

2-4-1.png

アプリの機能:
・ユーザーがアプリを開くと,名前,ニックネームを入力するフィールド,DONEボタン,スクロール可能なテキストが表示される
・ユーザーはニックネームを入力して,DONEボタンをタップします.
ニックネームを入力するフィールドとDONEボタンは,ニックネームを表示するテキストビューに置き換わります.

アプリがない場合は,ソースコードをここからダウンロードしてください.

ステップ

1. DataBindingを使って,findViewByID()を無くす

ここでは,DataBindingをセットアップし,findViewById()の呼び出しをDataBindingを使っての呼び出しに置き換えていきたいと思います.

まず,GradleファイルでDataBindingを有効にする必要があります.(デフォルトでは有効になっていません)
これは,コンパイル時間が長くなりアプリの起動時間が影響を受ける可能性があるためです.

build.gradle(Module: app)ファイルを開きます.
そして,android{}セクションにこちらを追加します.

dataBinding {
    enabled = true
}

コードを追加できたら,Sync(同期)をしてください.
File>Sync Project with Gradle Filesをクリックします.

次に,DataBindingを使用するには,XMLレイアウトを<layout>タグでラップする必要があります.

activity_main.xml
<layout>
   <LinearLayout ... >
   ...
   </LinearLayout>
</layout>

layoutには以下のタグが含まれてる必要があります.

<layout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto">

次にバインディングオブジェクトの変数をMainActivity.ktに追加し,それを使ってビューにアクセスできるようにします.
onCreate()の前に,こちらの変数を定義します.ActivityMainBindingクラスは,コンパイラによってこのMainActivity専用に作成されます.
この名前は,レイアウトファイルの名前はactivity_main + Bindingから派生しています.

private lateinit var binding: ActivityMainBinding

次に,DataBindingUtilクラスのsetContentView()関数を使って,MainActivity.ktをactivity_main.xmlに紐付けます.
onCreate()のsetContentView()の呼び出しをこのようなコードに置き換えます.

binding = DataBindingUtil.setContentView(this、R.layout.activity_main)

これで,findViewById()の全ての呼び出しをバインディングオブジェクトに置き換えることができます.
バインデングオブジェクトが生成されると,コンパイラはレイアウト内のビューのIDからバインディングオブジェクト内のビューの名前を生成し,それらをキャメルケースに変換します.
例えば,done_buttondoneButtonがバインデングオブジェクトにあたります.

クリックリスナーを実装するには,こちらのようにします.

binding.doneButton.setOnClickListener {
   addNickname(it)
}

他の箇所も同じように修正してみてください.

そして,アプリを実行してみて以前と動作が変わらないことを確認してみてください.

2. ビューにデータをバインドして,データを表示させる

DataBindingでは,データクラスを直接使うこともできます
この手法はコードを,より簡素化し,より複雑なケースに非常に役に立つそうです.

ここでは,文字列リソースを使って名前とニックネームを設定する代わりに,名前とニックネームのデータクラスを作成します.
DataBindingを使って,データクラスをビューで使えるようにしましょう.

まず,データクラス「MyName.kt」を作成します.
新しいKotlinファイルを作成しましょう.

データクラスはこのように定義して,デフォルト値としてからの文字列を使います.

data class MyName(var name: String = "", var nickname: String = "")

次に,activity_main.xmlファイルの<layout>タグと<LinearLayout>タグの間に、<data></data>タグを挿入します.
これはビューとデータを接続する場所で,データタグ内で,クラスへの参照を保持する名前付き変数を定義できます.

次に,タグ内にこちらを追加します.

<variable
       name="myName"
       type="com.example.android.aboutme.MyName" />

name変数に名前をつけるパラメーターを用意します.
Typeパラメーターを追加し,MyNameクラスの完全修飾名にtypeを設定します.
これで、名前に文字列リソースを使用する代わりに、myName変数を参照できるようになりました。

次に,android:text="@string/name”を以下のコードに置き換えます.
@={}は括弧内で,参照されるデータを取得するためのものです.

android:text="@={myName.name}"

これで,レイアウトファイルのデータへの参照が
次に,実際のデータを作成します.

MainActivity.ktファイルで,private変数を定義します.
変数にデータクラス「MyName」のインスタンスを割り当てて名前を渡します.

private val myName: MyName = MyName("Aleks Haecky")

そして,onCreate()内で,こちらを定義して,宣言したmyName変数に値を設定します.
XMLファイルの変数には,直接アクセスすることができないので,バインデングオブジェクトを介してアクセスします.

binding.myName = myName

次に,TextViewのニックネームにもデータクラスを使ってみます.

android:text="@={myName.nickname}"

そして,MainActivity.ktもこのように変更します.

myName?.nickname = binding.nicknameEdit.text.toString()

ニックネームを設定したら、MainActivity.ktで新しいデータでUIを更新する必要があります.
これを行うには,正しいデータで再作成されるように,すべてのバインディング式を無効にする必要があります.

ニックネームを設定した後にinvalidateAll()を追加して,更新されたバインディングオブジェクトの値でUIが更新されるようにします.

binding.apply {
   myName?.nickname = nicknameEdit.text.toString()
   invalidateAll()
   ...
}

アプリを実行してみましょう.以前と同じく動作したら成功です.

まとめ

・findViewById()の代わりに,DataBindingを使うと,レイアウトXMLファイルから直接データにアクセスできる

クイズ

1問目
findViewById()の呼び出しを最小限にしたいのはなぜですか?

・findViewById()呼び出されるたびに、毎回ビュー階層を横断するから
・findViewById() メインスレッドまたはUIスレッドで実行されるから
・この呼び出しは、ユーザーインターフェイスの速度を低下させる可能性があるから
・アプリがクラッシュする可能性は低くなるから

2問目
DataBindingに関して正しい説明はどれですか?

・コンパイル時に2つの離れた情報を接バインドするオブジェクトを作成します.これにより実行時にデータを探す必要がなくなります。
・バインディングを表示するオブジェクトは,バインディングオブジェクトと呼ばれる
・バインディングオブジェクトは、コンパイラによって作成される

3問目
次のうち、DataBindingのメリットではないものはどれですか?

・コードは短く、読みやすく、保守が簡単である
・データとビューは明確に分離されている
・各ビューを取得するために,ビュー階層を1回だけ探索する
・findViewById()の呼び出しは,コンパイラエラーを生成する
・Type safetyはビューにアクセスするためのである

4問目>
<layout>タグの機能は何ですか?

・レイアウト内のルートビューをラップします
・レイアウト内のすべてのビューに対してバインディングが作成されます
・DataBindingを使用するXMLレイアウトの最上位ビューを指定します
<data>内でタグを使用して<layout>変数をデータクラスにバインドします

5問目>
XMLレイアウトでバインドされたデータを参照する正しい方法はどれですか?

android:text="@={myDataClass.property}”
android:text="@={myDataClass}”
android:text="@={myDataClass.property.toString()}”
android:text="@={myDataClass.bound_data.property}”

クイズの正解

1問目 この呼び出しは、ユーザーインターフェイスの速度を低下させる可能性があるから
2問目

3問目 Type safetyはビューにアクセスするためのである
4問目 DataBindingを使用するXMLレイアウトの最上位ビューを指定します。
5問目 android:text="@={myDataClass.property}”

参考資料

ソースコード

Github:
https://github.com/syuheifujita/android-codeLab-fundamental-2-4

言葉の定義

・DataBinding(データバインディング)
findViewById()よりも効率的なビューの呼び出しができる技術
アプリのバインディングオブジェクトを作成して,ビュー階層を探索したりするこなく,バインディングオブジェクトを介してビューや他のデータにアクセスすることができるような技術

・Binding Object(バインディングオブジェクト)
DataBindingが実装されたときに生成されるオブジェクトのことで,各ビューへの参照を含みます

・データクラス
strings.xmlを介しての文字列データのやり取りの代わりに,データクラスを使って値の受け渡しをします
(より複雑なアプリの時に,非常に役に立つそうです.)

CodeLabs by Google

https://codelabs.developers.google.com/android-kotlin-fundamentals/
スクリーンショット 2020-02-05 4.07.59.png

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