Created
April 1, 2021 17:01
-
-
Save sinipelto/014b34940bc8bda2b173c0d3c1746814 to your computer and use it in GitHub Desktop.
DAO-Repo Pattern Android (Kotlin)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
), | |
) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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