- 投稿日:2019-05-06T19:21:42+09:00
【Android】外部から取得した画像をデコードする時に気をつけること
APIなどを叩いて外部から取得した画像データを利用する場合、エラー等のデコードできない可能性や大きすぎる画像である可能性などを考慮する必要がある。
かなり大きな画像をデコードしてしまうと、Out of Memoryでクラッシュする可能性もあるため、直接ピクセルデータをデコードするのではなく、最初にサイズ情報のみをデコードし、必要なサンプリング数を設定してからデコードすることが必要になる。
こうしたことを考慮すると、以下のようなメソッドをutilメソッドとして定義しておき、使い回すのが好ましい。fun decodeBitmapFromByteArray( data: ByteArray, @Dimension requestWidth: Int, @Dimension requestHeight: Int ): Bitmap { val options = BitmapFactory.Options() options.inJustDecodeBounds = true BitmapFactory.decodeByteArray(data, 0, data.size, options) options.inSampleSize = sampleSize( options.outWidth, options.outHeight, requestWidth, requestHeight ) options.inJustDecodeBounds = false return BitmapFactory.decodeByteArray(data, 0, data.size, options) } private fun sampleSize( sourceWidth: Int, sourceHeight: Int, requestWidth: Int, requestHeight: Int ): Int { if (requestWidth <= 0 || requestHeight <= 0) { return 1 } if (sourceWidth * requestHeight < requestWidth * sourceHeight) { return if (sourceWidth > requestWidth) sourceWidth / requestWidth else 1 } return if (sourceHeight > requestHeight) sourceHeight / requestHeight else 1 }
inJustDecodeBounds
をtrueにしてdecodeByteArrayを呼び出すと、画像をメモリに展開せずに画像サイズを取得することができる。これを利用して画像サイズからサンプリングのサイズを決定し、optionsのinSampleSizeに設定した後に、inJustDecodeBounds
をfalseにして実際にdecodeを行う。
- 投稿日:2019-05-06T18:59:34+09:00
Canvasを使ってあるBitmapから正円のBitmapを作成する処理を書いた話
やりたいこと
ある画像から、サムネイルのアイコンを作って表示する処理が必要になったので、Canvasを使って実装してみました。
例えばこういうネクタイの画像があったとすると、欲しいのは以下のようなアイコンになります。
拡大縮小はせず、中心部分をうまく丸く切り取る処理をする場合、以下のような実装になります。実際のソースコード
private fun getCroppedBitmap(bitmap: Bitmap): Bitmap { val size = Math.min(bitmap.width, bitmap.height) val output = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888) val canvas = Canvas(output) val paint = Paint() paint.isAntiAlias = true canvas.drawARGB(0, 0, 0, 0) canvas.drawCircle(size / 2f, size / 2f, size / 2f, paint) paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN) val max = Math.max(bitmap.width, bitmap.height) val start = (size - max) / 2 val left = if (bitmap.width < bitmap.height) 0 else start val top = if (bitmap.height < bitmap.width) 0 else start val rect = Rect(left, top, bitmap.width + left, bitmap.height + top) canvas.drawBitmap(bitmap, Rect(0, 0, bitmap.width, bitmap.height), rect, paint) return output }解説
やっていることはそんなに難しくはありません。
まず、出力先のBitmapをsize指定で作成します。この時、sizeは元画像の幅/高さのうち小さい方を指定します。
次にCanvasとPaintを用意します。
今回は円形に画像を切り抜くので、Paintにはジャギーが目立たなくなるようにisAntiAliasをtrueに設定します(詳細はアンチエイリアスを参照のこと)。
canvasは透明で塗りつぶしてから、drawCircle(中心点のx座標,中心点のy座標,半径,paint)
でsizeの半分の半径でちょうど中央に円を描きます。
paint.xfermode
を設定する前のdrawCircleがDST、その後のdrawBitmapがSRCなので、画像が丸の中に入るようにするにはPorterDuffXfermode(PorterDuff.Mode.SRC_IN)
をpaint.xfermode
に設定します。
最後に切り抜くrectを決めてdrawBitmapすることで、描画処理は完了です。
- 投稿日:2019-05-06T18:23:00+09:00
KotlinMPPでAndroid/iOS両対応のHTTPクライアントライブラリを作る
はじめに
Android/iOS、両プラットフォーム上で同じようなアプリを開発する際、明らかに同じような処理をKotlin/Swiftで実装することがあり、共通化したいなと思うところがありました。
例えば、通信部分やEntity、ValueObjectなど、OSが違っても共通になる処理については尚更です。こういった部分を二度も違う言語で実装するのは明らかに二度手間で余計な工数がかかりますし、バグが発生する可能性も増します。
なんとかこういった部分を共通化し、一度コードを書けば両デバイスで動かせる、それによって工数の削減をしたい。そういったモチベーションがあったため、KotlinMPP
について調査を行い、実際にそれを用いて両プラットフォームに一度で対応できるHTTPクライアントを作ってみました。KotlinMPPとは
正式名称
Kotlin Multiplatform Project
Kotlin1.2及び1.3で実験的に提供されている機能です。Gradleでターゲットとなるプラットフォームにどんな方法でビルドをするかを記述することで、一つのKotlinのソースを各プラットフォーム向けに動作する形でビルドすることが出来ます。
例えば、AndroidではJVMで動作するようにビルドする、iOSではネイティブで動作するようにビルドする、といった次第です。
Anrdoid,iOSはもちろん、mac,Windows,JSをターゲットとすることが出来ます。KotlinMPPを使ったHTTPクライアントライブラリ
サンプルプロジェクトを作ってみました。
5hyn3/KotlinMPPHttpClientSample以下のような言語やビルドツール、ライブラリを組み合わせて作られています
kotlin
- 1.3.31
gradle
- 5.4.1
ktor-client
- 1.1.5
kotlinx.serialization
- 0.11.0
kotlinx.coroutin
- 1.2.1
テストが一つ記述されており、実際にAPIからjsonを取得し、パースしてkotlinのオブジェクトとして取得する部分までの動作を確認することが出来ます。実際にアプリに組み込んで動作を確認したい場合は以下を参考にしてください。
Multiplatform Project: iOS and Android
ビルド成果物としてjarとframeworkができるので、それらを扱う形になります。現状、AndroidとiOSのみをターゲットに作っていますが、gradleに手を入れることでJSなどにも対応させることが出来ます。
苦労した点
以下サンプルプロジェクトを作った際に遭遇した大変だった点を書いていきます
バージョンごとにビルド時の挙動が違っており適切なバージョンの組み合わせを見つけるのが大変だった
主にGradleによるものです。色々なプロジェクトを参考にしてサンプルプロジェクトを作りましたが、バージョンがちょっとでも違うと全くビルドすら出来ませんでした。また、ライブラリが要求するGradleのバージョンが食い違っていたりして悩んだこともありました。
最終的に、現時点での最新版である5.4.1
を採用することで解決しました。本プロジェクト作成に費やした時間の8割くらいはここで適切なライブラリを選定することで消費されています。まだexperimentalなのでこのあたりはどうしてもしょうがないですね。
suspend
なmethodをSwiftから呼び出せない現状Kotlin/Nativeは
suspend
なmethodをswiftから呼び出せるインターフェースを作ってくれないようです。suspend
をやめ、Defferd
インスタンスを返すようにすることでSwiftからもきちんと扱えるようになりました。こちらであればSwift側からも扱えるインターフェースが提供されており、問題なく呼び出すことが出来ます。Kotlin/Nativeでビルドした際にランタイムで
IncorrectDereferenceException
が発生する本プロジェクトでは非プリミティブ型のトップレベル変数をメインスレッド外から触ろうとしたために発生しました。具体的にどこで発生したかというと、ここです。対処法としては
@ThreadLocal
を付与して、スレッドローカルとして扱うことで対応を行いました。終わりに
これでKotlinでコードを一つ書けばAndroid/iOS両方で動くライブラリを作ることが出来ました。これでより業務を加速し、同じことを二回も書くようなつまらない作業は減らして、もっと本質の部分に時間を使うことが出来るようになるといいなぁと思います。
参考
JetBrains/kotlin-mpp-example
Multiplatform Project: iOS and Android
kotlin-native /IMMUTABILITY.md
DroidKaigi/conference-app-2019
SimonSchubert/Newsout
- 投稿日:2019-05-06T17:23:46+09:00
Android の DatePickerDialog で日時範囲(開始日時~終了日時)の選択
目的
Android の DatePickerDialog を使って、日付範囲(開始日~終了日)を選択させる。
- DatePickerDialogで日付を選んで、Buttonのtextに表示させる。
- Buttonじゃなくて他のViewに表示させてもOK。TextViewとか。
- 選択できる日付を制限する。
- 開始日に終了日より後の日付を選択させない。
- 終了日に開始日より前の日付を選択させない。
その他の仕様(詳しい説明はこちらの記事に書いてます)
すでに日付が選択されており、DatePickerDialogで日付を選ばなかったとき、選択された日付を削除する。
- キャンセルを押したときにButton.textにセットされていた値を変更するということ。
- ここでは日付が未入力の場合、"指定なし"というテキストをセットすることにします。
動作環境
- Kotlin 1.3.21
- Android 8.0 (Oreo)
- AndroidStudio 3.3.2
動作イメージ
実装
layoutファイル
開始日付と終了日付を選択・表示するButtonとTextViewの構成とします。
activity_main.xml<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" tools:context=".MainActivity"> <TextView android:text="日付範囲" android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/date_range_text" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" android:layout_marginLeft="8dp" android:layout_marginStart="8dp" android:layout_marginTop="16dp"/> <Button android:text="指定なし" android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/begin_date_button" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toEndOf="@+id/date_range_text" android:layout_marginLeft="64dp" android:layout_marginStart="64dp" android:layout_marginTop="16dp"/> <TextView android:text="~" android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/range" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toEndOf="@+id/begin_date_button" android:layout_marginLeft="8dp" android:layout_marginStart="8dp" android:layout_marginTop="16dp"/> <Button android:text="指定なし" android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/end_date_button" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toEndOf="@+id/range" android:layout_marginLeft="8dp" android:layout_marginStart="8dp" android:layout_marginTop="16dp"/> </android.support.constraint.ConstraintLayout>activityファイル
以下は、MainActivity.onCreateメソッド内のコードですが、分かりやすくするために2つに分けます。
- dateSetOnClickListener.kt : ButtonのViewを取得とButton.setOnClickListenerの実装を行う。
- DateDialogFragment.kt : DateDialogFragmentを表示させるclass。
dateSetOnClickListener.ktval mBeginDate = findViewById<Button>(R.id.begin_date_button) val mEndDate = findViewById<Button>(R.id.end_date_button) /* 開始日付を指定するボタンを押したとき */ mBeginDate.setOnClickListener { // DialogFragmentを生成し、表示 DateDialogFragment(mBeginDate).show(supportFragmentManager, mBeginDate::class.java.simpleName) } /* 終了日付を指定するボタンを押したとき */ mEndDate.setOnClickListener { DateDialogFragment(mEndDate).show(supportFragmentManager, mEndDate::class.java.simpleName) }DateDialogFragment.kt/* 日付を入力する時に使用するinnerクラス DialogFragmentで日付を入力できるカレンダーを表示する DialogFragmentを呼び出したボタンを取得したいので、コンストラクタの引数にButtonを定義する */ class DateDialogFragment(val button: Button) : DialogFragment() { /* DatePickerDialogを返却するメソッド このメソッドで、日付を選択した後の処理や、日付範囲、Dialogのタイトルを設定する */ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { // 現在日付を取得 val calendar = Calendar.getInstance() return DatePickerDialog( context, theme, // DialogFragmentで日付を選択し、OKを押したときの処理 DatePickerDialog.OnDateSetListener { _, year, month, dayOfMonth -> val inputDate = Calendar.getInstance() // 選択された日付を取得 inputDate.set(year, month, dayOfMonth) val dfInputeDate = SimpleDateFormat("yyyy-MM-dd", Locale.US) // CalendarからStringへ変換 val strInputDate = dfInputeDate.format(inputDate.time) // DialogFragmentを呼び出したボタンのテキストに日付をセット button.text = strInputDate }, calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF_MONTH)) .also { /* 選択可能な日付の上限を設定 終了日付が指定されている場合、開始日付は終了日付より後を選べないようにする */ if (button === mBeginDate && mEndDate.text != "指定なし"){ val maxDate = Calendar.getInstance() val dfMaxDate = SimpleDateFormat("yyyy-MM-dd", Locale.JAPAN) // 指定されている終了日付を取得 val endInputDate = dfMaxDate.parse(mEndDate.text.toString()) maxDate.time = endInputDate // 選択可能な開始日付の上限に指定されている終了日付を設定 it.datePicker.maxDate = maxDate.timeInMillis } else { // 選択可能な開始日付の上限に現在日付を設定 it.datePicker.maxDate = calendar.timeInMillis } /* 選択可能な日付の下限を指定 */ val minDate = Calendar.getInstance() /* 開始日付が指定されている場合、終了日付は開始日付より前を選べないようにする */ if (button === mEndDate && mBeginDate.text != "指定なし"){ val dfMinDate = SimpleDateFormat("yyyy-MM-dd", Locale.JAPAN) val beginInputDate = dfMinDate.parse(mBeginDate.text.toString()) minDate.time = beginInputDate // 選択可能な終了日付の下限に指定されている開始日付を設定 it.datePicker.minDate = minDate.timeInMillis } else { // 選択可能な終了日付の下限を設定、ここでは2018/2/1とします minDate.set(2018, 2, 1) it.datePicker.minDate = minDate.timeInMillis } // タイトルが勝手に表示されるのを防ぐために空文字をセット it.setTitle("") } } /* DialogFragmentでcancelを押した際の処理 */ override fun onCancel(dialog: DialogInterface?) { super.onCancel(dialog) // cancelを押した際はセットされていた日付を削除し、"指定なし"をセット button.text = "指定なし" } }
- 投稿日:2019-05-06T17:12:43+09:00
SQLiteで昇順・降順にソートする方法
概要
データベースから値を取得するとき、昇順・降順にソートしてから取得ができる
※前提条件として、SQLiteOpenHelperを使用してデータベースに値が入っていること使い方
number title 15 ABCD 67 EFGH 22 IJKL 12 MNOP 例えば上記のようにデータベース(DB)に値があったとする
if(helper == null){ helper = new TestOpenHelper(getActivity().getApplicationContext()); } if(db == null){ db = helper.getReadableDatabase(); } Cursor cursor = db.query( "testdb", new String[] { "number","title" }, null, //selection null, //selectionArgs null, //groupBy null, //having null, //orderBy );登録順にDBを取得するだけならこの方法で良い。
order Byに指定する(ソート)
if(helper == null){ helper = new TestOpenHelper(getActivity().getApplicationContext()); } if(db == null){ db = helper.getReadableDatabase(); } String order_by = "number ASC"; //ソートしたい値,昇順(ASC)か降順(DESC)か Cursor cursor = db.query( "testdb", new String[] { "number","title" }, null, //selection null, //selectionArgs null, //groupBy null, //having order_by );Cursorに値を代入する時にorder Byに指定する
→昇順はASC、降順はDESCを指定するだけ!その後(おまけ)
//データリストを回す cursor.moveToFirst(); for (int i = 0; i < cursor.getCount(); i++) { //ここに任意の処理を書く cursor.moveToNext(); } cursor.close();よく使う、ソート後のデータを取得する方法をメモ。
- 投稿日:2019-05-06T16:13:42+09:00
SharedElementTransition with Navigation Architecture Component
この記事は?
- RecyclerView(一覧画面) -> 詳細画面 の構成
- SharedElementTransition の適用
- Fragment -> Fragment 遷移をNavigation Architecture Componentで行う
利用SDK
project/build.gradleclasspath "androidx.navigation:navigation-safe-args-gradle-plugin:2.1.0-alpha02"app/build.gradleapply plugin: 'androidx.navigation.safeargs.kotlin' // 省略 implementation "androidx.navigation:navigation-fragment-ktx:2.1.0-alpha02"手順まとめ
一覧画面
postponeEnterTransition()
を実行しておく- RecyclerViewの描画直前に、
startPostponedEnterTransition()
を実行- 共有したいViewに一意の
TransitionName
を設定
- RecyclerView内のアイテムなら、position等を利用して一意にする
- 詳細画面に
TransitionName
を渡す- 遷移処理時に、
addSharedElement
をする詳細画面
sharedElementEnterTransition
を設定する- 前画面と共有するViewに渡されてきた
TransitionName
を設定する実装
Navigation Graph
xmlを記載したら、一度ビルドしましょう。
後ほど必要なクラスが生成されます。nav_graph<?xml version="1.0" encoding="utf-8"?> <navigation 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/nav_graph" app:startDestination="@id/home_fragment" > <!-- 省略 --> <!-- 一覧画面 --> <fragment android:id="@+id/todo_list_fragment" android:name="com.helmos.mysandbox.presentation.todo.TodoListFragment" android:label="TodoListFragment" tools:layout="@layout/fragment_todo_list" > <action android:id="@+id/start_detail" app:destination="@id/todo_detail_fragment" > <argument android:name="todo" app:argType="com.helmos.mysandbox.data.entity.Todo" /> <argument android:name="transition_name" app:argType="string" /> </action> </fragment> <!-- 詳細画面 --> <fragment android:id="@+id/todo_detail_fragment" android:name="com.helmos.mysandbox.presentation.todo.detail.TodoDetailFragment" android:label="TodoDetailFragment" tools:layout="@layout/fragment_todo_detail" > <argument android:name="todo" app:argType="com.helmos.mysandbox.data.entity.Todo" /> <argument android:name="transition_name" app:argType="string" /> </fragment> </navigation>一覧画面
RecyclerViewの使い方などは省略しています。
TodoListFragmentoverride fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { // 省略 // RecyclerViewのTransitionは遅延させる postponeEnterTransition() todoRecycler.viewTreeObserver.addOnPreDrawListener { startPostponedEnterTransition() true } } // onBindViewHolder 共有要素にTransitionNameを設定 view.transitionName = "todo_transition_$position" // 一覧アイテムクリック時の処理 setOnItemClickListener { item, view -> val todo = (item as TodoItem).todo val sharedView = view.findViewById<View>(R.id.todo_icon) // 画面遷移 findNavController().navigate( // 自動生成されるDirectionsクラス TodoListFragmentDirections.startDetail(todo, sharedView.transitionName), // addSharedElementを行ってくれるUtilクラス FragmentNavigatorExtras( sharedView to sharedView.transitionName ) ) }詳細画面
TodoDetailFragment// 自動生成される引数データクラス private val args by navArgs<TodoDetailFragmentArgs>() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) sharedElementEnterTransition = TransitionInflater.from(context).inflateTransition(android.R.transition.move) } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { // 省略 // 共有要素に設定 todoImage.transitionName = args.transitionName }迷った点/今後調べること
android.R.transition.move
を使わないとダメだった自作のtransitionだと、
Activity->Activity時には動いていたが、Fragment->Fragmentでは動作しなかった
何か制約があるのかも?移動させるだけであれば、
android.R.transition.move
でOK詳細から戻った時にアニメーションしなかった
RecyclerView描画直前に
startPostponedEnterTransition()
を実行して解決
こちらもActivity->Activity時には設定していなくても動いていたので、Fragment間の遷移だと色々と勝手が違いそう??
- 投稿日:2019-05-06T15:57:41+09:00
【kotlin】okhttp3でログを出力する
API通信まわりの実装をする際は、ログを見れたほうが作業が捗りますよね。
今回はokhttp3を使ってログを出力する方法をご紹介します。
- macOS: 10.14.1
- Android Studio: 3.4
- kotlin: 1.3.21
手順
build.gradleへの追加
logging-interceptor
を追加します。app/build.gradledef okhttp_version = '3.11.0' implementation "com.squareup.okhttp3:okhttp:$okhttp_version" implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version" // ログ出力に必要OkHttpClientの設定
ランタイムなどの設定と一緒に、
.addInterceptor...
の箇所を追加します。init { val okHttpClient = OkHttpClient.Builder() .connectTimeout(20, TimeUnit.SECONDS) .writeTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) // ログを出力させる設定 .addInterceptor(HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY }) .build() }手順は以上です。
出力してみる
こんな感じで、LogcatにD/OkHttp:
のメッセージが吐き出されるようになれば成功です。
通信したURLや、GET/POST、ステータスコードや受け取ったレスポンスなど、色々表示してくれて便利です。※もし手順通りに設定したにもかかわらずログが出力されない場合は、通信以前のところでつまずいている可能性があるので(例えばインターネット通信の許可をしていないとか)確認してみてください。
参考
- 投稿日:2019-05-06T15:15:50+09:00
MonacaでAndroidのアップロード鍵で既存アプリが更新できなくなった時の対応
はじめに
最近、MonacaでAndroidアプリを更新したときにGoogle Play アプリ署名が有効にしたためにハマったことのメモを残しておきます。
Google Play アプリ署名の仕組みについてはこちらをご覧ください。MonacaのAndroidキーストアのエクスポート
MonacaのクラウドIDEのメニューから、Androidキーストア設定を開いて、キーストアをエクスポートします。
アップロード鍵の作成とキーストアの更新
ダウンロードしたキーストアファイル keystore.private にアップロード鍵を更新します。
アップロード鍵は、Google Play アプリ署名を有効にするときに作成するか、後で [リリース管理] > [アプリの署名] にアクセスして作成できます。
アップロード鍵を作成する際は下記をご参照ください。
- Android Developers サイトの手順に沿ってアップロード鍵を作成します。鍵は安全な場所に保管してください。
アップロード鍵の証明書を PEM 形式でエクスポートします。以下の下線付きの引数を置き換えてください。
$ keytool -export -rfc -keystore keystore.private -alias jp.linedesign -file upload_cert.der
リリース プロセス中、指示に沿って、証明書をアップロードして Google に登録します。
アップロード鍵を使用すると:
- アップロード鍵はアプリ作成者の認証する目的でのみ Google に登録されます。
- アップロードした APK は、ユーザーに送信される前に署名が削除されます。
MonacaのAndroidキーストアのインポート
最後にMonacaのクラウドIDEのメニューから、Androidキーストア設定を開いて、キーストアをインポートすると完了です。
- 投稿日:2019-05-06T12:56:55+09:00
外部決済代行業者にアプリ内課金をさせることができるか
スマホアプリ(iOS/Android)で有料会員のみに一部コンテンツへのアクセスを可能とさせる場合、通常はApple/GoogleにてI/Fが用意されているアプリ内課金の仕組みで実装するのが正攻法と考えられる。
しかし、例えばAmazonのPrime videoなどではアプリ内課金の仕組みは利用されておらず、外部ブラウザにて会員登録をさせる仕組みとなっている。これらの仕様はiOS/Androidそれぞれのディベロッパーポリシー等に違反しないのか、その解釈を調べてみた。
まず結論
下記条件であれば、アプリ内課金を利用しなくても(外部の決済代行業者を利用しても)良いと考えられる。
- 有料会員の特典は、アプリ外でも利用できる(アプリ内のみのコンテンツではない)
- 決済画面はアプリ内WebViewよりは、ブラウザアプリで表示させたほうが良い。
判断理由(iOS)
App Store Reviewガイドライン(https://developer.apple.com/jp/app-store/review/guidelines/#in-app-purchase)によると
3.1.5(a)Appの外部で使用する商品やサービス:ユーザーがAppの外部で使用する商品やサービスをAppで購入できるようにする場合、そうした商品の支払いにはApp内課金以外(Apple Payや クレジットカードなど)の方法を使用する必要があります。
とある。つまり、
- アプリケーションの「内部」で使用する商品やサービス: App内課金での支払いで実装
- アプリケーションの「外部」で使用する商品やサービス: App内課金以外の方法での支払い(クレジットカード支払いなど)で実装
と考えられる(参考:https://inside.pixiv.blog/danbo-tanaka/4749)。
有料会員の特典適応範囲がアプリ内のみであるとみなされた場合、リジェクト対象となる恐れがある。判断理由(Android)
デベロッパーポリシー(https://play.google.com/intl/ja/about/developer-content-policy-print/ )では下記と定義されています。
デベロッパーは、Google Play からダウンロードされた別のカテゴリのアプリ内でプロダクトを提供する場合、支払い方法として Google Play アプリ内課金を使用しなければなりません。
ただし、以下の場合を除きます。
・物理的なプロダクトのみの支払い
・そのアプリ以外で消費できるデジタル コンテンツに対する支払い(他の音楽プレーヤーで再生できる曲など)つまり、基本的な考えはiOSと同じと考えられます。
- 投稿日:2019-05-06T12:20:44+09:00
FirebaseでPUSH通知を行った時の覚書(Android)
やりたいこと
AndroidでFCM(Firebase Cloud Messaging)を使って、PUSH通知を受信できるようにする。
参画した案件の開発で純正FCMの実装を初めて行ったので、その覚書。やったこと
- Firebaseの導入
- FCMの導入(PUSH通知を受信するロジックの実装)
Firebaseの導入
Firebase全般の概要 を見て、プロジェクトを作成
メモ2: SHA1の取得方法
AndroidStudioを使う場合、コマンドを打たなくてもTaskがすでに用意されているので、
キャプチャにあるsigningReportを実行するとSHA1などを取得できます。
FCMの導入(PUSH通知を受信するロジックの実装)
ガイド を読むと、AndroidManifestの設定やServiceの実装についてわかります。
わからない部分やソースの全体像を確認したい場合は、サンプルが用意されています。
https://github.com/firebase/quickstart-android/tree/master/messagingわからなかった、使わなかった部分
Google Play 開発者サービスのチェック
使わなかったですが、理解を深めようとした時にこちらの記事が参考なりました。
GoogleMapなどを未使用だったので、チェックは未実装にしましたが、やっておいた方がよかったのかも。AndroidアプリでGoogle Play Serviceの使用可否を検証する
ベストプラクティスを知りたい部分
/** * Called if InstanceID token is updated. This may occur if the security of * the previous token had been compromised. Note that this is called when the InstanceID token * is initially generated so this is where you would retrieve the token. */ override fun onNewToken(token: String?) { Log.d(TAG, "Refreshed token: $token") // If you want to send messages to this application instance or // manage this apps subscriptions on the server side, send the // Instance ID token to your app server. sendRegistrationToServer(token) }サンプルコードの sendRegistrationToServer で 該当するユーザーやグループにのみメッセージを送ったりするためにTokenをアプリケーションサーバに送るのですが、このタイミングで通信が失敗した場合はどこでリカバリするべきなのか?今回はアプリ起動時にリカバリ処理(インスタンスIDの再送信)を入れましたが、ベストプラクティスがあれば教えていただきたいです。。
PUSH通知を受信する
https://firebase.google.com/docs/cloud-messaging/android/receive
データの処理自体は前述のサンプルにもコードがのっているので、それほど難しくないように思えます。
メッセージの処理 にある表のパターンを読んで、
それぞれのパターンごとに挙動を理解することが大事だと思います。今回できなかったこと
サンプルベースの開発だったため、トピック、デバイスグループを使用しませんでした。
実際は配信の目的やテストを考慮するとデバイスグループも使用するべきですし、
そもそもFirebaseコンソールを使って都度手動の送信を行うのではなく、
サーバ側のアプリケーションも準備しておいた方が良いと思いました。まとめ
覚書として、あとでやった時に忘れている・困りそうな部分を中心にメモしましたが、
Firebase自体は内容も大きいのでやっぱり細かい部分を忘れていたり(実装内容というより何でそうしたか)、
コードや参考URLを丁寧にまとめようとすると時間がかかりすぎるので、こまめにまとめようと思いました。。
- 投稿日:2019-05-06T11:57:08+09:00
Androidの開発環境構築
まえがき
AndroidStudioインストールからHello World!までやってみた。
思ってた以上に問題が起こらずにできたので、
あまりほかの人には参考にならないかもしれない。構築環境
- Windows Home 64bit
- AndroidStudio 3.4
Androidの開発環境構築
AndroidStudioのインストール
https://developer.android.com/studio
DOWNLOAD ANDROID STUDIO のボタンでインストーラーをダウンロード
ダウンロードしたexeを起動してインストール開始
最初はとりあえずNEXTで進めていく
NEXTで進めていく
(ショートカットはいらないかなと思って、チェックをつけてインストールしたら
どこにexeがあるのかわからなくなったので、チェックつけずにインストールしたほうがいいかもしれない)
初回起動
どちらか好きな色選べるみたいなので、
とりあえず黒い方を選んで進める
Hello World!
Android Studio が立ち上がるので、
[Start a new Android Studio project] から
新しいプロジェクトを立ち上げる
とりあえず[Empty Activity]を選択してNEXT
LanguageがKotlinになっていることを確認して、
ほかは、お試しなのでそのままでFinish
開発画面(らしきもの)が開く
下の領域を見るとなにかダウンロード?しているようなので、そのまま待機
しばらく待つと、少しエラー?が出ているものの、完了したらしい
右上のアイコンが並んでいる中から右向き三角(▶)をクリック
すると、使用するデバイスの選択画面が出る
実機持ってないので仮想デバイスの準備をする
[Create New Virtual Device]をクリック
エミュレータの画面サイズ(機種)を選択
とりあえず、そのままPixel2で、作ってみる
エミュレータのAndroidOSのバージョンの選択画面
せっかくなので一番新しいQを選ぶ
DownLoadをクリックしてつかう準備をする
再び、OSのバージョン選択画面で、
ダウンロードしたQを選択した状態で、NEXTで進める
使用するデバイスの選択画面に戻って、
さっき作った仮想デバイスを選択してOK
ビルドは正常に終わったけど、
仮想デバイスが起動に失敗している、、、?
アイコン並んでいるところから
端末とドロイド君のアイコンをクリック
仮想デバイスの画面を開いて、
作成した仮想デバイスのActionの列にある右向き▶アイコンをクリックして
仮想デバイスを起動
仮想デバイスを起動したまま、
再び、開発環境からプログラムを実行
無事、Hello World! が表示された。
あとがき
AndroidStudioインストールからHello World!まで、
なんとなくで進めて普通に動いた。いままで何回か環境構築やったけど、大体一回は環境構築時点で挫折していたので、
すんなりいかないだろうから、備忘録かねて記録取ろう記事作成しましたが、
あまり意味のないものになったかもしれない。今回はとりあえず動けばいいやと思い、いろいろ設定をすっ飛ばして構築したので
追々、設定も調べて追記(もしくは別記事)できたらと思います。
- 投稿日:2019-05-06T00:10:17+09:00
アンインストールに失敗して「INSTALL_FAILED_UPDATE_INCOMPATIBLE」と出てきた時の復旧方法
コマンドプロンプトからアプリをインストールしようとして失敗している様子症状
デバッグ中にアプリをリセットしたいと思い、GooglePlayの「アンインストール」ボタンを押してアンインストールを開始しました。
しかし、「アンインストール中...」のまま画面が変わらなくなり、全く終了する気配がしなかったので一度GooglePlayを強制終了したら、今度はインストールできないという事態にハマってしまいました...
設定のアプリ一覧にも表示されず、インストールもできず、本当に困りましたが、検索したらいろいろやり方が書いてあったのでまとめてみます。失敗した復旧方法
正しく復旧できた方法は下の「復旧方法」に書いています
1.GooglePlayから入れなおしてみる
ちょうどGooglePlayにリリースした自分のアプリだったので、アプリのページを開いて「インストール」ボタンを押してみるも...
「他のユーザーが既にこのデバイスに互換性のないバージョンをインストールしているため、このアプリケーションをインストールできません」
と表示されてインストールできませんでした。2.Apkファイルから直接インストールしてみる
ビルドしたApkファイルを端末に送って、開いて「インストール」ボタンを押してみるも...
「このアプリはインストールできません」
と表示されてまたもインストール失敗。3.コマンドプロンプトでアプリをアンインストールする
USBデバッグを許可したスマホをPCに接続し、「adb uninstall rabbitp.sns.notifisor(アプリのID)」とコマンドプロンプトで入力して実行してみましたが、「Exception occurred while executing:」の後に何行もエラーが返ってきました。「adb install "C:\androidstudio-pc\Notification Sorting\Playストア用\app_v1.7.apk"(アプリのパス)」と入力してインストールも試みましたが、「adb: failed to install C:\androidstudio-pc\Notification Sorting\Playストア用\app_v1.7.apk: Failure [INSTALL_FAILED_UPDATE_INCOMPATIBLE: Package rabbitp.sns.notifisort signatures do not match previously installed version; ignoring!]」とエラーが返ってきて出来ませんでした。
復旧方法
1.Android Studioでアプリのプロジェクトを開く
↓
2.左のタブにある「ビルド・バリアント」(私は開発環境の言語を日本語にしています。名前はバージョンによって異なる可能性あり。)をクリックする。
↓
3.タブが開いたら、「ビルド・バリアント」を「build」から「release」に変更する。
↓
4.ビルドする!
↓
5.一度アプリをアンインストールしてもいいですか?的なダイアログが出たら、「OK」(ボタンに色がついている方)をクリックする。
↓
6.見事アプリのインストール成功。直ってよかったです。
初期化しないとダメかと思いました...その後...
この後すぐにまた同じ状況になりましたが、今度はapkファイルを直接インストールしたらあっさり直りました。
状況によって復旧方法が異なるようです。
上に書いた復旧方法すべてを試してみることをお勧めします。
- 投稿日:2019-05-06T00:10:17+09:00
アンインストールに失敗して「INSTALL_FAILED_UPDATE_INCOMPATIBLE」と出てきた時の復旧方法(アプリをインストール/アンインストールできなくなった時)
コマンドプロンプトからアプリをインストールしようとして失敗している様子症状
デバッグ中にアプリをリセットしたいと思い、GooglePlayの「アンインストール」ボタンを押してアンインストールを開始しました。
しかし、「アンインストール中...」のまま画面が変わらなくなり、全く終了する気配がしなかったので一度GooglePlayを強制終了したら、今度はインストールできないという事態にハマってしまいました...
設定のアプリ一覧にも表示されず、インストールもできず、本当に困りましたが、検索したらいろいろやり方が書いてあったのでまとめてみます。失敗した復旧方法
正しく復旧できた方法は下の「復旧方法」に書いています
1.GooglePlayから入れなおしてみる
ちょうどGooglePlayにリリースした自分のアプリだったので、アプリのページを開いて「インストール」ボタンを押してみるも...
「他のユーザーが既にこのデバイスに互換性のないバージョンをインストールしているため、このアプリケーションをインストールできません」
と表示されてインストールできませんでした。2.Apkファイルから直接インストールしてみる
ビルドしたApkファイルを端末に送って、開いて「インストール」ボタンを押してみるも...
「このアプリはインストールできません」
と表示されてまたもインストール失敗。3.コマンドプロンプトでアプリをアンインストールする
USBデバッグを許可したスマホをPCに接続し、「adb uninstall rabbitp.sns.notifisor(アプリのID)」とコマンドプロンプトで入力して実行してみましたが、「Exception occurred while executing:」の後に何行もエラーが返ってきました。「adb install "C:\androidstudio-pc\Notification Sorting\Playストア用\app_v1.7.apk"(アプリのパス)」と入力してインストールも試みましたが、「adb: failed to install C:\androidstudio-pc\Notification Sorting\Playストア用\app_v1.7.apk: Failure [INSTALL_FAILED_UPDATE_INCOMPATIBLE: Package rabbitp.sns.notifisort signatures do not match previously installed version; ignoring!]」とエラーが返ってきて出来ませんでした。
復旧方法
1.Android Studioでアプリのプロジェクトを開く
↓
2.左のタブにある「ビルド・バリアント」(私は開発環境の言語を日本語にしています。名前はバージョンによって異なる可能性あり。)をクリックする。
↓
3.タブが開いたら、「ビルド・バリアント」を「build」から「release」に変更する。
↓
4.ビルドする!
↓
5.一度アプリをアンインストールしてもいいですか?的なダイアログが出たら、「OK」(ボタンに色がついている方)をクリックする。
↓
6.見事アプリのインストール成功。直ってよかったです。
初期化しないとダメかと思いました...その後...
この後すぐにまた同じ状況になりましたが、今度はapkファイルを直接インストールしたらあっさり直りました。
状況によって復旧方法が異なるようです。
上に書いた復旧方法すべてを試してみることをお勧めします。