// 안드로이드 스튜디오 4.0 이상일 때
android {
...
buildFeatures {
viewBinding = true
}
}
// 안드로이드 스튜디오 3.6 ~ 4.0. 3.6보다 낮으면 안됨
android {
...
viewBinding {
enabled true
}
}





📚 루트 뷰란?
- 뷰들은 계층 구조를 가지고 레이아웃을 구성
👉 루트 뷰: 모든 뷰들을 포함하는 뷰
class MainActivity : AppCompatActivity() {
private lateinit var binding : ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 바인딩 클래스에 포함된 inflate() 함수를 실행해 바인딩 클래스의 객체 생성
binding = ActivityMainBinding.inflate(layoutInflater)
val view = binding.root // 바인딩 객체의 root 뷰 참조
setContentView(view) // 생성한 뷰 설정
}
}
dependencies {
// CameraX core library using the camera2 implementation
val camerax_version = "1.3.0-alpha04"
// The following line is optional, as the core library is included indirectly by camera-camera2
implementation("androidx.camera:camera-core:${camerax_version}")
implementation("androidx.camera:camera-camera2:${camerax_version}")
// If you want to additionally use the CameraX Lifecycle library
implementation("androidx.camera:camera-lifecycle:${camerax_version}")
// If you want to additionally use the CameraX View class
implementation("androidx.camera:camera-view:${camerax_version}")
// If you want to additionally add CameraX ML Kit Vision Integration
implementation("com.goole.mlkit:barcode-scanning:${camerax_version}")
}
// 변수 선언
private lateinit var cameraProvideFuture : ListenableFuture<ProcessCameraProvider>
fun startCamera() {
// cameraProvideFuture에 객체의 참조값 할당
cameraProvideFuture = ProcessCameraProvider.getInstance(this)
// cameraProvideFuture 태스크가 끝나면 실행
cameraProvideFuture.addListener(Runnable {
// ProcessCameraProvider 객체 가져오기
// ProcessCameraProvider: 카메라의 생명 주기를 액티비티 생명 주기에 바인드 해줌
val cameraProvider = cameraProvideFuture.get()
// 미리보기 객체 가져오기
val preview = getPreview()
// DEFAULT_BACK_CAMERA(후면 카메라) 선택
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
// preview(미리보기) 쓰기 선택
cameraProvider.bindToLifecycle(this, cameraSelector, preview)
}, ContextCompat.getMainExecutor(this))
}
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
DEFAULT_BACK_CAMERA: 후면 카메라
DEFAULT_FRONT_CAMERA: 전면 카메라
cameraProvider.bindToLifecycle(this, cameraSelector, preview)
미리보기, 이미지 분석, 이미지 캡쳐 중 선택
<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />
class MainActivity : AppCompatActivity() {
// 태그 기능 코드 => 나중에 권한을 요청한 후 결과를 onRequestPermissionsResult에서 받을 떄 필요
// 0과 같거나 큰 양수이기만 하면 어떤 수든 상관없음
private val PERMISSIONS_REQUEST_CODE = 1
// 카메라 권한 지정
private val PERMISSIONS_REQUIRED = arrayOf(android.Manifest.permission.CAMERA)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
if (!hasPermissions(this)) { // 권한이 없을 때
// 카메라 권한 요청
requestPermissions(PERMISSIONS_REQUIRED, PERMISSIONS_REQUEST_CODE)
} else {
// 이미 권한 있을 때
startCamera()
}
}
// all => PERMISSIONS_REQUIRED 배열의 원소가 모두 조건문을 만족하면 true, 아니면 false 반환
fun hasPermissions(context: Context) = PERMISSIONS_REQUIRED.all {
ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
}
// 권한 요청 콜백 함수
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
// onCreate() 메서드에서 requestPermissions의 인수로 넣은 PERMISSIONS_REQUEST_CODE와 맞는지 확인
if (requestCode == PERMISSIONS_REQUEST_CODE) { // 권한 수락 시
if (PackageManager.PERMISSION_GRANTED == grantResults.firstOrNull()){
Toast.makeText(this@MainActivity, "권한 요청이 승인되었습니다.", Toast.LENGTH_LONG).show()
startCamera()
} else { // 권한 거부 시
Toast.makeText(this@MainActivity, "권한 요청이 거부되었습니다.", Toast.LENGTH_LONG).show()
finish()
}
}
}
}
실행 결과

import android.annotation.SuppressLint
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import com.google.mlkit.vision.barcode.BarcodeScanning
import com.google.mlkit.vision.common.InputImage
class QRCodeAnalyzer : ImageAnalysis.Analyzer{
// 바코드 스캐닝 객체 생성
private val scanner = BarcodeScanning.getClient()
@SuppressLint("UnsafeOptInUsageError")
override fun analyze(imageProxy: ImageProxy) {
val mediaImage = imageProxy.image
if (mediaImage != null) {
// 이미지가 찍힐 단시 카메라의 회전 각도를 고려해 입력 이미지 생성
val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
// scanner.process(image) 통해 이미지 분석
// SuccessListener, FailureListener, CompleteListener를 각각 달아주어 결과 확인 가능
scanner.process(image).addOnCanceledListener {
}
}
}
}
🛠️ Error Fix
BarcodeScanning.getClient() 작성 시 빨간줄 에러가 뜬다면
Alt + Enter를 누르다보면 앱모듈 수준 build.gradle에 자동으로 필요한 mlkit 의존성을 주입해줌implementation("com.google.android.gms:play-services-mlkit-barcode-scanning:18.3.0")
onDetectListener.kt
interface onDetectListener { fun onDetect(msg : String) }
class QRCodeAnalyzer(val onDetectListener: onDetectListener) : ImageAnalysis.Analyzer{
// 바코드 스캐닝 객체 생성
private val scanner = BarcodeScanning.getClient()
@SuppressLint("UnsafeOptInUsageError")
override fun analyze(imageProxy: ImageProxy) {
val mediaImage = imageProxy.image
if (mediaImage != null) {
// 이미지가 찍힐 단시 카메라의 회전 각도를 고려해 입력 이미지 생성
val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
// scanner.process(image) 통해 이미지 분석
// SuccessListener, FailureListener, CompleteListener를 각각 달아주어 결과 확인 가능
scanner.process(image).addOnSuccessListener { qrCodes ->
// QR 코드가 성공적으로 찍혔을 시
for (qrCode in qrCodes) {
// rawValue가 존재하면 rawValue값을 보내고, null이면 빈 문자열 보냄
onDetectListener.onDetect(qrCode.rawValue ?: "")
}
}.addOnFailureListener{
it.printStackTrace()
}.addOnCompleteListener{
imageProxy.close()
}
}
}
}
fun getImageAnalysis() : ImageAnalysis {
val cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor()
val imageAnalysis = ImageAnalysis.Builder().build()
// QRCodeAnalyzer 객체 생성 후 setAnalyzer() 함수의 인수로 넣어줌
// object를 통해 OnDetectListener 인터페이스 객체 생성 후 onDetect() 함수를 오버라이드
imageAnalysis.setAnalyzer(cameraExecutor, QRCodeAnalyzer(object : OnDetectListener {
override fun onDetect(msg: String) {
// onDetect() 함수가 QRCodeAnalyzer에서 불렀을 때 행동 정의
Toast.makeText(this@MainActivity, "${msg}", Toast.LENGTH_SHORT).show()
}
}))
return imageAnalysis
}
fun startCamera() {
// cameraProvideFuture에 객체의 참조값 할당
cameraProvideFuture = ProcessCameraProvider.getInstance(this)
// cameraProvideFuture 태스크가 끝나면 실행
cameraProvideFuture.addListener(Runnable {
// ProcessCameraProvider 객체 가져오기
// ProcessCameraProvider: 카메라의 생명 주기를 액티비티 생명 주기에 바인드 해줌
val cameraProvider = cameraProvideFuture.get()
// 미리보기 객체 가져오기
val preview = getPreview()
// 이미지분석 객체 가져오기
val imageAnalysis = getImageAnalysis()
// DEFAULT_BACK_CAMERA(후면 카메라) 선택
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
// preview(미리보기) 쓰기 선택
cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageAnalysis)
}, ContextCompat.getMainExecutor(this))
}
MainActivity.kt
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat
import com.example.qrcodereaderapp.databinding.ActivityMainBinding
import com.google.common.util.concurrent.ListenableFuture
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
class MainActivity : AppCompatActivity() {
// 바인딩 변수 생성
private lateinit var binding : ActivityMainBinding
// ListenableFuture형 변수 생성 => ListenableFuture에 태스크가 제대로 끝났을 때 동작 지정 가능
private lateinit var cameraProvideFuture : ListenableFuture<ProcessCameraProvider>
// 태그 기능 코드 => 나중에 권한을 요청한 후 결과를 onRequestPermissionsResult에서 받을 떄 필요
// 0과 같거나 큰 양수이기만 하면 어떤 수든 상관없음
private val PERMISSIONS_REQUEST_CODE = 1
// 카메라 권한 지정
private val PERMISSIONS_REQUIRED = arrayOf(android.Manifest.permission.CAMERA)
// onDetect() 함수가 여러 번 호출되는 것 방지 기능 변수 선언
private var isDetected = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 바인딩 클래스에 포함된 inflate() 함수를 실행해 바인딩 클래스의 객체 생성
binding = ActivityMainBinding.inflate(layoutInflater)
val view = binding.root // 바인딩 객체의 root 뷰 참조
setContentView(view) // 생성한 뷰 설정
if (!hasPermissions(this)) { // 권한이 없을 때
// 카메라 권한 요청
requestPermissions(PERMISSIONS_REQUIRED, PERMISSIONS_REQUEST_CODE)
} else {
// 이미 권한 있을 때
startCamera()
}
}
// all => PERMISSIONS_REQUIRED 배열의 원소가 모두 조건문을 만족하면 true, 아니면 false 반환
fun hasPermissions(context: Context) = PERMISSIONS_REQUIRED.all {
ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
}
// 권한 요청 콜백 함수
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
// onCreate() 메서드에서 requestPermissions의 인수로 넣은 PERMISSIONS_REQUEST_CODE와 맞는지 확인
if (requestCode == PERMISSIONS_REQUEST_CODE) { // 권한 수락 시
if (PackageManager.PERMISSION_GRANTED == grantResults.firstOrNull()){
Toast.makeText(this@MainActivity, "권한 요청이 승인되었습니다.", Toast.LENGTH_LONG).show()
startCamera()
} else { // 권한 거부 시
Toast.makeText(this@MainActivity, "권한 요청이 거부되었습니다.", Toast.LENGTH_LONG).show()
finish()
}
}
}
// 다시 사용자의 포커스가 MainActivity로 돌아온다면 다시 QR 코드를 인식할 수 있는 기능
// onResume() 함수를 오버라이드해 isDetected를 false로 다시 돌려줌
override fun onResume() {
super.onResume()
isDetected = false
}
fun getImageAnalysis() : ImageAnalysis {
val cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor()
val imageAnalysis = ImageAnalysis.Builder().build()
// QRCodeAnalyzer 객체 생성 후 setAnalyzer() 함수의 인수로 넣어줌
// object를 통해 OnDetectListener 인터페이스 객체 생성 후 onDetect() 함수를 오버라이드
imageAnalysis.setAnalyzer(cameraExecutor, QRCodeAnalyzer(object : OnDetectListener {
override fun onDetect(msg: String) {
// onDetect() 함수가 QRCodeAnalyzer에서 불렀을 때 행동 정의
if (!isDetected) { // 한 번도 QR 코드가 인식된 적 없는지 검사(중복 실행 방지)
isDetected = true // 데이터가 감지되었으므로 true
val intent = Intent(this@MainActivity, ResultActivity::class.java)
intent.putExtra("msg", msg) // 다음 액티비티로 데이터를 키-쌍의 형태로 넘기기 가능
startActivity(intent)
}
}
}))
return imageAnalysis
}
fun startCamera() {
// cameraProvideFuture에 객체의 참조값 할당
cameraProvideFuture = ProcessCameraProvider.getInstance(this)
// cameraProvideFuture 태스크가 끝나면 실행
cameraProvideFuture.addListener(Runnable {
// ProcessCameraProvider 객체 가져오기
// ProcessCameraProvider: 카메라의 생명 주기를 액티비티 생명 주기에 바인드 해줌
val cameraProvider = cameraProvideFuture.get()
// 미리보기 객체 가져오기
val preview = getPreview()
// 이미지분석 객체 가져오기
val imageAnalysis = getImageAnalysis()
// DEFAULT_BACK_CAMERA(후면 카메라) 선택
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
// preview(미리보기) 쓰기 선택
cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageAnalysis)
}, ContextCompat.getMainExecutor(this))
}
fun getPreview() : Preview {
val preview : Preview = Preview.Builder().build() // Preview 객체 생성
// setSurfaceProvider(): Preview 객체에 SurfaceProvider를 설정해줌
//SurfaceProvider: Preview에 Surface(화면에 보여지는 픽셀들이 모여 있는 객체)를 제공해주는 인터페이스
preview.setSurfaceProvider(binding.barcodePreview.getSurfaceProvider())
return preview
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.camera.view.PreviewView
android:id="@+id/barcode_preview"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
QRCodeAnalyzer.kt
import android.annotation.SuppressLint
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import com.google.mlkit.vision.barcode.BarcodeScanning
import com.google.mlkit.vision.common.InputImage
class QRCodeAnalyzer(val onDetectListener: OnDetectListener) : ImageAnalysis.Analyzer{
// 바코드 스캐닝 객체 생성
private val scanner = BarcodeScanning.getClient()
@SuppressLint("UnsafeOptInUsageError")
override fun analyze(imageProxy: ImageProxy) {
val mediaImage = imageProxy.image
if (mediaImage != null) {
// 이미지가 찍힐 단시 카메라의 회전 각도를 고려해 입력 이미지 생성
val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
// scanner.process(image) 통해 이미지 분석
// SuccessListener, FailureListener, CompleteListener를 각각 달아주어 결과 확인 가능
scanner.process(image).addOnSuccessListener { qrCodes ->
// QR 코드가 성공적으로 찍혔을 시
for (qrCode in qrCodes) {
// rawValue가 존재하면 rawValue값을 보내고, null이면 빈 문자열 보냄
onDetectListener.onDetect(qrCode.rawValue ?: "")
}
}.addOnFailureListener{
it.printStackTrace()
}.addOnCompleteListener{
imageProxy.close()
}
}
}
}
OnDetectListener.kt
interface OnDetectListener {
fun onDetect(msg : String)
}
ResultActivity.kt
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.example.qrcodereaderapp.databinding.ActivityResultBinding
class ResultActivity : AppCompatActivity() {
lateinit var binding: ActivityResultBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityResultBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
val result = intent.getStringExtra("msg") ?: "데이터가 존재하지 않습니다."
setUI(result)
}
private fun setUI(result: String) {
binding.tvResult.text = result
binding.btnGoBack.setOnClickListener {
finish()
}
}
}
activity_result.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ResultActivity">
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="168dp"
android:text="@string/title"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_result"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="결괏값 자리"
android:textColor="@color/gray"
android:textSize="18sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_title" />
<Button
android:id="@+id/btn_go_back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="68dp"
android:backgroundTint="@color/pink"
android:text="@string/back"
android:textSize="15sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>