2016年04月11日

ArrayAdapterのコンストラクタのResourceIDの役割

こんにちは、Androidエンジニアの外山です。

今回はAndroidのListViewなどにセットするArrayAdapterのコンストラクタで指定するResourceIDの役割について調べてみました。

ArrayAdapterのコンストラクタ

ArrayAdapterのコンストラクタは以下のようになっています。
public ArrayAdapter(Context context, @LayoutRes int resource, @IdRes int textViewResourceId,
            @NonNull List<T> objects) {
        mContext = context;
        mInflater = LayoutInflater.from(context);
        mResource = mDropDownResource = resource;
        mObjects = objects;
        mFieldId = textViewResourceId;
}

ResourceIDの使用箇所

mResourceとmFieldIdはgetViewでViewインスタンスを作成する時に使用されています。

mResourceはViewのレイアウトファイルを参照する際に、mFieldIdはレイアウトファイル内のTextViewを指定する際に使用されているようです(指定しなかった場合は生成したView自身をTextViewとして扱う)。

public View getView(int position, View convertView, ViewGroup parent) {
    return createViewFromResource(mInflater, position, convertView, parent, mResource);
}

private View createViewFromResource(LayoutInflater inflater, int position, View convertView,
        ViewGroup parent, int resource) {
    View view;
    TextView text;
    if (convertView == null) {
        view = inflater.inflate(resource, parent, false);
    } else {
        view = convertView;
    }
    try {
        if (mFieldId == 0) {
            //  If no custom field is assigned, assume the whole resource is a TextView
            text = (TextView) view;
        } else {
            //  Otherwise, find the TextView field within the layout
            text = (TextView) view.findViewById(mFieldId);
        }
    } catch (ClassCastException e) {
        Log.e("ArrayAdapter", "You must supply a resource ID for a TextView");
        throw new IllegalStateException(
                "ArrayAdapter requires the resource ID to be a TextView", e);
    }
    T item = getItem(position);
    if (item instanceof CharSequence) {
        text.setText((CharSequence)item);
    } else {
        text.setText(item.toString());
    }
    return view;
}

mDropDownResourceも同じような感じになっています。



まとめ

getViewをオーバーライドしてsuperメソッドを呼ばない場合は、引数で渡したResourceIDが使われることはないので`-1`など適当な値を渡しておけば大丈夫である。

リスト内のアイテムで一つのTextViewにのみ値をセットする場合はResourceIdを指定することでサブクラスを作成せずにAdapterを作成できるようだ。


RecyclerViewの登場によって使う機会が減ってきたListViewやGridViewですが簡単なリストを作成する際には手軽に使えていいですね。

posted by Seesaa京都スタッフ at 18:53| Comment(0) | Android | このブログの読者になる | 更新情報をチェックする

2015年10月23日

[Android]Robolectricを使ったテスト用ファイル読み込み

主にAndroidアプリ開発をしている外山です。

Androidのユニットテスト時にテスト用のファイルを読み込む方法について調べてみました。


事前準備

ライブラリ追加

今回はRobolectric 3.0を使用する
testCompile "org.robolectric:robolectric:3.0"

Testファイル

Testを記述するためのクラスを作成
ExampleUnitTest.java
@RunWith(CustomRobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21)
public class ExampleUnitTest {

	// InputStreamをStringへ変換するためのメソッド
	private static String streamToString(InputStream is) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte[] buf = new byte[2048];
        int size;
        try {
            while ((size = is.read(buf)) != -1) {
                baos.write(buf, 0, size);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        return new String(baos.toByteArray());
    }
}

assetsディレクトリを使う方法

デフォルトではユニットテスト時にはtest/assetsディレクトリが使われないのでassetsディレクトリのパスを指定することでテスト用のファイルを使用するように変更します

CustomRobolectricGradleTestRunner.javaを作成しgetAppManifest内でassetsのパスを指定する

public class CustomRobolectricGradleTestRunner extends RobolectricGradleTestRunner {
    public CustomRobolectricGradleTestRunner(Class klass) throws InitializationError {
        super(klass);
    }

    @Override
    protected AndroidManifest getAppManifest(Config config) {
        String buildVariant = (BuildConfig.FLAVOR.isEmpty() ? "" : BuildConfig.FLAVOR + "/") 
	        + BuildConfig.BUILD_TYPE;
        return new AndroidManifest(
                Fs.fileFromPath("src/main/AndroidManifest.xml"),
                Fs.fileFromPath("build/intermediates/res/" + buildVariant),
                Fs.fileFromPath("src/test/assets")
        );
    }
}

テスト

ExampleUnitTest.javaに以下を追加して読み込みができているかをチェックすると無事テストが成功しています

@Test
public void readFileFromAssets() throws IOException {
	InputStream is = RuntimeEnvironment.application.getAssets().open("assets_hello.txt");
	String text = streamToString(is);
	assertThat(text, is(equalTo("hello assets")));
}

resourcesディレクトリを使う方法

app/src/test/resources/resources_hello.txtにファイルを作成しておきます

resourcesディレクトリ内に作成したファイルから文字列を取得する場合は以下のようにします

プログラム

ExampleUnitTest.javaに以下を追加して読み込みができているかをチェックすると無事テストが成功しています
@Test 
public void readFileFromResources() {
	InputStream is = getClass().getResourceAsStream("/resources_hello.txt");
	String text = streamToString(is);
	assertThat(text, is(equalTo("hello resources")));
}

getClass().getResourceAsStream(path)を使用することでresourcesディレクトリ内のファイルへアクセスするためのInputStreamを取得することができます


まとめ

APIサーバーからのレスポンスをモックする際などコードに文字列を埋め込むのはつらいものがあるのでファイルから読み込めるというのはとても便利なのでテスト用に長い文字列を使いたい時などは使っていきたいですね。 今回はファイルから文字列を取得しましたが画像の読み込みなどももちろんできるので用途に応じて使い分けましょう。


posted by Seesaa京都スタッフ at 20:50| Comment(0) | Android | このブログの読者になる | 更新情報をチェックする

2015年10月07日

Nearby Messages APIを試してみる

久しぶりにガンプラを買いました、森脇です。
今回はGoogleが新たに提供を開始したNeaby Messages APIについて書いていきたいと思います。

Neaby Messages APIとは

Neaby Messages APIは、Googleが提供しているAPIの一つで、近くにあるスマートフォンやタブレット端末同士でのメッセージのやり取りを可能にするAPIです。

近距離での通信といえばNFCやBluetoothでのデータ送受信が頭に浮かびますが、Neaby Messages APIの場合はそうではなく、あくまでデータの送受信はサーバーを介して行われます。ただ、その送受信の際にBluetooth、BLE、WiFi、超音波などを用いることで相手を特定しています。
上記のような仕組みになりますのでネットワーク接続は必須となりますが、送信側の端末と受信側の端末が同じネットワークに接続している必要はありません。
ちなみにNearbyのもう一つのAPI、Nearby Connections APIは同じネットワークに接続している端末間でのデータの送受信を可能にするAPIですので注意が必要です。

事前準備

それではNeaby Messages APIを使うための準備を進めていきましょう。
まずGoogle Developers ConsoleにてAPIキーを取得します。GoogleMapsなどのAPIキーを取得する場合と手順は同じです。詳しい手順はこちらに記載してあります。

次にプロジェクトにGoogle Play Servicesを追加します。
build.gradleに以下の記述を追加します。Nearby Messages APIはGoogle Play Services 7.8.0以降でのみ扱えます。今回は8.1.0を使用します。
apply plugin: 'android'
...

dependencies {
    compile 'com.google.android.gms:play-services:8.1.0'
// Nearbyだけ追加したい場合は以下の記述でも問題ない
//    compile 'com.google.android.gms:play-services-nearby:8.1.0'
}
AndroidManifestに以下の記述を追加します。
<application ...>
    <meta-data
        android:name="com.google.android.nearby.messages.API_KEY"
        android:value="先ほど作成したAPIキー" />
    <activity>
    ...
    </activity>
</application>
これで準備は完了です。それでは以下に簡単な実装の流れを書いていきます。

Google API Clientに接続

Nearby Messages APIを使うには、まずGoogle API Clientを作成する必要があります。
この際、接続に成功した場合と失敗した場合のコールバックをそれぞれ追加することができます。
mGoogleApiClient = new GoogleApiClient.Builder(this)
     .addApi(Nearby.MESSAGES_API)
     .addConnectionCallbacks(this)
     .addOnConnectionFailedListener(this)
     .build();
Google API Clientへの接続に成功した場合の処理を以下のように記述します。ここではNearby Messages APIを使うにあたってユーザーからパーミッションの許可を得ているかどうかを確認しています。そして許可を得ていることが確認できた場合、メッセージを受信できる状態に移行します。
@Override
public void onConnected(Bundle bundle) {
    Nearby.Messages.getPermissionStatus(mGoogleApiClient)
            .setResultCallback(new ErrorCheckingCallback("getPermissionStatus", new Runnable() {
                @Override
                public void run() {
                    // メッセージの受信
                    subscribe();
                }
            }));
}
こちらを参考にNearby Messages APIを使用した際の共通のコールバックとして以下のように定義しています。パーミッションエラーが出た場合のみユーザーにパーミッションの許可を促すダイアログを表示します。
private class ErrorCheckingCallback implements ResultCallback<Status> {
    private final String method;
    private final Runnable runOnSuccess;

    private ErrorCheckingCallback(String method) {
        this(method, null);
    }

    private ErrorCheckingCallback(String method, @Nullable Runnable runOnSuccess) {
        this.method = method;
        this.runOnSuccess = runOnSuccess;
    }

    @Override
    public void onResult(@NonNull Status status) {
        if (status.isSuccess()) {
            Log.i(TAG, method + " succeeded.");
            if (runOnSuccess != null) {
                runOnSuccess.run();
            }
        } else {
            // Currently, the only resolvable error is that the device is not opted
            // in to Nearby. Starting the resolution displays an opt-in dialog.
            if (status.hasResolution()) {
                if (!mResolvingError) {
                    try {
                        // パーミッション確認ダイアログの表示
                        status.startResolutionForResult(MainActivity.this,
                                REQUEST_RESOLVE_ERROR);
                        mResolvingError = true;
                    } catch (IntentSender.SendIntentException e) {
                        Log.d(TAG, method + " failed with exception: " + e);
                    }
                } else {
                    // This will be encountered on initial startup because we do
                    // both publish and subscribe together.  So having a toast while
                    // resolving dialog is in progress is confusing, so just log it.
                    Log.i(TAG, method + " failed with status: " + status
                            + " while resolving error.");
                }
            } else {
                Log.d(TAG, method + " failed with : " + status
                        + " resolving error: " + mResolvingError);
            }
        }
    }
}
パーミッション確認ダイアログの結果はonActivityResultで受け取ります。
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == REQUEST_RESOLVE_ERROR) {
        mResolvingError = false;
        // User was presented with the Nearby opt-in dialog and pressed "Allow".
        if (resultCode == Activity.RESULT_OK) {
            // メッセージの受信
            subscribe();
        } else {
            Toast.makeText(this, "Failed to resolve error with code " + resultCode,
                    Toast.LENGTH_LONG).show();
        }
    }
}
またNearby Messages APIはバッテリーの消耗が激しいため、必ずライフサイクルに応じて接続を解除するようにしてください。
@Override
protected void onStart() {
    super.onStart();
    // 復帰時に再接続
    if (!mGoogleApiClient.isConnected()) {
        mGoogleApiClient.connect();
    }
}

@Override
protected void onStop() {
    // バッテリー消費を抑えるために必ず接続を解除する
    if (mGoogleApiClient.isConnected() && !isChangingConfigurations()) {
        unsubscribe();
        unpublish();
        mGoogleApiClient.disconnect();
    }
    super.onStop();
}

メッセージの送受信

まず送受信するメッセージはバイト配列でやり取りされます。データサイズは3KB以内が推奨とされていて、画像や動画などの大きなファイルのやり取りは想定されていません。 送信の場合、PublishOptionsでメッセージの送受信が有効な距離や送信状態を維持する時間などをStrategyにて設定することができます。またGoogle Play Services8.1.0以降から送信状態が終了した際のコールバックを設定することができます。
/**
 * メッセージの送信
 */
private void publish() {
    // GoogleApiClientに接続しているか確認
    if (!isApiClientConnected()) {
        return;
    }
    PublishOptions options = new PublishOptions.Builder()
            .setStrategy(PUB_SUB_STRATEGY)
            .setCallback(new PublishCallback() {
                @Override
                public void onExpired() {
                    super.onExpired();
                    // 送信状態が終了した時のコールバック
                    Log.d(TAG, "publish onExpired");
                }
            })
            .build();

    Nearby.Messages.publish(mGoogleApiClient, mMessage, options)
            .setResultCallback(new ErrorCheckingCallback("publish"));
}

/**
 * メッセージの送信解除
 */
private void unpublish() {
    // GoogleApiClientに接続しているか確認
    if (!isApiClientConnected()) {
        return;
    }
    Nearby.Messages.unpublish(mGoogleApiClient, mMessage)
            .setResultCallback(new ErrorCheckingCallback("unpublish"));
}
受信の場合は、あらかじめメッセージを受信した時の処理としてMessageListenerを定義しておきます。
mMessageListener = new MessageListener() {
    @Override
    public void onFound(final Message message) {
        // メッセージを受信した時の処理
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                SimpleMessage simpleMessage = SimpleMessage.fromMessage(message);
                Toast.makeText(getApplicationContext(), simpleMessage.getId(), Toast.LENGTH_LONG).show();
            }
        });
    }
};
SubscribeOptionsではPublishOptionsと同様の設定に加えて、Strategyにて受信するメッセージのフィルターを設定することができます。
/**
 * メッセージの受信
 */
private void subscribe() {
    // GoogleApiClientに接続しているか確認
    if (!isApiClientConnected()) {
        return;
    }
    SubscribeOptions options = new SubscribeOptions.Builder()
            .setStrategy(PUB_SUB_STRATEGY)
            .setCallback(new SubscribeCallback() {
                @Override
                public void onExpired() {
                    super.onExpired();
                    // 受信状態が終了した時のコールバック
                    Log.d(TAG, "subscribe onExpired");
                }
            })
            .build();

    Nearby.Messages.subscribe(mGoogleApiClient, mMessageListener, options)
            .setResultCallback(new ErrorCheckingCallback("subscribe"));
}

/**
 * メッセージの受信解除
 */
private void unsubscribe() {
    // GoogleApiClientに接続しているか確認
    if (!isApiClientConnected()) {
        return;
    }
    Nearby.Messages.unsubscribe(mGoogleApiClient, mMessageListener)
            .setResultCallback(new ErrorCheckingCallback("unsubscribe"));
}
以上です。ソースコードはこちらに置いておきます。
こちらのサンプルでは受信が成功すると相手の端末上のUUIDがToastで表示されます。
device-2015-10-07-170127.png

所感

アプリへ組み込む場面がかなり限定される機能ではありますが、今までQRコードを撮影したり、振った端末同士の位置情報を介していたID交換などがよりスムーズに行えるようになるかもしれないですね。実装自体はかなりお手軽というわけではなく、少しクセのあるAPIだとは思いますが、AndroidだけではなくiOSでも利用可能ということなので、そのあたりも含めて今後の開発に期待していきたいです。

参考

Nearby  |  Google Developers
Nearby Messages APIでチャットみたいなのを作ってみる
posted by Seesaa京都スタッフ at 17:48| Comment(0) | Android | このブログの読者になる | 更新情報をチェックする