あかんわ

覚えたことをブログに書くようにすれば多少はやる気が出るかと思ったんです

IntelliJ IDEAでScala on Android using sbt Part.5[カレンダーアプリ]後編

Scala on Androidの練習に、簡単なカレンダーアプリを書きました。

後編には、予定の保存先であるSQLiteデータベースの操作に使用するContentProviderのコードを記載しています。
前編では、アプリを構成する3つの画面(カレンダーを表示する画面・予定表を表示する画面・予定を編集する画面)のコードを記載しています。

IDEIntelliJ IDEA CE 2016.2で、ビルドツールはsbt 0.13.12を使っています。
動作確認は、API level 23のAndroidエミュレータで実施しました。

目次

Scala on Androidでカレンダーアプリ

月間カレンダーを表示して、日にちごとの予定を登録できるだけのアプリです。

ソースコードGitHubに置いてますので、動かしてみる場合はこちらの記事を参考にして下さい。

プロジェクトディレクトリの構成
~/CaledarApp
    |- build.sbt                               // sbtのビルド定義ファイル
    |- /project                                // sbt関連の設定ファイルを配置するディレクトリ
    |- /src                                    // アプリのモジュールを構成するディレクトリ
    |    |- /main                              // アプリのメインソースセットを配置するディレクトリ
    |    |    |- AndroidManifest.xml           // アプリに関する情報を記述するマニフェストファイル
    |    |    |- /libs                         // 外部ライブラリを配置するディレクトリ
    |    |    |- /res                          // リソースファイルを配置するディレクトリ
    |    |    |    |- /drawable-mdpi           // 中解像度の画像を配置するディレクトリ
    |    |    |    |- /drawable-hdpi           // 高解像度の画像を配置するディレクトリ
    |    |    |    |- /drawable-xhdpi          // 超高解像度の画像を配置するディレクトリ
    |    |    |    |    |- ic_launcher.png     // アプリのアイコン画像
    |    |    |    |- /drawable-xxhdpi         // 超超高解像度の画像を配置するディレクトリ
    |    |    |    |- /drawable-xxxhdpi        // 超超超高解像度の画像を配置するディレクトリ
    |    |    |    |- /layout                  // レイアウトの定義ファイルを配置するディレクトリ
    |    |    |    |    |- activity_main.xml             // 通常画面のレイアウト定義ファイル
    |    |    |    |    |- activity_scheudle.xml         // 予定表画面のレイアウト定義ファイル
    |    |    |    |    |- listview_scheudlelistitem.xml // 予定表画面のListView内のレイアウト定義ファイル
    |    |    |    |    |- activity_editscheudle.xml     // 予定編集画面のレイアウト定義ファイル
    |    |    |    |- /values                  // 配色や文字の定義ファイルを配置するディレクトリ
    |    |    |                                
    |    |    |- /scala                        // Scalaのコードを配置するディレクトリ
    |    |         |- /com.b0npu.calendarapp             // アプリのパッケージディレクトリ
    |    |              |- CalendarActivity.scala        // アプリのメインファイル
    |    |              |- ScheduleActivity.scala        // 予定表を表示するクラス
    |    |              |- EditScheduleActivity.scala    // 予定の編集と保存のためのクラス
    |    |              |- ScheduleContentProvider.scala // 予定表のSQLiteデータベースを操作するクラス
    |    |              |- ScheduleDB.scala              // 予定表のデータベース情報を管理するオブジェクト
    |    |                                
    |    |- /test                              // テストコードを配置するディレクトリ
    |                             
    |- /target                                 // ビルドで生成された成果物の出力先ディレクトリ
主なソースコード

カレンダーアプリへの予定の登録は、ContentProviderクラスを継承したScheduleContentProvider.scalaからSQLiteデータベースへ保存します。
SQLiteデータベースの操作は全て、ContentProviderであるScheduleContentProviderを経由しますので、SQLiteデータベースの作成を管理するSQLiteOpenHelperを継承したScheduleDBOpenHelperScheduleContentProvider.scalaでインナークラスとして定義しています。

また、SQLiteデータベースの変更をScheduleActivity.scalaでの予定表の表示に即時に反映させるため、queryメソッドでScheduleContentProviderのContent URICursorLoaderの監視対象のURIに登録し、insertupdatedeleteメソッドでSQLiteデータベースの変更をCursorLoaderに通知しています。

ScheduleContentProviderのContent URIは、AndroidManifest.xmlに追加したproviderの要素で設定しています。

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest
        xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.b0npu.calendarapp">

    <application
            android:icon="@drawable/ic_launcher"
            android:label="@string/app_name"
            android:theme="@style/AppTheme">

        <activity android:name=".CalendarActivity">

            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>

        <activity android:name=".ScheduleActivity"
                  android:label="@string/app_name">
        </activity>

        <activity android:name=".EditScheduleActivity"
                  android:label="@string/app_name">
        </activity>

        <provider android:name=".ScheduleContentProvider"
                  android:authorities="com.b0npu.calendarapp.ScheduleContentProvider">
        </provider>

    </application>

</manifest>
ScheduleDB.scala
package com.b0npu.calendarapp

import android.net.Uri

/**
  * 予定表のデータベース情報を定義するオブジェクト
  *
  * 予定表のSQLiteデータベースでテーブル名やカラム名といった
  * アプリ内でグローバルに扱いたい一意の値を定義する
  */
object ScheduleDB {

  /* SQLiteデータベースのテーブル名とカラム名  */
  val TABLE = "schedule_table"
  val ID = "_id"
  val CONTENT = "schedule_content"
  val DATE = "schedule_date"
  val TIME = "schedule_time"

  /* AndroidManifestに定義したContentProviderのURI */
  val CONTENT_URI: Uri = Uri.parse("content://com.b0npu.calendarapp.ScheduleContentProvider")
}
ScheduleContentProvider.scala
package com.b0npu.calendarapp

import android.content.{ContentProvider, ContentUris, ContentValues, Context}
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase.CursorFactory
import android.database.sqlite.{SQLiteDatabase, SQLiteOpenHelper}
import android.net.Uri

/**
  * 予定表のデータベースを扱うためのContentProviderのクラス
  *
  * 予定表のSQLiteデータベースに問い合わせ・追加・更新・削除を行う
  * SQLiteデータベースのテーブルはSQLiteOpenHelperクラスを継承した
  * インナークラスのScheduleDBOpenHelperで作成する
  */
class ScheduleContentProvider extends ContentProvider {

  /**
    * フィールドの定義
    *
    * インナークラスのScheduleDBOpenHelperを扱うための変数を定義する
    * (自クラスで使うだけのフィールドはprivateにして明示的に非公開にしてます)
    */
  private var scheduleDBOpenHelper: ScheduleDBOpenHelper = _

  /**
    * onCreateメソッドをオーバーライド
    *
    * このメソッドはContentProviderが読み込まれた際に初期化するメソッドで
    * ContentProviderが正常に読み込まれればTrueを失敗すればFalseを返す
    * インナークラスのScheduleDBOpenHelperにSQLiteデータベースのファイル名を
    * 渡してSQLiteデータベースを作成する
    * 既に同じファイル名のSQLiteデータベースがある場合は開いて使用する
    */
  override def onCreate(): Boolean = {
    /* SQLiteデータベースとしてschedule_table.sqliteファイルを作成する(既にあれば開く)  */
    scheduleDBOpenHelper = new ScheduleDBOpenHelper(getContext, s"${ScheduleDB.TABLE}.sqlite", null, 1)
    true
  }

  /**
    * queryメソッドをオーバーライド
    *
    * このメソッドはSQLiteデータベースへの問い合わせのためのメソッドで
    * データベースを検索した結果をCursorに格納して返す
    * selectionで指定されたschedule_tableのカラムをsortOrderの順にCursorに格納する
    * CursorLoaderでデータベースの変更をアプリに即時反映させるため
    * setNotificationUriでデータベースのURIを監視対象として登録する
    */
  override def query(uri: Uri, projection: Array[String], selection: String, selectionArgs: Array[String], sortOrder: String): Cursor = {
    /* データベースを読み出し専用で開いて問い合わせの内容を検索する */
    val scheduleSQLiteDB: SQLiteDatabase = scheduleDBOpenHelper.getReadableDatabase
    val scheduleContentCursor: Cursor = scheduleSQLiteDB.query(ScheduleDB.TABLE, projection, selection, selectionArgs, null, null, sortOrder)

    /* データベースの変更をCursorLoaderに通知するためURIを登録する */
    scheduleContentCursor.setNotificationUri(getContext.getContentResolver, uri)

    /* 検索結果を格納したCursorを返す */
    scheduleContentCursor
  }

  /**
    * insertメソッドをオーバーライド
    *
    * このメソッドはContentProviderにデータを追加するメソッドで
    * 追加されたデータのURIを返す
    * CursorLoaderでデータベースの変更をアプリに即時反映させるため
    * notifyChangeでデータベースへの追加をCursorLoaderに通知する
    */
  override def insert(uri: Uri, contentValues: ContentValues): Uri = {
    /* データベースを書き込み可能な状態で開いてデータを追加する */
    val scheduleSQLiteDB: SQLiteDatabase = scheduleDBOpenHelper.getWritableDatabase
    val newContentId: Long = scheduleSQLiteDB.insert(ScheduleDB.TABLE, null, contentValues)
    val newContentUri: Uri = ContentUris.withAppendedId(uri, newContentId)

    /* データベースへの追加をCursorLoaderに通知する */
    getContext.getContentResolver.notifyChange(newContentUri, null)

    /* 追加されたデータのURIを返す */
    newContentUri
  }

  /**
    * updateメソッドをオーバーライド
    *
    * このメソッドはContentProviderにデータを更新するメソッドで
    * データが更新された列の数を返す
    * CursorLoaderでデータベースの変更をアプリに即時反映させるため
    * notifyChangeでデータベースへの更新をCursorLoaderに通知する
    */
  override def update(uri: Uri, contentValues: ContentValues, selection: String, selectionArgs: Array[String]): Int = {
    /* データベースを書き込み可能な状態で開いてデータを更新する */
    val scheduleSQLiteDB: SQLiteDatabase = scheduleDBOpenHelper.getWritableDatabase
    val updatedRowNum: Int = scheduleSQLiteDB.update(ScheduleDB.TABLE, contentValues, selection, selectionArgs)

    /* データベースの更新をCursorLoaderに通知する */
    getContext.getContentResolver.notifyChange(uri, null)

    /* 更新された列の数を返す */
    updatedRowNum
  }

  /**
    * deleteメソッドをオーバーライド
    *
    * このメソッドはContentProviderからデータを削除するメソッドで
    * 削除された列の数を返す
    * CursorLoaderでデータベースの変更をアプリに即時反映させるため
    * notifyChangeでデータベースからの削除をCursorLoaderに通知する
    */
  override def delete(uri: Uri, selection: String, selectionArgs: Array[String]): Int = {
    /* データベースを書き込み可能な状態で開いてデータを削除する */
    val scheduleSQLiteDB: SQLiteDatabase = scheduleDBOpenHelper.getWritableDatabase
    val deletedRowNum: Int = scheduleSQLiteDB.delete(ScheduleDB.TABLE, selection, selectionArgs)

    /* データベースからの削除をCursorLoaderに通知する */
    getContext.getContentResolver.notifyChange(uri, null)

    /* 削除された列の数を返す */
    deletedRowNum
  }

  /**
    * getTypeメソッドをオーバーライド
    *
    * このメソッドはContentProviderからデータのタイプを取得するメソッドで
    * 与えられたURIに格納されているデータのMIMEタイプを返す
    * 予定表のSQLiteデータベースでは使用しないのでnullを返しておく
    */
  override def getType(uri: Uri): String = {
    null
  }

  /**
    * SQLiteデータベースを管理するヘルパークラス
    *
    * 予定表を保存するSQLiteデータベースの作成とバージョンを管理する
    * SQLiteデータベースの作成と操作はContentProviderを経由するので
    * ContentProviderのインナークラスとして定義する
    */
  class ScheduleDBOpenHelper(context: Context, name: String, factory: CursorFactory, version: Int)
    extends SQLiteOpenHelper(context, name, factory, version) {

    /**
      * SQLiteOpenHelperのonCreateメソッドをオーバーライド
      *
      * このメソッドはSQLiteデータベースの作成時に呼ばれるメソッドで
      * データベースにテーブルが存在しない場合にテーブルを作成する
      * 予定の内容・日付・時間を保存する予定表テーブルを作成する
      */
    override def onCreate(sqLiteDatabase: SQLiteDatabase): Unit = {
      /* SQLiteデータベースにテーブルを作成するSQLステートメントを実行する */
      sqLiteDatabase.execSQL(
        s""" CREATE TABLE ${ScheduleDB.TABLE} (
               ${ScheduleDB.ID} INTEGER PRIMARY KEY AUTOINCREMENT,
               ${ScheduleDB.CONTENT} TEXT,
               ${ScheduleDB.DATE} TEXT,
               ${ScheduleDB.TIME} TEXT);
         """
      )
    }

    /**
      * SQLiteOpenHelperのonUpgradeメソッドをオーバーライド
      *
      * このメソッドはSQLiteデータベースをアップグレードする際に呼ばれるメソッドで
      * データベースの構造を新しくする必要がある際に使用する
      * とりあえず古いテーブルを削除してテーブルを作り直す
      */
    override def onUpgrade(sqLiteDatabase: SQLiteDatabase, oldVersion: Int, newVersion: Int): Unit = {
      /* SQLiteデータベースに存在するテーブルを削除して新しくテーブルを作る */
      sqLiteDatabase.execSQL(s"DROP TABLE IF EXISTS ${ScheduleDB.TABLE}")
      onCreate(sqLiteDatabase)
    }
  }

}
Run/Debugの実行結果

うまくいけば、アプリらしいアプリの出来上がりに嬉しくなります。 f:id:b0npu:20161203145206g:plain

参考記事

ContentProviderやCursorLoaderの利用は、こちらの記事を参考にさせていただきました。

開発環境

関連記事