M2b: Android skeleton, :core:model contract, :domain passing all vectors
Gradle project in android/ with every module of SPEC §6.2 (build-logic convention plugins, version catalog, architecture check in check), :core:model types and repository interfaces of §14.2.4 and §14.5, :domain implementing §14.5.2 and §14.5.4 (125 vector cases), scenario-driven fakes with the empty scenario, Room 3 entities and KvittDatabase v1 with exported schema, :app shell with Hilt, en/sv strings with MissingTranslation as a lint error, Roborazzi goldens of the empty screen in light and dark. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
// :app — MainActivity, Application, Hilt wiring; navigation, bindings, deep links and notifications arrive with M8.
|
||||
plugins {
|
||||
id("kvitt.android.application")
|
||||
id("kvitt.android.compose")
|
||||
id("kvitt.android.hilt")
|
||||
alias(libs.plugins.roborazzi)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "se.hagstedt.kvitt"
|
||||
defaultConfig {
|
||||
applicationId = "se.hagstedt.kvitt"
|
||||
// SPEC §10.6: the release script passes kvitt.versionCode and kvitt.versionName; debug builds use 1 and "dev".
|
||||
versionCode = providers.gradleProperty("kvitt.versionCode").orElse("1").get().toInt()
|
||||
versionName = providers.gradleProperty("kvitt.versionName").orElse("dev").get()
|
||||
}
|
||||
buildTypes {
|
||||
debug {
|
||||
applicationIdSuffix = ".debug"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
roborazzi {
|
||||
outputDir.set(file("src/test/screenshots"))
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":core:model"))
|
||||
implementation(project(":core:design"))
|
||||
implementation(project(":domain"))
|
||||
implementation(project(":data"))
|
||||
implementation(project(":adapter:fcm"))
|
||||
implementation(project(":feature:onboarding"))
|
||||
implementation(project(":feature:home"))
|
||||
implementation(project(":feature:account"))
|
||||
implementation(project(":feature:expense"))
|
||||
implementation(project(":feature:settle"))
|
||||
implementation(project(":feature:analytics"))
|
||||
implementation(project(":feature:settings"))
|
||||
debugImplementation(project(":core:fakes"))
|
||||
testImplementation(project(":core:fakes"))
|
||||
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
|
||||
debugImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
testImplementation(libs.androidx.compose.ui.test.junit4)
|
||||
testImplementation(libs.robolectric)
|
||||
testImplementation(libs.roborazzi)
|
||||
testImplementation(libs.roborazzi.compose)
|
||||
testImplementation(libs.roborazzi.junit.rule)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application
|
||||
android:name=".KvittApplication"
|
||||
android:allowBackup="false"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@android:style/Theme.Material.NoActionBar">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,8 @@
|
||||
package se.hagstedt.kvitt
|
||||
|
||||
import android.app.Application
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
|
||||
/** Hilt's root. M8 adds the bindings from the `:core:model` interfaces to `:data`. */
|
||||
@HiltAndroidApp
|
||||
class KvittApplication : Application()
|
||||
@@ -0,0 +1,21 @@
|
||||
package se.hagstedt.kvitt
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
|
||||
/** The M2b shell: the themed surface with the app name. M8 replaces it with the navigation graph. */
|
||||
@Composable
|
||||
fun KvittEmptyScreen(modifier: Modifier = Modifier) {
|
||||
Surface(modifier = modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
Text(text = stringResource(R.string.app_name), style = MaterialTheme.typography.headlineMedium)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package se.hagstedt.kvitt
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import se.hagstedt.kvitt.design.KvittTheme
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
KvittTheme {
|
||||
KvittEmptyScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Kvitt</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- :app holds only the app name; shared strings live in :core:design, feature strings in each feature. -->
|
||||
<resources>
|
||||
<string name="app_name">Kvitt</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- SPEC §8.8: neither cloud backup nor device transfer copies the token or the database. -->
|
||||
<data-extraction-rules>
|
||||
<cloud-backup>
|
||||
<exclude domain="root" path="." />
|
||||
<exclude domain="file" path="." />
|
||||
<exclude domain="database" path="." />
|
||||
<exclude domain="sharedpref" path="." />
|
||||
<exclude domain="external" path="." />
|
||||
</cloud-backup>
|
||||
<device-transfer>
|
||||
<exclude domain="root" path="." />
|
||||
<exclude domain="file" path="." />
|
||||
<exclude domain="database" path="." />
|
||||
<exclude domain="sharedpref" path="." />
|
||||
<exclude domain="external" path="." />
|
||||
</device-transfer>
|
||||
</data-extraction-rules>
|
||||
@@ -0,0 +1,38 @@
|
||||
package se.hagstedt.kvitt
|
||||
|
||||
import androidx.compose.ui.test.junit4.v2.createComposeRule
|
||||
import androidx.compose.ui.test.onRoot
|
||||
import com.github.takahirom.roborazzi.captureRoboImage
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
import org.robolectric.annotation.GraphicsMode
|
||||
import se.hagstedt.kvitt.design.KvittTheme
|
||||
|
||||
/**
|
||||
* Golden screenshots of the empty screen in light and dark (Roborazzi on Robolectric). `check` verifies them;
|
||||
* re-record an intended change with `tools/gradle-serial.sh recordRoborazziDebug` and open every changed PNG.
|
||||
* Viewport: the Galaxy S26 (about 411 x 891 dp, SPEC §13.1).
|
||||
*/
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@GraphicsMode(GraphicsMode.Mode.NATIVE)
|
||||
@Config(sdk = [36], qualifiers = "w411dp-h891dp-xxhdpi")
|
||||
class KvittEmptyScreenScreenshotTest {
|
||||
@get:Rule
|
||||
val compose = createComposeRule()
|
||||
|
||||
@Test
|
||||
fun emptyScreenLight() = capture(darkTheme = false, name = "empty_screen_light")
|
||||
|
||||
@Test
|
||||
fun emptyScreenDark() = capture(darkTheme = true, name = "empty_screen_dark")
|
||||
|
||||
private fun capture(darkTheme: Boolean, name: String) {
|
||||
compose.setContent {
|
||||
KvittTheme(darkTheme = darkTheme) { KvittEmptyScreen() }
|
||||
}
|
||||
compose.onRoot().captureRoboImage("src/test/screenshots/$name.png")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user