- 投稿日:2020-09-12T19:25:32+09:00
Cookpad LicenseToolsPluginの横スクロール対策
クックパッドのAndroid向けのライセンス管理プラグイン cookpad / LicenseToolsPlugin
ライセンス表示ページ作る際にいつもお世話になってます!今回はプラグインから出力されたHTMLで横スクロールされて困ってる人へ向けての対策。
※ 2020/03/19リリースのv1.2.2
時点の対策になりますこの問題はライセンスのURLが改行されずに行で表示されることが原因となっています。
Wrap html links in licenses.html #2なのでURLを改行するように修正を行います。
Issuesを辿って旧プラグインのIssuesに行くと暫定対応の方法が書かれているのでそのとおりに対応すればOKです。
Wrap html links in licenses.html css #74これで
./gradlew generateLicensePage
のHTML出力時に改行設定を追加してくれます。
対策前 対策後 私の場合は以下のようにプロジェクトに書いてます(Issuesの内容とほぼ同じです)
app/build.gradle
apply plugin: "com.cookpad.android.plugin.license-tools" apply from: '../tools/cookpadLicensePatch.gradle'tools/cookpadLicensePatch.gradle
// Wrap html links in licenses.html // https://github.com/cookpad/LicenseToolsPlugin/issues/2 // https://github.com/cookpad/license-tools-plugin/issues/74 task patchLicensePageStyles() { doLast { def licensesHTMLFile = new File(project.rootDir, "/app/src/main/assets/licenses.html") try { def additionalStyles = " a { word-break: break-all; }" def licensesHTML = licensesHTMLFile.getText('UTF-8') licensesHTML = licensesHTML.replaceFirst(/(\s*<\/style>)/, "\n" + additionalStyles + "\$1") licensesHTMLFile.text = licensesHTML } catch(Exception e) { println e } } } generateLicensePage.finalizedBy(patchLicensePageStyles)リンク先と違うのはカスタマイズCSSファイルを使わず文字列で直接設定しているところ。
今回の対策に必要なCSSが短いのと複数ファイル管理が面倒なためです。半角スペースが入っているのは出力された
licenses.html
のStyleのインデントを揃えたかっただけなので無くても問題ありません。<style> ... input:checked ~ .license { max-height: none; } a { word-break: break-all; } </style>
tools/cookpadLicensePatch.gradle
はapp/build.gradleに直接書いて良いです。
私はbuild.gradleが大きくなりすぎないようにするためと、この設定はプロジェクトに依存しておらず他のプロジェクトでも使える、ということを表すために別ファイルに書くようにしています。
旧プラグインの修正PRがマージされていなかった理由が気になるところ。
修正されるといいな。
- 投稿日:2020-09-12T16:20:02+09:00
Inside Flutter Hooks
概要
Flutter Hooksを使う機会があり、すごい便利だなと思っていたのですが、
内部的にどんな風に実装されているのか掘り下げてみようかと思い、今回色々調べてみました。
(何か間違っていたりしたらコメントいただけると嬉しいです )Flutter Hooks とは?
React hooksをFlutterで実装したものになります。
作者はProvider等でおなじみのRemiさんです。
サンプルの実行環境
flutter: v1.20.3
flutter_hooks: 0.14.0Flutter Hooksの基本的な仕組み
useMemoized
を掘り下げる一番シンプルな
useMemoized
というhookを例にFlutter Hooksがどのような仕組みになっているのか追ってみたいと思います。そもそも
useMemoized
とは?
useMemoized
は何回ビルドが走っても初期値をキャッシュしてくれるhookです。↓簡単なサンプルとして現在時刻を
useMemoized
でキャッシュし、Floating Action Button
をタップする度に
カウンターが増えて再ビルドが走るようなサンプルを作成してみました。import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; void main() { runApp(MyApp()); } class MyApp extends HookWidget { @override Widget build(BuildContext context) { final DateTime now = useMemoized(() => DateTime.now()); // 初期値として現在日時を保存 final ValueNotifier<int> counter = useState<int>(0); return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, ), home: Scaffold( body: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( now.toString(), style: TextStyle(fontSize: 25, fontWeight: FontWeight.bold), ), Text( counter.value.toString(), style: TextStyle(fontSize: 25, fontWeight: FontWeight.bold), ) ], ), ), floatingActionButton: FloatingActionButton( onPressed: () => counter.value++, // カウンターが増えWidgetのビルドが走る child: Icon(Icons.add), ), )); } }↑ counterの値が更新されても初期値として設定した現在日時の値は変更されていないのが分かるかと思います。
(2020-09-11 11:21:40.... の箇所)
useMemoized
の実装は?こちら で実装されています。以下に関連箇所だけ抜き出しました。
T useMemoized<T>(T Function() valueBuilder, [List<Object> keys = const <dynamic>[]]) { return use(_MemoizedHook( valueBuilder, keys: keys, )); } class _MemoizedHook<T> extends Hook<T> { const _MemoizedHook( this.valueBuilder, { List<Object> keys = const <dynamic>[], }) : assert(valueBuilder != null, 'valueBuilder cannot be null'), assert(keys != null, 'keys cannot be null'), super(keys: keys); final T Function() valueBuilder; @override _MemoizedHookState<T> createState() => _MemoizedHookState<T>(); } class _MemoizedHookState<T> extends HookState<T, _MemoizedHook<T>> { T value; @override void initHook() { super.initHook(); value = hook.valueBuilder(); } @override T build(BuildContext context) { return value; } @override String get debugLabel => 'useMemoized<$T>'; }より詳細に見ていこうと思います。
useメソッド
先ずは
useMemoized
内で使用されているuse(_MemoizedHook(...
のuse
メソッド を掘り下げてみたいと思います。
このuse
メソッド は flutter_hooks/lib/src/framework.dart で以下の様に実装されています。R use<R>(Hook<R> hook) => Hook.use(hook);
Hook
というクラスのstatic メソッドuse
に引数のhook(ここでは_MemoizedHook)を渡して呼んでいます。
ここでHook
というクラスが出てきました。今度はこのHook
に着目したいと思います。
Hookクラス
Hookクラスはこちらに実装されています。
以下に要約したものを抜き出してみました。@immutable abstract class Hook<R> with Diagnosticable { const Hook({this.keys}); @Deprecated('Use `use` instead of `Hook.use`') static R use<R>(Hook<R> hook) { assert(HookElement._currentHookElement != null, ''' Hooks can only be called from the build method of a widget that mix-in `Hooks`. Hooks should only be called within the build method of a widget. Calling them outside of build method leads to an unstable state and is therefore prohibited. '''); return HookElement._currentHookElement._use(hook); } final List<Object> keys; @protected HookState<R, Hook<R>> createState(); }先程出てきた
Hook.use
に着目したいと思います。
@Deprecated
となっていて直接Hook.use
は呼ばずに先程のuseメソッド
を呼ぶようにとなっています。ここでは
HookElement._currentHookElement._use(hook)
が呼ばれており、
HookElement._currentHookElement
は後でも出てきますが こちらにstatic変数として定義されています。
HookElement._currentHookElement._use
は別途掘り下げるとしてcreateState
で生成されるHookState
を見てみます。
HookStateクラス
こちらに実装されています。こちらも要約したものを以下に抜き出してみました。
abstract class HookState<R, T extends Hook<R>> with Diagnosticable { @protected BuildContext get context => _element; HookElement _element; T get hook => _hook; T _hook; @protected void initHook() {} @protected void dispose() {} @protected R build(BuildContext context); @protected void didUpdateHook(T oldHook) {} void deactivate() {} void reassemble() {} @protected void setState(VoidCallback fn) { fn(); _element .._isOptionalRebuild = false ..markNeedsBuild(); } }こうして見ると
Hook
とHookState
の関係がStatefulWidget
とState
の関係に似てますね
HookState
内に先程出てきたHookElement
を保持し、BuildContext
としてgetできるようにしています。
後で出てきますが、HookElement
はComponentElement
をimplementしているのでBuildContext
として扱う事ができます。
詳しくはこちらを参照して下さい。
FlutterのBuildContextとは何か - Qiita
HookElement mixin
こちらに実装されています。こちらも要約して抜き出してみました。
HookElement
のuse
メソッドでの処理がFlutter Hooksのキモとなる処理になってきます。mixin HookElement on ComponentElement { static HookElement _currentHookElement; _Entry<HookState> _currentHookState; final LinkedList<_Entry<HookState>> _hooks = LinkedList(); LinkedList<_Entry<bool Function()>> _shouldRebuildQueue; LinkedList<_Entry<HookState>> _needDispose; bool _isOptionalRebuild = false; Widget _buildCache; @override Widget build() { // 色々な前処理 ... _currentHookState = _hooks.isEmpty ? null : _hooks.first; // ① HookElement._currentHookElement = this; // ② try { _buildCache = super.build(); } finally { // 後処理 .... } return _buildCache; } R _use<R>(Hook<R> hook) { if (_currentHookState == null) { _appendHook(hook); } else if (hook.runtimeType != _currentHookState.value.hook.runtimeType) { // ③ final previousHookType = _currentHookState.value.hook.runtimeType; _unmountAllRemainingHooks(); if (kDebugMode && _debugDidReassemble) { _appendHook(hook); } else { throw StateError(''' Type mismatch between hooks: - previous hook: $previousHookType - new hook: ${hook.runtimeType} '''); } } else if (hook != _currentHookState.value.hook) { final previousHook = _currentHookState.value.hook; if (Hook.shouldPreserveState(previousHook, hook)) { // ④ _currentHookState.value .._hook = hook ..didUpdateHook(previousHook); } else { _needDispose ??= LinkedList(); _needDispose.add(_Entry(_currentHookState.value)); _currentHookState.value = _createHookState<R>(hook); } } final result = _currentHookState.value.build(this) as R; _currentHookState = _currentHookState.next; // ⑤ return result; } }先程
HookState
で出てきたHookElement._currentHookElement
が定義されています。大まかな処理の流れ
_currentHookState
HookState
の LinkedListになっておりビルド中に useXXX で呼ばれた際の各HookStateの一覧を呼ばれた順で保持していますbuild メソッド
- ① : 前回Widgetのビルドが走った際の
HookState
のLinkedList のキャッシュがあれば_currentHookState
にセットしています- ② : staticな領域に現在build中のHookElementをセットしています
HookWidget
やStatefulHookWidget
クラスを使ったWidgetのビルドでは内部的にHookElement
を使用しているのでHookElement
のbuild()
メソッドが呼ばれます。use メソッド
- ③ : 前回ビルドした時のHookと今回ビルド中のHookの
runtimeType
が異なっている場合
- _currentHookStateに格納されているHookStateをすべてクリアします
- Debug中の場合(開発しててuseXXXを変更した等)今回Hookを新たに格納します
- ④ : 前回ビルド時のHookと異なるオブジェクトの場合
shouldPreserveState
メソッドでKeyが前回と異なっているかチェックを行います
- 異なっている場合
- 一旦以前のHookStateは破棄して今回のHookStateに入れ替えます
- 異なっていない場合
HookState
のdidUpdateHook
が呼ばれます- ⑤ : 次に備えて、
_currentHookState.next
で次のHookStateにLinkedList内の位置を移動させていますLinkedListを使用して前回ビルドのHookStateと比較する処理は flutter_hooksのREADMEにも載っていますが
React hooks: not magic, just arrays | by Rudi Yardley | Medium
こちらを読むとさらに理解が深まりそうでした。
useMemoized
に立ち返ってここで
useMemoized
内で呼ばれていたHook.use
に立ち返ってみるとHookElement._currentHookElement._use(hook)
が呼ばれていました。引数の
hook
は_MemoizedHook
が設定されuse
メソッドが呼ばれることになります。
_currentHookState
がnullの場合(初めてWidgetビルド中にuseXXXが呼ばれた場合)は_appendHook
が呼ばれてます。
_appendHook
は何をしているかというとextension on HookElement { HookState<R, Hook<R>> _createHookState<R>(Hook<R> hook) { assert(() { _debugIsInitHook = true; return true; }(), ''); final state = hook.createState() .._element = this .._hook = hook ..initHook(); assert(() { _debugIsInitHook = false; return true; }(), ''); return state; } void _appendHook<R>(Hook<R> hook) { final result = _createHookState<R>(hook); _currentHookState = _Entry(result); _hooks.add(_currentHookState); } }
Hookクラス
のcreateState
を呼び出してHookState
を作成し_currentHookState
に設定しています。
上記であった前回ビルド時のHookStateと比較する等の処理が終わったあと以下の処理が行われます。final result = _currentHookState.value.build(this) as R; _currentHookState = _currentHookState.next; return result;ここで
HookState
の buildメソッドを呼び出して戻りuseメソッドの戻り値として返しています。
useMemoized
の場合だと_MemoizedHookState
の buildが呼ばれることになり、
_MemoizedHookState
の buildは単に保存した値を返しているだけなので、いくらWidgetのビルドが走っても
更新されない保存された値を返し続けるという仕組みになっているようです@override T build(BuildContext context) { return value; }
HookWidget
最後にhookを使う側で必要な
HookWidget
クラスを見てみたいと思います。abstract class HookWidget extends StatelessWidget { const HookWidget({Key key}) : super(key: key); @override _StatelessHookElement createElement() => _StatelessHookElement(this); } class _StatelessHookElement extends StatelessElement with HookElement { _StatelessHookElement(HookWidget hooks) : super(hooks); }すごいシンプルで、
StatelessWidget
クラスを継承し、Elementを生成する際に
HookElement
を実装した_StatelessHookElement
を返すようになっています。また、
StatefulWidget
版も用意されている様でした。abstract class StatefulHookWidget extends StatefulWidget { const StatefulHookWidget({Key key}) : super(key: key); @override _StatefulHookElement createElement() => _StatefulHookElement(this); } class _StatefulHookElement extends StatefulElement with HookElement { _StatefulHookElement(StatefulHookWidget hooks) : super(hooks); }ここまで仕組みがどうなっているのか超ざっくり説明しました。
主な登場人物とざっくり相関図
これまでで登場してきたクラスやmixinを相関図にしてみました。
- Hook
- HookElement
- HookState
- HookWidget
- StatefulHookWidget (※今回は省いています)
※ 間違っていたらご指摘いただけると嬉しいです
他のhooks達
ここまでで何となくでも仕組みが理解できたので、他のhooksも見てみたいと思います。
useContext
実装はこちら
これは至ってシンプルで以下の様に実装されています。BuildContext useContext() { assert( HookElement._currentHookElement != null, '`useContext` can only be called from the build method of HookWidget', ); return HookElement._currentHookElement; }実装をみるとなぜbuild中じゃないと呼び出せないのか分かりますね
ちなみにHookElement._currentHookElement
が null になるタイミングはWidgetのビルドが終わったタイミングになります。
useEffect
抜き出したもの
void useEffect(Dispose Function() effect, [List<Object> keys]) { use(_EffectHook(effect, keys)); } class _EffectHook extends Hook<void> { const _EffectHook(this.effect, [List<Object> keys]) : assert(effect != null, 'effect cannot be null'), super(keys: keys); final Dispose Function() effect; @override _EffectHookState createState() => _EffectHookState(); } class _EffectHookState extends HookState<void, _EffectHook> { Dispose disposer; @override void initHook() { super.initHook(); scheduleEffect(); } @override void didUpdateHook(_EffectHook oldHook) { super.didUpdateHook(oldHook); if (hook.keys == null) { if (disposer != null) { disposer(); } scheduleEffect(); } } @override void build(BuildContext context) {} @override void dispose() { if (disposer != null) { disposer(); } } void scheduleEffect() { disposer = hook.effect(); } @override String get debugLabel => 'useEffect'; @override bool get debugSkipValue => true; }使い方としては
useEffect
第一引数で渡された処理が初回呼ばれて、以降は第二引数のKeyに変更が無い限り
処理が呼ばれる事はありません。useEffect(() { print('useEffect'); return () => print('dispose'); }, const []);↑の例だと第二引数のKeyに
const []
を指定しているので初回だけしかprint('useEffect');
は呼ばれません。
また第一引数の戻り値として終了処理をFunction()
として返せるのでもう一度処理が呼ばれる前にクリアさせたい等に使えそうです。Keyが変更されるサンプルとして
useMemoized
のサンプルにuseEffect
を呼ぶ処理を追加してみました。// ... 省略 class MyApp extends HookWidget { @override Widget build(BuildContext context) { final DateTime now = useMemoized(() => DateTime.now()); final ValueNotifier<int> counter = useState<int>(0); // ☆ここから追加 useEffect(() { print('useEffect'); return () => print('dispose'); }, [counter.value]); // ... 省略↑のサンプルを実行し + ボタンをタップすると
print('useEffect');
とprint('dispose');
が呼ばれるのが分かるかと思います。
useEffect
のしくみ初回呼ばれる
initHook
内でscheduleEffect
メソッドを呼び出しています。
scheduleEffect
メソッドがどうなっているかというとvoid scheduleEffect() { disposer = hook.effect(); }
useEffect
の第一引数で渡されたFunction()
を 呼び出し戻り値の dispose を内部で保存しています。
disposeはここではtypedef Dispose = void Function();
として定義されています。
このタイミングで初回の処理を呼び出しています。次に第二引数のKeyが変更された時点の処理を見てみたいと思います。
} else if (hook != _currentHookState.value.hook) { final previousHook = _currentHookState.value.hook; if (Hook.shouldPreserveState(previousHook, hook)) { _currentHookState.value .._hook = hook ..didUpdateHook(previousHook); } else { _needDispose ??= LinkedList(); _needDispose.add(_Entry(_currentHookState.value)); _currentHookState.value = _createHookState<R>(hook); } }既に説明した通り、
HookElement
のuse
メソッド内でKeyが変更されたかの判定をshouldPreserveState
で行っており
Keyが変更されている場合、新たにHookStateを作り直しています。
作り直す際にinitHook
が呼ばれ内部でscheduleEffect
を呼んでいます。
破棄された方のHookStateはbuildの最後でdispose
が呼ばれ、内部で保持していたdisposer()
を呼び出しています。
useState
抜き出したもの
ValueNotifier<T> useState<T>([T initialData]) { return use(_StateHook(initialData: initialData)); } class _StateHook<T> extends Hook<ValueNotifier<T>> { const _StateHook({this.initialData}); final T initialData; @override _StateHookState<T> createState() => _StateHookState(); } class _StateHookState<T> extends HookState<ValueNotifier<T>, _StateHook<T>> { ValueNotifier<T> _state; @override void initHook() { super.initHook(); _state = ValueNotifier(hook.initialData)..addListener(_listener); } @override void dispose() { _state.dispose(); } @override ValueNotifier<T> build(BuildContext context) { return _state; } void _listener() { setState(() {}); } @override Object get debugValue => _state.value; @override String get debugLabel => 'useState<$T>'; }こちらは既に
useMemoized
のサンプルで使ってましたが、こちらはValueNotifier
をラップし
扱いやすくしてくれているhooksになります。
useState
のしくみこちらはシンプルで
initHook
時にValueNotifier
を生成し、build時には生成したValueNotifier
を返しています。まとめ
基本的なhooksの仕組みを何となくでも把握しとけば、他のhooksもソースコードを読むことで
ある程度理解できるようになりました
今回のように一番シンプルなものから掘り下げていくのは余分なInputが少ない分理解しやすいですね。内部的な処理が分かっていれば、useContext をWidgetのビルドタイミング以外で使用したらダメだとか
事前に分かるので、広範囲でお世話になるライブラリ等は事前に内部がどんな風になっているのか把握しておくと、
トータル的にはハマる時間が無くなってスムーズかもしれませんまた次も機会があれば何か掘り下げようかと思います。
- 投稿日:2020-09-12T15:00:45+09:00
Firebase Dynamic Links 入門
Dynamic Links とは
Dynamic Links は、アプリのインストールの有無にかかわらず、複数のプラットフォームで機能するリンクです。iOS または Android で Dynamic Links を開くと、ネイティブアプリのリンク先のコンテンツに直接移動します。
ユースケース
ウェブユーザーをアプリユーザーにする
Dynamic Links を開いたユーザーの iOS デバイスまたは Android デバイスにアプリがインストールされていない場合にインストールを促し、インストール完了後にアプリを起動してリンクを開く。SNS、メール、SMS でのキャンペーン
どのプラットフォームでも機能するリンクを使ってプロモーションの特典を提供する。iOS、Android、ウェブブラウザのいずれであっても、アプリをインストール済みかどうかにかかわらず、既存のユーザーも見込みユーザーも特典を利用できる。Android アプリでDynamic Links を利用する
こちらのサンプルコードを参考にして、 Dynamic Links を試してみましょう。
FirebaseプロジェクトとAndroidアプリの準備
サンプルコードを入手する
GitHubからソースコードをローカル環境にクローンします。
今回はdynamiclinks
ディレクトリのコードを使用します。$ git clone https://github.com:firebase/quickstart-android && cd quickstart-android/dynamiclinksFirebaseプロジェクトを作成する
次にサンプルアプリケーションをデプロイするために Firebase プロジェクトを作成します。
Android プロジェクトに Firebase を追加する
作成したFirebaseプロジェクトにクローンしたAndroidアプリを追加します。
Dynamic Links の設定
※ 以下の画像中で設定している値はグローバルで一意でなければならないので、実際に試す場合は別の値を使用してください。
URL prefix を設定する
Firebase コンソールの [Dynamic Links] から
Add URL prefix
を選択し、Domain を設定します。あとはデフォルト値のまま、Verify
とFinish
します。
Dynamic Links を作成する
続いて、
New Dynamic Link
を選択します。
Define link behavior for iOS
今回は iOS アプリは使用しないので、デフォルト値のままにします。Define link behavior for Android
deep link が Android アプリで開かれたときの挙動を設定します。
今回は、別に用意している オブジェクト認識アプリ が起動されるように設定しました。
Campaign tracking, social tags and advanced options (optional)
デフォルト値のままにします。ソースコードの編集をする
Android Studio を起動し、app/build.gradle の、
dynamic_links_uri_prefix
の値を、Set up your short URL link で設定した Link preview の値に書き換えます。アプリを起動して Dynamic Links の挙動を確認する
Android Studioでアプリを起動すると、Emulator で下のような画面が開きます。
Java の OPEN を選択します。Send の下に deep link が生成されました。
生成された deep link をコピーし、同じ Emulator デバイス内のブラウザで開きます。
Define link behavior for Android で設定したとおり、オブジェクト認識アプリが起動されました。
- 投稿日:2020-09-12T13:41:48+09:00
Compose by exampleの動画を見たメモ
概要
Jetpack Composeの実用的な使い方を知ることができる動画で、すごく色んな部分で面白かったです。
https://www.youtube.com/watch?v=DDd6IOlH3io
https://github.com/android/compose-samples にそれぞれのサンプルがあります。
Theming
Make the easy things easy and the hard things possible というのを大きく語っていました。Colorからダークテーマ対応までは Make the easy things easy にあたるようです。
color、typography、shapeを定義して作る
MaterialTheme( colors = ... typography = ... shapes = ... ) { }Color
Material color stytemのクラスで作る。
val colors = Colors( primary = ... primaryVariant = ...他にもデフォルトやベースラインのカラーパレットから作るビルダー関数もある。
これを使うと変更したい色だけ変更して作ることができるval colors = lightColors( primary = ... secondary = ...複数のテーマがあるサンプルアプリのOwlではこのビルダー関数を使って何個もテーマを作っている。
Typography
Material Themeによってデフォルトが定義されている。
何もカスタマイズしたくなければ。そのまま使える。val typography = Typography()以下のようにTextStyleを使ってカスタマイズができる。
val typography = Typography( h1 = TextStyle( fontFamily = Rubik, fontSize = 96.sp, fontWeight = FontWeight.Bold, lineHeight = 120.sp )Shape
small, medium, largeのサイズのコンポーネントのshapeを定義する。
val shapes = Shapes( small = ... medium = ... large = ...角丸だとこう。
small = RoundedCornerShape(size = 4.dp)左上をカットするにはこう。
small = CutCornerShape(topLeft = 16.dp)テーマの推奨される適応方法
以下のようにしておいて、アプリ内で横断して使えるようにすることができる。
@Compose fun YellowTheme( content: @Composable () -> Unit ) { MaterialTheme( colors = YellowLightColors, ... ) }以下のようにすることで画面の一部だけテーマを変えることができる。
fun CourseDetails(...) { PinkTheme { ... BlueTheme { RelatedCourses(...) } } }テーマの要素の使い方
型安全にそれぞれの要素にアクセスして利用できる。
Text( text = ... style = MaterialTheme.typography.subtitle1, color = MaterialTheme.colors.(ここで保管が出る) )色をコピーして利用することも簡単にできるので、色をハードコードしてしまうことを防げ、複数のテーマをサポートすることに役立つ。
val background = MaterialTheme.colors.onSurface.copy( alpha = 0.2f ) Surface(color = background) {...}smart default
バックグラウンドの色を設定するとそれに対応するコンテンツの色が勝手につく。例えば以下ではcolor = primaryを設定しているので、コンテンツの色は勝手にonPrimaryの色になる。
同様にFloatingActionButtonなどでもこの仕組は利用できる。Surface(color = MaterialTheme.colors.primary) { // ここでのデフォルトカラーは `onPrimary` になる。 }(動画外):動画とは関係なくちょっと気になったので調べてみましたが、以下のようにcontentColorが作られるようです。
@Composable fun Surface( modifier: Modifier = Modifier, shape: Shape = RectangleShape, color: Color = MaterialTheme.colors.surface, contentColor: Color = contentColorFor(color), ...) @Composable fun contentColorFor(color: Color) = MaterialTheme.colors.contentColorFor(color).useOrElse { contentColor() }また以下のようにAmbientでcontentColorが配られることで利用できるようです。 (ambientについてはこちら )
Providers(ContentColorAmbient provides contentColor, children = content)ダークテーマ対応
isSystemInDarkTheme()を使って分けるだけ。簡単!
@Composable fun PinkTheme( darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit ) { if (darkTheme) PinkDarkColors else PinkLightColors ...(動画外)isSystemInDarkTheme()の中ではこんな感じで判定している。
@Composable fun isSystemInDarkTheme(): Boolean { val configuration = ConfigurationAmbient.current return (configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration .UI_MODE_NIGHT_YES }ダイナミックテーマ
ここからは Make the easy things easy and the hard things possible の the hard things possibleにあたる部分です。
ここではダイナミックテーマのサンプルとしてJetcasterというアプリの例で、画像から色を取得してそれをテーマとして使う例。
既存のPaletteライブラリを使って画像からdominantColorを取得して、それを使っている。
これでアニメーションもできる。(軽く書いているけどすごい。。)val currentImage = ... val palette = // Paletteライブラリを使って画像からpaletteを取得 val dominantColor = // paletteからdominant colorを取得 val colors = MaterialTheme.colors.copy( primary = animate(dominantColor), ) MaterialTheme(colors = colors) { content() }Layout
- Colum: 縦に並べる
- Row: 横に並べる
- Stack: 要素同士を上に重ねる (背景と上に乗るものみたいな)
- ConstraintLayout: Androidエンジニアにはおなじみの制約でレイアウトするレイアウト
Modifierでクリックやpadding、toggleable、verticalScroll()、zoomable()なども使える。すごい。
カスタムレイアウトを作るには
@Composable fun Layout()
の使い方。これを作るにはどうすればよいか。Layoutブロック使うとカスタムレイアウトを作れる
https://youtu.be/DDd6IOlH3io?t=869 より長くなってしまったのでこの詳細は以下Qiitaに切り出しました。見てみてください。
https://qiita.com/takahirom/items/c6625cbc7ebdda49de2fAnimation
シンプルなアニメーション
アニメーションなしでの書き方
val radius = if (selected) 28.dp else 0.dp val shape = RoundedCornerShape(topLeft = radius) Surface( shape = shape ...アニメーションありでの書き方
変更したい値でanimte()を使うだけ。val radius = animate(if (selected) 28.dp else 0.dp) val shape = RoundedCornerShape(topLeft = radius) Surface( shape = shape ...シンプルな例を実装してみました。 ( https://github.com/takahirom/jetpack-compose-animation-sample )
@Composable fun SimpleAnimation() { // 値を保持させている。変更されたときにrecomposeされる。 var isRightState by remember { mutableStateOf(false) } // ここでanimate()を使う。animateは前回の値を持っており、それとの変更でアニメーションが走る。 val leftMarginSize = animate(if (isRightState) 200.dp else 50.dp) Row( Modifier.fillMaxWidth() ) { Spacer(Modifier.width(leftMarginSize)) Surface( color = Color.Green, modifier = Modifier.size(100.dp) .clickable(onClick = { isRightState = !isRightState }), ) {} } }Transition
https://github.com/android/compose-samples/blob/34a75fb3672622a3fb0e6a78adc88bbc2886c28f/Owl/app/src/main/java/com/example/owl/ui/onboarding/Onboarding.kt#L156 にサンプルがあるのですが、
少し複雑なので、かんたんなサンプルを用意しました。https://github.com/takahirom/jetpack-compose-animation-sample
// 状態を表すenumを用意する private enum class BoxSelectedState { Selected, Unselected } // 変化させたい値のPropKeyを用意する (変数を大文字から始めているのはCompose内部のコードを真似ています) private val LeftMargin = DpPropKey() private val BoxColor = ColorPropKey() private val ShapeCornerPropKey = DpPropKey() // transitionDefinitionでtransitionDefinitionを定義する private val BoxTransitionDefinition = transitionDefinition<BoxSelectedState> { state(BoxSelectedState.Selected) { // それぞれの状態ごとの値を設定する this[LeftMargin] = 50.dp this[BoxColor] = Color.Green this[ShapeCornerPropKey] = 0.dp } state(BoxSelectedState.Unselected) { this[LeftMargin] = 200.dp this[BoxColor] = Color.Red this[ShapeCornerPropKey] = 24.dp } // これはなくても動く、durationなどを設定したり、keyframe{} で進捗度が0.5のときにどれなどの設定もできる transition { LeftMargin using tween(durationMillis = 1000) BoxColor using tween(durationMillis = 2000) ShapeCornerPropKey using tween(durationMillis = 2000) } } @Composable fun BoxTransitionAnimation() { var selectedState by remember { mutableStateOf(BoxSelectedState.Selected) } val transitionState = transition( definition = BoxTransitionDefinition, toState = selectedState ) Row( Modifier.fillMaxWidth() ) { Spacer(Modifier.width(transitionState[LeftMargin])) Surface( // transitionState[]でアクセスする color = transitionState[BoxColor], shape = RoundedCornerShape(transitionState[ShapeCornerPropKey]), modifier = Modifier.size(100.dp) .clickable(onClick = { selectedState = when (selectedState) { BoxSelectedState.Selected -> BoxSelectedState.Unselected BoxSelectedState.Unselected -> BoxSelectedState.Selected } }), ) {} } }今後Android Studioにアニメーションのinspectorが追加される
今後のcanaryで以下のようにアニメーションをキーフレームで見られるような機能が追加されるみたい。
https://www.youtube.com/watch?v=DDd6IOlH3io よりアニメーションをスクショを撮ってテストできる
clockTestRuleというのにアクセスして、時間を操作でき、またスクショの比較などもできるので、それを使ってアニメーションのテストが可能なようです。
private fun compareTimeScreenshot(timeMs: Long, goldenName: String) { // Start with a paused clock composeTestRule.clockTestRule.pauseClock() // Start the unit under test showAnimatedCircle() // Advance clock (keeping it paused) composeTestRule.clockTestRule.advanceClock(timeMs) // 時間を操作する // Take screenshot and compare with golden image in androidTest/assets assertScreenshotMatchesGolden(goldenName, onRoot()) // 保存されている画像とスクショを比較する }まとめ
アニメーションの記述の容易さのように簡単なことは簡単にできてすごく書きやすいと感じました。
また難しいことも、カスタムのレイアウトが簡単に作れたり、Transitionの途中でスクショを撮って比較してテストとかこれまで考えられなかったのですが、そのようなことが可能になっているなどとても拡張性も高いと感じました。
- 投稿日:2020-09-12T13:35:56+09:00
PCに接続できない環境でもLogcatのログを取得したい時に使えるアプリ「Logcat Recorder」の紹介
背景
Androidアプリのテストで、Logcatに出力されたログを収集したい時、方法として
- Android StudioのLogcatを利用する
- 端末とPCをUSB接続するなどしておき、
adb logcat -v time > log.txt
コマンドを叩いておくがあると思います。
ですが、耐久試験で1日以上放置しないといけなかったり、PCの環境に不安がある場合に、上記の方法だとどうしても難しい場合もあります。
そんな時に使えるアプリ「Logcat Recorder」を紹介したいと思います。アプリの概要
https://apkpure.com/jp/logcat-recorder/com.namakerorin.neko
からの引用です。PCに接続できないような環境下やlogを常時取得しておきたい局面などで重宝します。
■概要
・logcat出力をSDカードなどのストレージに出力します (任意のパスに切り替え可能です)
・logcatの出力形式は"-v threadtime" です
・logファイルは時間(1分~6時間)とサイズ(500KB~100MB)で自動的に分割します
・Broadcast Intentをlogに出力します (Battery、Telephony、Configulation、Wi-fi、Bluetooth、Mediaなど)
・logの全体サイズの制限できます (100MB~4GB、制限なし)
・logを参照したり削除ができるファイラーがついていますAndroid端末の環境
環境は以下の通りです。
- 端末名: Xperia 10 Ⅱ
- OS: Android 10
導入方法
https://apkpure.com/jp/logcat-recorder/com.namakerorin.neko
からAPKファイルをダウンロードします。ダウンロードしたら、テストしたい端末とUSB接続するなどしておき、以下のコマンドでインストール&権限付与します。
adb install 'Logcat Recorder_v1.0.2_apkpure.com.apk' adb shell pm grant com.namakerorin.neko android.permission.READ_LOGS導入後、アプリを起動すると、以下のような権限許可の画面が出てきます。
両方とも許可したうえで「続行」をタップします。
すると、以下のようなダイアログが出てきます。(うろ覚えですが、Android 8では出てこなかったと思います。どのバージョンから出るようになったかは不明)いったん「OK」をタップして次に進みます。
導入はこれで完了です。使い方
アプリを有効にするには、「Record logcat」のトグルをONにするだけです。
ONにすると、ステータスバーのところにアプリアイコンが表示されます。
このアイコンが表示されている間は、ログが保存され続けることになります。
OFFにすると保存が中断されます。保存されたログの確認方法
「Log viewer」をタップすると、以下の画面に遷移します。
日時ごとにどのくらいのデータ量が保存されているかが確認できます。
チェックボックスをタップすると、画面が以下のように変化し、ゴミ箱ボタンをタップするとログを削除することができます。日時のテキストをタップすると、さらにテキストファイルが表示されます。
先ほどと同様、チェックボックスをタップすると、画面が以下のように変化し、ゴミ箱ボタンをタップするとログを削除することができます。また、ファイル名をタップすると別アプリからファイルの中身を閲覧することができます。(私の環境ではエラーが出て表示できませんでした。。)
保存されたファイル群をPCにコピーすることもできて、その場合は以下コマンドを実行します。
adb pull /storage/emulated/0/neko/ [PC上の保存したい場所]まとめ
PCに接続できないような環境下や、耐久試験などでLogcatのログを取得し続けたいときに使えるアプリ「Logcat Recorder」を紹介しました。
ただし、Android 10以降ではうまく動作しない可能性がありますので注意してください。
(もっといい方法があれば教えてくださると嬉しいです)
- 投稿日:2020-09-12T12:04:58+09:00
【Android】VideoViewやExoPlayerを使って動画がうまく再生されない時は、動画形式を確認してみよう
背景
Androidで
VideoView
やExoPlayer
を使って動画再生機能を作ったはいいものの、なぜか動画がうまく再生されない。。
あのバージョンや端末だと問題なく再生できるのに、なぜかこのバージョンや端末だと再生がうまくいかない。
何故か黒みが出たり、表示が崩れたり、再生に失敗して何も表示されない。
そんなとき、ログを見て実装が怪しくないか疑うのもいいですが、もう一つ別のアプローチを紹介したいと思います。その動画、Androidプラットフォームでサポートされてる?
Androidでは全ての動画形式がサポートされているわけではありません。
例えばmovファイルなど、再生できない動画も存在します。
公式サイトでは以下のようなガイドラインが用意されています。サポートされているメディア形式 | Android デベロッパー | Android Developers
ガイドラインを確認して、そもそもその動画形式がちゃんとサポートされているのか確認するのも大事です。
そうしないといつまでも原因がわからず詰んでしまうからです。
(遭遇した感じ、現象が発生したりしなかったりなど、かなりトリッキーでした。。)動画形式の調べ方
動画形式を調べる方法の一つとして、FFmpegを利用するというのがあります。
(他にはMediainfoを利用するという手もあります)
導入方法はHomebrewで以下コマンドを叩くだけです。brew install ffmpeg動画形式を調べるコマンドは以下になります。
ffmpeg -i [入力ファイル名]例えば、以下の動画をダウンロードして確認してみます。
https://bestvpn.org/html5demos/assets/dizzy.mp4~ ffmpeg -i /Users/Hitoshi/Downloads/dizzy.mp4 ffmpeg version 4.3.1 Copyright (c) 2000-2020 the FFmpeg developers built with Apple clang version 11.0.3 (clang-1103.0.32.62) configuration: --prefix=/usr/local/Cellar/ffmpeg/4.3.1 --enable-shared --enable-pthreads --enable-version3 --enable-avresample --cc=clang --host-cflags= --host-ldflags= --enable-ffplay --enable-gnutls --enable-gpl --enable-libaom --enable-libbluray --enable-libdav1d --enable-libmp3lame --enable-libopus --enable-librav1e --enable-librubberband --enable-libsnappy --enable-libsrt --enable-libtesseract --enable-libtheora --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxml2 --enable-libxvid --enable-lzma --enable-libfontconfig --enable-libfreetype --enable-frei0r --enable-libass --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-librtmp --enable-libspeex --enable-libsoxr --enable-videotoolbox --disable-libjack --disable-indev=jack libavutil 56. 51.100 / 56. 51.100 libavcodec 58. 91.100 / 58. 91.100 libavformat 58. 45.100 / 58. 45.100 libavdevice 58. 10.100 / 58. 10.100 libavfilter 7. 85.100 / 7. 85.100 libavresample 4. 0. 0 / 4. 0. 0 libswscale 5. 7.100 / 5. 7.100 libswresample 3. 7.100 / 3. 7.100 libpostproc 55. 7.100 / 55. 7.100 Input #0, mov,mp4,m4a,3gp,3g2,mj2, from '/Users/Hitoshi/Downloads/dizzy.mp4': Metadata: major_brand : mp42 minor_version : 0 compatible_brands: isomavc1mp42 creation_time : 2009-10-25T14:18:33.000000Z Duration: 00:00:25.00, start: 0.000000, bitrate: 510 kb/s Stream #0:0(und): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 93 kb/s (default) Metadata: creation_time : 2009-10-25T14:18:33.000000Z handler_name : (C) 2007 Google Inc. v08.13.2007. Stream #0:1(und): Video: h264 (Constrained Baseline) (avc1 / 0x31637661), yuv420p, 480x360 [SAR 1:1 DAR 4:3], 413 kb/s, 30 fps, 30 tbr, 30k tbn, 60 tbc (default) Metadata: creation_time : 2009-10-25T14:18:33.000000Z handler_name : (C) 2007 Google Inc. v08.13.2007. At least one output file must be specifiedざっくり中を紐解いていくと、まず音声が、
Stream #0:0(und): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 93 kb/s (default)となっていて、形式が「AAC-LC」っぽいのでAndroidでサポートしている形式だな、というのが分かります。
また、動画の方も、Stream #0:1(und): Video: h264 (Constrained Baseline) (avc1 / 0x31637661), yuv420p, 480x360 [SAR 1:1 DAR 4:3], 413 kb/s, 30 fps, 30 tbr, 30k tbn, 60 tbc (default)となっていて、形式が「H.264 AVC Baseline Profile(BP)」っぽいのでこれもサポートしている形式だなというのが分かります。
他にも解像度やフレームレートなどもわかるので、これらの情報を照らし合わせていきながらサポートされているかどうか判断します。
少々泥臭くはなるかもしれませんが、動画形式がサポートされていたかどうかというアプローチで問題を切り分けることができます。
サポートされていなければ実装は問題なさそう、サポートされていれば実装に問題ありそう、といった具合ですね。まとめ
Androidで動画がうまく再生されないとき、場合によっては動画形式がサポートされていない可能性もありますよ、という話と、動画形式の調べ方についてFFmpegを使ったやり方を簡単に紹介しました。
参考URL
- 投稿日:2020-09-12T12:04:58+09:00
Androidで動画がうまく再生されない時は、動画形式を確認してみよう
背景
Androidで
VideoView
やExoPlayer
を使って動画再生機能を作ったはいいものの、なぜか動画がうまく再生されない。。
あのバージョンや端末だと問題なく再生できるのに、なぜかこのバージョンや端末だと再生がうまくいかない。
何故か黒みが出たり、表示が崩れたり、再生に失敗して何も表示されない。
そんなとき、ログを見て実装が怪しくないか疑うのもいいですが、もう一つ別のアプローチを紹介したいと思います。その動画、Androidプラットフォームでサポートされてる?
Androidでは全ての動画形式がサポートされているわけではありません。
例えばmovファイルなど、再生できない動画も存在します。
公式サイトでは以下のようなガイドラインが用意されています。サポートされているメディア形式 | Android デベロッパー | Android Developers
ガイドラインを確認して、そもそもその動画形式がちゃんとサポートされているのか確認するのも大事です。
そうしないといつまでも原因がわからず詰んでしまうからです。
(遭遇した感じ、現象が発生したりしなかったりなど、かなりトリッキーでした。。)動画形式の調べ方
動画形式を調べる方法の一つとして、FFmpegを利用するというのがあります。
(他にはMediainfoを利用するという手もあります)
導入方法はHomebrewで以下コマンドを叩くだけです。brew install ffmpeg動画形式を調べるコマンドは以下になります。
ffmpeg -i [入力ファイル名]例えば、以下の動画をダウンロードして確認してみます。
https://bestvpn.org/html5demos/assets/dizzy.mp4~ ffmpeg -i /Users/Hitoshi/Downloads/dizzy.mp4 ffmpeg version 4.3.1 Copyright (c) 2000-2020 the FFmpeg developers built with Apple clang version 11.0.3 (clang-1103.0.32.62) configuration: --prefix=/usr/local/Cellar/ffmpeg/4.3.1 --enable-shared --enable-pthreads --enable-version3 --enable-avresample --cc=clang --host-cflags= --host-ldflags= --enable-ffplay --enable-gnutls --enable-gpl --enable-libaom --enable-libbluray --enable-libdav1d --enable-libmp3lame --enable-libopus --enable-librav1e --enable-librubberband --enable-libsnappy --enable-libsrt --enable-libtesseract --enable-libtheora --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxml2 --enable-libxvid --enable-lzma --enable-libfontconfig --enable-libfreetype --enable-frei0r --enable-libass --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-librtmp --enable-libspeex --enable-libsoxr --enable-videotoolbox --disable-libjack --disable-indev=jack libavutil 56. 51.100 / 56. 51.100 libavcodec 58. 91.100 / 58. 91.100 libavformat 58. 45.100 / 58. 45.100 libavdevice 58. 10.100 / 58. 10.100 libavfilter 7. 85.100 / 7. 85.100 libavresample 4. 0. 0 / 4. 0. 0 libswscale 5. 7.100 / 5. 7.100 libswresample 3. 7.100 / 3. 7.100 libpostproc 55. 7.100 / 55. 7.100 Input #0, mov,mp4,m4a,3gp,3g2,mj2, from '/Users/Hitoshi/Downloads/dizzy.mp4': Metadata: major_brand : mp42 minor_version : 0 compatible_brands: isomavc1mp42 creation_time : 2009-10-25T14:18:33.000000Z Duration: 00:00:25.00, start: 0.000000, bitrate: 510 kb/s Stream #0:0(und): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 93 kb/s (default) Metadata: creation_time : 2009-10-25T14:18:33.000000Z handler_name : (C) 2007 Google Inc. v08.13.2007. Stream #0:1(und): Video: h264 (Constrained Baseline) (avc1 / 0x31637661), yuv420p, 480x360 [SAR 1:1 DAR 4:3], 413 kb/s, 30 fps, 30 tbr, 30k tbn, 60 tbc (default) Metadata: creation_time : 2009-10-25T14:18:33.000000Z handler_name : (C) 2007 Google Inc. v08.13.2007. At least one output file must be specifiedざっくり中を紐解いていくと、まず音声が、
Stream #0:0(und): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 93 kb/s (default)となっていて、形式が「AAC-LC」っぽいのでAndroidでサポートしている形式だな、というのが分かります。
また、動画の方も、Stream #0:1(und): Video: h264 (Constrained Baseline) (avc1 / 0x31637661), yuv420p, 480x360 [SAR 1:1 DAR 4:3], 413 kb/s, 30 fps, 30 tbr, 30k tbn, 60 tbc (default)となっていて、形式が「H.264 AVC Baseline Profile(BP)」っぽいのでこれもサポートしている形式だなというのが分かります。
他にも解像度やフレームレートなどもわかるので、これらの情報を照らし合わせていきながらサポートされているかどうか判断します。
少々泥臭くはなるかもしれませんが、動画形式がサポートされていたかどうかというアプローチで問題を切り分けることができます。
サポートされていなければ実装は問題なさそう、サポートされていれば実装に問題ありそう、といった具合ですね。まとめ
Androidで動画がうまく再生されないとき、場合によっては動画形式がサポートされていない可能性もありますよ、という話と、動画形式の調べ方についてFFmpegを使ったやり方を簡単に紹介しました。
参考URL
- 投稿日:2020-09-12T10:38:18+09:00
Androidエミュレータが起動しない場合の解決法メモ
どんな内容か?
Xamarinを利用していて、Androidの最近のVersionでテストする必要がありました。デバイスマネージャで新しいデバイスを追加したのですが、起動できずに困った話です。一応の解決方法が分かったのでメモしておくことにしました。同じ悩みを抱えた方の助けとなれば幸いです。全ての起動できない現象に一致する訳ではありません。
環境
OS:Windows10
開発環境:Visual Studio 2019 Community Version 16.7.3現象
デバイスマネージャで、Andoird9.0(API28)のOSまでは起動するのですが、それ以上のAndroid10.0(API29)やAndroid11.0(API30)が起動できません。詳しく書くと、エミュレータのスマホ枠は表示されるのですが、1秒くらいですぐに消えてダウンしてしまう状況です。
試行錯誤の様子
※結論だけ知りたい方は飛ばしてください
設定(プロパティ)で音声を無効化したり、GPSを無効化したり、全部のプロパティをOFFにしてもダメ。
メモリを増やしたりいろいろ行っても全く効果ありませんでした。効果なし!
困ったときはログを確認・・・
したいのですが、Androidデバイスマネージャーから直接起動するとログがどこに出ているのかよくわかりません。
そこで、Android デバイスマネージャーから直接ではなくて、Xamarin(Visual Studio)側からエミュレータをキックしてもらうと少しログが見えました。(既に解決しているので、キャプチャーはないのですが・・・)emulator: ERROR: VkCommonOperations.cpp:525: Failed to create Vulkan instance.VCPU shutdown request VCPU shutdown request VCPU shutdown request //(繰り返し)エラーのキーワードとなりそうなのは上記のメッセージでした。
キーワードから、少しググって、こちらのstack overflowの記事にぶつかりました。HAXM(Hardware Accelerated eXecution Manager)にたどり着きます。
自分の記憶では、自らインストールした記憶はありませんが昔のことなので分かりません。HAXMの最新版を適用することで解決しました。
HAXMのインストール時に、ポップアップのメッセージで、「既にインストールされている6.0.2から7.6.1に上げて良いか?」と問合せがあったので、そこそこ古かったようです。解決策
HAXMの最新のリリースをインストールし解決しました。
私の場合は、7.6.1を適用して解決しました。ということで、エミュレータで起動しないOSがある場合は、HAXMの最新版を適用すると幸せになれるかもしれないということで。
現場からは以上です。
- 投稿日:2020-09-12T10:24:36+09:00
avdmanagerを実行し「Exception in thread "main" java.lang.NoClassDefFoundError」が発生した場合の対処法
FlutterでAndroidの開発環境を構築する際、
avdmanager
をcliから実行すると下記のようなエラーが発生した際の備忘録です。$ ~/Library/Android/sdk/tools/bin/avdmanager list Exception in thread "main" java.lang.NoClassDefFoundError: javax/xml/bind/annotation/XmlSchema at com.android.repository.api.SchemaModule$SchemaModuleVersion.<init>(SchemaModule.java:156) at com.android.repository.api.SchemaModule.<init>(SchemaModule.java:75) at com.android.sdklib.repository.AndroidSdkHandler.<clinit>(AndroidSdkHandler.java:81) at com.android.sdklib.tool.AvdManagerCli.run(AvdManagerCli.java:213) at com.android.sdklib.tool.AvdManagerCli.main(AvdManagerCli.java:200) Caused by: java.lang.ClassNotFoundException: javax.xml.bind.annotation.XmlSchema at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:602) at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178) at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:522) ... 5 more使用するJavaのバージョンをAndroid Studioに同梱されているJavaに変更することで解決しました。
# パスを通す $ echo 'export JAVA_HOME="/Applications/Android Studio.app/Contents/jre/jdk/Contents/Home"' >> ~/.zprofile $ source ~/.zprofile # 確認 $ ~/Library/Android/sdk/tools/bin/avdmanager list Available Android Virtual Devices: Parsing ~ : :Android Studioに同梱されているJAVAのパスは、アプリを立ち上げ
cmd + ;
→ 「JDK location」から確認が可能です。
- 投稿日:2020-09-12T10:19:15+09:00
【Android】XMLに1行追加でImageButtonにエフェクトを付加する
ImageButtonにおいて
android:background="@null"
としてしまうと,タップ時のエフェクトが表示れません。
「imagebutton エフェクト」で検索すると割と手間がかかる方法がヒットするのですが,
別にエフェクトのデザインに拘らない場合,レイアウトファイルに1行追加するだけで簡単にimagebuttonにエフェクトを追加することができます。
ImageButtonの養素に以下を追加するだけですandroid:background="?android:attr/selectableItemBackgroundBorderless"もし,背景を既に設定して変えたくない場合,foregroundも指定できます。
以下,サンプルです
<ImageButton android:id="@+id/menuSetting" android:layout_width="wrap_content" android:layout_height="match_parent" android:background="?android:attr/selectableItemBackgroundBorderless" android:src="@drawable/ic_baseline_settings_24"/>
- 投稿日:2020-09-12T07:16:48+09:00
AWS AmplifyでS3のBucket名を取得する [メモ]
はじめに
基本は自分のための備忘録だが、誰かの参考になれば幸い。
なお、クイックハックです。正式なやり方ではないと思うのでご注意を。動機
AWSのAmplifyを使ってモバイルアプリを作ることになったが、使いやすい半面、小回りがきかなそうで困ってる。
すごいざっくりのAmplify概要
Amplifyは、CLIから簡単にバックエンドを構築できて、そのバックエンドへのアクセス方法をライブラリが隠蔽するので、開発者はその詳細を知らなくてもアクセス出来るというところがメリット。
また、Dev -> Prodの2Stageで開発するときに、アプリで使うバックエンドを下記のコマンドでDev用、Prod用に切り替えることが出来る。当然、開発者はバックエンドの詳細を知らなくてもよい。
$ amplify env checkout <stage>困ってること
詳細が隠蔽されているので、バックエンドのリソース名が取れない(ようにみえる)。
例えば、バケット名を表示したいと思っても、のバケット名を取得する方法がない(ようにみえる)。
具体的に困っているのは、Amplifyを使ってS3にアップロードしたファイルを、Amplifyで作成したAPI-Gateway -> Lambdaからアクセスしたい。だが、Lambdaが直接Amplifyが作成したS3のBucketの名前を取得する方法がない(こちらも、ようにみえる)。なので、アプリからAPI-Gatewayを叩くときに、バケット名を渡してやる必要がある。必要があるのだが、上記のようにバケット名を取る方法がない。こまるー。
→Lambdaで使う場合、バケットにアクセスすることを明示しておけば、環境変数に入れてくれるようでした。。Amplify ディスってごめんよ。一応、以下、何かのときのために残しておく。方式案
方式案1:設定ファイルから読み込む。
上述した
amplify env checkout <stage>
でバックエンドを切り替えると、/res/raw/amplifyconfiguration.json
の内容が書き換わる。これにより、開発者は詳細を知らずにバックエンドを切り替えてアプリを動かすことが出来る。なので、こいつをパースしてやれば良い。
方式案2:android-amplifyをカスタマイズしちゃう。
https://github.com/aws-amplify/amplify-android からcloneして、ソースコード書き換えちゃおうぜ。という方法。
方式案1 vs 方式案2
どちらも、正式な方法じゃないと思われる。思われるが、おそらく方式案1のほうがよろしかろうと思われる。
でも、どうせ正しくないのだから、敢えて方式案2でやってみる。(^_^:)/
ちなみに、正しい方法は、featureリクエスト/pullリクエストを出して実装してもらうことだ。
手順
githubからクローン
$ git clone https://github.com/aws-amplify/amplify-android.git -b release_v1.3.1 $ git checkout -b release_v1.3.1arrを作成
プロジェクトを開く
プロジェクトをオープンする。モジュールがいろいろ入っているが、トップのプロジェクトをオープンしましょう。
必要なソースコード編集を行う。
今回は、
aws-storage-s3
のcom.amplifyframework.storage.s3.AWSS3StoragePlugin.java
をいじります。(ここでは、すごく適当にやってますが、ご参考頂く方々はもう少しマトモに配慮いただけると信じてます。)インスタンス変数に下記を追加。
public String regionStr; public String bucket;
configure
メソッドのローカル変数をコメントアウトpublic void configure( JSONObject pluginConfiguration, @NonNull Context context ) throws StorageException { // String regionStr; // String bucket;ビルド
Android Studioの右にGradleのタスク一覧を出せるウィンドウがあるので1、buildを選んで実行しましょう。
なんか、エラーがちらほら出ますが、s3のモジュールには出てないようなので、今回は無視します。
build/outputs/arr/aws-storage-s3-release.aar
が作成されていたらOKです。arrをインポート
ファイル名に先ほど作成したarrを指定します。
これにより、
Open Modules settings
から依存関係を設定できるようになります。なるはずです、、、なりませんね。おかしい。。。(バグかな? 使っているのはv4.0.1です。)ということで、自力で
build.gradle
を書き換えます。dependencies
に下記を追加します。implementation project(path: ':aws-storage-s3-release') implementation 'com.amazonaws:aws-android-sdk-s3:2.18.0' // カスタムのaws-storage-s3-releaseを使うときに必要。理由は不明。以上で、pluginからバケット名にアクセスすることが出来るようになりました。下のような文を追加して、正しく出力されるか確認してみましょう。
val plugin = Amplify.Storage.getPlugin("awsS3StoragePlugin") as AWSS3StoragePlugin Log.e(TAG,"Bucket ${plugin.bucketName} ${plugin.regionStr}")最後に
普通にjsonファイルをパースしたほう(方式案1)が早そうですね。ただ、これまでAmplifyを使った結果、今後もいろいろ小回りがきかなくて困りそうだったので、カスタマイズすることを前提にarrのインポートをするという手段をとってみました。
どなたかのご参考になれば幸いです。(自己責任でお願いします。)
デフォルトでは右端の細長いところにあるのですが、最初これが見つからなくて困りました。AndroidStudioは機能が多すぎて大変です。。。 ↩