おはようございます。@naokiurです。
引き続き、以前から興味があった、KotlinでAndroid開発を、
こっそりやっています。
課題が出て、解決案を考えたので、
ケーススタディの一例として、書き留めておきたいと思います。
課題
ステータスを表す値の、文字列とBooleanの変換、どこでやる?
経緯
- タスク管理アプリを作成している
- チェックボックスでTODOを
DOING
からDONE
へステータスを変えたい! - SQLiteのテーブルに
Status
カラムを追加したぜ! - SQLiteはBoolean型がないので、コード値の文字列、
0
,1
で管理するぜ! - あ…AndroidのCheckBox Viewは、Booleanか…
- テーブルから取得した際…変換しないと…変換して…テーブルに入れないと…
- あれ…どこで…? 誰が…?
環境
- MacBook Pro (Retina 13-inch、Early 2015)
- macOS High Sierra Capitan 10.13.2
- Kotlin 1.2
- Android Studio 3.1.2
目標
いい感じのクラスに、変換のロジックの責務をお願いする
結果
案3を採用、
いい感じのクラスに責務を負わせることができ、
また、読みやすくなったと感じ、目標が達成できたと感じました。
現状
前回、こそこそ作成しているタスク管理アプリに、
タスクを追加する機能を実装する、という記事を書きました。
そこでは、 残念なことですが 以下のように、コメントがないと なんで0を入れているのかがわからないソースコード
になってしまっています。
save_button.setOnClickListener { view ->
val task = TaskModel(
task_name.text.toString(),
"0", // 初期値を `Doing`とする
beginDate,
limitDate,
Calendar.getInstance().timeInMillis
)
val result = taskDatabaseHelper.insertTask(task)
if (result) {
Toast.makeText(this, "new Task Added!", LENGTH_LONG).show()
}
}
また、登録したタスクを表示する機能とActivity、
CheckBox Viewをクリックした際に、ステータスを更新する機能を、暫定的に作成しました。
fun updateCheck(id: Long, status: String): Boolean {
val db = writableDatabase
val values = ContentValues()
values.put(DBContract.TaskEntry.STATUS, status)
db.update(
DBContract.TaskEntry.TABLE_NAME,
values,
DBContract.TaskEntry.ID + " = ?",
arrayOf(id.toString())
)
return true
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val taskDatabaseHelper = TasksDatabaseHelper(this)
val taskList:ArrayList<TaskModel> = taskDatabaseHelper.selectAll()
// 暫定的になので、1つめのレコードを、画面に表示する
if (taskList.size != 0) {
val firstTask = taskList[0]
first_task_name.text = firstTask.name
// first_status.isChecked = firstTask.status
first_status.setOnClickListener { _ ->
// taskDatabaseHelper.updateCheck(firstTask.id, first_status.isChecked)
}
}
Doing
のとき、CheckBox Viewはチェックなし(isChecked == false)、
Done
のとき、CheckBox Viewはチェックあり(isChecked == true)としたいところですが、
上記の通り、SQLiteはBoolean型がなく、コード値で管理しているため、
当然ながら、 first_status.isChecked = firstTask.status
は、エラーです。
また、TasksDatabaseHelper.kt#updateCheck
は、idとstatus(String)を渡すようにした上に、
同様にCheckBox ViewのisCheckedはBooleanなので、
当然ながら、 first_status.isChecked = firstTask.status
は、エラーです。
解決案
案1 if文を追加し、変換する
愚直にif文を追加し、変換します。
// 暫定的になので、1つめのレコードを、画面に表示する
if (taskList.size != 0) {
val firstTask = taskList[0]
first_task_name.text = firstTask.name
first_status.isChecked = firstTask.status == "1" // firstTask.statusが'0'ならfalse, '1'ならtrue
// firstTask.statusが'0'ならfalse, '1'ならtrue
first_status.setOnClickListener { _ ->
if (first_status.isChecked) {
taskDatabaseHelper.updateCheck(firstTask.id, "1")
} else {
taskDatabaseHelper.updateCheck(firstTask.id, "0")
}
}
}
よくなったところ
- エラーがなくなった。
アレなところ
- 使用する度に、if分岐を追加しなければならず、ロジックが分散する。
- 相変わらずコメントがないと
なんで0を入れているのかがわからないソースコード
である。- 定数化すれば、まだマシになる。
- けれど、Activityにその定数を持つことになってしまい、複数Activityでステータスを用いる場合、定数が乱立する。
- グローバル定数的なクラスが必要になる。
- けれど、Activityにその定数を持つことになってしまい、複数Activityでステータスを用いる場合、定数が乱立する。
- 定数化すれば、まだマシになる。
タスク管理アプリでは、今のところこの2箇所で済んでいますが、
もっと複雑なアプリの場合、こういった箇所がたくさん出てきてしまうと考えられ、
読みにくい場所が乱立してしまうと思われます。
あんまりよくないですね。
案2 Converterクラスを作成し、変換する
TaskModelクラスのStatusのための、StatusConverterクラスを作成し、
変換する責務を負ってもらいます。
class StatusConverter {
companion object {
private const val DOING_CODE: String = "0"
private const val DONE_CODE: String = "1"
fun toCode(isDone: Boolean): String {
return if (isDone) DONE_CODE else DOING_CODE
}
fun toIsDone(code: String): Boolean {
return code == DONE_CODE
}
}
}
// 暫定的になので、1つめのレコードを、画面に表示する
if (taskList.size != 0) {
val firstTask = taskList[0]
first_task_name.text = firstTask.name
first_status.isChecked = StatusConverter.toIsDone(firstTask.status)
first_status.setOnClickListener { _ ->
taskDatabaseHelper.updateCheck(firstTask.id, StatusConverter.toCode(first_status.isChecked))
}
}
よくなったところ
- ロジックが集約された。
アレなところ
- TaskModelクラスとStatusConverterクラスの紐付きを、意識しなければならない。
- 定数化したけれど、どっちがtrueでどっちがfalseだっけ、が、ひと目ではわかりにくい。
比較的、MainActivity.kt内の記述量が減ったので、
読みやすくはなったと思います。
ただ、案1の問題と同様ですが、
複雑なアプリの場合、コード値を持つものがたくさん出てくると考えられ、
その際は、Converterクラスが乱立、
紐付きが見えにくくなり、読みにくいソースになってしまうと思われます。
案3 DTOであるModelクラスにInner Enumクラスを作成し、変換する。
良さげ、と思っているものです。
個人的に、(Java触ってたときから)Enum大好き人間である、という、ある種趣味全開なものです。
Inner Enumクラスを作成し、
変換する責務を負ってもらいます。
class TaskModel(val name: String, val status: String, val beginData: Long, val endDate: Long, val updateTime: Long) {
var id: Long = 0
constructor(id: Long = 0, name: String, status: String, beginData: Long, endDate: Long, updateTime: Long) : this(name, status, beginData, endDate, updateTime) {
this.id = id
}
enum class Status(val value: String, val isDone: Boolean) {
DOING("0", false),
DONE("1", true);
companion object {
fun fromValue(value: String): Status {
return values().first { it.value == value }
}
fun fromIsDone(isDone: Boolean): Status {
return values().first { it.isDone == isDone }
}
}
}
}
// 暫定的になので、1つめのレコードを、画面に表示する
if (taskList.size != 0) {
val firstTask = taskList[0]
first_task_name.text = firstTask.name
first_status.isChecked = TaskModel.Status.fromValue(firstTask.status).isDone
first_status.setOnClickListener { _ ->
taskDatabaseHelper.updateCheck(firstTask.id, TaskModel.Status.fromIsDone(first_status.isChecked).value)
}
}
よくなったところ
- ロジックが集約された。
- 1クラスに集約されたため、紐付きがわかりやすくなった。
- Enumにしたことにより、コード値とBooleanのマッピングが読みやすくなった。
-
private const val
書かなくてよくなった。- 個人的に、
private const val
はあまり乱立させたくないため…。
- 個人的に、
アレなところ
- 一旦Enumに変換し、String/Booleanを取得する、というのが、冗長に見えるかもしれない。
- 個人的には、あんまり感じていないが、人によってあるいは…?
今後
より良いソースコードを、見つけていきたいです!