fix(ui): resolve dropdown 'val cannot be reassigned' and scroll list

This commit is contained in:
Rakha adi 2025-11-20 21:56:35 +07:00
parent 9280ce0a89
commit 961ebfd757
7 changed files with 181 additions and 88 deletions

View File

@ -10,9 +10,11 @@
<entry key="bf5061b4-a1e9-4600-a273-ea8e51b2ce89"> <entry key="bf5061b4-a1e9-4600-a273-ea8e51b2ce89">
<value> <value>
<set> <set>
<option value="056c3bd2-6151-4564-b839-d7d6f53d4342" />
<option value="0c019b23-843c-4710-8469-2dd1211d89a3" /> <option value="0c019b23-843c-4710-8469-2dd1211d89a3" />
<option value="0e6a533b-db2a-4977-8228-bfeaa867eef9" /> <option value="0e6a533b-db2a-4977-8228-bfeaa867eef9" />
<option value="1bb82ec0-7888-48c9-b016-116821338f0e" /> <option value="1bb82ec0-7888-48c9-b016-116821338f0e" />
<option value="26bdd760-c8b3-419a-9b6b-a194651aed3d" />
<option value="2a8432cf-e7cb-4250-9c7c-00664ec22a28" /> <option value="2a8432cf-e7cb-4250-9c7c-00664ec22a28" />
<option value="2b41e5bf-4d90-46d8-8fc8-7b41c3f788e1" /> <option value="2b41e5bf-4d90-46d8-8fc8-7b41c3f788e1" />
<option value="34b3c4dc-eb50-43b4-98d0-2b9771161619" /> <option value="34b3c4dc-eb50-43b4-98d0-2b9771161619" />
@ -22,11 +24,14 @@
<option value="44d2412a-3471-4972-8b90-5cad4c5a00e3" /> <option value="44d2412a-3471-4972-8b90-5cad4c5a00e3" />
<option value="461c1e05-de5d-4220-af67-2f29e576b9ea" /> <option value="461c1e05-de5d-4220-af67-2f29e576b9ea" />
<option value="4f53b359-f3df-449a-ab3e-d112d5df446a" /> <option value="4f53b359-f3df-449a-ab3e-d112d5df446a" />
<option value="51c4a9e4-545f-4c67-8fee-80b3de88d2be" />
<option value="520750f7-1625-4419-beb3-c4ecf7d9dc2b" /> <option value="520750f7-1625-4419-beb3-c4ecf7d9dc2b" />
<option value="52e099a9-0cd7-4716-8310-8a43635ca894" /> <option value="52e099a9-0cd7-4716-8310-8a43635ca894" />
<option value="6971fc05-6110-4b33-ac58-69628d336a48" /> <option value="6971fc05-6110-4b33-ac58-69628d336a48" />
<option value="6dffb37e-ecb0-4c89-8cbe-e38ed347c397" /> <option value="6dffb37e-ecb0-4c89-8cbe-e38ed347c397" />
<option value="7216273c-1271-4ea2-88d8-177e139316f1" />
<option value="7237b251-2008-431b-b6b3-005dea48c3bf" /> <option value="7237b251-2008-431b-b6b3-005dea48c3bf" />
<option value="72c65d6c-d369-4e6e-ad89-52749cab36a6" />
<option value="76858669-1b07-4bac-bc00-8448c673d06d" /> <option value="76858669-1b07-4bac-bc00-8448c673d06d" />
<option value="80ef4e51-2808-48f4-9166-bf883578c6f0" /> <option value="80ef4e51-2808-48f4-9166-bf883578c6f0" />
<option value="8589889d-a532-45c6-9ffe-0671c2d32583" /> <option value="8589889d-a532-45c6-9ffe-0671c2d32583" />
@ -36,12 +41,14 @@
<option value="9523ff40-4e2b-4c96-b52e-69093f946698" /> <option value="9523ff40-4e2b-4c96-b52e-69093f946698" />
<option value="9ebc3bf3-3f18-425d-a4fb-c87a6a121c9a" /> <option value="9ebc3bf3-3f18-425d-a4fb-c87a6a121c9a" />
<option value="aaf8bf1e-ce4a-48ab-a65e-6da5790fc566" /> <option value="aaf8bf1e-ce4a-48ab-a65e-6da5790fc566" />
<option value="ac68227b-a347-492a-8990-f2313a4c4838" />
<option value="b32841ec-424b-46e4-af25-b31a5ca4ae51" /> <option value="b32841ec-424b-46e4-af25-b31a5ca4ae51" />
<option value="ba4f0602-d62b-4a9c-b86f-ee0dce40374d" /> <option value="ba4f0602-d62b-4a9c-b86f-ee0dce40374d" />
<option value="c759a8ff-cf83-4618-90d4-31101dea7c77" /> <option value="c759a8ff-cf83-4618-90d4-31101dea7c77" />
<option value="e2907e39-fda1-4832-99ff-fb7407d64db3" /> <option value="e2907e39-fda1-4832-99ff-fb7407d64db3" />
<option value="ea6afe5a-c296-461b-a4a7-49209b3d7937" /> <option value="ea6afe5a-c296-461b-a4a7-49209b3d7937" />
<option value="ec11cbcb-475d-4b28-8eb3-f71715723d6c" /> <option value="ec11cbcb-475d-4b28-8eb3-f71715723d6c" />
<option value="ee1de16e-76b5-4384-8ed5-b2e3f4e3c2cb" />
<option value="f607f271-4885-4c4e-91da-e2caf2abd67a" /> <option value="f607f271-4885-4c4e-91da-e2caf2abd67a" />
<option value="fe272d92-3acf-42e8-bd88-66dc00800c3f" /> <option value="fe272d92-3acf-42e8-bd88-66dc00800c3f" />
</set> </set>
@ -57,6 +64,9 @@
<option value="file://$PROJECT_DIR$/app/build.gradle.kts" /> <option value="file://$PROJECT_DIR$/app/build.gradle.kts" />
<option value="file://$PROJECT_DIR$/app/src/main/java/id/ac/ubharajaya/panicbutton/MainActivity.kt" /> <option value="file://$PROJECT_DIR$/app/src/main/java/id/ac/ubharajaya/panicbutton/MainActivity.kt" />
<option value="file://$PROJECT_DIR$/app/src/main/java/id/ac/ubharajaya/panicbutton/EvacuationMaps.kt" /> <option value="file://$PROJECT_DIR$/app/src/main/java/id/ac/ubharajaya/panicbutton/EvacuationMaps.kt" />
<option value="file://$PROJECT_DIR$/app/src/main/java/id/ac/ubharajaya/panicbutton/EvacuationMapsActivity.kt" />
<option value="file://$PROJECT_DIR$/app/src/main/java/id/ac/ubharajaya/panicbutton/EvacuationMapDetailActivity.kt" />
<option value="file://$PROJECT_DIR$/app/src/main/AndroidManifest.xml" />
</set> </set>
</entry> </entry>
</pendingWorkingSetItems> </pendingWorkingSetItems>

View File

@ -50,6 +50,8 @@ dependencies {
implementation(libs.androidx.compose.ui.graphics) implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.material3)
// Foundation (for verticalScroll, rememberScrollState, etc.)
implementation("androidx.compose.foundation:foundation:1.5.0")
testImplementation(libs.junit) testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.espresso.core)

View File

@ -22,6 +22,18 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<!-- Evacuation map activities -->
<activity
android:name=".EvacuationMapsActivity"
android:exported="false"
android:label="Peta Evakuasi" />
<activity
android:name=".EvacuationMapDetailActivity"
android:exported="false"
android:label="Detail Peta" />
</application> </application>
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
</manifest> </manifest>

View File

@ -0,0 +1,34 @@
package id.ac.ubharajaya.panicbutton
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
class EvacuationMapDetailActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val which = intent.getStringExtra("which") ?: "selatan"
val resId = if (which == "selatan") R.drawable.lantai_1_selatan else R.drawable.lantai_1_utara
setContent {
MaterialTheme {
EvacuationMapDetailContent(resId)
}
}
}
}
@Composable
fun EvacuationMapDetailContent(drawableResId: Int) {
Column(modifier = Modifier.fillMaxSize().padding(12.dp)) {
Text(text = "Detail Peta", modifier = Modifier.padding(bottom = 12.dp))
Image(painter = painterResource(id = drawableResId), contentDescription = "Map Detail", modifier = Modifier.fillMaxWidth().heightIn(max = 800.dp))
}
}

View File

@ -0,0 +1,68 @@
package id.ac.ubharajaya.panicbutton
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
class EvacuationMapsActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
EvacuationMapsContent(onSelect = { which ->
val intent = Intent(this, EvacuationMapDetailActivity::class.java)
intent.putExtra("which", which)
startActivity(intent)
})
}
}
}
}
@Composable
fun EvacuationMapsContent(onSelect: (String) -> Unit) {
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
Text(text = "Peta Jalur Evakuasi", style = MaterialTheme.typography.titleLarge, modifier = Modifier.padding(bottom = 12.dp))
// Selatan
Card(modifier = Modifier.fillMaxWidth().padding(bottom = 12.dp).clickable { onSelect("selatan") }, shape = RoundedCornerShape(12.dp), elevation = CardDefaults.cardElevation(defaultElevation = 6.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(12.dp)) {
Image(painter = painterResource(id = R.drawable.lantai_1_selatan), contentDescription = "Selatan", modifier = Modifier.size(120.dp))
Spacer(modifier = Modifier.width(12.dp))
Column {
Text(text = "Jalur Evakuasi Selatan", fontSize = 18.sp)
Spacer(modifier = Modifier.height(6.dp))
Text(text = "Lihat detail jalur evakuasi lantai selatan.")
}
}
}
// Utara
Card(modifier = Modifier.fillMaxWidth().padding(bottom = 12.dp).clickable { onSelect("utara") }, shape = RoundedCornerShape(12.dp), elevation = CardDefaults.cardElevation(defaultElevation = 6.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(12.dp)) {
Image(painter = painterResource(id = R.drawable.lantai_1_utara), contentDescription = "Utara", modifier = Modifier.size(120.dp))
Spacer(modifier = Modifier.width(12.dp))
Column {
Text(text = "Jalur Evakuasi Utara", fontSize = 18.sp)
Spacer(modifier = Modifier.height(6.dp))
Text(text = "Lihat detail jalur evakuasi lantai utara.")
}
}
}
}
}

View File

@ -1,51 +1,26 @@
package id.ac.ubharajaya.panicbutton package id.ac.ubharajaya.panicbutton
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.viewModels import androidx.lifecycle.ViewModelProvider
import androidx.compose.runtime.Composable import androidx.compose.material3.MaterialTheme
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.navArgument
import androidx.navigation.compose.rememberNavController
import id.ac.ubharajaya.panicbutton.ui.theme.PanicButtonTheme
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private val viewModel: MainViewModel by viewModels() private lateinit var viewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContent { viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
PanicButtonTheme {
AppNav(viewModel = viewModel)
}
}
}
}
@Composable val openEvacMaps = {
fun AppNav(viewModel: MainViewModel) { startActivity(Intent(this, EvacuationMapsActivity::class.java))
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "main") {
composable("main") {
MainScreen(viewModel = viewModel, onOpenEvacMaps = { navController.navigate("evac_maps") })
} }
composable("evac_maps") { setContent {
EvacuationMapsScreen(navController = navController, onBack = { navController.popBackStack() }) MaterialTheme {
} MainScreen(viewModel = viewModel, onOpenEvacMaps = openEvacMaps)
composable(
route = "evac_map/{resName}",
arguments = listOf(navArgument("resName") { type = NavType.StringType })
) { backStackEntry ->
val resName = backStackEntry.arguments?.getString("resName") ?: ""
val resId = when (resName) {
"selatan" -> R.drawable.lantai_1_selatan
"utara" -> R.drawable.lantai_1_utara
else -> 0
} }
EvacuationMapDetailScreen(drawableResId = resId, onBack = { navController.popBackStack() })
} }
} }
} }

View File

@ -1,37 +1,37 @@
package id.ac.ubharajaya.panicbutton package id.ac.ubharajaya.panicbutton
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.KeyboardArrowUp import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Close
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.ui.draw.shadow import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.graphics.Brush
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.layout.IntrinsicSize import androidx.compose.foundation.layout.Arrangement
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.foundation.layout.IntrinsicSize
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun MainScreen(viewModel: MainViewModel, onOpenEvacMaps: () -> Unit) { fun MainScreen(viewModel: MainViewModel, onOpenEvacMaps: () -> Unit) {
Scaffold( Scaffold(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
bottomBar = { bottomBar = {
// wrap PanicButton in a Box so we can center it over the bottom bar
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -41,73 +41,65 @@ fun MainScreen(viewModel: MainViewModel, onOpenEvacMaps: () -> Unit) {
PanicButton(onClick = { viewModel.sendAlert() }, buttonSize = 130.dp, shadowSize = 160.dp) PanicButton(onClick = { viewModel.sendAlert() }, buttonSize = 130.dp, shadowSize = 160.dp)
} }
} }
) { paddingValues -> ) { padding ->
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(paddingValues) .padding(padding)
.padding(20.dp), .padding(20.dp),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
// Emergency instruction
// Emergency instruction text (darurat)
Text( Text(
text = "JANGAN PANIK, SEGERA EVAKUASI DIRI ANDA KE TITIK KUMPUL", text = "JANGAN PANIK, SEGERA EVAKUASI DIRI ANDA KE TITIK KUMPUL!!",
color = MaterialTheme.colorScheme.error, color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Bold), style = MaterialTheme.typography.bodyLarge.copy(
fontWeight = FontWeight.Bold
),
textAlign = TextAlign.Center,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(bottom = 12.dp), .padding(bottom = 12.dp)
.background(
MaterialTheme.colorScheme.error.copy(alpha = 0.2f),
shape = RoundedCornerShape(8.dp)
)
.border(
width = 1.dp,
color = MaterialTheme.colorScheme.error,
shape = RoundedCornerShape(8.dp)
)
.padding(12.dp),
maxLines = 2, maxLines = 2,
) )
// Styled Emergency Condition Card
EmergencyConditionCard(viewModel) EmergencyConditionCard(viewModel)
Spacer(Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
// Button to open evacuation maps
Button( Button(
onClick = onOpenEvacMaps, onClick = onOpenEvacMaps,
modifier = Modifier modifier = Modifier
.fillMaxWidth(0.6f) .fillMaxWidth(0.6f)
.height(48.dp), .height(48.dp),
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(12.dp)
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary)
) { ) {
Text(text = "Lihat Peta Evakuasi", color = MaterialTheme.colorScheme.onPrimary) Text(text = "Lihat Peta Evakuasi")
} }
// Tambahkan spacer di luar Card agar ada jarak dari bottom bar Spacer(modifier = Modifier.height(20.dp))
Spacer(Modifier.height(20.dp))
} }
// Dialog Feedback // Dialog
val dialogMessage = viewModel.dialogMessage val dialogMessage = viewModel.dialogMessage
if (!dialogMessage.isNullOrBlank()) { if (!dialogMessage.isNullOrBlank()) {
AlertDialog( AlertDialog(
onDismissRequest = { viewModel.clearDialog() }, onDismissRequest = { viewModel.clearDialog() },
confirmButton = { confirmButton = {
TextButton(onClick = { viewModel.clearDialog() }) { TextButton(onClick = { viewModel.clearDialog() }) { Text("OK") }
Text("OK")
}
}, },
title = { title = { Text("🚨 Notifikasi", style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold)) },
Text( text = { Text(dialogMessage ?: "") }
"Notifikasi",
style = MaterialTheme.typography.titleMedium.copy(
fontWeight = FontWeight.Bold
)
)
},
text = {
Text(
dialogMessage,
style = MaterialTheme.typography.bodyMedium
)
},
containerColor = MaterialTheme.colorScheme.surface
) )
} }
} }
@ -231,8 +223,8 @@ fun EmergencyConditionCard(viewModel: MainViewModel) {
) )
// Show first 2 selected items as chips // Show first 2 selected items as chips
selectedItems.take(2).forEach { option -> selectedItems.take(2).forEach { opt ->
SelectedChip(option = option, viewModel = viewModel) SelectedChip(option = opt, viewModel = viewModel)
} }
if (selectedItems.size > 2) { if (selectedItems.size > 2) {
@ -250,7 +242,7 @@ fun EmergencyConditionCard(viewModel: MainViewModel) {
expanded = expanded, expanded = expanded,
onDismissRequest = { expanded = false }, onDismissRequest = { expanded = false },
modifier = Modifier modifier = Modifier
.width(IntrinsicSize.Max) .fillMaxWidth()
.background( .background(
color = MaterialTheme.colorScheme.surface, color = MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(16.dp) shape = RoundedCornerShape(16.dp)
@ -263,12 +255,12 @@ fun EmergencyConditionCard(viewModel: MainViewModel) {
.heightIn(max = 280.dp) .heightIn(max = 280.dp)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
) { ) {
viewModel.options.forEach { option -> viewModel.options.forEach { opt ->
EmergencyOptionItem( EmergencyOptionItem(
option = option, option = opt,
isChecked = viewModel.isChecked(option.label), isChecked = viewModel.isChecked(opt.label),
onCheckedChange = { checked -> onCheckedChange = { checked ->
viewModel.setChecked(option.label, checked) viewModel.setChecked(opt.label, checked)
} }
) )
} }