feat: implement dynamic feature module for control actuator

This commit is contained in:
Cutiful 2025-05-16 11:14:03 +07:00
parent 812cd4b23b
commit 05b707fbdd
36 changed files with 683 additions and 226 deletions

View File

@ -12,6 +12,7 @@
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/control_feature" />
<option value="$PROJECT_DIR$/diseasedetection_feature" />
</set>
</option>
<option name="resolveExternalAnnotations" value="false" />

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="2.0.0" />
<option name="version" value="2.0.21" />
</component>
</project>

View File

@ -3,6 +3,8 @@ plugins {
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
id("com.google.gms.google-services")
id("com.google.devtools.ksp")
id("com.google.dagger.hilt.android")
}
android {
@ -13,8 +15,8 @@ android {
applicationId = "com.syaroful.agrilinkvocpro"
minSdk = 29
targetSdk = 35
versionCode = 1
versionName = "1.0"
versionCode = 2
versionName = "1.0.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@ -29,42 +31,59 @@ android {
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "11"
jvmTarget = "1.8"
}
buildFeatures {
compose = true
}
dynamicFeatures += setOf(":control_feature")
dynamicFeatures += setOf(":control_feature", ":diseasedetection_feature")
}
dependencies {
// Android Core and Lifecycle
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
// Jetpack Compose UI
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(platform(libs.androidx.compose.bom)) // BOM for consistent Compose versions
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
// Testing Dependencies
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(platform(libs.androidx.compose.bom)) // BOM for consistent Compose test versions
androidTestImplementation(libs.androidx.ui.test.junit4)
// Debugging Dependencies
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
// firebase service
implementation(platform(libs.firebase.bom))
// Firebase Services
implementation(platform(libs.firebase.bom)) // BOM for consistent Firebase versions
implementation(libs.firebase.database)
// viewModel
// ViewModel
implementation(libs.androidx.lifecycle.viewmodel.compose)
// Dynamic Feature Modules
implementation(libs.feature.delivery)
implementation(libs.feature.delivery.ktx)
// Dependency Injection
implementation(libs.hilt.android)
ksp(libs.hilt.android.compiler)
// navigation with compose
implementation(libs.androidx.navigation.compose)
}

View File

@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools">
<application
android:name=".MyApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"

View File

@ -0,0 +1,11 @@
package com.syaroful.agrilinkvocpro
sealed class DownloadState {
data object Idle : DownloadState()
data object Starting : DownloadState()
data object Downloading : DownloadState()
data object Downloaded : DownloadState()
data object Installed : DownloadState()
data class Failed(val message: String) : DownloadState()
data class DownloadingWithProgress(val progress: Float) : DownloadState()
}

View File

@ -1,68 +1,196 @@
package com.syaroful.agrilinkvocpro
import android.content.res.Configuration
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.Text
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import com.syaroful.agrilinkvocpro.core.components.AppButton
import com.syaroful.agrilinkvocpro.core.components.AppPasswordField
import com.syaroful.agrilinkvocpro.core.components.AppTextField
import com.syaroful.agrilinkvocpro.ui.pages.HomeScreen
import androidx.compose.ui.unit.dp
import androidx.lifecycle.lifecycleScope
import com.syaroful.agrilinkvocpro.core.components.DownloadModuleConfirmationDialog
import com.syaroful.agrilinkvocpro.core.components.DownloadProgressDialog
import com.syaroful.agrilinkvocpro.core.components.MenuItemButton
import com.syaroful.agrilinkvocpro.ui.pages.GreenHouseInformationSection
import com.syaroful.agrilinkvocpro.ui.theme.AgrilinkVocproTheme
import com.syaroful.agrilinkvocpro.viewModel.DynamicModuleViewModel
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
@Suppress("DEPRECATION")
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
private val viewModel: DynamicModuleViewModel by viewModels()
private val CONTROL_FEATURE_MODULE_NAME = "control_feature"
// State untuk menampilkan dialog log/progress
private val showProgressDialog = mutableStateOf(false)
private val progressMessage = mutableStateOf("")
private val progressPercent = mutableFloatStateOf(0f)
private var dialogState = mutableStateOf(false)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
AgrilinkVocproTheme {
HomeScreen()
DownloadProgressDialog(
showDialog = showProgressDialog.value,
message = progressMessage.value,
progress = progressPercent.floatValue,
onDismiss = { showProgressDialog.value = false }
)
}
}
}
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
observeDownloadStatus()
}
Column {
Text(
text = "Hello $name!, silahkan Login",
modifier = modifier.fillMaxWidth(),
textAlign = TextAlign.Center
private fun observeDownloadStatus() {
lifecycleScope.launchWhenStarted {
viewModel.downloadState.collect { status ->
when (status) {
is DownloadState.Idle -> {
showProgressDialog.value = false
progressMessage.value = ""
progressPercent.floatValue = 0f
}
is DownloadState.Starting -> {
showProgressDialog.value = true
progressMessage.value = "Starting download..."
progressPercent.floatValue = 0f
}
is DownloadState.Downloading -> {
showProgressDialog.value = true
progressMessage.value = "Downloading module..."
// Progress update akan di-handle via listener tambahan (lihat catatan di bawah)
}
is DownloadState.Downloaded -> {
progressMessage.value = "Download completed"
progressPercent.floatValue = 1f
}
is DownloadState.Installed -> {
progressMessage.value = "Install completed"
progressPercent.floatValue = 1f
// Tutup dialog setelah beberapa saat
delayAndDismissDialog()
}
is DownloadState.Failed -> {
progressMessage.value = "Failed: ${status.message}"
showProgressDialog.value = true
progressPercent.floatValue = 0f
}
is DownloadState.DownloadingWithProgress -> {
showProgressDialog.value = true
}
}
}
}
}
private fun delayAndDismissDialog() {
lifecycleScope.launch {
kotlinx.coroutines.delay(1500)
showProgressDialog.value = false
openControlFeature()
}
}
@Composable
fun HomeScreen() {
AgrilinkVocproTheme {
Scaffold { padding ->
Column(
modifier = Modifier
.padding(padding)
.fillMaxWidth()
) {
GreenHouseInformationSection()
Spacer(modifier = Modifier.height(20.dp))
DynamicFeatureSection()
Spacer(modifier = Modifier.height(32.dp))
DownloadModuleConfirmationDialog(dialogState, ::downloadDynamicModule)
}
}
}
}
@Composable
fun DynamicFeatureSection() {
Row(
modifier = Modifier
.padding(horizontal = 20.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
MenuItemButton(
label = "Kontrol\nAktuator",
icon = painterResource(id = R.drawable.control_actuator_icon),
onClick = { openControlFeature() })
MenuItemButton(
label = "Resep\nPertumbuhan",
icon = painterResource(id = R.drawable.growth_recipe_icon),
onClick = {}
)
AppTextField(
hint = "Enter Your Email",
leadingIcon = painterResource(R.drawable.icon_email),
keyboardType = KeyboardType.Email
MenuItemButton(
label = "Harga\nKomoditas",
icon = painterResource(id = R.drawable.commodity_price_prediction_icon),
onClick = {}
)
AppPasswordField(
hint = "Enter your password",
keyboardType = KeyboardType.Password
)
AppButton(
label = "Login",
MenuItemButton(
label = "Deteksi\nPenyakit",
icon = painterResource(id = R.drawable.plant_disease_detection_icon),
onClick = {}
)
}
}
}
private fun openControlFeature() {
if (viewModel.isModuleDownloaded(CONTROL_FEATURE_MODULE_NAME)) {
startControlActuatorActivity()
} else {
dialogState.value = true
}
}
@Preview(showBackground = true, name = "Light Mode")
@Preview(showBackground = true, name = "Dark Mode", uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun GreetingPreview() {
AgrilinkVocproTheme {
HomeScreen()
private fun downloadDynamicModule() {
viewModel.downloadModule(CONTROL_FEATURE_MODULE_NAME)
}
private fun startControlActuatorActivity() {
val intent = Intent().apply {
setClassName(
"com.syaroful.agrilinkvocpro",
"com.syaroful.agrilinkvocpro.control_feature.ControlActuatorActivity"
)
}
startActivity(intent)
}
}

View File

@ -0,0 +1,9 @@
package com.syaroful.agrilinkvocpro
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class MyApplication : Application() {
}

View File

@ -7,8 +7,15 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
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.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
@ -55,8 +62,8 @@ fun AppTextField(
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
focusedIndicatorColor = MainGreen, // Warna stroke saat TextField aktif
unfocusedIndicatorColor = LightGrey // Warna stroke saat TextField tidak aktif
focusedIndicatorColor = MainGreen,
unfocusedIndicatorColor = LightGrey
),
singleLine = true,
placeholder = {

View File

@ -27,11 +27,12 @@ import com.syaroful.agrilinkvocpro.ui.theme.MainGreen
@Composable
fun MenuItemButton(
label: String,
icon: Painter
icon: Painter,
onClick: () -> Unit = {}
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
OutlinedButton(
onClick = {},
onClick = onClick,
modifier = Modifier.size(48.dp),
shape = CircleShape,
border = BorderStroke(0.dp, Color.Transparent),

View File

@ -1,6 +1,5 @@
package com.syaroful.agrilinkvocpro.ui.pages
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@ -16,7 +15,6 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Person
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@ -24,23 +22,17 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.syaroful.agrilinkvocpro.R
import com.syaroful.agrilinkvocpro.core.components.MenuItemButton
import com.syaroful.agrilinkvocpro.core.components.textTheme
import com.syaroful.agrilinkvocpro.ui.theme.AgrilinkVocproTheme
import com.syaroful.agrilinkvocpro.ui.theme.MainGreen
@Composable
fun HomeScreen() {
AgrilinkVocproTheme {
Scaffold { padding ->
Column(
modifier = Modifier
.padding(padding)
.fillMaxWidth()
) {
fun GreenHouseInformationSection() {
Row(horizontalArrangement = Arrangement.End, modifier = Modifier.fillMaxWidth()) {
IconButton(
onClick = {},
@ -84,24 +76,4 @@ fun HomeScreen() {
.clip(RoundedCornerShape(10.dp))
)
}
Spacer(modifier = Modifier.height(20.dp))
Row(modifier = Modifier.padding(horizontal = 20.dp).fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
MenuItemButton(label = "Kontrol\nAktuator",icon = painterResource(id = R.drawable.control_actuator_icon))
MenuItemButton(label = "Resep\nPertumbuhan",icon = painterResource(id = R.drawable.growth_recipe_icon))
MenuItemButton(label = "Harga\nKomoditas",icon = painterResource(id = R.drawable.commodity_price_prediction_icon))
MenuItemButton(label = "Deteksi\nPenyakit",icon = painterResource(id = R.drawable.plant_disease_detection_icon))
}
}
}
}
}
@Preview(showBackground = true, name = "Light Mode")
@Preview(showBackground = true, name = "Dark Mode", uiMode = UI_MODE_NIGHT_YES)
@Composable
fun HomeScreenPreview() {
HomeScreen()
}

View File

@ -10,7 +10,12 @@ val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)
val Grey10 = Color(0xFFE7E7E7)
val LightGrey = Color(0xFFAAB3D0)
val DividerColor = Color(0xFFC2C2C2)
val DarkGrey = Color(0xFF6C707E)
val MainGreen = Color(0xFF179678)
val DarkGreen = Color(0xFF0A3732)
val LightGreen = Color(0xFFE2FFF8)
val LemonGreen = Color(0xFFC9F000)

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -2,4 +2,15 @@
<string name="app_name">Agrilink Vocpro</string>
<string name="title_control_feature">Control Module</string>
<string name="title_activity_detail_control_screen">DetailControlScreen</string>
<string name="title_diseasedetection_feature">Disease Detection Module</string>
<string name="control_feature_label">Kontrol Aktuator</string>
<string name="play_store_icon_desc">Google Play Store</string>
<string name="download_module_title">Unduh Modul Fitur Dinamis</string>
<string name="download_module_message">anda perlu mengunduh modul fitur dinamis agar fitur ini dapat digunakan</string>
<string name="download">Download</string>
<string name="cancel">Cancel</string>
<string name="title_activity_control_actuator">ControlActuatorActivity</string>
</resources>

View File

@ -5,4 +5,6 @@ plugins {
alias(libs.plugins.kotlin.compose) apply false
id("com.google.gms.google-services") version "4.4.2" apply false
alias(libs.plugins.android.dynamic.feature) apply false
id("com.google.devtools.ksp") version "2.0.21-1.0.27" apply false
id("com.google.dagger.hilt.android") version "2.56.2" apply false
}

View File

@ -16,8 +16,7 @@ android {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
"proguard-rules-dynamic-features.pro"
)
}
}
@ -35,6 +34,7 @@ android {
dependencies {
implementation(project(":app"))
// UI and Compose
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.runtime.android)
implementation(libs.androidx.lifecycle.runtime.ktx)
@ -44,18 +44,22 @@ dependencies {
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
// ViewModel
implementation(libs.androidx.lifecycle.viewmodel.compose)
// Firebase
implementation(platform(libs.firebase.bom))
implementation(libs.firebase.database)
// Testing
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4)
// Debugging
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
// firebase
implementation(platform(libs.firebase.bom))
implementation(libs.firebase.database)
// viewModel
implementation(libs.androidx.lifecycle.viewmodel.compose)
}

View File

@ -13,6 +13,11 @@
</dist:module>
<application>
<activity
android:name=".ControlActuatorActivity"
android:exported="false"
android:label="@string/title_activity_control_actuator"
android:theme="@style/Theme.AgrilinkVocpro" />
</application>
</manifest>

View File

@ -0,0 +1,20 @@
package com.syaroful.agrilinkvocpro.control_feature
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import com.syaroful.agrilinkvocpro.control_feature.page.ControlActuatorScreen
import com.syaroful.agrilinkvocpro.control_feature.ui.theme.AgrilinkVocproTheme
class ControlActuatorActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
AgrilinkVocproTheme {
ControlActuatorScreen()
}
}
}
}

View File

@ -0,0 +1,97 @@
package com.syaroful.agrilinkvocpro.control_feature.components
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.syaroful.agrilinkvocpro.control_feature.ui.theme.DarkGreen
import com.syaroful.agrilinkvocpro.control_feature.ui.theme.DividerColor
import com.syaroful.agrilinkvocpro.control_feature.ui.theme.Grey10
import com.syaroful.agrilinkvocpro.control_feature.ui.theme.MainGreen
@Composable
fun ControlCard(
iconRes: Int,
label: String,
isOn: Boolean,
onToggle: (Boolean) -> Unit
) {
val backgroundColor = if (isOn) DarkGreen else Color.White
val iconTint = if (isOn) Color(0xFFB2FF59) else Color(0xFF4CAF50)
val textColor = if (isOn) MainGreen else MainGreen
Column(
modifier = Modifier
.clip(RoundedCornerShape(12.dp))
.border(1.dp, Grey10, RoundedCornerShape(12.dp))
.background(backgroundColor)
.padding(16.dp)
.fillMaxWidth()
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
Box(
modifier = Modifier
.size(32.dp)
.background(
color = iconTint.copy(alpha = 0.1f),
shape = CircleShape
),
contentAlignment = Alignment.Center
) {
Icon(
painter = painterResource(id = iconRes),
contentDescription = null,
tint = iconTint,
modifier = Modifier.size(20.dp)
)
}
Switch(
checked = isOn,
onCheckedChange = onToggle,
colors = SwitchDefaults.colors(
checkedThumbColor = DarkGreen,
uncheckedThumbColor = Color.White,
uncheckedBorderColor = DividerColor,
checkedTrackColor = MainGreen,
uncheckedTrackColor = DividerColor
)
)
}
Spacer(modifier = Modifier.height(16.dp))
Text(label, color = if (isOn) Color.White else Color.Black)
Text(
if (isOn) "On" else "Off",
color = textColor,
fontWeight = FontWeight.Bold
)
}
}

View File

@ -1,20 +1,14 @@
package com.syaroful.agrilinkvocpro.control_feature.page
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
@ -24,8 +18,6 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
@ -33,13 +25,10 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.syaroful.agrilinkvocpro.control_feature.R
import com.syaroful.agrilinkvocpro.control_feature.components.ControlCard
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -47,14 +36,19 @@ fun ControlActuatorScreen() {
Scaffold(
topBar = {
TopAppBar(
title = { Text("Control Actuator") },
title = {
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth()) {
Text("Control Actuator", style = MaterialTheme.typography.titleMedium)
}
},
navigationIcon = {
IconButton(onClick = { /* TODO: handle back */ }) {
IconButton(onClick = { }) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
}
},
actions = {
IconButton(onClick = { /* TODO: handle history */ }) {
IconButton(onClick = { }) {
Icon(Icons.Filled.Info, contentDescription = "History")
}
}
@ -113,63 +107,6 @@ fun ControlGrid(iconRes: Int, items: List<String>) {
}
}
@Composable
fun ControlCard(
iconRes: Int,
label: String,
isOn: Boolean,
onToggle: (Boolean) -> Unit
) {
val backgroundColor = if (isOn) Color(0xFF00332C) else Color.White
val iconTint = if (isOn) Color(0xFFB2FF59) else Color(0xFF4CAF50)
val textColor = if (isOn) Color(0xFF00E676) else Color(0xFF4CAF50)
Column(
modifier = Modifier
.clip(RoundedCornerShape(12.dp))
.background(backgroundColor)
.padding(16.dp)
.fillMaxWidth()
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
Box(
modifier = Modifier
.size(32.dp)
.background(color = if (isOn) Color(0xFF00332C) else Color(0xFFE0F2F1), shape = CircleShape),
contentAlignment = Alignment.Center
) {
Icon(
painter = painterResource(id = iconRes),
contentDescription = null,
tint = iconTint,
modifier = Modifier.size(20.dp)
)
}
Switch(
checked = isOn,
onCheckedChange = onToggle,
colors = SwitchDefaults.colors(
checkedThumbColor = Color(0xFF00E676),
uncheckedThumbColor = Color.Gray
)
)
}
Spacer(modifier = Modifier.height(16.dp))
Text(label, color = Color.Black)
Text(
if (isOn) "On" else "Off",
color = textColor,
fontWeight = FontWeight.Bold
)
}
}
@Preview
@Composable

View File

@ -0,0 +1,24 @@
package com.syaroful.agrilinkvocpro.control_feature.ui.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)
val Grey10 = Color(0xFFE7E7E7)
val LightGrey = Color(0xFFAAB3D0)
val DividerColor = Color(0xFFC2C2C2)
val DarkGrey = Color(0xFF6C707E)
val MainGreen = Color(0xFF179678)
val DarkGreen = Color(0xFF0A3732)
val LightGreen = Color(0xFFE2FFF8)
val LemonGreen = Color(0xFFC9F000)
val BackgroundLight = Color(0xFFF8F8F8)
val BackgroundDark = Color(0xFF222222)

View File

@ -0,0 +1,61 @@
package com.syaroful.agrilinkvocpro.control_feature.ui.theme
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80,
background = BackgroundDark
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40,
background = BackgroundLight
/* Other default colors to override
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
@Composable
fun AgrilinkVocproTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}

View File

@ -0,0 +1,34 @@
package com.syaroful.agrilinkvocpro.control_feature.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.AgrilinkVocpro" parent="android:Theme.Material.Light.NoActionBar" />
</resources>

View File

@ -0,0 +1 @@
/build

View File

@ -0,0 +1,37 @@
plugins {
alias(libs.plugins.android.dynamic.feature)
alias(libs.plugins.kotlin.android)
}
android {
namespace = "com.syaroful.agrilinkvocpro.diseasedetection_feature"
compileSdk = 35
defaultConfig {
minSdk = 29
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
"proguard-rules-dynamic-features.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
implementation(project(":app"))
implementation(libs.androidx.core.ktx)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
}

View File

@ -0,0 +1,24 @@
package com.syaroful.agrilinkvocpro.diseasedetection_feature
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.syaroful.agrilinkvocpro.diseasedetection_feature", appContext.packageName)
}
}

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:dist="http://schemas.android.com/apk/distribution">
<dist:module
dist:instant="false"
dist:title="@string/title_diseasedetection_feature">
<dist:delivery>
<dist:on-demand />
</dist:delivery>
<dist:fusing dist:include="true" />
</dist:module>
</manifest>

View File

@ -0,0 +1 @@
package com.syaroful.agrilinkvocpro.diseasedetection_feature.pages

View File

@ -0,0 +1,17 @@
package com.syaroful.agrilinkvocpro.diseasedetection_feature
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

View File

@ -1,22 +1,31 @@
[versions]
agp = "8.8.0"
agp = "8.8.2"
featureDelivery = "2.1.0"
firebaseBom = "33.13.0"
kotlin = "2.0.0"
coreKtx = "1.15.0"
hiltAndroid = "2.56.2"
hiltAndroidCompiler = "2.56.2"
kotlin = "2.0.21"
coreKtx = "1.16.0"
junit = "4.13.2"
junitVersion = "1.2.1"
espressoCore = "3.6.1"
lifecycleRuntimeKtx = "2.8.7"
activityCompose = "1.9.3"
composeBom = "2024.04.01"
lifecycleViewmodelCompose = "2.8.7"
runtimeAndroid = "1.8.0"
lifecycleRuntimeKtx = "2.9.0"
activityCompose = "1.10.1"
composeBom = "2025.05.00"
lifecycleViewmodelCompose = "2.9.0"
navigationCompose = "2.9.0"
runtimeAndroid = "1.8.1"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" }
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
feature-delivery = { module = "com.google.android.play:feature-delivery", version.ref = "featureDelivery" }
feature-delivery-ktx = { module = "com.google.android.play:feature-delivery-ktx", version.ref = "featureDelivery" }
firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" }
firebase-database = { module = "com.google.firebase:firebase-database" }
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroid" }
hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hiltAndroidCompiler" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }

View File

@ -1,6 +1,6 @@
#Wed Jan 08 14:02:58 WIB 2025
#Fri May 09 13:08:34 WIB 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@ -22,3 +22,4 @@ dependencyResolutionManagement {
rootProject.name = "Agrilink Vocpro"
include(":app")
include(":control_feature")
include(":diseasedetection_feature")