Skip to content

Commit c463f50

Browse files
authored
feat: added sample for Service with Dagger-hilt (#14)
1 parent 9a85b03 commit c463f50

39 files changed

+1030
-2
lines changed

app-service-hilt/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/build

app-service-hilt/Readme.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Service
2+
3+
## Initial Setup
4+
5+
This is a sample project on how to use `compose-floating-window` on a service for long running operations
6+
7+
1. Create a new service. Like [this](src/main/java/com/github/only52607/compose/window/service/MyService.kt) for example
8+
9+
2. Add the permission to the manifest file
10+
11+
```xml
12+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
13+
```
14+
15+
3. Declare the service in the manifest file
16+
17+
```xml
18+
<service android:name=".MyService" />
19+
```
20+
21+
4. Follow the `MyService` sample on how to use it on a service.
22+
23+
## Usage
24+
25+
Do not follow the `build.gradle.kts` setup as it was done for the sample project
26+
27+
Note:
28+
- Be sure to `AppCompatActivity` instead of `ComponentActivity` in order to change the theme of the app
29+
30+
This is mostly just a sample project on how to incorporate hilt into viewmodels and services.
31+

app-service-hilt/build.gradle.kts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
buildscript {
2+
dependencies {
3+
classpath(libs.ksp.gradle)
4+
classpath(libs.hilt.android.gradle.plugin)
5+
}
6+
}
7+
hilt {
8+
enableAggregatingTask = false
9+
}
10+
11+
plugins {
12+
id("com.android.application")
13+
id("org.jetbrains.kotlin.android")
14+
alias(libs.plugins.compose.compiler)
15+
16+
alias(libs.plugins.hilt.android)
17+
alias(libs.plugins.ksp)
18+
}
19+
20+
android {
21+
namespace = "com.github.only52607.compose.window.hilt"
22+
compileSdk = libs.versions.compile.sdk.get().toInt()
23+
24+
defaultConfig {
25+
applicationId = "com.github.only52607.compose.window"
26+
minSdk = libs.versions.min.sdk.get().toInt()
27+
targetSdk = libs.versions.target.sdk.get().toInt()
28+
versionCode = 1
29+
versionName = "1.0"
30+
31+
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
32+
vectorDrawables {
33+
useSupportLibrary = true
34+
}
35+
}
36+
37+
buildTypes {
38+
release {
39+
isMinifyEnabled = true
40+
proguardFiles(
41+
getDefaultProguardFile("proguard-android-optimize.txt"),
42+
"proguard-rules.pro"
43+
)
44+
signingConfig = signingConfigs.getByName("debug")
45+
}
46+
}
47+
compileOptions {
48+
sourceCompatibility = JavaVersion.VERSION_17
49+
targetCompatibility = JavaVersion.VERSION_17
50+
}
51+
kotlinOptions {
52+
jvmTarget = "17"
53+
}
54+
buildFeatures {
55+
compose = true
56+
}
57+
packaging {
58+
resources {
59+
excludes += "/META-INF/{AL2.0,LGPL2.1}"
60+
}
61+
}
62+
}
63+
64+
dependencies {
65+
implementation(project(":library"))
66+
implementation(libs.androidx.core.ktx)
67+
implementation(libs.androidx.lifecycle.runtime.ktx)
68+
implementation(libs.androidx.lifecycle.viewmodel.compose)
69+
implementation(libs.activity.compose)
70+
implementation(platform(libs.compose.bom))
71+
implementation(libs.compose.ui)
72+
implementation(libs.compose.ui.graphics)
73+
implementation(libs.compose.ui.tooling.preview)
74+
implementation(libs.compose.material3)
75+
testImplementation(libs.junit)
76+
androidTestImplementation(libs.androidx.test.junit)
77+
androidTestImplementation(libs.androidx.test.espresso)
78+
androidTestImplementation(platform(libs.compose.bom))
79+
androidTestImplementation(libs.compose.ui.test.junit4)
80+
debugImplementation(libs.compose.ui.tooling)
81+
debugImplementation(libs.compose.ui.test.manifest)
82+
debugImplementation(libs.leak.canary)
83+
84+
implementation(libs.dagger.hilt.android)
85+
ksp(libs.dagger.hilt.compiler)
86+
87+
implementation(libs.appcompat)
88+
implementation(libs.bundles.datastore)
89+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Add project specific ProGuard rules here.
2+
# You can control the set of applied configuration files using the
3+
# proguardFiles setting in build.gradle.
4+
#
5+
# For more details, see
6+
# http://developer.android.com/guide/developing/tools/proguard.html
7+
8+
# If your project uses WebView with JS, uncomment the following
9+
# and specify the fully qualified class name to the JavaScript interface
10+
# class:
11+
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12+
# public *;
13+
#}
14+
15+
# Uncomment this to preserve the line number information for
16+
# debugging stack traces.
17+
#-keepattributes SourceFile,LineNumberTable
18+
19+
# If you keep the line number information, uncomment this to
20+
# hide the original source file name.
21+
#-renamesourcefileattribute SourceFile
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.github.only52607.compose.window
2+
3+
import androidx.test.platform.app.InstrumentationRegistry
4+
import androidx.test.ext.junit.runners.AndroidJUnit4
5+
6+
import org.junit.Test
7+
import org.junit.runner.RunWith
8+
9+
import org.junit.Assert.*
10+
11+
/**
12+
* Instrumented test, which will execute on an Android device.
13+
*
14+
* See [testing documentation](http://d.android.com/tools/testing).
15+
*/
16+
@RunWith(AndroidJUnit4::class)
17+
class ExampleInstrumentedTest {
18+
@Test
19+
fun useAppContext() {
20+
// Context of the app under test.
21+
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22+
assertEquals("com.github.only52607.compose.window", appContext.packageName)
23+
}
24+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3+
xmlns:tools="http://schemas.android.com/tools">
4+
5+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
6+
7+
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
8+
<application
9+
android:name=".FloatingApplication"
10+
android:allowBackup="true"
11+
android:dataExtractionRules="@xml/data_extraction_rules"
12+
android:fullBackupContent="@xml/backup_rules"
13+
android:icon="@mipmap/ic_launcher"
14+
android:label="@string/app_name"
15+
android:roundIcon="@mipmap/ic_launcher_round"
16+
android:supportsRtl="true"
17+
android:theme="@style/AppTheme"
18+
tools:targetApi="31">
19+
<activity
20+
android:name=".MainActivity"
21+
android:exported="true"
22+
>
23+
<intent-filter>
24+
<action android:name="android.intent.action.MAIN" />
25+
26+
<category android:name="android.intent.category.LAUNCHER" />
27+
</intent-filter>
28+
</activity>
29+
30+
<service android:name=".MyService" />
31+
</application>
32+
33+
</manifest>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.github.only52607.compose.window.hilt
2+
3+
import android.app.Application
4+
import dagger.hilt.android.HiltAndroidApp
5+
6+
@HiltAndroidApp
7+
class FloatingApplication: Application()
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package com.github.only52607.compose.window.hilt
2+
3+
import android.os.Bundle
4+
import androidx.activity.compose.setContent
5+
import androidx.appcompat.app.AppCompatActivity
6+
import androidx.appcompat.app.AppCompatDelegate
7+
import androidx.compose.foundation.layout.Arrangement
8+
import androidx.compose.foundation.layout.Column
9+
import androidx.compose.foundation.layout.Spacer
10+
import androidx.compose.foundation.layout.fillMaxSize
11+
import androidx.compose.foundation.layout.height
12+
import androidx.compose.material3.Button
13+
import androidx.compose.material3.MaterialTheme
14+
import androidx.compose.material3.Surface
15+
import androidx.compose.material3.Text
16+
import androidx.compose.runtime.LaunchedEffect
17+
import androidx.compose.runtime.getValue
18+
import androidx.compose.runtime.mutableStateOf
19+
import androidx.compose.runtime.remember
20+
import androidx.compose.runtime.rememberCoroutineScope
21+
import androidx.compose.ui.Alignment
22+
import androidx.compose.ui.Modifier
23+
import androidx.compose.ui.platform.LocalContext
24+
import androidx.compose.ui.unit.dp
25+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
26+
import com.github.only52607.compose.window.hilt.repository.UserPreferencesRepository
27+
import com.github.only52607.compose.window.hilt.ui.DialogPermission
28+
import com.github.only52607.compose.window.hilt.ui.theme.ComposeFloatingWindowTheme
29+
import dagger.hilt.android.AndroidEntryPoint
30+
import kotlinx.coroutines.flow.distinctUntilChanged
31+
import kotlinx.coroutines.launch
32+
import javax.inject.Inject
33+
34+
@AndroidEntryPoint
35+
class MainActivity : AppCompatActivity() {
36+
37+
@Inject
38+
lateinit var userPreferencesRepository: UserPreferencesRepository
39+
40+
override fun onCreate(savedInstanceState: Bundle?) {
41+
super.onCreate(savedInstanceState)
42+
setContent {
43+
ComposeFloatingWindowTheme {
44+
LaunchedEffect(userPreferencesRepository.darkModeFlow) {
45+
userPreferencesRepository
46+
.darkModeFlow
47+
.distinctUntilChanged()
48+
.collect { darkMode ->
49+
AppCompatDelegate.setDefaultNightMode(
50+
if (darkMode) AppCompatDelegate.MODE_NIGHT_YES
51+
else AppCompatDelegate.MODE_NIGHT_NO
52+
)
53+
}
54+
}
55+
56+
val showDialogPermission = remember { mutableStateOf(false) }
57+
58+
val context = LocalContext.current
59+
60+
val isShowing by MyService.serviceStarted.collectAsStateWithLifecycle(false)
61+
62+
Surface(
63+
modifier = Modifier.fillMaxSize(),
64+
color = MaterialTheme.colorScheme.background
65+
) {
66+
Column(
67+
Modifier.fillMaxSize(),
68+
verticalArrangement = Arrangement.Center,
69+
horizontalAlignment = Alignment.CenterHorizontally
70+
) {
71+
Button(
72+
onClick = {
73+
MyService.start(context)
74+
},
75+
enabled = !isShowing
76+
) {
77+
Text("Show")
78+
}
79+
Spacer(modifier = Modifier.height(10.dp))
80+
Button(
81+
onClick = {
82+
MyService.stop(context)
83+
},
84+
enabled = isShowing
85+
) {
86+
Text("Hide")
87+
}
88+
}
89+
DialogPermission(showDialogState = showDialogPermission)
90+
}
91+
}
92+
}
93+
}
94+
95+
96+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package com.github.only52607.compose.window.hilt
2+
3+
import android.app.Service
4+
import android.content.Context
5+
import android.content.Intent
6+
import android.os.IBinder
7+
import com.github.only52607.compose.window.ComposeFloatingWindow
8+
import com.github.only52607.compose.window.hilt.repository.UserPreferencesRepository
9+
import com.github.only52607.compose.window.hilt.ui.FloatingWindowContent
10+
import com.github.only52607.compose.window.hilt.ui.FloatingWindowViewModel
11+
import dagger.hilt.android.AndroidEntryPoint
12+
import kotlinx.coroutines.flow.MutableStateFlow
13+
import kotlinx.coroutines.flow.StateFlow
14+
import kotlinx.coroutines.flow.asStateFlow
15+
import kotlinx.coroutines.flow.update
16+
import javax.inject.Inject
17+
18+
@AndroidEntryPoint
19+
class MyService : Service() {
20+
companion object {
21+
private var _serviceStarted = MutableStateFlow(false)
22+
val serviceStarted: StateFlow<Boolean>
23+
get() = _serviceStarted.asStateFlow()
24+
25+
fun start(context: Context) {
26+
val intent = Intent(context, MyService::class.java)
27+
context.startService(intent)
28+
}
29+
30+
fun stop(context: Context) {
31+
val intent = Intent(context, MyService::class.java)
32+
context.stopService(intent)
33+
}
34+
}
35+
36+
@Inject
37+
lateinit var userPreferencesRepository: UserPreferencesRepository
38+
39+
private val viewModel by lazy {
40+
FloatingWindowViewModel(userPreferencesRepository)
41+
}
42+
43+
private val floatingWindow by lazy {
44+
createFloatingWindow()
45+
}
46+
47+
private fun createFloatingWindow(): ComposeFloatingWindow =
48+
ComposeFloatingWindow(this).apply {
49+
setContent {
50+
FloatingWindowContent(viewModel)
51+
}
52+
}
53+
54+
override fun onCreate() {
55+
super.onCreate()
56+
_serviceStarted.update { true }
57+
floatingWindow.show()
58+
}
59+
60+
override fun onBind(intent: Intent?): IBinder? = null
61+
62+
override fun onDestroy() {
63+
_serviceStarted.update { false }
64+
// Call close for cleanup and it will hide it in the process
65+
floatingWindow.close()
66+
super.onDestroy()
67+
}
68+
}

0 commit comments

Comments
 (0)