IntelliJ IDEAでScala on Android using sbt Part.5[カレンダーアプリ]後編
Scala on Androidの練習に、簡単なカレンダーアプリを書きました。
後編には、予定の保存先であるSQLiteデータベースの操作に使用するContentProvider
のコードを記載しています。
前編では、アプリを構成する3つの画面(カレンダーを表示する画面・予定表を表示する画面・予定を編集する画面)のコードを記載しています。
IDEはIntelliJ 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
を継承したScheduleDBOpenHelper
はScheduleContentProvider.scala
でインナークラスとして定義しています。
また、SQLiteデータベースの変更をScheduleActivity.scala
での予定表の表示に即時に反映させるため、query
メソッドでScheduleContentProvider
のContent URIをCursorLoader
の監視対象のURIに登録し、insert
・update
・delete
メソッドで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の実行結果
うまくいけば、アプリらしいアプリの出来上がりに嬉しくなります。
参考記事
- Scalacheat - Scala Documentation
- CalendarView | Android Developers
- SQLiteOpenHelper | Android Developers
- ContentProvider | Android Developers
- Cursor | Android Developers
- SimpleCursorAdapter | Android Developers
- LoaderManager | Android Developers
- TimePickerDialog | Android Developers
- R.style | Android Developers
- Calendar | Android Developers
- android - Change the text color of NumberPicker - Stack Overflow
ContentProviderやCursorLoaderの利用は、こちらの記事を参考にさせていただきました。
- ContentProvider - Qiita
- 【Android】コンテンツプロバイダ(ContentProvider)を使ってみる - It’s now or never
- CursorLoader - Qiita
- ContentProvierとCursorLoaderとFragmentを使ったサンプルを作ってみた
開発環境
- OSX 10.11.6 El Capitan
- IDE: InteiijJ IDEA Community Edition 2016.2
- Java Development Kit: Java SE Development Kit 8u101
- Android SDK Tools: android-sdk_r24.4.1-macosx
- Android Virtual Device: Android 6.0(Google APIs) API level 23 - Scala 2.11.8
- ビルドツール: sbt 0.13.12
関連記事
- IntelliJ IDEAでScalaを使ってAndroid開発 - あかんわ
- IntelliJ IDEAでScala on Android using sbt Part.1[じゃんけんアプリ] - あかんわ
- IntelliJ IDEAでScala on Android using sbt Part.2[マシュマロパーミッション] - あかんわ
- IntelliJ IDEAでScala on Android using sbt Part.3[ギャラリーアプリ] - あかんわ
- IntelliJ IDEAでScala on Android using sbt Part.4[ビデオプレーヤー] - あかんわ