putBoolean() : boolean 유형의 값을 저장한다.putInt() : int 유형의 값을 저장한다..=putString() : 문자열 유형의 값을 저장한다.getBoolean() : boolean 유형의 값을 검색한다.getInt() : int 유형의 값을 검색한다.getString() : 문자열 유형의 값을 검색한다.getSharedPreferences(name, mode)//getString을 통해 SharedPreferences의 고유 식별자를 가져와서 SharedPreferences파일에 연결
val sharedPref = activity?.getSharedPreferences(
getString(R.string.preference_file_key), Context.MODE_PRIVATE)
//SharedPreferences파일명을 직접 적어서 연결
val pref = getSharedPreferences("pref",0)
//MODE_PRIVATE 와 0은 동일한 의미이다.MODE_PRIVATE (=0)를 사용한다.MODE_PRIVATE : 생성된 XML 파일은 호출한 애플리케이션 내에서만 읽기 쓰기가 가능getPreferences(mode)val sharedPref = activity?.getPreferences(Context.MODE_PRIVATE)Device File Explorer에 들어가면, data > data > 내가만든프로젝트패키지파일 > shared_pregs 안에 xml 파일이 만들어진 것을 확인 할 수 있다.build.gradle 파일에 plugins와 dependencies를 추가해줘야한다.plugins {
...
id 'kotlin-kapt'
...
}
...
dependencies {
...
//룸은 최신버전을 쓰는 것이 좋다. 검색하여 최신버전을 적용해주면 된다.
def room_version = "2.5.1"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
kapt "androidx.room:room-compiler:$room_version"
// optional - Kotlin Extensions and Coroutines support for Room
implementation "androidx.room:room-ktx:$room_version"
// optional - Test helpers
testImplementation "androidx.room:room-testing:$room_version"
}
//Androidx 사용하는 경우를 가정함, Android Studio와 SDK는 최신 버전으로 사용 @Entity : 클래스를 테이블 스키마로 지정하는 annotation@Entity(tableName = "student_table") // 테이블 이름을 student_table로 지정함
data class Student (
@PrimaryKey
@ColumnInfo(name = "student_id")
val id: Int,
val name: String
)@PrimaryKey : 기본키(고유식별값)로 지정해주는 annotation@ColumnInfo : 해당 필드가 데이터베이스 테이블의 열(Column)에 매핑될 때 사용되는 annotation@ForeignKey : 엔티티간의 외래 키 관계를 정의할 때 사용한다. (참조 무결성 유지에 도움)DAO는 interface나 abstract class로 정의되어야 한다.
Annotation에 SQL 쿼리를 정의하고 그 쿼리를 위한 메소드를 선언한다.
@Transaction : 메소드가 하나의 트랜잭션으로 실행되어야 한다는 뜻
+여러 연산을 하나의 작업으로 묶어서 실행할 때 사용한다.
@Index : 특정 컬럼에 인덱스를 생성할 때 사용한다. (쿼리 성능 향상에 유용)
DAO 쿼리와 관련된 주요 Annotation 종류 : @Insert, @Update, @Delete, @Query
@Dao
interface MyDAO {
@Insert(onConflict = OnConflictStrategy.REPLACE)
// INSERT, key 충돌이 나면 새 데이터로 교체
//+`OnConflictStrategy.ABORT` : key 충돌시 종료한다.
//+`OnConflictStrategy.IGNORE` : key 충돌 무시한다.
//+`OnConflictStrategy.REPLACE1` : key 충돌시 새로운 데이터로 변경한다.
suspend fun insertStudent(student: Student)
@Query("SELECT * FROM student_table")
fun getAllStudents(): LiveData<List<Student>> // LiveData<> 사용
@Query("SELECT * FROM student_table WHERE name = :sname") //인자 sname을 SQL에서 :sname으로 사용
suspend fun getStudentByName(sname: String): List<Student>
@Delete
suspend fun deleteStudent(student: Student); // primary key is used to find the student
...
}
@Insert(입력), @Update(변경/수정), @Delete(삭제)는 SQL 쿼리를 작성하지 않아도 컴파일러가 자동으로 생성한다.@Insert나@Update는 key가 중복되는 경우 처리를 위해 onConflict를 지정할 수 있다.@Query로 리턴되는 데이터의 타입을 LiveData<>로 하면 Observer를 통해 데이터가 업데이트 될 때 알 수 있다. (LiveData는 비동기적 동작으로 coroutine일 필요가 없다.)@Query에 SQL을 정의할 때 메소드의 인자를 사용할 수 있다.suspend는 Kotlin coroutine을 사용하는 것으로, 나중에 이 메소드를 부를 때는 runBlocking {} 내에서 호출해야 한다.RoomDatabase를 상속해서 만들 수 있다.@Database(entities = [Student::class, ClassInfo::class, Enrollment::class, Teacher::class], version = 1)
abstract class MyDatabase : RoomDatabase() {
//DAO를 가져오는 getter메서드 (메소드 정의는 자동으로 생성됨)
abstract fun getMyDao() : MyDAO
companion object {
//인스턴스는 하나만 있으면 되므로 Singleton 패턴을 사용
private var INSTANCE: MyDatabase? = null
//버전 관리를 위한 Migration
//여러개의 Migration 지정 가능
private val MIGRATION_1_2 = object : Migration(1, 2) { // version 1 -> 2
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE student_table ADD COLUMN last_update INTEGER")
}
}
private val MIGRATION_2_3 = object : Migration(2, 3) { // version 2 -> 3
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE class_table ADD COLUMN last_update INTEGER")
}
}
//인스턴스는 하나만 있으면 되므로 Singleton 패턴을 사용
fun getDatabase(context: Context) : MyDatabase {
if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(
context, MyDatabase::class.java, "school_database")
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
.build()
}
return INSTANCE as MyDatabase
}
}
}@Database annotation에 지정한다.Migration을 통한 데이터베이스의 버전 관리가 중요하다.addMigration() 메소드를 통해 migration을 수행하게 된다.Room.databaseBuilder()를 이용한다.(원래는 안드로이드 아키텍처에 따라 ViewModel사용이 권장되지만, 여기에서는 다루지 않는다.)
-> 뷰를 사용한 Android Room - Kotlin 참조
**RoomDatabase객체에서 DAO 객체를 받아오고, 이 DAO객체의 메소드를 호출하여 데이터베이스를 접근한다.
class MainActivity : AppCompatActivity() {
private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
lateinit var myDao: MyDAO
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
//**RoomDatabase객체에서 DAO 객체를 받아오고,
myDao = MyDatabase.getDatabase(this).getMyDao()
// (주의) UI를 블록할 수 있는 DAO 메소드를 UI 스레드에서 바로 호출하면 안됨
runBlocking {
myDao.insertStudent(Student(1, "james")) // suspend 지정되어 있음
}
//**이 DAO객체의 메소드를 호출하여 데이터베이스를 접근한다.
val allStudents = myDao.getAllStudents()
// Observer::onChanged() 는 단일 추상 메소드를 가진 인터페이스(SAM)이기 때문에 lambda로 대체
allStudents.observe(this) {
val str = StringBuilder().apply {
for ((id, name) in it) {
append(id)
append("-")
append(name)
append("\n")
}
}.toString()
binding.textStudentList.text = str
}
}
}
LiveData : 관찰 가능한 데이터 홀더 클래스
+UI 컴포넌트(예: 액티비티, 프래그먼트)는 데이터의 변경 사항을 관찰하고, 데이터가 변경될 때마다 LiveData는 관찰자에게 알림을 보내는 등 반응할 수 있다.
LiveData의 핵심 특징
LiveData<>를 리턴하는 DAO 메소드는 observe() 메소드를 이용하여 Observer를 지정한다. 데이터가 변경될 때마다 자동으로 Observer의 onChanged()가 호출된다.
+Observer를 통해 비동기적으로 데이터를 받기 때문에, UI 스레드에서 직접 호출해도 문제 없음
액티비티의 메인 UI 쓰레드를 직접 건드릴(바인딩 할) 때에는 withContext()를 사용하지 않으면 오류가 난다.
메인액티비티에서 데이터 인스턴스를 생성하고 -> DAO를 통해서 쿼리를 수행 -> 쿼리를 통해서 결과를 받을 수 있다.
App Inspection를 클릭하면 입력한 데이터를 확인할 수 있는 창이 나온다.1. 안드로이드에서 지도를 사용하기 위해서는 위치 접근 권한이 필요하다.
:AndroidManifest.xml 파일에 위치 권한을 추가하고, 런타임 시 사용자에게 권한을 요청해야한다.
//권한 요청 예제 코드
class MainActivity : AppCompatActivity() {
companion object {
private const val PERMISSION_REQUEST_ACCESS_FINE_LOCATION = 100
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
requestLocationPermission()
}
//앱에 위치 권한이 있는지 확인
private fun requestLocationPermission() {
if (ContextCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_FINE_LOCATION
) != PackageManager.PERMISSION_GRANTED
) {
// 권한이 없을 경우, 사용자에게 요청
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
PERMISSION_REQUEST_ACCESS_FINE_LOCATION
)
} else {
// 권한이 이미 있을 경우, 위치 정보를 사용할 수 있음
getLocation()
}
}
//결과를 처리하는 콜백 메소드
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
when (requestCode) {
PERMISSION_REQUEST_ACCESS_FINE_LOCATION -> {
if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
// 권한이 부여되면 위치 정보를 사용할 수 있음
getLocation()
} else {
// 권한이 거부되면, 기능 사용 불가
}
return
}
}
}
private fun getLocation
2. 사용자 위치를 추적하기 위한 3가지 권한 : 위치 정보를 얻을 땐 permission 체크가 필수이다.
android.permission.ACCESS_COARSE_LOCATION : 와이파이나 모바일 데이터(또는 둘 다)를 사용해 기기의 위치에 접근하는 권한으로, 도시에서 1블록 정도의 오차 수준이다.android.permission.ACCESS_FINE_LOCATION : 위성, 와이파이, 모바일 데이터 등 이용할 수 있는 위치 제공자를 사용해 최대한 정확한 위치에 접근하는 권한이다.android.permission.ACCESS_BACKGROUND_LOCATION : 안드로이드 10(API 레벨 29) 이상에서 백그라운드 상태에서 위치에 접근하는 권한이다.3. 위치 매니저를 통해 사용자의 위치를 얻을 수 있다.
위치 제공자는 GPS(위성),Network,(이동통신망),Wifi(와이파이),Passive(다른앱의 마지막위치) 중에 지정할 수 있다.
LocationManager 시스템 서비스를 이용
val manager = getSystemService(LOCATION_SERVICE) as LocationManager
//현재 위치에 어떤 위치 제공자가 있는지 알고 싶을 때 allProviders 사용
var result = "All Providers : "
val providers = manager.allProviders
for (provider in providers) {
result += " $provider. "
}
Log.d("maptest", result) // All Providers : passive, gps, network..
//지금 사용할 수 있는 위치 제공자를 알아보려면 getProviders()사용
result = "Enabled Providers : "
val enabledProviders = manager.getProviders(true)
for (provider in enabledProviders) {
result += " $provider. "
}
Log.d("maptest", result) // Enabled Providers : passive, gps, network..
단발성 위치 정보를 얻을 때 : LocationManager의 getLastKnownLocation() 함수를 이용
if (ContextCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
) {
val location: Location? = manager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
location?.let{
val latitude = location.latitude //위도
val longitude = location.longitude //경도
val accuracy = location.accuracy //정확도
val time = location.time //획득시간
Log.d("map_test", "$latitude, $location, $accuracy, $time")
}
}
연속성 위치 정보를 얻을 때 : LocationListener를 이용
val listener: LocationListener = object : LocationListener {
//onLocationChanged(): 새로운 위치를 가져오면 호출된다.
override fun onLocationChanged(location: Location) {
Log.d("map_test,","${location.latitude}, ${location.longitude}, ${location.accuracy}")
}
//onProviderEnabled(): 위치 제공자가 이용할 수 있는 상황이면 호출된다.
//onProviderDisabled(): 위치 제공자가 이용할 수 없는 상황이면 호출된다.
//위치 업데이트 요청
manager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 10_000L, 10f, listener)
//위치 업데이트 종료시 removeUpdates를 사용하여 리스너를 제거해야한다.
manager.removeUpdates(listener)
}
위치 제공자를 지정할 때에는 고려해야할 사항들이 있는데, 전력소비/정확도/API의 간편셩/부가기능 제공여부/기기 지원여부 등 여러 조건을 따졌을 때, 구글에서 제공하는 라이브러리를 사용하는 것이 유리하다.
구글 play 서비스를 사용하기 위해서는 build.gradle파일에 implementation 'com.google.android.gms:play-services:12.0.1'를 추가해줘야한다.
추가하면, Fused Location Provider라이브러리를 사용할 수 있다.
[Fused Location Provider에서 핵심 클래스]
val connectionCallback = object: GoogleApiClient.ConnectionCallbacks{
//위치 제공자를 사용할 수 있는 상황일 때
override fun onConnected(p0: Bundle?) {
// 위치 획득
if(ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) === PackageManager.PERMISSION_GRANTED){
//FusedLocationProviderClient의 getLastLocation() 함수 호출
providerClient.lastLocation.addOnSuccessListener(
this@MainActivity,
object: OnSuccessListener<Location> {
override fun onSuccess(p0: Location?) {
p0?.let {
val latitude = p0.latitude
val longitude = p0.longitude
Log.d("map_test", "$latitude, $longitude")
}
}
}
)
apiClient.disconnect()
}
}
override fun onConnectionSuspended(p0: Int) {
// 위치 제공자를 사용할 수 없을 때
}
}
val onConnectionFailCallback = object : GoogleApiClient.OnConnectionFailedListener{
override fun onConnectionFailed(p0: ConnectionResult) {
// 사용할 수 있는 위치 제공자가 없을 때
}
}
val apiClient = GoogleApiClient.Builder(this)
.addApi(LocationServices.API)
.addConnectionCallbacks(connectionCallback)
.addOnConnectionFailedListener(onConnectionFailCallback)
.build()
//FusedLocationProviderClient 초기화
val providerClient = LocationServices.getFusedLocationProviderClient(this)
//GoogleApiClient 객체에 위치 제공자를 요청
apiClient.connect()
FusedLocationProviderClient: 위치 정보를 얻는다.GoogleApiClient : 위치 제공자 준비 등 다양한 콜백을 제공한다.GoogleApiClient.ConnectionCallbacks와 GoogleApiClient.OnConnection FailedListener 인터페이스를 구현한 객체를 지정