From 3eb99d9d1a4a0570f15ac4e80823f87d4672a048 Mon Sep 17 00:00:00 2001 From: Kohei Date: Wed, 18 Feb 2026 19:17:29 +0900 Subject: [PATCH] feat: add Android app scaffold with clean architecture Multi-module Kotlin/Compose project with WebSocket chat client: - app: MainActivity, DI setup with Hilt - core/data: Room DB, WebSocket client, repository impl - core/domain: models, repository interface, use cases - core/ui: Material3 theme - feature/chat: ChatScreen with message bubbles, image preview, connection banner Co-Authored-By: Claude Opus 4.6 --- .gitignore | 7 + android/.gitignore | 13 ++ android/app/build.gradle.kts | 64 +++++++ android/app/proguard-rules.pro | 6 + android/app/src/main/AndroidManifest.xml | 26 +++ .../java/io/picoclaw/android/MainActivity.kt | 20 +++ .../java/io/picoclaw/android/PicoClawApp.kt | 16 ++ .../java/io/picoclaw/android/di/AppModule.kt | 65 +++++++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + android/app/src/main/res/values/colors.xml | 5 + .../main/res/values/ic_launcher_colors.xml | 5 + android/app/src/main/res/values/strings.xml | 3 + android/app/src/main/res/values/themes.xml | 4 + .../main/res/xml/network_security_config.xml | 7 + android/build.gradle.kts | 7 + android/core/data/build.gradle.kts | 35 ++++ .../android/core/data/local/AppDatabase.kt | 14 ++ .../core/data/local/converter/Converters.kt | 17 ++ .../android/core/data/local/dao/MessageDao.kt | 24 +++ .../core/data/local/entity/MessageEntity.kt | 14 ++ .../android/core/data/mapper/MessageMapper.kt | 67 ++++++++ .../core/data/remote/WebSocketClient.kt | 94 +++++++++++ .../core/data/remote/dto/WsIncoming.kt | 11 ++ .../core/data/remote/dto/WsOutgoing.kt | 6 + .../data/repository/ChatRepositoryImpl.kt | 72 ++++++++ android/core/domain/build.gradle.kts | 21 +++ .../android/core/domain/model/ChatMessage.kt | 14 ++ .../core/domain/model/ConnectionState.kt | 5 + .../core/domain/model/ImageAttachment.kt | 7 + .../core/domain/repository/ChatRepository.kt | 15 ++ .../domain/usecase/LoadMoreMessagesUseCase.kt | 9 + .../usecase/ObserveConnectionUseCase.kt | 9 + .../domain/usecase/ObserveMessagesUseCase.kt | 9 + .../core/domain/usecase/SendMessageUseCase.kt | 10 ++ android/core/ui/build.gradle.kts | 31 ++++ .../picoclaw/android/core/ui/theme/Color.kt | 17 ++ .../picoclaw/android/core/ui/theme/Theme.kt | 45 +++++ .../io/picoclaw/android/core/ui/theme/Type.kt | 17 ++ android/feature/chat/build.gradle.kts | 43 +++++ .../android/feature/chat/ChatEvent.kt | 13 ++ .../android/feature/chat/ChatUiState.kt | 15 ++ .../android/feature/chat/ChatViewModel.kt | 85 ++++++++++ .../chat/component/ConnectionBanner.kt | 45 +++++ .../feature/chat/component/ImagePreview.kt | 74 ++++++++ .../feature/chat/component/MessageBubble.kt | 85 ++++++++++ .../feature/chat/component/MessageInput.kt | 61 +++++++ .../feature/chat/component/MessageList.kt | 50 ++++++ .../android/feature/chat/screen/ChatScreen.kt | 79 +++++++++ android/gradle.properties | 6 + android/gradle/libs.versions.toml | 56 +++++++ android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43583 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + android/gradlew | 158 ++++++++++++++++++ android/gradlew.bat | 91 ++++++++++ android/settings.gradle.kts | 20 +++ 55 files changed, 1704 insertions(+) create mode 100644 android/.gitignore create mode 100644 android/app/build.gradle.kts create mode 100644 android/app/proguard-rules.pro create mode 100644 android/app/src/main/AndroidManifest.xml create mode 100644 android/app/src/main/java/io/picoclaw/android/MainActivity.kt create mode 100644 android/app/src/main/java/io/picoclaw/android/PicoClawApp.kt create mode 100644 android/app/src/main/java/io/picoclaw/android/di/AppModule.kt create mode 100644 android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 android/app/src/main/res/values/colors.xml create mode 100644 android/app/src/main/res/values/ic_launcher_colors.xml create mode 100644 android/app/src/main/res/values/strings.xml create mode 100644 android/app/src/main/res/values/themes.xml create mode 100644 android/app/src/main/res/xml/network_security_config.xml create mode 100644 android/build.gradle.kts create mode 100644 android/core/data/build.gradle.kts create mode 100644 android/core/data/src/main/java/io/picoclaw/android/core/data/local/AppDatabase.kt create mode 100644 android/core/data/src/main/java/io/picoclaw/android/core/data/local/converter/Converters.kt create mode 100644 android/core/data/src/main/java/io/picoclaw/android/core/data/local/dao/MessageDao.kt create mode 100644 android/core/data/src/main/java/io/picoclaw/android/core/data/local/entity/MessageEntity.kt create mode 100644 android/core/data/src/main/java/io/picoclaw/android/core/data/mapper/MessageMapper.kt create mode 100644 android/core/data/src/main/java/io/picoclaw/android/core/data/remote/WebSocketClient.kt create mode 100644 android/core/data/src/main/java/io/picoclaw/android/core/data/remote/dto/WsIncoming.kt create mode 100644 android/core/data/src/main/java/io/picoclaw/android/core/data/remote/dto/WsOutgoing.kt create mode 100644 android/core/data/src/main/java/io/picoclaw/android/core/data/repository/ChatRepositoryImpl.kt create mode 100644 android/core/domain/build.gradle.kts create mode 100644 android/core/domain/src/main/java/io/picoclaw/android/core/domain/model/ChatMessage.kt create mode 100644 android/core/domain/src/main/java/io/picoclaw/android/core/domain/model/ConnectionState.kt create mode 100644 android/core/domain/src/main/java/io/picoclaw/android/core/domain/model/ImageAttachment.kt create mode 100644 android/core/domain/src/main/java/io/picoclaw/android/core/domain/repository/ChatRepository.kt create mode 100644 android/core/domain/src/main/java/io/picoclaw/android/core/domain/usecase/LoadMoreMessagesUseCase.kt create mode 100644 android/core/domain/src/main/java/io/picoclaw/android/core/domain/usecase/ObserveConnectionUseCase.kt create mode 100644 android/core/domain/src/main/java/io/picoclaw/android/core/domain/usecase/ObserveMessagesUseCase.kt create mode 100644 android/core/domain/src/main/java/io/picoclaw/android/core/domain/usecase/SendMessageUseCase.kt create mode 100644 android/core/ui/build.gradle.kts create mode 100644 android/core/ui/src/main/java/io/picoclaw/android/core/ui/theme/Color.kt create mode 100644 android/core/ui/src/main/java/io/picoclaw/android/core/ui/theme/Theme.kt create mode 100644 android/core/ui/src/main/java/io/picoclaw/android/core/ui/theme/Type.kt create mode 100644 android/feature/chat/build.gradle.kts create mode 100644 android/feature/chat/src/main/java/io/picoclaw/android/feature/chat/ChatEvent.kt create mode 100644 android/feature/chat/src/main/java/io/picoclaw/android/feature/chat/ChatUiState.kt create mode 100644 android/feature/chat/src/main/java/io/picoclaw/android/feature/chat/ChatViewModel.kt create mode 100644 android/feature/chat/src/main/java/io/picoclaw/android/feature/chat/component/ConnectionBanner.kt create mode 100644 android/feature/chat/src/main/java/io/picoclaw/android/feature/chat/component/ImagePreview.kt create mode 100644 android/feature/chat/src/main/java/io/picoclaw/android/feature/chat/component/MessageBubble.kt create mode 100644 android/feature/chat/src/main/java/io/picoclaw/android/feature/chat/component/MessageInput.kt create mode 100644 android/feature/chat/src/main/java/io/picoclaw/android/feature/chat/component/MessageList.kt create mode 100644 android/feature/chat/src/main/java/io/picoclaw/android/feature/chat/screen/ChatScreen.kt create mode 100644 android/gradle.properties create mode 100644 android/gradle/libs.versions.toml create mode 100644 android/gradle/wrapper/gradle-wrapper.jar create mode 100644 android/gradle/wrapper/gradle-wrapper.properties create mode 100644 android/gradlew create mode 100644 android/gradlew.bat create mode 100644 android/settings.gradle.kts diff --git a/.gitignore b/.gitignore index ce30d749e..08783ba40 100644 --- a/.gitignore +++ b/.gitignore @@ -42,5 +42,12 @@ tasks/ .vscode/ .idea/ +# Android +*.apk +*.aab +*.keystore +*.jks +local.properties + # Added by goreleaser init: dist/ diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 000000000..56ce17a7a --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,13 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties +/app/build +/feature/*/build +/core/*/build diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts new file mode 100644 index 000000000..9149c4fd0 --- /dev/null +++ b/android/app/build.gradle.kts @@ -0,0 +1,64 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.compose.compiler) + alias(libs.plugins.serialization) +} + +android { + namespace = "io.picoclaw.android" + compileSdk = 35 + + defaultConfig { + applicationId = "io.picoclaw.android" + minSdk = 28 + targetSdk = 35 + versionCode = 1 + versionName = "1.0.0" + } + + buildTypes { + release { + isMinifyEnabled = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + buildFeatures { + compose = true + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } +} + +dependencies { + implementation(project(":feature:chat")) + implementation(project(":core:domain")) + implementation(project(":core:data")) + implementation(project(":core:ui")) + + implementation(platform(libs.compose.bom)) + implementation(libs.compose.ui) + implementation(libs.compose.material3) + implementation(libs.activity.compose) + implementation(libs.core.ktx) + implementation(libs.lifecycle.runtime.compose) + + implementation(libs.koin.android) + implementation(libs.koin.compose) + + implementation(libs.ktor.client.okhttp) + implementation(libs.ktor.client.websockets) + + implementation(libs.room.runtime) + implementation(libs.room.ktx) + + implementation(libs.coroutines.android) + + debugImplementation(libs.compose.ui.tooling) +} diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 000000000..226da85ae --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1,6 @@ +-keepattributes *Annotation*, InnerClasses +-dontnote kotlinx.serialization.AnnotationsKt +-keepclassmembers class kotlinx.serialization.json.** { *** Companion; } +-keepclasseswithmembers class io.picoclaw.android.** { + kotlinx.serialization.KSerializer serializer(...); +} diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..6a6ba1bf9 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/java/io/picoclaw/android/MainActivity.kt b/android/app/src/main/java/io/picoclaw/android/MainActivity.kt new file mode 100644 index 000000000..8d1857617 --- /dev/null +++ b/android/app/src/main/java/io/picoclaw/android/MainActivity.kt @@ -0,0 +1,20 @@ +package io.picoclaw.android + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import io.picoclaw.android.core.ui.theme.PicoClawTheme +import io.picoclaw.android.feature.chat.screen.ChatScreen + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + PicoClawTheme { + ChatScreen() + } + } + } +} diff --git a/android/app/src/main/java/io/picoclaw/android/PicoClawApp.kt b/android/app/src/main/java/io/picoclaw/android/PicoClawApp.kt new file mode 100644 index 000000000..b4d3be91e --- /dev/null +++ b/android/app/src/main/java/io/picoclaw/android/PicoClawApp.kt @@ -0,0 +1,16 @@ +package io.picoclaw.android + +import android.app.Application +import io.picoclaw.android.di.appModule +import org.koin.android.ext.koin.androidContext +import org.koin.core.context.startKoin + +class PicoClawApp : Application() { + override fun onCreate() { + super.onCreate() + startKoin { + androidContext(this@PicoClawApp) + modules(appModule) + } + } +} diff --git a/android/app/src/main/java/io/picoclaw/android/di/AppModule.kt b/android/app/src/main/java/io/picoclaw/android/di/AppModule.kt new file mode 100644 index 000000000..9ac3be9b0 --- /dev/null +++ b/android/app/src/main/java/io/picoclaw/android/di/AppModule.kt @@ -0,0 +1,65 @@ +package io.picoclaw.android.di + +import androidx.room.Room +import io.ktor.client.HttpClient +import io.ktor.client.engine.okhttp.OkHttp +import io.ktor.client.plugins.websocket.WebSockets +import io.picoclaw.android.core.data.local.AppDatabase +import io.picoclaw.android.core.data.remote.WebSocketClient +import io.picoclaw.android.core.data.repository.ChatRepositoryImpl +import io.picoclaw.android.core.domain.repository.ChatRepository +import io.picoclaw.android.core.domain.usecase.LoadMoreMessagesUseCase +import io.picoclaw.android.core.domain.usecase.ObserveConnectionUseCase +import io.picoclaw.android.core.domain.usecase.ObserveMessagesUseCase +import io.picoclaw.android.core.domain.usecase.SendMessageUseCase +import io.picoclaw.android.feature.chat.ChatViewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import okhttp3.OkHttpClient +import org.koin.android.ext.koin.androidContext +import org.koin.core.module.dsl.viewModel +import org.koin.dsl.module +import java.util.concurrent.TimeUnit + +val appModule = module { + // CoroutineScope + single { CoroutineScope(SupervisorJob() + Dispatchers.IO) } + + // Room + single { + Room.databaseBuilder( + androidContext(), + AppDatabase::class.java, + "picoclaw.db" + ).build() + } + single { get().messageDao() } + + // Ktor HttpClient + single { + HttpClient(OkHttp) { + install(WebSockets) + engine { + preconfigured = OkHttpClient.Builder() + .pingInterval(30, TimeUnit.SECONDS) + .build() + } + } + } + + // WebSocketClient + single { WebSocketClient(get()) } + + // Repository + single { ChatRepositoryImpl(get(), get(), get()) } + + // UseCases + factory { SendMessageUseCase(get()) } + factory { ObserveMessagesUseCase(get()) } + factory { ObserveConnectionUseCase(get()) } + factory { LoadMoreMessagesUseCase(get()) } + + // ViewModel + viewModel { ChatViewModel(get(), get(), get(), get(), get()) } +} diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..526d293ac --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml new file mode 100644 index 000000000..768b058a6 --- /dev/null +++ b/android/app/src/main/res/values/colors.xml @@ -0,0 +1,5 @@ + + + #FF000000 + #FFFFFFFF + diff --git a/android/app/src/main/res/values/ic_launcher_colors.xml b/android/app/src/main/res/values/ic_launcher_colors.xml new file mode 100644 index 000000000..e8bc98809 --- /dev/null +++ b/android/app/src/main/res/values/ic_launcher_colors.xml @@ -0,0 +1,5 @@ + + + #2196F3 + #FFFFFF + diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml new file mode 100644 index 000000000..9652097f9 --- /dev/null +++ b/android/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + PicoClaw + diff --git a/android/app/src/main/res/values/themes.xml b/android/app/src/main/res/values/themes.xml new file mode 100644 index 000000000..92c14c2ad --- /dev/null +++ b/android/app/src/main/res/values/themes.xml @@ -0,0 +1,4 @@ + + +