- 投稿日:2020-05-20T18:45:37+09:00
【Kotlin】RecyclerViewでリストを表示する
はじめに
今までRecyclerViewは複雑そうだと思って敬遠してたのですが、使ってみると意外と簡単かつ使い勝手も良さそうでしたので、忘れないうちまとめておこうと思います。
RecyclerViewの記事は他にも沢山あるので今更感が強いかもですが、私自身初学者ということもあり少し分かりづらい部分もあった為、同じような初学者向けに自分なりに分かりやすく書いてみようと思います。
なお、RecyclerViewそのものについての説明は割愛します。
それと、間違い等がございましたらご指摘頂けると嬉しいです。下準備
サポートライブラリを追加する必要がありますので、build.gradle(appのほう)のdependencies内に追加します。
build.gradle(Module.app)dependencies { implementation 'androidx.recyclerview:recyclerview:1.1.0'//これを追加 }構成
先にどのような構成か書いておきます。
-MainActivity.kt :リストを表示するアクティビティ
-activity_main.xml :メインアクティビティ用
-item.xml :リストのアイテム1つ分のレイアウト
-ItemData.kt :リストのアイテム1つ分が持っている変数
-CustomAdapter.kt :アダプター(今回はviewHolderを内装します)レイアウト
アクティビティにはとりあえずリストのみ表示させます。
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/recyclerListView" android:layout_width="match_parent" android:layout_height="match_parent"/> </androidx.constraintlayout.widget.ConstraintLayout>item.xmlはリストに表示するアイテム1つ分のレイアウトです。
今回はファイル名とファイルの更新日を表示しようと思います。item.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="wrap_content" android:background="@color/colorBackground"> <TextView android:id="@+id/list_name" android:layout_width="0dp" android:layout_height="wrap_content" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toStartOf="@+id/list_time" android:layout_marginStart="20dp" android:layout_marginTop="15dp" android:layout_marginBottom="15dp" android:text="Name" android:textSize="15sp" android:textColor="@color/colorText"/> <TextView android:id="@+id/list_time" android:layout_width="0dp" android:layout_height="wrap_content" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="5dp" android:text="Time" android:textSize="12sp" android:textColor="@color/colorText"/> </androidx.constraintlayout.widget.ConstraintLayout>レイアウト自体はLinearLayoutでもなんでも平気だと思います。
アイテム1つ分のデータ
ItemData.ktclass ItemData { var nameStamp:String = "" var timeStamp:String = "" }リストには表示させない変数があってもオッケーです。
カスタムアダプターの実装
CustomViewHolderクラスは別ファイルにしてもいいみたいです。
onCreateViewHolder()とgetItemCount()とonBindViewHolder()をオーバーライドしなくてはいけないみたい。
getItemCount()でリストのアイテム数を返してonCreateViewHolder()でアイテム数分のホルダーを作ってる感じなんですかね?
インターフェースやらリスナーやらは必要な場合のみ書きます。CustomAdapter.ktclass CustomAdapter(private val list: List<ItemData>):RecyclerView.Adapter<CustomAdapter.CustomViewHolder>() { lateinit var listener:OnItemClickListener class CustomViewHolder(val view: View):RecyclerView.ViewHolder(view){ val nameView:TextView = view.findViewById(R.id.list_name) val timeView:TextView = view.findViewById(R.id.list_time) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomViewHolder { val layoutInflater = LayoutInflater.from(parent.context) val item = layoutInflater.inflate(R.layout.item_list,parent, false) return CustomViewHolder(item) } override fun getItemCount(): Int { return list.size } override fun onBindViewHolder(holder: CustomViewHolder, position: Int) { //holderにItemDataのnameStampとtimeStampをセットする holder.nameView.text = list[position].nameStamp holder.timeView.text = list[position].timeStamp holder.itemView.setOnClickListener { listener.onItemClickListener(it,list[position]) } } //インターフェースをつくる interface OnItemClickListener{ fun onItemClickListener(view:View,itemData:ItemData) } fun setOnItemClickListener(listener: OnItemClickListener){ this.listener = listener } }アクティビティにセットする
今回はsavePathに保存されているファイルをリスト表示してみます。
MainActivity.ktclass MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) setList() } private fun setList(){ val recyclerView = findViewById(R.id.recyclerListView) val adapter = CustomAdapter(createList())//リストをわたします recyclerView.setHasFixedSize(true) val itemBreak = DividerItemDecoration(this,DividerItemDecoration.VERTICAL)//区切り線、必要あれば。 recyclerView.addItemDecoration(itemBreak) recyclerView.layoutManager = LinearLayoutManager(this) recyclerView.adapter = adapter adapter.setOnItemClickListener(object:CustomAdapter.onItemClickListener{ override fun onItemClickListener(view: View, itemData: ItemData) { //クリック時 Log.d("TapItem",itemData.nameStamp + itemData.timeStamp) } }) } private fun createList():List<ItemData>{ //CustomAdapterにわたすリストを作ります val dataList = mutableListOf<ItemData>() val savePath = ""//保存先のパス、適時変更等してください。 val files = File(savePath).listFiles() val sdf = SimpleDateFormat("yyyy/MM/dd/HH:mm:ss")//更新日用のフォーマット for (i in files.indices){ val fileName = files[i].name//ファイル名 val lastMod = files[i].lastModified()//更新日 val lastUpdate = sdf.format(lastMod)//更新日フォーマット後 val data:ItemData = ItemData().also { it.nameStamp = fileName it.timeStamp = lastUpdate } dataList.add(data) } dataList.sortWith(compareBy({it.nameStamp}))//ソートしてみる return dataList } }おしまい
まだまだ理解の浅い所もありますが、こうして一つ一つ分けてみるとそこまで複雑ではないように思います。
後からアイテムの要素を追加したりするのも簡単なので結構使い勝手が良いなと感じました。
- 投稿日:2020-05-20T18:37:58+09:00
Android Generate Signed Bundle or APKで必要なkey aliasの調べ方
はじめに
まず、この記事はキーストア(keystore)を紛失した人にとっては、意味のないものである!!!
(ちなみに僕は、キーストアファイルの拡張子にjks
は付けてません)そして、パスワードもわかっている人向けである!!!
key aliasとは?
Build > Generate Signed Bundle / APK
を選択し、Android App Bundle
もしくはAPK
どちらでもいいの選択し、nextkey aliasとは、下の画像でいうと、
key0
となってるあれ。
確かめ方
キーストア(keystore)へのパスを確認
コマンドプロントまたはターミナルを起動
-keystore
以降に、キーストア(keystore)へのパスを入れるkeytool -list -keystore /Users/gen/Desktop/basic/ionic/hCalendar/keystore
そしてパスワードを求められるので入れる
発見!
- 投稿日:2020-05-20T15:21:00+09:00
ViewDragHelperを紐解く
ViewのUI操作(特にマルチタップやドラッグ&ドロップ)などに使われる
ViewDragHelperを紐解いていきますViewDragHelperの呼び出し
ViewDragHelper.java/** * Factory method to create a new ViewDragHelper. * * @param forParent Parent view to monitor * @param sensitivity Multiplier for how sensitive the helper should be about detecting * the start of a drag. Larger values are more sensitive. 1.0f is normal. * @param cb Callback to provide information and receive events * @return a new ViewDragHelper instance */ public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb) { final ViewDragHelper helper = create(forParent, cb); helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity)); return helper; }ViewDragHelperのインスタンスを作成する
引数の
・第2引数sensitivity
はドラッグ検知度合いを表す値ですね、デフォルトで1.0fであり、この値自体あまり変えることはないと思います・第3引数
Callback
はViewDragHelper.Callback
であり、自分で実装することができます
ViewDragHelper.Callback
の動作を見ていきますonViewDragStateChanged
ViewDragHelper.java/** * Called when the drag state changes. See the <code>STATE_*</code> constants * for more information. * * @param state The new drag state * * @see #STATE_IDLE * @see #STATE_DRAGGING * @see #STATE_SETTLING */ public void onViewDragStateChanged(int state) {}Drag状態が変わった場合に呼び出されるということですね、Drag&Dropに使えそうです
onViewPositionChanged
ViewDragHelper.java/** * Called when the captured view's position changes as the result of a drag or settle. * * @param changedView View whose position changed * @param left New X coordinate of the left edge of the view * @param top New Y coordinate of the top edge of the view * @param dx Change in X position from the last call * @param dy Change in Y position from the last call */ public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {}Viewの位置が変わった場合ということですね
移動した後のNewXやNewYを主に使いそうですね
ViewGroup#getChildAt
等で元のchildViewのtopやleftを保存しておいて、そこからどれくらい移動したかとれそうですねonViewCaptured
ViewDragHelper.java/** * Called when a child view is captured for dragging or settling. The ID of the pointer * currently dragging the captured view is supplied. If activePointerId is * identified as {@link #INVALID_POINTER} the capture is programmatic instead of * pointer-initiated. * * @param capturedChild Child view that was captured * @param activePointerId Pointer id tracking the child capture */ public void onViewCaptured(View capturedChild, int activePointerId) {}Dragしている最中の場合に使えますね、例えばDrag中にそのViewの透明度を上げておくとかですかね
onViewReleased
ViewDragHelper.java/** * Called when the child view is no longer being actively dragged. * The fling velocity is also supplied, if relevant. The velocity values may * be clamped to system minimums or maximums. * * <p>Calling code may decide to fling or otherwise release the view to let it * settle into place. It should do so using {@link #settleCapturedViewAt(int, int)} * or {@link #flingCapturedView(int, int, int, int)}. If the Callback invokes * one of these methods, the ViewDragHelper will enter {@link #STATE_SETTLING} * and the view capture will not fully end until it comes to a complete stop. * If neither of these methods is invoked before <code>onViewReleased</code> returns, * the view will stop in place and the ViewDragHelper will return to * {@link #STATE_IDLE}.</p> * * @param releasedChild The captured child view now being released * @param xvel X velocity of the pointer as it left the screen in pixels per second. * @param yvel Y velocity of the pointer as it left the screen in pixels per second. */ public void onViewReleased(View releasedChild, float xvel, float yvel) {}ドラッグ等をしてそこからViewが離された場合に呼ばれることになります
releasedChild
ではViewDragHelper#createでParentViewを呼んだと思いますが、そのchildViewを呼ぶことになります
例えばFrameLayout -> ImageView
という構成でcreateでFrameLayoutを使った場合はImageViewが対象になるということですね今回で言えば、onViewPositionChangedで動いた分の長さをとっておいて、しきい値を超えたらActivity/Fragmentを終了したりできそうですね(swipeToDismiss)
onEdgeTouched
ViewDragHelper.java/** * Called when one of the subscribed edges in the parent view has been touched * by the user while no child view is currently captured. * * @param edgeFlags A combination of edge flags describing the edge(s) currently touched * @param pointerId ID of the pointer touching the described edge(s) * @see #EDGE_LEFT * @see #EDGE_TOP * @see #EDGE_RIGHT * @see #EDGE_BOTTOM */ public void onEdgeTouched(int edgeFlags, int pointerId) {}縁をタッチしている際の状況を取得できます
縁を使うようなユースケースとしてはPowerPointのテキストや画像をイメージしてもらうとわかりやすいですが、縁で伸ばしたり縮小・拡大したりということに使えそうですねonEdgeLock
ViewDragHelper.java/** * Called when the given edge may become locked. This can happen if an edge drag * was preliminarily rejected before beginning, but after {@link #onEdgeTouched(int, int)} * was called. This method should return true to lock this edge or false to leave it * unlocked. The default behavior is to leave edges unlocked. * * @param edgeFlags A combination of edge flags describing the edge(s) locked * @return true to lock the edge, false to leave it unlocked */ public boolean onEdgeLock(int edgeFlags) { return false; }縁をロックしたい場合に使うようですね、あまり使う機会はないかもしれませんが、縁でも別の挙動を一時的に適用した場合は一時的にtrueにしそうですね
onEdgeDragStarted
ViewDragHelper.java/** * Called when the user has started a deliberate drag away from one * of the subscribed edges in the parent view while no child view is currently captured. * * @param edgeFlags A combination of edge flags describing the edge(s) dragged * @param pointerId ID of the pointer touching the described edge(s) * @see #EDGE_LEFT * @see #EDGE_TOP * @see #EDGE_RIGHT * @see #EDGE_BOTTOM */ public void onEdgeDragStarted(int edgeFlags, int pointerId) {}縁をタッチしてドラッグがスタートした場合に使えますね
例えば縁を持って拡大・縮小する前にそもそものサイズを持っておいたりしておくことに使えそうです
後々undoでキャンセルしたりとかgetViewHorizontalDragRange
ViewDragHelper.java/** * Return the magnitude of a draggable child view's horizontal range of motion in pixels. * This method should return 0 for views that cannot move horizontally. * * @param child Child view to check * @return range of horizontal motion in pixels */ public int getViewHorizontalDragRange(View child) { return 0; }水平方向へのドラッグが可能な範囲を指定できます
また、0を返す場合は水平方向には移動させないことができますgetViewVerticalDragRange
ViewDragHelper.java/** * Return the magnitude of a draggable child view's vertical range of motion in pixels. * This method should return 0 for views that cannot move vertically. * * @param child Child view to check * @return range of vertical motion in pixels */ public int getViewVerticalDragRange(@NonNull View child) { return 0; }上記の
getViewHorizontalDragRange
の垂直方向バージョンですtryCaptureView
ViewDragHelper.java/** * Called when the user's input indicates that they want to capture the given child view * with the pointer indicated by pointerId. The callback should return true if the user * is permitted to drag the given view with the indicated pointer. * * <p>ViewDragHelper may call this method multiple times for the same view even if * the view is already captured; this indicates that a new pointer is trying to take * control of the view.</p> * * <p>If this method returns true, a call to {@link #onViewCaptured(android.view.View, int)} * will follow if the capture is successful.</p> * * @param child Child the user is attempting to capture * @param pointerId ID of the pointer attempting the capture * @return true if capture should be allowed, false otherwise */ public abstract boolean tryCaptureView(@NonNull View child, int pointerId);ドラッグすることができる場合に
true
が返り、その場合にonViewCaptured
が呼ばれるclampViewPositionHorizontal
/** * Restrict the motion of the dragged child view along the horizontal axis. * The default implementation does not allow horizontal motion; the extending * class must override this method and provide the desired clamping. * * * @param child Child view being dragged * @param left Attempted motion along the X axis * @param dx Proposed change in position for left * @return The new clamped position for left */ public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) { return 0; }ドラッグしているViewの水平方向の位置を取得
clampViewPositionVertical
/** * Restrict the motion of the dragged child view along the vertical axis. * The default implementation does not allow vertical motion; the extending * class must override this method and provide the desired clamping. * * * @param child Child view being dragged * @param top Attempted motion along the Y axis * @param dy Proposed change in position for top * @return The new clamped position for top */ public int clampViewPositionVertical(@NonNull View child, int top, int dy) { return 0; }ドラッグしているViewの垂直方向の位置を取得
- 投稿日:2020-05-20T14:43:45+09:00
Flutter初心者 & モバイルアプリ開発初心者の心得
皆さんアプリ開発楽しんでますかー??
どうも初めまして、okeと申します。
この度、Flutterで初めての個人アプリリリース、
そしてSwiftで2度目の個人アプリリリースを二つ合わせて1ヶ月程度で行いました。私は最近「個人開発」というものにハマっております!!とても楽しい!
しかしながら、これまでアプリのリリースは一度も行ったことはなかったため、
いざ自分のアプリを世に出そうとした時に、「早くこの情報知りたかった…」
という泣きたくなる出来事が山ほどありました。
特に私はモバイルアプリ開発をFlutterから入ったため、
SwiftやKotlinというネイティブ側の知識に乏しく、
Flutterの記事で時々見かける、「ここはいつものネイティブの開発のように…」
の「いつも」がわからない状態でした。
しかしながら私のように、
Flutter人気も上昇してきて、ネイティブを触らずにFlutterからアプリ開発に入る方もいらっしゃるかと思います。この記事では、
リリース手順やFlutterの基礎をまとめているわけではなく(他に良い記事はごまんとあるので)、
モバイル開発 & Flutter初心者の私がリリースに至るまでの失敗を元に、
皆さんが私の二の舞とならぬよう、
アプリ開発を始める前に知っておきたかった情報を書いております!!
(長文ですので、ゆったりと腰掛けてご覧ください!)第1章 アプリ開発・リリースが初めての方へ
第2章 Flutterでの開発をこれから始める方へ
という構成で進めて参ります!!対象読者
- Flutterからモバイルアプリ開発に入った方
- モバイルアプリ開発が初めての方
- これからスマホアプリを開発していきたい方
第1章 アプリ開発・リリースが初めての方へ
アプリ開発を始める前に…
皆さん、App StoreやGoogle Playの「ガイドライン」って読みましたか??
読んでいないなら、開発を始める前に読んでください!!!というのも…
私が作成したアプリ「Lyrics(リリックス)」は、
"音量ボタンの制御を奪って、歌詞のスクロールに使用する"
という発想から成るものでした。Bluetoothシャッターを使えば音量ボタンが作用して「足でもスクロールできる」ことを思いつき、
Bluetoothとか良く知らないけど、何となくできそうだし、、
「とりあえず作ってみるか!」
と意気込み、Flutterで実装をしていきました!!
そして1週間半後、なんとか形になりました!!
そしてリリース前に、
「リリースするし、一応ストアのガイドラインでも読んでおくかなー」と、「近所のスーパーに行く」くらい気楽な気持ちで読み始めました。
そして、、、
ガイドラインを見た私は唖然としました。
音量を上げる/下げる、サウンドオン/オフといった標準スイッチの機能、および他のネイティブユーザーインターフェイスの要素やその挙動を変更または無効にするAppは却下されます。
「 おいおい…… まってくれよ… 」
音量ボタン操作ありきの開発だったので、頭が真っ白になりました。
音量ボタンが使えないと、初心者本によく載っている「ただのメモアプリ」と化します。今までのアプリ開発をしてきた2週間が走馬灯のように駆け巡りました(?)。
「でもせっかく作ったアプリだし!」
と思って、
ダメ元で審査に提出したら…
「通ったーー!!!!」
きちんとApp Reviewのメモにも
「音量アップボタンを使用してスクロールするようにしています。
そのため、音量アップボタンが使用できませんが、注意事項としてその旨を記載しています。」と記載していた上で通ったので、大丈夫だったのだと思います。
(よくよく考えてみたら、カメラアプリとかも音量ボタンで撮影できますもんね…。)結果としてリリースはできたのでよかったのですが、
ガイドラインを見た瞬間は、まじで超焦りました(汗)
1週間ちょいだったからまだしも、長期に渡って開発をしていたら、、、
と考えたらゾッとします(怖)。今回に関しては大丈夫でしたが、
アプリを制作する前に、きちんと目を通して確認しておくべきだと感じました…。ここでの心得、、
" 時間を無駄にしたくない若者よ、まずはガイドラインじゃ"
Apple App Store Review ガイドライン
Google Play デベロッパーポリシーセンターAndroidとiOSをリリースするときはアイコンにも気をつけて!!
FlutterだとAndroidとiOSを両方一気に作成でき、超便利。
で、デザインに疎い私は、
意気揚々と友達にアイコンの案を伝えて、
デザインしてもらいました。どうですか?まあまあ良くないですか?
それで、これをflutterの便利パッケージを使用して反映!!!
https://pub.dev/packages/flutter_launcher_iconsiPhoneで確認したら…
良い感じ!!!
この調子で、またまた意気揚々とAndroidのアイコンを確認したら…
「Google Pixelのアイコンは丸型…だと…」
アイコンは四角く作るものと思っていた私はこれまた唖然。
「めちゃくちゃにちいせぇ…。」
そこでアイコンについて調べていると(はじめに調べとけというご意見、ごもっともです)
「アダプティブアイコン」なるものが!!!3分で分かる?Android OのAdaptive Iconに対応しよう
これでいける!と思い、早速試してみると、
「めちゃくちゃにでけぇ…」
まあ、元が四角の前提で作っている時点でだめですよね…。
友達に深く謝り、再度アイコンを作り直してもらい、
なんとか上手くいきました。
(友よ、すまぬ!!)しかし、これまた初めに確認しておけばよかった…
と大変後悔しました。。ここでの心得、、
"アイコンはアプリの顔。 イケメンかどうかよく確認するのじゃ!"
ストアの申請って結構めんどくさい
アプリが無事完成しました!
ってなったら、いよいよApp StoreやGoogle Playへの申請ですよね??で、申請しようと思った時にふと、
「Webサイトでよく見るプライバシーポリシーみたいなんっていらんのかな?」
と思いました。でも、私のアプリはローカルで完結する、言ってしまえばただの「メモアプリ」であり、
さすがにいらんだろうなーと思いつつ、Twitterに呟いてみると、
親切なことに、返信がきました!!(kazumatさんありがとうございます!!!)ローカル完結も利用規約入りますよー!
— kazumat@Flutter (@kzmat2) May 8, 2020
私が初めて作っ時は、必要なスクリーンショットの多さと、利用規約ページ作成にびっくりしました。
私はgithub pagesを使って利用規約書いてます〜「なぬーーーーー!?」
ローカルしか使ってないのにいるのか…。
調べてみると、iOSはプライバシーポリシー必須とのこと…。
以下、Appleの公式文章抜粋です。プライバシーポリシー:すべてのAppには、App Store Connectのメタデータフィールドと各App内にアクセスしやす>い形で、プライバシーポリシーへのリンクを必ず含める必要があります。プライバシーポリシーはわかりやすく明確なもの>である必要があります。
https://developer.apple.com/jp/app-store/review/guidelines/そこで、GitHub Pagesを使用して、プライバシーポリシーページを作成しました。
テンプレートも多くあり、ものの30分で完成しました!!
https://pages.github.com/肝心の内容ですが、、
広告(Admob)を使用していたため、その旨を記載したら、
審査に通りました!!下記の記事の文言そのまま記載しました。
AdMob, Firebase付きAndroidアプリのプライバシーポリシー、こうしてみました(要経過観察)プライバシーポリシーを書いて一件落着と思ったのも束の間、
「スクリーンショットもこんなにたくさん撮らないといけないの…」
という感じでした。。
AndroidとiOS用にそれぞれ作成しないといけないのですが、
作らないといけないサイズの種類が多いこと…。Androidの方はある程度自由がききます。
サイズとしては、
1辺の最小の長さ: 320px
1辺の最大の長さ: 3840px
であれば良いようです。(2020年5月現在)しかし、フィーチャーグラフィックなるものを作成する必要があるようで、Android用に作成しました…。
こちらは
横 1024 x 縦 500
であれば良いようです。(2020年5月現在)iOSの用意するサイズは下記のサイトが参考になりました。
https://r-abo.com/2019/11/05/aso018/また、サイズ変更は下のサイトが便利でした!
https://bulkresizephotos.com/ja?preserve_aspect_ratio=true&resize_type=exact&resize_value=1242&secondary_resize_value=2688そーやってやっとの思いで申請を行ったのですが、
iOSの方だけリジェクトされました(泣)内容を読んでみると、タイトルがだめというご意見。
当時私は
「Lyrics -歌詞メモを見やすく便利に」
というタイトルにしていたのですが、
調べてみると、どうやらサブタイトルのようなものをタイトルにするのはダメな模様…。
https://cocoamix.jp/archives/7706ということで、
「Lyrics(リリックス)」
に修正し、ハイフン以下をサブタイトルに移動すると…、審査が通りました!!
晴れてストアに私のアプリが並ぶことができました!!!
個人開発だとなかなかに作業量が多く、
アプリは完成しているのにリリースできない、
もどかしい時間が長く続きます。。ここでの心得、、
"アプリリリースまでが開発。根気よく頑張るのじゃ!"
第2章 Flutterでの開発をこれから始める人へ
Flutterだけでは実現できないこともある!
私はFlutterを万能なツールだと思ってました。
「iOSもAndroidも両方一気に作れるじゃん?」
「マジで超最高じゃん?」そんなイキってた時期もありました。
しかし、音量ボタンでスライドする機能を作成する過程で、
「音量ボタンでスライドはできるけど、音量ゲージが表示されるじゃん…」
という状況に陥りました。でもFlutterは最強だと思っていた私は、
「誰かパッケージ出してるっしょ!!」
と、そんなミルクティーよりも甘い気持ちでいました。
しかし、、
探しても探しても見つからない!!!
めちゃくちゃ時間を費やしました。
しかし、見つかりませんでした。
悲しかった。全俺が泣いた。でも1つの突破口は知っていました。
検索していたらたくさん出てきました。「ネイティブ言語で実装」
目を背けていました。
Flutterは一気にiOSとAndroidのアプリできて最高だぜって友達に自慢してた俺。
SwiftとかKotlin? 時代はFlutterだぜとか意味わからないことを思っていた俺。全て桜のようにはかなく散っていきました。
そして、藁にもすがる思いでSwiftとKotlinを勉強し、実装した結果…
「できたーーーー!!」 (よかったーー!)
音量ゲージを表示せずに、画面をスライドすることができるようになりました!
Flutterだけで全て完結できると思っているFlutter初心者諸君への心得、、
"Flutterで完結できることもある。でも、できないこともある。"
因みに、今回はネイティブ側からFlutterへの通知のみ行ったので、
EventChannelというものを使用して、実装しました。
Flutter EventChannel APIの使い方パッケージってすっごく便利!!
Flutter初心者の方、下記のサイトは見ていますか??
https://pub.dev/もうね、先人たちがものすごくいいパッケージをたくさん作ってくれてるんです。
アニメーションとかも簡単に実装出来たり、
チュートリアル画面も簡単に実装出来ちゃったり、
アラート画面をいいー感じにしてくれたりするんです!!下記の記事を見ると、
「こんなことが簡単にできちゃうのー!?」
ってなります。
【Flutter】アプリ開発_初心者のアプリをプロっぽくする最強のpackageを紹介
アプリをおしゃれにデコレーションする Flutter パッケージ「なんかいいー感じにできないかなー」
って思ったときは、パッケージを探してみてください!!楽しいですよ!!因みに、「Lyrics(リリックス)」では下記のパッケージを使用しているページがあります。
背景が炭酸水みたいになって面白いです。良かったら探してみてください!
超絶簡単に実装できました。
https://pub.dev/packages/animated_backgroundここでの心得、、
"便利かついい感じにしてくれる最強パッケージを見逃すでない!!"
SNSを大いに活用しよう!!
わざわざ書くほどのことでは無いかもしれませんが、私自身が助けられたので記載します。
Flutterの記事はまだまだ多いというわけではなく、
不足している情報や、理解できない情報もあると思います。そんなときに役に立つのが、
「Twitter!!!!」Flutterについて詳しい方々が、
最新の情報や、
便利と思った情報を投稿してくださります。
その情報を参考に、自分の実装を考えることも多かったです。Flutterで検索して、日本人で記事を書いている方はだいたいTwitterをやっています(偏見です)。
その方々をフォローするとともに、自分自身も参考になった情報等を発信していきましょう!!そうすることで、少しずつフォロワーが増え、
先ほど書いたプライバシーポリシーの件のように、
自分の疑問に答えてくださる方もいらっしゃいます。是非活用していきましょう!!!
ここでの心得、、
"便利なツールはどんどん使っていくのじゃ!!"
最後に
ここまで読んでいただき、ありがとうございました。
アプリ開発は楽しく、やりがいをもってできる最高の娯楽だと思ってます。(私だけでしょうか笑)
そんな娯楽ライフをさらに有意義なものにするため、
少しでも皆さんのお役に立てればと思っています。これからも何か得た知見等あれば発信していくので、よろしくお願いします!!
@oke331(因みに今回作成したアプリはCMも作成しました。良かったらご覧ください。)
↓Swiftで開発
MOJI PIC(モジピク) CM↓Flutterで開発
Lyrics(リリックス) CM 〜自称シンガー編〜
- 投稿日:2020-05-20T11:59:25+09:00
AdaptiveIconに対応する
環境
Android Studio 3.5
概要
AdaptiveIconについては下記URLを参照。
https://developer.android.com/guide/practices/ui_guidelines/icon_design_adaptive?hl=jaアイコンの表示の違い
未対応の場合はアイコンの中央に配置された状態でアプリアイコンが表示されます。
様々な形状に対応
画像の用意
使用する画像はベクタ画像がおすすめです。
用意する画像は単色の背景の場合はColorResourceを使うのでアイコンのイラスト部分にあたる画像のみでOKです。
背景に画像を適用したい場合は別途画像が必要です。
ベクター画像の場合のサイズは108x108dpで作成します。
png等を利用する場合はサイズ次第で荒くなるので注意が必要です。
pngで画像を作る場合は背景透過で全体のサイズを432x432px、イラスト部分を288x288pxで作成すると粗さもなくいい感じになります。アイコンの作成
アイコンの作成はAndroidStudioのImageAssetを利用して作成するのが便利です。
New→ImageAssetで作成ツールが起動します。
NameとLayerNameはデフォルトのままにしておくと既存のアイコンに上書きされるので設定の手間が少なくなります。アイコン作成手順
ForegroundLayerタブを開き、前景に配置する画像を設定します。
Resizeのスライダーで画像のサイズ調整ができるのでガイドからはみ出ていたりする場合はこれで調整しましょう。
次にBackgroundLayerタブを開き、前景に配置する画像を設定します。
単色背景の場合はAssetTypeをカラーに、画像を使用する場合はImageを選択します。
ImageはForegroundLayerと同様にResizeのスライダーで調整可能です。
調整が終わったらNextをクリックします。生成される画像が一覧で表示されるので表示に問題ないかチェックしましょう。
mipmap-anydpi-v26がAndroid8以降のAdaptiveアイコン、それ以外はAndroid8以前のアイコンとして使用されます。問題なければFinishをクリックします。
アイコンの設定
AndroidManifestのapplicationタグのiconに作成したアイコンのDrawableを入力します。
作成時にデフォルトのままにしていた場合は上書きされるので上記の作業は不要です。
あとはアプリをインストールしてアイコンが設定されているかを確認しましょう。端末のアイコンの形状の設定変更方法
前提として開発者モードを有効にしておきます。
1. 設定アプリを起動
2. システム→詳細設定→開発者向けオプション→テーマ設定セクションのアイコンの形
3. 任意の形状を選択する
※端末によっては設定方法が異なるので注意
- 投稿日:2020-05-20T11:54:42+09:00
AndroidアプリをDarkThemeに対応させる
概要
DarkThemeについては以下を参照
https://developer.android.com/guide/topics/ui/look-and-feel/darkthemeリソースの用意
ダークテーマ用のリソースはnightの修飾子がついたディレクトリ(以下nightディレクトリとする)に格納する。
(colorだったらvalues-nightディレクトリにカラーリソースファイルを格納)
既存のリソースディレクトリはlightモード時のものになる。
リソースのうち、ダークテーマに対応する必要がないものはnightディレクトリに格納する必要はない。
(lightモードのものが適用される)。
AppThemeを変更
AppThemeを
Theme.AppCompat.DayNight
に変更する。
(NoActionBarの場合はTheme.AppCompat.DayNight.NoActionBar
)
- 投稿日:2020-05-20T11:46:55+09:00
[Android] WebAPIでのサーバー接続
Androidアプリから、APIでWebサーバーへアクセスするライブラリ sankosc/webapi-clientの紹介となります。Kotlinです。
このライブラリの特徴は
- jsonの解析はKotlinx Jsonで自動で行う。データ構造だけ定義します。
- 通信のリトライと、AccessTokenのRefreshの処理を半自動で行います。
- バックグラウンドにて実行します。
- 通信成功、失敗の処理をイベントとして渡します。そのイベントはUIスレッドで呼び出されます。インストール方法
Kotolinx Jsonを使用しますので、そのインストールも併せて行います。
プロジェクトのbundle.gradleとアプリのbundle.gradleを編集
bundle.gradle(Project)dependencies { classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" }bundle.gradle(app)apply plugin: 'kotlinx-serialization' dependencies { implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.20.0" implementation 'jp.co.sankosc:webapi-client:1.0.2' }パーミッション追加
AndroidManifest.xmlにパーミッションを追加します。
AndroidManifest.xml<uses-permission android:name="android.permission.INTERNET" />データ定義
データ定義はKotolinx Jsonで行います。
詳しい定義方法はこちらをご参照ください:Kotolinx Jsonsampleimport kotlinx.serialization.* @Serializable data class Message( val message: String? ) @Serializable data class Login( val email: String, val password: String ) @Serializable data class Token( @SerialName("access_token") val accessToken: String )Get
Genericでデータ構造を指定。APIのURLと成功、失敗時のイベントハンドラを引数で渡します。
成功時には、イベントハンドラに指定した構造のデータが渡されます。sampleval url = "https://develop.sankosc.co.jp/apitest/api/hello" val client = WebApiClient() client.get<Message>(url, { // Success println(it.message) }, { // Error it.printStackTrace() })Post
Genericでデータ構造を指定。APIのURLと送信データ、成功、失敗時のイベントハンドラを引数で渡します。
成功時には、イベントハンドラに指定した構造のデータが渡されます。sampleval url = "https://develop.sankosc.co.jp/apitest/api/echo" val client = WebApiClient() client.post<Message, Message>(url, Message("post data"), { // Success println(it.message) }, { // Error it.printStackTrace() })エラー処理
失敗時には、発生した例外がハンドラに渡されます。
サーバーエラーの場合は、WebApiClientExceptionが渡されます。
WebApiClientExceptionは、サーバーのステータスコードを含みます。
例外は、Exceptionとして渡されるので、必要であればキャストして使用します。例)エラー処理client.get<Message>(url, { }, { // Error if (it is WebApiClientException) { println(it.code) } })認証APIへのアクセス
Login処理などで取得したAccessトークンをインスタンスに設定してGet, Postを使用します。
そうするとヘッダにAuthorization: Bearer [Access Token]
を付けてサーバーにアクセスするようになります。例)ログイン処理val url = "https://develop.sankosc.co.jp/apitest/api/login" val client = WebApiClient() val data = Login("sample@sankosc.co.jp", "sample123") client.post<Login, Token>(url, data, { client.accessToken = it.accessToken }, { it.printStackTrace() })AccessTokenは、アプリ起動時などに保存しておいたものをコンストラクタにて設定することもできます。
sampleval client = WebApiClient(accessToken = "[保存しておいたトークン]")認証エラーの処理
認証エラーの処理は、WebApiClientインスタンス生成時にコンストラクタを通して設定します。
sampleval client = WebApiClient(handleAuthFail = { // 認証エラー処理 })AccessTokenのリフレッシュ
AccessTokenのリフレッシュの処理は、WebApiClientインスタンス生成時にコンストラクタを通して設定します。設定したハンドラは、サーバーからステータスコード401が返されたときに呼び出されます。
このハンドラ(handleTokenExpired)は、引数を2つもちます。呼び出し元のWebApiClientインスタンスとリフレッシュしたトークンを渡すonRefreshed関数です。onRefreshed関数は、呼び出されたときに、認証切れになる直前に呼ばれていたAPIを再度自動で呼び出します。
トークンリフレッシュの例val client = WebApiClient(handleTokenExpired= {client, onRefreshed -> // トークンリフレッシュ処理 val url = "https://develop.sankosc.co.jp/apitest/api/refresh" client.refresh<Token>(url, { onRefreshed(it.accessToken) }, { it.printStackTrace() }) })トークンのリフレッシュには、通常のget, postメソッドではなくrefreshメソッドを使います。
get, postメソッドとrefreshメソッドは、機能はまったく一緒ですが認証エラー時の処理が違います。おしまい
もしご興味ある方は、こちらに他のパラメータも記載していますのでご参照ください。
GitHub: https://github.com/sankosc/webapi-client-androidまた確認用のAPIサーバーを準備していますので、こちらで試して頂けたら幸いです。
APIサーバー https://develop.sankosc.co.jp/apitest/ありがとうございました。
- 投稿日:2020-05-20T05:45:03+09:00
【flutter初心者】画面の構成とファイル分割
flutter初心者(自分)がアプリを作るにあたり最初に困ったのでメモ。
おそらく初歩的過ぎてちゃんと説明してるところがあまり見当たらなかったので。よくあるサンプルコードは大体こんな感じになってることが多いです。
main.dart
import 'package:flutter/material.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', home: MainPage(), ); } } class MainPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('main画面のタイトルバー'), ), body: Center( child: RaisedButton( child: Text('ボタン'), ), ), ); } }ふむふむ、main処理のrunAppメソッドでMyApp クラスを呼び、MyAppクラスで画面全体のウィジェットをビルド・・・なるほど。
これはこれでなるほどよく分かるのですが、「じゃあボタンを押して新しい画面を開くには」どうすればよいか?
画面(ビュー)はScaffoldウィジェットが親になる、と考えて良いかと思います。(Scaffoldはアプリケーションバーやbodyを持つウィジェットです)。
なので新しい画面を開くには
・新しい画面をビルドするクラスを定義する。
・このクラスはScaffoldをリターンする。とすればOKです。この新しいクラスを別ソースファイルにすることで、画面ごとにファイル分割が可能です。
上記を改修したサンプルはこちら
main.dart
import 'package:flutter/material.dart'; import './newview1.dart'; //★1 外出ししたファイルのインポート void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', home: MainPage(), ); } } class MainPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('main画面のタイトルバー'), ), body: Center( child: RaisedButton( child: Text('ボタン'), //★2 画面遷移のボタンイベント onPressed: () => Navigator.of(context).push(MaterialPageRoute(builder: (context) { return NewView1(); })), //★2追加ここまで ), ), ); } }★1、★2を追記
newview1.dart (新しい画面用のファイル/クラス)
import 'package:flutter/material.dart'; class NewView1 extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('NewView1のタイトルバー'), ), body: Center( child: Text('NewView1'), ), ); } }これで、初期表示画面の上に新しい画面が表示されます。
このような形で画面/機能ごとにソースファイルを分割していくことが可能です。
- 投稿日:2020-05-20T02:21:38+09:00
Android向けPlayゲームサービスのスタートガイドの備忘録
Android向けPlayゲームサービスのスタートガイド
Google Play ゲームサービスを使った Android ゲームの開発について、Android 向け Play ゲームサービスのスタートガイドを実際に行い、必要な内容を備忘録としてまとめました。
参考 :
Play ゲーム サービス
https://developers.google.com/games/servicesAndroid向けPlayゲームサービスのスタートガイド
https://developers.google.com/games/services/android/quickstart概要
Play ゲーム SDK では、タブレットやモバイル デバイスでのゲームに実績、リーダーボード、保存済みゲーム、リアルタイム マルチプレーヤー型ゲーム(Android 上)など人気のゲーム機能を簡単に統合できるクロス プラットフォームとして、Google Play ゲームサービスを提供されています。
Google Play ゲームサービス機能詳細
- 実績: 目標を設定し、プレーヤーがゲームをより長時間プレイして新しい機能を試すように促します。プレーヤーには、Play ゲーム プロフィールのレベルアップに利用できる経験値を特典として提供します(Play ゲーム プロフィールで他のプレーヤーに自分のレベルを伝えることができます)。
- リーダーボード: 日別、週別、全期間のリーダーボードを作成して、プレーヤー同士の競争を盛り上げます。ゲームレベルやその他のゲームの機能別に、複数のリーダーボードを作成します。
- マルチプレーヤー型ゲーム: リアルタイムまたはターン制のゲームに、他のプレーヤーと対戦する機能を追加します。プレーヤーは、端末の種類や対応プラットフォームの枠を超えて、友だちを招待してゲームのインストールをすすめることができます。
- ゲームギフト: プレーヤーが友だちに仮想のゲーム内オブジェクトを贈ることのできる機能を追加して、ダウンロード数とエンゲージメントを高めます。また、プレーヤーがギフトをリクエストできるようにして、ゲーム内でギフトを使って取引ができるようにします。
注意
Play ゲームサービスのマルチプレーヤー型ゲーム用 API のサポートを終了します。
リアルタイム マルチプレーヤー型ゲームとターン制マルチプレーヤー型ゲームのサポートは、2020 年 3 月 31 日をもって終了します。新しいゲームでこれらの機能を有効にすることはできません。目的
Android のサンプル ゲームアプリを理解し、独自の Android ゲームの作成をすぐに始められるように準備します。
準備
- Android 開発環境をセットアップ
- Android アプリを初めて開発する場合は、初めてのアプリを作成するの記事を確認
- Android 4.0(Ice Cream Sandwich)以上が稼働する Android デバイスをテスト用に準備
手順
手順 1: サンプルアプリをダウンロードする
Type-a-Number という Android サンプルアプリを使用します。
1: Android サンプルアプリをダウンロード ページからダウンロードします。
Macでターミナルを開いて、以下のコマンドを実行します。git clone git@github.com:playgameservices/android-basic-samples.git2: android-basic-samples プロジェクトをインポートします。
このプロジェクトには TypeANumber とその他の Android サンプルゲームが含まれています。Android Studio で手順のとおり進めます。
[Import project (Gradle, EclipseADT, etc.)] をクリックします。
android-basic-samples をダウンロードしたディレクトリを参照します。3: TypeANumber で使用されている build.gradleファイルのapplicationIdを独自のパッケージ名に変更します。
パッケージ名
com.google.example.games.replace.me
を別の独自の名前に変更します。
AndroidManifest.xmlファイルを編集する必要はありません。
新しいパッケージ名の先頭にcom.google
、com.example
、com.android
を使用することはできません。build.gradledefaultConfig { /* TODO: Replace this value with your application's package name */ applicationId "com.google.example.games.replace.me"手順 2: Google Play Console でゲームをセットアップする
Google Play Console では、各ゲーム用に Google Play ゲームサービスを管理し、ゲームの承認および認証のためのメタデータを設定します。
Google Play Console でサンプルゲームをセットアップするには...
1: ウェブブラウザで Google Play Console に移動してログインします。これまで Google Play Console に登録していない場合は、登録を求めるメッセージが表示されます。
2: 以下の手順のとおり、Google Play Console にゲームを追加します。
左側の[ゲームサービス]タブを選択します。
[ GOOGLE PLAY ゲーム サービスをセットアップ ]ボタンをクリックします。
Google Play ゲーム サービスをアプリ用にセットアップする画面が表示されます。
[ゲームで Google API をまだ使用していません] のタブを選択します。
ゲームの名前を入力してカテゴリを割り当て、[ 次へ ]ボタンをクリックします。
ゲームの詳細フォームで、ゲームの説明、カテゴリ、グラフィックアセットを追加します。
- テストには表示名のみが必要です。ゲームを公開する前に、他のフィールドに入力する必要があります。
- ゲームの表示名と説明は、同じGoogle Playゲームサービスを共有するゲームのすべてのバージョンに適用できるように十分に汎用的である必要があります。
- グラフィックアセットの作成に関するガイドラインについては、Google Play for Developersガイドと Google Playの注目画像のガイドラインを確認する必要があります。
- このデベロッパー ガイドの目的は、独自のゲームについての詳細をフォームに入力できるようになることです。そのために利用できるプレースホルダ アイコンやスクリーンショットが、ダウンロード ページで提供されています。
3: Type-a-Number Challenge の実績を設定します。
Google Play Console で [実績] タブを選択します。
次のようなサンプルの実績を追加します。
名前 説明 特記事項 Prime 素数のスコアを獲得します。 なし Humble スコア 0 点を獲得します。 なし Don't get cocky, kid いずれかのモードでスコア 9999 点を獲得します。 なし OMG U R TEH UBER LEET! スコア 1337 点を獲得した場合に達成となります。 隠しクエストとして設定します。 Bored ゲームを 10 回プレイします。 10 ステップでロック解除される増分実績として設定します。 Really Really Bored ゲームを 100 回プレイします。 100 ステップでロック解除される増分実績として設定します。 作成した各実績の ID(長い英数字の文字列)を記録します。
作成するゲームに合った実績を設定します。詳細については、実績の背後にあるコンセプトについての記事と Android での実績の実装方法についての記事を確認します。4: Type-a-Number Challenge 用のリーダーボードを設定します。
Google Play Console で [リーダーボード] タブを選択します。
「Easy High Scores」と「Hard High Scores」という名の 2 つのサンプル リーダーボードを追加します。どちらのリーダーボードも、[スコアの形式] として [小数点以下の桁数] が 0 の整数を使用し、[順番付け] を [スコアは高いほど良い] とします。
作成した各リーダーボードの ID(長い英数字の文字列)をメモします。
作成するゲームに合ったリーダーボードを設定します。詳細については、リーダーボードの背後にあるコンセプトについての記事と Android でのリーダーボードの実装方法についての記事を確認します。5: 以下の手順のとおり、Android アプリ用に OAuth 2.0 クライアント ID を生成します。
Google Playゲームサービスの呼び出しを認証および承認するには、ゲームにOAuth 2.0クライアントIDが必要です。クライアントIDとゲームの関連付けを設定するには、Google Play Consoleを使用してクライアントIDを生成し、ゲームにリンクします。
リンクされたアプリケーションを作成するには...
ゲームサービスをGoogle API Consoleプロジェクトにリンクするには、左側の[リンク済みアプリ]タブを選択ます。
Android アプリ用に OAuth 2.0 クライアント ID を生成するので[ANDROID]を選択します。
Android アプリをリンクする際、サンプル パッケージの名前を変更したときに使用した新しいパッケージ名を正確に指定します。
キーストアや署名済みの証明書がない場合は、Android Studio で署名済み APK の生成ウィザードを使用して、新しく生成できます。署名済み APK の生成ウィザードの使用方法については、Android Studio でのアプリの署名についてを確認します。
手順 3: コードを変更する
ゲームを実行するには、Android プロジェクトにリソースとしてアプリケーション ID を設定する必要があります。また、AndroidManifest.xml にゲームのメタデータを追加する必要もあります。
res/values/ids.xml を開いて、プレースホルダの ID を置き換えます。
手順 4: ゲームをテストする
実機のテストデバイスでゲームを実行する
まとめ
このトレーニングで、Android のサンプル ゲームアプリをインストールし、独自の Android ゲームの作成をすぐに始める方法を理解しました。