機能 #165
未完了Androidアプリケーション(検温記録)を古典MVC構造で作成する
50%
説明
目的¶
毎日の体温計測をスマートフォンのメモ機能で記録していたが、記入が結構面倒であった。
また、昨今のAndroidアプリケーション開発事情を把握するため、5年前の古典的なMVC構造と対比したい。そこで、まずMVC構造で作成してみる。
条件¶
- アプリケーション名は、「Temp.Recorder」
- 単独のアプリケーションでデータ永続化機能を持つ
- 開発言語はJava、完成後Kotlin版作成
- ActivityとContentProviderで構成する
- データ永続化はSQLiteを使う
- 開発環境は、Windows 10、Android Studio 4.0.1、APIレベル29
- 古典的MVC版では便利ライブラリは極力排する
- レイアウトイメージ
+---------------------+ |Temp.Recorder | <-- TextView, title of input region | 08.31 07:10 | <-- TextView, date and time to be record | [+10min] [-10min] | <-- Button x 2 wrapped by LinearLayout(Horizontal) | 35 4 | | ---- --- | <-- NumberPicker x 2 +- wrapped by LinearLayout | 36 . 5 [Submit] | <-- Button + (Horizontal) | ---- --- | | 37 6 | |Temp.Record | <-- TextView | 08.30 08:05 36.6 | <-- RecyclerView | 08.29 07:30 36.4 | | : | +---------------------+
完了条件¶
リポジトリにビルド・実行可能なアプリケーションを登録する。
結果¶
ソースコードリポジトリ
source:learn/android/TempRecorderClassic
No. | ファイル名 | ステートメント数 |
---|---|---|
1 | MainActivity.java | 81 |
2 | TempAdapter.java | 66 |
3 | TempProvider.java | 58 |
4 | TempColumns.java | 11 |
5 | TempDbHelper.java | 23 |
課題、残件¶
- NumberPickerはフォントのサイズを指定する属性がない
→ ぐぐると、NumberPickerを継承しサイズを変更する回避方法がちらほら見つかる - RecyclerViewは変更箇所(インデックス番号)を指定すると最小限の表示更新処理をするが、Cursorの場合変更箇所(インデックス)を取るのが面倒(新旧Cursorの差分を取る等)なので丸ごと変更としている
高橋 徹 さんが4年以上前に更新
- 説明 を更新 (差分)
- ステータス を 新規 から 進行中 に変更
- 進捗率 を 0 から 50 に変更
Android Studio で新規プロジェクトを作成、空のActivityとする。
- TempRecordeJavaプロジェクトを新規作成
- MainActivityのデフォルトのレイアウトはConstraintLayoutとなっている
レイアウトに関して
ConstraintLayoutは、Java/SwingのSpringLayout に類似して、上下左右の間隔を定義して配置する方式で、柔軟性が高い。
しかし、設定が複雑になるため、LinearLayout(入れ子を含む)等で実現できるUIであればそちらを使うと簡潔に作成できる。
GridLayout、RelativeLayoutはレガシー扱いとなっている。
今回は、縦にウィジェットを並べるのでLinearLayoutを採用する。
NumberPickerは、Android Studio のレイアウトエディタのパレットにはないので、activity_main.xmlに直接編集で記述する。
NumberPickerの数値範囲はXMLでは設定できず、コードで設定する。そのため、XMLの段階ではlayout_widthが決まらない。そこで、layout_widthはwrap_contextではなく実際の大きさを指定する。
レイアウトエディタでもっともベースとなる(トップレベルの)レイアウトは削除できないので、レイアウト種類を変更するにはレイアウトエディタ上でConstraintLayoutを右クリックし[Convert view] > [LinearLayout]を選択する。
デフォルトでは水平方向となっているので、orientation属性を[vertical]に設定する。
リスト表示の部品は、ListViewがレガシー扱いとなっているため、RecyclerViewを使う。
高橋 徹 さんが4年以上前に更新
- 説明 を更新 (差分)
日時と体温のデータベースを管理するContentProviderを作成する。
ContentProviderは、CONTENT URIを定義する(content://<authority>/<path>/<id>)
authorityは、コンテンツプロバイダのFQCNを全て小文字とした名前とする慣習(com.torutk.temprecorder.tempcontentprovider)
pathはテーブル名
作成するクラスは大よそ次のとおり
BaseColumnsをimplementsしたTempColumnsクラス(URI、テーブル名、列名等の定数定義)
ContentProviderを継承したTempContentProviderクラス
SQLiteOpenHelperを継承したTempDBHelperクラス
BaseColumnsにはID定数フィールドが定義されており値は"_id"となっている。このカラム名はAndroidのライブラリで使用するので、SQLiteのスキーマ上"_id"の名前でPKを用意することが制約事項となる模様。
SQLiteのスキーマは次の想定
テーブル名:Temperatures
ID列:_id INTEGER (PK)
検温日時列:measured_at TEXT
体温列:value REAL
高橋 徹 さんが4年以上前に更新
RecyclerViewの実装が結構大変そうである。
レイアウトファイル、関連クラスの実装が必要
- レイアウトXML
1行のViewのレイアウトを定義 - RecycleView.Adapterの派生クラス
1件のデータを1行のViewに設定 - RecycleView.ViewHolderの派生クラス
1行のView(ウィジェット)参照を保持
activity_main.xmlにRecyclerViewを追加¶
Android Studio 4.0.1のレイアウトエディタに 用意されるパレットにはRecyclerViewがないので、直接XMLに追記する。
追記後は、レイアウトエディタで属性の設定が可能となる。
Containersカテゴリ内にRecyclerViewがあるので、これを画面にドロップする。
idとLayoutManagerが必須設定、LayoutManager属性の欄にはLinearLayoutManagerを直接記載
レイアウトXML¶
1行のViewのレイアウトXMLを作成する。[File]メニュー > [New] > [Layout Resource File] で作成- FileName: item_temperature.xml
- Root element: LinearLayout
LinearLayout のorientationを Horizontal に変更
LinearLayout のlayout_heightを wrap_content に変更(あるいは固定サイズで)
TextViewを2つ配置
両TextView の文字位置を中央、文字種類をmonospace、文字サイズを18sp、layout_marginStartとlayout_marginEndに数値を入れて間隔を開ける
RecycleView.ViewHolderの派生クラスを含むRecyclerView.Adapter派生クラス¶
まず、Adapter派生クラスの雛形を生成した。ViewHolder派生クラスはAdapter派生クラスのネストクラスとして実装するケースが多いのでそれに倣う。
public class TempAdapter extends RecyclerView.Adapter<TempAdapter.ViewHolder> {
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
// 要実装
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
// 要実装
}
@Override
public int getItemCount() {
// 要実装
}
// RecyclerViewに関するViewHolder実装クラス
static class ViewHolder extends RecyclerView.ViewHolder {
TextView dateTimeView;
TextView temperatureView;
ViewHolder(View itemView) {
super(itemView);
dateTimeView = itemView.findViewById(R.id.dateTimeView);
temperatureView = itemView.findViewById(R.id.temperatureView);
}
}
}
ここで、ContentProviderからCursorでデータを取得しRecyclerViewに表示する実装方針を探ってみた。
- 方針1)AdapterがArrayListを保持し、Cursorから取得した(全)データをArrayListに詰めてRecyclerViewに表示する
- データ量が多いとメモリ圧迫、アプリ起動時の時間がかかる等、Cursor使う意義が薄れる
- 方針2)CursorAdapterを使う
高橋 徹 さんが4年以上前に更新
調査メモ
AdapterのswapCursorメソッドに、
notifyItemRangeInsertedを呼ぶコードを入れる? 次URLのコメント参照
https://gist.github.com/skyfishjy/443b7448f59be978bc59
void swapCursor(Cursor newCursor) {
if (cursor == newCursor) {
return;
}
Cursor oldCursor = cursor;
cursor = newCursor;
if (cursor != null) {
idColumnIndex = cursor.getColumnIndexOrThrow(BaseColumns._ID);
int newSize = newCursor.getCount();
int oldSize = oldCursor.getCount();
notifyItemRangeInserted(oldSize, newSize - oldSize);
}
:
実行すると、int oldSize = oldCursor.getCount();
でNullPointerException、初回はcursorは確かにnullだなぁ。
nullチェックを追加した。挙動が変わったが期待とは異なる。うーん。