- 投稿日:2020-01-19T23:47:59+09:00
Shibuya.apk#38 参加メモ
はじめに
2020/1/17(金)の shibuya.apk #38 - report from KotlinConf 2019 に参加してきました。
slackにメモを残しても忘れてしまうので、どんな発表があったのか、
どんな資料が紹介されていたのかまとめました。
(自分用のメモです)Java Bytecode Vertical Tasting
punchdrunkerさん
途中参加だったので、話の概要を掴めず...
JavaのLTSがついていないのはすぐにサポートが切れる。今の本命バージョンは11
このあたりが要点?かも。
あと、innerクラスからフィールドにアクセスすんなコンパイラで余計なクラスが増えるかららしい。
KotlinConf Overviewと2021年のアプリ開発トレンド
mhidakaさん
・春にはKotlin1.4が出るが、Updateはちょっと大変らしい。 ・今後、宣言的UIは流行する ・lonaの紹介 ・lonaは大規模開発時にデザインシステムをつくるためのOSS、概念的なもの ・成長するアプリが抱える課題、複雑なコードに対して今まで以上に開発スピードを上げる ・サーバ連携してレイアウト組み立てられると良いhttps://github.com/airbnb/Lona
Repository with Store4
wasabeef_jpさん
https://speakerdeck.com/wasabeef/repository-with-store4-ja
・store4の紹介 ・なんで4かというと、製作者が転職ごとにリポジトリを移動しているかららしい ・RxよりFlowの方がDisposableの扱いがない分、フールプルーフにならないhttps://github.com/dropbox/Store
kotlinx.serialization について
red_fat_darumaさん
https://speakerdeck.com/jmatsu/serialization-in-kotlin-world
集中切れ...
LT
How to use multi camera in Android Kurunさん
Capture view and register to gallery うめちゃんさん
https://docs.google.com/presentation/d/1EsB-7X2zFtkIwyQXzlI1FWdiRdof1QMeUogsvEAUOhw/edit#slide=id.p1
Asynchronous Data Streams with Kotlin Flow を要約したかった sobayaさん
https://speakerdeck.com/kurunpan/how-to-use-multi-camera-api-in-android
WorkManagerを使って指定した時間に音量をゼロにするアプリを作った話 kikuchiさん
まとめ
話の内容が刺激的で良い刺激を受けました。
全然別の話ですが、Qiitaにspeakerdeck埋め込めるといいんだけどなぁ...
- 投稿日:2020-01-19T23:32:28+09:00
Delphi タッチアニメーションを無効化する
はじめに
Delphiでモバイル開発をしていると、コントロールをタッチした際に波紋のようなアニメーションが発生することに気がつきます。
ユーザーが操作可能なオブジェクトであることが分かりやすい一方、微妙にダサいというか・・頼んでないというか・・そういう気分になることもありますね!
(意外と撮るのに苦労するスクリーンショット)しかしながら、無効化の方法については、ドキュメントを見つけることができませんでした。
なので、この記事は「方法が正しいかは解らないけどこれで無効化できたよ!」という内容になります。Delphi 10.3 CommunityEditionで確認しています。
※iOSについては手元に環境が無いため、確認できていません。やり方
TButtonを例に説明します。
- マルチデバイスアプリケーションのプロジェクトを作成し、スタイルを「Android」に設定します。
- FormにTButtonを配置し、右クリックし「カスタムスタイルの編集」を選択します。
- スタイルデザイナが開いたら(
Button1Style1
というスタイルが表示されています)、「名前を付けて保存」で適当なファイル(Button.style)に保存します。- テキストエディタで Button.style を開きます。おおよそ次のような中身になっていると思います。
object TStyleContainer object TLayout StyleName = 'Button1Style1' Align = Center Size.Width = 297.000000000000000000 Size.Height = 233.000000000000000000 Size.PlatformDefault = False TabOrder = 0 object TTintedButtonStyleObject StyleName = 'background' Align = Contents SourceLookup = 'AndroidL Lightstyle.png' Size.Width = 297.000000000000000000 Size.Height = 233.000000000000000000 Size.PlatformDefault = False (中略) TouchAnimation.Link = < item SourceRect.Left = 336.000000000000000000 SourceRect.Top = 233.000000000000000000 SourceRect.Right = 396.000000000000000000 SourceRect.Bottom = 293.000000000000000000 end (略) item Scale = 3.000000000000000000 SourceRect.Left = 1009.000000000000000000 SourceRect.Top = 704.000000000000000000 SourceRect.Right = 1189.000000000000000000 SourceRect.Bottom = 884.000000000000000000 end> TouchAnimation.Padding.Left = 1.000000000000000000 TouchAnimation.Padding.Top = 1.000000000000000000 TouchAnimation.Padding.Right = 1.000000000000000000 TouchAnimation.Padding.Bottom = 2.000000000000000000 HotTint.Shadow = <> HotTint.Mask = < (後略)この
TouchAnimetion.Link
要素が、波紋アニメーションを発生させる際の当たり判定と考えて良さそうです。
よって、TouchAnimation.Link
の中身をバッサリ削除します。TouchAnimation.Padding.*
も削除します。TouchAnimation.Link = <> //中身を削除 HotTint.Shadow = <> //その他は前のまま
スタイルファイルを保存します。(変更前にスタイルファイルのバックアップを取った方が安全)
スタイルデザイナに戻り、
Button1Style1
を削除します。そして、「開く」アイコンをクリックし、先ほど修正したスタイル ファイルを読み込みます。スタイルを適用し、デバッグ実行で動作を確認します。
原理など
タッチアニメーションの発生・描画は、
FMX.Style.Object.pas
ファイル内の、TTouchAnimationAdapter
クラスが担っており、このクラスをTActiveStyleObject
などの、スタイル定義クラスが所持しています。先ほどの
Button1style1
の場合は、backgroud
というTTintedButtonStyleObject
クラスのオブジェクトが、TTouchAnimationAdapter
を持っています。(スタイルデザイナのオブジェクトインスペクタ上でも確認できます。デザイナ上で設定を変更できればよかったのですが、出来なかった為、ファイルを直接編集して対処しました。)スタイルの定義に
TActiveStyleObject
等を用いなければ、タッチアニメーションも発生しない、ということでもあります。逆に、スタイルを触ってたらタッチアニメーションが発生しなくなってしまった!という場合は、
TouchAnimation.Link
の情報が消えてしまっていることが考えられるので、スタイルファイルを確認してみてください。おわりに
正直「こんな手間なことってある?」と半信半疑なのですが・・。なにかもっと良い方法というか、正しいやり方がありましたら、情報頂ければと思います!
- 投稿日:2020-01-19T21:53:22+09:00
React NativeのAndroidビルドでapp:mergeReleaseResources FAILED(Error: Duplicate resources)
下記の記事の補足
https://qiita.com/sekitaka_1214/items/2c9c3a698727d8bde659
やること
android/app/src/main/res/drawable-*
のフォルダを削除raw
フォルダ内のキャッシュみたいなファイルを削除cd android && ./gradlew assembleRelease
を実行2に関しては、必要なファイルが含まれている場合もあるので、一旦全部削除して3を行い、not foundになったファイルだけをgitで元に戻す必要があるようです。
- 投稿日:2020-01-19T21:47:57+09:00
MaterialButtonToggleGroupを使ってみる
まえおき
とあるアプリ開発で「定休日」の入力フォームを作っていたときのこと。
こういうフォームを作る必要があった。一昔前だったら「そんなiOSっぽいフォームはAndroidの世界にはありませんよー?」だったんだけど、
今はもうMaterial Designのガイドにも存在している。
調べてみると、Android用のコンポーネントもあるようだ。
https://material.io/develop/android/components/material-button-toggle-group/そんなわけで、昔からあるCheckBox、ToggleButtonではなく、新しそうなMaterialButtonToggleGroupを使ってみた。
画面横幅にあわせてボタンを配置する
何も考えずにリファレンスのとおりにボタンを配置してみる
MaterialButtonToggleGroupに愚直にボタンを置くだけだと・・・
res/layout/activity.xml<com.google.android.material.button.MaterialButtonToggleGroup android:layout_width="match_parent" android:layout_height="wrap_content"> <com.google.android.material.button.MaterialButton style="?attr/materialButtonOutlinedStyle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="日" /> <com.google.android.material.button.MaterialButton style="?attr/materialButtonOutlinedStyle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="月" /> <com.google.android.material.button.MaterialButton style="?attr/materialButtonOutlinedStyle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="火" /> (以下略)こんな感じで、画面をはみ出してしまう。
これは、MaterialButtonはButton継承の部品なので、minWidth, minHeightが設定されているためだ。じゃあ
minWidth=0
を指定すると・・・?res/layout/activity.xml<com.google.android.material.button.MaterialButtonToggleGroup android:layout_width="match_parent" android:layout_height="wrap_content"> <com.google.android.material.button.MaterialButton style="?attr/materialButtonOutlinedStyle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:minWidth="0dp" android:text="日" /> <com.google.android.material.button.MaterialButton style="?attr/materialButtonOutlinedStyle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:minWidth="0dp" android:text="月" /> <com.google.android.material.button.MaterialButton style="?attr/materialButtonOutlinedStyle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:minWidth="0dp" android:text="火" />wrap_parentの挙動そのものになる。でも画面幅ぴったりにはならない。
layout_weightを指定すれば良い!
画面幅にあわせるといえば、
layout_weight
だ。
ただ、このプロパティはLinearLayoutが親じゃないと使えない。そこでもう一度リファレンスを見てみよう。
なんとMaterialButtonToggleGroupはLinearLayout継承のコンポーネントではないか!
res/layout/activity.xml<com.google.android.material.button.MaterialButtonToggleGroup android:layout_width="match_parent" android:layout_height="wrap_content"> <com.google.android.material.button.MaterialButton style="?attr/materialButtonOutlinedStyle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" android:minWidth="0dp" android:text="日" /> <com.google.android.material.button.MaterialButton style="?attr/materialButtonOutlinedStyle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" android:minWidth="0dp" android:text="月" /> <com.google.android.material.button.MaterialButton style="?attr/materialButtonOutlinedStyle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" android:minWidth="0dp" android:text="火" />
layout_weight=1
を各ボタンに付けることで、期待通りになった!ViewModelを使って2-wayデータバインディングする
画面回転するとフォームの入力値がリセットされるのでは困る。
よほど凝ったフォームじゃない限りは、フォームの入力値はViewModelに持たせるのが定石である。MainActivityViewModel.ktclass MainActivityViewModel : ViewModel() { val title = MutableLiveData<String>() val closedOnSun = MutableLiveData<Boolean>() val closedOnMon = MutableLiveData<Boolean>() val closedOnTue = MutableLiveData<Boolean>() val closedOnWed = MutableLiveData<Boolean>() val closedOnThu = MutableLiveData<Boolean>() val closedOnFri = MutableLiveData<Boolean>() val closedOnSat = MutableLiveData<Boolean>() }超適当だけど、とりあえずこんな感じで各ボタンのチェック状態を覚えておくLiveDataをもったViewModelを作り、
res/layout/activity.xml<layout> <data> <variable name="viewModel" type="io.github.yusukeiwaki.materialbuttontogglegroupplayground.MainActivityViewModel" /> </data> (中略) <com.google.android.material.button.MaterialButtonToggleGroup android:layout_width="match_parent" android:layout_height="wrap_content"> <com.google.android.material.button.MaterialButton style="?attr/materialButtonOutlinedStyle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" android:checked="@={viewModel.closedOnSun}" android:minWidth="0dp" android:text="日" /> <com.google.android.material.button.MaterialButton style="?attr/materialButtonOutlinedStyle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" android:checked="@={viewModel.closedOnMon}" android:minWidth="0dp" android:text="月" /> <com.google.android.material.button.MaterialButton style="?attr/materialButtonOutlinedStyle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" android:checked="@={viewModel.closedOnTue}" android:minWidth="0dp" android:text="火" />こんな感じで、
android:checked
にそれを指定する。CheckBoxやToggleButtonだとこれでうまくいくはずだ。
しかしながらMaterialButtonToggleGroupを使うとこれではうまくいかない。レイアウトファイルのエラーを見てみると、
Cannot find a getter for <com.google.android.material.button.MaterialButton android:checked> that accepts parameter type 'java.lang.Boolean' If a binding adapter provides the getter, check that the adapter is annotated correctly and that the parameter type matches.ようするにbinding adapterが無いよって言われている。
MaterialButtonには
setChecked(Bool)
/isChecked: Bool
の setter/getterは定義されてるんだけども、CompoundButton(CheckBoxやToggleButtonの基底クラス)を継承はしていない。
きっとcheckが変化したリスナーを自動では見つけられないのだろうと推測。とりあえずbinding adapterを作る
CompoundButtonのbinding adapterをコピペすれば動くだろう、ということで
https://android.googlesource.com/platform/frameworks/data-binding/+/master/extensions/baseAdapters/src/main/java/android/databinding/adapters
ソースを読む。CompoundButtonBindingAdapter.java@BindingMethods({ @BindingMethod(type = CompoundButton.class, attribute = "android:buttonTint", method = "setButtonTintList"), @BindingMethod(type = CompoundButton.class, attribute = "android:onCheckedChanged", method = "setOnCheckedChangeListener"), }) @InverseBindingMethods({ @InverseBindingMethod(type = CompoundButton.class, attribute = "android:checked"), }) public class CompoundButtonBindingAdapter { @BindingAdapter("android:checked") public static void setChecked(CompoundButton view, boolean checked) { if (view.isChecked() != checked) { view.setChecked(checked); } } @BindingAdapter(value = {"android:onCheckedChanged", "android:checkedAttrChanged"}, requireAll = false) public static void setListeners(CompoundButton view, final OnCheckedChangeListener listener, final InverseBindingListener attrChange) { if (attrChange == null) { view.setOnCheckedChangeListener(listener); } else { view.setOnCheckedChangeListener(new OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { if (listener != null) { listener.onCheckedChanged(buttonView, isChecked); } attrChange.onChange(); } }); } } }なるほど。onCheckedChangedは今回使わないので、とりあえず適当にコピペすればいけるだろう。
MaterialButtonBindingAdapter.java@InverseBindingMethods({ @InverseBindingMethod(type = MaterialButton.class, attribute = "android:checked"), }) public class MaterialButtonBindingAdapter { @BindingAdapter("android:checked") public static void setChecked(MaterialButton view, boolean checked) { if (view.isChecked() != checked) { view.setChecked(checked); } } @BindingAdapter(value = {"android:checkedAttrChanged"}, requireAll = false) public static void setListeners(MaterialButton view, final InverseBindingListener attrChange) { if (attrChange != null) { // TODO: // 丁寧に実装するには、TextViewBindingAdapterのようにListenerUtilというクラスを使って // 前回仕掛けたリスナーを明示的に解除する必要がある。 // 参考: https://android.googlesource.com/platform/frameworks/data-binding/+/master/extensions/baseAdapters/src/main/java/android/databinding/adapters/TextViewBindingAdapter.java view.clearOnCheckedChangeListeners(); view.addOnCheckedChangeListener(new MaterialButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(MaterialButton buttonView, boolean isChecked) { attrChange.onChange(); } }); } } }こんな感じでbinding adapterを作る。
これで、ビルドが通る。
めでたしめでたし。
まとめ
MaterialButtonToggleGroup を使うと、ちょっと今風なトグルが作れる。ただし
- 場合によってはMaterialButtonに minWidth=0, layout_weight=1 指定が必要かもしれない
- 2-wayデータバインディングを使うには、MaterialButtonのバインディングアダプタを自前で実装する必要がある
というハマりどころがあった。
お試しソースコードはまとめてここにおいてあります→ https://github.com/YusukeIwaki/MaterialButtonToggleGroupPlayground
- 投稿日:2020-01-19T21:41:03+09:00
[Android]Room を使ったサンプルと解説
はじめに
とりあえずこれに従えばできるみたいなサンプルを作ろうと思います。
サンプルは次の手順で作成していきたいと思います。
- Room をセットアップする
- Entity(User) を定義する
- Dao(UserDao) を定義する
- RoomDatabase(AppDatabase) クラスを定義する
役割 クラス名 役割 Entity User データベースのテーブルを表すクラス Data Access Objects UserDao データベースにアクセスするメソッドを定義するクラス RoomDatabase AppDatabase Daoを生成するためのRoomDatabaseを継承した抽象クラス 0. Roomをセットアップする
次の依存関係を記述し Room をセットアップする。
KTX や Coroutines を利用する場合にはroom-ktx
を入れます。
Kotlin を利用するならばannotationProcessor
ではなくkapt
を利用するようにします。build.gradle(app)dependencies { def room_version = "2.2.2" implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-ktx:$room_version" kapt "androidx.room:room-compiler:$room_version" }1. Entity ( User ) を定義する
データベースにテーブルを表す Entity を定義します。
Entity は@Entity
をクラスに付けるだけで定義できます。
デフォルトだとテーブル名称としてクラス名称が登録されるようになっています。
必要であればtableName
にてテーブル名称を変更してください。@Entity(tableName = "users") data class User(val id: Int, val firstName: String?, val lastName: String?, val age: Int)Entity には必ず 1つの主キーを指定する必要があります。
主キーは@PrimaryKey
アノテーションをクラスのプロパティにつけるだけで指定します。
また@PrimaryKey
ではautoGenerate
にて自動生成するか設定できるようになっています。
なので必要に応じてautoGenerate = true
を付けて、主キーを自動生成するようにして下さい。@Entity(tableName = "users") data class User( @PrimaryKey(autoGenerate = true) val id: Int, val firstName: String?, val lastName: String?, val age: Int )デフォルトだとテーブルに登録されるカラム名称はプロパティ名称が登録されるようになっています。
登録されるカラム名称は@ColumnInfo
で変更できるようになっています。
なので必要に応じて@ColumnInfo(name ="xxxxx")
を付けて、カラム名称を変更するようにしてください。@Entity(tableName = "users") data class User( @PrimaryKey(autoGenerate = true) val id: Int, @ColumnInfo(name = "first_name") val firstName: String?, @ColumnInfo(name = "last_name") val lastName: String?, val age: String? )デフォルトだと Entity に定義される全てのプロパティをテーブルのカラムとして登録します。
あるプロパティをテーブルに含めたくない場合、@Ignore
を付けることでテーブルに含めないようにできます。@Entity(tableName = "users") data class User( @PrimaryKey(autoGenerate = true) val id: Int, @ColumnInfo(name = "first_name") val firstName: String?, @ColumnInfo(name = "last_name") val lastName: String?, @Ignore val age: String? ) { // @Ignore を付ける場合は、テーブルに含めるプロパティで初期化するコンストラクタを作成する必要があります。 constructor(id: Int, firstName: String?, lastName: String) : this(id, firstName, lastName, null) }2. Dao ( UserDao ) を定義する
データベースにアクセスするクラスである Dao を定義していきます。
Dao は@Dao
をインタフェースに付けるだけで定義できます。
このインタフェースに挿入・削除・更新・取得を行うメソッドを実装していきます。@Dao interface UserDao {}挿入は
@Insert
、削除は@Delete
、更新は@Update
を付けて定義します。
このようにアノテーションを定義すれば、あとは Room が勝手に処理を実装してくれます。@Dao interface UserDao { @Insert fun insert(user : User) @Update fun update(user : User) @Delete fun delete(user : User) }
@Query
を付けば自身で記述したクエリを実行できるようにします。
@Query
には引数に指定したパラメータをクエリに含めることもできます。@Dao interface UserDao { @Insert fun insert(user : User) @Update fun update(user : User) @Delete fun delete(user : User) @Query("delete from users") fun deleteAll() @Query("select * from users") fun getAll(): List<User> @Query("select * from users where id = :id") fun getUser(id: Int): User }3. RoomDatabase ( AppDatabase ) を実装する
RoomDatabase を拡張した抽象クラスである AppDatabase を生成します。
abstract class AppDatabase : RoomDatabase() {}
@Database
を付け、entities
に 1. で定義した Entity を指定します。@Database(entities = arrayOf(User::class), version = 1) abstract class AppDatabase : RoomDatabase() {}そして 2.で定義した Dao を取得するためのメソッドを用意すれば実装完了です。
@Database(entities = arrayOf(User::class), version = 1) abstract class AppDatabase : RoomDatabase() { abstract fun userDao(): UserDao }おわりに
ここまで定義したクラスを利用して処理を記述する次の感じになります。
Room は Main スレッドで実行すると怒られるので、
Coroutine などを利用して Main スレッドで実行しないようにしましょう。val database = Room.databaseBuilder(applicationContext, AppDatabase::class.java, "database-name") .fallbackToDestructiveMigration() .build() val userDao = database.userDao() val newUser = User(0, Date().time.toString(), Date().time.toString(), null) userDao.insert(newUser) Log.v("TAG", "after insert ${userDao.getAll().toString()}")V/TAG: after insert [User(id=138, firstName=1579438392369, lastName=1579438392369, age=null)] V/TAG: after deleteAll []参考資料
- 投稿日:2020-01-19T21:00:05+09:00
Androidアプリにプライバシーポリシーを置くときの構成
ここ最近は、Androidアプリにプライバシーポリシーを盛り込むことが当たり前になってきています。
以前にも、Googleのポリシー違反の取り締まりが厳しくなり、GooglePlayから大量のAndroidアプリがリジェクトされる事態もありました。その時は、ブログに適当にプライバシーポリシーのページを作って、リンクを貼ることで難を逃れました。(いいのか、それで・・・)
しばらく放置していましたが、そろそろ改善しようといろいろ考えて、自分なりに考えてみましたので、まとめます。
これまでの構成
- ブラウザを起動して、プライバシーポリシーのページを表示
改善したいこと
- 通信が出来ない時は表示できない
- ブログにページを作ったのでイケてない
改善した構成
- FirebaseStorageにあるファイルの更新日時を確認
- ローカルの方が古ければ、FirebaseStorageからダウンロードして最新化
- 表示は、アプリ内のWebViewで行う
- 初回起動時にネットに繋げなくても、アプリ内のAssertsに置いてあるページを表示
なぜHostingじゃなくてStorageか
- Storageのほうが安い
- ネットが繋がってなくても表示したい
まず、価格はStorageの方が安いです。
HostingとStorageの価格は以下の通りです。無料の使用量
Hosting Storage 容量 1GB 5GB 転送 10GB/月 1GB/日 あとはHostingにすると、結局これまでの構成と何ら変わりがないですね。
プライバシーポリシーの更新について
- Storageに置いてあるファイルを最新にする
- アプリ内のAssetsに置いてあるファイルを最新にして、新しいアプリをアップロードする
プライバシーポリシーの更新に関しては、Hostingの方が便利です。
しかし、ネットがない状態で確認できないのは少しネックですし、これが良いとは思いません。やはり、内蔵するのが一番良いです。
とはいえ、アプリ自体を更新しないと最新化されないのもネックです。
そこで、最新のプライバシーポリシーをStorageから取得し、なければローカルで表示という方法を取りました。プライバシーポリシーを作る
これに関してはあまりノウハウはないのですが、次の本の雛形を参考に作りました。サービスの拡大をしながら、充実していければ良いかなと思います。
良いウェブサービスを支える「利用規約」の作り方
https://www.amazon.co.jp/dp/B07Q721691/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1まとめ
いろんなアプリを見てますが、プライバシーポリシーの表示は、通信してるのが多いですね。今回Firebaseを使いましたが、とっても便利で簡単です。どんどんアプリ開発に活用していきたいですね。
- 投稿日:2020-01-19T18:10:35+09:00
React Nativeでコードの変更が反映されない時(Android)
コードを変えても反映されない問題(RN)
React Nativeでしばらく触っていなかったプロジェクトやアップグレードを行なった際に、コードを変えても変更がデバイス上で反映されないことがある。
以下のコマンドで解決した。
react-native bundle --platform android --dev false --entry-file index.js --bundle-output android/app/src/main/assets/index.android.bundle --assets-dest android/app/src/main/res/ # ↑を叩いたらいつも通り起動 react-native run-android # or npx react-native run-android参考:
https://stackoverflow.com/questions/48304116/react-native-run-android-do-not-updating-modified-code
- 投稿日:2020-01-19T16:35:18+09:00
クリーンアーキテクチャーをざっと理解できる短いコードを書いた
クリーンアーキテクチャーは沢山インターフェースやクラスが登場し、その関係をざっと把握するのが難しいです。
そこですべての要素が入ったなるべく短いコードを書いてみました。XCodeのPlaygroundなので、そのまま実行できます。githubのリポジトリはこちらです。なお、今回はざっと理解することを優先するため、クリーンアーキテクチャーについての詳しい説明は割愛します。参考記事をご覧ください。このコードは、Uncle Bobの書いたClean Architecture 達人に学ぶソフトウェアの構造と設計と、次の記事を参考に書いています。
参考記事:
実装クリーンアーキテクチャ: https://qiita.com/nrslib/items/a5f902c4defc83bd46b8
Laravelで実践クリーンアーキテクチャ: https://qiita.com/nrslib/items/aa49d10dd2bcb3110f22クリーンアーキテクチャーの概要図
引用:Clean Architecture 達人に学ぶソフトウェアの構造と設計概要図と今回のコードとの対応
サンプルコードに現れるインターフェース、クラスをなるべく本家の図と同じ位置に同じ色で配置しています。
この図で、本家と違うのが
SomeView
がUserController
にも依存しているところです。理由は、本家はMVC1を前提としていますが、Webフレームワークなどで使われているMVC2に寄せたほうが理解がしやすいと判断したためです。このあたりは『クリーンアーキテクチャの Presenter が分かりにくいのは MVC 2 じゃないから』という記事に解説があります。コード
実際のコードです。簡単な説明がコメント行に入っています。
クリーンアーキテクチャーのコード// 【Frameworks & Drivers】アプリケーションフレームワークやドライバなど実装の詳細にあたる部分 // DB ORMやDAOなどDBとのやり取りをするAPI。このサンプルでは何もしない class SomeDB { static func executeQuery(sql: String, bindParam: [String]) { // Dummy ここで実際にユーザーを登録する } } // UI UIKitやRailsの表示部分など画面表示をするためのAPI class SomeView { var userController: UserController var viewModel: UserCreateViewModel init(userController: UserController, viewModel: UserCreateViewModel) { self.userController = userController self.viewModel = viewModel self.viewModel.bind { userName in print("登録:" + userName + "さん") } } func start(){ userController.createUser(userName: "test user") } } // 【Interface Adapters】Application Business RulesとFrameworks & Driversの型の相互変換 // Controllers 入力をUserCaseのために変換する(入力のための変換) class UserController { var userCreateUseCase: UserCreateUseCaseInputPort init(userCreateUseCase: UserCreateUseCaseInputPort) { self.userCreateUseCase = userCreateUseCase } func createUser(userName: String) { let input = UserCreateInputData(userName: userName) userCreateUseCase.handle(input: input) } } // GateWays Frameworks & Driversからのデータを抽象化する class UserDataAccess: UserDataAccessInterface { func save(user: UserEntity) { SomeDB.executeQuery( sql: "REPLACE INTO USER (USER_NAME) VALUES (?) ", bindParam: [user.userName] ) } } // Presenters データをViewに適した加工する(出力のための変換) class UserCreatePresenter: UserCreateUseCaseOutputPort { var viewModel: UserCreateViewModel init(viewModel: UserCreateViewModel) { self.viewModel = viewModel } func complete(output: UserCreateOutputData) { let userName = output.userName self.viewModel.update(userName: userName) } } class UserCreateViewModel { typealias CallBackType = (String)->Void var userName: String var callBack: CallBackType? init(userName: String) { self.userName = userName } func bind(callBack: @escaping CallBackType) { self.callBack = callBack } func update(userName: String) { self.userName = userName self.callBack?(userName) } } // 【Application Business Rules】 アプリケーションのビジネスルール // UseCaseと上位層との遣り取りをするためのオブジェクト protocol UserDataAccessInterface { func save(user: UserEntity) } protocol UserCreateUseCaseOutputPort { // Output Boundaryともいう func complete(output: UserCreateOutputData) } struct UserCreateInputData { var userName: String } struct UserCreateOutputData { var userName: String } // Use Cases ユースケースを表す protocol UserCreateUseCaseInputPort { // Input Boundaryともいう func handle(input: UserCreateInputData) } class UserCreateInteractor: UserCreateUseCaseInputPort { var userDataAccess: UserDataAccess var presenter: UserCreateUseCaseOutputPort init(userDataAccess: UserDataAccess, presenter: UserCreateUseCaseOutputPort) { self.userDataAccess = userDataAccess self.presenter = presenter } func handle(input: UserCreateInputData) { let userName = input.userName let user = UserEntity(userName: userName) userDataAccess.save(user: user) let output = UserCreateOutputData(userName: user.userName) presenter.complete(output: output) } } // 【Enterprise Business Rules】 ドメイン層 // Entities ビジネスルールをカプセル化したもの struct UserEntity { var userName: String } // Entry Point このサンプルの実行開始ポイント let viewModel = UserCreateViewModel(userName: "") let userDataAccess = UserDataAccess() let presenter = UserCreatePresenter(viewModel: viewModel) let useCase = UserCreateInteractor(userDataAccess: userDataAccess, presenter: presenter) let userController = UserController(userCreateUseCase: useCase) var ui = SomeView(userController: userController, viewModel: viewModel) ui.start()まとめ
短い、といいながら140行ほどになってしまいました。また、なるべく簡単にするために
userName
だけを持つclass
ばかりとなってしまい、それぞれのclass
の必要性がつかみにくくなってしまったのが残念です。コードと図を書いていて気がついたのは、
UseCase
が依存するインターフェースはすべてUseCase
と同じレイヤーに配置されているということです。このあたりに依存性逆転の原則がしっかり守られていると感じました。
- 投稿日:2020-01-19T16:18:46+09:00
[kotlin]RecyclerViewの中身をGridLayout + CardViewにして表示する
今回やること
RecyclerViewでgridLayout、tableLayoutっぽく表示する。あとcardViewを使ってUIを手っ取り早く、それっぽくする
これ↓
RecyclerViewの配置
レイアウトファイルにRecyclerViewを配置する
activity_main.xml<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/gridRecyclerView" android:layout_width="0dp" android:layout_height="0dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>RecyclerViewの中身
CardViewの中に画像とテキストを入れたレイアウトを作成する
CardViewはelevation
とRadius
を5dpくらいつけとけば手軽にそれっぽくなる
grid_view.xml<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/constrainLayout" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="8dp" android:orientation="vertical"> <androidx.cardview.widget.CardView android:layout_width="match_parent" android:layout_height="wrap_content" app:cardCornerRadius="4dp" app:cardElevation="5dp" app:cardUseCompatPadding="true" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:id="@+id/imageView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:scaleType="fitCenter" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:srcCompat="@mipmap/ic_launcher_round" /> <TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="TextView" android:textSize="12sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="@+id/imageView" app:layout_constraintStart_toStartOf="@+id/imageView" app:layout_constraintTop_toBottomOf="@+id/imageView" /> </androidx.constraintlayout.widget.ConstraintLayout> </androidx.cardview.widget.CardView> </androidx.constraintlayout.widget.ConstraintLayout>アダプターをつくる
RecyclerView用のアダプターをRecyclerView.Adapterを継承して作成する
CustomAdapter.kt//// customListはrecyclerViewのコンテンツとしてに表示するString配列のデータ class CustomAdapter(private val customList: Array<String>) : RecyclerView.Adapter<CustomAdapter.CustomViewHolder>() { // ViewHolderクラスを作成 class CustomViewHolder(val view: View) : RecyclerView.ViewHolder(view) { val img = view.imageView val txt = view.textView } // getItemCount onCreateViewHolder onBindViewHolderを実装 // recyclerViewのコンテンツのサイズ override fun getItemCount(): Int { return customList.size } // 上記のViewHolderクラスを使ってViewHolderを作成 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomViewHolder { val layoutInflater = LayoutInflater.from(parent.context) val item = layoutInflater.inflate(R.layout.grid_view, parent, false) return CustomViewHolder(item) } // ViewHolderに表示するデータを挿入 override fun onBindViewHolder(holder: CustomViewHolder, position: Int) { holder.view.textView.text = customList[position] holder.view.imageView.setImageResource(R.mipmap.ic_launcher_round) } }アダプターをセットする
表示するデータ配列を作ってアダプターをrecyclerViewにセット
MainActivity.ktclass MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) /// 表示するテキスト配列を作る [テキスト1, テキスト2, ....] val list = Array<String>(23) {"テキスト$it"} val adapter = CustomAdapter(list) val layoutManager = GridLayoutManager(this, 4, GridLayoutManager.VERTICAL, false) // アダプターとレイアウトマネージャーをセット gridRecyclerView.layoutManager = layoutManager gridRecyclerView.setHasFixedSize(true) gridRecyclerView.adapter = adapter } }
layoutManager
に何を使うかでだいぶ変わってくる。
今回使ったのはGridLayoutManager
だがよく使うのはLinearLayoutManager
でlinearだとrecyclerViewの中身が上から1個ずつ積み重なるよく見かけるあのレイアウトになる。他にはStaggeredGridLayoutManager
とかある。GridLayoutManager
引数について
GridLayoutManager(コンテキスト, カラム数, GridLayoutのタイプ, 下から要素を埋めていくか?)
「カラム数」は1行に何個表示させるか、GridLayoutのタイプは
VERTICAL
のほかにHORIZONTAL
とかがありHORIZONTAL
にすると横に要素が積み重なり、横にスクロールできるようになる。おわり
CardView使ったら簡単にUIが、それっぽくなるから便利。あとlayoutManagerを変えるだけでどんな風に要素を重ねていくか決めれるのは楽でいいなと改めて思った。
個人的余談だけど Androidでのバックエンド言語に何を使うのがメジャーなのかわからない... SQLiteとかGo MobileとかサーバーだけどAWSとかいろいろあるけど何を使うのが一般的なんだろ?
- 投稿日:2020-01-19T11:19:19+09:00
iOS, Androidアプリの価格変更(キャンペーン)の仕方
はじめに
以前アプリをリリース(iOS, Mac, Androidのアプリをリリースしてみた
)したのですが、複数の方から100円も払えるかクソが!!といわれましたまだ機能が思ってる完成形には達していないし、やっぱだれかがダウンロードしてくれないとおもしろくないししばらくは無料にすることにしました!!
たまにみるキャンペーンみたいなので期間限定無料をやりたいと思い調べていたのですがやり方があんま出てこなかったので備忘録として書きます。
iOS, Mac版
iOSとMacアプリについてです。やり方はめっちゃ簡単。(だからあんま調べてもでてこないのかも)
- 下記ページを開きます
App Store Connect -> マイ App -> 対象アプリ -> App Store -> 価格および配信状況- 価格表の「価格変更を計画」をクリック
- 価格、開始日、終了日を選択
- 「終了」クリック
- 「保存」クリック
終了日は元の価格に戻る日付のようです。(2月いっぱい無料にしたかったのに間違えた)
Android版
Androidアプリについてです。こっちもめっちゃ簡単。
- 下記ページを開きます
Google Play Console -> 対象アプリ -> ストアでの表示 -> 価格と配布 -> セール- セール名を設定
- セール期間を設定(最大8日間らしいです)
- 価格を設定(最低30%オフらしいです)
- 「販売スケジュールを設定」クリック
一旦セールを行うと次のセール作成は30日間おこなえないようです。
参考:Play Console ヘルプー有料アプリのセールを作成する
さいごに
セールの開始日を当日にすると反映までは多少時間がかかるみたいです。余裕をもってセールを設定しましょう!!(iOSは3時間くらいでAndroidは8時間くらいでした)
今無料なんでだれかダウンロードしてください
- 投稿日:2020-01-19T09:54:40+09:00
GatsbyJSとNetlifyでiOS/AndroidアプリのLPを勢いで作ってみた
経緯
2020年1月にiOSアプリをリリースして、もともとAndroidのアプリがあったので
これは今ならLP作れるのでは...
という浅い考えと、何か新しい方法で試してみたいという欲求から今回は
GatsbyJSとNetlifyを使ってLPを作る事にしました。2020年初のアプリリリースしました?
— slowhand (@wes_ja0927) <a href="https://twitter.com/wes_ja0927/status/1218172126093438976?refsrc=twsrc%5Etfw">January 17, 2020
神社巡ったり好きな方は良ければ使って見てください。iOSアプリです。m(_ )mhttps://t.co/qv2C9dDhu1GatsbyJSとNetlify
- GatsbyJS
- GatsbyJSに関してはこちら参照 GatsbyJSを試してみた
- Netlify
- 静的サイトをホスティングすることができるWebサービス
- 料金に関して
- 今回は無料の枠内で対応
環境構築
今回はこちらのテンプレートを使用してLPを作成したいと思います。
$ gatsby new app-lp https://github.com/anubhavsrivastava/gatsby-starter-newage
早速起動してみます。
$ cd app-lp $ gatsby developブラウザで
http://localhost:8000/
にアクセスして↓のページが表示されればOKです。
LPポイですねw (当たり前)ここまで出来たら、一度Github上にリポジトリを作成しPushします。
今回はPrivateなリポジトリでも可能なのか検証も含めPrivateリポジトリで試しました。
(確認後Publicにしてます)サイトページの内容を修正
基本的な情報
config.js
で設定されているので、ここを適宜修正します。module.exports = { siteTitle: '[アプリ名]', // <title> ☆修正 manifestName: 'app-lp', // ☆修正 manifestShortName: 'Landing', // max 12 characters manifestStartUrl: '/', manifestBackgroundColor: '#EB6101', // ☆修正 manifestThemeColor: '#EB6101', // ☆修正 manifestDisplay: 'standalone', manifestIcon: 'src/assets/img/website-icon.png', // ☆アイコン修正 pathPrefix: `/app-lp/`, // ☆修正 // social ☆必要に応じて修正 socialLinks: [ { icon: 'fa-github', name: 'Github', url: 'https://github.com/xxxx', }, { icon: 'fa-linkedin-in', name: 'Linkedin', url: 'https://linkedin.com/in/xxxx/', }, { icon: 'fa-twitter', name: 'Twitter', url: 'https://twitter.com/xxxx', }, { icon: 'fa-facebook-f', name: 'Facebook', url: 'https://facebook.com/xxxx', }, ], };Footerの修正
まずは簡単な所から、
src/components/Footer.js
のコピーライトの部分を修正しました。import React from 'react'; export default function Footer() { return ( <footer> <div className="container"> <p>© [名前]. All Rights Reserved.</p> </div> </footer> ); }コンテンツ全般の修正
次にコンテンツ全般の修正をガッツリ行いました。主に修正するファイルは
src/pages/index.js
になります。
また、適宜CSSも微調整しました。Netlifyでページ公開
ページを修正したら、早速デプロイして公開していきたいと思います。
Netlifyにログインすると以下のページが表示されます。
早速Githubのレポジトリと連携しようと思うので、「New from site Git」から連携するリポジトリを選択します。
設定はデフォルトのままで「Deploy site」で早速デプロイしてみます。
今回作成したLPは以下に公開してます !
https://ss-map-lp.netlify.com/
※ドメイン変更まで行うと有料になるので、今回はやりませんでした。リポジトリはこちら
https://github.com/Slowhand0309/ss-map-lp
(※Privateでも大丈夫そうだったので、今は公開してます)ものの数時間でデプロイできました 更新があっても後はリポジトリにPushするだけなのでとても便利そうです。
参考になったURL
- 投稿日:2020-01-19T01:07:53+09:00
【Kotlin備忘】他のActivityへBitmapの受け渡し
photoはbitmap
受け渡し側val intent = Intent(this, XXXXActivity::class.java) intent.putExtra("data",photo) startActivity(intent)受け取り側val intent = getIntent() val photo = intent.getParcelableExtra<Bitmap>("data")bitmapがデカすぎる場合、URIを利用した方がいいらしいけど
- 投稿日:2020-01-19T01:01:16+09:00
【Firebase】failed: DatabaseError: Permission denied
Firebase RealtimeDatabase使っている時、パーミッションがの設定値が初期値だとエラーが出る
failed: DatabaseError: Permission denied