【Android】【ビルドエラー】Could not find method setVariantDir()エラーの解決策

エラー

Androidのビルドで以下のエラーが出ました

com.android.build.gradle.internal.crash.ExternalApiUsageException: org.gradle.internal.metaobject.AbstractDynamicObject$CustomMessageMissingMethodException: Could not find method setVariantDir() for arguments [debug] on task ‘:app:processDebugGoogleServices’ of type com.google.gms.googleservices.GoogleServicesTask.

解決策

google-servicesの4.3.6のバージョンが原因らしく、4.3.5に修正するとうまく行きました。

対象ファイル
build.gradle

1
2
3
4
// 前
classpath 'com.google.gms:google-services:4.3.6'
// 後
classpath 'com.google.gms:google-services:4.3.5'

参考

https://stackoverflow.com/questions/67496084/why-am-i-getting-abstractdynamicobjectcustommessagemissingmethodexception-error

Android Studioでアイコン画像を追加する

※これはアプリ内に設置するアイコン画像の話です!
アプリ自体のアイコンの話ではないので注意!

画像を追加する

Android Studio > リソース・マネージャー>「+」アイコン>ベクターアセット>クリップアート
から追加したいアイコン画像を選ぶ

自分の環境だと
色:FFFFF
透明度:100%
にするといい感じになった

例えばnotifications アイコンを選択した場合、
res/drawable/ic_baseline_notifications_24.xmlファイルが作成される

画像を利用する

1
2
3
4
<hoge.view.AppCompatImageView
android:id="@+id/hoge"
android:layout_marginTop="2dp"
android:src="@drawable/ic_baseline_notifications_24" />

公式のアイコン、結構数が多くてありがたいですね。

【Android】ダイアログのサンプルコード

はじめに

DialogFragmentのサンプルコードです。

最初はインターフェースを使わない方法を模索しましたが、親がアクティビティの時は無理っぽいので諦めました。
もし親がフラグメントのみと断定できるのなら参考リンクの通りに実装すればインターフェースが不要になっていい感じになります。

あと最初はBuilderなしでイケないかなあと模索しましたが、Builderを使わないと画面回転時に変数が初期化されちゃうので諦めました。

前提知識

Android のダイアログのクラスが色々あって混乱したので整理します。

まず、ダイアログ関連のクラスは以下の3種類あります。

  1. Dialog
  2. AlertDialog
  3. DialogFragment

1 の Dialog は大元のクラスです。
これを直接扱うことはないです。

2 の AlertDialog は 1 の Dialog のサブクラスです。
一応これを使えばダイアログは表示できるのですが、これを直接使うのはメモリリークするので非推奨です。

参考
Android のダイアログ表示でメモリリーク?

じゃあどうするのということで、3 の DialogFragment を使うという訳ですね。
これを使えばライフサイクルを使えるのでメモリリークを防げます。

具体的なコードは以下の通りです。

【Android】 「5 月 5 日より、アプリがストレージへの広範なアクセスを必要とする理由をお知らせいただく必要があります」について

はじめに

5 月 5 日より、アプリがストレージへの広範なアクセスを必要とする理由をお知らせいただく必要があります

Google Play Console に上記の警告が来ていて、何のことか分からなかったので調べてみました。

背景

アプリデータのアクセス権をより限定的にしていく(他のアプリからアクセスできないようにする)ことで、これからますますセキュリティを高めたいと Google は思ってる

前提

通常のテキストデータなどは SQLite に保存できるが、画像は SQLite には保存できない。
じゃあどこに保存してるのかというと、ストレージ。
で、ストレージには内部ストレージと外部ストレージ(SD カードとか)があるが、画像などの容量が大きいものは外部ストレージを使うのが一般的。
自分のアプリもそうしてる。

Android 9(API 28)まで

デフォルト設定の場合、外部ストレージはどのアプリでも自由に使えたし、他のアプリのディレクトリも見れた。セキュリティはガバガバだった。

Android 10(API 29)から

ユーザーがファイルを詳細に管理して整理できるように、Android 10(API レベル 29)以降をターゲットとするアプリには、外部ストレージに対する特別アクセス権限がデフォルトで付与されます(対象範囲別ストレージ)。
このようなアプリでアクセスできるのは、外部ストレージにあるアプリ固有ディレクトリと、そのアプリが作成したメディアタイプだけに限られます。

参考:https://developer.android.com/about/versions/11/privacy/storage?hl=ja

つまりアプリ固有のディレクトリが付与された(対象範囲別ストレージ)。
だけどこれを利用するためにはコードの修正が必要で開発コストがかかる、、
ので、特別に対象範囲別ストレージを利用しない設定にできる

それがrequestLegacyExternalStorageを true にすること
そういう理由で今は自分のアプリは true にしてる

Android 11(API 30)から

なんとrequestLegacyExternalStorageが使えなくなった!
つまり絶対に対象範囲別ストレージを使わないといけない、、!
ただし、もしどうしても対象範囲別ストレージを使いたくないって場合はマニフェストファイルを修正(MANAGE_EXTERNAL_STORAGE)し、Google Play Console で申告が必要!

現在の自分のアプリのターゲット

targetSdkVersion 29
Android 10

利用箇所の調査

検索ワード

  • getExternalFilesDir
  • getExternalCacheDir

結論

現在の自分のアプリのターゲットは Android 10 なので、これを変更しない限りは対応不要。

と言ってももちろん近いうちに Android 11 に更新はしたい。

で、せっかくなのでその時に対象範囲別ストレージを利用するように修正すればいい、と思う。

開発コストがどのくらいかかるかは不明だけど多分そこまでだと思うし、セキュリティ的にもその方が良い。

Androidテストについて整理した

テストの種類とフレームワーク

公式ページ
https://developer.android.com/studio/test/index.html?authuser=3

Local Test

実機やエミュレータの起動なしに実施可能なテスト
Android の API に依存していない場合など
JVM 上で実行される
テストフォルダはsrc/test

Instrumented Test

実機やエミュレータの起動が必要なテスト
Android の API に依存しているメソッドの場合など
テストフォルダはsrc/androidTest

ユニットテスト

任意のクラスのメソッドを実行して返り値が期待通りかを確認するテスト
デファクトスタンダードのフレームワーク

  • JUnit4

テストランナー

複数のテストをまとめて実行するテストランナーと呼ぶ

  • org.junit.runners.JUnit4(JUnit4 標準のテストランナー)
  • AndroidJUnitRunner(Instrumented Test を実行するための Android 標準のテストランナー)
  • MockitoJUnitRunner、RobolectricTestRunner(その他のランナー)

テストコードを簡潔に書くためのアサーションライブラリ

  • AssertJ(ただし Kotlin 未対応)
  • assertk
  • Expekt

モックライブラリ

  • Mockito(ただし Kotlin 未対応)
  • Mockito-Kotlin(Kotlin 対応)

モックのフレームワーク

ローカルテストなら JUnit4 だけで OK だが、Instrumented Test を行う場合はモックが必要
Context を利用する方法

  • Robolectric

インテグレーションテスト

複数画面に跨がる機能や Service を利用する機能などのテスト
主に実機やエミュレータを利用する

UI テスト

アプリの UI を操作するテスト
基本的に Instrumented Test となる。

デファクトスタンダードのフレームワーク

  • Espresso
  • Appium

テスト時に必要な端末の設定

• 設定から開発者オプションを開く
• 次の 3 つの設定項目を「オフ」に変更する
– ウィンドウアニメスケール
– トランジションアニメスケール
– アニメーター再生時間スケール

参考書籍

Android テスト全書
https://peaks.cc/books/android_testing

体系的にまとまっていて、かなりオススメです!

【Android開発】タイムゾーンを考慮した日付・時刻の取得方法

タイムゾーンを考慮した日付・時刻の取得方法です。

1
2
3
4
5
6
7
8
9
10
import org.threeten.bp.ZoneId;
import org.threeten.bp.ZonedDateTime;

// 現在時刻
ZonedDateTime now = ZonedDateTime.now();
// 任意の日付
ZonedDateTime target = ZonedDateTime.of(2021, 04, 01, 0, 0, 0, 0, ZoneId.systemDefault());
if (now.compareTo(target_date) < 0) {
// targetが未来日付の場合はここに入る!
}

【Android開発】Retrofit2のレスポンスを文字列として取り出す方法

Retrofit2のレスポンスを文字列として取り出す方法です。
レスポンスは例えば以下のようなイメージです。

{“error_code”:”E100001000”,”messages”:[“僕はエラーメッセージだよ”]}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Call<ResponseBody> call = client.hoge();
Response<ResponseBody> response = call.execute();
if (response.isSuccessful()) {
response.body().close();
} else {
List<String> list = getErrorMessages(response.errorBody().string());
Objects.requireNonNull(getActivity()).runOnUiThread(() -> Toast.makeText(getContext(), list.get(0), Toast.LENGTH_LONG).show());
}

/**
* APIのエラーレスポンスからmessagesを取り出してListとして返す
*/
public static List<String> getErrorMessages(String errorBody) throws JSONException {
JSONObject errorJson = new JSONObject(errorBody);
String s = errorJson.getString("messages");
s = s.replace("[", "").replace("]", "");
String[] split = s.split(",");
return Arrays.asList(split);
}

replace周りで地味にハマってしまった、、

【Android開発】TextViewの一部をハイパーリンクにする【Java】

TextView の一部をハイパーリンクにする方法です。
やりたいことはこんな簡単なことなのに、Android 開発ってなんでこんなに複雑なコードになっちゃうんですかね、、

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/**
* テキストにハイパーリンクを設定する
*/
TextView textView = root.findViewById(R.id.hoge);
String link_text = "こちら";
String link_url = "https://hoge.com";
addHyperLink(textView, link_text, link_url, this);

/**
* テキストにハイパーリンクを設定する関数
*/
public static void addHyperLink(TextView textView, String link_text, String link_url, Fragment fragment) {
Map<String, String> map = new HashMap<>();
map.put(link_text, link_url);
SpannableString ss = MiscUtils.createSpannableString(textView.getText().toString(), map, fragment);
textView.setText(ss);
textView.setMovementMethod(LinkMovementMethod.getInstance());
}

/**
* 内部関数
*/
private static SpannableString createSpannableString(String message, Map<String, String> map, Fragment fragment) {

SpannableString ss = new SpannableString(message);

for (final Map.Entry<String, String> entry : map.entrySet()) {
int start = 0;
int end = 0;

// リンク化対象の文字列の start, end を算出する
Pattern pattern = Pattern.compile(entry.getKey());
Matcher matcher = pattern.matcher(message);
while (matcher.find()) {
start = matcher.start();
end = matcher.end();
break;
}

// SpannableString にクリックイベント、パラメータをセットする
ss.setSpan(new ClickableSpan() {
@Override
public void onClick(View textView) {
String url = entry.getValue();
Uri uri = Uri.parse(url);
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
fragment.startActivity(intent);
}
}, start, end, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
}

return ss;
}

参考

TextView の一部のリンク化+クリックイベントの指定、をサクッと作る

【Android開発】Can’t toast on a thread that has not called looper.prepare()エラーの解決策

表題のエラーの解決策です。

原因としては、本来Toastはメインスレッドでのみ実行できる仕様ですが、サブスレッドで実行してしまっていることです。

解決策はHandlerを使ってメインスレッドで実行するように変更すればOKです。
以下、具体例です。

修正前

1
2
3
4
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(() -> {
Toast.makeText(getContext(), "エラーメッセージ", Toast.LENGTH_LONG).show()
});

修正前

1
2
3
4
5
6
7
final Handler handler = new Handler();
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(() -> {
handler.post(() -> {
Toast.makeText(getContext(), "エラーメッセージ", Toast.LENGTH_LONG).show()
});
});

handler.postの処理はnew Handler()したスレッドで実行されるのでnew Handler()はメインスレッドで実行する必要があります!

Android開発は非同期処理が鬼門ですよね、、

おわり

【Android】Spinnerの初期表示時にonItemSelectedが呼ばれちゃう対処法

Spinnerの初期表示時にonItemSelectedが呼ばれちゃう対処法です。
初回に呼ばれるまでfocusableをfalseにしておき、初回に呼ばれたらtrueにする作戦です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class HogeFragment extends BaseFragment implements AdapterView.OnItemSelectedListener {

public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) {
spinner.setFocusable(false);
})

@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
// 画面表示時にonItemSelectedが呼ばれちゃう対処法
Spinner spinner = mRoot.findViewById(R.id.spinner_id);
if (!spinner.isFocusable()) {
spinner.setFocusable(true);
return;
}
}
}

以上です!

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×