구글 맵에 표시하기 기능 추가
https://developers.google.com/maps/documentation/android-sdk/get-api-key?hl=ko
결제 정보를 크레딧 카드를 연결하고 프로젝트 생성 후 키를 발급 받는다
키 제한 설정(패키지 이름과 SHA-1 인증서 디지털 지문)을 하여 특정 어플에서만 키를 사용할 수 있도록 한다
SHA-1 코드 얻는 방법
Gradle탭에서 아래 그림과 같이 하면 SHA1 코드 얻을 수 있다
gradle signingreport 검색하면 빌드과 되고 코드가 Run에 나옴
//network call
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
//Json loader
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.google.android.gms:play-services-location:21.3.0'
implementation 'com.google.maps.android:maps-compose:2.15.0'
implementation 'com.google.android.gms:play-services-maps:18.1.0'
implementation 'androidx.navigation:navigation-compose:2.5.3'
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.ShoppingApp"
tools:targetApi="31">
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="key값"
/>
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.ShoppingApp">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
package com.lullulalal.shoppingapp
data class LocationData(
val latitude:Double,
val longitude:Double,
)
data class GeocordingResponse(
val result:List<GeocordingResult>,
val status: String,
)
data class GeocordingResult (
val formatted_address: String,
)
위치 선택용 찹업 만들기
두가지 매개변수 필요
1.실제 위치 : 화면이나 지도레서 선택해서 얻은 위도 경도
2.위치 선택했을 때 실행하려는 것, 선택한 위치로 어떤 것을 해야하는지
사용자의 위치가 바뀌면 지도상 보여지는 위치도 변경됨
GoogleMap 클래스
package com.lullulalal.shoppingapp
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.google.android.gms.maps.model.LatLng
import com.google.android.gms.maps.model.CameraPosition
import com.google.android.gms.maps.model.Marker
import com.google.maps.android.compose.GoogleMap
import com.google.maps.android.compose.Marker
import com.google.maps.android.compose.MarkerState
import com.google.maps.android.compose.rememberCameraPositionState
//두가지 매개변수 필요
//1.실제 위치 : 화면이나 지도레서 선택해서 얻은 위도 경도
//2.위치 선택했을 때 실행하려는 것, 선택한 위치로 어떤 것을 해야하는지
@Composable
fun LocationSelectionScreen(
lacation : LocationData,
onLocationSelected : (LocationData) -> Unit
) {
//실제 사용자 위치
val userLocation = remember{
mutableStateOf(LatLng(lacation.latitude, lacation.longitude))
}
//구글 지도 선택지에서 내 위치 찾기
var cameraPositionState = rememberCameraPositionState {
position = CameraPosition.fromLatLngZoom(userLocation.value, 10f) //내 위지(핸드폰 위치)와 그 위치 기준으로 얼마나 확대해 보여줄건지
}
Column(modifier = Modifier.fillMaxSize()) {
GoogleMap(
modifier = Modifier
.weight(1f)
.padding(top = 16.dp),
cameraPositionState = cameraPositionState,
onMapClick = {
userLocation.value = it //지도에서 클릭한 it이 가르키는 LatLng 클래스가 userLocation에 들어가도록
}
) {
Marker(state = MarkerState(position = userLocation.value))
}
var newLocation : LocationData
Button(onClick = {
/* 버튼 클릭 시 위지 셋팅 */
newLocation = LocationData(
userLocation.value.latitude,
userLocation.value.longitude)
onLocationSelected(newLocation) //매개변수로 설정한 놈, 위치 클릭하면 위도 경도 가져옴
}) {
Text(text = "Set Location")
}
}
}
위치를 관리해 주는 뷰모델, 새로운 위치가 생성되면 업데이트 한다
package com.lullulalal.shoppingapp
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
class LocationViewModel : ViewModel() {
//우리 위치를 관리해 주는 뷰모델
private val _location = mutableStateOf<LocationData?>(null)
val location : State<LocationData?> = _location
//새로운 위치가 생겼을 때 업데이트 하는 함수
fun updateLocation(newLocation:LocationData) {
_location.value = newLocation //뷰 모델 안 상태를 State 머신이 관리해줌
}
}
이전 프로젝트에서 만들었던 LocationUtils파일 복붙
package com.lullulalal.shoppingapp
import android.Manifest
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.PackageManager
import android.location.Address
import android.location.Geocoder
import android.os.Looper
import androidx.core.content.ContextCompat
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationCallback
import com.google.android.gms.location.LocationRequest
import com.google.android.gms.location.LocationResult
import com.google.android.gms.location.LocationServices
import com.google.android.gms.location.Priority
import com.google.android.gms.maps.model.LatLng
import java.util.Locale
class LocationUtils(val context: Context) {
//FusedLocationProviderClient -> 위도 경도 찾을 수 있음
private val _fusedLocationClient : FusedLocationProviderClient
= LocationServices.getFusedLocationProviderClient(context)
//실제 위치를 탐색할 함수
@SuppressLint("MissingPermission")
fun requestLocationUpdate(viewModel: LocationViewModel) {
//Callback : 무언가를 요청한 후 해당 작업이 완료될 때까지 기다린 다음 반환해주는 것
//시간이 걸릴 것 같은 경우 이 콜백 써준다
val locationCallback = object : LocationCallback(){
//클래스에서 상속받아 오버라이드 하는 대신 객체를 직접 만들고 그 내에서 바로 오버라이드 가능
override fun onLocationResult(locationResult: LocationResult) {
super.onLocationResult(locationResult)
//locationResult.lastLocation : 제일 최근 위치 반환
locationResult.lastLocation?.let {
//let 함수 안에서 it이 정보를 보여하고 있음
val location = LocationData(latitude = it.latitude, longitude = it.longitude) //it함수 안에서 정보 추출
viewModel.updateLocation(location) //뷰 모델에 위도경도 업데이트
}
}
}
//위치 요청 빌더 만들기
//LocationRequest.Builder(위치 정확도, 얼마나 자주 탐색할것인지).build()
//Priority엔 4가지 속성 있음
//정확도가 높을 수록 배터리 많이 사용
//1.PRIORITY_HIGH_ACCURACY : 높은 정확도
//2.PRIORITY_LOW_POWER : 배터리 덜 사용
//3.PRIORITY_BALANCED_POWER_ACCURACY : 배터리와 정확도 균형
//4.PRIORITY_PASSIVE : 배터리 아주 적게 사용
val locationRequest = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 1000).build()
//_fusedLocationClient의 requestLocationUpdates 매소드 사용하여 모든것을 하나로 묶기
_fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, Looper.getMainLooper())
//-> 해당 기능을 사용하고 싶을 떄마다 권한을 요청해야됨
//하지만 실제로 권한이 있을 떄만 이 함수를 실행할것이기떄문에 퍼미션을 무시하라는 어노테이션 붙여주기 : @SuppressLint("MissingPermission")
//Looper.getMainLooper() : 위치 업데이트를 위한 threading과 메세지 처리하는 데 사용함
//특정 looper를 제공해 어떤 스레드 위치 업데이트가 전달 될 지 정할 수 있음 지금은 메인 루퍼 전달함
}
//위치 권한에 액세스가 있는지 없는지 확인하는 함수
fun hasLocationPermission(context: Context):Boolean {
return ContextCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED //PERMISSION_GRANTED문자열이 ACCESS_FINE_LOCATION에 있는지 체크하는 부분
&&
ContextCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED
}
//checkSelfPermission는 리턴 값이 int임 우린 Boolean이 필요 떄문에 PERMISSION_GRANTED와 같은 지 체크하여 액세스 승인했는지 체크함
fun reverseGeoDecodeLocation(location: LocationData) : String {
val geocoder = Geocoder(context, Locale.getDefault())
//Geocoder(Context, 언어 설정 (Locale.getDefault()는 폰의 기본 언어 설정 부름))
val coordinator = LatLng(location.latitude,location.longitude)
//구글 맵에서 제공하는 LatLng매소드에 좌표 설정
val addresses : MutableList<Address>? =
geocoder.getFromLocation(
coordinator.latitude,
coordinator.longitude,
1)
//특정 위치에 맞는 주소가 여러개 있을 수 있기 때문에 MutableList로 받음
//geocoder.getFromLocation(위도,경도,찾은 것중 리턴할 주소 갯수)
return if (addresses?.isNotEmpty() == true) {
addresses[0].getAddressLine(0)
} else {
"찾은 주소가 없습니다"
}
}
}
LocationSelectionScreen을 부르는 버튼 추가하기
data class ShoppingItem(val id:Int,
var name:String,
var quantity: Int,
var isEditing:Boolean = false,
var address: String="")
@Composable
fun ShoppingListItem(
item: ShoppingItem,
onEditClick: () -> Unit,
onDeleteClick: () -> Unit,
) {
Row(modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
.border(
border = BorderStroke(2.dp, Color.Blue),
shape = RoundedCornerShape(20)
),
horizontalArrangement = Arrangement.SpaceBetween
) {
Column(
Modifier
.weight(1f)
.padding(8.dp)) {
Row {
Text(text = item.name, modifier = Modifier.padding(8.dp))
Text(text = "Qty: ${item.quantity}", modifier = Modifier.padding(8.dp))
}
Row(Modifier.fillMaxWidth()) {
Icon(imageVector = Icons.Default.LocationOn, contentDescription = "map")
Text(text = item.address) //주소 표시
}
}
Row(modifier = Modifier.padding(8.dp)) {
IconButton(onClick = onEditClick) {
Icon(imageVector = Icons.Default.Edit, contentDescription = "수정")
//Icons.Default.Edit : 안드로이드에서 기본 제공 icon
}
IconButton(onClick = onDeleteClick) {
Icon(imageVector = Icons.Default.Delete, contentDescription = "삭제")
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ShoppingListApp(
locationUtils: LocationUtils,
viewModel: LocationViewModel,
navController: NavController,
context: Context,
address: String,
) {...}
3-2) 수정버튼 누르면 이름과 수량 지도에서 선택한 주소 표시
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ShoppingListApp(
locationUtils: LocationUtils,
viewModel: LocationViewModel,
navController: NavController,
context: Context,
address: String,
) {...
if (item.isEditing){ //수정
ShoppingItemEditor(item = item, onEditComplete = {
editedName, editedQuanitity ->
sItems = sItems.map { it.copy(isEditing = false) }
val editedItem = sItems.find { it.id == item.id } //특정 항목 찾아줌
editedItem?.let {
it.name = editedName //편집 항목의 이름이 지금 텍스트 입력창에 입력한 이름이로 변경
it.quantity = editedQuanitity
it.address = address
}
})
}
...
//다이아 로그
if (showDialog) {
...
confirmButton = {
Row(modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
horizontalArrangement = Arrangement.SpaceBetween //양 끝에 위치하기
) {
Button(onClick = {
if (itemName.isNotBlank() && itemQuntity.isNotBlank()) {
val newItem = ShoppingItem(
id = sItems.size+1,
name = itemName,
quantity = itemQuntity.toInt(),
address = address
)
sItems = sItems + newItem
showDialog = false
itemName = ""
}
}) {
Text(text = "Add")
}
Button(onClick = {showDialog = false}) {
Text(text = "Close")
}
}
})
...
}
}
//위치 권한 런처
val requestPermissionLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestMultiplePermissions(), //contract는 permissions를 리턴함
onResult = {permissions ->
if (permissions[Manifest.permission.ACCESS_COARSE_LOCATION]==true
&& permissions[Manifest.permission.ACCESS_FINE_LOCATION]==true ) {
//위치에 권한 있다
//위도 -경도 셋팅하는 함수 호출!!
locationUtils.requestLocationUpdate(viewModel)
} else {
//권한 요청 필요
val rationaleRequired = ActivityCompat.shouldShowRequestPermissionRationale(
context as MainActivity, //메인 액티비치 말곤 다른창에선 이 팝업 열지말라는 등록
Manifest.permission.ACCESS_FINE_LOCATION
) || ActivityCompat.shouldShowRequestPermissionRationale(
context as MainActivity,
Manifest.permission.ACCESS_COARSE_LOCATION
)
//이 퍼미션이 필요한 이유를 설명해주기
if (rationaleRequired) {
Toast.makeText(context, "이 기능을 사용하려면 위치 권한이 필요합니다.", Toast.LENGTH_SHORT).show()
} else {
//rationaleRequired이 거부 상황일경우 사용자의 설정이나 안드로이드 폰 설정으로 이동
Toast.makeText(context, "설정에 들어가 위치권한을 활성화 해주세요.", Toast.LENGTH_SHORT).show()
}
}
}) //contract에서 리턴받은 permissions가 모두 허용됐는지 체크하는 부분
if (showDialog) {
AlertDialog(onDismissRequest = {showDialog = false},
title = { Text(text = "추가") },
text = { Column {
OutlinedTextField(
value = itemName,
onValueChange = { itemName = it},//값을 입력할 떄 마다 보여줌
singleLine = true, //다중줄 막음
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
)
OutlinedTextField(
value = itemQuntity,
onValueChange = {
if (it.all { char -> char.isDigit() }) {
itemQuntity = it
}
},//값을 입력할 떄 마다 보여줌
singleLine = true, //다중줄 막음
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Number
)
)
//권한 요청 버튼
Button(onClick = {
if (locationUtils.hasLocationPermission(context)) {
locationUtils.requestLocationUpdate(viewModel)
navController.navigate("locationscreen"){
this.launchSingleTop //스택 내부에는 한가지 화면만 있어야한다하는 뜻
}
} else {
requestPermissionLauncher.launch(arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
))
}
}) {
Text(text = "address here")
}
}},
confirmButton = {
Row(modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
horizontalArrangement = Arrangement.SpaceBetween //양 끝에 위치하기
) {
Button(onClick = {
if (itemName.isNotBlank() && itemQuntity.isNotBlank()) {
val newItem = ShoppingItem(
id = sItems.size+1,
name = itemName,
quantity = itemQuntity.toInt()
)
sItems = sItems + newItem
showDialog = false
itemName = ""
}
}) {
Text(text = "Add")
}
Button(onClick = {showDialog = false}) {
Text(text = "Close")
}
}
})
}
package com.mbsysoft.myshoppinglistapp
import android.Manifest
import android.content.Context
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.core.app.ActivityCompat
import androidx.navigation.NavController
import com.lullulalal.shoppingapp.LocationUtils
import com.lullulalal.shoppingapp.LocationViewModel
import com.lullulalal.shoppingapp.MainActivity
data class ShoppingItem(val id:Int,
var name:String,
var quantity: Int,
var isEditing:Boolean = false,
var address: String="")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ShoppingListApp(
locationUtils: LocationUtils,
viewModel: LocationViewModel,
navController: NavController,
context: Context,
address: String,
) {
// 목록 상태를 지켜보는 변수
//Compose에서 어떠한 상태 값이 바뀌게 되면 재구성(Recomposition)이 일어나게 된다
//MutableState 클래스는 Compose에서 읽기와 쓰기를 관찰하기 위해 만들어진 클래스
var sItems by remember {//by 키워드를 사용하게 되면 get/set에 대한 위임이 이루어지
mutableStateOf(listOf<ShoppingItem>())
}
var showDialog by remember {
mutableStateOf(false) //표시 상태 지켜보는 애 true가 되면 보임
}
var itemName by remember {
mutableStateOf("")
}
var itemQuntity by remember {
mutableStateOf("")
}
//위치 권한 런처
val requestPermissionLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestMultiplePermissions(), //contract는 permissions를 리턴함
onResult = {permissions ->
if (permissions[Manifest.permission.ACCESS_COARSE_LOCATION]==true
&& permissions[Manifest.permission.ACCESS_FINE_LOCATION]==true ) {
//위치에 권한 있다
//위도 -경도 셋팅하는 함수 호출!!
locationUtils.requestLocationUpdate(viewModel)
} else {
//권한 요청 필요
val rationaleRequired = ActivityCompat.shouldShowRequestPermissionRationale(
context as MainActivity, //메인 액티비치 말곤 다른창에선 이 팝업 열지말라는 등록
Manifest.permission.ACCESS_FINE_LOCATION
) || ActivityCompat.shouldShowRequestPermissionRationale(
context as MainActivity,
Manifest.permission.ACCESS_COARSE_LOCATION
)
//이 퍼미션이 필요한 이유를 설명해주기
if (rationaleRequired) {
Toast.makeText(context, "이 기능을 사용하려면 위치 권한이 필요합니다.", Toast.LENGTH_SHORT).show()
} else {
//rationaleRequired이 거부 상황일경우 사용자의 설정이나 안드로이드 폰 설정으로 이동
Toast.makeText(context, "설정에 들어가 위치권한을 활성화 해주세요.", Toast.LENGTH_SHORT).show()
}
}
}) //contract에서 리턴받은 permissions가 모두 허용됐는지 체크하는 부분
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center
) {
Button(
onClick = { showDialog = true },
modifier = Modifier.align(Alignment.CenterHorizontally)
) {
Text(text = "Add Item")
}
LazyColumn( //리사이클러뷰 같은 거 - Lazy column 은 잠재적으로 무한하거나 끝이 없는 항목을 처리하도록 설계
// 그리드 레이아웃을 만들기 위한 것이 아니라 수직 시퀀스에 잠재적으로 무한한 항목 목록을 표시하는 데 사용
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
content = {
items(sItems) {
// ShoppingListItem(it, {}, {})
item ->
if (item.isEditing){ //수정
ShoppingItemEditor(item = item, onEditComplete = {
editedName, editedQuanitity ->
sItems = sItems.map { it.copy(isEditing = false) }
val editedItem = sItems.find { it.id == item.id } //특정 항목 찾아줌
editedItem?.let {
it.name = editedName //편집 항목의 이름이 지금 텍스트 입력창에 입력한 이름이로 변경
it.quantity = editedQuanitity
it.address = address
}
})
} else {
ShoppingListItem(item = item, onEditClick = {
//어떤 항목을 편집하는지 알아내는 코드
//편집하는 것은 isEditing이 true인것을 의미한다
sItems = sItems.map { it.copy(isEditing = it.id == item.id) }
}, onDeleteClick = {
sItems = sItems - item
})
}
}
})
}
//다이아 로그
if (showDialog) {
AlertDialog(onDismissRequest = {showDialog = false},
title = { Text(text = "추가") },
text = { Column {
OutlinedTextField(
value = itemName,
onValueChange = { itemName = it},//값을 입력할 떄 마다 보여줌
singleLine = true, //다중줄 막음
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
)
OutlinedTextField(
value = itemQuntity,
onValueChange = {
if (it.all { char -> char.isDigit() }) {
itemQuntity = it
}
},//값을 입력할 떄 마다 보여줌
singleLine = true, //다중줄 막음
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
keyboardOptions = KeyboardOptions.Default.copy(
keyboardType = KeyboardType.Number
)
)
//권한 요청 버튼
Button(onClick = {
if (locationUtils.hasLocationPermission(context)) {
locationUtils.requestLocationUpdate(viewModel)
navController.navigate("locationscreen"){
this.launchSingleTop //스택 내부에는 한가지 화면만 있어야한다하는 뜻
}
} else {
requestPermissionLauncher.launch(arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
))
}
}) {
Text(text = "address here")
}
}},
confirmButton = {
Row(modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
horizontalArrangement = Arrangement.SpaceBetween //양 끝에 위치하기
) {
Button(onClick = {
if (itemName.isNotBlank() && itemQuntity.isNotBlank()) {
val newItem = ShoppingItem(
id = sItems.size+1,
name = itemName,
quantity = itemQuntity.toInt(),
address = address
)
sItems = sItems + newItem
showDialog = false
itemName = ""
}
}) {
Text(text = "Add")
}
Button(onClick = {showDialog = false}) {
Text(text = "Close")
}
}
})
}
}
@Composable
fun ShoppingItemEditor(item: ShoppingItem, onEditComplete:(String, Int) -> Unit){
var editeName by remember { mutableStateOf(item.name) }
var editeQuality by remember { mutableStateOf(item.quantity.toString()) }
var isEditing by remember { mutableStateOf(item.isEditing) }
Row(modifier = Modifier
.fillMaxWidth()
.background(Color.White)
.padding(8.dp),
horizontalArrangement = Arrangement.SpaceEvenly) {
Column() {
BasicTextField(
value = editeName,
onValueChange = { editeName = it },
singleLine = true,
modifier = Modifier
.wrapContentWidth()
.padding(8.dp))
BasicTextField(
value = editeQuality,
onValueChange = { editeQuality = it },
singleLine = true,
modifier = Modifier
.wrapContentWidth()
.padding(8.dp))
Button(onClick = {
isEditing = false
onEditComplete(editeName, editeQuality.toIntOrNull()?:1)
}) {
Text(text = "Save")
}
}
}
}
@Composable
fun ShoppingListItem(
item: ShoppingItem,
onEditClick: () -> Unit,
onDeleteClick: () -> Unit,
) {
Row(modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
.border(
border = BorderStroke(2.dp, Color.Blue),
shape = RoundedCornerShape(20)
),
horizontalArrangement = Arrangement.SpaceBetween
) {
Column(
Modifier
.weight(1f)
.padding(8.dp)) {
Row {
Text(text = item.name, modifier = Modifier.padding(8.dp))
Text(text = "Qty: ${item.quantity}", modifier = Modifier.padding(8.dp))
}
Row(Modifier.fillMaxWidth()) {
Icon(imageVector = Icons.Default.LocationOn, contentDescription = "map")
Text(text = item.address) //주소 표시
}
}
Row(modifier = Modifier.padding(8.dp)) {
IconButton(onClick = onEditClick) {
Icon(imageVector = Icons.Default.Edit, contentDescription = "수정")
//Icons.Default.Edit : 안드로이드에서 기본 제공 icon
}
IconButton(onClick = onDeleteClick) {
Icon(imageVector = Icons.Default.Delete, contentDescription = "삭제")
}
}
}
}
retrofit 기능을 사용하여 데이터 로딩, 구글 맵 url 맵핑하기
package com.lullulalal.shoppingapp
import retrofit2.http.GET
import retrofit2.http.Query
interface GeocordingAptService {
@GET("maps/api/geocode/json")
suspend fun getAddressFromCoordinates(
@Query("latlng") latlng:String,
@Query("key") apiKey: String
): GeocordingResponse
}
package com.lullulalal.shoppingapp
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
object RetrofitClient {
private const val BASE_URL="https://maps.googleapis.com/"
fun create(): GeocordingAptService {
val retrofit = Retrofit.Builder().baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
return retrofit.create(GeocordingAptService::class.java)
}
}
뷰 모델은 데이터를 사져오거나 관리를 담당하기 떄문에 viewModel에서 사용한다
package com.lullulalal.shoppingapp
import android.util.Log
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
class LocationViewModel : ViewModel() {
//우리 위치를 관리해 주는 뷰모델
private val _location = mutableStateOf<LocationData?>(null)
val location : State<LocationData?> = _location
private val _address = mutableStateOf(listOf<GeocordingResult>())
val address:State<List<GeocordingResult>> = _address
//새로운 위치가 생겼을 때 업데이트 하는 함수
fun updateLocation(newLocation:LocationData) {
_location.value = newLocation //뷰 모델 안 상태를 State 머신이 관리해줌
}
//주소 얻는 함수
fun fetchAddress(latLng:String) {
try {
viewModelScope.launch {
val result = RetrofitClient.create()
.getAddressFromCoordinates(latLng, "")
_address.value = result.results //리스트의 첫번째 항목을 가져와 화면에 표시할 것임
}
} catch (e: Exception) {
Log.d("TAG","${e.message}")
}
}
}
package com.lullulalal.shoppingapp
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.dialog
import androidx.navigation.compose.rememberNavController
import com.lullulalal.shoppingapp.ui.theme.ShoppingAppTheme
import com.mbsysoft.myshoppinglistapp.ShoppingListApp
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ShoppingAppTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Navigation()
}
}
}
}
}
//navigation을 사용하여 쇼핑리스트와 지도맵 화면을 바꿔줘야함
@Composable
fun Navigation() {
val navController = rememberNavController()
val viewModel: LocationViewModel = viewModel()
val context = LocalContext.current
val locationUtil = LocationUtils(context = context)
NavHost(navController, startDestination = "shoppinglistscreen"){
composable("shoppinglistscreen") {
ShoppingListApp(
locationUtils = locationUtil,
viewModel = viewModel,
navController = navController,
context = context,
address = viewModel.address.value.firstOrNull()?.formatted_address ?: "No Address"
//firstOrNull() : list에 있는 함수로 찾아온 항목이 몇개든지 첫번쨰 아이템만 필요하다는 뜻
)
}
//NavHost는 전체화면 차지하는 composable만 사용하는 것이 아니라 dialog도 사용가능함
dialog("locationscreen") {
backStack-> viewModel.location.value?.let {
//it이 부르는 lacationCata가 locationSelectionScreen에 위치로 사용가능해짐
LocationSelectionScreen(lacation = it, onLocationSelected = {
viewModel.fetchAddress("${it.latitude}, ${it.longitude}")
navController.popBackStack()
})
}
}
}
}