20201207のAndroidに関する記事は9件です。

【超入門】android(kotlin)アプリにユニットテストを導入する

はじめに

この記事は2020年のRevCommアドベントカレンダー8日目の記事です。7日目は@seckieさんの「Adobe XD を使った Web 開発のワークフロー」でした。

こんにちは。RevCommで主にandroid開発を担当している @marienplatz です。
テスト導入の経験がなかったので「やってみたい!」と立候補したところ、せっかくなので記事にしちゃえとお題が決まりました。

目次

  • 1. 最もシンプルなユニットテストを導入
    • 1.1. テストコードを書く準備
    • 1.2. テストを導入
  • 2. 複雑なメソッドのテストテクニック(スタブを使用)
    • 2.1. 問題のあるメソッドの作成
    • 2.2. スタブの使用

超入門的な内容から徐々に本格的な内容を!という予定でしたが、時間切れで入門のみで終えてしまい目次を見返しては溜め息をついています。。。
それはさておき早速ですが、テストコードを導入していきます。

1. 最もシンプルなテストを導入

1.1. テストコードを書く準備

四則計算をするだけのプロジェクトを作成しました。

Screenshot_20201207-124535.png

左のeditTextと右のeditTextに数字を、真ん中のeditTextに四則演算子を記入し、
計算ボタンを押したら計算結果を表示するというものです。
ユーザーが手入力するので要らない空白が入ってしまったり、半角を想定しているのに全角数字がきてしまったり何でもアリ!なのでvalidatorは欠かせません。

Validator.kt
class Validator {
    fun isValidNumber(text: String): Boolean {
        return text.matches(Regex("[0-9]+"))
    }
}

というわけで早速validatorを書きました。

このvalidatorクラスのテストコードを書いていきます。

まずは、下の画像の「Validator」という文字列にカーソルを持ってきて、
command + shift + t を押します。
スクリーンショット 2020-12-08 13.03.03.png
Create New Test...をクリックすると、
スクリーンショット 2020-12-07 13.14.27.png

こんなダイアログが表示されるので、
setUp/@Before
tearDown/@After

をチェックしてOKを押してください。
そうするとテスト用のディレクトリにファイルが自動生成されます。

ValidatorTest.kt
class ValidatorTest : TestCase() {

    public override fun setUp() {
        super.setUp()
    }

    public override fun tearDown() {}
}

こんなファイルが自動生成されました。
Validatorクラスに対応したクラスです。
先ほどチェックを入れたおかげで自動生成されたsetUp()やtearDown()というメソッドですが、
setUp()は初期化、tearDown()は後処理に使います。
これで下準備は完了です!

1.2. テストコードを導入

いよいよテストコードを導入します!

ValidatorTest.kt
@RunWith(JUnit4::class)
class ValidatorTest : TestCase() {

    public override fun setUp() {
        super.setUp()
    }

    public override fun tearDown() {}

    @Test
    fun isValidNumber_givenAlpha_returnsFalse() {
        val target = Validator()
        val actual = target.isValidNumber("a")
        assertThat(actual, `is`(false))
    }
}

アルファベットが与えられたときはfalseを返す、というテストが書けました!

このテストを実行するにはandroid studioのテストメソッドの右側にある緑の実行ボタン(下の画像参照)を押すと実行されます。
スクリーンショット 2020-12-07 16.37.17.png

assertThat()でisValidNumber()の返り値がfalseであることを確認しています。
もしここでtrueが返ってきたとき(テストに失敗したとき)はAssertionFailedErrorという例外が投げられます。

無事にテスト導入ができました!
これくらいシンプルなユニットテストであれば少し手順を覚えるだけなので簡単ですよね。
が、実務で書かれているメソッドであればこんな風にシンプルにテストできるものばかりではないはずです。

次は複雑なメソッドのテスト方法について扱っていきたいと思います。

2. 複雑なメソッドのテストテクニック(スタブを使用)

2.1. 問題のあるメソッドの作成

前章でvalidator()を作成しました。
今度はそれらを用いてユーザーの入力内容は計算可能かチェックし、不可能であればエラーメッセージを取得するメソッドを作ってみます。

Validator.kt
open class Validator {
    open fun isValidNumber(text: String): Boolean {
        return text.matches(Regex("[0-9]+"))
    }
    // 演算子のチェックも一応追加しました
    open fun isValidOperator(text: String): Boolean {
        return text.matches(Regex("""[-+*/]"""))
    }
}
InputNumbers.kt
data class InputNumbers(
    val num1: String,
    val num2: String,
    val operator: String
)
InputChecker.kt
open class InputChecker {

    var errMsg: String = ""

    open fun canCalc(input: InputNumbers): Boolean {
        val validator = Validator()
        if (!validator.isValidNumber(input.num1)) {
            errMsg = "数字を入力してください"
            return false
        }
        if (!validator.isValidNumber(input.num2)) {
            errMsg = "数字を入力してください"
            return false
        }
        if (!validator.isValidOperator(input.operator)) {
            errMsg = "+, -, *, /のいずれかを入力してください"
            return false
        }
        // クラッシュが気になって追加しましたがここでは重要でないです
        if (input.operator == "/" && input.num2 == "0") {
            errMsg = "0で割らないでください"
            return false
        }
        return true
    }
}

このcanCalc()というメソッドなのですが、テストをしようにも、前章で作ったvalidatorクラスに依存してしまっています。(ここが問題です。)
バグの切り分けがしづらいですし、運が悪いとバグが打ち消しあってしまって気付けないなんてことも起こりえます。

この依存を取り除く手段の一つが、スタブを用いることです。

2.2. スタブの使用

Validator.kt
open class Validator {
    open fun isValidNumber(text: String): Boolean {
        return text.matches(Regex("[0-9]+"))
    }
    open fun isValidOperator(text: String): Boolean {
        return text.matches(Regex("""[-+*/]"""))
    }
}

class StubValidator(private val isNumber: Boolean, private val isOperator: Boolean): Validator() {
    override fun isValidNumber(text: String): Boolean {
        return isNumber
    }
    override fun isValidOperator(text: String): Boolean {
        return isOperator
    }
}

StubValidatorクラスを追加しました。(Validatorクラスを継承しています)
ValidatorクラスをStubValidatorクラスに置き換えて、コンストラクタにisValidNumber()やisValidOperator()に返して欲しい真偽値を持たせることにより、狙った返り値が得られます。
例えば、「isValidNumber()がfalseを返して、isValidOperator()はtrueを返すテストをしたいな」と思ったときはValidator()をStubValidator(false, true)に置き換えることで代替可能です。

「ところで、ValidatorクラスとStubValidatorクラスを差し替えるってどうやるの?」という話になるわけですが、

InputCheckerクラスのcanCalc()の引数にValidatorクラスを追加します。

これによってStubValidatorクラスと差し替え可能になります。

BeforeではValidatorクラスをcanCalc()内で初期化していました。

InputChecker.kt(Before)
open class InputChecker {
    open fun canCalc(input: Operator.InputNumbers): Boolean {
        val validator = Validator()
        (略)
}

引数に差し替えの必要なValidatorクラスを置きますと

InputChecker.kt(After)
open class InputChecker {
    open fun canCalc(input: Operator.InputNumbers, validator: Validator): Boolean {
        (略)
    }
}

テスト時に引数をStubValidatorクラスに差し替えることにより容易にテストできるようになります。(より詳しく知りたい方はDI(依存性注入)について調べてみると良いかもです)

スタブを用いたテストコード例です。

InputCheckerTest.kt
@RunWith(JUnit4::class)
class InputCheckerTest {

    lateinit var target: InputChecker

    @Before
    fun setUp() {
        target = InputChecker()
    }

    @After
    fun tearDown() {
    }

    // 入力された値が全て正常なとき
    @Test
    fun checkCanCalc_givenValidValues_returnsTrue() {
        // inputの値はなんでもいい
        val input = InputNumbers("11", "100", "+")
        val stubValidator = StubValidator(isNumber = true, isOperator = true)
        assertThat(target.canCalc(input, stubValidator), `is`(true))
    }

    // 入力された演算子が異常なとき
    @Test
    fun checkCanCalc_givenInvalidOperator_returnsFalse() {
        // inputの値はなんでもいい
        val input = InputNumbers("11", "100", "+")
        val stubValidator = StubValidator(isNumber = true, isOperator = false)
        assertThat(target.canCalc(input, stubValidator), `is`(false))
        assertThat(target.errMsg == "+, -, *, /のいずれかを入力してください", `is`(true))
    }

    // 入力された数字が異常なとき
    @Test
    fun checkCanCalc_givenInvalidNumber_returnsFalse() {
        // inputの値はなんでもいい
        val input = InputNumbers("11", "100", "+")
        val stubValidator = StubValidator(isNumber = false, isOperator = true)
        assertThat(target.canCalc(input, stubValidator), `is`(false))
        assertThat(target.errMsg == "数字を入力してください", `is`(true))
    }
}

このように、スタブを用いることによりvalidatorクラスのメソッドの返り値が真であった場合と偽であった場合のそれぞれのケースで容易にテストすることができました。
スタブのおかげでvalidatorクラスに依存することなくユニットテストが可能となりました。
以上が複雑なメソッドのテストテクニックについてです。

おわりに

本当はUIテストまで書きたかったのですがとても収拾がつかなさそうなのでここで終わります。
長くなりましたがお付き合いいただき、ありがとうございました。

明日は @sukekd さんの「Google Workspace(旧称G Suite) アカウントのSAML認証を使ってAWS CLIの認証を行う」です。

参考

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

[Flutter][Add-to-app] デバッグビルドしたapk内のlibflutter.soが壊れる問題と回避方法

問題

Kotlinで実装されている既存アプリにFlutterを追加した所、デバッグビルドしたapkを端末にインストールできないことが起きるようになった。頻度は10回に1回くらい。

エラー発生時のlogcatの抜粋
2020-12-01 16:09:54.960 1578-2005/? W/ziparchive: Zip: inflate zerr=-3 (nIn=0xb40000787007ebda aIn=25638 nOut=0xb40000787006ebc0 aOut=24608)
2020-12-01 16:09:54.960 1578-2005/? W/zipro: ExtractToMemory failed with Zlib error
2020-12-01 16:09:54.960 1578-2005/? E/NativeLibraryHelper: Failed uncompressing libflutter.so to /data/app/vmdl854882831.tmp/lib/arm64/tmp.hVakYp
...
    --------- beginning of system
2020-12-01 16:09:54.961 1578-2005/? W/NativeHelper: Failure copying native libraries [errorCode=-18]

原因

apkのビルド時にlibflutter.soを圧縮しているが、それが正常できないのが原因。
問題が起きる時のapkからlibflutter.soを取り出してみると、サイズが約200KBになっている。libflutter.soは、正常時なら10MB以上ある。

回避策

libflutter.soの圧縮に問題があるので、libflutter.soの圧縮をしないことで問題は回避できる模様。
AndroidManifestのextractNativeLibsにfalseを設定することで、実現できる1

<manifest>
    <application
        android:extractNativeLibs="false" >
        ...
    </application>
</manifest>

ただし、libflutter.soを圧縮しなくなる分、アプリのサイズは増大するので注意。
問題が起きていたアプリでは、サイズが120MBから230MBに増えた。
基本、Wifi環境下でしかダウンロードしないのでよいが、そうでないなら、別の対処を考えた方が良い。
同じアプリでも、aabをReleaseビルドする場合は問題が起きたことがない。なので、デバッグビルド固有の設定に問題がありそうである。

補足

原因詳細

エラーログの「Zip: inflate zerr=-3」の部分に着目。エラーコードの定義箇所を見ると、 kInvalidFileというエラーで、意味は「入力ファイルを zip アーカイブとして処理できません。通常、小さすぎたり、大きすぎたり、有効な署名がないために処理できません」。

ということでlibflutter.soが破損しているということが分かる。

問題が起きるとき、MacのFinderからそのapkファイルを解凍しようとするとエラーが起きるので、Android OSの解凍処理には問題ないことが分かる。shellのunzipコマンドならapkファイルの解凍できる。解凍後、libflutter.soのサイズを見ると、約200KBになっていることが分かる。

Firebase Test Labでのエラーの見え方

この問題に気づいたのは、CIでFlutte入りのアプリをデバッグビルドし、Firebase Test LabでE2Eテストを実行しようとしたとき。
「The app APK is not a valid Android application.」というエラーが表示される。
以下、エラーログを示す[^2]。

sudo gcloud firebase test android run \
  --app "${DEBUG_APP_APK_PATH}" \
  --test "${TEST_APK_PATH}" \
  --test-targets "user bot2" \
  --device model=NexusLowRes,version=28 \
  --use-orchestrator \
  --environment-variables clearPackageData=true,coverage=true,coverageFile="/sdcard/coverage.ec" \
  --directories-to-pull /sdcard \
  --results-bucket ${TEST_RESULTS_BUCKET}

Have questions, feedback, or issues? Get support by visiting:
  https://firebase.google.com/support/

Uploading [app/build/outputs/apk/*******/debug/app-*******-debug.apk] to Firebase Test Lab...
Uploading [app/build/outputs/apk/androidTest/*******/debug/app-*******-debug-androidTest.apk] to Firebase Test Lab...
Raw results will be stored in your GCS bucket at [https://console.*******ers.google.com/storage/browser/*****************************/2020-12-01_04:15:54.367295_CwcV/]

Test [matrix-2dcbutvhf7ld6] has been created in the Google Cloud.
Firebase Test Lab will execute your instrumentation test on 1 device(s).
ERROR: (gcloud.firebase.test.android.run) 
Matrix [matrix-2dcbutvhf7ld6] failed during validation: The app APK is not a valid Android application.

Exited with code exit status 1
CircleCI received exit code 1

  1. 英語のリファレンスを読むと、Android Gradle Plugin 3.6.0以上だとデフォルトfalseとのこと。ただ、問題のアプリはextractNativeLibsの設定に対応していないAndroid 4.4が最小サポートOSのせいか、trueの動作になっていた。 

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

Androidで起動時、deplhiで作成したアプリを自動起動にする。

delphi 2020 アドベントカレンダー 7日目です

こんにちは
やましょうです。

Delphiで作ったアプリをAndrid起動時に自動起動。

1.まず検索

1.classes.dexを入れ替えてパッケージを作る方法
https://dannywind.nl/auto-start-delphi-xe5-android-app-after-boot/

2.jar ファイルを自分でつくる方法
https://blog.csdn.net/tanqth/article/details/74357209
お金で解決がここは中華口座が必要。

結果jarファイルで行う方が無難だと思う。

2.javaでソースを仕方なく書く

package com.qa65000;

import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Intent;
import android.content.Context;
import android.util.Log;

public class BootReceiver extends BroadcastReceiver
{

    @Override
    public void onReceive(Context context, Intent intent) 
    {
            Log.d("test_TAG", "onRecive()");
        if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
            Log.d("test_TAG", "Booo........t Complated()");
           Intent launchintent = new Intent();
            launchintent.setClassName(context, "com.embarcadero.firemonkey.FMXNativeActivity");           
            launchintent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            context.startActivity(launchintent);  
        }
    }
}

3.batファイルを作ってコンパイルする。

echo.
echo Compiles your Java code into classes.dex
echo Verified to work for Delphi XE6
echo.
echo Place this batch in a java folder below your project (project\java)
echo Place the source in project\java\src\com\dannywind\delphi
echo If your source file location or name is different, please modify it below.
echo This assumes a Win64 system with the 64-bit Java installed by the Delphi XE6 
echo installer in C:\Program Files\Java\jdk1.70.0_25
echo.

setlocal

set ANDROID_JAR="C:\Users\Public\Documents\Embarcadero\Studio\20.0\PlatformSDKs\android-sdk-windows\platforms\android-26\android.jar"
set DX_LIB="C:\Users\Public\Documents\Embarcadero\Studio\20.0\PlatformSDKs\android-sdk-windows\build-tools\28.0.2\lib"
set EMBO_DEX="C:\Program Files (x86)\Embarcadero\Studio\20.0\lib\android\debug\classes.dex"
set PROJ_DIR=%CD%
set VERBOSE=0
set JAVASDK="C:\Program Files\Java\jdk1.8.0_60\bin"
set DX_BAT="C:\Users\Public\Documents\Embarcadero\Studio\20.0\PlatformSDKs\android-sdk-windows\build-tools\28.0.2\dx.bat"

echo.
echo Compiling the Java source files
echo.
pause
mkdir output 2> nul
mkdir output\classes 2> nul
if x%VERBOSE% == x1 SET VERBOSE_FLAG=-verbose
%JAVASDK%\javac %VERBOSE_FLAG% -classpath %ANDROID_JAR% -d    bin\classes src\com\qa65000\BootReceiver.java
%JAVASDK%\javac %VERBOSE_FLAG% -classpath %ANDROID_JAR% -d output\classes src\com\qa65000\BootReceiver.java

jar cvf bin\BootReceiver.jar -C bin\classes jp

echo. ここから不要だと思う(classes.dexを書き換える場合のコンパイル)
echo Creating jar containing the new classes
echo.
pause
mkdir output\jar 2> nul
if x%VERBOSE% == x1 SET VERBOSE_FLAG=v
%JAVASDK%\jar c%VERBOSE_FLAG%f output\jar\test_classes.jar -C output\classes com

echo.
echo Converting from jar to dex...
echo.
pause

mkdir output\dex 2> nul
if x%VERBOSE% == x1 SET VERBOSE_FLAG=--verbose
call %DX_BAT% --dex %VERBOSE_FLAG% --output=%PROJ_DIR%\output\dex\test_classes.dex --positions=lines %PROJ_DIR%\output\jar\test_classes.jar

echo.
echo Merging dex files
echo.
pause
%JAVASDK%\java -cp %DX_LIB%\dx.jar com.android.dx.merge.DexMerger %PROJ_DIR%\output\dex\classes.dex %PROJ_DIR%\output\dex\test_classes.dex %EMBO_DEX%
echo.
echo Now use output\dex\classes.dex instead of default classes.dex
echo And add broadcastreceiver to AndroidManifest.template.xml
echo.

:Exit

endlocal


                                      javac -classpath "C:\Users\Public\Documents\Embarcadero\Studio\15.0\PlatformSDKs\adt-bundle-windows-x86-20131030\sdk\platforms\android-19\android.jar" -d bin\classes src\com\example\hello\Hello.java

C:\Program Files\Java\jdk1.8.0_60\bin"\javac  -Xlint:all -class

4.作成したBootReciver.jarをプロジェクトに追加する。

(10.3で64bitは追加できない。泣)

SnapCrab_NoName_2020-12-7_20-3-8_No-00.png

.5 コンパイルして転送する。

たぶん動くと思う(夏にやった奴なのでうごかなかったらごめんなさいです。)

本家のコードが
一応気になる人は
https://www.youtube.com/watch?v=4_CkU9L2mCo&t=173s
をみてください。

以上
やましょうでした。

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

Androidではこのライブラリ・APIを使うけどFlutterでは何を使うの?

はじめに

Livesense Advent Calendar8日目を担当します @Soma-Kato です。普段は転職やバイトのAndroidアプリの開発・運用を担当しています。
今回は社外でやっているFlutterアプリ開発で、Androidではこのライブラリ・APIを使うけどFlutterでは何を使うの?みたいなことが度々あったので、それらを解決したライブラリを紹介したいと思います。
本記事内ではKotlin/Javaで開発されたAndroidアプリをAndroidネイティブと記述しています。

View系

flutter_speed_dial

タップすると複数のボタンが出てくるFABです。Googleカレンダーなどでよく使われているタップすると展開するFABですね。
Androidネイティブで実装するには少し面倒ですが、Flutterではflutter_speed_dialという便利なライブラリがあります。

flutter_speed_dial

導入方法は以下の通りです。

pubspec.yaml
dependencies:
  flutter_speed_dial: ^1.2.5
  • FlutterでMDCのFABを扱うのと同様に、ScaffoldfloatingActionButton引数にSpeedDialWidgetを渡すと、画面右下にFABが表示されます。
  • SpeedDialanimatedIcon引数にAnimatedIconsを渡すと、トップレベルのFABのアイコンを定義できます。
  • children引数にSpeedDialChildWidgetを渡すと展開されるFABのレイアウトやタップ時の挙動を定義できます。
    return Scaffold(
      floatingActionButton: SpeedDial(
        animatedIcon: AnimatedIcons.menu_close,
        animatedIconTheme: IconThemeData(size: 22.0),
        visible: _dialVisible,
        children: [
          SpeedDialChild(
              child: Icon(Icons.accessibility),
              backgroundColor: Colors.red,
              label: 'First',
              onTap: () => print('FIRST CHILD')),
          SpeedDialChild(
              child: Icon(Icons.brush),
              backgroundColor: Colors.blue,
              label: 'Second',
              onTap: () => print('SECOND CHILD')),
        ],
      ),
    );

flutter_svg

Androidネイティブ開発ではデザイナーさんに書き出してもらったSVGファイルをAndroidStudioのVector Assetで読み込んで、xmlにしてそれを参照していると思うのですが、Flutterにはその機能はありません。
しかしFlutterではflutter_svgという、SVGを直接読み込めるライブラリがあります。

導入方法は以下の通りです。
assets/を定義することにより、assets/配下のSVGファイルをコードから参照できるようになります

pubspec.yaml
dependencies:
  flutter_svg: ^0.18.0

flutter:
  assets:
    - assets/

SvgPicture.assetでファイル名を渡してあげると、SVGを描画できます。
SvgPicture自体はStatefulWidgetなので、1つ前のセクションでお話ししたFABのアイコンなどにも使うことができます。

SvgPicture.asset('assets/icon.svg')

scrollable_positioned_list

Flutter標準で実装されているListViewWidgetは優秀ですが、AndroidネイティブのRecyclerViewのようにListのpositionを指定して自動でスクロールされる機能はありません。(scrollToPositionのようなメソッドがありません。)
それを実現できるWidgetを提供しているライブラリがscrollable_positioned_listです。

導入方法は以下の通りです。

pubspec.yaml
dependencies:
  scrollable_positioned_list: ^0.1.8
final itemScrollController = ItemScrollController();
final itemPositionsListener = ItemPositionsListener.create();

ScrollablePositionedList.builder(
  itemCount: 20,
  itemBuilder: (context, index) => Text('Item $index'),
  itemScrollController: itemScrollController,
  itemPositionsListener: itemPositionsListener,
);

builder自体はListViewとほとんど変わらないのですが、ItemScrollControllerItemPositionsListenerを引数に渡すことができます。
ItemScrollControllerjumpTo(index)がRecyclerViewのscrollToPositionと似たような振舞をします。

itemScrollController.jumpTo(index: 10);

keyboard_avoider

AndroidネイティブだとTextFieldをタップしてIMEが表示された時に、タップしたTextFieldがIMEに隠れないように自動でViewを押し上げてくれると思いますが、Flutterではそのような機能がありません。
それを実現できるWidgetを提供しているライブラリがkeyboard_avoiderです。
keyboard_avoider
導入方法は以下の通りです。

pubspec.yaml
dependencies:
  keyboard_avoider: ^0.1.2

autoScroll引数にtrueを渡すと、child引数に定義されている、TextFieldがIMEに隠れないように自動でスクロールしてくれます。

KeyboardAvoider(
  autoScroll: true,
    child: Column(
      children: [
        TextFormField(),
        Container(height: 500),
        TextFormField(),
      ],
  ),
);

Utility系

json_serializable

Androidネイティブ開発で、APIのレスポンスをPOJOへ変換するためにGsonなどを使うことがあると思います。
Flutterではこのような時に、json_serializableライブラリを使い、Dartクラスへ変換します。

導入方法は以下の通りです。

pubspec.yaml
dependencies:
  json_annotation: 3.0.1

dev_dependencies:
  json_serializable: 3.3.0
Person.dart
part 'Person.g.dart';

@JsonSerializable()
class Person {
  final String firstName;
  final String lastName;

  Person({this.firstName, this.lastName});
}

Personクラスに@JsonSerializableをつけ、flutter packages pub run build_runner buildをターミナルなどのコマンドラインで実行すると、MapからPersonへ変換する_$PersonFromJsonと、PersonからMapへ変換する_$PersonToJsonを自動生成してくれます。

Person.g.dart
part of 'Person.dart';

// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************

Person _$PersonFromJson(Map<String, dynamic> json) {
  return Person(
    firstName: json['firstName'] as String,
    lastName: json['lastName'] as String,
  );
}

Map<String, dynamic> _$PersonToJson(Person instance) => <String, dynamic>{
      'firstName': instance.firstName,
      'lastName': instance.lastName,
    };

これを先ほど定義したPersonクラスのfactoryコンストラクタなどに記述してあげると、APIレスポンスをDartクラスへ変換できるようになります。
_$PersonToJsonを呼び出せば、PersonクラスをMap<String, dynamic>へ変換できるので、APIにpostしたり、sqfliteを使ってアプリ内DBに保存したりできます。

Person.dart
...
factory Person.fromJson(Map<String, dynamic> json) =>
    _$PersonFromJson(json);

Map<String, dynamic> toJson() => _$PersonToJson(this);

また、Gson@SerializedNameのように、@JsonKeyを使いKey名を変更することもできます。

Person.dart
class Person {
  @JsonKey(name: "first_name")
  final String firstName;
  @JsonKey(name: "last_name")
  final String lastName;

tuple

Kotlinで関数から複数の値を返したい時などに、PairTripleを使うと思いますが、Dartはそのようなクラスが実装されていません。
それを実現できるクラスを提供しているライブラリがtupleです。

導入方法は以下の通りです。

pubspec.yaml
dependencies:
  tuple: ^1.0.3

実装はとても簡単で、2つの値を持たせたい場合はTuple2を使い値を渡します。
取り出す時はitem1, item2を呼び出し値を取り出します。
Tuple2〜Tuple7まであるので、最大で7つの値を定義することがきます。

Tuple2<int, String> tuple2 = Tuple2(0, "1");
int number = tuple2.item1;
String name = tuple2.item2;

shared_preferences

名前の通りAndroidネイティブ開発でおなじみのSharedPreferencesです。Flutterでは標準で実装されていないので、shared_preferencesライブラリを使うことになります。

導入方法は以下の通りです

pubspec.yaml
dependencies:
  shared_preferences: 0.5.10

実装自体はAndroidネイティブのSharedPreferencesとほとんど同じですが、値をセットする時の戻り値がFuture<void>になるので、非同期処理が必要になります。そういう点で言うと、AndroidXのPreferencesDataStoreに近い感じになっています。

SharedPreferences prefs = await SharedPreferences.getInstance();
int counter = (prefs.getInt('counter') ?? 0) + 1;
print('Pressed $counter times.');
await prefs.setInt('counter', counter);

デバック系

flutter_stetho

AndroidネイティブにもあるStethoライブラリです。
NetworkInspectorやDatabaseInspectorなどが利用できます。
Stethoを使ったことがある人はご存知だと思いますが、AndroidStudio4.1から実装されているDatabaseInspectorとは違い、StethoはDatabaseのテーブルに直接変更を加えることができません。あくまで参照するくらいしかできません。
なので個人的にはちょっと不便だなと思っていますが、FlutterでDatabaseInspector機能があるライブラリが他にない(知らない)ので利用しています。
network_inspector (2).gif

導入方法は以下の通りです

pubspec.yaml
dependencies:
  flutter_stetho: ^0.5.2

main関数内で初期化をし、chrome://inspectを開けば、いつものStethoの画面を開くことができます。

void main() {
  Stetho.initialize();

  runApp(new MyApp());
}

alice

Androidネイティブ開発でよく使われるNetworkInspectorのChuckと同じようなことができるライブラリです。
StethoはChromeブラウザで開きますが、aliceはアプリ内で通信ログを確認できるのでとても便利です。
image.png

導入方法は以下の通りです

pubspec.yaml
dependencies:
  alice: ^0.1.6

実装は少し長いので省略しますが、Exampleを見ていただければ導入できると思います。

まとめ

今回はAndroidではこのライブラリ・APIを使うけどFlutterでは何を使うの?を中心としたライブラリ紹介をさせていただきました。
個人的にFlutterのライブラリは、AndroidやiOSのネイティブ開発に触発されたものが多くあるように感じています。なので、大体必要なものは揃っていると思います。
弊社内でもFlutterを導入するタイミングを見計っています:eyes:

参考・画像参照

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

備忘録:ソフトウェアテスト自動化カンファレンス2020に参加してみた

はじめに

こんにちは。oka-shi@PjM(プロジェクトマネージャー)、兼、モバイルアプリエンジニアです。

先日、テスト自動化研究会のオンライン勉強会『ソフトウェアテスト自動化カンファレンス2020』 に参加してきたので、自分用に備忘録を残します。
勉強会の資料はconnpassに公開されていますので、興味がありましたらご参照ください。

勉強会の概要

テストの自動化は多くの現場で実施されるようになってきており、それぞれの場で様々な工夫がなされております。しかし、それらを聞く機会はなかなかないのが現状です。
そこで、今年度はテスト自動化研究会以外の多くの方にご協力いただき、様々な現場でのテスト自動化について工夫や試行錯誤の様子をお話頂きます。

  • セッション ※太字に参加
    • 会議室A(初心者向け)
      • 自動システムテストのテストケース見直しの観点とその自動化について
      • システムテスト自動化はじめの0.5歩 〜あつまれテスト自動化レイトマジョリティさんの森〜
      • "全部乗せ" フレームワーク CodeceptJS でE2Eテストを楽にしよう
      • E2E自動テストの前に、手動テストを効率化するための自動化を行った話
      • EMTEを使って自動化の費用対効果をわかりやすく表現する
      • FindElementできない画面のElementをFindした方法、そこから発展していくテスト自動化
      • ローコード開発テストガイドライン
      • テスト自動化ツールで考えるモデルベースドテスト
      • E2Eテストのflakyと向き合う
      • 中の人が語るテスト自動化SaaSのCS - カスタマーサポート、そしてサクセスへ -
    • 会議室B(経験者向け)
      • CI パイプラインでのテスト戦略とその実現方法
      • AppiumのWebViewアプリテストの仕組みとハマりどころ
      • テスト自動化導入に取り組んだ1年の歩み 〜E2E編〜
      • E2E自動テストを浸透させるために工夫したこと
      • APIテスト、どうやってますか?
      • E2Eのリトライを少し賢くして、落ちにくくしてみた
      • reg-suitとQA Wolfを活用したVisual Regression Test

何故参加したか

  • プロジェクトを成功させる要素の1つとして、ソフトウェアテスト自動化の知見を得るため
  • モバイルアプリのテスト自動化の知見を得るため

参加して学んだこと

PjMとモバイルアプリエンジニアの観点で、勉強会で参考になった文章を引用して、理解した内容をまとめました。

PjM観点

自動システムテストのテストケース見直しの観点とその自動化について

Google Analyticsによるページ価値の導出

本セッションでは、Webサイトの自動システムテストを効果的に行う方法の1つとして、Google Analyticsのページ価値を活用することを理解しました。
Webサイトの運営や分析を経験していませんが、機会があれば参考にしたいです。

システムテスト自動化はじめの0.5歩 〜あつまれテスト自動化レイトマジョリティさんの森〜

コラボレーション
 開発者・POなど関係者との対話・協業が最重要。
 ・ホワイトボードを囲んで絵を描く(リモートでも)
 ・同じものを見ながら一緒に働く
変化ではなく実験の精神
 まずやってみる
 ・失敗したっていい
 ・いつまでも結果が出ないより失敗という結果が出た方がマシ

本セッションでは、システムテスト自動化を、小さくはじめるメリットや、開発チームやテストチームがどのように話合うかが参考になりました。

個人的には、プロジェクトを前に進めるために、2つの要素が重要だと感じました。1つ目は、チームでプロジェクトのゴールを共有する文化があること。2つ目は、失敗という結果が出ることにも価値があること。PjMは、チーム間で協業する文化や、小さくはじめる挑戦を支援していくべきだと感じました。

E2E自動テストの前に、手動テストを効率化するための自動化を行った話

まとめ
・Bookmarkletで手動テストを効率化するアプローチ
・E2Eを選ばなかった理由
 → チームの状況的に難しかった
 → チーム状況に合わせた取り組みを選択した

本セッションでは、Webサイトのデータ入力を、Bookmarkletを使用して自動化する方法を理解しました。
Bookmarkletはお手軽でいいですね。小さくはじめて、改善を繰り返して効率化が進んでいく流れが参考になりました。

EMTEを使って自動化の費用対効果をわかりやすく表現する

EMTEとは
EMTE(Equivalent Manual Test Effort) = テストを手動で実行する工数

本セッションでは、プロジェクトのコスト管理に使用できるメトリクス"EMTE"について理解しました。
元ネタは、ご本人の技術ブログ 自動テストの効果測定に使われるEMTEとは? のようです。また、自動テストの資格(ISTQBの自動化エンジニア資格 CTAL-TAE)があるそうです。知らなかった。

FindElementできない画面のElementをFindした方法、そこから発展していくテスト自動化

なんとかならないものなのか・・・
・QAでは太刀打ちできない・・・
 ・内部コードを触れる事ができる人はいない
・開発チームとMTGしてみた
UIの自動操作が可能になった!!
 このJSONには座標などの情報が入っているため
 この情報を利用してSeleniumでクリックする!

本セッションでは、WebサイトでFindElementできない画面の場合でも、JSONの座標データを活用することで、テストを自動化する方法を理解しました。
こちらもQAチームと開発チームが協業することで、新しい価値を生み出していました。文化が良いですね。

ローコード開発テストガイドライン

まとめ
■ローコード開発のテストは
・単体テスト工程では、「カスタマイズ有無」で実装内容の方針が分かれる
・結合テスト工程および総合テスト工程では、原則従来のプロセスやアクティビティを踏襲し、結合テスト工程および総合テスト工程をテーラリング
・非機能テストでは、原則従来のプロセスやアクティビティを踏襲し、使用するツールの制約有無をツール提供会社に確認し、実施する

本セッションでは、ローコード開発においてテストガイドラインを定めて、品質を担保する方法があると理解しました。ローコード開発の品質は、採用ツール側で担保されている点を前提で、カスタマイズ部分を中心に品質担保のアプローチをしていくのですね。

APIテスト、どうやってますか?

事例1
・どうやった?
 ・内部結合
  アーキテクチャ図、プログラムフロー
  →(EC2⇔EC2、EC2⇔RDS)観点だし、テストケース作成
  テストケースごとに実行
  → Response確認、EC2・RDSのLog確認
 ・Component Test
  API仕様書
  → パターンを抽出
  → パターンを組み合わせ、テストケース作成
  → テストケース毎に実行
  → Response確認

本セッションでは、テスト自動化ソリューションを行っているHuman Crestにおける、APIテストのメリットと課題、事例が参考になりました。
また、AWSのEC2/RDSのAPIテストにおいて、テストチームの具体的な活動がイメージできました。

中の人が語るテスト自動化SaaSのCS - カスタマーサポート、そしてサクセスへ -

カスタマーサポートの流れ
一次切り分け > 技術的調査 > 問題対応

本セッションでは、Autifyの中の人による、WEBアプリ向けのE2Eテストのサービスの概要を理解しました。
Autifyは、セッション参加者のコメントでも好評のようで、カスタマーサポートが充実しているように見受けられました。テスト自動化の選択肢として、QA自動化プラットフォームを利用するのもアリですね。

モバイルアプリ観点

E2Eテストのflakyと向き合う

テスト自動化におけるflakyとは
・同じコードで成功と失敗の両方が観測できるテスト
・「不安定なテスト」ではなく「不安定なテスト結果」
取り組んでいる工夫
・Flakyが発生したときのルールを決める
 ・例)別のビルドで2回Flakyが発生したら、開発チームに確認する

本セッションでは、テスト自動化におけるflaky≒不安定なテスト結果に対して、取り組んでいる工夫などが参考になりました。
モバイルアプリ開発でも、flakyの調査で時間を無駄にしている場合があるので、flakyに対してルールを決めて対応することを取り入れたいですね。

AppiumのWebViewアプリテストの仕組みとハマりどころ

WebViewアプリのデバッグ Android
・Chrome DevToolsを使う
Appium
・OSSでクロスプラットフォームなE2Eテスト自動化フレームワーク
・WebDriverを拡張したプロトコルでiOSやAndroidアプリを自動操作できる
・WebViewアプリテストではコンテキストとウィンドウハンドルを扱う必要がある

本セッションでは、WebViewアプリのデバッグ、テスト自動化、ハマりどころがまとまっており、WebViewアプリ開発時に参考になる資料だと感じました。
基本的なツールや用語の説明から、テストの仕組みで必要となる知識(通信経路や要素探索など)まで、一通りまとまっていて分かりやすかったです。Android/iOSどちらも説明がある点もありがたいです。

まとめ

今回は、PjMとモバイルアプリエンジニアの観点で、オンライン勉強会『ソフトウェアテスト自動化カンファレンス2020』に参加した内容をアウトプットしました。

PjM観点では、ソフトウェアテスト自動化を「小さくはじめて大きく育てる」事例が印象に残りました。また、ソフトウェアテスト自動化を、テストチームと開発チームが協業して解決する事例があり、チーム間で協業しやすい文化や仕組み作りも重要だと再認識しました。まずやってみるを心掛けて活動していきたいと考えています。
今後は、ソフトウェアテスト自動化の分野も、浅く広くキャッチアップを続けていこうと思います。

モバイルアプリ観点では、「AppiumのWebViewアプリテストの仕組みとハマりどころ」が参考になりました。Android/iOSでWebViewアプリを作成する機会があれば、活用したいです。

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

Navigation Component の by navArgs() で "IllegalStateException: Fragment has null arguments" が発生する

はじめに

Jetpack の Navigation Component(の ktx 版) に含まれる by navArgs() を使用すると、以下のように簡単にFragment間の遷移時の引数を取得できます。

private val args: HogeFragmentArgs by navArgs()

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    val fuga = args.fuga
    ...
}

詳細は以下をご確認ください。
Safe Args を使用してタイプセーフにデータを渡す

Dependencies

今回使用したバージョンです

def navigation_version = "2.3.1"
implementation "androidx.navigation:navigation-fragment-ktx:$navigation_version"
implementation "androidx.navigation:navigation-ui-ktx:$navigation_version"

本題:例外が発生してしまう

以下のようなコードを書いた際に IllegalStateException: Fragment HogeFragment has null arguments が発生しました。

private val args: HogeFragmentArgs by navArgs()
private val id: Int = args.id

by navArgs() の実装を覗いてみます。以下の通りです。

@MainThread
inline fun <reified Args : NavArgs> Fragment.navArgs() = NavArgsLazy(Args::class) {
    arguments ?: throw IllegalStateException("Fragment $this has null arguments")
}

どうやら 上記の arguments が null になってしまうようです。
そこで by navArgs() の JavaDoc を確認すると以下の記述に気づきました。こちらでも確認できます。

This property can be accessed only after the Fragment's constructor.

なぜ Fragment のコンストラクタ呼び出し後でないといけないのでしょうか。
そこで、 Navigation Component がどのように Fragment の arguments をセットするのか確認します。以下がそれに該当する FragmentNavigator#navigate 内の処理です。

final Fragment frag = instantiateFragment(mContext, mFragmentManager,
        className, args);
frag.setArguments(args);

Fragment がインスタンス化された後、frag.setArguments(args) により Fragment#arguments がセットされています。
つまり、インスタンス生成過程では Fragment#argumentsnull を返します。そのため、インスタンス生成過程で by navArgs() にアクセスした場合、by navArgs() は例外を投げます。
このことが 、「by navArgs() へのアクセスは Fragment のコンストラクタ呼び出し後でないといけない」の理由だと考えられます。

プロパティ宣言時の処理はインスタンス生成過程に含まれるので、
今回の例外の原因は、以下のコードのようにプロパティ宣言時に by navArgs() にアクセスしていることだとわかります。

private val args: HogeFragmentArgs by navArgs()
private val id: Int = args.id // ダメ

ちなみに上記の instantiateFragmentargsを渡していますが、実際には argsは使われていません。以下が instantiateFragment の内容です。

@NonNull
public Fragment instantiateFragment(
    @NonNull Context context,
    @NonNull FragmentManager fragmentManager,
    @NonNull String className, 
    @SuppressWarnings("unused") @Nullable Bundle args
) {
    return fragmentManager.getFragmentFactory().instantiate(
           context.getClassLoader(), className);
}

解決策

以下のように by lazy を使い by navArgs() へのアクセスを遅らせることで解決できます。

private val args: HogeFragmentArgs by navArgs()
private val id: Int by lazy {
    args.id
}

args がプロパティとして不要なら以下でもいいと思います。

private val id: Int by lazy {
    val args: HogeFragmentArgs by navArgs()
    args.id
}

補足

本題からは逸れます。
by navArgs() の実装内容は上記で確認しました。その実装内容からわかる通り、Navigation Component(& SafeArgs)を使用しない遷移時でも、 by navArgs() の使用は可能です。
ただ、非推奨みたいです。(Navigation Component を導入し、かつ、遷移に Navigation Component 以外を使用するケースってあるのかよくわからない)

以下は by navArgs() の JavaDoc に記載のある内容です。

It is strongly recommended that this method only be used when the Fragment is created by [androidx.navigation.NavController.navigate] with the corresponding [androidx.navigation.NavDirections] object, which ensures that the required arguments are present.



参考:
https://developer.android.com/guide/navigation/navigation-pass-data?hl=ja
https://developer.android.com/reference/kotlin/androidx/navigation/fragment/package-summary

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

Oreo以降でもAndroidのWi-FiアクセスポイントをアプリでON/OFF切替できた!

qnote Advent Calendar 2020 の9日目です。

今は昔、Android Lの頃はsetWifiApEnabledをリフレクション で呼び出すことでテザリングの開始・停止を切り替えることができていました。

sample.kt
private fun changeTetheringState(state: Boolean) {
    val wifi = applicationContext.getSystemService(WIFI_SERVICE) as WifiManager
    val method = wifi.javaClass.getMethod("setWifiApEnabled", WifiConfiguration::class.java, Boolean::class.javaPrimitiveType)
    method.invoke(wifi, null, state)
}

こんなやつですね

今更ですが、この方法がAndroid 8(APIレベル:26)からは使えなくなっています。
僕も最近になってやろうとしたら使えなくて困りました...。
元々がリフレクションを使ったやり方なので仕方ないですが、なんとかしたいですよね?

なんとかいたしましょう!!!

呼び出すべきメソッド

startTethering.java
@SystemApi
@Deprecated
@RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
public void startTethering(int type, boolean showProvisioningUi, final OnStartTetheringCallback callback, Handler handler)
stopTethering.java
@SystemApi
@Deprecated
@RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
public void stopTethering(int type)

この2つのメソッドをリフレクションで呼び出します。
問題はstartTetheringの第三引数にあるOnStartTetheringCallbackで、これは抽象クラスとなっています。

OnStartTetheringCallback.java
@SystemApi
@Deprecated
public static abstract class OnStartTetheringCallback {
    /**
     * Called when tethering has been successfully started.
     */
    public void onTetheringStarted() {}

    /**
     * Called when starting tethering failed.
     */
    public void onTetheringFailed() {}
}

抽象クラスはリフレクションを使っても生成ができないので詰まってしまいました。

Dexmaker

隠された抽象クラスの生成について調べてみると以下の記事を見つけました。
java - Instance of abstract class with hidden constructor - Stack Overflow

こちらでDexmakerというライブラリを使って抽象クラスを生成できるという記述があったので試していきます。

ライブラリのページにはGradleのdependenciesに以下の記述をするようにあります。
androidTestImplementation ‘com.linkedin.dexmaker:dexmaker-mockito:2.28.0’
今回はテストで使う訳ではないのでimplementationに書き換えてみたのですが、Stack OverflowにあるProxyBuilderなるクラスが存在しません…!

内容が古いのでダメなのか…と、半ば諦めていたのですが、
Stack Overflowでは「You will need dexmaker.1.4.jar and dexmaker-dx.1.4.jar」と書いてあったのでひとまずimplementation ‘com.linkedin.dexmaker:dexmaker:2.28.0’に書き換えてみたところProxyBuilderが見つかりました!

わざわざ古い方でやる事もないと思うのでこのまま進めます。

実装

という訳で実装は以下のようになりました。
エラーハンドリング等々は特に考慮しませんでしたがとりあえず動いたコードになります。

startTethering.kt
private fun startTethering() {
    fun getOnStartTetheringCallbackClass(): Class<*>? {
        return Class.forName("android.net.ConnectivityManager\$OnStartTetheringCallback")
    }

     val connectivityManager = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager

     val proxy = ProxyBuilder.forClass(getOnStartTetheringCallbackClass())
            .dexCache(cacheDir).handler(InvocationHandler { proxy, method, args -> ProxyBuilder.callSuper(proxy, method, *args) }).build()

    val method: Method = connectivityManager.javaClass.getDeclaredMethod("startTethering", Int::class.javaPrimitiveType, Boolean::class.javaPrimitiveType, getOnStartTetheringCallbackClass(), Handler::class.java)
    method.invoke(connectivityManager, ConnectivityManager.TYPE_MOBILE, false, proxy, null)
}

今回はテザリングできさえすればよかったのでコールバックは何もさせていませんが、もし「OnStartTetheringCallback」で何かしたいことがある場合には、「InvocationHandler」で「ProxyBuilder.callSuper」している箇所でmethod名で分岐させて処理を書けばOKです。
適当なコールバッククラスを作って事前に引数として渡すのも良さそうです。

「stopTethering」はもはやおまけですね…

stopTethering.kt
private fun stopTethering() {
    val connectivityManager = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
    val method: Method = connectivityManager.javaClass.getDeclaredMethod("stopTethering", Int::class.javaPrimitiveType)
    method.invoke(connectivityManager, ConnectivityManager.TYPE_MOBILE)
}

備考

テザリングのON/OFF切替にはシステム設定の変更の許可が必要で、普通のパーミッション許可と違うので、ストアリリースするアプリに組み込むのはちょっと抵抗があるかもしれません…
それから自分の端末ではないので詳細不明ですが一部端末で動作しないようです。

それから、今回使ったメソッドも@Deprecatedなのできっとまた使えなくなる日がくるのでしょう...

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

Android開発 kotlin ボタン、画面遷移の実装

はじめに〜自己紹介〜

工業高校を出てから情報系の専門学校に2年通い今年新卒で就職しました。
ただいまアプリ開発に奮闘中です。

投稿した経緯

会社でiOS/Androidアプリの開発を頼まれて、Androidアプリの開発でだいぶ手こずっているので今後のためのメモとして投稿します。Flutterとか使った方よかったのかな...(苦)もしかしたら間違っていることがあるかもしれませんが悪しからず。

開発環境

  • iMac 2019モデル Big sur(BootCampでWindowsを使用しています。)
  • 32GBメモリ
  • Android Studio
  • kotlin

Kotlinとは

はじめにKotlinについて少しだけ語ろうかと思います。
kotlinは比較的に新しいプログラミング言語で、2011年にJetBrainsにより開発され、2017年にGoogleにより、Androidアプリ開発の公式サポートを受けました。JVM上で動作する言語です。Androidだけでなくサーバとかでも使えるみたいです?最大の特徴はJavaと100%の相互運用が可能というとこでしょうかね!

本題

SwiftUIでiOSアプリを開発した後にKotlinにうつった時に非常に苦労しました(今もしていますが?)
では早速いきましょう!

-ボタン編

Buttonを画面に追加したらxml内にも勝手に追加されます

activity_main.xml
    <Button
        android:id="@+id/button" <--これ!
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Button"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

(xml名はその時によりますよ)
xml内のandroid:id="@+id/button"MainActivitiy.ktで使用します。

MainActivity.kt
    class MainActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)

            //ここからボタンのコード
            val button: Button = findViewById<Button>(R.id.button)
            button.setOnClickListener{
                //ここに処理内容を書く
                Toast.makeText(this,"タップ",Toast.LENGTH_LONG).show()
             }
           //ここまで
        }
    }

findViewByIdで先程のボタン固有のIDを指定します。指定の仕方はR.id.【id名】です
ちなみに<>の中に入ってるのは変数の型です。今回はボタンを押したらトースト通知が出る処理を書きました!これでボタンの基本は大体オッケーです?setOnClickListenerはボタン以外にも画像などにもしようできますよ!

-画面遷移編

アプリ作成時には欠かせない画面遷移です。今回はもっとも簡単に実装できる方法をご紹介します

手順

1.ファイル→新規→空のアクティビティ(任意)からアクティビティを追加します。
 >ここで設定した名前をあとから使用します。わかりやすくしときましょう!
2.遷移元のktファイルに遷移のためのコードを書きます。
3.遷移先のktファイルに戻るためのコードを書きます。

実装例

遷移元ktファイル

MainActivity.kt
    class MainActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
            val button: Button = findViewById<Button>(R.id.button)
            button.setOnClickListener{
                //ここから遷移用のコード
                val intent = Intent(this,MainActivity2::class.java)    //intentインスタンスの生成(第二引数は遷移先のktファイル名)
                startActivity(intent)
                //ここまで
            }
        }
    }

遷移先ktファイル

MainActivity2.kt
    class MainActivity2 : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main2)
        }
        val button2:Button = findViewById<Button>(R.id.button2)
        button2.setOnClickLitener{
            finish()    //これが画面遷移元へ戻るやつ
        }
    }

今回はボタンで画面遷移を行うようにしました。
これで、遷移元のボタンを押すと遷移先に遷移し、遷移先のボタンをおすと遷移元の画面に戻ってきます。
ついでにIntentを使用した値渡しのコードも紹介します

MainActivity.kt
    val button:Button = findViewById<Button>(R.id.button)
    button.setOnClickListener{
        val intent = Intent(this,MainActivity2::class.java)
        val text = "Hello,Kotlin!"
        intent.putExtra("TEXT_KEY",text)                       //第一引数KEY値(何でも良い),第二引数渡したい値
   }
MainActivity2.kt
    class MainActivity2 : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main2)
        }
        val text = intent.getStringExtra("TEXT_KEY")        //遷移元で指定したKEYを使用してデータを受け取る
    }

意外と簡単に画面遷移の実装と値の受け渡しができますね!

今回はここまでにしておきます。
また何か分かったら随時更新していこうと思います。

わかりにくい点や間違っているところがありましたらコメント欄にてお願いします。

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

最新の Retrofit の使い方についてまとめた(2020 年 12 月時点)

本記事はAndroid Advent Calendar 2020 - Qiitaの 7 日目の記事です。

最近、今更ながら Retrofit で HTTP クライアントを実装しました。
使い方をネットで調べていたのですが、古い情報が多いように感じたので、これから Retrofit を導入したい人向けに最新の使い方をまとめます。
※2020 年 12 月時点の情報なので、今後古くなる可能性があります。

Retrofit とは

Retrofitとは、型安全な Android 向けの HTTP クライアントライブラリです。
正確にはOkHttpのラッパーで、アノテーションなどを使ってより実装しやすくするためのライブラリです。
アプリ アーキテクチャ ガイドにも取り上げられており、HTTP クライアントを実装する上でメジャーなライブラリといえます。

導入方法

app/build.gradleに以下を追加してください。

app/build.gradle
def retrofit_version = "2.9.0"
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:converter-moshi:$retrofit_version"
implementation "com.squareup.okhttp3:logging-interceptor:4.9.0"

com.squareup.retrofit2:converter-moshiは JSON ライブラリのMoshiを Retrofit 内部で使うために必要です。
Moshi とは、同じ JSON ライブラリのGsonの不満点を解消したライブラリです。
Kotlin との相性が良いため、特に理由がなければ Moshi をおすすめします。
詳細は以下の記事を参照してください。
Android Retrofit2 と Moshi - Qiita

Digest 認証をしたい場合

Retrofit もとい OkHttp で Digest 認証をしたい場合、app/build.gradleに追加で以下のライブラリを追加してください。

app/build.gradle
implementation 'com.burgstaller:okhttp-digest:2.5'

使い方

例で載せているソースコードやデータはこちらで適当に用意したサンプルのため、そのままコピー&ペーストしても使えません。適宜読み替えてください。

JSON 形式のレスポンスの取得

例えば以下の JSON を返却する API があるとします。

{
  "users": [
    {
      "id": 1,
      "detail": "ユーザ1"
    },
    {
      "id": 2,
      "detail": "ユーザ2"
    },
    {
      "id": 3,
      "detail": "ユーザ3"
    },
    {
      "id": 4,
      "detail": "ユーザ4"
    }
  ]
}

メソッドはGET、エンドポイントはapi/v1/users/{user_name}/infoとします。

上記の例の場合、クライアント側の実装は以下のようになります。

UserService.kt
interface UserService {
    @GET("api/v1/users/{user_name}/info")
    suspend fun getUserInfo(
        @Path("user_name") userName: String,
        @Query("query") query: String
    ): Response<UserResponse>
}

data class UserResponse(
    // 変数名とJSONのキー名を一致させている
    val users: List<User>
)

data class User(
    // 変数名とJSONのキー名を一致させている
    val id: Int,
    val detail: String
)

@GET("api/v1/users/{user_name}/info")で、メソッドとエンドポイントを指定します。

メソッドは@GET以外にも@POST@PATCHなど用意されており、すべての API に対応できるはずです。

エンドポイントの中でパスが動的に変化する箇所(例で言うとuser_name)がある場合、@Path("user_name")と指定することで、引数で渡されてきた値を設定できます。
GET なので URL パラメータを付与できます。付与する場合、@Query("query")と指定することで、引数で渡されてきた値を設定できます。
例えばuser_namehogequerytestの場合、エンドポイントはapi/v1/users/hoge/info?query=testになります。

coroutine にも対応しているので、suspend関数として定義できます。

Responseのジェネリクスに、JSON をでシリアライズする際のデータ型UserResponseを指定します。注意点として、変数名と JSON のキー名を一致させる必要があります。

Service の実体を生成

UserService はインタフェースなので、Retrofit.Builderで以下のようにインタフェースの実体を生成します。

// Timberを使う場合
val logging = HttpLoggingInterceptor {
    Timber.tag("OkHttp").d(it)
}
logging.setLevel(HttpLoggingInterceptor.Level.BASIC)

val client = OkHttpClient.Builder()
    .addInterceptor(logging)
    .build()

val userService = Retrofit.Builder()
    .baseUrl("https://qiita.com")
    .client(client)
    .addConverterFactory(MoshiConverterFactory.create())
    .build()
    .create(UserService::class.java)

もしロガーにTimberを使用している場合、HttpLoggingInterceptorの中で Timber を設定できます。
addConverterFactoryで、どの JSON ライブラリを使うか指定できます。Moshi を使う場合は、MoshiConverterFactory.create()を設定します。
最後のcreateで生成したい Service のクラスを指定します。

また、Digest 認証をやりたい場合は以下のようになります。

// Digest認証に使用するUserNameとPasswordを設定する
val digestAuthenticator = DigestAuthenticator(Credentials("UserName", "Password"))
val authCache: Map<String, CachingAuthenticator> = ConcurrentHashMap()

// Timberを使う場合
val logging = HttpLoggingInterceptor {
    Timber.tag("OkHttp").d(it)
}
logging.setLevel(HttpLoggingInterceptor.Level.BASIC)

val client = OkHttpClient.Builder()
    .authenticator(CachingAuthenticatorDecorator(digestAuthenticator, authCache))
    .addInterceptor(AuthenticationCacheInterceptor(authCache))
    .addInterceptor(logging)
    .build()

val userService = Retrofit.Builder()
    .baseUrl("https://qiita.com")
    .client(client)
    .addConverterFactory(MoshiConverterFactory.create())
    .build()
    .create(UserService::class.java)

呼び出し方

生成後は先ほど定義したメソッドを以下のように呼び出します。
呼び出す際、getUserInfoは suspend 関数なので、別の suspend 関数から呼び出すか、コルーチンビルダー内で呼び出さないとビルドエラーになります。

launch {
    val response = userService.getUserInfo("hoge", "test")
    if (response.isSuccessful) {
        val users = response.body()
    }
}

response.isSuccessfulはリクエストに成功した場合trueとなります。
response.body()は実際に取得した JSON をジェネリクスで指定したデータ型UserResponseとして取得できます。

HTTP ステータスしか返却しない場合

ResponseのジェネリクスにVoidを指定します。
それ以外は同じなので説明は省略します。

UserVerificationService.kt
interface UserVerificationService {
    @GET("api/v1/user/verification")
    suspend fun getUserVerification(): Response<Void>
}

ファイルのダウンロード

クライアント側の実装は以下のようになります。

DownloadService.kt
interface DownloadService {
    @GET
    suspend fun downloadFile(@Url fileUrl: String): Response<ResponseBody>
}

@Urlで API のパス自体を動的に変更でき、任意の URL を渡せるようになります。
Responseのジェネリクスに、バイナリデータを受け取れるようResponseBodyを指定します。

呼び出し側の実装は以下のようになります。

launch {
    val fileUrl = "https://html5demos.com/assets/dizzy.mp4"
    val response = downloadService.downloadFile(fileUrl)
    if (response.isSuccessful) {
        val inputStream = response.body()?.byteStream() ?: return
        val file = File(context.getExternalFilesDir(null), "dizzy.mp4")
        DataInputStream(inputStream).use { dataInputStream ->
            FileOutputStream(file).use { fileOutputStream ->
                DataOutputStream(BufferedOutputStream(fileOutputStream)).use { dataOutStream ->
                    // データ読み込み
                    val b = ByteArray(4096)
                    var readByte: Int
                    while (-1 != dataInputStream.read(b).also { readByte = it }) {
                        dataOutStream.write(b, 0, readByte)
                    }
                    dataOutStream.flush()
                }
                fileOutputStream.flush()
            }
            dataInputStream.close()
        }
        inputStream.close()
    }
}

response.body().byteStream()でバイトデータを取得できるので、あとはファイルに書き込みます。
注意点として、最後にinputStream.close()を呼び出してリソースを解放する必要があります。

ファイルのアップロード

クライアント側の実装は以下のようになります。

UploadService.kt
interface UploadService {
    @Multipart
    @POST("api/v1/upload")
    suspend fun postUpload(
        @Part file: MultipartBody.Part
    ): Response<Void>
}

@MultipartContent-Type: multipart/form-dataを送信するための定義です。
仕様によっては@Multipartがいらないそうです。詳しくは以下の記事を参照してください。
Retrofit2 で MultipartPOST - Qiita

引数には@Partを指定して、フォームデータを送るための定義をします。
multipart/form-dataについては以下のサイトが参考になりました。
【HTTP】multipart/form-data の boundary って何ぞや? - Qiita

呼び出し側の実装は以下のようになります。

launch {
    val file = File(context.getExternalFilesDir(null), "result.log")
    val requestBody = file.asRequestBody(MultipartBody.FORM)
    val multipartBody = MultipartBody.Builder("--*****")
        .addFormDataPart("file", file.name, requestBody)
        .build()
    uploadService.postUpload(multipartBody.part(0))
}

File クラスに拡張関数asRequestBodyが追加されているので、これを利用してリクエストボディを生成します。その際、MultipartBody.FORMを指定してContent-Type: multipart/form-dataを設定します。
最後にMultipartBody.Builderでマルチパートボディを生成します。
Builderの引数に設定している--*****は boundary です。
addFormDataPartで生成したリクエストボディを格納します。
multipartBody.partは List 型で、MultipartBody.Builderで格納した FormDataPart の数に応じて変化します。
今回は 1 つしか入れていないので、インデックス 0 を指定します。

まとめ

最新の Retrofit の使い方についてまとめました。
実際以前の書き方では非推奨になっているので、このやり方を見て参考になれば幸いです。
もし間違っている点などあれば指摘いただけると嬉しいです。

参考 URL

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