- 投稿日:2020-05-28T23:32:32+09:00
【Flutter】Flutter製アプリをリリースしてみた、あとARとかも使ってみた話
アプリ概要
ゴルフのオリンピックゲームの計算やボールとピンまでの距離のAR測定,1−18までの乱数を生成することができる、ゴルフ幹事向けのアプリです。
App Store はこちら
Google Play Store はこちら— TeppeiKikuchi??Flutter修行中 (@tpi29) May 28, 2020使用した技術
- bloc
- Provider
- rxDart
- Admob
- Firebase Analytics
- ARKit(iOSのみ)
ソースコードはこちら
https://github.com/Tetsukick/enGolf
AR技術のご紹介
iPhoneのみARKitを活用したプラグイン(arkit_flutter)が公開されていたので、そちらを活用して、タップした2点間の距離を測れるアプリを作成いたしました。
AR距離測定画面のソースコードを記載しておきます。
Sampleを80%ぐらい流用して簡単に開発できました。import 'package:arkit_plugin/arkit_plugin.dart'; import 'package:flutter/material.dart'; import 'package:vector_math/vector_math_64.dart' as vector; import 'dart:io'; class ARMeasureScreen extends StatefulWidget { @override _ARMeasureScreen createState() => _ARMeasureScreen(); } class _ARMeasureScreen extends State<ARMeasureScreen> { ARKitController arkitController; vector.Vector3 lastPosition; @override void dispose() { arkitController?.dispose(); super.dispose(); } @override Widget build(BuildContext context){ return Platform.isIOS ? Scaffold( body: Container( child: ARKitSceneView( enableTapRecognizer: true, onARKitViewCreated: onARKitViewCreated, ), ), floatingActionButton: FloatingActionButton( child: Icon(Icons.delete), backgroundColor: Colors.lightGreen, onPressed: () { arkitController.remove('point'); arkitController.remove('text'); arkitController.remove('line'); lastPosition = null; }, ), ) : Scaffold( body: Center( child: Text('Androidには対応しておりません\n現在開発中のためしばらくお待ち下さい。'), ), ); } void onARKitViewCreated(ARKitController arkitController) { this.arkitController = arkitController; this.arkitController.onARTap = (ar) { final point = ar.firstWhere( (o) => o.type == ARKitHitTestResultType.featurePoint, orElse: () => null, ); if (point != null) { _onARTapHandler(point); } }; } void _onARTapHandler(ARKitTestResult point) { final position = vector.Vector3( point.worldTransform.getColumn(3).x, point.worldTransform.getColumn(3).y, point.worldTransform.getColumn(3).z, ); final material = ARKitMaterial( lightingModelName: ARKitLightingModel.constant, diffuse: ARKitMaterialProperty(color: Colors.blue)); final sphere = ARKitSphere( radius: 0.006, materials: [material], ); final node = ARKitNode( name: 'point', geometry: sphere, position: position, ); arkitController.add(node); if (lastPosition != null) { final line = ARKitLine( fromVector: lastPosition, toVector: position, ); final lineNode = ARKitNode( name: 'line', geometry: line ); arkitController.add(lineNode); final distance = _calculateDistanceBetweenPoints(position, lastPosition); final point = _getMiddleVector(position, lastPosition); _drawText(distance, point); } lastPosition = position; } String _calculateDistanceBetweenPoints(vector.Vector3 A, vector.Vector3 B) { final length = A.distanceTo(B); return '${(length * 100).toStringAsFixed(2)} cm'; } vector.Vector3 _getMiddleVector(vector.Vector3 A, vector.Vector3 B) { return vector.Vector3((A.x + B.x) / 2, (A.y + B.y) / 2, (A.z + B.z) / 2); } void _drawText(String text, vector.Vector3 point) { final textGeometry = ARKitText( text: text, extrusionDepth: 1, materials: [ ARKitMaterial( diffuse: ARKitMaterialProperty(color: Colors.red), ) ], ); const scale = 0.001; final vectorScale = vector.Vector3(scale, scale, scale); final node = ARKitNode( name: 'text', geometry: textGeometry, position: point, scale: vectorScale, ); arkitController.add(node); } }
- 投稿日:2020-05-28T22:56:01+09:00
[Navigation] Single Activity構成時のアナリティクスのスクリーン計測Tips
Navigationコンポーネントを使い、アプリをSingle Activity構成にした際のアナリティクス(Firebase Analytics)のTipsを紹介します。
存在感の強い
MainActivity
アナリティクスを使う場合、Activityは自動で計測されるため、特に意識せずとも画面遷移がトラッキングされ便利です。
https://firebase.google.com/docs/analytics/screenviewsしかしながら、自動計測はActivityのみのため、Navigationコンポーネントを使ったSingle Activity構成の場合に、そのままだと 存在感の強いMainActivity が出来上がってしまいます。
addOnDestinationChangedListener
を使うとシュッと対応できる?存在感の強いMainActivityを無くすために、各Fragmentにトラッキング用の仕組みを入れても良いかと思いますが、Navigationの画面遷移時に呼ばれる
NavController#addOnDestinationChangedListener
を使うことでシュッと対応することができます。サンプルコード
addOnDestinationChangedListener
を使ったサンプルコードです。MainActivity.ktval navController: NavController = ... navController.addOnDestinationChangedListener { _, destination, _ -> // ① 型チェック if (destination is FragmentNavigator.Destination) { // ② Fragmentのクラス名を取得 val screenName = destination.className.split(".").last() // ③ 送信 FirebaseAnalytics.getInstance(this).setCurrentScreen(this, screenName, screenName) } }ポイントを解説します。
① 型チェック
addOnDestinationChangedListener
のdestination
は型がNavDestination
となっており、そのままだとクラス名が取得できません。そこで、
FragmentNavigator.Destination
かどうかをチェックし、スマートキャストでクラス名(className
)を取得できるようにします。ちなみにNavigationでDialogFragmentを使っている場合はこれだと対応出来ないので、
DialogFragmentNavigator.Destination
かどうかのチェックも必要になりそうです。② Fragmentのクラス名を取得
FragmentNavigator.Destination#className
はcom.example.ExampleFragment
のようにパッケージ名も含んでいるため、 雑にsplit(".").last()
とすることでFragmentのクラス名だけを抽出しています。③ 送信
②で抽出した
screenName
をsetCurrentScreen()
で送信します。
setCurrentScreen (Activity activity, String screenName, String screenClassOverride)以上で、Single Activity構成時でも画面遷移時にいい感じにトラッキングができるようになりました。
まとめ
簡単ではありますが、Navigationコンポーネントを使い、アプリをSingle Activity構成にした際のアナリティクスのTipsを紹介しました。
addOnDestinationChangedListener
便利
- 投稿日:2020-05-28T17:49:47+09:00
recyclerViewにステータスバー分のパディングを入れる
最初に
このページはステータスバーを透過させた際にステータスバーのメニューと
リストの内容が被ってしまうことがあるため備忘録として記させていただきます実装方法
MainFragment.ktoverride fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) presenter.onViewCreated() recyclerView.requestApplyInsets() recyclerView.setOnApplyWindowInsetsListener{v, insets -> v.setPadding(0,insets.systemWindowInsetTop,0,0) return@setOnApplyWindowInsetsListener insets } }setOnApplyInstesListener内のinsets.systemWindowInsetTopでステータスバー分の高さを取得してパディングに設定します
ステータスバー分下げるだけの場合はsetOnApplyWindowInsetsListener内のみの
処理で十分ですが、ここで一つ注意していただきたいのがタブなどでフラグメントを
制御している場合、遷移後にsetOnApplyWindowInsetsListener内に処理が入ってくれずパディングが設定されないことがあるので
requestApplyInsets()を書いてあげることにより一度リセットをしてくれるため
再度中身の処理に入ってくれます最後に
前回ステータスバーの透過方法を書かせていただいたのですが仕事で作っている
アプリでrecyclerViewに実装した際にパディングが実装されない不具合に直面した
ため書かせていただきました
- 投稿日:2020-05-28T15:49:23+09:00
Android App Bundleをコマンドラインで署名して生成する方法
はじめに
初めてAndroidアプリのリリースビルドを行ったのですが、いくつかつまづいたところがあったので、体系的にまとめることにします。
Android Studioで行うとかんたんだと公式ドキュメントに記載されていますが、今後CD環境を構築することを考慮し、コマンドラインでビルドすることにしました。
「Android App Bundle」とは?
拡張子が(
.aab
)であることもあり、本記事では「AAB」と略します。アプリのコードとリソースをモジュールにまとめる署名付きのバイナリファイルです。
AABをPlay Consoleにアップロードすることで、ユーザーに提供するさまざまなAPKを自動で生成します。Android Studio 3.2 以降で使えます。
AABのメリット
従来のAPK形式と比べ、以下のメリットがあります。
メリットは他にもあります。詳細は公式ドキュメントをご参照ください。
https://developer.android.com/platform/technology/app-bundle?hl=jaAPKを効率的に管理できる
1つのAABからさまざまなAPKを生成できるため、APKを1つずつ署名やアップロードする必要がなくなります。
アプリのサイズが小さくなる
Google PlayのDynamic DeliveryはAABを使い、各デバイスの設定に合わせて最適化したAPKをビルドして配信します。
ユーザーは自分のデバイスでのみ使われるコードとリソースのみダウンロードされるようになり、アプリのサイズが小さくなります。ビルド時間が短縮される
Gradleなどによるビルドは、モジュール式アプリ向けに最適化されているため、大規模なモノリシックアプリに比べてビルド時間を大幅に短縮できます。
環境
- OS:macOS Mojave 10.14.6
- Android Studio: 3.6.1
- Kotlin:1.3.72
- Gradle:6.3
- Gradle plugin:3.6.3
AABの生成手順
AABの生成手順を説明します。
秘密鍵の生成
まず、署名に使う「Keystore(キーストア)」と呼ばれる秘密鍵を生成します。
公式ドキュメントに記載されている通りのコマンドを実行します。
https://developer.android.com/studio/build/building-cmdline?hl=ja$ keytool -genkey -v -keystore {Keystore name} -keyalg RSA -keysize 2048 -validity 10000 -alias {Alias name}私はDroidKaigi 2020のアプリを参考に、キーストア名を
release.keystore
、エイリアス名を対象アプリ名(私の場合はuhooipicbook
)にしました。
https://github.com/DroidKaigi/conference-app-2020/blob/master/android-base/build.gradle.kts#L34
https://github.com/DroidKaigi/conference-app-2020/blob/master/android-base/build.gradle.kts#L46$ keytool -genkey -v -keystore release.keystore -keyalg RSA -keysize 2048 -validity 10000 -alias uhooipicbook秘密鍵はウィザード形式で生成します。
キーストアのパスワードを入力してください:
キーストアのパスワードを入力します。
後ほど使うので、忘れないようにしてください。新規パスワードを再入力してください:
確認のため、キーストアのパスワードを再度入力します。
姓名は何ですか。
自分の名前をフルネームで入力します。
私はuhooi
と入力しました。組織単位名は何ですか。
所属組織の単位名を入力します。
私は個人開発なのでpersonal
と入力しました。組織名は何ですか。
組織の名前を入力します。
私はTHE Uhooi
と入力しました。都市名または地域名は何ですか。
都市名または地域名を入力します。
私はSetagaya-ku
と入力しました。都道府県名または州名は何ですか。
日本の場合、都道府県名を入力します。
私はTokyo
と入力しました。この単位に該当する2文字の国コードは何ですか。
国コードを入力します。
私はJP
と入力しました。CN=uhooi, OU=personal, O=THE Uhooi, L=Setagaya-ku, ST=Tokyo, C=JPでよろしいですか。
確認のため、今まで入力した内容が表示されます。
修正する場合はEnterキーを押下し、問題ない場合はy
を入力します。10,000日間有効な2,048ビットのRSAの鍵ペアと自己署名型証明書(SHA256withRSA)を生成しています ディレクトリ名: CN=uhooi, OU=personal, O=THE Uhooi, L=Setagaya-ku, ST=Tokyo, C=JP <uhooipicbook>の鍵パスワードを入力してください (キーストアのパスワードと同じ場合はRETURNを押してください):鍵のパスワードを入力します。
こちらも後ほど使うので、忘れないようにしてください。新規パスワードを再入力してください:
確認のため、鍵のパスワードを再度入力します。
[release.keystoreを格納中] Warning: JKSキーストアは独自の形式を使用しています。"keytool -importkeystore -srckeystore release.keystore -destkeystore release.keystore -deststoretype pkcs12"を使用する業界標準の形式であるPKCS12に移行することをお薦めします。
こちらの警告が気になりますが、私は無視してしまいました。対応方法をご存じの方は教えていただけると嬉しいです。
これで現在のフォルダに
release.keystore
が存在したら秘密鍵の生成は完了です。秘密鍵の格納
生成した秘密鍵を対象モジュールのルートフォルダに格納します。
私の場合、app
フォルダ直下に格納しました。
秘密鍵をバージョン管理から無視
秘密鍵は外部に漏れてはいけないため、パブリックリポジトリで開発している場合はバージョン管理の対象外にします。
Gitを使っている場合、以下を「.gitignore」に追加するのみでOKです。
.gitignore+ *.keystore
デバッグ用の秘密鍵をコミットしたい場合は、ワイルドカードで指定した上で例外として定義すると安全です。
.gitignore*.keystore !app/debug.keystoreDroidKaigi 2020のアプリを参考にしました。
https://github.com/DroidKaigi/conference-app-2020/blob/master/.gitignore#L250-L251
build.gradle
に署名の設定を追加モジュールレベルの
build.gradle
に署名の設定を追加します。プライベートリポジトリの場合、キーストアと鍵のパスワードを直接記述してもいいと思います。
パブリックリポジトリの場合はパスワードが流出したら問題なので、何らかの方法で隠蔽する必要があります。
私はDroidKaigi 2020のアプリを参考に、環境変数から取得するようにしました。
https://github.com/DroidKaigi/conference-app-2020/blob/master/android-base/build.gradle.kts#L45
https://github.com/DroidKaigi/conference-app-2020/blob/master/android-base/build.gradle.kts#L47/app/build.gradleandroid { + signingConfigs { + release { + storeFile file("release.keystore") + storePassword System.getenv("RELEASE_KEYSTORE_STORE_PASSWORD") + keyAlias "uhooipicbook" + keyPassword System.getenv("RELEASE_KEYSTORE_KEY_PASSWORD") + } + } buildTypes { release { + signingConfig signingConfigs.release } } }パスワードの隠蔽方法は環境変数を使う以外にもあり、公式ドキュメントでは
keystore.properties
を使う方法が紹介されています。
https://developer.android.com/studio/publish/app-signing?hl=ja#secure-shared-keystoreAABの生成
ここまでで設定は完了です。
あとはキーストアと鍵のパスワードをエクスポートし、対象モジュールで
bundle{Variant}
タスクを実行することで、AABが生成されます。
私の場合、app
モジュールのRelease
バリアントなので、以下のコマンドを実行します。$ export RELEASE_KEYSTORE_STORE_PASSWORD={キーストアのパスワード} $ export RELEASE_KEYSTORE_KEY_PASSWORD={鍵のパスワード} $ ./gradlew :app:bundleReleaseビルドに成功すると、
{Module name}/build/outputs/bundle/{Variant}
フォルダに{Module name}-{Variant}.aab
が生成されます。
私の場合、app/build/outputs/bundle/release
フォルダにapp-release.aab
が生成されました。エラー:キーストアファイルが対象フォルダにない
以下のエラーが発生する場合、キーストアのファイルが対象モジュールのルートフォルダに存在しません。
配置しているか確認してください。> Keystore file '.../{Project name}/app/release.keystore' not found for signing config 'release'.エラー:キーストアまたは鍵のパスワードを設定していない
以下のエラーが発生する場合、キーストアまたは鍵のパスワードの環境変数をエクスポートしていません。
環境変数を使う場合は忘れずにエクスポートしてください。* What went wrong: Execution failed for task ':app:signReleaseBundle'. > A failure occurred while executing http://com.android.build.gradle.internal.tasks.Workers$ActionFacade > kotlin.KotlinNullPointerException (no error message)おわりに
これでコマンドラインでAABを生成することができました!
あとはPlay Consoleにアップロードすることで、テストやリリースを行えます。参考リンク
- 投稿日:2020-05-28T13:28:02+09:00
ローカライズファイルをAndroidとiOSで共通管理する
はじめに
AndroidとiOSでサービスを展開している場合、同じ文言を使っている。
プラットフォームごとに管理することで文言の誤字が発生しやすく、コストも上がる。
ローカライズファイルを効率よく管理する方法を考えました前提条件
- AndroidとiOSで共通のリソースを扱う
- 差分を追いやすいこと
githubを使って管理
- オリジナルデータとなる文言とkeyはjsonにして管理
- githubを使うことで差分を追いやすくなる
- CIを使うことで自動化できる
ローカライズファイルを更新するツール
- JSONからローカライズファイルを生成するのを簡単にするためにツールを作成した
- t-osawa-009/JSONToString
使い方
1.オリジナルデータとなるJSONファイルを追加
[ { "key": "hoge", "key_android": "hoge", "key_ios": "hoge", "value_android": "hoge", "value_ios": "hoge" }, ]2.ローカライズファイルの生成を設定する
.JSONToString.yml
ファイルを追加outputs: - key: key value_key: value_ios output: Strings/Localizable.strings format: strings sort: asc - key: key value_key: value_android output: Strings/strings.xml format: xml3.コマンドを実行
$ JSONToString --json_path Strings.json4.生成したファイルを各プロジェクトに追加する
"hoge" = "hoge";<?xml version="1.0" encoding="utf-8"?> <resources> <string name="hoge">hoge</string> </resources>まとめ
- githubで管理することで1元管理が可能になった
- 運用を考えて、ツールを自作するのは有効である
参考リンク
- 投稿日:2020-05-28T12:35:39+09:00
Androidアプリが非SDKインターフェースを使用していないか調べる方法
分かりやすい方法としては、対象のOSをエミュレーターで起動しアプリのすべての操作を実施して以下のようなログが表示されていないか確認する方法がある。
この方法であれば発生箇所がある程度予測できるし、非SDKを使用してはならないなどの要件にも対応しやすい。Accessing hidden field Landroid/os/Message;->flags:I (light greylist, JNI)
- 投稿日:2020-05-28T11:46:36+09:00
AndroidStudioで可変リスト表示を作る(前編)
AndroidStudioで可変リスト表示を作る(前編)
リスト表示を作ってみましょう
Android アンドロイド Java ジャバ Kotlin コトリン こういうリスト表示を作ってみます。
表示が固定ではなくボタンを押すと追加されるように作ります。画面表示に使用する文字列を登録してしまいます。
strings.xml<?xml version="1.0" encoding="utf-8" ?> <resources> <string name="app_name">QiitaHelloSample</string> <string name="btAdd">追加</string> <string name="btDel">削除</string> <string name="etHint">追加する値</string> </resources>ListViewと追加ボタン、削除ボタンを並べたいのでレイアウトを工夫します。
activity_main.xmlに元からあるTextViewは消してしまいましょう。
スッキリしたところでデザインからLinearLayout(vertical)をドラッグして配置します。
ドラッグ先はレイアウトの画面では無く下のコンポーネントツリーにドラッグしていきます。
1.LinearLayout(vertical)
2.LinearLayout(horizontal)
3.ListView
4.Button を 2つ
5.EditTextLinerLayoutコンポーネントはvertical(縦)とhorizontal(横)がそれぞれ独立しているように見えますが実は同じでorientationの値が異なるだけです。
コンポーネントツリーのコンポーネントをドラッグして
置けたらidをそれぞれ lvMain、btAdd、btDelと付けます。
このようにします。
上下で位置の入れ替え、左右でツリーの階層が変わります。1.LinearLayout(vertical)は
LinearLayout(vertical).xml<LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent">2.LinearLayout(horizontal)は
LinearLayout(horizontal).xml<LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal">3.ListViewは
ListView.xml<ListView android:id="@+id/lvMain" android:layout_width="wrap_content" android:layout_height="match_parent"> </ListView>4.Buttonは
Button.xml<Button android:id="@+id/btAdd" style="@style/Widget.AppCompat.Button" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:text="@string/btAdd" /> <Button android:id="@+id/btDel" style="@style/Widget.AppCompat.Button" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:text="@string/btDel" />5.EditTextは
EditText.xml<EditText android:id="@+id/etValue" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:autofillHints="password" android:ems="10" android:hint="@string/etHint" android:inputType="text" />ButtonやTextEditのlayout_weightは表示割合ですので追加ボタンと削除ボタンとテキスト入力の表示割合は1:1:1となります。値を色々変えてどのように変わるか試しましょう。
ListViewがデザイン時に配置しづらい
他のコンポーネントは置いた状態ですぐに表示されますが、ListViewは値が無いため配置がしづらく感じます。わざわざ値を用意するのも大変なのでそんな場合はListView選択状態でentriesの値を入力する所に「@」を入力すると最初から定義されている文字列リスト候補が表示されますので適当に採用します。
デザインが終わったら空白にすれば元に戻せます。独自デザインのListViewを使う
ListViewには簡単に使える方法が用意していますがそれだとデザインが固定されます。1行に3つ以上の値を表示しようとすると用意されているものは使えませんので、ここでは独自にデザインすることが出来る方法を使います。
プロジェクト構成ツリーからres -> layoutの中にはメイン画面のレイアウトしかない状態ですがここにレイアウトを追加します。マウスの右クリックから新規 -> レイアウトリソースファイルを選択し
ファイル名を row.xml ※行をデザインしていますよという意味
ルート要素を LinearLayoutとしてOKすると
row.xml<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> </LinearLayout>1行分のレイアウトのベースとなるxmlファイルが作られます。
これをrow.xml<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/tvDateTime" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginLeft="8dp" android:layout_marginTop="8dp" android:textSize="18sp"/> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginBottom="8dp" android:layout_marginLeft="8dp" android:orientation="horizontal"> <TextView android:id="@+id/tvMsg" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="14sp"/> </LinearLayout> </LinearLayout>このように変更します。デザインの説明は割愛してTextViewの2箇所にidが割り当てられています。
ここのリストとして表示させる文字列を割り当ててリストにするわけです。ListViewデザインを確認
row.xmlをデザイン表示しても値が無いので何も表示されません。
1行分がどんな表示になるのかを確認する場合はTextViewのtextに試験的に文字を入れてしまいましょう。BaseAdapterを継承してListViewを作る
10行分表示する独自ListViewを作ってみましょう。
独自のListViewを作るためにBaseAdapterを継承したCustomListViewクラスを定義します。
BaseAdapterには継承しないといけないgetItem、getItemId、getItemId、getCountメソッドがあるので定義します。
今回は10行分のリストにするのでgetCountの返り値を10に、getItemId、getItemIdはまだ使いませんので適当な値を返します。getView
getViewはリスト表示が必要になった時に発生するイベントです。
p0に表示する行数が渡されます。MainActivity.ktclass CustomListView(private val context: Activity): BaseAdapter() { override fun getView(p0: Int, p1: View?, p2: ViewGroup?): View { val inflater = context.layoutInflater val view1 = inflater.inflate(R.layout.row,null) var fMsg = view1.findViewById<TextView>(R.id.tvMsg) // row.xmlレイアウトのtvMsgを参照 fMsg.setText("Msg = $p0") // tvMsgのtextに 引数 p0を返す return view1 } override fun getItem(position: Int): Any { return "" // p0 にあるデータを返す※今回は未使用 } override fun getItemId(position: Int): Long { return position.toLong() // 識別するためのidを返す } override fun getCount(): Int { return 10 // 一覧の件数 } }作ったCustomListViewクラスを宣言してListViewが持つadapterに渡してあげると独自クラスが表示されます。
MainActivity.ktclass MainActivity : AppCompatActivity() { val customAdaptor= CustomListView(this@MainActivity) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) var listView = findViewById<ListView>(R.id.lvMain) listView.adapter = customAdaptor } }たったこれだけのことで簡単に独自クラスを作る事が出来ます。
可変リストを作る(後編)
ListView用に使用する可変コレクションリストを作ってみます。
mutableMapOfを使って見る
まずはコレクションを作ってみます。
map.ktprivate var map = mutableMapOf<String,Any>()このように定義しているとキーを文字列、値の型は自由に決めることが出来ます。
定義したmapに値を与えます。
map.ktmap = mutableMapOf("key" to "Jan","eng" to "January")
キー String 値 Any key jan eng January 要素の数に制限はありません。
map.ktmap = mutableMapOf("key" to "Jan","eng" to "January","jp" to "ジャニュアリー","id" to 1)
キー String 値 Any key jan eng January jp ジャニュアリー id 1 mutableMapOfを使うとキーと値の組み合わせでデータを管理することが出来ますが、表計算で言うところの横方向のデータだけの管理となります。
これを縦方向にも使おうとするとMutableListを使うことになります。MutableListを使う
map.ktprivate var map = mutableMapOf<String,Any>() private var mapList : MutableList<MutableMap<String,Any>> = mutableListOf()MutableList<MutableMapの後ろに続くは mutableMapOfを定義したものと同じものを定義します。
= mutableListOf()はMutableListの初期化をしています。
ではさっそく使って見ます。map.ktmap = mutableMapOf("key" to "Jan","eng" to "January","jp" to "ジャニュアリー","id" to 1) mapList.add(map) map = mutableMapOf("key" to "Feb","eng" to "February","jp" to "フェブラリー","id" to 2) mapList.add(map)mapListは
index key eng jp id 0 jan January ジャニュアリー 1 1 Feb February フェブラリー 2 このような中身になっています。
Januaryを取り出そうと思うと
mapList[0].get("eng")と書きます。
フェブラリーを取り出そうと思うと
mapList[1].get("jp")と書きます。CustomListView.ktprivate var mapList:MutableList<MutableMap<String,Any>> = mutableListOf() private var map = mutableMapOf<String,Any>()定義したクラス内でAddメソッドとDelメソッドを作ります。
CustomListView.ktfun Add(msg : String){ map = mutableMapOf("msg" to msg) mapList.add(map) } fun Del(idx : Int){ mapList.removeAt(idx) }前編で作ったCustomListViewに組み込むと
CustomListView.ktclass CustomListView(private val context: Activity): BaseAdapter() { private var mapList:MutableList<MutableMap<String,Any>> = mutableListOf() private var map = mutableMapOf<String,Any>() override fun getView(p0: Int, p1: View?, p2: ViewGroup?): View { val inflater = context.layoutInflater val view1 = inflater.inflate(R.layout.row,null) var fMsg = view1.findViewById<TextView>(R.id.tvMsg) // row.xmlレイアウトのtvMsgを参照 fMsg.setText(mapList[p0].get("msg").toString()) // tvMsgのtextに 引数 p0を返す return view1 } override fun getItem(position: Int): Any { return "" // p0 にあるデータを返す※今回は未使用 } override fun getItemId(position: Int): Long { return position.toLong() // 識別するためのidを返す } override fun getCount(): Int { return mapList.count() // 一覧の件数 } fun Add(msg : String){ map = mutableMapOf("msg" to msg) mapList.add(map) } fun Del(idx : Int){ mapList.removeAt(idx) } }これをメインアクティビティに実装してボタンを押されたときの処理も作ります。
MainActivity.ktclass MainActivity : AppCompatActivity() { val customAdaptor= CustomListView(this@MainActivity) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) Show() } fun onButtonAdd(view : View){ val et = findViewById<EditText>(R.id.etValue) val msg = et.text.toString() customAdaptor.Add(msg) Show() } fun onButtonDel(view : View){ customAdaptor.Del(0) Show() } fun Show(){ var listView = findViewById<ListView>(R.id.lvMain) listView.adapter = customAdaptor } }CustomListViewでうまくカプセル化できたのでメインの方のコード量は非常に少なくなりました。
リストに日時を追加
追加ボタンを押されたときの日時を表示して見ましょう。
CustomListView.ktfun Add(msg : String){ val dt = LocalDateTime.now() map = mutableMapOf("msg" to msg,"dt" to dt) mapList.add(map) }Addメソッドに処理を追加します。
変数 dtに現在日時を代入します。
コレクションとして キー dt にその値を代入します。メインの方に日時を表示する処理を追加します。
MainActivity.ktoverride fun getView(p0: Int, p1: View?, p2: ViewGroup?): View { val inflater = context.layoutInflater val view1 = inflater.inflate(R.layout.row,null) var fMsg = view1.findViewById<TextView>(R.id.tvMsg) // row.xmlレイアウトのtvMsgを参照 var fDt = view1.findViewById<TextView>(R.id.tvDateTime) fMsg.setText(mapList[p0].get("msg").toString()) // tvMsgのtextに 引数 p0を返す fDt.setText(mapList[p0].get("dt").toString()) return view1 }たったこれだけの追加ですが日時が表示されるようになりました。
でも日時の表示が日本風ではありません。MainAcivity.ktoverride fun getView(p0: Int, p1: View?, p2: ViewGroup?): View { val inflater = context.layoutInflater val view1 = inflater.inflate(R.layout.row,null) var fMsg = view1.findViewById<TextView>(R.id.tvMsg) // row.xmlレイアウトのtvMsgを参照 var fDt = view1.findViewById<TextView>(R.id.tvDateTime) val formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss") val s = mapList[p0].get("dt").toString() val dt = LocalDateTime.parse(s) val dts = dt.format(formatter) fMsg.setText(mapList[p0].get("msg").toString()) // tvMsgのtextに 引数 p0を返す fDt.setText(dts) // tvMsgのtextに 引数 p0を返す return view1 }文字列を日時型に変換したりさらにそれを日時の形式を指定した変換になっているので処理が複雑になりましたがこれで理想的な表示になりました。