20200119のAndroidに関する記事は13件です。

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さん

https://t.co/eVuB2yUeZh?amp=1

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埋め込めるといいんだけどなぁ...

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

Delphi タッチアニメーションを無効化する

はじめに

Delphiでモバイル開発をしていると、コントロールをタッチした際に波紋のようなアニメーションが発生することに気がつきます。
ユーザーが操作可能なオブジェクトであることが分かりやすい一方、微妙にダサいというか・・頼んでないというか・・そういう気分になることもありますね!
touchAnimation.png(意外と撮るのに苦労するスクリーンショット)

しかしながら、無効化の方法については、ドキュメントを見つけることができませんでした。
なので、この記事は「方法が正しいかは解らないけどこれで無効化できたよ!」という内容になります。

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の情報が消えてしまっていることが考えられるので、スタイルファイルを確認してみてください。

おわりに

正直「こんな手間なことってある?」と半信半疑なのですが・・。なにかもっと良い方法というか、正しいやり方がありましたら、情報頂ければと思います!

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

React NativeのAndroidビルドでapp:mergeReleaseResources FAILED(Error: Duplicate resources)

下記の記事の補足

https://qiita.com/sekitaka_1214/items/2c9c3a698727d8bde659

やること

  1. android/app/src/main/res/drawable-*のフォルダを削除
  2. rawフォルダ内のキャッシュみたいなファイルを削除
  3. cd android && ./gradlew assembleReleaseを実行

2に関しては、必要なファイルが含まれている場合もあるので、一旦全部削除して3を行い、not foundになったファイルだけをgitで元に戻す必要があるようです。

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

MaterialButtonToggleGroupを使ってみる

まえおき

とあるアプリ開発で「定休日」の入力フォームを作っていたときのこと。
Rectangle.png
こういうフォームを作る必要があった。

一昔前だったら「そんなiOSっぽいフォームはAndroidの世界にはありませんよー?」だったんだけど、
今はもうMaterial Designのガイドにも存在している。
貼り付けた画像_2020_01_19_17_29.png

調べてみると、Android用のコンポーネントもあるようだ。
https://material.io/develop/android/components/material-button-toggle-group/

そんなわけで、昔からあるCheckBox、ToggleButtonではなく、新しそうなMaterialButtonToggleGroupを使ってみた。
image.png

画面横幅にあわせてボタンを配置する

何も考えずにリファレンスのとおりにボタンを配置してみる

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="火" />

  (以下略)

image.png

こんな感じで、画面をはみ出してしまう。
これは、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="火" />

image.png

wrap_parentの挙動そのものになる。でも画面幅ぴったりにはならない。

layout_weightを指定すれば良い!

画面幅にあわせるといえば、layout_weightだ。
ただ、このプロパティはLinearLayoutが親じゃないと使えない。

そこでもう一度リファレンスを見てみよう。

貼り付けた画像_2020_01_19_21_10.png

なんと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="火" />

image.png

layout_weight=1を各ボタンに付けることで、期待通りになった!

ViewModelを使って2-wayデータバインディングする

画面回転するとフォームの入力値がリセットされるのでは困る。
よほど凝ったフォームじゃない限りは、フォームの入力値はViewModelに持たせるのが定石である。

MainActivityViewModel.kt
class 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を使うとこれではうまくいかない。

image.png

レイアウトファイルのエラーを見てみると、

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

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

[Android]Room を使ったサンプルと解説

はじめに

とりあえずこれに従えばできるみたいなサンプルを作ろうと思います。
サンプルは次の手順で作成していきたいと思います。

  1. Room をセットアップする
  2. Entity(User) を定義する
  3. Dao(UserDao) を定義する
  4. RoomDatabase(AppDatabase) クラスを定義する
役割 クラス名  役割
Entity User データベースのテーブルを表すクラス
Data Access Objects UserDao データベースにアクセスするメソッドを定義するクラス
RoomDatabase AppDatabase Daoを生成するためのRoomDatabaseを継承した抽象クラス

image.png

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 []

参考資料

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

Androidアプリにプライバシーポリシーを置くときの構成

ここ最近は、Androidアプリにプライバシーポリシーを盛り込むことが当たり前になってきています。
以前にも、Googleのポリシー違反の取り締まりが厳しくなり、GooglePlayから大量のAndroidアプリがリジェクトされる事態もありました。

その時は、ブログに適当にプライバシーポリシーのページを作って、リンクを貼ることで難を逃れました。(いいのか、それで・・・)

しばらく放置していましたが、そろそろ改善しようといろいろ考えて、自分なりに考えてみましたので、まとめます。

これまでの構成

old.png

  • ブラウザを起動して、プライバシーポリシーのページを表示

改善したいこと

  • 通信が出来ない時は表示できない
  • ブログにページを作ったのでイケてない

改善した構成

new.png

  • 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を使いましたが、とっても便利で簡単です。どんどんアプリ開発に活用していきたいですね。

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

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

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

クリーンアーキテクチャーをざっと理解できる短いコードを書いた

クリーンアーキテクチャーは沢山インターフェースやクラスが登場し、その関係をざっと把握するのが難しいです。
そこですべての要素が入ったなるべく短いコードを書いてみました。XCodeのPlaygroundなので、そのまま実行できます。githubのリポジトリはこちらです。なお、今回はざっと理解することを優先するため、クリーンアーキテクチャーについての詳しい説明は割愛します。参考記事をご覧ください。

このコードは、Uncle Bobの書いたClean Architecture 達人に学ぶソフトウェアの構造と設計と、次の記事を参考に書いています。

参考記事:
実装クリーンアーキテクチャ: https://qiita.com/nrslib/items/a5f902c4defc83bd46b8
Laravelで実践クリーンアーキテクチャ: https://qiita.com/nrslib/items/aa49d10dd2bcb3110f22

クリーンアーキテクチャーの概要図

この2つの図が有名かと思います。
image.png

image.png
引用:Clean Architecture 達人に学ぶソフトウェアの構造と設計

概要図と今回のコードとの対応

サンプルコードに現れるインターフェース、クラスをなるべく本家の図と同じ位置に同じ色で配置しています。

image.png

image.png

この図で、本家と違うのがSomeViewUserControllerにも依存しているところです。理由は、本家は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と同じレイヤーに配置されているということです。このあたりに依存性逆転の原則がしっかり守られていると感じました。

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

[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はelevationRadiusを5dpくらいつけとけば手軽にそれっぽくなる
キャプafaチャ.PNG

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.kt
class 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とかいろいろあるけど何を使うのが一般的なんだろ?

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

iOS, Androidアプリの価格変更(キャンペーン)の仕方

はじめに

以前アプリをリリース(iOS, Mac, Androidのアプリをリリースしてみた
)したのですが、複数の方から100円も払えるかクソが!!といわれました:sob:

まだ機能が思ってる完成形には達していないし、やっぱだれかがダウンロードしてくれないとおもしろくないししばらくは無料にすることにしました!!

たまにみるキャンペーンみたいなので期間限定無料をやりたいと思い調べていたのですがやり方があんま出てこなかったので備忘録として書きます。

iOS, Mac版

iOSとMacアプリについてです。やり方はめっちゃ簡単。(だからあんま調べてもでてこないのかも)

  1. 下記ページを開きます
    App Store Connect -> マイ App -> 対象アプリ -> App Store -> 価格および配信状況
  2. 価格表の「価格変更を計画」をクリック
  3. 価格、開始日、終了日を選択
    ios_price
  4. 「終了」クリック
  5. 「保存」クリック

終了日は元の価格に戻る日付のようです。(2月いっぱい無料にしたかったのに間違えた:see_no_evil:

ios_sell

Android版

Androidアプリについてです。こっちもめっちゃ簡単。

  1. 下記ページを開きます
    Google Play Console -> 対象アプリ -> ストアでの表示 -> 価格と配布 -> セール
  2. セール名を設定
  3. セール期間を設定(最大8日間らしいです) android_sell
  4. 価格を設定(最低30%オフらしいです) android_price_annotation
  5. 「販売スケジュールを設定」クリック

一旦セールを行うと次のセール作成は30日間おこなえないようです。

android_new_sell

参考:Play Console ヘルプー有料アプリのセールを作成する

さいごに

セールの開始日を当日にすると反映までは多少時間がかかるみたいです。余裕をもってセールを設定しましょう!!(iOSは3時間くらいでAndroidは8時間くらいでした)

今無料なんでだれかダウンロードしてください:bow_tone1::bow_tone2::bow_tone3::bow_tone4::bow_tone5:

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

GatsbyJSとNetlifyでiOS/AndroidアプリのLPを勢いで作ってみた

経緯

2020年1月にiOSアプリをリリースして、もともとAndroidのアプリがあったので
これは今ならLP作れるのでは... :sparkles:
という浅い考えと、何か新しい方法で試してみたいという欲求から今回は
GatsbyJSとNetlifyを使ってLPを作る事にしました。

GatsbyJSとNetlify

:computer:環境構築


今回はこちらのテンプレートを使用してLPを作成したいと思います。

$ gatsby new app-lp https://github.com/anubhavsrivastava/gatsby-starter-newage

早速起動してみます。

$ cd app-lp
$ gatsby develop

ブラウザで http://localhost:8000/にアクセスして↓のページが表示されればOKです。
image1.png
LPポイですねw (当たり前)

ここまで出来たら、一度Github上にリポジトリを作成しPushします。
今回はPrivateなリポジトリでも可能なのか検証も含めPrivateリポジトリで試しました。
(確認後Publicにしてます)

:pencil: サイトページの内容を修正


基本的な情報

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>&copy; [名前]. All Rights Reserved.</p>
      </div>
    </footer>
  );
}

コンテンツ全般の修正

次にコンテンツ全般の修正をガッツリ行いました。主に修正するファイルは
src/pages/index.js になります。
また、適宜CSSも微調整しました。

Netlifyでページ公開

ページを修正したら、早速デプロイして公開していきたいと思います。
Netlifyにログインすると以下のページが表示されます。
image2.png

早速Githubのレポジトリと連携しようと思うので、「New from site Git」から連携するリポジトリを選択します。

設定はデフォルトのままで「Deploy site」で早速デプロイしてみます。

これだけでデプロイが完了しました :tada: 簡単。

最後にサイト名を修正します :sparkles:
image7.png

今回作成したLPは以下に公開してます !
https://ss-map-lp.netlify.com/
※ドメイン変更まで行うと有料になるので、今回はやりませんでした。

リポジトリはこちら
https://github.com/Slowhand0309/ss-map-lp
(※Privateでも大丈夫そうだったので、今は公開してます)

ものの数時間でデプロイできました :sparkles: 更新があっても後はリポジトリにPushするだけなのでとても便利そうです。

:link: 参考になったURL


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

【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を利用した方がいいらしいけど

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

【Firebase】failed: DatabaseError: Permission denied

Firebase RealtimeDatabase使っている時、パーミッションがの設定値が初期値だとエラーが出る

failed: DatabaseError: Permission denied

ここのreadとwriteをtrueにしてあげれば通るようになる
スクリーンショット 2020-01-19 0.58.02.png

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