From cf4226d20ca559f456fca04df6aea0c1ae17e5ff Mon Sep 17 00:00:00 2001 From: William Brawner Date: Thu, 24 Dec 2020 18:02:05 -0700 Subject: [PATCH] WIP: Implement local database caching/networking with KMP --- .../ui/categories/CategoryFormActivity.kt | 4 +- .../transactions/TransactionFormActivity.kt | 4 +- budgetlib/build.gradle | 4 +- build.gradle | 1 + common/build.gradle | 4 +- settings.gradle | 2 +- shared/.gitignore | 1 + shared/build.gradle.kts | 98 +++++++++++++++++++ shared/proguard-rules.pro | 21 ++++ shared/src/androidMain/AndroidManifest.xml | 2 + .../twigs/shared/AndroidTwigsDatabase.kt | 12 +++ .../com/wbrawner/twigs/shared/Responses.kt | 84 ++++++++++++++++ .../twigs/shared/TwigsDatabaseFactory.kt | 16 +++ .../com/wbrawner/twigs/db/model/Budget.sq | 6 ++ .../com/wbrawner/twigs/db/model/Category.sq | 6 ++ .../wbrawner/twigs/db/model/Transaction.sq | 6 ++ .../com/wbrawner/twigs/db/model/User.sq | 6 ++ .../twigs/shared/NativeTwigsDatabase.kt | 10 ++ storage/build.gradle | 4 +- 19 files changed, 280 insertions(+), 11 deletions(-) create mode 100644 shared/.gitignore create mode 100644 shared/build.gradle.kts create mode 100644 shared/proguard-rules.pro create mode 100644 shared/src/androidMain/AndroidManifest.xml create mode 100644 shared/src/androidMain/kotlin/com/wbrawner/twigs/shared/AndroidTwigsDatabase.kt create mode 100644 shared/src/commonMain/kotlin/com/wbrawner/twigs/shared/Responses.kt create mode 100644 shared/src/commonMain/kotlin/com/wbrawner/twigs/shared/TwigsDatabaseFactory.kt create mode 100644 shared/src/commonMain/sqldelight/com/wbrawner/twigs/db/model/Budget.sq create mode 100644 shared/src/commonMain/sqldelight/com/wbrawner/twigs/db/model/Category.sq create mode 100644 shared/src/commonMain/sqldelight/com/wbrawner/twigs/db/model/Transaction.sq create mode 100644 shared/src/commonMain/sqldelight/com/wbrawner/twigs/db/model/User.sq create mode 100644 shared/src/iosMain/kotlin/com/wbrawner/twigs/shared/NativeTwigsDatabase.kt diff --git a/android/src/main/java/com/wbrawner/budget/ui/categories/CategoryFormActivity.kt b/android/src/main/java/com/wbrawner/budget/ui/categories/CategoryFormActivity.kt index fc9a188..cac8da2 100644 --- a/android/src/main/java/com/wbrawner/budget/ui/categories/CategoryFormActivity.kt +++ b/android/src/main/java/com/wbrawner/budget/ui/categories/CategoryFormActivity.kt @@ -77,8 +77,8 @@ class CategoryFormActivity : AppCompatActivity() { return true } - override fun onOptionsItemSelected(item: MenuItem?): Boolean { - when (item?.itemId) { + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { android.R.id.home -> { val upIntent: Intent? = NavUtils.getParentActivityIntent(this) diff --git a/android/src/main/java/com/wbrawner/budget/ui/transactions/TransactionFormActivity.kt b/android/src/main/java/com/wbrawner/budget/ui/transactions/TransactionFormActivity.kt index 48abe77..6e2e55f 100644 --- a/android/src/main/java/com/wbrawner/budget/ui/transactions/TransactionFormActivity.kt +++ b/android/src/main/java/com/wbrawner/budget/ui/transactions/TransactionFormActivity.kt @@ -171,8 +171,8 @@ class TransactionFormActivity : AppCompatActivity(), CoroutineScope { return true } - override fun onOptionsItemSelected(item: MenuItem?): Boolean { - when (item?.itemId) { + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { android.R.id.home -> onNavigateUp() R.id.action_save -> { val date = GregorianCalendar.getInstance().apply { diff --git a/budgetlib/build.gradle b/budgetlib/build.gradle index f9358ae..25acbfb 100644 --- a/budgetlib/build.gradle +++ b/budgetlib/build.gradle @@ -4,11 +4,11 @@ apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' android { - compileSdkVersion 29 + compileSdkVersion 30 defaultConfig { minSdkVersion 21 - targetSdkVersion 29 + targetSdkVersion 30 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/build.gradle b/build.gradle index 8501da3..a379346 100644 --- a/build.gradle +++ b/build.gradle @@ -16,6 +16,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:4.1.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath 'com.squareup.sqldelight:gradle-plugin:1.4.3' } } diff --git a/common/build.gradle b/common/build.gradle index 32595ed..259f3ff 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -4,12 +4,12 @@ apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' android { - compileSdkVersion 29 + compileSdkVersion 30 defaultConfig { minSdkVersion 21 - targetSdkVersion 29 + targetSdkVersion 30 versionCode 1 versionName "1.0" diff --git a/settings.gradle b/settings.gradle index 9392ebd..59599cb 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include ':android', ':budgetlib', ':common', ':storage' +include ':android', ':budgetlib', ':common', ':storage', ':shared' diff --git a/shared/.gitignore b/shared/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/shared/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts new file mode 100644 index 0000000..16b2aad --- /dev/null +++ b/shared/build.gradle.kts @@ -0,0 +1,98 @@ +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget + +plugins { + kotlin("multiplatform") + id("com.android.library") + id("kotlin-android-extensions") + kotlin("plugin.serialization") version "1.4.10" + id("com.squareup.sqldelight") +} +group = "com.wbrawner.twigs" +version = "1.0-SNAPSHOT" + +repositories { + gradlePluginPortal() + google() + jcenter() + mavenCentral() +} +kotlin { + android() + ios { + binaries { + framework { + baseName = "twigs" + } + } + } + sourceSets { + val commonMain by getting { + dependencies { + implementation("io.ktor:ktor-client-core:1.4.2") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1") + implementation("io.ktor:ktor-client-serialization:1.4.2") + } + } + val commonTest by getting { + dependencies { + implementation(kotlin("test-common")) + implementation(kotlin("test-annotations-common")) + } + } + val androidMain by getting { + dependencies { + implementation("com.google.android.material:material:1.2.0") + implementation("com.squareup.sqldelight:android-driver:1.4.3") + api("io.ktor:ktor-client-android:1.4.2") + } + } + val androidTest by getting { + dependencies { + implementation(kotlin("test-junit")) + implementation("junit:junit:4.12") + } + } + val iosMain by getting { + dependencies { + implementation("io.ktor:ktor-client-ios:1.4.2") + implementation("com.squareup.sqldelight:native-driver:1.4.3") + } + } + val iosTest by getting + } +} + +sqldelight { + database("TwigsDatabase") { + packageName = "com.wbrawner.twigs" + } +} + +android { + compileSdkVersion(30) + sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") + defaultConfig { + minSdkVersion(23) + targetSdkVersion(30) + versionCode = 1 + versionName = "1.0" + } + buildTypes { + getByName("release") { + isMinifyEnabled = false + } + } +} +val packForXcode by tasks.creating(Sync::class) { + group = "build" + val mode = System.getenv("CONFIGURATION") ?: "DEBUG" + val sdkName = System.getenv("SDK_NAME") ?: "iphonesimulator" + val targetName = "ios" + if (sdkName.startsWith("iphoneos")) "Arm64" else "X64" + val framework = kotlin.targets.getByName(targetName).binaries.getFramework(mode) + inputs.property("mode", mode) + dependsOn(framework.linkTask) + val targetDir = File(buildDir, "xcode-frameworks") + from({ framework.outputDirectory }) + into(targetDir) +} +tasks.getByName("build").dependsOn(packForXcode) \ No newline at end of file diff --git a/shared/proguard-rules.pro b/shared/proguard-rules.pro new file mode 100644 index 0000000..ff59496 --- /dev/null +++ b/shared/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle.kts. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/shared/src/androidMain/AndroidManifest.xml b/shared/src/androidMain/AndroidManifest.xml new file mode 100644 index 0000000..95ce212 --- /dev/null +++ b/shared/src/androidMain/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/shared/src/androidMain/kotlin/com/wbrawner/twigs/shared/AndroidTwigsDatabase.kt b/shared/src/androidMain/kotlin/com/wbrawner/twigs/shared/AndroidTwigsDatabase.kt new file mode 100644 index 0000000..9f69f10 --- /dev/null +++ b/shared/src/androidMain/kotlin/com/wbrawner/twigs/shared/AndroidTwigsDatabase.kt @@ -0,0 +1,12 @@ +package com.wbrawner.twigs.shared + +import android.content.Context +import com.squareup.sqldelight.android.AndroidSqliteDriver +import com.squareup.sqldelight.db.SqlDriver +import com.wbrawner.twigs.TwigsDatabase + +actual class DriverFactory(private val context: Context) { + actual fun createDriver(): SqlDriver { + return AndroidSqliteDriver(TwigsDatabase.Schema, context, "twigs.db") + } +} diff --git a/shared/src/commonMain/kotlin/com/wbrawner/twigs/shared/Responses.kt b/shared/src/commonMain/kotlin/com/wbrawner/twigs/shared/Responses.kt new file mode 100644 index 0000000..d47a7ea --- /dev/null +++ b/shared/src/commonMain/kotlin/com/wbrawner/twigs/shared/Responses.kt @@ -0,0 +1,84 @@ +package com.wbrawner.twigs.shared + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class Summary( + @SerialName("domains_being_blocked") + val domainsBeingBlocked: String, + @SerialName("dns_queries_today") + val dnsQueriesToday: String, + @SerialName("ads_blocked_today") + val adsBlockedToday: String, + @SerialName("ads_percentage_today") + val adsPercentageToday: String, + @SerialName("unique_domains") + val uniqueDomains: String, + @SerialName("queries_forwarded") + val queriesForwarded: String, + @SerialName("clients_ever_seen") + val clientsEverSeen: String, + @SerialName("unique_clients") + val uniqueClients: String, + @SerialName("dns_queries_all_types") + val dnsQueriesAllTypes: String, + @SerialName("queries_cached") + val queriesCached: String, + @SerialName("no_data_replies") + val noDataReplies: String? = null, + @SerialName("nx_domain_replies") + val nxDomainReplies: String? = null, + @SerialName("cname_replies") + val cnameReplies: String? = null, + @SerialName("in_replies") + val ipReplies: String? = null, + @SerialName("privacy_level") + val privacyLevel: String, + override val status: Status, + @SerialName("gravity_last_updated") + val gravity: Gravity? = null, + val type: String? = null, + val version: Int? = null +) : StatusProvider + +@Serializable +enum class Status { + @SerialName("enabled") + ENABLED, + @SerialName("disabled") + DISABLED +} + +@Serializable +data class Gravity( + @SerialName("file_exists") + val fileExists: Boolean, + val absolute: Int, + val relative: Relative +) + +@Serializable +data class Relative( + val days: String, + val hours: String, + val minutes: String +) + +@Serializable +data class VersionResponse(val version: Int) + +@Serializable +data class TopItemsResponse( + @SerialName("top_queries") val topQueries: Map, + @SerialName("top_ads") val topAds: Map +) + +@Serializable +data class StatusResponse( + override val status: Status +) : StatusProvider + +interface StatusProvider { + val status: Status +} diff --git a/shared/src/commonMain/kotlin/com/wbrawner/twigs/shared/TwigsDatabaseFactory.kt b/shared/src/commonMain/kotlin/com/wbrawner/twigs/shared/TwigsDatabaseFactory.kt new file mode 100644 index 0000000..781af6b --- /dev/null +++ b/shared/src/commonMain/kotlin/com/wbrawner/twigs/shared/TwigsDatabaseFactory.kt @@ -0,0 +1,16 @@ +package com.wbrawner.twigs.shared + +import com.squareup.sqldelight.db.SqlDriver +import com.wbrawner.twigs.TwigsDatabase + +expect class DriverFactory { + fun createDriver(): SqlDriver +} + +fun createDatabase(driverFactory: DriverFactory): TwigsDatabase { + val driver = driverFactory.createDriver() + val database = TwigsDatabase(driver) + + // Do more work with the database (see below). + return database +} diff --git a/shared/src/commonMain/sqldelight/com/wbrawner/twigs/db/model/Budget.sq b/shared/src/commonMain/sqldelight/com/wbrawner/twigs/db/model/Budget.sq new file mode 100644 index 0000000..34656d3 --- /dev/null +++ b/shared/src/commonMain/sqldelight/com/wbrawner/twigs/db/model/Budget.sq @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS budget ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + description TEXT, + currencyCode TEXT +); \ No newline at end of file diff --git a/shared/src/commonMain/sqldelight/com/wbrawner/twigs/db/model/Category.sq b/shared/src/commonMain/sqldelight/com/wbrawner/twigs/db/model/Category.sq new file mode 100644 index 0000000..34656d3 --- /dev/null +++ b/shared/src/commonMain/sqldelight/com/wbrawner/twigs/db/model/Category.sq @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS budget ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + description TEXT, + currencyCode TEXT +); \ No newline at end of file diff --git a/shared/src/commonMain/sqldelight/com/wbrawner/twigs/db/model/Transaction.sq b/shared/src/commonMain/sqldelight/com/wbrawner/twigs/db/model/Transaction.sq new file mode 100644 index 0000000..34656d3 --- /dev/null +++ b/shared/src/commonMain/sqldelight/com/wbrawner/twigs/db/model/Transaction.sq @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS budget ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + description TEXT, + currencyCode TEXT +); \ No newline at end of file diff --git a/shared/src/commonMain/sqldelight/com/wbrawner/twigs/db/model/User.sq b/shared/src/commonMain/sqldelight/com/wbrawner/twigs/db/model/User.sq new file mode 100644 index 0000000..34656d3 --- /dev/null +++ b/shared/src/commonMain/sqldelight/com/wbrawner/twigs/db/model/User.sq @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS budget ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + description TEXT, + currencyCode TEXT +); \ No newline at end of file diff --git a/shared/src/iosMain/kotlin/com/wbrawner/twigs/shared/NativeTwigsDatabase.kt b/shared/src/iosMain/kotlin/com/wbrawner/twigs/shared/NativeTwigsDatabase.kt new file mode 100644 index 0000000..142f5fb --- /dev/null +++ b/shared/src/iosMain/kotlin/com/wbrawner/twigs/shared/NativeTwigsDatabase.kt @@ -0,0 +1,10 @@ +package com.wbrawner.twigs.shared + +import com.squareup.sqldelight.db.SqlDriver +import com.wbrawner.twigs.TwigsDatabase + +actual class DriverFactory { + actual fun createDriver(): SqlDriver { + return NativeSqliteDriver(TwigsDatabase.Schema, "twigs.db") + } +} diff --git a/storage/build.gradle b/storage/build.gradle index 63108eb..6793064 100644 --- a/storage/build.gradle +++ b/storage/build.gradle @@ -4,12 +4,12 @@ apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' android { - compileSdkVersion 29 + compileSdkVersion 30 defaultConfig { minSdkVersion 23 - targetSdkVersion 29 + targetSdkVersion 30 versionCode 1 versionName "1.0"