プロジェクト

全般

プロフィール

機能 #165

未完了

Androidアプリケーション(検温記録)を古典MVC構造で作成する

高橋 徹 さんが3年以上前に追加. 3年以上前に更新.

ステータス:
フィードバック
優先度:
通常
担当者:
カテゴリ:
Android
対象バージョン:
-
開始日:
2020/08/31
期日:
進捗率:

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の差分を取る等)なので丸ごと変更としている

関連するチケット 2 (2件未完了0件完了)

関連している 機能 #166: Androidアプリケーション(検温記録)を古典MVC構造で作成する(Kotlin編)解決高橋 徹2020/09/12

操作
関連している 機能 #167: Androidアプリケーション(検温記録)をJetPackを用いたMVVM構造で作成する(Kotlin編)解決高橋 徹2020/09/27

操作

高橋 徹 さんが3年以上前に更新

  • 説明 を更新 (差分)
  • ステータス新規 から 進行中 に変更
  • 進捗率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を使う。

高橋 徹 さんが3年以上前に更新

日時と体温のデータベースを管理する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

高橋 徹 さんが3年以上前に更新

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を使う

高橋 徹 さんが3年以上前に更新

高橋 徹 さんが3年以上前に更新

高橋 徹 さんが3年以上前に更新

高橋 徹 さんが3年以上前に更新

  • 関連している 機能 #166: Androidアプリケーション(検温記録)を古典MVC構造で作成する(Kotlin編) を追加

高橋 徹 さんが3年以上前に更新

  • ステータス進行中 から 解決 に変更
  • 進捗率50 から 80 に変更

データベースはinsertとqueryのみ実装し、update、deleteは未実装だが目的は達しているので本チケットは解決とする。

高橋 徹 さんが3年以上前に更新

  • 関連している 機能 #167: Androidアプリケーション(検温記録)をJetPackを用いたMVVM構造で作成する(Kotlin編) を追加

高橋 徹 さんが3年以上前に更新

  • ステータス解決 から フィードバック に変更
  • 進捗率80 から 50 に変更

しばらく使用していると、検温履歴の表示で次の事象が発生するようになった。

  • 順番がID逆順ではなく順不同で表示されることがある
  • 同じ日時の検温が履歴上複数表示されることがある

状況から、onResumeが走るときに生じているように推測

RecyclerViewの使い方を事例調査すると、onResumeでは次の処理を記述する模様

  • Adapterに変更通知(notifyDataSetChanged)
  • RecyclerViewの表示を空にする ⇒ Cursorの場合方法不明

高橋 徹 さんが3年以上前に更新

調査メモ

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チェックを追加した。挙動が変わったが期待とは異なる。うーん。

他の形式にエクスポート: Atom PDF