14_쇼핑앱2

소정·2024년 7월 31일
0

Android_with_compose

목록 보기
14/17
post-thumbnail

구글 맵에 표시하기 기능 추가

1. 구글 개발자 페이지에서 key발급

https://developers.google.com/maps/documentation/android-sdk/get-api-key?hl=ko

결제 정보를 크레딧 카드를 연결하고 프로젝트 생성 후 키를 발급 받는다
키 제한 설정(패키지 이름과 SHA-1 인증서 디지털 지문)을 하여 특정 어플에서만 키를 사용할 수 있도록 한다

SHA-1 코드 얻는 방법
Gradle탭에서 아래 그림과 같이 하면 SHA1 코드 얻을 수 있다
gradle signingreport 검색하면 빌드과 되고 코드가 Run에 나옴


2. 매니패스트에 키 메타 데이터로 등록& bulid.gradle에 디펜던시

2-1) build.gradle에 구글맵 사용하기 위한 디펜던시

//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'

2-2) application 태그 안에 구글 맵 키 메타 데이터로 등록한다

<?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>

3. 구글 맵 응답용 class만들기

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,
)

4. 위치 선택 화면 만들기

위치 선택용 찹업 만들기

두가지 매개변수 필요
1.실제 위치 : 화면이나 지도레서 선택해서 얻은 위도 경도
2.위치 선택했을 때 실행하려는 것, 선택한 위치로 어떤 것을 해야하는지

사용자의 위치가 바뀌면 지도상 보여지는 위치도 변경됨

GoogleMap 클래스


4-1) 구글맵에서 위치 선택 셋팅 코드

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")
        }
    }
}

5. 뷰 모델 설정

위치를 관리해 주는 뷰모델, 새로운 위치가 생성되면 업데이트 한다

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 머신이 관리해줌
    }

}

6. LocationUtils 클래스 파일 만들기

이전 프로젝트에서 만들었던 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 {
            "찾은 주소가 없습니다"
        }

    }

}

7. SoppingList 화면 변경

LocationSelectionScreen을 부르는 버튼 추가하기

  1. ShoppingItem data에 주소 담을 변수 추가
data class ShoppingItem(val id:Int,
                        var name:String,
                        var quantity: Int,
                        var isEditing:Boolean = false,
                        var address: String="")
  1. ShoppingListItem안 항목도 바꾸기
    -> 위치 찾는 버튼과 결과값 보여줄 부분 추가
@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 = "삭제")
            }
        }
    }
}
  1. 쇼핑리스트앱 업데이트
    3-1) ShoppingListApp함수에 아래 5개 항목 매개변수 받기
    -> 퍼미션을 위한 LocationUtils,
    location을 관리해주는 LocationViewModel,
    다양한 화면과 컴포저블 사이간 이동을 위한 NavController,
    퍼미션과 새 화면표시, NavController 사용에 쓸 Context,
    마지막으로 ShoppingItem의 address 매개변수로 받음
@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")
                    }
                }
            })
    
    ...
    
    }

}

  1. request permission 런처 추가
//위치 권한 런처
    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가 모두 허용됐는지 체크하는 부분
  1. 위치 권한 요청 버튼 생성
    AlertDialog에 퍼미션 버튼 추가
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")
                    }
                }
            })
    }

shoppingList.kt 수정 한 총 코드

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 = "삭제")
            }
        }
    }
}




8. 주소 로딩, 좌표에서 주소 가져오고 부르기

retrofit 기능을 사용하여 데이터 로딩, 구글 맵 url 맵핑하기

8-1) 엔드포인트 service 파일 만들기

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

}

8-2) 레트로핏 빌더 만들기

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)
    }
}

8-3) viewModel에서 만든 create() 함수 사용하기

뷰 모델은 데이터를 사져오거나 관리를 담당하기 떄문에 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}")
        }
    }

}

9. navController 사용하여 mainActivity에서 화면 연동하기

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()
                    })
            }
        }
    }
}


참고) https://tutorials.eu/mastering-integration-api-calls-json-parsing-and-google-maps-day-12-android-14-masterclass/

profile
보조기억장치

0개의 댓글