SMS token faill to responce - android

I've the below code trying to work with SMS Token, i got the token code, but once i sent it in SMS I got no responce!
MainActivity.kt
package com.sms
import android.app.PendingIntent
import android.content.Intent
import android.os.Bundle
import android.telephony.SmsManager
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.google.android.gms.auth.api.phone.SmsRetriever
import com.google.android.gms.auth.api.phone.SmsRetrieverClient
import com.sms.ui.theme.SmsTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// val smsManager: SmsManager = SmsManager.getDefault()
val smsManager: SmsManager = getSystemService(SmsManager::class.java)
val appSmsToken = smsManager.createAppSpecificSmsToken(createSmsTokenPendingIntent())
print(appSmsToken)
setContent {
SmsTheme {
// A surface container using the 'background' color from the theme
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
Greeting(appSmsToken)
}
}
}
}
private fun createSmsTokenPendingIntent(): PendingIntent? {
return PendingIntent.getActivity(
this, 1234,
Intent(this, SmsTokenResultVerificationActivity::class.java),
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT) // setting the mutability flag
}
}
#Composable
fun Greeting(name: String) {
Text(text = "Token is: $name")
}
#Preview(showBackground = true)
#Composable
fun DefaultPreview() {
SmsTheme {
Greeting("Android")
}
}
And SmsTokenResultVerificationActivity.kt is:
package com.sms
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.sms.ui.theme.SmsTheme
class SmsTokenResultVerificationActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
SmsTheme {
// A surface container using the 'background' color from the theme
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
Greeting("Welcome")
}
}
}
}
#Composable
fun Greeting(name: String) {
Text(text = "hi: $name")
}
}

As another option, I found using SMS retriever API in Android could solve it, but I still interested in using the Token as i want my app to depend on the user device token rather than the app token.
BaseApplication.kt
package com.shishirthedev.smsretriverapi
import android.app.Application
import android.util.Log
import android.widget.Toast
class BaseApplication : Application() {
private val TAG = BaseApplication::class.java.simpleName
override fun onCreate() {
super.onCreate()
// Generate Hash Key >>>>>
val appSignatureHashHelper = AppSignatureHashHelper(this)
Log.e(TAG, "HashKey: " + appSignatureHashHelper.appSignatures[0])
var msg = "HashKey: " + appSignatureHashHelper.appSignatures[0]
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
// Storing data into SharedPreferences
val sharedPreferences = getSharedPreferences("MySharedPref", MODE_PRIVATE)
// Creating an Editor object to edit(write to the file)
val myEdit = sharedPreferences.edit()
// Storing the key and its value as the data fetched from edittext
myEdit.putString("token", appSignatureHashHelper.appSignatures[0])
// myEdit.putInt("age", age.getText().toString().toInt())
// Once the changes have been made,
// we need to commit to apply those changes made,
// otherwise, it will throw an error
myEdit.commit()
}
}
AppSignatureHashHelper.kt
package com.shishirthedev.smsretriverapi
import android.annotation.TargetApi
import android.content.Context
import android.content.ContextWrapper
import android.content.pm.PackageManager
import android.util.Base64
import android.util.Log
import java.nio.charset.StandardCharsets
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.util.*
class AppSignatureHashHelper(context: Context?) :
ContextWrapper(context) {// Get all package details
/**
* Get all the app signatures for the current package
*
* #return
*/
val appSignatures: ArrayList<String>
get() {
val appSignaturesHashs = ArrayList<String>()
try {
// Get all package details
val packageName = packageName
val packageManager = packageManager
val signatures = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
packageManager.getPackageInfo(
packageName,
PackageManager.GET_SIGNING_CERTIFICATES
).signingInfo.apkContentsSigners
} else {
TODO("VERSION.SDK_INT < P")
}
for (signature in signatures) {
val hash = hash(packageName, signature.toCharsString())
if (hash != null) {
appSignaturesHashs.add(String.format("%s", hash))
}
}
} catch (e: Exception) {
Log.e(TAG, "Package not found", e)
}
return appSignaturesHashs
}
companion object {
val TAG = AppSignatureHashHelper::class.java.simpleName
private const val HASH_TYPE = "SHA-256"
const val NUM_HASHED_BYTES = 9
const val NUM_BASE64_CHAR = 11
#TargetApi(19)
private fun hash(packageName: String, signature: String): String? {
val appInfo = "$packageName $signature"
try {
val messageDigest = MessageDigest.getInstance(HASH_TYPE)
messageDigest.update(appInfo.toByteArray(StandardCharsets.UTF_8))
var hashSignature = messageDigest.digest()
// truncated into NUM_HASHED_BYTES
hashSignature = Arrays.copyOfRange(hashSignature, 0, NUM_HASHED_BYTES)
// encode into Base64
var base64Hash =
Base64.encodeToString(hashSignature, Base64.NO_PADDING or Base64.NO_WRAP)
base64Hash = base64Hash.substring(0, NUM_BASE64_CHAR)
return base64Hash
} catch (e: NoSuchAlgorithmException) {
Log.e(TAG, "No Such Algorithm Exception", e)
}
return null
}
}
}
SMSReceiver.kt`
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.google.android.gms.auth.api.phone.SmsRetriever
import com.google.android.gms.common.api.CommonStatusCodes
import com.google.android.gms.common.api.Status
import java.util.regex.Pattern
class SMSReceiver : BroadcastReceiver() {
private var otpListener: OTPReceiveListener? = null
fun setOTPListener(otpListener: OTPReceiveListener?) {
this.otpListener = otpListener
}
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == SmsRetriever.SMS_RETRIEVED_ACTION) {
val extras = intent.extras
val status = extras!![SmsRetriever.EXTRA_STATUS] as Status?
when (status!!.statusCode) {
CommonStatusCodes.SUCCESS -> {
val sms = extras[SmsRetriever.EXTRA_SMS_MESSAGE] as String?
sms?.let {
// val p = Pattern.compile("[0-9]+") check a pattern with only digit
val p = Pattern.compile("\\d+")
val m = p.matcher(it)
if (m.find()) {
val otp = m.group()
if (otpListener != null) {
otpListener!!.onOTPReceived(otp)
}
}
}
}
}
}
}
interface OTPReceiveListener {
fun onOTPReceived(otp: String?)
}
}
MainActivity.kt
package com.shishirthedev.smsretriverapi
import android.content.IntentFilter
import android.os.Bundle
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.google.android.gms.auth.api.phone.SmsRetriever
import com.shishirthedev.smsretriverapi.SMSReceiver.OTPReceiveListener
class MainActivity : AppCompatActivity() {
private var intentFilter: IntentFilter? = null
private var smsReceiver: SMSReceiver? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
var token = findViewById<TextView>(R.id.textView)
// Retrieving the value using its keys the file name
// must be same in both saving and retrieving the data
val sh = getSharedPreferences("MySharedPref", MODE_PRIVATE)
// The value will be default as empty string because for
// the very first time when the app is opened, there is nothing to show
val t = sh.getString("token", "")
// val a = sh.getInt("age", 0)
// We can then use the data
token.text = t
// age.setText(a.toString())
// Init Sms Retriever >>>>
initSmsListener()
initBroadCast()
}
private fun initBroadCast() {
intentFilter = IntentFilter(SmsRetriever.SMS_RETRIEVED_ACTION)
smsReceiver = SMSReceiver()
smsReceiver?.setOTPListener(object : OTPReceiveListener {
override fun onOTPReceived(otp: String?) {
showToast("OTP Received: $otp")
}
})
}
private fun initSmsListener() {
val client = SmsRetriever.getClient(this)
client.startSmsRetriever()
}
override fun onResume() {
super.onResume()
registerReceiver(smsReceiver, intentFilter)
}
override fun onPause() {
super.onPause()
unregisterReceiver(smsReceiver)
}
override fun onDestroy() {
super.onDestroy()
smsReceiver = null
}
private fun showToast(msg: String?) {
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
}
}
manifest
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.shishirthedev.smsretriverapi">
<application
android:name=".BaseApplication"
android:allowBackup="true"
android:icon="#mipmap/ic_launcher"
android:label="#string/app_name"
android:roundIcon="#mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="#style/Theme.SmsRetriverAPi">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
build.gradle (Module)
plugins {
id 'com.android.application'
id 'kotlin-android'
}
android {
compileSdkVersion 30
buildToolsVersion "29.0.3"
defaultConfig {
applicationId "com.shishirthedev.smsretriverapi"
minSdkVersion 21
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.2.1'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
implementation 'com.google.android.gms:play-services-auth:19.0.0'
implementation 'com.google.android.gms:play-services-auth-api-phone:17.5.0'
}
``build.gradle (Project)`
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = "1.4.10"
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
Credit goes to
SHISHIR

Related

Problem with TensorFlow Lite detection Canvas "Custom view Draw is missing constructor"

I'm having troubles with a class inside "utils" package on my Kotlin code
Classic issues I guess, but I have been looking for solutions on forums and nothing works, I'm just trying the bindPreview method with CameraX
This is my main activity
import android.annotation.SuppressLint
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.util.Size
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 androidx.databinding.DataBindingUtil
import androidx.lifecycle.LifecycleOwner
import com.fablab.tensorflowcamerax.databinding.ActivityMainBinding
import com.fablab.tensorflowcamerax.utils.Draw
import com.google.common.util.concurrent.ListenableFuture
import com.google.mlkit.common.model.LocalModel
import com.google.mlkit.vision.common.InputImage
import com.google.mlkit.vision.objects.ObjectDetection
import com.google.mlkit.vision.objects.ObjectDetector
import com.google.mlkit.vision.objects.custom.CustomObjectDetectorOptions
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var objectDetector: ObjectDetector
private lateinit var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
val cameraProvider = cameraProviderFuture.get()
bindPreview(cameraProvider = cameraProvider)
}, ContextCompat.getMainExecutor(this))
val localModel = LocalModel.Builder()
.setAssetFilePath("object_detection.tflite")
// \\assets\object_detection.tflite
.build()
val customObjectDetectorOptions = CustomObjectDetectorOptions.Builder(localModel)
.setDetectorMode(CustomObjectDetectorOptions.STREAM_MODE)
.enableClassification()
.setClassificationConfidenceThreshold(0.5f)
.setMaxPerObjectLabelCount(3)
.build()
objectDetector = ObjectDetection.getClient(customObjectDetectorOptions)
}
#SuppressLint("UnsafeOptInUsageError")
private fun bindPreview (cameraProvider: ProcessCameraProvider) {
val preview = Preview.Builder().build()
val cameraSelector = CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
.build()
preview.setSurfaceProvider(binding.previewView.surfaceProvider)
val imageAnalysis = ImageAnalysis.Builder()
.setTargetResolution(Size(1200,720))
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
imageAnalysis.setAnalyzer(ContextCompat.getMainExecutor(this)
) { imageProxy ->
val rotationDegreesValue = imageProxy.imageInfo.rotationDegrees
val image = imageProxy.image
if (image != null) {
val processImage = InputImage.fromMediaImage(image, rotationDegreesValue)
objectDetector
.process(processImage)
.addOnSuccessListener { objects ->
for (i in objects) {
if (binding.parentLayout.childCount > 1) binding.parentLayout.removeViewAt(1)
val context = binding.root.context
val element = Draw(
context = context,
rect = i.boundingBox,
text = i.labels.firstOrNull()?.text ?: "Undefined"
)
binding.parentLayout.addView(element)
}
imageProxy.close()
}.addOnFailureListener {
Log.v("MainActivity", "Error - ${it.message}")
imageProxy.close()
}
}
}
cameraProvider.bindToLifecycle(this as LifecycleOwner, cameraSelector, preview, imageAnalysis)
}
}
My custom view "Draw"
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.view.View
class Draw(context: Context?, var rect: Rect, var text: String): View(context) {
lateinit var boundaryPaint: Paint
lateinit var textPaint: Paint
init {
init()
}
private fun init(){
boundaryPaint = Paint()
boundaryPaint.color = Color.BLACK
boundaryPaint.strokeWidth = 10f
boundaryPaint.style = Paint.Style.STROKE
textPaint = Paint()
textPaint.color = Color.BLACK
textPaint.textSize = 50f
textPaint.style = Paint.Style.FILL
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
canvas?.drawText(
text,
rect.centerX().toFloat(),
rect.centerY().toFloat(),
textPaint)
canvas?.drawRect(
rect.left.toFloat(),
rect.top.toFloat(),
rect.right.toFloat(),
rect.bottom.toFloat(),
boundaryPaint
)
}
}
And the gradle (I know there are deprecated dependencies, I deliveratedly choose them because compile and work with the method that I'm replicating)
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'kotlin-kapt'
}
android {
compileSdk 33
//buildToolsVersion "30.0.1"
defaultConfig {
minSdk 21
//noinspection ExpiredTargetSdkVersion
targetSdk 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
aaptOptions {
noCompress "tflite"
}
dataBinding {
enabled = true
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.2.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
// Object detection
implementation 'com.google.mlkit:object-detection-custom:16.3.0'
// CameraX
implementation "androidx.camera:camera-camera2:1.3.0-alpha02"
implementation "androidx.camera:camera-lifecycle:1.3.0-alpha02"
implementation "androidx.camera:camera-view:1.3.0-alpha02"
}
The "Draw" class is inside utils package and the IDE gives a warning that says:
"Custom view Draw is missing constructor used by tools: (Context) or (Context,AttributeSet) or (Context,AttributeSet,int)"
I tried with and without declaring context value on the objectDetector for, and nothing happens
I also tried converting to secondary constuctor, but did not working either
The project compiles, when I build an APK and execute the app, I got access to native camera (giving manually hardware permission, but I will work on that detail later) but nothing about object detection and canvas drawing.
Please guide me to find a solution without changing again the dependencies version, and I need to know if the assetFilePath is correct too.
Thank you.

etTodoTitle.text not being accessed?

when the app is ran the Edit Text's text is not being accessed and passed for some reason instead this is what it shows in the recycler view and my room database:
https://imgur.com/a/7XjDodL
Sorry if this question doesn't make much sense I didn't really know how to phrase it. Any help is greatly appreciated.
Activity Main
package com.example.todoit
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.todoit.data.Todo
import com.example.todoit.data.TodoDataBase
import com.example.todoit.databinding.ActivityMainBinding
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() {
private lateinit var todoAdapter: TodoAdapter
private lateinit var todoDB: TodoDataBase
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
todoDB = TodoDataBase.getInstance(this)
todoAdapter = TodoAdapter(mutableListOf())
val rvTodoItems = binding.rvTodoItems
val btnAddTodo = binding.btnAddTodo
val btnDeleteTodo = binding.btnDeleteTodo
rvTodoItems.layoutManager = LinearLayoutManager(this)
rvTodoItems.adapter = todoAdapter
btnAddTodo.setOnClickListener {
val todoTitle = binding.etTodoTitle.toString()
if (todoTitle.isNotEmpty()) {
val todo = Todo(null, todoTitle, false)
GlobalScope.launch {
todoDB.todoDao().insertAll(todo)
}
todoAdapter.addTodo(todo)
binding.etTodoTitle.text.clear()
Toast.makeText(this, "Successfully written data", Toast.LENGTH_LONG).show()
} else {
Toast.makeText(this, "There was an error while writing data", Toast.LENGTH_LONG)
.show()
}
}
btnDeleteTodo.setOnClickListener {
todoAdapter.deleteDoneTodos()
Toast.makeText(this, "Selected Todo(s) Deleted", Toast.LENGTH_LONG).show()
}
}
}
TodoAdapter
package com.example.todoit
import android.graphics.Paint.STRIKE_THRU_TEXT_FLAG
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.example.todoit.data.Todo
import kotlinx.android.synthetic.main.item_todo.view.*
class TodoAdapter(
private val todos: MutableList<Todo>,
) : RecyclerView.Adapter<TodoAdapter.TodoViewHolder>() {
class TodoViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TodoViewHolder {
return TodoViewHolder(
LayoutInflater.from(parent.context).inflate(
R.layout.item_todo,
parent,
false
)
)
}
fun addTodo(todo: Todo) {
todos.add(todo)
notifyItemInserted(todos.size - 1)
}
fun deleteDoneTodos() {
todos.removeAll { todo ->
todo.isChecked
}
notifyDataSetChanged()
}
private fun toggleStrikeThrough(tvTodoTitle: TextView, isChecked: Boolean) {
if(isChecked) {
tvTodoTitle.paintFlags = tvTodoTitle.paintFlags or STRIKE_THRU_TEXT_FLAG
} else {
tvTodoTitle.paintFlags = tvTodoTitle.paintFlags and STRIKE_THRU_TEXT_FLAG.inv()
}
}
override fun onBindViewHolder(holder: TodoViewHolder, position: Int) {
val curTodo = todos[position]
holder.itemView.apply {
tvTodoTitle.text = curTodo.title
cbDone.isChecked = curTodo.isChecked
toggleStrikeThrough(tvTodoTitle, curTodo.isChecked)
cbDone.setOnCheckedChangeListener { _, isChecked ->
toggleStrikeThrough(tvTodoTitle, isChecked)
curTodo.isChecked = !curTodo.isChecked
}
}
}
override fun getItemCount(): Int {
return todos.size
}
}
Gradle(app)
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id "kotlin-android-extensions"
}
apply plugin: 'kotlin-kapt'
android {
compileSdk 32
defaultConfig {
applicationId "com.example.todoit"
minSdk 21
targetSdk 32
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildFeatures{
viewBinding = true
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
//ROOM
def roomVersion = "2.4.2"
implementation "androidx.room:room-ktx:$roomVersion"
kapt "androidx.room:room-compiler:$roomVersion"
// Navigation Component
implementation 'androidx.navigation:navigation-fragment-ktx:2.2.2'
implementation 'androidx.navigation:navigation-ui-ktx:2.2.2'
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.6.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
// Lifecycle components
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
implementation "androidx.lifecycle:lifecycle-common-java8:2.2.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
// Kotlin components
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.72"
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.5"
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.5"
}
If you need anymore code then please let me know.
You might want to change the following assignment:
val todoTitle = binding.etTodoTitle.toString()
to this:
val todoTitle = binding.etTodoTitle.text.toString()
Otherwise any new Todo objects created by the onClick will store the View ID of the editText instead of the value you typed inside it...

Jetpack Compose: Displaying data in compose using MVVM

Need a bit of help on why data from viewmodel is not shown in the composable function MainContent. I tried to use MVVM style with coroutine but without DI which I think will be easier but somehow, I could not get it to work.
The viewmodel is working as the log.d is showing the correct data from server but somehow, I could not get it to display in
Text(text = viewModel.posts[it].phrase)
Any help will be greatly appreciated. The github link for this program is in https://github.com/somaria/LearnChnCompose
package com.gamecrawl.learnchncompose
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.ViewModel
import com.gamecrawl.learnchncompose.ui.theme.LearnChnComposeTheme
import io.ktor.client.*
import io.ktor.client.engine.android.*
import io.ktor.client.features.*
import io.ktor.client.features.json.*
import io.ktor.client.features.json.serializer.*
import io.ktor.client.features.logging.*
import io.ktor.client.request.*
import io.ktor.http.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val viewModel: MainViewModel by viewModels()
setContent {
LearnChnComposeTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
MainContent(viewModel)
}
}
}
}
}
class MainViewModel : ViewModel() {
private var _posts = mutableListOf(Post("12", "test phrase", true))
var posts get() = _posts; set(value) {
_posts = value
}
init {
CoroutineScope(Dispatchers.IO).launch {
_posts = KtorClient.httpClient.get("https://learnchn.herokuapp.com/") {
header("Content-Type", "application/json")
}
Log.d("HomeViewModel", "init: ${_posts[1].phrase}")
Log.d("HomeViewModel", "init: ${_posts[1].id}")
}
}
fun addPost(post: Post) {
CoroutineScope(Dispatchers.IO).launch {
val addedpost: Post = KtorClient.httpClient.post("https://learnchn.herokuapp.com/add") {
header("Content-Type", "application/json")
body = post
}
}
}
}
#Composable
fun MainContent(viewModel: MainViewModel) {
Column {
LazyColumn {
items(viewModel.posts.size) {
Text(text = viewModel.posts[it].phrase)
}
}
Button(onClick = {
viewModel.addPost(Post("test", "adding post 222", true))
}) {
Text(text = "Add Post")
}
}
}
#Serializable
data class Post(
val id: String,
val phrase: String,
val published: Boolean
)
object KtorClient {
val json = Json {
encodeDefaults = true
ignoreUnknownKeys = true
isLenient = true
}
val httpClient = HttpClient(Android) {
install(HttpTimeout) {
socketTimeoutMillis = 200000
requestTimeoutMillis = 200000
connectTimeoutMillis = 200000
}
install(Logging) {
logger = object : Logger {
override fun log(message: String) {
Log.d("TAG", "log: $message")
}
}
}
install(JsonFeature) {
serializer = KotlinxSerializer(json)
}
defaultRequest {
contentType(ContentType.Application.Json)
accept(ContentType.Application.Json)
}
}
}
The data type of the posts is a MutableList<Post>. This means that changes to this variable will not cause the function to recompose. When the UI is loaded, then the variable does not have any data, since you fetch the data in an asynchronous coroutine. However, when the variable is updated, the UI is not recomposed.
To fix this issue, you must declare _posts to be a MutableState<List<Post>> from the compose library instead. Reconfigure your ViewModel in the following way:
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
class MainViewModel : ViewModel() {
private val _posts = mutableStateOf(listOf<Post>()) // <- requires init value
val posts: State<List<Post>> = _posts // <- keep both variables immutable 'val'
/* always expose the immutable form of State */
init {
CoroutineScope(Dispatchers.IO).launch {
/* _posts.value is used now due to the datatype change */
_posts.value = KtorClient.httpClient.get("https://learnchn.herokuapp.com/") {
header("Content-Type", "application/json")
}
Log.d("HomeViewModel", "init: ${_posts.value[1].phrase}")
Log.d("HomeViewModel", "init: ${_posts.value[1].id}")
}
}
fun addPost(post: Post) {
CoroutineScope(Dispatchers.IO).launch {
val addedpost: Post = KtorClient.httpClient.post("https://learnchn.herokuapp.com/add") {
header("Content-Type", "application/json")
body = post
}
}
}
}
Now since your public posts variable is of type State<T>, you need to make changes to your composable function:
#Composable
fun MainContent(viewModel: MainViewModel) {
val posts = viewModel.posts.value // <- grab the value of the state variable.
/* The function will recompose whenever there's a change in posts */
Column {
LazyColumn {
items(posts.size) {
Text(text = posts[it].phrase)
}
}
Button(onClick = {
viewModel.addPost(Post("test", "adding post 222", true))
}) {
Text(text = "Add Post")
}
}
}
This should help your issue.

Room database not working-2 Errors See Description

When the app is ran, the following errors occur:
Errors:
C:\Users\John\AndroidStudioProjects\Todoit 2\app\build\tmp\kapt3\stubs\debug\com\example\todoit\data\TodoDao.java:11: error: Not sure how to handle insert method's return type.
public abstract java.lang.Object addTodo(#org.jetbrains.annotations.NotNull()
C:\Users\John\AndroidStudioProjects\Todoit 2\app\build\tmp\kapt3\stubs\debug\com\example\todoit\data\TodoDao.java:13: error: Type of the parameter must be a class annotated with #Entity or a collection/array of it.
kotlin.coroutines.Continuation<? super kotlin.Unit> continuation);
Any help would be greatly appreciated.
Code:
Todo
package com.example.todoit.data
import androidx.room.Entity
import androidx.room.PrimaryKey
#Entity(tableName = "todo_data")
data class Todo (
#PrimaryKey val id: Int,
val title: String,
var isChecked: Boolean = false
)
TodoDao
package com.example.todoit.data
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
#Dao
interface TodoDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun addTodo(todo: Todo)
#Query("SELECT * FROM todo_data ORDER BY id ASC")
fun readAllData(): LiveData<List<Todo>>
}
TodoDataBase
package com.example.todoit.data
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
#Database(entities = [Todo::class],version = 1, exportSchema = false)
abstract class TodoDataBase: RoomDatabase() {
abstract fun todoDao(): TodoDao
companion object{
#Volatile
private var INSTANCE: TodoDataBase? = null
fun getDataBase(context: Context):TodoDataBase{
val tempInstance = INSTANCE
if(tempInstance != null){
return tempInstance
}
synchronized(this){
val instance = Room.databaseBuilder(
context.applicationContext,
TodoDataBase::class.java,
"todo_database"
).build()
INSTANCE = instance
return instance
}
}
}
}
TodoRepository
package com.example.todoit.data
import androidx.lifecycle.LiveData
class TodoRepository(private val todoDao:TodoDao) {
val readAllData: LiveData<List<Todo>> = todoDao.readAllData()
suspend fun addTodo(todo:Todo) {
todoDao.addTodo(todo)
}
}
TodoViewModel
package com.example.todoit.data
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class TodoViewModel(application: Application) : AndroidViewModel(application) {
private val readAllData: LiveData<List<Todo>>
private val repository: TodoRepository
init {
val todoDao = TodoDataBase.getDataBase(application).todoDao()
repository = TodoRepository(todoDao)
readAllData = repository.readAllData
}
fun addTodoToDataBase(todo: Todo) {
viewModelScope.launch(Dispatchers.IO) {
repository.addTodo(todo)
}
}
}
MainActivity
package com.example.todoit
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.todoit.data.Todo
import com.example.todoit.data.TodoViewModel
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
private lateinit var todoAdapter: TodoAdapter
private lateinit var todoViewModel: TodoViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
todoViewModel = ViewModelProvider(this).get(TodoViewModel::class.java)
todoAdapter = TodoAdapter(mutableListOf())
rvTodoItems.layoutManager = LinearLayoutManager(this)
rvTodoItems.adapter = todoAdapter
btnAddTodo.setOnClickListener {
val todoTitle = etTodoTitle.text.toString()
if (todoTitle.isNotEmpty()) {
val todo = Todo(0,todoTitle,false)
etTodoTitle.text.clear()
insertDataToDataBase(todo)
todoAdapter.addTodo(todo)
}
btnDeleteTodo.setOnClickListener {
todoAdapter.deleteDoneTodos()
}
}}
private fun insertDataToDataBase(todo: Todo) {
val todoTitle = etTodoTitle.text.toString()
if(todoTitle.isNotEmpty()) {
//Add data to database
todoViewModel.addTodoToDataBase(todo)
}
}
}
TodoAdapter
package com.example.todoit
import android.graphics.Paint.STRIKE_THRU_TEXT_FLAG
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.example.todoit.data.Todo
import kotlinx.android.synthetic.main.item_todo.view.*
class TodoAdapter(
private val todos: MutableList<Todo>,
) : RecyclerView.Adapter<TodoAdapter.TodoViewHolder>() {
class TodoViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TodoViewHolder {
return TodoViewHolder(
LayoutInflater.from(parent.context).inflate(
R.layout.item_todo,
parent,
false
)
)
}
fun addTodo(todo: Todo) {
todos.add(todo)
notifyItemInserted(todos.size - 1)
}
fun deleteDoneTodos() {
todos.removeAll { todo ->
todo.isChecked
}
notifyDataSetChanged()
}
private fun toggleStrikeThrough(tvTodoTitle: TextView, isChecked: Boolean) {
if (isChecked) {
tvTodoTitle.paintFlags = tvTodoTitle.paintFlags or STRIKE_THRU_TEXT_FLAG
} else {
tvTodoTitle.paintFlags = tvTodoTitle.paintFlags and STRIKE_THRU_TEXT_FLAG.inv()
}
}
override fun onBindViewHolder(holder: TodoViewHolder, position: Int) {
val curTodo = todos[position]
holder.itemView.apply {
tvTodoTitle.text = curTodo.title
cbDone.isChecked = curTodo.isChecked
toggleStrikeThrough(tvTodoTitle, curTodo.isChecked)
cbDone.setOnCheckedChangeListener { _, isChecked ->
toggleStrikeThrough(tvTodoTitle, isChecked)
curTodo.isChecked = !curTodo.isChecked
}
}
}
override fun getItemCount(): Int {
return todos.size
}
}
Gradle(Module)
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id "kotlin-android-extensions"
}
apply plugin: 'kotlin-kapt'
android {
compileSdk 32
defaultConfig {
applicationId "com.example.todoit"
minSdk 21
targetSdk 32
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
//ROOM
def roomVersion = "2.4.2"
implementation 'androidx.room:room-ktx:2.2.1'
kapt "androidx.room:room-compiler:2.2.1"
// Navigation Component
implementation 'androidx.navigation:navigation-fragment-ktx:2.2.2'
implementation 'androidx.navigation:navigation-ui-ktx:2.2.2'
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.6.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
// Lifecycle components
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
implementation "androidx.lifecycle:lifecycle-common-java8:2.2.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
// Kotlin components
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.72"
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.5"
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.5"
}
Gradle(Project)
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '7.2.0' apply false
id 'com.android.library' version '7.2.0' apply false
id 'org.jetbrains.kotlin.android' version '1.5.30' apply false
}
task clean(type: Delete) {
delete rootProject.buildDir
}
I think you should set the dependencies for room as follows:
def roomVersion = "2.4.2"
implementation "androidx.room:room-ktx:$roomVersion"
kapt "androidx.room:room-compiler:$roomVersion"

Ignoring header X-Firebase-Locale because its value was null. (cannot login and signup with authentication in firebase)

I got a problem of cannot signup and also login (only able to sign up twice and after that couldn't do that anymore). Below are my codes:
this is login activity:
package com.example.mae_assignment
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Patterns
import android.widget.Button
import android.widget.Toast
import com.google.android.material.textfield.TextInputEditText
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.FirebaseUser
class Login : AppCompatActivity() {
private lateinit var auth : FirebaseAuth
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.login)
auth = FirebaseAuth.getInstance()
val btnSignUp = findViewById<Button>(R.id.btnSignUp)
btnSignUp.setOnClickListener{
startActivity(Intent(this, SignUp::class.java))
finish()
}
val btnLogin = findViewById<Button>(R.id.btnSignIn)
btnLogin.setOnClickListener{
doLogin()
}
}
private fun doLogin() {
val password = findViewById<TextInputEditText>(R.id.txtPwd)
val email = findViewById<TextInputEditText>(R.id.txtUE)
if (email.text.toString().isEmpty()){
email.error = "Please enter email."
email.requestFocus()
return
}
if (!Patterns.EMAIL_ADDRESS.matcher(email.text.toString()).matches()){
email.error = "Please enter email."
email.requestFocus()
return
}
if (password.text.toString().isEmpty()){
password.error = "Please enter password."
password.requestFocus()
return
}
auth.createUserWithEmailAndPassword(email.text.toString().trim(), password.text.toString().trim())
.addOnCompleteListener(this) { task->
if (task.isSuccessful){
val user =auth.currentUser
updateUI(user)
}else{
val myToast = Toast.makeText(baseContext, "Login failed.", Toast.LENGTH_SHORT)
myToast.show()
updateUI(null)
}
}
}
public override fun onStart() {
super.onStart()
val currentUser = auth.currentUser
updateUI(currentUser)
}
private fun updateUI(currentUser:FirebaseUser?){
if(currentUser != null){
startActivity(Intent(this, Home::class.java))
val myToast = Toast.makeText(baseContext, "Login successful.", Toast.LENGTH_SHORT)
myToast.show()
} else {
val myToast = Toast.makeText(baseContext, "Login failed.", Toast.LENGTH_SHORT)
myToast.show()
}
}
}
this is signup activity:
package com.example.mae_assignment
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Patterns
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import com.google.android.material.textfield.TextInputEditText
import com.google.firebase.auth.FirebaseAuth
class SignUp : AppCompatActivity() {
private lateinit var auth : FirebaseAuth
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.signup)
auth = FirebaseAuth.getInstance()
val btnCreateAcc = findViewById<Button>(R.id.btnCreateAccount)
btnCreateAcc.setOnClickListener{
signup()
}
}
private fun signup(){
val txtpassword = findViewById<TextInputEditText>(R.id.txtpassword)
val txtemail = findViewById<TextInputEditText>(R.id.txtemail)
if (txtemail.text.toString().isEmpty()){
txtemail.error = "Please enter email."
txtemail.requestFocus()
return
}
if (!Patterns.EMAIL_ADDRESS.matcher(txtemail.text.toString()).matches()){
txtemail.error = "Please enter email."
txtemail.requestFocus()
return
}
if (txtpassword.text.toString().isEmpty()){
txtpassword.error = "Please enter password."
txtpassword.requestFocus()
return
}
auth.createUserWithEmailAndPassword(txtemail.text.toString().trim(), txtpassword.text.toString().trim())
.addOnCompleteListener(this) { task->
if (task.isSuccessful){
startActivity(Intent(this, Login::class.java))
finish()
}else{
val myToast = Toast.makeText(baseContext, "Sign up failed.", Toast.LENGTH_SHORT)
myToast.show()
}
}
}
}
package com.example.mae_assignment
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Patterns
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import com.google.android.material.textfield.TextInputEditText
import com.google.firebase.auth.FirebaseAuth
class SignUp : AppCompatActivity() {
private lateinit var auth : FirebaseAuth
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.signup)
auth = FirebaseAuth.getInstance()
val btnCreateAcc = findViewById<Button>(R.id.btnCreateAccount)
btnCreateAcc.setOnClickListener{
signup()
}
}
private fun signup(){
val txtpassword = findViewById<TextInputEditText>(R.id.txtpassword)
val txtemail = findViewById<TextInputEditText>(R.id.txtemail)
if (txtemail.text.toString().isEmpty()){
txtemail.error = "Please enter email."
txtemail.requestFocus()
return
}
if (!Patterns.EMAIL_ADDRESS.matcher(txtemail.text.toString()).matches()){
txtemail.error = "Please enter email."
txtemail.requestFocus()
return
}
if (txtpassword.text.toString().isEmpty()){
txtpassword.error = "Please enter password."
txtpassword.requestFocus()
return
}
auth.createUserWithEmailAndPassword(txtemail.text.toString().trim(), txtpassword.text.toString().trim())
.addOnCompleteListener(this) { task->
if (task.isSuccessful){
startActivity(Intent(this, Login::class.java))
finish()
}else{
val myToast = Toast.makeText(baseContext, "Sign up failed.", Toast.LENGTH_SHORT)
myToast.show()
}
}
}
}
And this is the gradle build:
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = "1.5.0"
repositories {
google()
mavenCentral()
}
dependencies {
classpath "com.android.tools.build:gradle:4.2.1"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.google.gms:google-services:4.3.8'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
mavenCentral()
jcenter() // Warning: this repository is going to shut down soon
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
this is gradle build (app):
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'com.google.gms.google-services'
}
android {
compileSdkVersion 30
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "com.example.mae_assignment"
minSdkVersion 16
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'com.google.firebase:firebase-auth:21.0.1'
implementation 'com.firebaseui:firebase-ui-auth:6.4.0'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
implementation platform('com.google.firebase:firebase-bom:28.0.1')
implementation 'com.google.firebase:firebase-analytics-ktx'
}
Please help to solve this TQVM. I have been doing this for like 2 weeks, sigh.

Categories

Resources