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

[Android][Kotlin]Androidの設定値について

指定したターゲット(パッケージ名や利用するサービスの設定)でAndroidアプリケーションをビルドする方法は複数ある。初期のものであったり、それを改善したものであったり。最新の方法だと利点が多いので選択できればいいのだが、歴史が長いソースコードだと、様々な理由から方法を変更できなかったり。

何がベストなのかは状況によって異なるが、まずは知らないことには話にならないということで調べてた方法を列挙してみる。

androidコマンド

最新のAndroid SDKのツールではなくなったコマンド。オプションで指定したAndroidアプリケーションの雛形となるソース一式を生成されるので、例えば、異なるパッケージ名のソースを生成する場合に使っていた。

$ android create project \
    --name プロジェクト名 \
    --target ターゲットID \
    --path プロジェクトを生成するファイル・パス \
    --package パッケージ名 \
    --activity デフォルト・アクティビティ名

代替リソース

条件毎に用意したディレクトリにリソースを置いて切り替える方法。


以下は高密度画面(hdpi)とそれ以外の画面の端末でアイコン画像を切り替える例だ。

res/drawable/icon.png
res/drawable-hdpi/icon.png

hdpiは設定修飾子と呼ばれる識別子だが、どんな設定修飾子があるのか表にしてみる。

設定 修飾子の値 説明
MCC と MNC mcc310 mcc310-mnc004 モバイル カントリー コード(MCC)とモバイル ネットワーク コード(MNC)
言語と地域 en en-rUS b+en+US ISO 639-1言語コード BCP 47言語タグ
レイアウトの方向 ldrtl ldltr ldrtlは右から左。ldltrは左から右。
smallestWidth swdp 使用可能な画面領域の最小寸法で指定。
使用可能な幅 wdp 使用可能な最小の画面幅をdp単位で指定。
使用可能な高さ hdp 使用可能な最小の画面の高さをdp単位で指定。
画面サイズ small normal large xlarge
画面アスペクト long notlong
円形の画面 round notround
広色域 widecg nowidecg
HDR highdr lowdr ハイ ダイナミック レンジ
画面の向き port land
UI モード car desk ... 端末をホルダーに装着したり、取り外す場合
ナイトモード night notnight
画面ピクセル密度 ldpi xxxhdpi ... 画面ピクセル密度(dpi)
タッチスクリーン タイプ notouch finger
テキスト入力方法 nokeys qwerty 12key
ナビゲーション キー navexposed navhidden
ナビゲーション方法 nonav dpad trackball wheel
プラットフォーム Ver. v3 v4 v7 ... API レベル

ローカライズ

設定修飾子を使った代替リソースを利用する方法。例を挙げる。

res/values/strings.xml
res/drawable/
res/values-en/strings.xml
res/values-ja/strings.xml
res/drawable-ja/

文字列リソースを英語と日本語で切り替えるのと、画像を日本語とそれ以外で切り替える例となっている。

ビルド バリアント

ビルド・バリアントは、ビルドタイプとプロダクト フレーバーを組み合わせたも。build.gradleの例だ。

android {
    defaultConfig {
    }
 
    signingConfigs {
        release {}
    }
 
    buildTypes {
        release {
            signingConfig signingConfigs.release
        }
        debug {}
        staging {}
    }
 
    flavorDimensions "api", "mode"
    productFlavors {
        demo {
            dimension "mode"
        }
        full {
            dimension "mode"
        }
 
        minApi24 {
            dimension "api"
        }
        minApi23 {
            dimension "api"
        }
        minApi21s {
            dimension "api"
        }
    }
 
    variantFilter { variant ->
        def names = variant.flavors*.name
        if (names.contains("minApi21") && names.contains("demo")) {
            setIgnore(true)
        }
    }
}

ビルド バリアントの形式は、<product-flavor><Build-Type>となる。

例えば、minApi24DemoDebugは、minApi24とdemoとdebugの組み合わせという意味。この場合、apkはapp-minApi24-demo-debug.apkとなる。

ビルド・バリアントに対応したJavaソースのパスは、例えば src/demoMinApi24/java/ となる。

ソースセットの優先順位の例だ。

  • src/demoDebug/
  • src/debug/
  • src/demo/
  • src/main/

gradle.properties

build.gradleに渡すパラメータを設定できる。

gradle.propertiesの例。

DEMO_STRING="demo string"
DEMO_NUM_DEMO=1234
DEMO_NUM_FULL=5678
DEMO_BOOLEAN=true

build.gradleの例。

android {
    defaultConfig {
        // resの値を追加(AndroidManifest.xml で参照)
        resValue 'string', 'DEMO_STRING', "${DEMO_STRING}"
 
        // BuildConfigクラスのメンバーを追加(Kotlinコードで参照)
        buildConfigField 'boolean', 'DEMO_BOOLEAN', "${DEMO_BOOLEAN}"
    }
    productFlavors {
        demo {
            // BuildConfigクラスのメンバーを追加(Kotlinコードで参照)
            buildConfigField 'long', 'DEMO_NUM', "${DEMO_NUM_DEMO}"
        }
        full {
            // BuildConfigクラスのメンバーを追加(Kotlinコードで参照)
            buildConfigField 'long', 'DEMO_NUM', "${DEMO_NUM_FULL}"
        }
}

リソースの値を設定することでAndroidManifest.xmlで使い値を切り替えたり、BuildConfigクラスのメンバーを設定することで、ソースコードで参照する値を切り替えられる。

【関連情報】
集まれKotlin好き!Kotlin愛好会 vol.20

Cocoa.swift

Cocoa勉強会 関東

Cocoa練習帳

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

Flutter WidgetのKey指定が必要な理由と仕組みについて解説 - 前編

はじめに

Flutterの Widget でたまに指定するKeyについて、何となく必要性は理解できるものの、完全に理解するためにまとめることにしました。

今回は前編として、主にKeyとは何かと、Keyが必要になる理由について解説します。
後編では、Keyの種類一覧やKey指定位置について解説予定です。

Keyとは?

簡単に言うと、ElementからWidgetを識別するためのIDです (RenderObjectもWidgetを参照します…)。Keyは意図的に指定しないとデフォルトではnullです。

必要となるシーンが限定される感じですが、よく紹介されているのは以下ですね。
ただ、必要性が分かるようで分からない感じで、何となく使えている感じがします。

  • ToDoアプリのようなStateをもったWidget郡のソート, 追加, 削除
  • Listのスクロール位置の保存

本記事を最後まで読んで頂くと理解できると思いますが、Widgetツリーの中でノード (あるWidget) を他の場所に移動させる場合など、Widgetツリーと対になるElementツリー側から特定のWidgetを識別する必要がある場合にのみKeyを利用します。

何故Keyが必要になるのか?

サンプルアプリを用いて、Keyの必要性を理解していきます。

タイルを入れ替える簡単なアプリ

FABをクリックする度に2つのタイル (ランダムな色で塗り潰た四角) の位置を入れ替えるだけの簡易アプリです。
ezgif.com-video-to-gif.gif

ソースコードは以下です。
StatelessWidgetを用いてアプリ起動時にランダムな色のタイルを配置し、ボタンが押される度にListの順番を入れ替えるだけです。

ソースコード
import 'package:flutter/material.dart';
import 'dart:math';

class StatelessColorfulTile extends StatelessWidget {
  final Color _color = UniqueColorGenerator.getColor();
  @override
  Widget build(BuildContext context) {
    return Container(
        color: _color, child: Padding(padding: EdgeInsets.all(100.0)));
  }
}

class UniqueColorGenerator {
  static List _colorOptions = [
    Colors.blue,
    Colors.red,
    Colors.green,
    Colors.yellow,
    Colors.purple,
    Colors.orange,
    Colors.indigo,
    Colors.amber,
    Colors.black,
  ];
  static Random _random = new Random();
  static Color getColor() {
    if (_colorOptions.length > 0)
      return _colorOptions.removeAt(_random.nextInt(_colorOptions.length));
    else
      return Color.fromARGB(_random.nextInt(256), _random.nextInt(256),
          _random.nextInt(256), _random.nextInt(256));
  }
}

class SampleAppPageState extends State<SampleAppPage> {
  List<Widget> _tiles;
  @override
  void initState() {
    super.initState();
    _tiles = [
      StatelessColorfulTile(),
      StatelessColorfulTile(),
    ];
  }

  void _swapTiles() {
    setState(() {
      _tiles.insert(1, _tiles.removeAt(0));
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Row(children: _tiles),
      floatingActionButton: FloatingActionButton(
          child: Icon(Icons.sentiment_satisfied), onPressed: _swapTiles),
    );
  }
}

StatelessWidgetの動作

StatelessWidgetの場合、作成されるStatelessElementStateを保有しないため、Widgetは描画毎に必ずrebuild (Widget build(BuildContext context)) が実行されます。つまり、FABを押す度にsetStateをトリガーにして、2つのタイルが再構成 (rebuild) されて、再描画され、結果的にスワップしたように見えます。

また、Flutterは各Widgetをツリー構成で管理しており、Widget/Element/RenderObjectの3つのツリーが構築されます。ざっくり、WidgetがUIパーツの構成/仕様、ElementがそのWidgetの状態管理やRenderObjectとの仲介、RenderObjectElementが描画管理という役割分担で、それぞれのツリーは相互参照の関係になります。

今回のサンプルでのツリー構成を以下に示します。
ただし、説明簡略化のためにRenderObjectElementは割愛します。
スクリーンショット 2020-02-24 15.23.11.png

rebuild時の動きは以下のpackages/flutter/lib/src/widgets/framework.dartのコードを追ってみると分かります。

仕組みを追いかけるなら、サンプルを作成してそれをデバッガでステップ実行しながら動作確認する手法がお勧めです。
Element.updateChildStatelessElement.updateElement.rebuildComponentElement.performRebuildStatelessElement.buildStatelessColorfulTile.buildという順番でコールされます。

StatelessElement.update
class StatelessElement extends ComponentElement {
  /// Creates an element that uses the given widget as its configuration.
  StatelessElement(StatelessWidget widget) : super(widget);

  @override
  StatelessWidget get widget => super.widget;

  @override
  Widget build() => widget.build(this);

  @override
  void update(StatelessWidget newWidget) {
    super.update(newWidget);
    assert(widget == newWidget);
    _dirty = true;
    rebuild(); // ← ここでrebuildがコールされる
  }
}

上記サンプルをStatefulWidgetで作ってみると?

今度はStatelessWidgetからStatefulWidgetに置き換えて動かしてみます。

StatefulWidgetに置き換え
class StatefulColorfulTile extends StatefulWidget {
  StatefulColorfulTile({Key key}) : super(key: key);

  @override
  ColorfulTileState createState() => ColorfulTileState();
}

class ColorfulTileState extends State<StatefulColorfulTile> {
  Color _color;

  @override
  void initState() {
    super.initState();
    _color = UniqueColorGenerator.getColor();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
        color: _color,
        child: Padding(
          padding: EdgeInsets.all(100.0),
        ));
  }
}

class SampleAppPageState extends State<SampleAppPage> {
  List<Widget> _tiles;
  @override
  void initState() {
    super.initState();
    _tiles = [
      //StatelessColorfulTile(),
      //StatelessColorfulTile(),
      StatefulColorfulTile(), // 変更!!
      StatefulColorfulTile(), // 変更!!
    ];
  }

すると、FABを押してスワップしません。
ezgif.com-video-to-gif-2.gif

何故でしょう?その理由こそKeyが必要な理由です。
(個人的には、フレームワークの作り上の都合な産物な気もしますが…)

StatefulWidget (Key指定なし) の動作

StatefulWidgetStatelessWidgetの最大の違いはStateの有無です。

StatefulWidgetsetStateをトリガーにした描画更新処理でElementのrebuildを実施するかどうかの判定に、参照する新旧のWidgetのKeyRuntimeType (クラス名のこと) を参照しています。

ソースコードでは、Element.updateChildとWidget.canUpdateが該当部分になります。

Element.updateChild
@protected
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
  assert(() {
    if (newWidget != null && newWidget.key is GlobalKey) {
      final GlobalKey key = newWidget.key;
      key._debugReserveFor(this);
    }
    return true;
  }());
  if (newWidget == null) {
    if (child != null)
      deactivateChild(child);
    return null;
  }
  if (child != null) {
    if (child.widget == newWidget) {
      if (child.slot != newSlot)
        updateSlotForChild(child, newSlot);
      return child;
    }
    if (Widget.canUpdate(child.widget, newWidget)) { // Key,RuntimeTypeが同じならWidgetの参照だけ更新して終了
      if (child.slot != newSlot)
        updateSlotForChild(child, newSlot);
      child.update(newWidget);
      assert(child.widget == newWidget);
      assert(() {
        child.owner._debugElementWasRebuilt(child);
        return true;
      }());
      return child;
    }
    deactivateChild(child);
    assert(child._parent == null);
  }
  return inflateWidget(newWidget, newSlot);
}
Widget.canUpdate
@immutable
abstract class Widget extends DiagnosticableTree {
  const Widget({ this.key });
  final Key key;

  @protected
  Element createElement();

  @override
  String toStringShort() {
    return key == null ? '$runtimeType' : '$runtimeType-$key';
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.dense;
  }

  // 新旧でRuntimeTypeとKeyが同じかどうかチェック
  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }
}

Key指定しない場合は新旧Widgetどちらもnull、RuntimeTypeは同じクラス名となり、rebuildは実施されず、ElementからWidgetへの参照だけの更新となります。その後のRenderObjectもElementがrebuildされていないため、描画処理が更新されません。

この時点でおかしな状態になっていることが分かります。

以上のことを図示すると以下のようになります。
スクリーンショット 2020-02-24 15.38.56.png

Key追加による解決

先ほどの問題を解決するために、StatefulColorfulTileクラスの引数のKeyにUniqueKey()を追加します。

Key追加
class SampleAppPageState extends State<SampleAppPage> {
  List<Widget> _tiles;
  @override
  void initState() {
    super.initState();
    _tiles = [
      //StatelessColorfulTile(),
      //StatelessColorfulTile(),
      //StatefulColorfulTile(),
      //StatefulColorfulTile(),
      StatefulColorfulTile(key: UniqueKey()), // 変更!!
      StatefulColorfulTile(key: UniqueKey()), // 変更!!
    ];
  }

ezgif.com-video-to-gif.gif

StatefulWidget (Key指定あり) の動作

Keyを指定した場合は、Element.updateChild内の判定で新旧WidgetのKeyが異なるため、rebuild処理が走ります。ただし、rebuildといってもStatelessWidgetとは異なり、Elementツリーの同一階層内で同じKeyのElementを探し、もし存在すればそのElementと位置を入れ替えることで処理を軽量化しています。

図示すると以下のような感じになります。

スクリーンショット 2020-02-24 16.08.43.png

参考文献

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

Flutter WidgetにKeyが必要な理由, 仕組み, 使い方について解説 - 前編

はじめに

Flutterの Widget でたまに指定するKeyについて、何となく必要性は理解できるものの、完全に理解するためにまとめることにしました。

今回は前編として、主にKeyとは何かと、Keyが必要になる理由について解説します。
後編では、Keyの種類一覧やKey指定位置について解説予定です。

Keyとは?

簡単に言うと、ElementからWidgetを識別するためのIDです (RenderObjectもWidgetを参照します…)。Keyは意図的に指定しないとデフォルトではnullです。

必要となるシーンが限定される感じですが、よく紹介されているのは以下ですね。
ただ、必要性が分かるようで分からない感じで、何となく使えている感じがします。

  • ToDoアプリのようなStateをもったWidget郡のソート, 追加, 削除
  • Listのスクロール位置の保存

本記事を最後まで読んで頂くと理解できると思いますが、Widgetツリーの中でノード (あるWidget) を他の場所に移動させる場合など、Widgetツリーと対になるElementツリー側から特定のWidgetを識別する必要がある場合にのみKeyを利用します。

Keyが必要な理由について

サンプルアプリを用いて、Keyの必要性を理解していきます。

タイルを入れ替える簡単なアプリ

FABをクリックする度に2つのタイル (ランダムな色で塗り潰た四角) の位置を入れ替えるだけの簡易アプリです。
ezgif.com-video-to-gif.gif

ソースコードは以下です。
StatelessWidgetを用いてアプリ起動時にランダムな色のタイルを配置し、ボタンが押される度にListの順番を入れ替えるだけです。

ソースコード
import 'package:flutter/material.dart';
import 'dart:math';

class StatelessColorfulTile extends StatelessWidget {
  final Color _color = UniqueColorGenerator.getColor();
  @override
  Widget build(BuildContext context) {
    return Container(
        color: _color, child: Padding(padding: EdgeInsets.all(100.0)));
  }
}

class UniqueColorGenerator {
  static List _colorOptions = [
    Colors.blue,
    Colors.red,
    Colors.green,
    Colors.yellow,
    Colors.purple,
    Colors.orange,
    Colors.indigo,
    Colors.amber,
    Colors.black,
  ];
  static Random _random = new Random();
  static Color getColor() {
    if (_colorOptions.length > 0)
      return _colorOptions.removeAt(_random.nextInt(_colorOptions.length));
    else
      return Color.fromARGB(_random.nextInt(256), _random.nextInt(256),
          _random.nextInt(256), _random.nextInt(256));
  }
}

class SampleAppPageState extends State<SampleAppPage> {
  List<Widget> _tiles;
  @override
  void initState() {
    super.initState();
    _tiles = [
      StatelessColorfulTile(),
      StatelessColorfulTile(),
    ];
  }

  void _swapTiles() {
    setState(() {
      _tiles.insert(1, _tiles.removeAt(0));
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Row(children: _tiles),
      floatingActionButton: FloatingActionButton(
          child: Icon(Icons.sentiment_satisfied), onPressed: _swapTiles),
    );
  }
}

StatelessWidgetの動作

StatelessWidgetの場合、作成されるStatelessElementStateを保有しないため、Widgetは描画毎に必ずrebuild (Widget build(BuildContext context)) が実行されます。つまり、FABを押す度にsetStateをトリガーにして、2つのタイルが再構成 (rebuild) されて、再描画され、結果的にスワップしたように見えます。

また、Flutterは各Widgetをツリー構成で管理しており、Widget/Element/RenderObjectの3つのツリーが構築されます。ざっくり、WidgetがUIパーツの構成/仕様、ElementがそのWidgetの状態管理やRenderObjectとの仲介、RenderObjectElementが描画管理という役割分担で、それぞれのツリーは相互参照の関係になります。

今回のサンプルでのツリー構成を以下に示します。
ただし、説明簡略化のためにRenderObjectElementは割愛します。
スクリーンショット 2020-02-24 15.23.11.png

rebuild時の動きは以下のpackages/flutter/lib/src/widgets/framework.dartのコードを追ってみると分かります。

仕組みを追いかけるなら、サンプルを作成してそれをデバッガでステップ実行しながら動作確認する手法がお勧めです。
Element.updateChildStatelessElement.updateElement.rebuildComponentElement.performRebuildStatelessElement.buildStatelessColorfulTile.buildという順番でコールされます。

StatelessElement.update
class StatelessElement extends ComponentElement {
  /// Creates an element that uses the given widget as its configuration.
  StatelessElement(StatelessWidget widget) : super(widget);

  @override
  StatelessWidget get widget => super.widget;

  @override
  Widget build() => widget.build(this);

  @override
  void update(StatelessWidget newWidget) {
    super.update(newWidget);
    assert(widget == newWidget);
    _dirty = true;
    rebuild(); // ← ここでrebuildがコールされる
  }
}

StatefulWidgetだとどうなるか?

今度はStatelessWidgetからStatefulWidgetに置き換えて動かしてみます。

StatefulWidgetに置き換え
class StatefulColorfulTile extends StatefulWidget {
  StatefulColorfulTile({Key key}) : super(key: key);

  @override
  ColorfulTileState createState() => ColorfulTileState();
}

class ColorfulTileState extends State<StatefulColorfulTile> {
  Color _color;

  @override
  void initState() {
    super.initState();
    _color = UniqueColorGenerator.getColor();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
        color: _color,
        child: Padding(
          padding: EdgeInsets.all(100.0),
        ));
  }
}

class SampleAppPageState extends State<SampleAppPage> {
  List<Widget> _tiles;
  @override
  void initState() {
    super.initState();
    _tiles = [
      //StatelessColorfulTile(),
      //StatelessColorfulTile(),
      StatefulColorfulTile(), // 変更!!
      StatefulColorfulTile(), // 変更!!
    ];
  }

すると、FABを押してスワップしません。
ezgif.com-video-to-gif-2.gif

何故でしょう?その理由こそKeyが必要な理由です。
(個人的には、フレームワークの作り上の都合な産物な気もしますが…)

StatefulWidget (Key指定なし) の動作

StatefulWidgetStatelessWidgetの最大の違いはStateの有無です。

StatefulWidgetsetStateをトリガーにした描画更新処理でElementのrebuildを実施するかどうかの判定に、参照する新旧のWidgetのKeyRuntimeType (クラス名のこと) を参照しています。

ソースコードでは、Element.updateChildとWidget.canUpdateが該当部分になります。

Element.updateChild
@protected
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
  assert(() {
    if (newWidget != null && newWidget.key is GlobalKey) {
      final GlobalKey key = newWidget.key;
      key._debugReserveFor(this);
    }
    return true;
  }());
  if (newWidget == null) {
    if (child != null)
      deactivateChild(child);
    return null;
  }
  if (child != null) {
    if (child.widget == newWidget) {
      if (child.slot != newSlot)
        updateSlotForChild(child, newSlot);
      return child;
    }
    if (Widget.canUpdate(child.widget, newWidget)) { // Key,RuntimeTypeが同じならWidgetの参照だけ更新して終了
      if (child.slot != newSlot)
        updateSlotForChild(child, newSlot);
      child.update(newWidget);
      assert(child.widget == newWidget);
      assert(() {
        child.owner._debugElementWasRebuilt(child);
        return true;
      }());
      return child;
    }
    deactivateChild(child);
    assert(child._parent == null);
  }
  return inflateWidget(newWidget, newSlot);
}
Widget.canUpdate
@immutable
abstract class Widget extends DiagnosticableTree {
  const Widget({ this.key });
  final Key key;

  @protected
  Element createElement();

  @override
  String toStringShort() {
    return key == null ? '$runtimeType' : '$runtimeType-$key';
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.dense;
  }

  // 新旧でRuntimeTypeとKeyが同じかどうかチェック
  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }
}

Key指定しない場合は新旧Widgetどちらもnull、RuntimeTypeは同じクラス名となり、rebuildは実施されず、ElementからWidgetへの参照だけの更新となります。その後のRenderObjectもElementがrebuildされていないため、描画処理が更新されません。

この時点でおかしな状態になっていることが分かります。

以上のことを図示すると以下のようになります。
スクリーンショット 2020-02-24 15.38.56.png

Keyを指定して解決

先ほどの問題を解決するために、StatefulColorfulTileクラスの引数のKeyにUniqueKey()を追加します。

Key追加
class SampleAppPageState extends State<SampleAppPage> {
  List<Widget> _tiles;
  @override
  void initState() {
    super.initState();
    _tiles = [
      //StatelessColorfulTile(),
      //StatelessColorfulTile(),
      //StatefulColorfulTile(),
      //StatefulColorfulTile(),
      StatefulColorfulTile(key: UniqueKey()), // 変更!!
      StatefulColorfulTile(key: UniqueKey()), // 変更!!
    ];
  }

ezgif.com-video-to-gif.gif

StatefulWidget (Key指定あり) の動作

Keyを指定した場合は、Element.updateChild内の判定で新旧WidgetのKeyが異なるため、rebuild処理が走ります。ただし、rebuildといってもStatelessWidgetとは異なり、Elementツリーの同一階層内で同じKeyのElementを探し、もし存在すればそのElementと位置を入れ替えることで処理を軽量化しています。

図示すると以下のような感じになります。

スクリーンショット 2020-02-24 16.08.43.png

参考文献

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

AndroidのTextViewでフォントサイズの変更について考えてみた

はじめに

Androidアプリ開発をしていて、

  • デザイナーさんがデザインを起こしてくれる
  • デザインの数値通りにレイアウトを組む
  • 解像度、ディスプレイサイズの違いに対応する
  • フォントサイズ変更で崩れるところに対応する

までが一連の流れになってきたので、特に4点目に対応することを考えました..
そもそも、デザインが文字サイズの変更や解像度の違いを考慮してくれたら最高に有り難いですが、
基本iPhoneベースのデザインでAndroidを作ることが多いので、そんなことはなく...

4点目は「一旦置いて」、後で「対応しよう」になることが多いので先に対応方法をまとめておきたいと思いました。

試してみたこと

  • LinearLayout と ConstraintLayout の違いの比較
  • Layoutの高さを固定しないようにする
  • AutoSizingTextView

sampleアプリ

ソースはこちら

キャプチャ(フォント標準)

単純なレイアウトですが、この時点で崩れはありません。

default.png

キャプチャ(フォント最小)

小さくするだけなので、崩れはありません。

mini_size.png

キャプチャ(フォント最大)

このあたりで差が生まれてきます。

  • LinearLayout が ConstraintLayoutよりも崩れが大きいように見える(合計のところ)のは、 合計のwidthが固定だから
  • 高さをwrap_contentにして、文字サイズ(20sp + margin)で高さを合わせるのが一番応用が効きそう (ただし、見た目のレイアウトの上下は変わる...スクロールないとダメかも)
  • TextViewAutoSizingは一番いい感じにしてくれそうだけど、全部同じspにはできないから、 どの道ダメなパターンは出てきそう(同じspにするとクラッシュする)。
Ng
                <TextView
                    android:id="@+id/amount3_cl_3"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:gravity="center_vertical|end"
                    android:layout_marginEnd="10dp"
                    android:textSize="30sp"
                    android:autoSizeTextType="uniform"
                    android:autoSizeMaxTextSize="30sp"
                    android:autoSizeMinTextSize="30sp"
                    android:textStyle="bold"
                    android:text="¥3,000"
                    app:layout_constraintEnd_toEndOf="parent"
                    app:layout_constraintTop_toTopOf="parent"
                    app:layout_constraintBottom_toBottomOf="parent"/>

Safe
                <TextView
                    android:id="@+id/amount3_cl_3"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:gravity="center_vertical|end"
                    android:layout_marginEnd="10dp"
                    android:textSize="30sp"
                    android:autoSizeTextType="uniform"
                    android:autoSizeMaxTextSize="32sp"
                    android:autoSizeMinTextSize="20sp"
                    android:textStyle="bold"
                    android:text="¥3,000"
                    app:layout_constraintEnd_toEndOf="parent"
                    app:layout_constraintTop_toTopOf="parent"
                    app:layout_constraintBottom_toBottomOf="parent"/>

max_size.png

まとめ

レイアウトを崩さない対応するならdpにするのが一番楽ですが、
ユーザビリティの低下・ソースが警告で読みにくくなるのであまり採用したくない手法です。

TextViewAutoSizingをきちんと扱えれば一番良さそうですが、
情報が少なくて表示をコントロールするには一苦労しそうです。
そもそも、高さ・幅を固定にする前提のレイアウトや文字が詰まったレイアウトにしないのが一番いいかもしれません。
(キャプチャのうち、3つ目のパターン)

一旦、整理してみましたが、あまりいい方法を整理できませんでした..
ベストプラクティスがあれば共有して欲しいです...

追伸

後、アプリ起動中にフォントを変更すると、
アプリが一度キルされて、再度作成されるのでそれも予期せぬ不具合を生みます...

TextViewAutoSizing公式

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

Material Design motion system (Beta)とAndroidの実装 (Android Material Component 1.2.0-alpha05時点)

きっかけ

Android Material Component 1.2.0-alpha05がリリースされ、これが入ったので見てみました。
image.png

https://github.com/material-components/material-components-android/releases

概要

このドキュメントによると、Material Design’s motion systemのUIのコンポーネントや画面の遷移のパターンは4つから構成されます。
https://material.io/design/motion/the-motion-system.html#transition-patterns

  • Container transform
  • Shared axis
  • Fade through
  • Fade

それぞれの説明と実装方法

基本的にはここにあるとおりですが、ちょっとかいつまんで説明します。
https://material.io/develop/android/theming/motion/#material-container-transform

Container transform

共通の要素がある場合の遷移、リストの一行と詳細画面が関連あるなどの場合。
例:
- DroidKaigiのアプリでいうとセッション一覧とセッション詳細。
- セッション詳細にあるスピーカーの情報とスピーカー詳細。

どんなアニメーション?
AndroidでいうとViewGroupなどコンテナになっている部分をSharedElementTransionするイメージ
speaker.gif

実装方法

MaterialContainerTransformを利用します。MaterialContainerTransformはAndroidXではなくAndroidのプラットフォームにあるandroid.transition.Transitionを継承しています。
これまでは画像など単体のViewをShared Element Transitionとして共有していましたが、この方法ではViewGroupなどを共有します。

MaterialContainerTransformは具体的にどうやってレイアウトを描画するか?

MaterialContainerTransform.TransitionDrawableというのがMaterialContainerTransformの中にあり、そのcanvasに始まりと終わりのViewをdrawすることによって実現しているようです。結構コード読みやすいので、ハマったらコード読むと解決できます。

※ MaterialContainerTransform.TransitionDrawable内
image.png

Fragment AからFragment Bに遷移するときの基本的な実装方法

  • 遷移元フラグメントFragment A transitionNameを遷移元レイアウトにつけ、FragmentNavigatorExtrasで渡して(Navigationを使わないでやる場合は .addSharedElement(view, “shared_element_container”) といった感じでやるみたいです。)、Navigationのextraで渡してあげればOKです。
// 今まではImageViewなどにつけていたが、レイアウトにtransitionNameをつける
binding.root.transitionName =
    "${speaker.id}-${SessionDetailFragment.TRANSITION_NAME_SUFFIX}"
binding.root.setOnClickListener {
    // ViewとTransitionNameの関連付けをextraに入れる。
    val extras = FragmentNavigatorExtras(
        binding.root to binding.root.transitionName
    )
    binding.root.findNavController()
        .navigate(
            SessionDetailFragmentDirections.actionSessionToSpeaker(
                speaker.id,
                SessionDetailFragment.TRANSITION_NAME_SUFFIX,
                null
            ),
            // extraをNavigationで渡す
            extras
        )
}
  • Fragment BのenterTransitionにMaterialContainerTransform()を入れる。
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    sharedElementEnterTransition = MaterialContainerTransform(requireContext())
  • 行き先のルートViewなどに同じTransitionNameをつけてあげる。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    val binding = FragmentSpeakerBinding.bind(view)
    binding.speakerRoot.transitionName = "${navArgs.speakerId}-${navArgs.transitionNameSuffix}"

基本的にはMaterialContainerTransformを使うこと以外はレイアウトを指定したShared Element Transitionを実装すればOKです

プルリクで実装を見たければこちらで見られます。
https://github.com/DroidKaigi/conference-app-2020/pull/810/files

ハマりどころ(今回は詳しく書かないです)

  • exit transitionがデフォルトでAndroidXのtransitionになっていて、enter transitionがプラットフォームのtransitionになるのでクラッシュする。両方プラットフォームかAndroidXのtransitionでなくてはならない。
  • アニメーションする先が画面全体ではない場合(DroidKaigiアプリではToolbarがActivityにあった)、drawingViewIdを使って画面の裏がフェードする範囲を変える必要がある。
  • Transition対象のViewにはbackgroundをつけておかないとうまく動かなかったりする。

MaterialContainerTransform.setDrawDebugEnabledを使うことでアニメーションのデバッグができます。

MaterialContainerTransform.gif

Shared axis

空間やナビゲーションなどで位置関係があるUI要素での切り替え。X Y Z軸でスライドさせるような形で利用でき、要素間の関係を強化できる。
例:
- チュートリアルで下にカルーセルがあって、押したときに横にスクロールされて切り替えるなど。並列の要素で位置関係があるもの。

shared_axis.gif

MaterialSharedAxisというTransitionを利用します。

詳しくはドキュメントやこのあたりの実装を見ていくことで実装できそうです。
https://material.io/develop/android/theming/motion/#shared-axis
https://github.com/material-components/material-components-android/blob/8e8d20c94f3573f7f7418f3971fbbc41a4fa83bd/catalog/java/io/material/catalog/transition/TransitionSharedAxisDemoFragment.java#L101

Fade through

お互いに関係がないUI要素間での切り替えに利用します。BottomNavigationでの切り替えなどに利用します。

fade_through.gif

MaterialFadeThroughというTransitionを利用します。

詳しくはドキュメントやこのあたりの実装を見ていくことで実装できそうです。
https://material.io/develop/android/theming/motion/#shared-axis
https://github.com/material-components/material-components-android/blob/8e8d20c94f3573f7f7418f3971fbbc41a4fa83bd/catalog/java/io/material/catalog/transition/TransitionSharedAxisDemoFragment.java#L101

Fade

例えばFabが表示されたり、Dialogが表示されたり消えたりと言った。UI要素が表示されるときに利用するパターンのようです。
fade.gif

MaterialFadeというTransitionを利用します。
https://material.io/develop/android/theming/motion/#fade
https://github.com/material-components/material-components-android/blob/8e8d20c94f3573f7f7418f3971fbbc41a4fa83bd/catalog/java/io/material/catalog/transition/TransitionFadeDemoFragment.java#L62

まとめ

新しいMaterial Design motion systemではアニメーションの種類がかなり絞られて、また実装的にも簡単にできるように工夫されており、Transitionでハマって時間をめちゃくちゃ吸われるようなことが少なくなりました。
Jetpack Composeにも将来的には同じような仕組みが入るのかなと予想できますが待ってみましょう。

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

AndroidのAudioTrackで繰り返し再生する

はじめに

初投稿です。

Androidでメトロノームアプリを作りたいと思い、
AudioTrackで実装を進めていましたが、かなりハマったので備忘のために残します。

環境

Windows10
AndroidStudio
Kotlin
minSdkVersion:23
targetSdkVersion:28

AudioTrack

MediaPlayer、SoundPool、AudioTrackがありますが、
繰り返し同じリズムを刻むにはAudioTrackが良いようでした。

初期化

AudioTrack.Builder()を使います。

・setUsage:USAGE_MEDIA=音楽など
・setContentType:CONTENT_TYPE_MUSIC=音楽など
 他にも音声やビープ音などもありますが、変更しても何も変わりませんでした。

・setEncoding:ENCODING_PCM_8BIT
・setSampleRate:44100
・setChannelMask:CHANNEL_OUT_MONO
 再生するデータに合わせます。

・setBufferSizeInBytes:バッファサイズ
 再生するデータに合わせます。
 MODE_STREAMの場合は余裕がないと途切れます。2~4倍くらい?

・setTransferMode:MODE_STREAMまたはMODE_STATIC
 MODE_STREAM→バッファにデータを追加して、消費していくイメージ。
 MODE_STATIC→最初にセットしたデータを使いまわす。

var audioTrack: AudioTrack? = null

val SMPL = 44100
val bufSize = SMPL * 2
val mode = AudioTrack.MODE_STATIC

fun init() {
        audioTrack = AudioTrack.Builder()
            .setAudioAttributes(
                AudioAttributes.Builder()
                    .setUsage(AudioAttributes.USAGE_MEDIA)
                    .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
                    .build()
            )
            .setAudioFormat(
                AudioFormat.Builder()
                    .setEncoding(AudioFormat.ENCODING_PCM_8BIT)
                    .setSampleRate(SMPL)
                    .setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
                    .build()
            )
            .setBufferSizeInBytes(bufSize)
            .setTransferMode(mode)
            .build()
}

主要メソッド

各メソッドのイメージがつかみにくかったので、
自分なりに解釈した内容です。
これが分かるまで、呼び出す順番が良くわかりませんでした。

音が出るタイミングは、直観的にはplayだと思い込んでいたのですが、
writeのタイミングでした。

ここでかなりハマりました。

メソッド 目的
play playStateを再生状態(PLAYSTATE_PLAYING)にする。
stop playStateを停止状態(PLAYSTATE_STOPPED)にする。
write データを再生バッファに書き込む。
flush 再生バッファのデータをクリアする。
release AudioTrackオブジェクトを破棄する。

音を再生する

呼び出し元ではinit→readyの後、playを繰り返します。

MODE_STATICの場合

前処理でデータをセットして、
あとはreloadStaticData()で繰り返し再利用する感じです。

同じパターンを繰り返し再生するメトロノームは、
こちらが適しているようでした。

毎回stop()していますが、stop()なしでも連続再生できます。
今回の要件的に必要なので入れています。

ループ再生もできますが、
再生タイミングとUI操作を同期する要件があるので使用していません。

    //前処理
    fun ready(arr: ByteArray) {
        //バッファにあらかじめデータをセットしておく
        audioTrack?.write(arr, 0, arr.count(), WRITE_BLOCKING)
    }

    //再生
    fun play(arr: ByteArray) {
        if (audioTrack?.playState == AudioTrack.PLAYSTATE_PLAYING) {
            //再生中の場合は止める
            audioTrack?.stop()
            //再生バッファをクリアする
            audioTrack?.flush()
        }

        //読み込み済データを再度読み出す
        audioTrack?.reloadStaticData()
        //再生バッファにデータを書き込む
        audioTrack?.write(arr, 0, arr.count(), WRITE_BLOCKING)

        //再生中にする
        audioTrack?.play()
    }

MODE_STREAMの場合

再生完了時にいったんstop()しないと音が出なくなります。

また、バッファの中身が枯渇すると以下のエラーが出て、1回分の再生がスキップされます。
「releaseBuffer() track %p name=%s disabled due to previous underrun, restarting」

前処理でバッファを埋めておくことで一応解決しましたが、
この方法で良いのかどうかわかりません。。

    //前処理
    fun ready(arr: ByteArray) {
        if (audioTrack?.playState == AudioTrack.PLAYSTATE_PLAYING) {
            //再生中の場合は止める
            audioTrack?.stop()
            audioTrack?.flush()
        }

        if (audioTrack != null) {
            //バッファを埋めておく
            val loopCount = bufSize / arr.count()
            for (i in 0 until loopCount) {
                audioTrack?.write(arr, 0, arr.count(), WRITE_BLOCKING)
            }
        }
    }

    //再生
    fun play(arr: ByteArray) {
        //再生バッファにデータを書き込む
        audioTrack?.write(arr, 0, arr.count(), WRITE_BLOCKING)

        //1回分の再生終了を検知して停止する
        audioTrack?.setPlaybackPositionUpdateListener(
            object : AudioTrack.OnPlaybackPositionUpdateListener {
                override fun onPeriodicNotification(track: AudioTrack){
                }
                override fun onMarkerReached(track: AudioTrack) {
                    //再生完了
                    if(track.playState == AudioTrack.PLAYSTATE_PLAYING){
                        //停止
                        track.stop()
                    }
                }
            }
        )

        //再生終了検知するためのNotificationをセット
        audioTrack?.setNotificationMarkerPosition(arr.count())

        //再生状態にする
        if (audioTrack?.playState != AudioTrack.PLAYSTATE_PLAYING) {
            audioTrack?.play()
        }
    }
```kotlin:


停止・終了(共通)

    //停止
    fun stop() {
        if (audioTrack?.playState == AudioTrack.PLAYSTATE_PLAYING) {
            //再生中の場合は止める
            audioTrack?.stop()
            //再生バッファをクリアする
            audioTrack?.flush()
        }
    }

    //終了時にオブジェクトを破棄する
    fun release() {
        try {
            this.stop()

            audioTrack?.release()
            audioTrack = null

            Logger.set(LogKind.INFO, "sound released")
        } catch (e: Exception) {
            Logger.set(LogKind.ERROR, e.toString())
        }
    }

WRITE_BLOCKINGとWRITE_NON_BLOCKING

writeメソッドでWRITE_BLOCKINGとWRITE_NON_BLOCKINGが指定できます。

WRITE_BLOCKINGは前の再生が終わるまで次の再生を待ち、
WRITE_NON_BLOCKINGは前の再生が終わってなくても次の再生をする感じでした。

WRITE_NON_BLOCKINGにすると、繰り返す際に
最後の音と最初の音がかぶって再生されることがありました。

今回の要件ではBPMを保ち、一定の間隔で繰り返し再生する必要があるため、
WRITE_BLOCKINGで再生のたびに停止→再生することで開始のタイミングがずれないようにしています。

おわりに

情報が少ない&古いものが多く、手探りでの実装となりましたので、
より良い方法をご存知の方は是非、教えていただけると幸いです。

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

JSON 以外のデータ形式を Retrofit で扱う(なら Converter を実装する)

概要

JSON 以外のデータ形式を Retrofit で扱う方法について述べます。時間がない方は Retrofit の公式 を見てください。

前提

Retrofit2 の使い方については知っているものとします。

なぜやるのか?

趣味で RSS リーダーを自作したいと思った際に、XML を Retrofit で扱うにはどうしたら良いのかとわからなくなり、だったら自分で実装しようと思ってやってみたので、それを残しておきます。

RSS は XML の派生形式です。JSON ではありません。かつては、Retrofit で XML を扱う際は Simple XML を使うのが一般的のようでした。

Deprecation

……が、いま SimpleXmlConverterFactory をコードに書くと Deprecated の警告が出ます。

リポジトリーを見てみると、

@deprecated we recommend switching to the JAXB converter.

「JAXB の Converter を使いましょう」とのことです。

does not work on Android.

ではと思って JAXB Converter を見に行くと、README に以下の記述があります。

Note that JAXB does not work on Android.

ちょっと何て書いてあるか英語がよくわからないですね。 Android で XML を処理したいという開発者のニーズは存在しなくなったのでしょうか?

Issue

どうしたらよいのかと思って調べたら、別に悩んでいる人がいました
……が、特に解決策があるわけでもないようです。

Solution?

もうちょっと探したら、以下のQ&Aを発見しました。 Tickaroo TikXML というものがあるらしいです。
What kind of XmlConverter can I use for Retrofit in Android?

GitHub Repository

Tickaroo/tikxml

Retrofit2 用の Converter も用意されています。

implementation 'com.tickaroo.tikxml:retrofit-converter:0.8.15'

普通の方はこれを用いると良いでしょう。

My solution?

上の Converter の導入が上手くいかないので諦め、ほかにどうしたらよいのかと思って調べたら、
faruktoptas/RetrofitRssConverterFactory という Converter を見つけました。 これを見ると、自分で Converter を作ればどんなレスポンスでもオブジェクトに Mapping できるようです。

RSS のパースに関しては過去に自分で作った Parser があるので、せっかくだからそれを使ってみました。

Official solution

これは大発見でも何でもなく、 Retrofit の公式 にちゃんと書いてありました。

If you need to communicate with an API that uses a content-format that Retrofit does not support out of the box (e.g. YAML, txt, custom format) or you wish to use a different library to implement an existing format, you can easily create your own converter. Create a class that extends the Converter.Factory class and pass in an instance when building your adapter.

というわけで、HTTP 通信で JSON 以外のデータをやり取りするアプリでは、 Retrofit の Converter をがんばって作りましょう。もしくは JSON に置き換えましょう。

実装

Converter の実装

今回重要なのはここです。retrofit2.Converter インターフェイスを実装します。
convert メソッドで、受け取った OkHttp3 の ResponseBody を特定の型に変換します。
この処理を変えることで、あらゆるデータ形式を扱うことができるようになります。

class RssResponseConverter : Converter<ResponseBody, Rss> {

    private val parser = Parser()

    override fun convert(responseBody: ResponseBody): Rss? {
        val bodyString = responseBody.string()
        return parser.parse(bodyString.split(findSplitter(bodyString)))
    }

    private fun findSplitter(body: String) =
            if (body.contains(LINE_SEPARATOR)) LINE_SEPARATOR else "\n"

    companion object {
        private val LINE_SEPARATOR = System.getProperty("line.separator")
    }
}

Parser

今回は勉強だと思ってゴリゴリのパーサーを自作しました。実際の開発は既存のライブラリーを用いましょう。

Converter.Fatctory の実装

先ほど実装した Converter のインスタンスを生成するファクトリーを返すクラスです。

class RssConverterFactory private constructor(): Converter.Factory() {

    override fun responseBodyConverter(
            type: Type?,
            annotations: Array<Annotation>?,
            retrofit: Retrofit?
    ): Converter<ResponseBody, *> = RssResponseConverter()

    companion object {

        fun create() = RssConverterFactory()
    }
}

Converter の利用

あとは addConverterFactory で自作の ConverterFactory を指定して、Retrofit を普通に使いましょう。Retrofit の普通の使い方については多数の記事があるので、ここでは特に触れません。

class RssReaderApi {

    private val converter = RssConverterFactory.create()

    @WorkerThread
    operator fun invoke(rssUrl: String): Rss? {
        val uri = rssUrl.toUri()
        val retrofit = Retrofit.Builder()
                .baseUrl("${uri.scheme}://${uri.host}")
                .addConverterFactory(converter)
                .build()

        val service = retrofit.create(RssService::class.java)
        val call = service.call(rssUrl)
        return call.execute().body()
    }
}

一応書いておくと RssService インターフェイスはこんな感じです。

interface RssService {

    @GET
    fun call(@Url url: String): Call<Rss?>
}

おわりに

JSON 以外のデータ形式、今回は XML を Retrofit で扱う方法について述べました。

で、これを知って何の役に立つの?

自分で Converter を作れば、どんなデータ形式でも(CSVやYAML等々)扱うことができるということだけ知っておけば良いかと思います。
新しいデータ形式や社内独自のデータ形式も Retrofit で扱えて幸せになるかもしれません。

そして、メジャーなデータ形式であれば、誰かが Converter を作っているかもしれないので、それを GitHub から探し出した方が早いかもしれない、ということを知っておくのも大事なことです。

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

BaseAdapter で ViewHolder パターンを実装する

概要

BaseAdapter で ViewHolder を実装する方法について述べます。主に Spinner を扱う際に役立ちます。

なぜやるのか?

Spinner と BaseAdapter を組み合わせた実装だと、LayoutInfrater.inflate する行で Android Studio の警告が出ます。

waning.png

Unconditional layout inflation from view adapter: Should use View Holder pattern (use recycled view passed into this method as the second parameter) for smoother scrolling.

別にこれを放置してもビルドはできますし、Spinner でスクロールのパフォーマンスが致命的になるほど要素を詰め込むことはそうそうないので、直さなくても構わないのではないかと思ってしまいがちです。実際私もずっと放置していました。

(ちなみに、リンク先を開くと RecyclerView のドキュメント が出てきます)

……が、昨今では技術者の評価に GitHub のリポジトリーを見ることも増えてきているようです。こうした雑なコードを残しておくのは自身の今後に悪い影響を与えかねないため、直しておくべきでしょう。

というのは冗談です。ViewHolder パターン自体は普遍的なデザインパターンですので、覚えておいて損はしません。私も RecyclerView を使い始めるまでは何度か書いていました。もう数年前になるので完全に忘れていましたが、せっかくなので思い出します。

BaseAdapterで画像とテキストをListView表示

実装

1. ViewHolder クラスを定義

各 View を持つクラスを定義します。これは Kotlin だと簡単です。Item を受け取って属性を各 View に当てる関数もこのクラスに実装させると良いでしょう。

class ViewHolder(private val icon: ImageView, private val text: TextView) {

    fun bindItem(item: Item) {
        icon.setImageDrawable(AppCompatResources.getDrawable(context, item.iconId))
        text.setText(item.id)
    }

}

2. getView で convertView の状態による分岐を書く

2-1 convertView が null の時

以下の 1-6 の処理をします。

  1. Item を入れる Layout を inflate
  2. 要素 View を find
  3. ViewHolder を初期化
  4. view の tag に ViewHolder のインスタンスをセット
  5. viewHolder に item の属性を設定
  6. inflate した Layout を return
    if (convertView == null) {
        val view = inflater.inflate(LAYOUT_ID, parent, false)
        val icon = view.findViewById<ImageView>(R.id.image)
        val text = view.findViewById<TextView>(R.id.text)
        val viewHolder = ViewHolder(icon, text)
        view.tag = viewHolder
        viewHolder.bindItem(item)
        return view
    }

2-2 convertView が null でない時

Layout の inflate は不要なので、 tag にセットした ViewHolder を取り出して item の属性を View に当てればよいです。

    val viewHolder = (convertView.tag as ViewHolder)
    viewHolder.bindItem(item)
    return convertView

全体のコード

override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
    val item = items[position]

    if (convertView == null) {
        val view = inflater.inflate(LAYOUT_ID, parent, false)
        val icon = view.findViewById<ImageView>(R.id.image)
        val text = view.findViewById<TextView>(R.id.text)
        val viewHolder = ViewHolder(icon, text)
        view.tag = viewHolder
        viewHolder.bindItem(item)
        return view
    }

    val viewHolder = (convertView.tag as ViewHolder)
    viewHolder.bindItem(item)
    return convertView
}

おわりに

Spinner を扱う際に知っておくと有益な ViewHolder パターンの実装について紹介しました。Kotlin だと大分書きやすくなったという印象を受けました。そもそも Spinner 自体そんなに使わないかもしれないですが、忘れた時にこのドキュメントが役立つとちょっとうれしいです。


参考

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