wear decompose stub

This commit is contained in:
2026-09-08 22:34:27 +03:00
parent 695525ae21
commit 410c879d0d
15 changed files with 149 additions and 297 deletions
+5 -1
View File
@@ -5,9 +5,10 @@ plugins {
alias(libs.plugins.convention.compose) alias(libs.plugins.convention.compose)
alias(libs.plugins.convention.koin) alias(libs.plugins.convention.koin)
alias(libs.plugins.convention.ktor.client) alias(libs.plugins.convention.ktor.client)
alias(libs.plugins.convention.serialization)
} }
val appVersion = "1.27.1" val appVersion = "1.0"
android { android {
buildFeatures { buildConfig = true } buildFeatures { buildConfig = true }
@@ -24,6 +25,9 @@ android {
dependencies { dependencies {
implementation(platform(libs.androidx.compose.bom)) implementation(platform(libs.androidx.compose.bom))
implementation(project(":libs:ui"))
implementation(project(":libs:http-client"))
implementation(project(":libs:base"))
implementation(libs.activity.compose) implementation(libs.activity.compose)
implementation(libs.androidx.compose.foundation) implementation(libs.androidx.compose.foundation)
implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.material3)
@@ -6,51 +6,21 @@
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<application <application
android:name=".presentation.App"
android:allowBackup="true" android:allowBackup="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@android:style/Theme.DeviceDefault"> android:theme="@android:style/Theme.DeviceDefault">
<service
android:name=".complication.MainComplicationService"
android:exported="true"
android:label="@string/complication_label"
android:permission="com.google.android.wearable.permission.BIND_COMPLICATION_PROVIDER">
<intent-filter>
<action android:name="android.support.wearable.complications.ACTION_COMPLICATION_UPDATE_REQUEST" />
</intent-filter>
<meta-data
android:name="android.support.wearable.complications.SUPPORTED_TYPES"
android:value="SHORT_TEXT" />
<meta-data
android:name="android.support.wearable.complications.UPDATE_PERIOD_SECONDS"
android:value="3600" />
</service>
<service
android:name=".tile.MainTileService"
android:exported="true"
android:label="@string/tile_label"
android:permission="com.google.android.wearable.permission.BIND_TILE_PROVIDER">
<intent-filter>
<action android:name="androidx.wear.tiles.action.BIND_TILE_PROVIDER" />
</intent-filter>
<meta-data
android:name="androidx.wear.tiles.PREVIEW"
android:resource="@drawable/tile_preview" />
</service>
<uses-library <uses-library
android:name="com.google.android.wearable" android:name="com.google.android.wearable"
android:required="true" /> android:required="false" />
<uses-library <uses-library
android:name="wear-sdk" android:name="wear-sdk"
android:required="false" /> android:required="false" />
<!--
Set to true if your app is Standalone, that is, it does not require the handheld
app to run.
-->
<meta-data <meta-data
android:name="com.google.android.wearable.standalone" android:name="com.google.android.wearable.standalone"
android:value="true" /> android:value="true" />
@@ -68,4 +38,4 @@
</activity> </activity>
</application> </application>
</manifest> </manifest>
@@ -1,41 +0,0 @@
package ru.shadowsparky.gigawear.complication
import androidx.wear.watchface.complications.data.ComplicationData
import androidx.wear.watchface.complications.data.ComplicationType
import androidx.wear.watchface.complications.data.PlainComplicationText
import androidx.wear.watchface.complications.data.ShortTextComplicationData
import androidx.wear.watchface.complications.datasource.ComplicationRequest
import androidx.wear.watchface.complications.datasource.SuspendingComplicationDataSourceService
import java.util.Calendar
/**
* Skeleton for complication data source that returns short text.
*/
class MainComplicationService : SuspendingComplicationDataSourceService() {
override fun getPreviewData(type: ComplicationType): ComplicationData? {
if (type != ComplicationType.SHORT_TEXT) {
return null
}
return createComplicationData("Mon", "Monday")
}
override suspend fun onComplicationRequest(request: ComplicationRequest): ComplicationData {
return when (Calendar.getInstance().get(Calendar.DAY_OF_WEEK)) {
Calendar.SUNDAY -> createComplicationData("Sun", "Sunday")
Calendar.MONDAY -> createComplicationData("Mon", "Monday")
Calendar.TUESDAY -> createComplicationData("Tue", "Tuesday")
Calendar.WEDNESDAY -> createComplicationData("Wed", "Wednesday")
Calendar.THURSDAY -> createComplicationData("Thu", "Thursday")
Calendar.FRIDAY -> createComplicationData("Fri", "Friday")
Calendar.SATURDAY -> createComplicationData("Sat", "Saturday")
else -> throw IllegalArgumentException("too many days")
}
}
private fun createComplicationData(text: String, contentDescription: String) =
ShortTextComplicationData.Builder(
text = PlainComplicationText.Builder(text).build(),
contentDescription = PlainComplicationText.Builder(contentDescription).build()
).build()
}
@@ -1,136 +0,0 @@
/* While this template provides a good starting point for using Wear Compose, you can always
* take a look at https://github.com/android/wear-os-samples/tree/main/ComposeStarter to find the
* most up to date changes to the libraries and their usages.
*/
package ru.shadowsparky.gigawear.presentation
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.speech.RecognizerIntent
import android.speech.tts.TextToSpeech
import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.wear.compose.foundation.lazy.TransformingLazyColumn
import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState
import androidx.wear.compose.material3.AppScaffold
import androidx.wear.compose.material3.Button
import androidx.wear.compose.material3.ButtonDefaults
import androidx.wear.compose.material3.EdgeButton
import androidx.wear.compose.material3.ListHeader
import androidx.wear.compose.material3.MaterialTheme
import androidx.wear.compose.material3.ScreenScaffold
import androidx.wear.compose.material3.SurfaceTransformation
import androidx.wear.compose.material3.Text
import androidx.wear.compose.material3.lazy.rememberTransformationSpec
import androidx.wear.compose.material3.lazy.transformedHeight
import ru.shadowsparky.gigawear.R
import ru.shadowsparky.gigawear.presentation.theme.AndroidDevTheme
import java.util.Locale
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
WearApp("Android")
}
}
}
@Composable
fun WearApp(greetingName: String) {
val context = LocalContext.current
var spokenText by remember { mutableStateOf("Тут будет твой текст") }
val tts = remember { mutableStateOf<TextToSpeech?>(null) }
DisposableEffect(Unit) {
tts.value = TextToSpeech(context) { status ->
if (status == TextToSpeech.SUCCESS) {
tts.value?.language = Locale.getDefault()
}
}
onDispose {
tts.value?.stop()
tts.value?.shutdown()
}
}
val voiceLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val results = result.data?.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)
val text = results?.firstOrNull()
if (!text.isNullOrBlank()) {
spokenText = text
tts.value?.speak(text, TextToSpeech.QUEUE_FLUSH, null, null)
}
}
}
AndroidDevTheme {
AppScaffold {
val listState = rememberTransformingLazyColumnState()
val transformationSpec = rememberTransformationSpec()
ScreenScaffold(
scrollState = listState,
edgeButton = {
EdgeButton(
onClick = {
val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM)
putExtra(RecognizerIntent.EXTRA_LANGUAGE, Locale.getDefault().toString())
}
try {
voiceLauncher.launch(intent)
} catch (e: Exception) {
spokenText = " Ошибка: Голосовой ввод недоступен"
}
},
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
),
) {
Text(" Начать ввод")
}
},
) { contentPadding ->
TransformingLazyColumn(contentPadding = contentPadding, state = listState) {
item {
ListHeader(
modifier = Modifier.fillMaxWidth().transformedHeight(this, transformationSpec),
transformation = SurfaceTransformation(transformationSpec),
) {
Text(text = stringResource(R.string.hello_world, greetingName))
}
}
item {
Button(
onClick = {
tts.value?.speak(spokenText, TextToSpeech.QUEUE_FLUSH, null, null)
},
modifier = Modifier.fillMaxWidth().transformedHeight(this, transformationSpec),
transformation = SurfaceTransformation(transformationSpec),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer
)
) {
Text(text = spokenText)
}
}
}
}
}
}
}
@@ -1,17 +0,0 @@
package ru.shadowsparky.gigawear.presentation.theme
import androidx.compose.runtime.Composable
import androidx.wear.compose.material3.MaterialTheme
@Composable
fun AndroidDevTheme(
content: @Composable () -> Unit
) {
/**
* Empty theme to customize for your app.
* See: https://developer.android.com/jetpack/compose/designsystems/custom
*/
MaterialTheme(
content = content
)
}
@@ -1,65 +0,0 @@
package ru.shadowsparky.gigawear.tile
import android.content.Context
import androidx.wear.protolayout.ResourceBuilders.Resources
import androidx.wear.protolayout.TimelineBuilders
import androidx.wear.protolayout.material3.Typography.BODY_LARGE
import androidx.wear.tiles.RequestBuilders
import androidx.wear.tiles.TileBuilders
import androidx.wear.tiles.TileService
import com.google.common.util.concurrent.Futures
import androidx.wear.protolayout.material3.materialScope
import androidx.wear.protolayout.material3.primaryLayout
import androidx.wear.protolayout.material3.text
import androidx.wear.protolayout.types.layoutString
import androidx.wear.tiles.RequestBuilders.ResourcesRequest
import androidx.wear.tiles.tooling.preview.Preview
import androidx.wear.tiles.tooling.preview.TilePreviewData
import androidx.wear.tooling.preview.devices.WearDevices
import ru.shadowsparky.gigawear.R
import com.google.common.util.concurrent.ListenableFuture
private const val RESOURCES_VERSION = "0"
class MainTileService : TileService() {
override fun onTileRequest(requestParams: RequestBuilders.TileRequest): ListenableFuture<TileBuilders.Tile> =
Futures.immediateFuture(tile(requestParams, this))
override fun onTileResourcesRequest(requestParams: ResourcesRequest): ListenableFuture<Resources> =
Futures.immediateFuture(resources(requestParams))
}
private fun resources(requestParams: ResourcesRequest): Resources {
return Resources.Builder()
.setVersion(RESOURCES_VERSION)
.build()
}
private fun tile(
requestParams: RequestBuilders.TileRequest,
context: Context,
): TileBuilders.Tile {
return TileBuilders.Tile.Builder()
.setResourcesVersion(RESOURCES_VERSION)
.setTileTimeline(
TimelineBuilders.Timeline.fromLayoutElement(
materialScope(context, requestParams.deviceConfiguration) {
primaryLayout(
mainSlot = {
text(
context.getString(R.string.hello_world, "Tile").layoutString,
typography = BODY_LARGE
)
}
)
}
)
)
.build()
}
@Preview(device = WearDevices.SMALL_ROUND)
@Preview(device = WearDevices.LARGE_ROUND)
fun tilePreview(context: Context) = TilePreviewData(::resources) {
tile(it, context)
}
@@ -0,0 +1,10 @@
package ru.shadowsparky.gigawear
import org.koin.core.annotation.ComponentScan
import org.koin.core.annotation.Configuration
import org.koin.core.annotation.Module
@Module
@ComponentScan
@Configuration
class GigaWearModule
@@ -0,0 +1,16 @@
package ru.shadowsparky.gigawear.presentation
import android.app.Application
import org.koin.core.annotation.KoinApplication
import org.koin.plugin.module.dsl.startKoin
@KoinApplication
annotation class GigaWearApp
class App : Application() {
override fun onCreate() {
super.onCreate()
startKoin<GigaWearApp>()
}
}
@@ -0,0 +1,24 @@
package ru.shadowsparky.gigawear.presentation
import com.arkivanov.decompose.ComponentContext
import kotlinx.serialization.KSerializer
import org.koin.core.Koin
import org.koin.core.annotation.Factory
import ru.shadowsparky.gigawear.presentation.nav.Route
import ru.shadowsparky.ui.nav.RootComponent
class GigaWearRootComponent(
context: ComponentContext
) : RootComponent<Route>(context) {
override val initStack: List<Route> = listOf(Route.MainScreen())
override val serializer: KSerializer<Route> = Route.serializer()
override val koin: Koin = ru.shadowsparky.koin
}
@Factory
class RootComponentFactory {
fun create(context: ComponentContext): GigaWearRootComponent {
return GigaWearRootComponent(context)
}
}
@@ -0,0 +1,26 @@
package ru.shadowsparky.gigawear.presentation
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import com.arkivanov.decompose.retainedComponent
import org.koin.android.ext.android.inject
import ru.shadowsparky.gigawear.presentation.main.MainScreen
import ru.shadowsparky.gigawear.presentation.nav.Child
import ru.shadowsparky.ui.nav.Nav
class MainActivity : ComponentActivity() {
private val rootFactory by inject<RootComponentFactory>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val root = retainedComponent { rootFactory.create(it) }
setContent {
Nav(root) { child, _ ->
when (child) {
is Child.MainChild -> MainScreen(child.component)
}
}
}
}
}
@@ -0,0 +1,27 @@
package ru.shadowsparky.gigawear.presentation.main
import com.arkivanov.decompose.ComponentContext
import org.koin.core.annotation.Factory
import org.koin.core.annotation.Named
import ru.shadowsparky.gigawear.presentation.nav.Child
import ru.shadowsparky.gigawear.presentation.nav.Route
import ru.shadowsparky.ui.nav.ComponentFactory
import ru.shadowsparky.ui.nav.RouteNavigator
class MainComponent(
private val context: ComponentContext
) : ComponentContext by context {
}
@Factory
@Named(Route.MAIN)
class MainComponentFactory : ComponentFactory<Child, Route.MainScreen, Route> {
override fun create(
route: Route.MainScreen,
child: ComponentContext,
root: RouteNavigator<Route>
): Child {
val component = MainComponent(child)
return Child.MainChild(component)
}
}
@@ -0,0 +1,14 @@
package ru.shadowsparky.gigawear.presentation.main
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.safeContentPadding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@Composable
fun MainScreen(component: MainComponent) {
Box(modifier = Modifier.safeContentPadding()) {
Text("MainScreen $component")
}
}
@@ -0,0 +1,7 @@
package ru.shadowsparky.gigawear.presentation.nav
import ru.shadowsparky.gigawear.presentation.main.MainComponent
sealed class Child : ru.shadowsparky.ui.nav.Child {
class MainChild(val component: MainComponent) : Child()
}
@@ -0,0 +1,14 @@
package ru.shadowsparky.gigawear.presentation.nav
import kotlinx.serialization.Serializable
@Serializable
sealed class Route : ru.shadowsparky.ui.nav.Route() {
@Serializable
data class MainScreen(override val routeId: String = MAIN) : Route()
companion object {
const val MAIN = "main"
}
}
@@ -1,8 +1,7 @@
<resources> <resources>
<style name="MainActivityTheme.Starting" parent="Theme.SplashScreen"> <style name="MainActivityTheme.Starting" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@android:color/black</item> <item name="windowSplashScreenBackground">@android:color/black</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/splash_icon</item> <item name="windowSplashScreenAnimatedIcon">@drawable/splash_icon</item>
<item name="postSplashScreenTheme">@android:style/Theme.DeviceDefault</item> <item name="postSplashScreenTheme">@android:style/Theme.DeviceDefault</item>
</style> </style>
</resources> </resources>