2026/03/11

오늘의 이야기

Cloud Function


서버를 보유 하지(Serverless) 않고 서버가 있는 것처럼 업무 구현을 하고 싶습니다. 개발을 하면서 데이터 베이스와 스토리지를  firebase을 활용하고 있다면 cloud function 도 배워서 준비를 해야 할 것 같습니다. 지금 개발하고 있는 앱이 payapp와 API 연동을 해야 하지만, 가난한(?) 개발자는 서버가 없습니다

대안으로 생각해 볼 수 있는 것을 찾아보다가 알게 된 cloud function의 구현에 대한 이야기를 해 보겠습니다.

* google cloud function : python, node.js, java 등등이 지원이 된다고 합니다.
** firebase cloud function : node.js 만 (2022.11.12 현재로는) 지원이 되고 있습니다

node.js을 잘 알고 있다면 firebase을 활용할 수 도 있겠지만, 아직은 python을 더 잘 알고 있기 때문에 google cloud function을 구현해 보기로 했습니다.

https://cloud.google.com/functions/docs/create-deploy-http-python?hl=ko#windows







빠른 시작: Python을 사용하여 HTTP Cloud 함수 만들기 및 배포  |  Cloud Functions 문서  |  Google Cloud


의견 보내기 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 빠른 시작: Python을 사용하여 HTTP Cloud 함수 만들기 및 배포 Python을 사용하여 HTTP Cloud 함수 만들기


cloud.google.com




이 글은 위 링크의 내용에서 발췌하고 따라하기를 했음을 밝혀 둡니다.


Firebase realtime database 에 저장

저장된 결과


이 그림이 나의 목표 입니다. 서버가 없기 때문에 payapp의 API을 호출했을 때, 상대방 서버에서 feedback url로 보내주는 결과를 수신해서 database에 저장하는 것이 목표입니다.

이걸 준비 하는 과정은 가이드에 따라서 잘 따라가기를 하면 되기는 했습니다. 단지, 이해가 어려웠던 부분이 있다면
1. cloud client 설치
2. project 설정 ( 혹시 console.google 에 project 가 여러 개 있을 때 project을 선택해야 하는 문제)
3. 해당 project의 결제 계정 연결
4. function 연동 설정

이런 정도라고 볼 수 있습니다. 한번 해 보고 나서 정리를 하는 것이라, 쉽게 말하고 있기는 하지만, 이것 설정을 준비하는 동안만 3~4일 걸렸고, 어떻게 할까에 대한 고민을 여러 가지로 했습니다. 아무튼 이제 할 수 있습니다. 따라 하는 설명은
다음에 하도록 하겠습니다.





오늘의 이야기

https://github.com/ahmedmolawale/AndroidNanoHttpd



 


GitHub - ahmedmolawale/AndroidNanoHttpd: A sample android project to showcase the use of NanoHttpd in Android.


A sample android project to showcase the use of NanoHttpd in Android. - GitHub - ahmedmolawale/AndroidNanoHttpd: A sample android project to showcase the use of NanoHttpd in Android.


github.com




오늘은 번외 편으로 안드로이드를 이용한 웹서버 간략 구현에 대해서 알아볼까 합니다.  이 포스팅은 위 링크의 글을 참조하였음을 밝혀 둡니다. 


 


Gradle 추가


// nano HTTP 구현
implementation 'com.nanohttpd:nanohttpd:2.2.0'

이거 하나 추가 하면 끝입니다.  다음은 Local에서 WebServer로 사용할 코드를 구현해 보겠습니다. 



import android.content.Context
import android.os.StatFs
import android.util.Log
import androidx.core.content.ContextCompat
import fi.iki.elonen.NanoHTTPD
import java.io.File
import java.io.FileInputStream
import java.io.IOException
import java.net.Inet4Address
import java.net.NetworkInterface
import java.net.SocketException

class LocalWebserver(context: Context, port: Int) : NanoHTTPD(port) {

var context: Context? = context
val MIME_JAVASCRIPT = "text/javascript"
val MIME_CSS = "text/css"
val MIME_JPEG = "image/jpeg"
val MIME_PNG = "image/png"
val MIME_SVG = "image/svg+xml"
val MIME_JSON = "application/json"
val MIME_GIF = "image/gif"
val MIME_BMP = "image/bmp"
var mimeType = MIME_HTML
val folderName = "portfolio" //name of the folder holding the asset of the page you wanna load

val TAG = "LocalWebserver"

override fun serve(session: IHTTPSession?): Response? {
val uri = session?.uri
try {
when {
uri!!.endsWith(".js") -> {
mimeType = MIME_JAVASCRIPT
}
uri.endsWith(".css") -> {
mimeType = MIME_CSS
}
uri.endsWith(".html") -> {
mimeType = MIME_HTML
}
uri.endsWith(".jpeg") -> {
mimeType = MIME_JPEG
}
uri.endsWith(".png") -> {
mimeType = MIME_PNG
}
uri.endsWith(".jpg") -> {
mimeType = MIME_JPEG
}
uri.endsWith(".svg") -> {
mimeType = MIME_SVG
}
uri.endsWith(".bmp") -> {
mimeType = MIME_BMP
}
uri.endsWith(".gif") -> {
mimeType = MIME_GIF
}
uri.endsWith(".json") -> {
mimeType = MIME_JSON
}
}
} catch (e: Exception) {
Log.e(TAG, "MINE ERROR ... ${e.message}")
}
Log.e(TAG, "${getLocalIpAddress()} ${getStoragePath()}")
val root = "${getStoragePath()}${File.separator}"
var fis: FileInputStream? = null
val file = File(
root +
"${folderName}/${uri}"
)
try {
if (file.exists()) {
fis = FileInputStream(file);
}
} catch (ioe: IOException) {
Log.e("Httpd %s", ioe.toString())
}
return newFixedLengthResponse(
Response.Status.OK,
mimeType,
fis,
file.length()
)
}

private fun getStoragePath(): String? {
var path: String? = null
var space: Long = 0
val files: Array<File> = ContextCompat.getExternalFilesDirs(context!!, null)
// go through the options to choose one with more available storage capacity
for (f in files) {
val stat = StatFs(f.path)
val blockSize = stat.blockSizeLong
val totalBlocks = stat.blockCountLong

// check if storage capacity is more than the previous one
if (totalBlocks * blockSize > space) {
space = totalBlocks * blockSize
path = f.path
}
}
return path
}

private fun getLocalIpAddress(): String {
try {
val en = NetworkInterface.getNetworkInterfaces()
while (en.hasMoreElements()) {
val intf = en.nextElement()
val enumIpAddr = intf.inetAddresses
while (enumIpAddr.hasMoreElements()) {
val inetAddress = enumIpAddr.nextElement()
if (!inetAddress.isLoopbackAddress && inetAddress is Inet4Address) {
return inetAddress.getHostAddress().toString()
}
}
}
} catch (ex: SocketException) {
ex.printStackTrace()
}
return ""
}
}

코드는 참조한 링크의 내용을 그대로 구현 했습니다. 잘 모르는 부분도 있고 해서요. 코드를 보면 local IP을 확인하고, 해당 아이피를 기준으로 웹서버를 구동하는 그런 정도입니다.  추가적으로 해야 할 부분은 이제 화면에서 보여줄 html 코드를 구현해 보는 것인데, 


 


아직은 그럴 생각이 없기 때문에 그냥 이렇게 구현 하는 정도까지만 포스팅을 해 두겠습니다. 이제 실행을 해 볼까요?


 


웹서버 구동


var localWebserver = LocalWebserver(this@MainActivity, 8035)
localWebserver.start()

 이런 정도의 코드 구현으로 구현이 됩니다. 그러면 위 구현된 코드에서 getStoreagePath()에서 구해온 경로에 index.html을 등을 넣어 주면 호출이 되는 것을 알 수 있었습니다. 


 


구동된 로그 샘플



크롬 등으로 현재 테스트 중인 앱의 서버에 접속을 해 보고 그 실행 로그가 출력되는 것을 확인하였습니다.  지금은 내가 사용하는 스마트폰에 wifi debugging을 실행하고 앱을 빌드해서 실행해 본 것입니다.


 


이것으로 로컬 웹서버가 필요한 경우 구현해 볼 수 있을 것 같습니다.





오늘의 이야기



#스치니1000프로젝트 #재미 #행운기원 #Compose #Firebase

🎯 야 너 토요일마다 로또 확인하냐?
나도 맨날 "혹시나~" 하면서 봤거든 ㅋㅋ

근데 이제는 그냥 안 해
AI한테 맡겼어 🤖✨

그것도 구글 Gemini로다가!

그래서 앱 하나 만들었지
👉 "로또 예상번호 by Gemini" 🎱

AI가 분석해서 번호 딱! 뽑아줌
그냥 보고 참고만 하면 됨

재미로 해도 좋고…
혹시 모르는 거잖아? 😏


https://play.google.com/store/apps/details?id=com.billcorea.gptlotto1127




오늘의 이야기

앱을 만들어 수익을 얻는 방법 중 하나인 Admob 광고가 버전에 따른 중단 일정이 공지되고 있습니다.  잘 기억해 두었다가


playstore에 게시된 앱의 버전 패치에 참고하시길 바랄게요.


 


중단 일정 공지 내용


광고중단 일정



이 글은 아래 링크의 내용을 참조하였습니다. 


 


https://developers.google.com/admob/android/deprecation?source=ui&hl=ko 



 


지원 중단 및 일몰  |  Android  |  Google Developers


이 페이지는 Cloud Translation API를 통해 번역되었습니다. Switch to English 지원 중단 및 일몰 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 새로운 주요 Google 모


developers.google.com




 





오늘의 이야기


#스하리1000명프로젝트

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

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

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

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





오늘의 이야기

진행률

앱을 구현하다 보면 간혹은 화면을 전환하는 동안에 진행률(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




오늘의 이야기

Cloud Function 서버를 보유 하지(Serverless) 않고 서버가 있는 것처럼 업무 구현을 하고 싶습니다. 개발을 하면서 데이터 베이스와 스토리지를  firebase을 활용하고 있다면 cloud function 도 배워서 준비를 해야 할 ...