Skip to content

Instantly share code, notes, and snippets.

@sinipelto
Created April 1, 2021 17:01
Show Gist options
  • Save sinipelto/014b34940bc8bda2b173c0d3c1746814 to your computer and use it in GitHub Desktop.
Save sinipelto/014b34940bc8bda2b173c0d3c1746814 to your computer and use it in GitHub Desktop.
DAO-Repo Pattern Android (Kotlin)
package fi.tuni.sinipelto.laskuvelho.data
import android.content.Context
import android.util.Log
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import androidx.sqlite.db.SupportSQLiteDatabase
import fi.tuni.sinipelto.laskuvelho.data.constant.DataConstants
import fi.tuni.sinipelto.laskuvelho.data.dao.InvoiceDao
import fi.tuni.sinipelto.laskuvelho.data.entity.InvoiceEntity
import fi.tuni.sinipelto.laskuvelho.data.model.InvoiceType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import java.util.*
// No migration plan for the database in this application => do NOT export the db schema to file
// A global application-level database
@Database(entities = [InvoiceEntity::class], version = 1, exportSchema = false)
@TypeConverters(fi.tuni.sinipelto.laskuvelho.data.converter.TypeConverters::class) // We need a type converter class to handle non-primitive types between app <-> db
abstract class AppDatabase : RoomDatabase() {
// Connect DAO for invoices table
abstract fun invoiceDao(): InvoiceDao
// Companion for static access (no object instance)
companion object {
// Static database Singleton instance stored here
@Volatile
private var INSTANCE: AppDatabase? = null
// Get existing database instance or create new if does not exist yet
// Lazy loading (not created until requested)
fun getDatabase(ctx: Context/*, scope: CoroutineScope*/): AppDatabase {
return INSTANCE ?: synchronized(this) {
Log.d(DataConstants.LOG_TAG, "Creating database...")
// Create NON-NULL type of instance inside sync context
val inst = Room.databaseBuilder(
ctx.applicationContext,
AppDatabase::class.java,
DataConstants.APP_DATABASE_NAME
)
// .addCallback(DatabasePopulator(scope)) // Enabled for DEV purposes
.build()
// Set this instance to the created database object
INSTANCE = inst
inst
}
}
// Class for pre-populating the database on creation
private class DatabasePopulator(private val scope: CoroutineScope) : Callback() {
// Override database creation function
override fun onCreate(db: SupportSQLiteDatabase) {
// Do all parent operations first
super.onCreate(db)
Log.d(DataConstants.LOG_TAG, "Populating datbase with prepopulated values...")
scope.launch { // Inside the let scope (db not null), insert values to the db using the DAO obj
INSTANCE?.let { db ->
scope.launch {
val dao = db.invoiceDao()
dao.deleteAllInvoices()
dao.insertInvoices(PREP_DATA)
}
}
}
Log.d(DataConstants.LOG_TAG, "Database pre-population done.")
}
}
// Hack to prevent compiler warning 'param always 20' => param count e.g.: "15".toInt()
private fun generateRandomEntries(count: Int): Collection<InvoiceEntity> {
val rnd = Random(Date().time)
val list = mutableListOf<InvoiceEntity>()
for (i in 0..count) {
list.add(PREP_DATA[rnd.nextInt(PREP_DATA.size)]) // rnd elem betw 0.. size - 1
}
return list
}
// Private collection of data to be inserted on creation
// Date(EpochMilliseconds: Long)
private val PREP_DATA = listOf(
InvoiceEntity(
0,
"Tampereen Vesilaitos Oy",
InvoiceType.getInvoiceTypeCodeById(InvoiceType.Companion.Type.WaterInvoice),
15.30,
Date(1_600_000_000_000)
),
InvoiceEntity(
0,
"Telia Oyj",
InvoiceType.getInvoiceTypeCodeById(InvoiceType.Companion.Type.PhoneInvoice),
9.99,
Date(1_150_000_000_000)
),
InvoiceEntity(
0,
"Pikavippi Oy",
InvoiceType.getInvoiceTypeCodeById(InvoiceType.Companion.Type.CreditInvoice),
137.68,
Date(1_050_055_000_000)
),
InvoiceEntity(
0,
"Huoltopalvelu Oy",
InvoiceType.getInvoiceTypeCodeById(InvoiceType.Companion.Type.MaintenanceInvoice),
95.00,
Date(1_300_000_000_000)
),
InvoiceEntity(
0,
"ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGH",
InvoiceType.getInvoiceTypeCodeById(InvoiceType.Companion.Type.MaintenanceInvoice),
9999999999.52838892377895,
Date(1_300_000_000_000)
),
InvoiceEntity(
0,
"Elisa Oyj",
InvoiceType.getInvoiceTypeCodeById(InvoiceType.Companion.Type.PhoneInvoice),
9.99,
Date(1_150_000_000_000)
),
InvoiceEntity(
0,
"DNA Oy",
InvoiceType.getInvoiceTypeCodeById(InvoiceType.Companion.Type.PhoneInvoice),
9.99,
Date(1_150_000_000_000)
),
InvoiceEntity(
0,
"Puhelinfirma Oy",
InvoiceType.getInvoiceTypeCodeById(InvoiceType.Companion.Type.PhoneInvoice),
10.238763284576,
Date(1_700_000_000_000)
),
InvoiceEntity(
0,
"SATO",
InvoiceType.getInvoiceTypeCodeById(InvoiceType.Companion.Type.RentInvoice),
697.238763284576,
Date()
),
InvoiceEntity(
0,
"Verkkokauppa.com Oyj / Apuraha",
InvoiceType.getInvoiceTypeCodeById(InvoiceType.Companion.Type.EcommerceInvoice),
100.990000103127362167,
Date(Date().time + 1_000_000)
),
)
}
}
package fi.tuni.sinipelto.laskuvelho.data.constant
import android.graphics.Color
class DataConstants {
companion object {
const val LOG_TAG = "laskuvelho.data.logger"
const val APP_DATABASE_NAME = "laskuvelho_database"
const val INVOICE_TABLE_NAME = "invoices"
const val DEFAULT_TEXT_COLOR = Color.BLACK
const val DANGER_TEXT_COLOR = Color.RED
const val WARNING_TEXT_COLOR = Color.YELLOW
}
}
package fi.tuni.sinipelto.laskuvelho.data.comparator
import android.util.Log
import androidx.recyclerview.widget.DiffUtil
import fi.tuni.sinipelto.laskuvelho.data.constant.DataConstants
import fi.tuni.sinipelto.laskuvelho.data.entity.InvoiceEntity
class InvoiceComparator : DiffUtil.ItemCallback<InvoiceEntity>() {
override fun areItemsTheSame(oldItem: InvoiceEntity, newItem: InvoiceEntity): Boolean {
Log.d(DataConstants.LOG_TAG, "Items same: $oldItem === $newItem")
return oldItem === newItem
}
// Check if all fields match, is a duplicate
override fun areContentsTheSame(oldItem: InvoiceEntity, newItem: InvoiceEntity): Boolean {
Log.d(
DataConstants.LOG_TAG,
"Item contents same: ${oldItem.invoiceId} === ${newItem.invoiceId}"
)
return oldItem.invoiceId == newItem.invoiceId
}
}
package fi.tuni.sinipelto.laskuvelho.data.dao
import androidx.room.*
import fi.tuni.sinipelto.laskuvelho.data.entity.InvoiceEntity
import kotlinx.coroutines.flow.Flow
@Dao
interface InvoiceDao {
@Query("SELECT * FROM invoices ORDER BY due_date ASC")
fun getAllInvoicesByDueAsc(): Flow<List<InvoiceEntity>>
@Query("SELECT * FROM invoices ORDER BY due_date DESC")
fun getAllInvoicesByDueDesc(): Flow<List<InvoiceEntity>>
@Query("SELECT * FROM invoices WHERE invoice_type = :invoiceType")
fun getAllInvoicesByType(invoiceType: Int): Flow<List<InvoiceEntity>>
@Query("SELECT * FROM invoices WHERE invoice_type = :invoiceType ORDER BY due_date DESC")
fun getAllInvoicesByTypeOrderByDueDesc(invoiceType: Int): Flow<List<InvoiceEntity>>
// We do not tolerate duplicate entries -> unique autogen id ensures no duplicate entries
@Insert(onConflict = OnConflictStrategy.ABORT)
suspend fun insertInvoice(invoice: InvoiceEntity)
@Insert(onConflict = OnConflictStrategy.ABORT)
suspend fun insertInvoices(invoices: Collection<InvoiceEntity>)
@Delete
suspend fun deleteInvoice(invoice: InvoiceEntity)
@Query("DELETE FROM invoices")
suspend fun deleteAllInvoices()
}
package fi.tuni.sinipelto.laskuvelho.data.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import fi.tuni.sinipelto.laskuvelho.data.constant.DataConstants
import java.util.*
@Entity(tableName = DataConstants.INVOICE_TABLE_NAME)
data class InvoiceEntity(
// We might have multiple invoices with same targets, thus
// separate, unqiue key is needed
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") val invoiceId: Long, // Sqlite does not support java.math.BigInteger java type
@ColumnInfo(name = "payee_name") val payee: String,
@ColumnInfo(name = "invoice_type") val invoiceType: Int, // Regular integer has sufficient bitcount for invoice type enumeration
@ColumnInfo(name = "amount") val amount: Double, // Sqlite doesnt support java.math.BigDecimal java type
@ColumnInfo(name = "due_date") val dueDate: Date // Using TypeConverter for java.util.Date <-> kotlin.Long
)
package fi.tuni.sinipelto.laskuvelho.data.repository
import androidx.annotation.WorkerThread
import fi.tuni.sinipelto.laskuvelho.data.dao.InvoiceDao
import fi.tuni.sinipelto.laskuvelho.data.entity.InvoiceEntity
import kotlinx.coroutines.flow.Flow
class InvoiceRepository(private val invoiceDao: InvoiceDao) {
val allInvoices: Flow<List<InvoiceEntity>> = invoiceDao.getAllInvoicesByDueAsc()
@WorkerThread
suspend fun insert(invoice: InvoiceEntity) {
invoiceDao.insertInvoice(invoice)
}
@WorkerThread
suspend fun remove(invoice: InvoiceEntity) {
invoiceDao.deleteInvoice(invoice)
}
}
package fi.tuni.sinipelto.laskuvelho.data.model
// Static mapping of available invoice types
// Does not change after compilation (readonly), thus can be static
class InvoiceType(val id: Type) {
// To use this class as stringable object
override fun toString(): String {
// Cannot ever be null, since string equivalent always mapped in the static mapping
return STR_MAPPING[id]!!
}
companion object {
enum class Type {
Invoice,
TravelInvoice,
MaintenanceInvoice,
ElectricityInvoice,
CreditInvoice,
CreditCardInvoice,
WaterInvoice,
RentInvoice,
EcommerceInvoice,
PhoneInvoice,
LoanInvoice,
}
// Existing entries should NOT be modified
// DO NOT MODIFY EXISTING ENTRIES FOR BACKWARDS COMPABILITY
private val ID_MAPPING: Map<Int, Type> = mapOf(
0 to Type.Invoice,
1 to Type.TravelInvoice,
2 to Type.MaintenanceInvoice,
3 to Type.ElectricityInvoice,
4 to Type.CreditInvoice,
5 to Type.WaterInvoice,
6 to Type.RentInvoice,
7 to Type.EcommerceInvoice,
8 to Type.PhoneInvoice,
9 to Type.LoanInvoice,
10 to Type.CreditCardInvoice,
)
private val STR_MAPPING: Map<Type, String> = mapOf(
Type.Invoice to "Muu lasku",
Type.TravelInvoice to "Matkalasku",
Type.MaintenanceInvoice to "Huoltolasku",
Type.ElectricityInvoice to "Sähkölasku",
Type.CreditInvoice to "Luottolasku",
Type.WaterInvoice to "Vesilasku",
Type.RentInvoice to "Vuokralasku",
Type.EcommerceInvoice to "Verkkokauppalasku",
Type.PhoneInvoice to "Puhelinlasku",
Type.LoanInvoice to "Lainalyhennys",
Type.CreditCardInvoice to "Luottokorttilasku"
)
fun getObjects(): Collection<InvoiceType> {
return Type.values().map { InvoiceType(it) }
}
fun getInvoiceTypeNames(): Collection<String> = STR_MAPPING.values
fun getInvoiceTypeNameByCode(code: Int): String {
val id = ID_MAPPING[code] // Will throw if code doesnt exist
return STR_MAPPING[id]!! // Cannot be null => Num-Type mapping is 1:1
}
fun getInvoiceTypeCodeById(type: Type): Int {
return ID_MAPPING.entries.first { i -> i.value == type }.key
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment