[TIL] Android 앱 개발 심화 : Shared Preferences, Room(DataBase), Map

지혜·2024년 1월 23일

Android_TIL

목록 보기
44/70

✏240123 화요일 TIL(Today I learned) 오늘 배운 것

📖Shared Preferences

[Preference]

  • 데이터를 핸드폰 기기 내에 영구적으로 저장하는 용도로 사용한다.
  • 사용자의 옵션 선택(알림음, 설정 온오프 등) 사항이나, 프로그램의 구성정보를 저장하는데에 많이 쓰인다.
  • 키-값 세트로 xml 포맷 텍스트파일에 정보를 저장한다.

[SharedPreferences 클래스]

  • Preference의 데이터(키-값)를 관리하는 클래스
  • 응용 프로그램 내의 액티비티간에 공유되며, 한 액티비티에서 수정되더라도 다른 액티비티에서 수정된 값을 읽을 수 있다.
  • 서버 등을 통하지 않고 기기 내에서 데이터의 입력과 저장이 이루어진다. 응용 프로그램의 고유한 정보이므로 외부에서 읽을 수 없다.
  • [사용 가능한 데이터 타입]
    • 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은 동일한 의미이다.
    • 여러 개의 Shared Preferences파일을 사용하는 경우에 사용한다. 기본적으로 여러개의 파일을 사용하는게 일반적이기 때문에 대부분 getSharedPreferences를 사용한다.
    • name : Preference 데이터를 저장할 xml 파일의 이름
    • mode : 파일의 공유 모드 MODE_PRIVATE (=0)를 사용한다.
      +MODE_PRIVATE : 생성된 XML 파일은 호출한 애플리케이션 내에서만 읽기 쓰기가 가능
      +이외의 모드들은 보안상의 이유로 API level 17에서 deprecated됨
  • getPreferences(mode)
    val sharedPref = activity?.getPreferences(Context.MODE_PRIVATE)
    • 한 개의 Shared Preference 파일을 사용하는 경우에 사용한다. 보통 한 개만 사용할 일은 거의 없으므로 잘 사용되지 않는다.
    • 생성한 액티비티의 전용이라 같은 패키지의 다른 액티비티이더라도 읽을 수 없다.
    • xml파일의 이름은 액티비티의 이름과 동일하게 생성된다.

[저장한 데이터 안드로이드스튜디오에서 확인하기]

  • 에뮬레이터의 Device File Explorer에 들어가면, data > data > 내가만든프로젝트패키지파일 > shared_pregs 안에 xml 파일이 만들어진 것을 확인 할 수 있다.
  • 파일에 들어가보면 xml파일 형태로, 키 값인 name과 그 안에 실제값을 확인 할 수 있다.


📖Room (DataBase)

  • SQLite를 쉽게 사용할 수 있는 데이터베이스 객체 매핑 라이브러리. SQLite 보다 Room을 사용할 것을 권장한다.
  • 쉽게 Query를 사용할 수 있는 API를 제공하고, Query를 컴파일 시간에 검증한다.
  • Query결과를 LiveData로하여 데이터베이스가 변경될 때 마다 쉽게 UI를 변경할 수 있다.

[gradle 파일 설정]

  • Room을 사용하기 위해서는 build.gradle 파일에 pluginsdependencies를 추가해줘야한다.
    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는 최신 버전으로 사용

[Room의 주요 3요소 + UI 연결]

1. Entity 생성

  • @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 : 엔티티간의 외래 키 관계를 정의할 때 사용한다. (참조 무결성 유지에 도움)

2. DAO 생성

  • 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을 정의할 때 메소드의 인자를 사용할 수 있다.
    • (주의) fun 앞에 suspend는 Kotlin coroutine을 사용하는 것으로, 나중에 이 메소드를 부를 때는 runBlocking {} 내에서 호출해야 한다.

3. Database 생성

  • 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
           }
       }
    }
  • 포함되는 Entity들과 데이터베이스 버전(version)을 @Database annotation에 지정한다.
  • Migration을 통한 데이터베이스의 버전 관리가 중요하다.
    : version이 기존에 저장되어 있는 데이터베이스보다 높으면, 데이터베이스를 open할 때 RoomDatabase 객체의 addMigration() 메소드를 통해 migration을 수행하게 된다.
  • Room 클래스의 객체 생성은 Room.databaseBuilder()를 이용한다.

4. 매인액티비티에서 UI와 연결하기

(원래는 안드로이드 아키텍처에 따라 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의 핵심 특징

    • 안드로이드의 수명주기를 인식한다. 이에 따라 알림도 자동으로 관리하며 충돌을 방지한다.
    • UI와 데이터의 상태를 자동으로 갱신하기 때문에 최신의 정보를 일관성있게 유지한다.
    • 뷰모델과 함께 사용하면 데이터를 중앙 집중적(효율적)으로 관리할 수 있게 된다.
  • LiveData<>를 리턴하는 DAO 메소드는 observe() 메소드를 이용하여 Observer를 지정한다. 데이터가 변경될 때마다 자동으로 Observer의 onChanged()가 호출된다.
    +Observer를 통해 비동기적으로 데이터를 받기 때문에, UI 스레드에서 직접 호출해도 문제 없음

  • 액티비티의 메인 UI 쓰레드를 직접 건드릴(바인딩 할) 때에는 withContext()를 사용하지 않으면 오류가 난다.

  • 메인액티비티에서 데이터 인스턴스를 생성하고 -> DAO를 통해서 쿼리를 수행 -> 쿼리를 통해서 결과를 받을 수 있다.

[저장한 데이터 안드로이드스튜디오에서 확인하기]

  • Run, Terminal 등의 메뉴와 함께 있는 App Inspection를 클릭하면 입력한 데이터를 확인할 수 있는 창이 나온다.


📖Map : 사용자 위치 얻기

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..
  • 단발성 위치 정보를 얻을 때 : LocationManagergetLastKnownLocation() 함수를 이용

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

[구글 Play 서비스의 위치 라이브러리로 사용자 위치 가져오기]

  • 위치 제공자를 지정할 때에는 고려해야할 사항들이 있는데, 전력소비/정확도/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.ConnectionCallbacksGoogleApiClient.OnConnection FailedListener 인터페이스를 구현한 객체를 지정

✏새롭게 알게된 것과 내일 할 일

  • 오늘은 어플의 완성도를 높이는 데이터 설정들과 위치정보 얻는 법을 배웠다.
  • 늘 어플리케이션이 꺼지면 새로 추가한 데이터들도 날라가서 아쉬웠는데, 그런 것을 보완할 수 있는 기술이라서 흥미있게 배운 것 같다. Room도 다행이 쿼리문을 보는게 처음이 아니여서 낯설지 않았다. 다만 코틀린에서 실사용할 때 적응하기 위해 노력해야 할 것 같다.
  • 위치정보는 사실, 실제로 구글지도API를 따오는 법까지 들었는데, 이건 내일 실습과제를 통해 레트로핏과 함께 직접 적용해보면서 정리하는 것이 더 좋을 것 같다.

  • 그래서 내일은, 실습강의를 들으면서 주어진 심화주차 강의를 마무리 할 것이다. 목요일에는 수준별 수업을 들어야하기 때문에 강의를 완강하는 것이 더 유리할 것 같다. 수준별 수업에서 어떤 과제가 나올지는 모르겠지만, 큰 문제가 없다면 수준별 수업을 듣고 바로 개인과제를 진행할 수 있었으면 좋겠다. 이번 과제는 생짜로 해야하니까..ㅎㅎ 쿼리문 빼고는 낯선것도 많고.. 그래서 시간을 좀 충분히 들여서 하고 싶다. 과제 수행 시간이 뭔가 2주나 주는 것 같지만 실상은 강의듣고 어쩌고 하면 꼴랑 일주일 나오나 안나오나 싶다.. 이번엔 도전과제까지는 별 생각도 안들고, 그냥 필수과제랑 수준별과제를 수행하는데 집중해봐야겠다. 심화주차도 파이팅!
profile
파이팅!

0개의 댓글