이것이 안드로이드다 with 코틀린(고돈호 지음) 으로 공부한 내용을 정리한 글입니다.
기기의 카메라 기능을 이용하려고 할 경우 카메라에 대한 권한과 카메라로 촬영한 사진에 대한 접근 권한을 얻어야 합니다.
// AndroidManifest.xml
<uses-permission android:name="android.permission.CAMERA"/>
// kotlin
if(ContextCompat.checkSelfPermission(this, permission) !=
PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, permissions, flag)
return false
}
// ...
override fun onRequestPermissionsResult(requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray) {
// ...
}
카메라 앱을 호출하는 것은 카메라 액티비티를 실행하는 것과 같으므로 Intent
를 통해 카메라 앱을 호출합니다.
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
startActivityForResult(intent, FLAG)
카메라로 촬영한 사진 정보는 onActivityReult
메서드로 전달됩니다.
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(reqeustCode, resultCode, data)
if(resultCode == Activity.RESULT_OK) {
when (requestCode) {
FLAG -> {
if(data?.extras?.get("data") != null) {
val bitmap = data?.extras?.get("data") as Bitmap
imagePreview.setImageBitmmap(bitmap)
}
}
}
}
}
외부 저장소에 촬영한 사진을 저장하려면 MediaStore
를 이용해야 합니다.
MediaStore
안드로에드에서 외부 저장소를 관리하는 데이터베이스
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
fun saveImageFile(filename: String, mimeType: String, bitmap: Bitmap) : Uri? {
var values = ContentValues()
values.put(MediaStore.Images.Media.DISPLAY_NAME, filename)
values.put(MediaStore.Images.MIME_TYPE, mimeType)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// 파일 저장을 완료하기 전까지 다른 곳에서 해당 데이터를 요청하는 것을 무시
values.put(MediaStore.Images.Media.IS_PENDING, 1)
}
// MediaStore에 파일 등록
val uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
try {
if (uri != null) {
// 파일 디스크립터 획득
var descriptor = contentResolver.openFileDescriptor(uri, "w")
if (descriptor != null) {
// FileOutputStream으로 비트맵 파일 저장. 숫자는 압축률
val fos = FileOutputStream(descriptor.fileDescriptor)
bimap.compress(Bitmap.CompressFormat.JPEG, 100, fos)
fos.close()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// 데이터 요청 무시 해제
values.clear()
values.put(MediaStore.Images.Media.IS_PENDING, 0)
contentResolver.update(uri, values, null, null)
}
}
}
} catch (e:java.lang.Exception) {
Log.e("File", "error=${e.locatizedMessage}")
}
return uri
}
URI
통합 자원 식별자는 특정 리소스 자원을 고유하게 실벽할 수 잇는 식별자를 의미
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
카메라 앱을 호출할 때와 마찬가지로 Intent
를 통해 갤러리 앱을 호출합니다.
val intent = Intent(Intent.ACTION_PICK)
intent.type = MediaStore.Images.Media.CONTENT_TYPE
startActivtyForResult(intent, FLAG_REQ_STORAGE)
갤러리 앱에서 선택한 이미지에 대한 정보가 onActivityResult
메서드로 전달됩니다.
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
//...
when (reqeustCode) {
FLAG -> {
val uri = data?.data
imagePreview.setImageURI(uri)
}
}
}
안드로이드의 쓰레드는 크게 1개의 메인 쓰레드와 여러개의 워커 쓰레드로 나눌 수 있습니다.
기본적으로 모든 컴포넌트는 단일 프로세스 및 메인 쓰레드에서 실행됩니다. 메인 쓰레드는 다음과 같은 특징을 같습니다.
안드로이드 시스템은 UI 이벤트 및 작업에 대해 수 초 내에 응답하지 않으면 앱을 종료시키고 ANR 팝업창을 띄웁니다. 따라서 시간이 오래 걸리는 작업은 워커 쓰레드를 생성해서 처리해야 합니다.
ANR: Application Not Responding
네트워크 작업, 시간이 오래 걸리는 계산, 파일 업로드와 다운로드, 이미치 처리, 데이터 로딩 등은 처리 시간을 미리 계산할 수 없습니다. 이러한 작업들은 워커 쓰레드를 통해 백그라운드에서 처리하는 것을 권장합니다. 따라서 워커 쓰레드는 백그라운드 쓰레드라고도 불립니다.
Thread
클래스를 상속받아 쓰레드를 생성할 수 있습니다.
class WorkerThread : Thread() {
override fun run() {
// 워커 쓰레드에서 수행할 작업 정의
}
}
// ...
override fun onCreate(savedInstanceState: Bundle?) {
// ...
var thread = WorkerThread()
thread.start() // 쓰레드 실행
}
Runnable
인터페이스를 구현해 쓰레드를 생성할 수 있습니다.
class WorkerRunnable: Runnable {
override fun run() {
// 워커 쓰레드에서 수행할 작업 정의
}
}
Runnable
인터페이스를 구현한 객체는 Thread
클래스의 생성자로 전달하고 Thread
클래스의 start()
메서드를 호출해야 쓰레드가 생성됩니다.
override fun onCreate(savedInstanceState: Bundle?) {
// ...
var thread = Thread(WorkerRunnable())
thread.start() // 쓰레드 실행
}
Runnable
인터페이스는 자바나 코틀린에서 다중 상속을 허용하지 않기 때문에 이미 다른 클래스를 상속받은 클래스도 쓰레드를 구현할 수 있도록 하기 위해 사용됩니다.
코틀린에서 인터페이스 내부에 메서드가 하나만 있는 경우 람다식으로 변환이 가능합니다.
Thread {
// run() 메소드에 정의했던 작업 작성
}.start()
람다식
익명함수를 생성하기 위한 식으로 객체 지향적이기 보다 함수 지향적임
코틀린에서는 다음과 같이 백그라운드를 사용할 수 있습니다.
thread(start=true) {
// 백그라운드 작업 정의
}
UI는 메인 쓰레드에서만 접근할 수 있으며 백그라운드 쓰레드에서 UI에 접근하는 경우 예외가 발생하고 앱이 종료됩니다. 따라서 백그라운드에서 UI에 접근해야 하는 경우 UI 작업을 메인 쓰레드에 의뢰해야 하는데, 이때 사용하는 것이 핸들러와 루퍼입니다.
루퍼는 지속적으로 루프를 돌아 Message Queue에 메시지가 들어오는 것을 감지하고 메시지를 꺼내서 해당 메시지와 연결된 핸들러를 호출합니다.
메인 쓰레드는 기본적으로 루퍼를 가지고 있으나 워커 쓰레드는 루퍼를 생성해야 합니다. 루퍼는 하나의 쓰레드만 담당할 수 있고 하나의 쓰레드도 오직 하나의 루퍼만 가질 수 있습니다.
핸들러는 루퍼와 연결된 메시지 큐로 메시지나 Runnable 객체를 보내고 메시지 큐에서 꺼내진 메시지를 처리합니다. 핸들러는 생성과 동시에 해당 핸들러를 생성한 쓰레드에 연결되어 그 쓰레드의 루퍼 및 Message Queue에 대한 참조를 갖게 됩니다. 쓰레드 하나에 여러 개의 핸들러를 생성할 수 있습니다.
handleMessage
메소드 override
obtainMessage
메소드로 메시지를 생성sendMessage
메소드를 통해 유저 쓰레드에서 메인 쓰레드의 Message Queue로 메시지 전달AsyncTask 클래스는 비동기 처리를 할 수 있도록 쓰레드와 핸들러 기능을 하나의 클래스에 합쳐놓은 것입니다.
AsyncTask
클래스 내부에 3가지 인터페이스를 구현하는 것만으로 쓰레드와 핸들러 생성을 대체할 수 있습니다.
doInBackground()
블럭에서 publishProgress()
가 호출될 때마다 실행되는 메서드로 메인 쓰레드에서 동작하고 파일 다운로드 시에 현재 진행률을 보여주는 형태로 많이 사용됨doInBackground()
가 완료된 후에 호출되는 메서드로 메인 쓰레드에서 동작합니다. 따라서 작업의 결과를 UI에 업데이트할 수 있습니다.val asyncTask = object : AsyncTask<Void, Int, String>() {
override fun doInBackground(vararg params: Void?): String {
}
override fun onProgressUpdate(vararg values: Int?) {
}
override fun onPostExecute(result: String?) {
}
}
asyncTask.execute()
가변인자 vararg
파라미터의 개수가 가변적일 경우vararg
로 선언함. 파라미터는temp[0]
과 같이 배열처럼 사용
AsyncTask
는 다시 실행할 수 없고 새로운 AsyncTask
를 생성해 실행해야 함