create-cmp-cli 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +232 -0
- package/bin/create-cmp.mjs +91 -0
- package/options.schema.json +110 -0
- package/package.json +52 -0
- package/src/bootstrap/checks.mjs +283 -0
- package/src/bootstrap/exec.mjs +90 -0
- package/src/commands/clean.mjs +195 -0
- package/src/commands/create.mjs +223 -0
- package/src/commands/doctor.mjs +256 -0
- package/src/commands/upgrade.mjs +196 -0
- package/src/commands/verify.mjs +94 -0
- package/src/doctor.mjs +109 -0
- package/src/lib/args.mjs +38 -0
- package/src/lib/clean.mjs +72 -0
- package/src/lib/fsutil.mjs +90 -0
- package/src/lib/log.mjs +40 -0
- package/src/lib/project-doctor.mjs +329 -0
- package/src/lib/registry.mjs +102 -0
- package/src/lib/rename.mjs +116 -0
- package/src/lib/schema.mjs +123 -0
- package/src/lib/toggle.mjs +130 -0
- package/src/lib/tokens.mjs +96 -0
- package/src/lib/toml.mjs +137 -0
- package/src/lib/upgrade.mjs +159 -0
- package/src/lib/verify.mjs +97 -0
- package/src/scaffold.mjs +347 -0
- package/src/verify.mjs +4 -0
- package/src/versions/registry.json +42 -0
- package/template/.github/workflows/verify.yml +70 -0
- package/template/build.gradle.kts +17 -0
- package/template/composeApp/build.gradle.kts +257 -0
- package/template/composeApp/google-services.json +29 -0
- package/template/composeApp/proguard-rules.pro +34 -0
- package/template/composeApp/src/androidDebug/AndroidManifest.xml +13 -0
- package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/ComposeRootRegistry.kt +40 -0
- package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/InspectorCatalog.kt +80 -0
- package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/InspectorHttpServer.kt +322 -0
- package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/InspectorInit.kt +16 -0
- package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/LiveSemanticsJson.kt +102 -0
- package/template/composeApp/src/androidDebug/kotlin/com/example/app/inspector/RemoteControlPage.kt +106 -0
- package/template/composeApp/src/androidDebug/res/xml/debug_network_security_config.xml +9 -0
- package/template/composeApp/src/androidMain/AndroidManifest.xml +28 -0
- package/template/composeApp/src/androidMain/kotlin/com/example/app/AppApplication.kt +72 -0
- package/template/composeApp/src/androidMain/kotlin/com/example/app/MainActivity.kt +24 -0
- package/template/composeApp/src/androidMain/kotlin/com/example/app/core/connectivity/NetworkMonitor.kt +42 -0
- package/template/composeApp/src/androidMain/kotlin/com/example/app/data/local/DatabaseBuilder.android.kt +13 -0
- package/template/composeApp/src/androidMain/kotlin/com/example/app/di/AndroidModule.kt +9 -0
- package/template/composeApp/src/androidMain/kotlin/com/example/app/presentation/components/TestTagAutomation.android.kt +11 -0
- package/template/composeApp/src/androidMain/res/drawable/ic_launcher_background.xml +9 -0
- package/template/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml +6 -0
- package/template/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml +6 -0
- package/template/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_foreground.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_foreground.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_foreground.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_foreground.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_foreground.png +0 -0
- package/template/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
- package/template/composeApp/src/androidMain/res/values/colors.xml +6 -0
- package/template/composeApp/src/androidMain/res/values/themes.xml +9 -0
- package/template/composeApp/src/androidMain/res/values-v31/themes.xml +8 -0
- package/template/composeApp/src/androidRelease/kotlin/com/example/app/inspector/InspectorInit.kt +13 -0
- package/template/composeApp/src/commonMain/composeResources/font/DMSans_Bold.ttf +0 -0
- package/template/composeApp/src/commonMain/composeResources/font/DMSans_Medium.ttf +0 -0
- package/template/composeApp/src/commonMain/composeResources/font/DMSans_Regular.ttf +0 -0
- package/template/composeApp/src/commonMain/composeResources/font/DMSans_SemiBold.ttf +0 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/App.kt +7 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/core/connectivity/NetworkMonitor.kt +7 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/data/local/AppDatabase.kt +28 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/data/local/DatabaseBuilder.kt +14 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/data/local/ItemDao.kt +18 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/data/remote/FirebaseConfig.kt +5 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/data/remote/ItemRepositoryImpl.kt +22 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/di/AppModule.kt +23 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/domain/model/Item.kt +8 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/domain/repository/ItemRepository.kt +8 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/domain/usecase/GetItemsUseCase.kt +12 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/App.kt +13 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/BaseScreen.kt +69 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/components/TestTagAutomation.kt +18 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/home/DetailScreen.kt +39 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/home/HomeScreen.kt +87 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/home/HomeViewModel.kt +35 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/navigation/AppNavHost.kt +43 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/navigation/AppShell.kt +144 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/navigation/AppTab.kt +25 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/navigation/Screen.kt +15 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/profile/ProfileScreen.kt +43 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/theme/DesignToken.kt +25 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/theme/Shape.kt +12 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/theme/Theme.kt +64 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/theme/Tokens.kt +19 -0
- package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/theme/Typography.kt +36 -0
- package/template/composeApp/src/desktopMain/kotlin/com/example/app/core/connectivity/NetworkMonitor.desktop.kt +9 -0
- package/template/composeApp/src/desktopMain/kotlin/com/example/app/data/local/DatabaseBuilder.desktop.kt +13 -0
- package/template/composeApp/src/desktopMain/kotlin/com/example/app/di/DesktopModule.kt +32 -0
- package/template/composeApp/src/desktopMain/kotlin/com/example/app/main.kt +29 -0
- package/template/composeApp/src/desktopMain/kotlin/com/example/app/presentation/components/TestTagAutomation.desktop.kt +6 -0
- package/template/composeApp/src/iosMain/kotlin/com/example/app/KoinHelper.kt +50 -0
- package/template/composeApp/src/iosMain/kotlin/com/example/app/MainViewController.kt +10 -0
- package/template/composeApp/src/iosMain/kotlin/com/example/app/core/connectivity/NetworkMonitor.kt +32 -0
- package/template/composeApp/src/iosMain/kotlin/com/example/app/data/local/DatabaseBuilder.ios.kt +10 -0
- package/template/composeApp/src/iosMain/kotlin/com/example/app/presentation/components/TestTagAutomation.ios.kt +11 -0
- package/template/docs/dev-client.md +52 -0
- package/template/gitignore +21 -0
- package/template/gradle/libs.versions.toml +100 -0
- package/template/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/template/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/template/gradle.properties +9 -0
- package/template/gradlew +152 -0
- package/template/gradlew.bat +94 -0
- package/template/iosApp/Podfile +27 -0
- package/template/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-1024.png +0 -0
- package/template/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json +14 -0
- package/template/iosApp/iosApp/Assets.xcassets/Contents.json +6 -0
- package/template/iosApp/iosApp/ContentView.swift +17 -0
- package/template/iosApp/iosApp/GoogleService-Info.plist +34 -0
- package/template/iosApp/iosApp/Info.plist +43 -0
- package/template/iosApp/iosApp/PrivacyInfo.xcprivacy +42 -0
- package/template/iosApp/iosApp/iOSApp.swift +33 -0
- package/template/iosApp/project.yml +56 -0
- package/template/local.properties.example +2 -0
- package/template/manifest.json +125 -0
- package/template/qa/appium/README.md +23 -0
- package/template/qa/appium/lib/appium-client.mjs +225 -0
- package/template/qa/appium/package.json +8 -0
- package/template/qa/appium/run-android-smoke.mjs +39 -0
- package/template/settings.gradle.kts +34 -0
- package/template/tests/appium/cmp/conftest.py +96 -0
- package/template/tests/appium/cmp/test_smoke.py +17 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
package __PACKAGE__
|
|
2
|
+
|
|
3
|
+
import android.graphics.Color
|
|
4
|
+
import android.os.Bundle
|
|
5
|
+
import androidx.activity.ComponentActivity
|
|
6
|
+
import androidx.activity.SystemBarStyle
|
|
7
|
+
import androidx.activity.compose.setContent
|
|
8
|
+
import androidx.activity.enableEdgeToEdge
|
|
9
|
+
|
|
10
|
+
class MainActivity : ComponentActivity() {
|
|
11
|
+
|
|
12
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
13
|
+
super.onCreate(savedInstanceState)
|
|
14
|
+
// Edge-to-edge: the status and navigation bars are transparent and content draws
|
|
15
|
+
// beneath them. Screens own their insets in shared code via BaseScreen's Scaffold
|
|
16
|
+
// (statusBarsPadding / navigationBarsPadding), which also maps to iOS safe areas
|
|
17
|
+
// under Compose Multiplatform. Never set the deprecated window.statusBarColor on API 35.
|
|
18
|
+
enableEdgeToEdge(
|
|
19
|
+
statusBarStyle = SystemBarStyle.dark(Color.TRANSPARENT),
|
|
20
|
+
navigationBarStyle = SystemBarStyle.light(Color.TRANSPARENT, Color.TRANSPARENT),
|
|
21
|
+
)
|
|
22
|
+
setContent { App() }
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
package __PACKAGE__.core.connectivity
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.net.ConnectivityManager
|
|
5
|
+
import android.net.Network
|
|
6
|
+
import android.net.NetworkCapabilities
|
|
7
|
+
import android.net.NetworkRequest
|
|
8
|
+
import kotlinx.coroutines.flow.MutableStateFlow
|
|
9
|
+
import kotlinx.coroutines.flow.StateFlow
|
|
10
|
+
import kotlinx.coroutines.flow.asStateFlow
|
|
11
|
+
|
|
12
|
+
actual class NetworkMonitor actual constructor(context: Any?) {
|
|
13
|
+
|
|
14
|
+
private val cm = (context as Context)
|
|
15
|
+
.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
|
16
|
+
|
|
17
|
+
private val _isOnline = MutableStateFlow(
|
|
18
|
+
cm.activeNetwork
|
|
19
|
+
?.let { cm.getNetworkCapabilities(it) }
|
|
20
|
+
?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) == true
|
|
21
|
+
)
|
|
22
|
+
actual val isOnline: StateFlow<Boolean> = _isOnline.asStateFlow()
|
|
23
|
+
|
|
24
|
+
init {
|
|
25
|
+
val request = NetworkRequest.Builder()
|
|
26
|
+
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
|
27
|
+
.build()
|
|
28
|
+
|
|
29
|
+
val cb = object : ConnectivityManager.NetworkCallback() {
|
|
30
|
+
override fun onAvailable(network: Network) {
|
|
31
|
+
_isOnline.value = true
|
|
32
|
+
}
|
|
33
|
+
override fun onLost(network: Network) {
|
|
34
|
+
_isOnline.value = cm.activeNetwork != null
|
|
35
|
+
}
|
|
36
|
+
override fun onUnavailable() {
|
|
37
|
+
_isOnline.value = false
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
cm.registerNetworkCallback(request, cb)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
package __PACKAGE__.data.local
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import androidx.room.Room
|
|
5
|
+
import androidx.room.RoomDatabase
|
|
6
|
+
|
|
7
|
+
lateinit var appContext: Context
|
|
8
|
+
|
|
9
|
+
actual fun getDatabaseBuilder(): RoomDatabase.Builder<AppDatabase> =
|
|
10
|
+
Room.databaseBuilder(
|
|
11
|
+
context = appContext,
|
|
12
|
+
name = appContext.getDatabasePath("app.db").absolutePath
|
|
13
|
+
)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
package __PACKAGE__.di
|
|
2
|
+
|
|
3
|
+
import org.koin.core.module.Module
|
|
4
|
+
import org.koin.dsl.module
|
|
5
|
+
|
|
6
|
+
// Android-only DI bindings (platform services that need a Context, etc.).
|
|
7
|
+
// Empty by default — add your platform-specific singletons here.
|
|
8
|
+
val androidModule: Module = module {
|
|
9
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
package __PACKAGE__.presentation.components
|
|
2
|
+
|
|
3
|
+
import androidx.compose.ui.ExperimentalComposeUiApi
|
|
4
|
+
import androidx.compose.ui.Modifier
|
|
5
|
+
import androidx.compose.ui.semantics.semantics
|
|
6
|
+
import androidx.compose.ui.semantics.testTagsAsResourceId
|
|
7
|
+
|
|
8
|
+
/** Android: surface testTags as `resource-id` entries for uiautomator/Appium id-selectors. */
|
|
9
|
+
@OptIn(ExperimentalComposeUiApi::class)
|
|
10
|
+
actual fun Modifier.exposeTestTagsForAutomation(): Modifier =
|
|
11
|
+
semantics { testTagsAsResourceId = true }
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<!-- Adaptive-icon background: a brand gradient tile. Replace with your own brand colours. -->
|
|
3
|
+
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
|
4
|
+
<gradient
|
|
5
|
+
android:type="linear"
|
|
6
|
+
android:angle="270"
|
|
7
|
+
android:startColor="#0E2E50"
|
|
8
|
+
android:endColor="#0A2540" />
|
|
9
|
+
</shape>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
3
|
+
<background android:drawable="@drawable/ic_launcher_background" />
|
|
4
|
+
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
|
|
5
|
+
<monochrome android:drawable="@mipmap/ic_launcher_foreground" />
|
|
6
|
+
</adaptive-icon>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
3
|
+
<background android:drawable="@drawable/ic_launcher_background" />
|
|
4
|
+
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
|
|
5
|
+
<monochrome android:drawable="@mipmap/ic_launcher_foreground" />
|
|
6
|
+
</adaptive-icon>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<resources>
|
|
3
|
+
<!-- Brand primary, used as the window background so there is no white flash before
|
|
4
|
+
Compose draws its first frame. Keep in sync with __THEME_PREFIX__Colors.Primary. -->
|
|
5
|
+
<color name="app_window_background">#0A2540</color>
|
|
6
|
+
</resources>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<resources>
|
|
3
|
+
<!-- NoActionBar + a brand-coloured window background so the very first frames
|
|
4
|
+
(system launch window, any pre-Compose frame) match the app. Status/nav bar
|
|
5
|
+
styling is set in MainActivity (edge-to-edge), not here. -->
|
|
6
|
+
<style name="Theme.App" parent="@android:style/Theme.Material.Light.NoActionBar">
|
|
7
|
+
<item name="android:windowBackground">@color/app_window_background</item>
|
|
8
|
+
</style>
|
|
9
|
+
</resources>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<resources>
|
|
3
|
+
<!-- API 31+ platform splash: brand background so the splash window matches frame 0. -->
|
|
4
|
+
<style name="Theme.App" parent="@android:style/Theme.Material.Light.NoActionBar">
|
|
5
|
+
<item name="android:windowBackground">@color/app_window_background</item>
|
|
6
|
+
<item name="android:windowSplashScreenBackground">@color/app_window_background</item>
|
|
7
|
+
</style>
|
|
8
|
+
</resources>
|
package/template/composeApp/src/androidRelease/kotlin/com/example/app/inspector/InspectorInit.kt
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
package __PACKAGE__.inspector
|
|
2
|
+
|
|
3
|
+
import android.app.Application
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* RELEASE variant: no-op twin of the androidDebug `startInspector()`. Release builds compile
|
|
7
|
+
* this body instead — no server class, no registry, no inspector endpoint strings. The
|
|
8
|
+
* inspector is structurally absent from release, not merely disabled.
|
|
9
|
+
*/
|
|
10
|
+
@Suppress("UnusedReceiverParameter")
|
|
11
|
+
fun Application.startInspector() {
|
|
12
|
+
// Intentionally empty.
|
|
13
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
package __PACKAGE__.data.local
|
|
2
|
+
|
|
3
|
+
import androidx.room.ConstructedBy
|
|
4
|
+
import androidx.room.Database
|
|
5
|
+
import androidx.room.Entity
|
|
6
|
+
import androidx.room.PrimaryKey
|
|
7
|
+
import androidx.room.RoomDatabase
|
|
8
|
+
import androidx.room.RoomDatabaseConstructor
|
|
9
|
+
|
|
10
|
+
// ── Example Room entity for the `home` feature's offline cache ───────────────────────
|
|
11
|
+
@Entity(tableName = "items")
|
|
12
|
+
data class ItemEntity(
|
|
13
|
+
@PrimaryKey val id: String,
|
|
14
|
+
val title: String,
|
|
15
|
+
val subtitle: String,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
@Database(entities = [ItemEntity::class], version = 1, exportSchema = true)
|
|
19
|
+
@ConstructedBy(AppDatabaseConstructor::class)
|
|
20
|
+
abstract class AppDatabase : RoomDatabase() {
|
|
21
|
+
abstract fun itemDao(): ItemDao
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// The actual implementation is generated by Room's KSP processor per platform.
|
|
25
|
+
@Suppress("NO_ACTUAL_FOR_EXPECT", "KotlinNoActualForExpect")
|
|
26
|
+
expect object AppDatabaseConstructor : RoomDatabaseConstructor<AppDatabase> {
|
|
27
|
+
override fun initialize(): AppDatabase
|
|
28
|
+
}
|
package/template/composeApp/src/commonMain/kotlin/com/example/app/data/local/DatabaseBuilder.kt
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
package __PACKAGE__.data.local
|
|
2
|
+
|
|
3
|
+
import androidx.room.RoomDatabase
|
|
4
|
+
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
|
|
5
|
+
|
|
6
|
+
expect fun getDatabaseBuilder(): RoomDatabase.Builder<AppDatabase>
|
|
7
|
+
|
|
8
|
+
fun buildDatabase(): AppDatabase =
|
|
9
|
+
getDatabaseBuilder()
|
|
10
|
+
// BundledSQLiteDriver is required for Room KMP on Kotlin/Native (iOS).
|
|
11
|
+
.setDriver(BundledSQLiteDriver())
|
|
12
|
+
.fallbackToDestructiveMigration(dropAllTables = true)
|
|
13
|
+
.fallbackToDestructiveMigrationOnDowngrade(dropAllTables = true)
|
|
14
|
+
.build()
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
package __PACKAGE__.data.local
|
|
2
|
+
|
|
3
|
+
import androidx.room.Dao
|
|
4
|
+
import androidx.room.Insert
|
|
5
|
+
import androidx.room.OnConflictStrategy
|
|
6
|
+
import androidx.room.Query
|
|
7
|
+
|
|
8
|
+
@Dao
|
|
9
|
+
interface ItemDao {
|
|
10
|
+
@Query("SELECT * FROM items")
|
|
11
|
+
suspend fun getAll(): List<ItemEntity>
|
|
12
|
+
|
|
13
|
+
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
|
14
|
+
suspend fun upsertAll(items: List<ItemEntity>)
|
|
15
|
+
|
|
16
|
+
@Query("DELETE FROM items")
|
|
17
|
+
suspend fun clear()
|
|
18
|
+
}
|
package/template/composeApp/src/commonMain/kotlin/com/example/app/data/remote/ItemRepositoryImpl.kt
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
package __PACKAGE__.data.remote
|
|
2
|
+
|
|
3
|
+
import __PACKAGE__.domain.model.Item
|
|
4
|
+
import __PACKAGE__.domain.repository.ItemRepository
|
|
5
|
+
import kotlinx.coroutines.delay
|
|
6
|
+
|
|
7
|
+
// Example data source for the `home` feature. This is intentionally dependency-light
|
|
8
|
+
// (no Firebase / no Room coupling) so the scaffold builds in every feature combination.
|
|
9
|
+
//
|
|
10
|
+
// Real apps swap this for a Firestore/Ktor source and add a Room cache (see data/local).
|
|
11
|
+
// The Clean Architecture seam is the ItemRepository interface in the domain layer.
|
|
12
|
+
class ItemRepositoryImpl : ItemRepository {
|
|
13
|
+
override suspend fun getItems(): List<Item> {
|
|
14
|
+
delay(300) // simulate I/O
|
|
15
|
+
return listOf(
|
|
16
|
+
Item("1", "Welcome to __APP_NAME__", "Your Compose Multiplatform app is wired end-to-end."),
|
|
17
|
+
Item("2", "Clean Architecture", "presentation → domain → data, with Koin DI."),
|
|
18
|
+
Item("3", "Edge-to-edge, pre-solved", "BaseScreen owns the window insets for you."),
|
|
19
|
+
Item("4", "Android + iOS", "One codebase, two green builds."),
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
package __PACKAGE__.di
|
|
2
|
+
|
|
3
|
+
import __PACKAGE__.data.remote.ItemRepositoryImpl
|
|
4
|
+
import __PACKAGE__.domain.repository.ItemRepository
|
|
5
|
+
import __PACKAGE__.domain.usecase.GetItemsUseCase
|
|
6
|
+
import __PACKAGE__.presentation.home.HomeViewModel
|
|
7
|
+
import org.koin.core.module.dsl.viewModelOf
|
|
8
|
+
import org.koin.dsl.module
|
|
9
|
+
|
|
10
|
+
val repositoryModule = module {
|
|
11
|
+
single<ItemRepository> { ItemRepositoryImpl() }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
val useCaseModule = module {
|
|
15
|
+
factory { GetItemsUseCase(get()) }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
val viewModelModule = module {
|
|
19
|
+
viewModelOf(::HomeViewModel)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Aggregated common modules, started from AppApplication (Android) and KoinHelper (iOS).
|
|
23
|
+
val appModules = listOf(repositoryModule, useCaseModule, viewModelModule)
|
package/template/composeApp/src/commonMain/kotlin/com/example/app/domain/usecase/GetItemsUseCase.kt
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
package __PACKAGE__.domain.usecase
|
|
2
|
+
|
|
3
|
+
import __PACKAGE__.domain.model.Item
|
|
4
|
+
import __PACKAGE__.domain.repository.ItemRepository
|
|
5
|
+
|
|
6
|
+
// A use case is a single business action. ViewModels depend on use cases, not repositories
|
|
7
|
+
// directly, so business rules stay testable and out of the presentation layer.
|
|
8
|
+
class GetItemsUseCase(
|
|
9
|
+
private val repository: ItemRepository,
|
|
10
|
+
) {
|
|
11
|
+
suspend operator fun invoke(): List<Item> = repository.getItems()
|
|
12
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
package __PACKAGE__.presentation
|
|
2
|
+
|
|
3
|
+
import androidx.compose.runtime.Composable
|
|
4
|
+
import __PACKAGE__.presentation.navigation.AppNavHost
|
|
5
|
+
import __PACKAGE__.presentation.theme.__THEME_PREFIX__Theme
|
|
6
|
+
|
|
7
|
+
// Root composable. Wraps the whole app in the theme and hosts navigation.
|
|
8
|
+
@Composable
|
|
9
|
+
fun App() {
|
|
10
|
+
__THEME_PREFIX__Theme {
|
|
11
|
+
AppNavHost()
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
package __PACKAGE__.presentation.components
|
|
2
|
+
|
|
3
|
+
import androidx.compose.foundation.layout.Box
|
|
4
|
+
import androidx.compose.foundation.layout.PaddingValues
|
|
5
|
+
import androidx.compose.foundation.layout.WindowInsets
|
|
6
|
+
import androidx.compose.foundation.layout.consumeWindowInsets
|
|
7
|
+
import androidx.compose.foundation.layout.fillMaxSize
|
|
8
|
+
import androidx.compose.foundation.layout.navigationBarsPadding
|
|
9
|
+
import androidx.compose.foundation.layout.padding
|
|
10
|
+
import androidx.compose.foundation.layout.statusBarsPadding
|
|
11
|
+
import androidx.compose.material3.Scaffold
|
|
12
|
+
import androidx.compose.runtime.Composable
|
|
13
|
+
import androidx.compose.ui.Modifier
|
|
14
|
+
import androidx.compose.ui.graphics.Color
|
|
15
|
+
import __PACKAGE__.presentation.theme.designToken
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The insets moat, pre-solved. Every screen wraps its content in [BaseScreen] instead of
|
|
19
|
+
* re-deriving edge-to-edge padding. The Activity is edge-to-edge (transparent system bars);
|
|
20
|
+
* this Scaffold owns the status-bar / navigation-bar insets in shared code, which also maps
|
|
21
|
+
* to iOS safe areas under Compose Multiplatform.
|
|
22
|
+
*
|
|
23
|
+
* - [topBar] / [bottomBar] draw edge-to-edge (e.g. a nav bar that bleeds behind the gesture
|
|
24
|
+
* pill) and are responsible for their own inset padding.
|
|
25
|
+
* - The content lambda receives padding already accounting for any bars; by default the body
|
|
26
|
+
* gets status + navigation bar padding so plain screens are safe with zero ceremony.
|
|
27
|
+
*/
|
|
28
|
+
@Composable
|
|
29
|
+
fun BaseScreen(
|
|
30
|
+
modifier: Modifier = Modifier,
|
|
31
|
+
containerColor: Color = Color.Unspecified,
|
|
32
|
+
applyStatusBarPadding: Boolean = true,
|
|
33
|
+
applyNavBarPadding: Boolean = true,
|
|
34
|
+
topBar: @Composable () -> Unit = {},
|
|
35
|
+
bottomBar: @Composable () -> Unit = {},
|
|
36
|
+
content: @Composable (PaddingValues) -> Unit,
|
|
37
|
+
) {
|
|
38
|
+
Scaffold(
|
|
39
|
+
modifier = modifier,
|
|
40
|
+
// Screens manage their own insets via the padding modifiers below, so the Scaffold
|
|
41
|
+
// itself consumes nothing — this avoids the classic doubled-padding bug.
|
|
42
|
+
contentWindowInsets = WindowInsets(0, 0, 0, 0),
|
|
43
|
+
containerColor = if (containerColor == Color.Unspecified)
|
|
44
|
+
androidx.compose.material3.MaterialTheme.colorScheme.background else containerColor,
|
|
45
|
+
topBar = topBar,
|
|
46
|
+
bottomBar = bottomBar,
|
|
47
|
+
) { innerPadding ->
|
|
48
|
+
Box(
|
|
49
|
+
modifier = Modifier
|
|
50
|
+
.fillMaxSize()
|
|
51
|
+
.padding(innerPadding)
|
|
52
|
+
.consumeWindowInsets(innerPadding)
|
|
53
|
+
.then(if (applyStatusBarPadding) Modifier.statusBarsPadding() else Modifier)
|
|
54
|
+
.then(if (applyNavBarPadding) Modifier.navigationBarsPadding() else Modifier)
|
|
55
|
+
// Inspector: self-report the inset facts this Box actually applies. No dimen
|
|
56
|
+
// token is used here — the padding comes from Scaffold insets, not the design
|
|
57
|
+
// system — so the tokens list is empty and only the resolved facts are emitted.
|
|
58
|
+
.designToken(
|
|
59
|
+
tokens = emptyList(),
|
|
60
|
+
resolved = mapOf(
|
|
61
|
+
"statusBarPadding" to applyStatusBarPadding.toString(),
|
|
62
|
+
"navBarPadding" to applyNavBarPadding.toString(),
|
|
63
|
+
),
|
|
64
|
+
)
|
|
65
|
+
) {
|
|
66
|
+
content(innerPadding)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
package __PACKAGE__.presentation.components
|
|
2
|
+
|
|
3
|
+
import androidx.compose.ui.Modifier
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Exposes every `Modifier.testTag` in the subtree to the platform's UI-automation layer,
|
|
7
|
+
* so Appium/uiautomator id-selectors (and the cmp-test-generated suites) find them on a
|
|
8
|
+
* stock build with no extra flags.
|
|
9
|
+
*
|
|
10
|
+
* - Android actual: `semantics { testTagsAsResourceId = true }` → tags surface as
|
|
11
|
+
* `resource-id` in the uiautomator/Appium tree.
|
|
12
|
+
* - iOS actual: same semantics flag → tags surface as `accessibilityIdentifier` (XCUITest).
|
|
13
|
+
* - Desktop actual: no-op — the skiko `ui` artifact has no `testTagsAsResourceId`
|
|
14
|
+
* (there is no resource-id concept on desktop; tests use the Compose test APIs directly).
|
|
15
|
+
*
|
|
16
|
+
* Apply once at the root (AppShell does); it covers the whole subtree.
|
|
17
|
+
*/
|
|
18
|
+
expect fun Modifier.exposeTestTagsForAutomation(): Modifier
|
package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/home/DetailScreen.kt
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
package __PACKAGE__.presentation.home
|
|
2
|
+
|
|
3
|
+
import androidx.compose.foundation.layout.Column
|
|
4
|
+
import androidx.compose.foundation.layout.fillMaxSize
|
|
5
|
+
import androidx.compose.foundation.layout.padding
|
|
6
|
+
import androidx.compose.material3.MaterialTheme
|
|
7
|
+
import androidx.compose.material3.Text
|
|
8
|
+
import androidx.compose.material3.TextButton
|
|
9
|
+
import androidx.compose.runtime.Composable
|
|
10
|
+
import androidx.compose.ui.Modifier
|
|
11
|
+
import __PACKAGE__.presentation.components.BaseScreen
|
|
12
|
+
import __PACKAGE__.presentation.theme.__THEME_PREFIX__Tokens
|
|
13
|
+
import __PACKAGE__.presentation.theme.designToken
|
|
14
|
+
|
|
15
|
+
@Composable
|
|
16
|
+
fun DetailScreen(
|
|
17
|
+
itemId: String,
|
|
18
|
+
onBack: () -> Unit,
|
|
19
|
+
) {
|
|
20
|
+
BaseScreen {
|
|
21
|
+
Column(
|
|
22
|
+
Modifier
|
|
23
|
+
.fillMaxSize()
|
|
24
|
+
.designToken(
|
|
25
|
+
tokens = listOf("PaddingPage"),
|
|
26
|
+
resolved = mapOf("padding" to "16dp"),
|
|
27
|
+
)
|
|
28
|
+
.padding(__THEME_PREFIX__Tokens.PaddingPage),
|
|
29
|
+
) {
|
|
30
|
+
TextButton(onClick = onBack) { Text("← Back") }
|
|
31
|
+
Text("Detail", style = MaterialTheme.typography.headlineMedium)
|
|
32
|
+
Text(
|
|
33
|
+
"Item id: $itemId",
|
|
34
|
+
style = MaterialTheme.typography.bodyLarge,
|
|
35
|
+
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
package/template/composeApp/src/commonMain/kotlin/com/example/app/presentation/home/HomeScreen.kt
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
package __PACKAGE__.presentation.home
|
|
2
|
+
|
|
3
|
+
import androidx.compose.foundation.clickable
|
|
4
|
+
import androidx.compose.foundation.layout.Arrangement
|
|
5
|
+
import androidx.compose.foundation.layout.Box
|
|
6
|
+
import androidx.compose.foundation.layout.Column
|
|
7
|
+
import androidx.compose.foundation.layout.fillMaxSize
|
|
8
|
+
import androidx.compose.foundation.layout.fillMaxWidth
|
|
9
|
+
import androidx.compose.foundation.layout.padding
|
|
10
|
+
import androidx.compose.foundation.lazy.LazyColumn
|
|
11
|
+
import androidx.compose.foundation.lazy.items
|
|
12
|
+
import androidx.compose.material3.CircularProgressIndicator
|
|
13
|
+
import androidx.compose.material3.MaterialTheme
|
|
14
|
+
import androidx.compose.material3.Surface
|
|
15
|
+
import androidx.compose.material3.Text
|
|
16
|
+
import androidx.compose.runtime.Composable
|
|
17
|
+
import androidx.compose.runtime.getValue
|
|
18
|
+
import androidx.compose.ui.Alignment
|
|
19
|
+
import androidx.compose.ui.Modifier
|
|
20
|
+
import androidx.compose.ui.semantics.semantics
|
|
21
|
+
import androidx.compose.ui.semantics.testTag
|
|
22
|
+
import androidx.compose.ui.unit.dp
|
|
23
|
+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
24
|
+
import __PACKAGE__.presentation.theme.__THEME_PREFIX__Tokens
|
|
25
|
+
import __PACKAGE__.presentation.theme.designToken
|
|
26
|
+
import org.koin.compose.viewmodel.koinViewModel
|
|
27
|
+
|
|
28
|
+
@Composable
|
|
29
|
+
fun HomeScreen(
|
|
30
|
+
onItemClick: (String) -> Unit,
|
|
31
|
+
viewModel: HomeViewModel = koinViewModel(),
|
|
32
|
+
) {
|
|
33
|
+
val state by viewModel.state.collectAsStateWithLifecycle()
|
|
34
|
+
|
|
35
|
+
Column(
|
|
36
|
+
Modifier
|
|
37
|
+
.fillMaxSize()
|
|
38
|
+
.designToken(
|
|
39
|
+
tokens = listOf("PaddingPage"),
|
|
40
|
+
resolved = mapOf("padding" to "16dp"),
|
|
41
|
+
)
|
|
42
|
+
.padding(__THEME_PREFIX__Tokens.PaddingPage),
|
|
43
|
+
) {
|
|
44
|
+
Text(
|
|
45
|
+
text = "Home",
|
|
46
|
+
style = MaterialTheme.typography.headlineMedium,
|
|
47
|
+
modifier = Modifier.semantics { testTag = "home_title" }.padding(bottom = 12.dp),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
if (state.isLoading) {
|
|
51
|
+
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
|
52
|
+
CircularProgressIndicator()
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
LazyColumn(verticalArrangement = Arrangement.spacedBy(__THEME_PREFIX__Tokens.GapCard)) {
|
|
56
|
+
items(state.items, key = { it.id }) { item ->
|
|
57
|
+
Surface(
|
|
58
|
+
color = MaterialTheme.colorScheme.surface,
|
|
59
|
+
shape = MaterialTheme.shapes.medium,
|
|
60
|
+
tonalElevation = __THEME_PREFIX__Tokens.ElevationCard,
|
|
61
|
+
modifier = Modifier
|
|
62
|
+
.fillMaxWidth()
|
|
63
|
+
.designToken(
|
|
64
|
+
tokens = listOf("RadiusCard", "ElevationCard", "PaddingCard"),
|
|
65
|
+
resolved = mapOf(
|
|
66
|
+
"radius" to "16dp",
|
|
67
|
+
"elevation" to "2dp",
|
|
68
|
+
"padding" to "16dp",
|
|
69
|
+
"color" to "#FFFFFFFF",
|
|
70
|
+
),
|
|
71
|
+
)
|
|
72
|
+
.clickable { onItemClick(item.id) },
|
|
73
|
+
) {
|
|
74
|
+
Column(Modifier.padding(__THEME_PREFIX__Tokens.PaddingCard)) {
|
|
75
|
+
Text(item.title, style = MaterialTheme.typography.titleMedium)
|
|
76
|
+
Text(
|
|
77
|
+
item.subtitle,
|
|
78
|
+
style = MaterialTheme.typography.bodyMedium,
|
|
79
|
+
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|