2026/03/07

오늘의 이야기

https://vtsen.hashnode.dev/free-android-development-learning-resources-for-beginners



 


Free Android Development Learning Resources for Beginners


Ultimate free resources and courses that I find useful while learning Android development as a beginner


vtsen.hashnode.dev




 


오늘은 아침에 찾은 링크 하나를 공유하는 것으로...


 


짧은 영어 이기는 해도 배우는 것들이 다 영문 사이트 이므로... 한 번은 읽어봐야 할 것 같아서...


 


끝.


 


링크이미지






오늘의 이야기


#스하리1000명프로젝트

스치니들!
내가 만든 이 앱은, 내 폰에 오는 알림 중에서 중요한 키워드가 있는 경우
등록해둔 친구에게 자동으로 전달해주는 앱이야 📲

예를 들어, 카드 결제 알림을 와이프나 자녀에게 보내주거나
이번 달 지출을 달력처럼 확인할 수도 있어!

앱을 함께 쓰려면 친구도 설치 & 로그인해줘야 해.
그래야 친구 목록에서 서로 선택할 수 있으니까~
서로 써보고 불편한 점 있으면 알려줘 🙏

👉 https://play.google.com/store/apps/details?id=com.nari.notify2kakao





오늘의 이야기

앱에서 알림을 구현하는 방법은 여러 가지가 있다. alertDiaglog을 이용하는 방법도 있기는 하지만, 요새는 SnackBar을 구현하는 경우가 많은 것 같다. Jetpack Compose에서는 아직 잘 모르겠는 부분이 있어서 구글을 하다 찾아보게 되었다.


 


https://stackoverflow.com/questions/68909340/how-to-show-snackbar-with-a-button-onclick-in-jetpack-compose



 


How to show snackbar with a button onclick in Jetpack Compose


I want to show snackbar with a button onclick in Jetpack Compose I tried this Button(onClick = { Snackbar(action = {}) { Text("hello") } } But AS said "@Composable


stackoverflow.com




 


이제 실전으로 갈 볼까 ? 먼저 위 글에서 퍼온 코드를 일부 수정해서 공통적으로 사용할 수 있도록 준비를 해 보아야겠다.


 


import android.annotation.SuppressLint
import android.util.Log
import androidx.compose.material.Scaffold
import androidx.compose.material.SnackbarResult
import androidx.compose.material.rememberScaffoldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import kotlinx.coroutines.launch

@SuppressLint("UnusedMaterialScaffoldPaddingParameter", "CoroutineCreationDuringComposition")
@Composable
fun SnackBarShow(
message:String, actionLabel: String,
doDismissed:() -> Unit,
doActionPerformed:() -> Unit
) {
val scaffoldState = rememberScaffoldState() // this contains the `SnackbarHostState`
val coroutineScope = rememberCoroutineScope()

Scaffold(
modifier = Modifier,
scaffoldState = scaffoldState // attaching `scaffoldState` to the `Scaffold`
) {
coroutineScope.launch {
// using the `coroutineScope` to `launch` showing the snackbar
// taking the `snackbarHostState` from the attached `scaffoldState`
val snackbarResult = scaffoldState.snackbarHostState.showSnackbar(
message = message,
actionLabel = actionLabel
)
when (snackbarResult) {
SnackbarResult.Dismissed -> {
Log.e("SnackBarShow", "Dismissed")
doDismissed()
}
SnackbarResult.ActionPerformed -> {
Log.e("SnackBarShow", "SnackBar's button clicked")
doActionPerformed()
}
}
}
}
}

코드를 일부 동작할 수 있도록 수정했다.  @SuppressLint("UnusedMaterialScaffoldPaddingParameter", "CoroutineCreationDuringComposition")는 코드에서 오류 표시가 나는 것 때문에 수정을 하기는 했다. Padding Parameter을 사용하지 않는 것과 DuringComposition 부분인 것 같기는 하지만, 아직은 잘 모른다.


 


그리고 doDismissed(), doActionPerformed() 함수는 화면의 버튼을 클릭 했을 때 동작을 처리하는 부분을 구현해야 하기 때문에 return 될 함수의 선언으로 추가해 주었다.


 


이제 MainActivity 에서 코드를 구현해 보아야겠다.


 


setContent{
SnackBarShow(
getString(R.string.msgFinish), getString(R.string.titleFinish),
doDismissed = {},
doActionPerformed = {
finish()
}
)
}

어디에서는 간에 setCotent 로 감싸고 나서 위에서 작성한 SnackBarShow을 호출해 주는 것이다. 파라미터는 메시지로 보여줄 것과 버튼에 들어갈 문구를 전달하고 return 되는 함수는 SnackBar의 버튼을 클릭하지 않은 경우 처리와 , 버튼을 클릭했을 때 사용할 처리를 위해서 구현한 함수 2개를 돌려받았다.


 


실행이미지



 





오늘의 이야기

https://flatteredwithflutter.com/using-compose-destinations%ef%bf%bc/



 


Using compose destinations


We will cover briefly: Current navigation in composeUsing compose destinations(Optional) Modify existing test cases Current navigation in compose We get Compose Navigation from the Jetpack Com…


flatteredwithflutter.com




navigation 의 첫 이야기 다음... 그것을 어떻게 풀어낼 것인가를 찾아 돌아다니다가 또 하나의 링크를 찾았다. 이것을 보면서 이해를 하기 시작해 본다. 


 


그래서 오늘은 따라해 보기를 해 보아야겠다.


 


먼저 build gradle 에 설정을 따라해 본다. 


 


plugins {
...
id 'com.google.devtools.ksp' version '1.7.0-1.0.6'
}

1.7.0-1.0.6 은 코틀린 버전과 연계가 되어야 하는 버전을 맞추어 주는 것으로 이해를 하였다. 코틀린이 1.7.10까지 패치가 되어 가는 것 같기는 하지만, 일단 확인된 바로는 1.7.0 까지 인 것 같아서...


 


android {

...

applicationVariants.all { variant ->
kotlin.sourceSets {
getByName(variant.name) {
kotlin.srcDir("build/generated/ksp/${variant.name}/kotlin")
}
}
}

kotlin.sourceSets.all {
languageSettings.optIn("kotlin.RequiresOptIn")
}

...

}

이 부분이 들어가면 gradle 빌드를 통해서 kotlin 이라는 폴더가 생기면서 필요한 class 등을 만들어 주는 경로를 설정하는 것으로 이해가 되었다.


 


dependencies {

...
// compose destination
// https://github.com/raamcosta/compose-destinations 에서 최종 버전을 확인
implementation 'io.github.raamcosta.compose-destinations:animations-core:1.7.15-beta'
ksp 'io.github.raamcosta.compose-destinations:ksp:1.7.15-beta'
implementation "androidx.hilt:hilt-navigation-compose:1.0.0"

}

다음은 implementation  을 선언해 주는 것인데, 버전 확인은 원작자의 github 에서 확인하여 수정하면 최신 버전이 사용될 것 같다. 


 


다음은 activity 을 만들어 주어야 하는 데... 지금은 테스트 하는 것이기 때문에 간단하게 수정해 보았다.


override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

doUpdateCheck()

setContent {

MainTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = softBlue
) {
// github 에서 본 것 처럼 추가.
val navHostEngine = rememberAnimatedNavHostEngine()
DestinationsNavHost(navGraph = NavGraphs.root, engine = navHostEngine)

}
}
}
}

이렇게 추가를 해 주고 나면 NavGraphs 가 생성이 되지 않아서 오류 표시가 나오지만, 일단은 무시하고 화면을 구성할 부분을 만들어 주었다.


 


@Destination(start = true)
@Composable
fun LoginScreen(
navigator: DestinationsNavigator
) {
Card(modifier = Modifier.fillMaxSize()) {
Text(text = stringResource(id = R.string.AppId))
}
Button(onClick = {
navigator.navigate(HomeScreenDestination)
}) {
Text(text = stringResource(id = R.string.action_geoList))
}

}

@Destination
@Composable
fun HomeScreen(
navigator: DestinationsNavigator
) {
/*...*/
Button(
onClick = {
navigator.navigate(
ProfileScreenDestination(
id = "someId",
isEditable = true
)
)
}
) {
Text(text = stringResource(id = R.string.action_addItem))

}
}

@Destination
@Composable
fun ProfileScreen(
navigator: DestinationsNavigator,
id: String,
isEditable: Boolean = false
) {
Button (onClick = {
navigator.popBackStack()
navigator.navigate(SearchScreenDestination("Text"))
}) {
Text(text = stringResource(id = R.string.action_Setting))
}
}

@Destination
@Composable
fun SearchScreen(
navigator: DestinationsNavigator,
query: String?
) {
Button (onClick = {
navigator.navigate(HomeScreenDestination)
}) {
Text(text = stringResource(id = R.string.action_setHome))
}
}

github 에서 보았던 예제를 참고해서 만들었고 동작 확인을 위해서 버튼만 추가해서 처리가 되는 지 확인해 보았다.


 


그 다음은 빌드를 위한 준비를 해 보자. android studio 의 terminal 에서 command 창을 열어서 


 


gradlew 실행



./gradlew clean build 을 입력해서 실행해 주면 gradle 을 실행 되면서 필요한 빌드를 하게 된다. 


 


생성된 폴더 와 class들



실행이 완료 되면 kotlin 폴더 아래와 위 그림과 같이 generated 된 파일들이 생성이 되고 이제 정말 앱을 빌드할 준비가 된다. 이제 나의 앱을 build 해서 실행해 보면 된다. 


 


https://billcorea.tistory.com/205



 


안드로이드 앱 만들기 : Compose Navigation ... 인터넷 펌.


https://medium.com/@cybercoder.naj/compose-navigation-in-3-minutes-5cff3c57c34e Compose Navigation in 3 Minutes Quick guide for navigation between composables in a Compose project medium.com 나름대..


billcorea.tistory.com




이렇게 실행해 보면 navigation 선언등등 이전 포스팅에서 적었던 것 같은 NavigationItem 등등 선언하지 않아도 navigation 을 구현할 수 있으므로 추가 하거나 할 때 다른 작업들을 잊어 버려도 오류가 나지 않을 것 같다.   (기능등을 비교해 보면 좋을 것 같다.)


 


즐~ 코딩 하길 바라며... 이걸 만든 원작자에게 감사의 인사를 보낸다...





오늘의 이야기


#스하리1000명프로젝트,
A veces es difícil hablar con trabajadores extranjeros, ¿verdad?
¡Hice una aplicación sencilla que ayuda! Escribes en tu idioma y los demás lo ven en el suyo.
Se traduce automáticamente según la configuración.
Súper útil para chatear fácilmente. ¡Echa un vistazo cuando tengas la oportunidad!
https://play.google.com/store/apps/details?id=com.billcoreatech.multichat416




오늘의 이야기

https://medium.com/@ibrajix/how-i-built-this-nice-looking-app-using-jetpack-compose-3974db7eb9e



 


How I built this nice looking app using Jetpack Compose


Jetpack compose is the future of building UI's on Android.


ibrajix.medium.com




이 글을 읽고 있는 중이다... kotlin을 이용해서 앱을 구현해 보고 있는 중이라서...  이글에는 splash 화면에 대한 이야기도 있는 것 같고, navigation에 대한 이야기도 있는 듯하다. navigation 은 원래 이분이 작성한 것은 아닌 듯하고,


저 글에서 봐야 하는 것은 splash 화면에 대한 부분인데, 난 그것도 보다도 아래 나와 있는 navigation에 대한 부분이 보였다.


 


@Destination 태그를 이용해서 android 가 제공하는 navigation 보다 수월하게 사용할 수 있다는 이야기를 하고 싶은 것 같다. 아직은 조금 더 배워야 할 것 같아서 링크를 달아 두려고 한다.  아래 링크는 라이브러리 형태로 활용하는 설명이 나와 있는 원본 게시물이다. 이것도 나중에 정독을 해야 할 것 같다.


 


https://github.com/raamcosta/compose-destinations



 


GitHub - raamcosta/compose-destinations: Annotation processing library for type-safe Jetpack Compose navigation with no boilerpl


Annotation processing library for type-safe Jetpack Compose navigation with no boilerplate. - GitHub - raamcosta/compose-destinations: Annotation processing library for type-safe Jetpack Compose na...


github.com




 


시니어 개발자로 살아 보는 건... 늘 배워야 하는 가 보다.


 





오늘의 이야기

kotlin 과 compose 의 버전 호환성



 


앱을 만들다 보니 이런 건도 알고 있어야 하네... 


 


빌드 오류 메시지



Caused by: org.gradle.api.GradleException: Compilation error. See log for more details 이 메시지를 보기 전에 지나가버린 메시지 가 있는데, 그건 잘 보이지 않는 경우가 있다. 


 


그래서 빌드창을 위아래로 드래그를 해 봐도 이것만 봐서는 알 수가 없고, 구글링을 해도 딱히 맞는 오류 대처 방안이 보이지 않는 다.  그래서 다시 빌드를 하면서 메시지들이 넘어가지 않도록 조절을 해서 찾은 이전 메시지


 




앞에서 로그의 자세히 보라고 했으니 잘 찾아 보았다면 헤매지 않아도 되겠지만, 영어가 짧은 개발자는 마지막 메시지를 그냥 구글링을 해 보게 된다는 것이다. ㅋㅋ~


 


그래서 찾은 상세 메시지에는 kotlin 컴파일러와 compose 컴파일러가 호환되는 버전을 찾아 주도록 해야 하는 부분이 있는 것이다. 


 


kotlin 이 1.7.10 까지 패치가 되었다고는 하는데, compose와 궁합이 맞는 버전은 아직 까지는 1.7.0 인 것 같다.  그래서 일단은 1.7.0과 1.2.0과 매칭을 해서 이번 작업을 시작해 보아야겠다.  자세한 정보는 아래 링크에서 참고하시길...


 


https://developer.android.com/jetpack/androidx/releases/compose-kotlin



 


Compose와 Kotlin의 호환성 지도  |  Android 개발자  |  Android Developers


Compose와 Kotlin의 호환성 지도 종속 항목 선언 Compose 컴파일러에 관한 종속 항목을 추가하려면 프로젝트에 Google Maven 저장소를 추가해야 합니다. 자세한 내용은 Google Maven 저장소를 읽어보세요. 다


developer.android.com




 





오늘의 이야기


#billcorea #운동동아리관리앱
🏸 Schneedle, ¡una aplicación imprescindible para los clubes de bádminton!
👉 Match Play: registra puntuaciones y encuentra oponentes 🎉
¡Perfecto para cualquier lugar, solo, con amigos o en un club! 🤝
Si te gusta el bádminton, definitivamente pruébalo.

Ir a la aplicación 👉 https://play.google.com/store/apps/details?id=com.billcorea.matchplay




오늘의 이야기

이것의정답은 타우린 이라네요







오늘의 이야기

https://blog.devgenius.io/how-to-use-biometric-authentication-in-kotlin-9885f372230f



 


How to use Biometric Authentication in Kotlin


You can use biometric authentication, like face recognition or fingerprint recognition, to protect sensitive data or premium content in…


blog.devgenius.io




오늘도 한걸음 배워 보도록 하겠다.  다른 게 아니라 로그인할 때 지문을 이용하여 로그인하는 기능을 구현해 보는 케이스를 찾았다. 그것도 kotlin으로 말이다. 


 


다음 앱을 개발할 때 적용해 보아야겠다. 그래서 오늘도 레이하네 에 자파나 님에게 허락을 득하지는 않았지만, 그분이 작성해 놓은 source을 읽어 보면서 배워 보도록 하겠다. 


 


먼저 gradle에 설정을 해야 한다. 


 


implementation "androidx.biometric:biometric-ktx:1.2.0-alpha04"

현재까지는 저버 전이 최종인 것 같다. 저것을 이용하면 지문 인증이 수월하게 구현이 되는 걸 배웠다.


 


화면구현



작성자님이 만들어 놓은 화면은 지문 이미지 하나, 


동작을 실행할 버튼 하나,  상태를 표현할 text 하나


 


3개가 들어 있는 화면을 구성했다.


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


다음은 아래처럼 지문 동작을 사용할 수 있는지 체크하는 함수를 기술한다.  지문 인식 기능을 구동해 보아야 하기 때문에 AVD에서는 설정에 따라 동작에 오류가 발생할 수 도 있다. 그래서 실물 폰에서 디버깅을 해 보는 것이 좋을 것 같다.


fun checkDeviceHasBiometric() {
val biometricManager = BiometricManager.from(this)
when (biometricManager.canAuthenticate(BIOMETRIC_STRONG or DEVICE_CREDENTIAL)) {
BiometricManager.BIOMETRIC_SUCCESS -> {
Log.e("MY_APP_TAG", "App can authenticate using biometrics.")
info = "App can authenticate using biometrics."
binding.btnLogin.isEnabled = true

}
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
Log.e("MY_APP_TAG", "No biometric features available on this device.")
info = "No biometric features available on this device."
binding.btnLogin.isEnabled = false

}
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> {
Log.e("MY_APP_TAG", "Biometric features are currently unavailable.")
info = "Biometric features are currently unavailable."
binding.btnLogin.isEnabled = false

}
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
// Prompts the user to create credentials that your app accepts.
val enrollIntent = Intent(Settings.ACTION_BIOMETRIC_ENROLL).apply {
putExtra(Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED,
BIOMETRIC_STRONG or DEVICE_CREDENTIAL)
}
binding.btnLogin.isEnabled = false

startActivityIfNeeded(enrollIntent, 100)
}
}
binding.tvMsg.text = info
}

상태를 확인하고 동작이 가능하다고 하면 이번에는 main activity에서 동작을 실행할 부분을 구현해 보는 것이다. 


 


private lateinit var executor: Executor
private lateinit var biometricPrompt: BiometricPrompt
private lateinit var promptInfo: BiometricPrompt.PromptInfo

선언된 변수를 보면 Excutor 실행자라고 봐야 하는 건가? 아직 잘 이해가 되지 않는 것이 하나 있고, 지문인식 안내 창에 사용할 프롬프트 선언과, 프롬프트 정보가 들어가는 것 하나 이렇게 선언이 되어 있다.


 


executor = ContextCompat.getMainExecutor(this)
biometricPrompt = BiometricPrompt(this, executor,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(
errorCode: Int,
errString: CharSequence,
) {
super.onAuthenticationError(errorCode, errString)
Toast.makeText(applicationContext,
"Authentication error: $errString", Toast.LENGTH_SHORT)
.show()
}

override fun onAuthenticationSucceeded(
result: BiometricPrompt.AuthenticationResult,
) {
super.onAuthenticationSucceeded(result)
Toast.makeText(applicationContext,
"Authentication succeeded!", Toast.LENGTH_SHORT)
.show()
}

override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
Toast.makeText(applicationContext, "Authentication failed",
Toast.LENGTH_SHORT)
.show()
}
})

promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Biometric login for my app")
.setSubtitle("Log in using your biometric credential")
.setNegativeButtonText("Use account password")
.build()

// Prompt appears when user clicks "Log in".
// Consider integrating with the keystore to unlock cryptographic operations,
// if needed by your app.

binding.btnLogin.setOnClickListener {
biometricPrompt.authenticate(promptInfo)
}

핵심은 이렇게 구현된 것인데, 필요에 따라 수정해 사용하면 될 것 같다.


excutor는 실행자가 이 앱이라는 것을 선언한다고 이해가 될 것 같기는 하다.


다음은 biometric prompt를 실행해서 지문 인식을 구동하고 결과를 받아서 다음 처리를 구현하면 될 것 같다. 


그다음은 promptInfo을 설정해서 지문인식 창이 구동되었을 때 나오는 안내문구 등을 선언할 수 있고, setNegativeButtonText을 이용해서 지문인식이 아닌 다른 방식으로 전환을 유도하는 안내문구도 달아볼 수 있을 것 같다.


 


이제 구동을 시켜 보는 것으로 끝.


 


구동화면



한 가지 또 보이지 않았던 것은 지문인증 이 실행된 상태에서는 화면 캡처가 되지 않는 다. 이건 보너스(?) 인가? 그래서 노트북 카메라로 찍었다.


 


나도 배우는 중이니, 전체 소스는 원작자의 글에서 찾아보시는 것으로 다가... git에 전체 소스가 있으니 배우는 데는 그렇게 어렵지 않을 것으로 생각된다.   다시 한번 원작자 님에게 감사를 드리며... 저 글을 읽고 나서 박수를 한번 쳐 드렸다.


 





오늘의 이야기


#스하리1000명프로젝트,
แพ้เกาหลีเหรอ? แม้ว่าคุณจะพูดภาษาเกาหลีไม่ได้ แต่แอปนี้จะช่วยให้คุณเดินทางได้อย่างง่ายดาย
เพียงพูดภาษาของคุณ ระบบจะแปล ค้นหา และแสดงผลลัพธ์เป็นภาษาของคุณ
เหมาะสำหรับนักเดินทาง! รองรับมากกว่า 10 ภาษา รวมถึงภาษาอังกฤษ ญี่ปุ่น จีน เวียดนาม และอื่นๆ อีกมากมาย
ลองตอนนี้!
https://play.google.com/store/apps/details?id=com.billcoreatech.opdgang1127




오늘의 이야기

  해가 저무는  바다는 이 더위를 식혀줄 요량인지 바람이  살랑 거린다. 하루 종일 이글 거리던 햇살을 이제 감추어야 하는 시간이 되어 바라보던 바닷가에 아이가 바다를 바라본다. 무엇을 남기고 싶은 건가? 이제 긴 여름도 입추라는 계절의 변화에 아직...