2026/03/11

오늘의 이야기

진행률

앱을 구현하다 보면 간혹은 화면을 전환하는 동안에 진행률(progressbar) 표시를 통해서 사용자와 소통(?)을 해 보고자 하는 경우가 있습니다.  이런 경우 어떻게 할 것인지를 찾아보면서 이번 포스팅을 정리하고자 합니다. 


 


https://www.jetpackcompose.net/jetpack-compose-progress-indicator-progressbar



 


Jetpack Compose Progress Indicator (Progressbar)


In Jetpack Compose, Progress Indicator is a widget to indicate some actions are in progress to the user. Types of Progress Indicators available in Jetpack Compose LinearProgressIndicator CircularProgressIndicator For long-time operations such as file downl


www.jetpackcompose.net




참조했던 내용의 링크를 먼저 올려 둡니다.  이번 구현의 위 링크의 내용을 참조했습니다.


 


구현 샘플 미리 보기



linear progressbar 표시


동작하는 영상은 4초가량인데, 시작하면서 바로 '합계=' 아래에 linear progressbar 가 흘러가는 모양을 볼 수 있습니다. 


 


import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.*
import androidx.compose.material.LinearProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.billcoreatech.bespeak1003.ui.theme.softBlue

@Composable
fun CustomCircularProgressBar(visible : Boolean){
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
AnimatedVisibility(
visible = visible,
enter = fadeIn(
// Overwrites the initial value of alpha to 0.4f for fade in, 0 by default
initialAlpha = 0.4f
),
exit = fadeOut(
// Overwrites the default animation with tween
animationSpec = tween(durationMillis = 250)
)
) {
LinearProgressIndicator(
modifier = Modifier
.fillMaxWidth()
.height(3.dp),
backgroundColor = Color.LightGray,
color = softBlue
)
}
}

}

위 코드 구현과 같이 구현이 됩니다.  이렇게 구현한 것은 MainActivity에서 boolean 형으로 파라미터를 받아와서 progressbar 가 보이고 어떤 action 이 마무리되면 boolean의 값이 false 가 되면서 progressbar을 사라지는 기능을 구현하기 위해서 AnimatedVisibility 을 사용 했습니다.  이렇게 구현 하면 progressbar 을 필요에 따라서 보이고, 사라지고를 선택할 수 있게 됩니다.


 


앞서 링크에서 설명을 보는 것처럼 Circular, Linear 형 선택적으로 사용할 수 있습니다. 일반적으로 Circular을 사용하는 것이기는 하나, 이번에는 Linear 형으로 구현을 해 보았습니다.


 


오늘도 다들 즐~ 코딩하세요.





오늘의 이야기

CAMERA Permission


앱에서 카메라 권한을 획득해야 하는 경우가 종종 발생합니다. 예전에는 앱이 시작되는 시점에 모든 권한을 획득하고 시작을 했지만, 요즘은 실제 행위가 발생하는 시점에 권한 획득을 하도록 유도하고 있습니다. 


<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" />

항상 그렇지만, manifest 파일에는 위와 같은 권한을 선언해 둡니다.  그래야 다음 동작을 구현할 때 오류가 발생하지 않습니다.  다음은 gradle 파일에 권한 획득을 위한 라이브러리를 가져올 수 있도록 구현합니다. 


dependencies {

.....

// 권한 획득
implementation "com.google.accompanist:accompanist-permissions:0.27.0"

.....

}

이제 구현된 코드를 보도록 하겠습니다. 


@OptIn(ExperimentalPermissionsApi::class)
@Composable
private fun FeatureThatRequiresCameraPermission(
doResult:(ty:Boolean) -> Unit
) {

// Camera permission state
val cameraPermissionState = rememberPermissionState(
Manifest.permission.CAMERA
)

when (cameraPermissionState.status) {
// If the camera permission is granted, then show screen with the feature enabled
PermissionStatus.Granted -> {
doResult(true)
}
is PermissionStatus.Denied -> {
Column(
modifier = Modifier.padding(3.dp),
horizontalAlignment = Alignment.End
) {
val textToShow = if ((cameraPermissionState.status as PermissionStatus.Denied).shouldShowRationale) {
// If the user has denied the permission but the rationale can be shown,
// then gently explain why the app requires this permission
stringResource(id = R.string.msgGetPermissonCamera)
} else {
// If it's the first time the user lands on this feature, or the user
// doesn't want to be asked again for this permission, explain that the
// permission is required
stringResource(id = R.string.msgGetPermissonCamera)
}
IconButton(onClick = {

cameraPermissionState.launchPermissionRequest()
doResult(false)

}) {
Icon(
imageVector = Icons.Outlined.PermCameraMic,
contentDescription = "Grant a Camera",
tint = softBlue
)
}
Text(textToShow)
}
}
}
}

doResult() 함수의 경우는 다른 화면 구성에서 호출 했을 때 권한 획득 여부를 return을 해 주면 해당 위치에서 다른 기능을 구현할 수 있게 됩니다.  그것을 이용하기 위해서 처리한 부분입니다. 


 


이제 구현된 화면이 구동 모습을 보도록 하겠습니다. 


권한 획득 흐름



이렇게 하면 앱이 구동중에 권한이 필요한 경우 메시지를 보여 주고 해당 권한을 획득한 후 필요한 동작을 구현해 볼 수 있습니다. 


 


이 포스팅은 아래 링크의 내용을 참조 하였음을 밝혀 둡니다.


https://google.github.io/accompanist/permissions/



 


Guide - Accompanist


Jetpack Compose Permissions A library which provides Android runtime permissions support for Jetpack Compose. Warning The permission APIs are currently experimental and they could change at any time. All of the APIs are marked with the @ExperimentalPermiss


google.github.io




 





오늘의 이야기


#스하리1000명프로젝트,
Kadang-kadang susah nak bercakap dengan pekerja asing kan?
Saya membuat aplikasi mudah yang membantu! Anda menulis dalam bahasa anda, dan orang lain melihatnya dalam bahasa mereka.
Ia auto-terjemah berdasarkan tetapan.
Sangat berguna untuk sembang mudah. Lihatlah apabila anda mendapat peluang!
https://play.google.com/store/apps/details?id=com.billcoreatech.multichat416




오늘의 이야기

바이오인증


앱을 구현하는 동안 이런 것도 생각을 해 볼 수 있었습니다.  지문인증은 어떻게 구현하는 건가?


그래서 구글에게 물어보았습니다. 어떻게 하는 거냐고...  늘 항상 답을 보여 주기는 하나 긴가 민가 하는 생각이 들 무렵 게시글 하나를 찾았고 그것을 따라 해 보기로 했습니다. 


 


gradle 설정


// bio
implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05"

 module 수준의 gradle 파일에 추가된 것은 위 한 줄입니다. 


 


Hardware 검증


이번에는 지문 인증을 사용할 수 있는 것인지 확인하는 처리를 해 봅니다.   아래 코드의 함수를 호출하게 되면, 


사용이 가능한 상태 (BIOMETRIC_SUCCESS),


지문인식 센서가 없는 경우(BIOMETRIC_ERROR_NO_HARDWARE),


지문인식 센서가 이미 동작하고 있어서 준비가 되지 않은 경우(BIOMETRIC_ERROR_HW_UNAVAILABLE)


지문이 등록되지 않은 경우(BIOMETRIC_ERROR_NONE_ENROLLED)


 


이런 경우의 오류(또는 상태)를 만나게 됩니다.  지문 등록이 되지 않은 경우는 등록을 하도록 안내하는 페이지로 이동하도록 구성하고, 다른 경우들은 그에 맞게 알림을 주거나 인증 후 동작을 구현하게 됩니다.  여기서는 사용 가능한 상태만 저장하는 것으로 마무리를 합니다.


fun checkDeviceHasBiometric() {
val biometricManager = BiometricManager.from(this)
when (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.DEVICE_CREDENTIAL)) {
BiometricManager.BIOMETRIC_SUCCESS -> {
Log.e("MY_APP_TAG", "App can authenticate using biometrics.")
info = getString(R.string.biometric_success) // "App can authenticate using biometrics."
productsViewModel.isBioAuth.value = true

}
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
Log.e("MY_APP_TAG", "No biometric features available on this device.")
info = getString(R.string.biometric_error_no_hardware) // "No biometric features available on this device."
productsViewModel.isBioAuth.value = false

}
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> {
Log.e("MY_APP_TAG", "Biometric features are currently unavailable.")
info = getString(R.string.biometric_error_hw_unavailable) // "Biometric features are currently unavailable."
productsViewModel.isBioAuth.value = 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,
BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.DEVICE_CREDENTIAL
)
}
productsViewModel.isBioEnabled.value = false

startActivityIfNeeded(enrollIntent, BIOMETRIC_CODE)
}
}
}

 


지문 인식 하기


다음 코드의 함수와 같이 작성하게 되면, 하드 웨어 적인 지문 인식 기능을 사용할 수 있게 됩니다.


promptInfo의 속성들을 보면 화면에 보여줄 제목과 상세 설명 등을 설정할 수 있으니 그것에 앱의 기능 구현에 필요한 상태에 따라서 제목, 메시지 등을 추가할 수 있습니다.


 


biometricPrompt을 호출해 주는 것으로 동작이 실행되는 것을 볼 수 있습니다. 여기서는 지문 인증이 잘 되었는지에 따라서 다음 동작을 구현해 볼 수 있습니다.


private fun doFingerPrint() {
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle(getString(R.string.biometric_prompt_title))
.setSubtitle(getString(R.string.biometric_prompt_description))
.setNegativeButtonText(getString(android.R.string.cancel))
.build()

val biometricPrompt = BiometricPrompt(
this@MainActivity,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(
errorCode: Int,
errString: CharSequence
) {
if (errorCode !in biometricsIgnoredErrors) {
Toast.makeText(
this@MainActivity,
getString(R.string.biometric_error, errString),
Toast.LENGTH_LONG
).show()
}
productsViewModel.isBioEnabled.value = false
}
override fun onAuthenticationSucceeded(
result: BiometricPrompt.AuthenticationResult
) {
productsViewModel.isBioEnabled.value = true
}
override fun onAuthenticationFailed() {
Toast.makeText(
this@MainActivity,
R.string.biometric_authentication_error,
Toast.LENGTH_LONG
).show()
productsViewModel.isBioEnabled.value = true
}
}
)

biometricPrompt.authenticate(promptInfo)
}

 


활용 예시


이렇게 아래 화면과 같은 기능이 구현 가능해집니다.  이전 화면에서 지문인식 동작하는 버튼을 클릭하면 아래 그림과 같이 팝업 안내창이 나오고 지문 인식 대기 상태가 되어 사용자가 지문을 클릭 하면 위 코드와 같이 다음 동작으로 넘어가는 기능 구현을 할 수 있게 됩니다.


지문인식사용 예시



 


참고자료


이번 글의 재료는 아래 링크에서 참조했음을 밝혀 둡니다.  다음번에는 아래 링크의 내용 중에서 PIN을 입력받는 코드 구현을 해 볼까 합니다.


https://medium.com/@fvilarino/adding-a-pin-screen-with-biometric-authentication-in-jetpack-co mpose-a9bf7bd8acc9



 


Adding a PIN screen with biometric authentication in Jetpack Compose


In this article we'll find out how to add biometric authentication to an Android app developed with Jetpack Compose. As not all devices…


fvilarino.medium.com




 





오늘의 이야기

https://github.com/firebase/firebase-android-sdk/issues/4223



 


Missing class com.google.android.datatransport.runtime.ForcedSender in Crashlytics 18.3.0 · Issue #4223 · firebase/firebase-an


Android Studio version: Android Studio Dolphin | 2021.3.1 Patch 1 Firebase Component: Crashlytics Component version: 18.3.0 Describe the problem If a crash occurs within the Android app (e.g. by th...


github.com





위에 나와 있는 글을 읽어 보면  firebase에서 오류 감지를 위해서 사용하고 있는 API에 오류가 있는 것처럼 보입니다.

crashlytics는 firebase에서 제공하는 API 중에서 앱의 오류를 감지합니다.  playstore 에도 릴리즈 된 앱의 경우는 그 내용이 감지되어 알려 주기는 하지만, firebase의 경우는 조금 더 상세하게 알게 되는 것 같아서 사용하는 중입니다.

위 링크 글에서 보는 것처럼 최종 버전인 것 같은 18.3.0은 사용을 보류하고 18.2.3으로 당분간은 유지를 해지 않나 하는 생각을 하면서 이 글을  정리해 봅니다.


관련이미지




--- 아래 내용은 링크된 글의 번역한 내용입니다. ---

문제 설명

Android 앱 내에서 충돌이 발생하는 경우(예: 앱 내에서 버튼을 누를 때 NullPointerException 발생) 앱을 다시 시작할 때 앱이 로드되지 않고 com.google.android.datatransport.runtime 누락으로 인해 충돌합니다.  이 버전에서 새로 추가된 ForcedSender 클래스입니다.

이것은 crashlytics 종속성만 사용하여 18.3.0으로 업데이트할 때 발생합니다.
구현 'com.google.firebase:firebase-crashlytics:18.3.0'
저는 BOM 종속성을 사용하지 않습니다(따라서 BOM을 사용할 때 이 종속성이 해결되었는지 모르지만 위의 사용법에서는 확실히 누락되었습니다.)

그 사이에 18.2.13이 안정적이어서 되돌렸습니다.

재현 단계:

무슨 일이에요?  문제가 발생하도록 하려면 어떻게 해야 합니까?
Crashlytics 18.3.0으로 업데이트하면 앱이 충돌한 다음 다시 시작하려고 합니다.





오늘의 이야기


#billcorea #운동동아리관리앱
🏸 Schneedle, aplikasi mesti ada untuk kelab badminton!
👉 Main Perlawanan – Rekod Markah & Cari Lawan 🎉
Sesuai untuk mana-mana sahaja, bersendirian, bersama rakan-rakan atau dalam kelab! 🤝
Jika anda suka badminton, pasti mencubanya

Pergi ke aplikasi 👉 https://play.google.com/store/apps/details?id=com.billcorea.matchplay




오늘의 이야기

https://relay.material.io/getting-started







Relay


Instant Handoff for Android UI. Design UI components in Figma and use them directly in Jetpack Compose projects.


relay.material.io





Relay


릴레이, 계전기, 교체 구글 번역기가 말해주는 번역된 단어입니다. 알게 된 것은 HOLIX라는 앱에서 jetpack compose 채팅방에 들어가 보다가 workspace 님이 올리신 글을 보고 알게 되었습니다.

찾아서 가 따라 해 보기를 해 보았습니다. 그 동안 고민했던 것들 중에 화면 구성을 어떻게 하면 그나마 쉽게 다이내믹하게 할 수 있을 까에 대한 고민을 해소할 수 있을 것 같은 생각이 듭니다.

그동안의 화면 구성은 그냥 단순하게 Object을 위치하는 것으로 대신하는 중이었습니다. 뭐 아직은 화려하거나 이쁘거나 한 화면 구성을 못해보는 것이 아쉬울 따름 이었지만, Figma라는 것이 온라인으로 화면 디자인을 할 수 있다는 것 정도만 알고 있었지 이것을 UI와 연동할 수 있다는 것을 모르고 있었으니까요.

샘플앱


이런 디자인은 간단한 것이라 그냥 그려도 되겠지만, 연동을 해서 만들어 보면 좋을 것 같다는 생각이 들었습니다.

차근 차근 배워 봐야 할 것 같아요.

이번 이야기는 10/24 에 있었던 Android Dev Summit 2022 에서 발표된 이야기 랍니다.





오늘의 이야기

TabRow  


앱을 구성하는 부분 중에서 메뉴판 같은 것을 구현하게 되는 경우가 발생합니다. 다른 방법도 있기는 하겠지만, 카테고리별로 TabRow을 구현해서 만들면 같은 종류의 상품을 배열하거나 할 때 도움이 될 것 같습니다.  그래서 오늘은 그걸 만들어 보려고 합니다. 


 


gradle (Module) 파일에 아래 2줄을 추가해 주었습니다.


// tab layout 구현
implementation "com.google.accompanist:accompanist-pager:0.20.1"
implementation "com.google.accompanist:accompanist-pager-indicators:0.20.1"

이제 코드 구현을 해 보겠습니다. 


 


val pagerState = rememberPagerState(0)

먼저 사용할 탭의 상태를 기억할 변수를 선언 합니다. 괄호 안에 숫자는 시작 시 어디에 위치하게 할 건가를 정하게 됩니다. 인덱스는 0부터 시작됩니다.


 


구성된 화면은 다음 그림과 같이 될 예정입니다.


구성된 화면



그래서 화면을 구성할 때 상단에 있는 Tab을 나타내는 부분과 그 아래 내용을 보여줄 부분으로 나누어서 설정을 하게 됩니다. (*맨 아래는 BottomNavigation으로 이번 글과는 무관한 구현이라 설명을 생략합니다.)


 


Column(
modifier = Modifier.background(grayWhite)
) {
Tabs(pagerState = pagerState)
TabsContent(pagerState = pagerState)
}

위에 Tabs는 화면 상단에 표시할 탭을 나타내며 아래 TabsContent는 아래 화면을 구성할 내용을 채워 넣을 공간입니다.


 


@ExperimentalPagerApi
@Composable
fun Tabs(pagerState: PagerState) {
val list = listOf(
"Order" to Icons.Outlined.GifBox,
"Shopping" to Icons.Outlined.ShoppingCart,
"Payments" to Icons.Outlined.Payments
)
val scope = rememberCoroutineScope()
TabRow(

selectedTabIndex = pagerState.currentPage,
backgroundColor = softBlue,
contentColor = Color.White,
indicator = { tabPositions ->
TabRowDefaults.Indicator(
Modifier.pagerTabIndicatorOffset(pagerState, tabPositions),
height = 2.dp,
color = Color.White
)
}
) {
list.forEachIndexed { index, _ ->
Tab(
icon = {
Icon(imageVector = list[index].second, contentDescription = null)
},
text = {
Text(
list[index].first,
color = if (pagerState.currentPage == index) grayWhite else Color.LightGray
)
},
selected = pagerState.currentPage == index,
onClick = {
scope.launch {
pagerState.animateScrollToPage(index)
}
}
)
}
}
}

먼저 배열에 표시할 Tab의 제목으로 사용할 부분을 만들고, TabRow을 이용해서 그 항목들을 나열하는 구현을 하게 됩니다. 


 


@ExperimentalPagerApi
@Composable
fun TabsContent(pagerState: PagerState) {
HorizontalPager(modifier = Modifier.fillMaxSize(), state = pagerState, count = 5) {
page ->
when (page) {
0 -> TabContentScreen(data = "Order")
1 -> TabContentScreen(data = "Shipping")
2 -> TabContentScreen(data = "Payments")
}
}
}

다음은 그 내용들을 표시할 화면을 만들고 HorizontalPager을 이용해서 어느 탭을 선택했는지 전달받고 그 값에 따라서 하단에 각각의 화면을 구성하는 모양을 만들면 됩니다. 


 


이 적용을 통해서 구현을 잘해 보도록 하겠습니다.


 


이 구현은 다음의 링크에서 참조했음을 밝혀 둡니다.


https://www.geeksforgeeks.org/tab-layout-in-android-using-jetpack-compose/



 


Tab Layout in Android using Jetpack Compose - GeeksforGeeks


A Computer Science portal for geeks. It contains well written, well thought and well explained computer science and programming articles, quizzes and practice/competitive programming/company interview Questions.


www.geeksforgeeks.org




 





오늘의 이야기


#스하리1000명프로젝트,
Perso in Corea? Anche se non parli coreano, questa app ti aiuta a muoverti facilmente.
Basta parlare la tua lingua: traduce, cerca e mostra i risultati nella tua lingua.
Ottimo per i viaggiatori! Supporta oltre 10 lingue tra cui inglese, giapponese, cinese, vietnamita e altre.
Provalo adesso!
https://play.google.com/store/apps/details?id=com.billcoreatech.opdgang1127




2026/03/10

오늘의 이야기

 


네이버 지식인 프로필



아주 오래전에 네이버 지식인에 등록을 했었습니다.  이게 뭔가 하고... 그렇게 잊혀 지나가길 10여 년이 지났을 언제가 같이 근무하는 지인의 친구가 네이버 지식인 활동을 통해서 수입이 생긴다는 말을 들었던 시절쯤, 다시 지식인 활동을 시작했습니다. 그러다가 뜨문뜨문 하게 되어 시간이 많이 흘렀습니다. 


 


등급이 올라가는 속도는 갈 수록 느려지는 것 같아요. 처음 몇 단계는 쉽게 올라가더니... 이제 지존 다음 단계인 초인으로 가기 위해서는 답변 채택이 200개가 더 있어야 한다고 하네요. 답변 채택률이 87% 수준이면 잘하는 건가는 모르겠지만,  비율로 치면 220개가량은 답을 달아야 한다는 결론이 나오네요.


 


그것도 시간으로 때워야 할 것 같은 생각이 들기는 합니다. 다만, expert 활동을 할 수 있는 조건이 완화 되었다는 소식이 있네요. 그래서 조만간 export 활동을 해 볼 수 있을 것 같습니다. 


 


그날을 기다리면서... 오늘도 수고 했으니 잘 쉬세요. 





오늘의 이야기

지난 포스팅에 연속해서



https://billcorea.tistory.com/267







개발일기 #5 PAYAPP 연동을 위한 준비


이번 개발을 시작하면서 준비해야 할 것 중에 하나가 결제처리를 지원하는 PG(Payment GateWay) 연동 이었습니다. 이전 글에서도 적었던 것처럼 일반적인 PG 연동 API 들은 하나 같이 쇼핑몰 앱을 기반(


billcorea.tistory.com





구동되는 이야기는 지난 포스팅에 적었습니다. 참고하시면 될 것 같고요. 오늘은 그것들을 구현하기 위해 했던 코드 구현에 대해서 이야기해 보겠습니다.


HTTP 통신은


일반적으로 API 통신은 HTTP 을 이용해서 호출하라고 합니다. 이번에 사용했던 PAYAPP의 경우에도 동일합니다. 이런 경우 안드로이드에서는 통신과 관련된 몇 가지 해야 할 부분들이 있습니다.

먼저 manifest 에서의 권한 설정 부분이 필요합니다.


<uses-permission android:name="android.permission.INTERNET" />

인터넷 사용에 대한 권한을 등록합니다. 다음은 통신의 하는 경우 네트워크와 관련해서는


<application
...
android:usesCleartextTraffic="true"
... >

<activity
android:name=".MainActivity"
android:exported="true"

...

안드로이드 가이드 발췌


개발자 가이드에서 알려주고 있는 것과 같이 네트워크 트래픽에 대한 설정을 true로 해 주어야 합니다.

그다음 요구되는 사항은 http connect을 맺거나 하는 경우 비동기 설정을 해 주어야 하는 부분이 있습니다. 그래서 구글링을 해보면 여러 가지 방법들이 설명이 되어 있습니다. thread을 이용하여 하거나 그 외 다른 비동기 통신 방법을 구현하고 설명합니다.

https://ktor.io/







Ktor: Build Asynchronous Servers and Clients in Kotlin


Kotlin Server and Client Framework for microservices, HTTP APIs, and RESTful services


ktor.io




이번에는 Ktor 방식을 이용한 비동기 통신을 구현해 보기로 했습니다.



gradle 파일 설정


gradle 파일에는 아래와 같이 ktor을 사용하기 위해서 implementaion을 설정했습니다. 2022.10.20 쯤에는 2.1.2 버전이 최신 버전이라고 되어 있습니다.


//ktor
implementation "io.ktor:ktor-client-core:2.1.2"
implementation "io.ktor:ktor-client-cio:2.1.2"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4'


Helper 구현하기


먼저 통신을 위한 helper 을 구현해 봅니다.


import android.util.Log
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.util.date.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

class HttpRequestHelper {

companion object {
val TAG: String = HttpRequestHelper::class.java.name
}

private val client: HttpClient = HttpClient(CIO)

/**
* https://api.payapp.kr/oapi/apiLoad.html
*
* cmd=payrequest&userid=payapptest&goodname=testGood&price=1000&recvphone=01055559999&smsuse=n
*
*/
suspend fun requestKtorIoInDetail(): String =
withContext(Dispatchers.IO) {
// set HttpRequestBuilder
val response: HttpResponse = client.request("https://api.payapp.kr/oapi/apiLoad.html") {
method = HttpMethod.Post
headers {
append("Content-Type", "application/x-www-form-urlencoded")
}
parameter("cmd","payrequest")
parameter("userid","payapptest")
parameter("goodname","testGood")
parameter("price","1000")
parameter("recvphone","01055559999")
parameter("smsuse","n")
}
val responseStatus = response.status
Log.d(TAG, "requestKtorIo: $responseStatus")

if (responseStatus == HttpStatusCode.OK) {
response.bodyAsText()
} else {
"error: $responseStatus"
}
}
}

개발자 가이드에서 말하는 것처럼 thread을 지원하여 사용자는 앱이 멈춤을 느끼지 않도록 하고 비동기 통신을 이용하여 목적에 필요한 자료를 전송하고 결과를 수집할 수 있습니다.

https://developer.android.com/kotlin/coroutines-adv?hl=ko







Kotlin 코루틴으로 앱 성능 향상  |  Android 개발자  |  Android Developers


Kotlin 코루틴으로 앱 성능 향상 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Kotlin 코루틴을 사용하면 네트워크 호출이나 디스크 작업과 같은 장기 실행 작


developer.android.com




payapp 결제 연동


payapp api 가이드에서 설명하는 결제 연동 페이지로의 호출 부분은 위 예시된 코드와 같이 url을 호출하면 됩니다. 이번에 구현하고자 하는 앱의 경우는 결제 연동만 일단 구현해 볼 요량이므로 위 코드와 같이 구현하고, 관련된 정보를 추가로 파라미터로 전송하여 결제 연동을 위한 QRcode 이미지를 webView에 보여주는 방식으로 진행하고 있습니다.

































파라미터 전달 값
cmdpayrequest (고정값 : 결제 요청시 사용)
useridpayapp 에 판매 회원으로 가입한 id
goodname판매하는 상품명칭
price가격
recvphone결과를 수신받을 휴대전화번호
smsusesms 사용여부

위 정도는 필수값이고 그 외에도 다른 항목들이 있지만 그것은 API 문서를 참조하여 보시기 바랍니다. 이번 앱 구현에서는 이 정도만 있어도 됩니다.


MainActivity에서 사용


class MainActivity : ComponentActivity() , CoroutineScope {

.....
private lateinit var job: Job
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job

...

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

sp = getSharedPreferences(packageName, MODE_PRIVATE)
job = Job() // CoroutineScope를 위해 job을 할당

...

}

.....

private fun doPunched(destinationsNavigator: DestinationsNavigator) {
var errno = ""
var qrUrl = ""
launch(Dispatchers.Main) {
val result = HttpRequestHelper().requestKtorIoInDetail()
val decoded = URLDecoder.decode(result, "UTF-8")
var lineItems = decoded?.split("&")
if (lineItems != null) {
for (line in lineItems) {
Log.e("", "${line}")
if (line.indexOf("errno") > -1) {
errno = line.split("=")[1]
}
if (line.indexOf("qrurl") > -1) {
qrUrl = line.split("=")[1]
}
}
}
Log.e(TAG, "${qrUrl} ${errno}")
if (!"".equals(qrUrl)) {
doWebView(qrUrl, destinationsNavigator)
}
}
}

.....

fun doFinish() {
job.cancel() // 종료 되게...
finish()
}

mainActivity에서 다른 구현이 많이 있기는 하겠지만, 코루틴 사용을 위해서 Job을 선언하는 부분 등 필요한 부분만 나열을 하였으니 코드 구현 시 참고하셔야 합니다. 위 예시 코드와 같이 launch 구문을 이용해서 위에서 작성한 helper을 호출하면 결과 값이 string으로 돌아오기 때문에 그 값을 파싱 해서 qrurl 부분만 값으로 받아오고 그 값을 전달해서 webview에 표시하는 것으로 구현은 마무리가 됩니다.


@Composable
fun Url2WebView(
qrUrl : String,
doBackQRCode:() -> Unit
){

val fontFamily = FontFamily(Font(R.font.poorstory_regular, FontWeight.Normal))
Column(
modifier = Modifier
.fillMaxSize()
.padding(20.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Row(
modifier = Modifier.padding(2.dp),
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically
) {
IconButton(onClick = {
doBackQRCode()
}) {
Icon(
imageVector = Icons.Outlined.ArrowBack,
contentDescription = "ArrowBack",
tint = softBlue
)
}
Text(text = stringResource(id = R.string.msgPunchedQRCode), style = TextStyle(
fontWeight = FontWeight.Bold, color = softBlue, fontSize = 24.sp,
fontFamily = fontFamily
))
}
// Adding a WebView inside AndroidView
// with layout as full screen
AndroidView(factory = {
WebView(it).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
webViewClient = WebViewClient()
loadUrl(qrUrl)
}
}, update = {
it.loadUrl(qrUrl)
})
}
}


이제 앱이 잘 구현 되기를 바라면 글을 정리해 보겠습니다.





오늘의 이야기

GoogleMap 그려보기 오늘은 jetppack compose 환경에서 구글 지도를 활용하면서 조심해야 할 것 하나를 정리해 두고자 합니다.  이전 포스팅에서 구글 지도를 jetpack compose에 올리는 부분은 기술해 두었으니 참고하시면 될 것...