1. 사용자 인증키 만들기

 

구글 클라우드 들어가기

https://console.cloud.google.com/

 

Google 클라우드 플랫폼

로그인 Google 클라우드 플랫폼으로 이동

accounts.google.com

 

프로젝트 선택 -> 새 프로젝트 -> 프로젝트 이름 기입 -> 만들기

프로젝트 선택

새 프로젝트

프로젝트 이름 기입 후 만들기

 

 


저의 경우 이미 만들어놓은 PracticeGoogleMap으로 진행하겠습니다.

왼쪽 상단의 메뉴 이미지 > API 및 서비스 > 사용자 인증 정보

 

 

 

 

해당 프로젝트 선택 후 API 및 서비스 > 사용자 인증 정보

 

 

 

사용자 인증 정보 만들기

 

 

사용자 인증 정보 만들기

 

 

API키 

 

 

API키

 

↓ 

 

그럼 해당 키가 나옵니다. 지금 부터는 옵션인데 제한 사항을 세팅하는 과정입니다.

제한 사항을 추가안하실 분들은 이 부분은 넘기셔도 됩니다.

현재 내 프로젝트만 사용 가능하게끔 제한을 걸기위해선 패키지명과 SHA-1 값이 필요합니다.

안드로이드 스튜디오 터미널 창을 열어 해당 코드를 쳐줍니다.

 

./gradlew signingReport

 

 

아래와 같이 SHA1 값을 복사해줍니다.

 

 

SHA-1 인증서 디지털 지문 입력란에 붙여넣기

 

 

 

구글 맵 API를 이용할거기에 Maps SDK for Android 선택

 

 

API Key 값 복사

 

 

 

 

2. 안드로이드 스튜디오 Manifest 및 gradle 설정

 

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.INTERNET"/>

 

매니페스트 파일에 사용자 권한 추가

 

Android 스튜디오에서 프로젝트 수준 build.gradle 파일을 열고 다음 코드를 buildscript 아래 dependencies 요소에 추가합니다.

plugins {
    ...
    id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") version "2.0.1" apply false
}

 

그런 다음 모듈 수준 build.gradle 파일을 열고 plugins 요소에 다음 코드를 추가합니다.

plugins {
    ...
    id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin")
}

 

모듈 수준 build.gradle 파일 dependencies 요소에 다음 코드를 추가합니다.

dependencies {

    ...
    
    // GoogleMap
    implementation("com.google.android.gms:play-services-maps:18.2.0")
    // (옵션) GPS 이용해서 위치 확인
    implementation("com.google.android.gms:play-services-location:21.1.0")
}
  1. 파일을 저장하고 프로젝트를 Gradle과 동기화합니다.
  2. 프로젝트 수준 디렉터리에서 local.properties를 열고 다음 코드를 추가합니다. YOUR_API_KEY를 API 키로 변경합니다.
  3. API 키를 AndroidManifest.xml 파일에 그대로 추가하지 마세요. 그렇게 하면 API 키가 안전하지 못한 방식으로 저장됩니다. 아래와 같이 loacal.properties 파일에 저장 후 Manifest 파일에서 참조하여 사용하세요.

MAPS_API_KEY=YOUR_API_KEY

 

AndroidManifest.xml 파일에서 com.google.android.geo.API_KEY로 이동한 후 android:value attribute를 다음과 같이 업데이트합니다.

<meta-data
    android:name="com.google.android.geo.API_KEY"
    android:value="${MAPS_API_KEY}" />

 

이러면 환경설정은 끝

ListAdapter란?

ListAdapter는 RecyclerView.Adapter의 확장된 형태로, RecyclerView의 데이터를 리스트 형태로 관리하며, 효율적인 데이터 리스트 업데이트를 위해 DiffUtil을 내부적으로 사용한다. 기존의 RecyclerViewAdapter와 ListAdapter의 주요 차이점은 데이터의 변경을 관리하는 방식에 있다.

 

조금 더 보태자면 기존의 RecyclerViewAdapter는 정적인, 즉 고정된 데이터를 보여주기에 최적화되어 있고 ListAdapter는 데이터가 동적으로 추가되고 삭제되는 일들이 많을 때 효율적으로 동작하기 위해 최적화되어있다고 생각하면 된다. 

RecyclerViewAdapter와의 차이점

RecyclerViewAdapter

  • 데이터 변경을 감지하고 UI를 업데이트하기 위해서는 개발자가 명시적으로 notifyDataSetChanged(), notifyItemInserted()notifyItemRemoved() 등의 메서드를 호출해야 한다.
  • 데이터가 변경될 때마다 전체 리스트를 업데이트하거나, 특정 위치의 데이터가 변경되었다는 것을 직접 알려줘야 한다.
  • 능상의 이유로, 대량의 데이터가 변경될 때 비효율적일 수 있다. ( 모든 아이템을 전부 다시 그리기 때문)

ListAdapter

  • 내부적 으로 AsyncListDiffer를 사용하여 데이터의 변경사항을 비동기적으로 계산하고, 필요한 최소한의 업데이트를 RecyclerView에 알려준다. (DiffUtil 메소드를 통해서 변경된 부분만 뷰를 그림)
  • 개발자는 단순히 새로운 리스트를 submitList() 메서드를 통해 전달하면, ListAdapter가 나머지를 처리합니다.
  • 데이터의 변경 사항이 자동으로 계산되어 효율적인 업데이트가 가능합니다.

DiffUtil이란?

DiffUtil은 두 리스트 간의 차이점을 계산하는 유틸리티 클래스로, 어떤 항목이 추가, 제거, 변경되었는지를 찾아낸다. 이를 위해 DiffUtil.Callback을 상속받은 클래스를 만들어서 두 리스트의 항목이 같은지(areItemsTheSame()), 내용이 같은지(areContentsTheSame())를 비교하는 로직을 구현해야 한다.


DiffUtil은 이러한 정보를 바탕으로 최소한의 업데이트로 RecyclerView를 갱신할 수 있는 'DiffResult'를 생성합니다. 이 결과는 RecyclerView의 어댑터에 적용되어, 효율적으로 UI를 업데이트할 수 있게 도와준다.

ListAdapter를 사용하면 이 모든 과정을 더 쉽게 처리할 수 있게 되어, 데이터가 변경될 때마다 RecyclerView를 더 효율적으로 업데이트할 수 있습니다.

 

ListAdapter 사용법

class ContactAdapter(var viewType: Int) :
    ListAdapter<ContactData, RecyclerView.ViewHolder>(diffUtil) {
    companion object {
        private val diffUtil = object : DiffUtil.ItemCallback<ContactData>() {
            override fun areItemsTheSame(oldItem: ContactData, newItem: ContactData): Boolean {
                // 연락처가 같은지 확인
                return oldItem == newItem
            }

            override fun areContentsTheSame(oldItem: ContactData, newItem: ContactData): Boolean {
                // 모든 필드가 같은지 확인 (data class의 equals 사용)
                return oldItem == newItem
            }
        }

        const val TYPE_LIST = 0
        const val TYPE_GRID = 1
    }

    override fun getItemViewType(position: Int): Int {
        return viewType
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        // ViewHolder 생성 로직
        return when (viewType) {
            TYPE_LIST -> ListViewHolder(
                ItemRecyclerViewListBinding.inflate(
                    LayoutInflater.from(
                        parent.context
                    ), parent, false
                )
            )
            TYPE_GRID -> GridViewHolder(
                ItemRecyclerViewGridBinding.inflate(
                    LayoutInflater.from(
                        parent.context
                    ), parent, false
                )
            )
            else -> throw IllegalArgumentException("Invalid view type")
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        // ViewHolder 바인딩 로직
        when (holder.itemViewType) {
            TYPE_LIST -> (holder as ListViewHolder).bind(getItem(position))
            TYPE_GRID -> (holder as GridViewHolder).bind(getItem(position))
        }
    }

	// ViewHolder 구현
    inner class ListViewHolder(private val binding: ItemRecyclerViewListBinding) :
        RecyclerView.ViewHolder(binding.root) {
        fun bind(contact: ContactData) {
            /* 아이템 바인딩 로직 */
        }
    }

    inner class GridViewHolder(private val binding: ItemRecyclerViewGridBinding) :
        RecyclerView.ViewHolder(binding.root) {
        fun bind(contact: ContactData) {
            /* 아이템 바인딩 로직 */
        }
}

 

위 코드는 연락처 앱의 어댑터를 구성한 코드이다. 2개의 뷰 타입을 볼 수 있게 하기 위해 ListViewHolder와 GridViewHolder를 생성한 점을 제외하면 일반 리스트어댑터의 구성과 동일하다.

 

ListAdapter를 상속받아서 그 안에 데이터와 뷰 홀더를 넣어준다. 기존에 RecyclerViewAdapter에서는 인자로 dataList를 받아왔지만 ListAdapter는 변경이 있을 때 submitList를 통해 리스트를 받아오기에 인자가 필요없다.

 

내부적으로 getItem 메소드를 통해서 ListAdapter의 currentList를 받아와 RecyclerViewAdapter의 mItems[position].Item 을 대체한다. 

 

어댑터 구현이 완료되면 어댑터를 세팅하고 어댑터에 데이터를 넣을 때는아래와 같다.

object ContactManager {
	val contactList: MutableList<ContactData> = mutableListOf()
    fun getList(): List<ContactData> {
            return contactList.toList()
        }
}

contactAdapter.submitList(ContactManager.getList())

 

 

중요한 점은 기존의 contactList는 MutableList이지만 호출할 때는 toList()로 변환하여 List형태로 submit한다는 점이다.

ListAdapter의 DiffUtil 이 받아오는 리스트의 주소값이 같으면 동작을 안하기 때문에 toList로 변환하여서 주소값을 변경시켜주어서 다른 리스트라는 것을 알려주어야 한다.

Android 데이터 저장 3가지 방법

1. DB 데이터 베이스에 데이터 저장하기 (ROOM, SQLite 등)

2. SharedPreferences를 이용해 데이터 저장하기

3. 파일 형태로 저장하기 (주로 FileWriterBufferedWriter 등을 사용)

 

여기선 SharedPreferences에 대해서 알아보자.

sharedPreferences란 무엇일까?

안드로이드 앱 개발을 할 때 데이터 저장이 필요한 순간은 반드시 온다.

많은 양의 데이터나 중요한 데이터들은 DB에 저장하는게 바람직하지만 보통 간단한 프로그램 설정 정보들은 sharedPreferences를 이용해서 데이터를 저장한다.

안드로이드에서 기본적으로 제공하는 클래스이기에 따로 gradle설정 필요x

 

한줄 요약 : 

프로그램의 설정 정보 (사용자의 옵션 선택 사항 이나 프로그램의 구성 정보)를 영구적으로 저장하는 용도로 사용

예) 알림 권한 설정 여부, 자동 로그인 설정 여부 등

 

sharedPreferences 특징

간단한 키-값 쌍의 데이터를 저장하기 위한 메커니즘을 제공

기본적으로 SharedPreferences는 내부적으로 XML 파일로 데이터를 저장

주로 앱의 구성 설정, 사용자 기본 설정, 애플리케이션 상태 등을 저장하는 데 사용

 

사용 방법

SharedPreferences에 데이터 저장하기

private fun saveData() {
    val pref = getSharedPreferences("pref", MODE_PRIVATE)
    val editor = pref.edit()  //수정 모드

    // 첫 번째는 key, 두 번째는 value
    editor.putString("name", binding.etText.text.toString())
    editor.apply() // 저장 완료
}

 

pref라는 변수에 SharedPreferences 객체 저장 pref는 파일 이름이며 뒤에 모드는 현재 이 파일은 앱 내에서만 접근 가능하도록 하는 모드다. ( + 모드에 대한 설명은 따로 밑에 적어두었어요.)

 

.edit() 은 SharedPreferences의 Editro 객체를 반환하는 메소드로, 파일을 변경할 수 있게 만들어주는 녀석이라고 생각하면 된다.

 

.put자료형을 통해서 첫 번째 인자에 키 값을 넣고 두 번째 인자에 실제 저장할 value값을 넣어주면 된다.

.apply()로 변경 사항을 저장한다. (저장하는 방법은 [apply, commit] 두 가지가 있다. 둘의 차이도 따로 밑에 정리해놨다.)

 

SharedPreferences에 있는 데이터 가져오기 

private fun loadData() {
    val pref = getSharedPreferences("pref", 0)

    // 첫 번째는 key, 두 번째는 데이터가 없을 경우 default value -> 저장하지 않은 경우도 생각해야 한다.
    binding.etText.setText(pref.getString("name", ""))
}

 

저장한 데이터를 세팅할 때는 마찬가지로 pref라는 변수에 해당 파일이 담긴 객체를 전달해준다.

뒤에 0은 MODE_PRIVATE을 의미하며 0으로 대체하여 사용가능하다. 

 

애플리케이션에서 SharedPreferences 데이터 저장 확인 방법

 

Device Explorer에 가서 data -> data -> [현재 프로젝트 이름] 을 찾는다.

 

프로젝트를 열어보면 shared_prefs 폴더가 있다. 그 안에 우리가 저장한 XML 파일을 찾아볼 수 있다.

 

pref.xml을 열어보면 저장한 데이터 map 형태로 잘 담겨있다.

 

SharedPreferences 모드 설명

 

SharedPreferencesmode 매개변수는 해당 SharedPreferences 파일의 액세스 권한 및 공유 범위를 정의합니다.

모드의 종류로는 다음과 같이 4가지가 있다.

MODE_PRIVATE
MODE_WORLD_READABLE
MODE_WORLD_WRITEABLE
MODE_MULTI_PROCESS

// 인터페이스 내용
@IntDef(flag = true, prefix = { "MODE_" }, value = {
            MODE_PRIVATE,
            MODE_WORLD_READABLE,
            MODE_WORLD_WRITEABLE,
            MODE_MULTI_PROCESS,
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface PreferencesMode {}

 

 

현재 MODE_PRIVATE 을 제외한 옵션은 Deprecated(사용이 권장되지 않음)되었다.

 

MODE_WORLD_READABLE 이 모드를 사용하면 다른 앱에서 해당 SharedPreferences 파일을 읽을 수 있다.

MODE_WORLD_WRITEABLE 이 모드를 사용하면 다른 앱에서 해당 SharedPreferences 파일을 수정할 수 있다.

위의 두가지 모드는 현재 보안 이슈로 인해 Deprecated되었고, API 레벨 23에서는 사용 중지되었습니다.

 

 

MODE_MULTI_PROCESS 다중 프로세스 간에 SharedPreferences 파일을 공유할 수 있도록 한다. API 레벨 23에서 Deprecated 되었고, API 레벨 30에서는 완전히 사용이 중지되었다. 이를 대체하기 위해, 다중 프로세스 간에 데이터를 공유해야 하는 경우에는 ContentProvider, BroadcastReceiver 및 일부 동기화 기술을 사용하는 것이 권장된다고 한다.

 

commit, apply 차이

commit 모드

  • commit 메소드는 동기적으로 동작하며, 변경 사항을 즉시 SharedPreferences에 기록한다.
  • 모든 변경 사항이 저장소에 성공적으로 쓰여질 때까지 commit 메소드는 블록된다. 따라서 호출자는 commit의 반환 값을 통해 변경 사항이 성공적으로 저장되었는지 여부를 확인할 수 있다.
boolean success = editor.commit()
if (success) {
    // 변경 사항이 성공적으로 저장됨
} else {
    // 변경 사항 저장 실패
}

 

apply 모드

  • apply 메소드는 비동기적으로 동작하며, 변경 사항을 즉시 메모리에 적용한 다음, 백그라운드에서 비동기적으로 디스크에 저장소에 커밋한다.
  • apply 메소드는 즉시 반환되므로 호출자는 변경 사항의 저장 성공 여부에 대한 피드백을 받을 수 없다. 따라서 호출자는 비동기적으로 변경 사항이 저장되는 것을 기다리거나 확인할 방법이 없다.
editor.apply()
// 변경 사항이 비동기적으로 저장되므로 피드백이 없음

 

일반적으로, 변경 사항이 성공 여부를 확인하고 결과에 따라 추가 작업이 필요한 경우에는 commit을 사용하고, 결과에 대한 피드백이 필요하지 않거나 비동기적으로 처리해도 무방한 경우에는 apply을 사용하는 것이 적절하다.

동기 프로그래밍 (sync)  vs 비동기 프로그래밍 (async)

비동기 프로그래밍에 대해서 알기 위해선 우선 동기 프로그래밍이 뭔지 먼저 알아야 한다.

 

동기적 프로그래밍은 "순서대로 하나의 작업씩 수행하는 행위"를 의미한다. 단순 반복하는 기계처럼

1 시작 1 끝

2 시작 2 끝

3 시작 3 끝 

................

이런 방식은 순차적으로 작업을 진행하기에 앞선 작업에 필연적으로 영향을 받을 수 밖에 없다.

즉, 1이 시작해서 안끝나면 그 뒤에 작업들은 1이 끝날 때까지 기다릴 수 밖에 없다.

 

이제 일상생활에서의 예를 가지고 이해해보자.

아침에 일어나서 따뜻한 우유와 빵을 같이 먹는다고 가정해보자.

 

그렇다면 해야할 일을 나열해보자.

 

침대정리

냉장고에서 우유꺼내기

머그컵에 우유 붓기

우유가 담긴 머그컵 전자레인지에 넣고 1분 돌리기

(따뜻해진 우유)전자레인지에서 꺼내기

식빵 자르기

잼 바르기

먹기

 

이 일련의 과정을 동기식으로 한다면 위와 똑같을 것이다.

다만, 비동기식으로 한다면 

 

침대정리

냉장고에서 우유꺼내기

머그컵에 우유 붓기

우유가 담긴 머그컵 전자레인지에 넣고 1분 돌리기

식빵 자르기

잼 바르기

(따뜻해진 우유)전자레인지에서 꺼내기

먹기

 

즉, 전자레인지에 요청을 보내놓고 끝날 때까지 기다렸다가 다음 일을 하느냐 아니면 다른 일을 하고 있다가 요청에 대한 결과가 나오면 이어서 일을 하느냐 차이다.

 

그렇다고 이런 동기적 프로그래밍 방식이 비효율적이라는 것은 아니다. 만약 A가 끝나야지만 B가 실행되어야하는

로직의 프로그램이 있다면 동기적으로 처리해야 바람직하게 작동할 것이다. 여기서는 우유를 꺼내오지도 않고 머그컵에 우유를 부으려는 행위가 비유에 맞을거 같다.

 

여기서 알 수 있듯이, 비동기 프로그래밍은 꼭 동기적으로 실행하지 않아도 되는 기능을 보완하기 위해 나온 것을 볼 수 있다. 우유가 다 따뜻해질때까지 굳이 앞에서 아무것도 안하고 기다릴 필요가 없는 것처럼,,

 

비동기 프로그래밍은 요청을 보내놓고 결과값을 받을 때까지 또 다른 일을 수행할 수 있게 즉, 멈추지 않고 계속 일을 할 수 있게끔 프로그래밍해주는 것이다.

Scope Function

자기 자신의 객체를 전달해서 효율적인 처리를 한다.

  Scope에서 접근방식 this Scope에서 접근방식 it
블록 수행 결과를 반환 run, with let
객체 자신을 반환 apply also

 

let function  

// let
var strNum = "10"

    var result = strNum?.let {
        // 중괄호 안에서는 it으로 활용함
        Integer.parseInt(it)
    }

 

 

run function & with function

+ run은 with와 달리 null 체크 가능

+ with는 반드시 null이 아닐 때만 사용 + this생략해서 사용 가능

    // run
    var student = Student("참새", 10)
    student?.run {
        displayInfo()
    }
    
    // with
    var alphabets = "abcd"
    with(alphabets) {
//    	var result = this.subSequence(0,2)
      	var result = subSequence(0,2)
      	println(result)
    }

 

also function & apply function

// also
    var student = Student("참새", 10)
    var result = student?.also {
        it.age = 50
    }
    result?.displayInfo()
    student.displayInfo()
    
// apply
    var student = Student("참새", 10)
    var result = student?.apply {
        student.age = 50
    }
    result?.displayInfo()
    student.displayInfo()

 

문자열을 숫자로 변환해주는 기능

var strNum1 = "10"         // 정수 문자열
var strNum2 = "10.21"      // 실수 문자열

var num1 = Integer.parseInt(strNum1)      //정수 문자열 "10"을 정수 10으로 변환
var num2 = strNum2.toDouble()             //실수 문자열 "10.21"을 실수 10.21로 변환

 

2개 이상의 값을 리턴해주는 기능

fun 메서드명(): Pair<타입, 타입> {     // 반환값 2개
	var 변수명 = Pair("감동란", "맥반석")
    return 변수명
}


fun 메서드명(): Triple<타입, 타입, 타입> {      // 반환값 3개
	var 변수명 = Triple("감동란", "맥반석", 25)
    return 변수
}

 

+ Recent posts