I have had many errors and while searching around I have received many different solutions that confused me and thus I man making this post to search for one final answer. So as the title states, I am trying to make my app get the HTML code of the website link that I feed it and will then sniff out information from that to set into my EditText. I have tried searching around, and a solution I received and changed a little was to use this Utility class
import androidx.appcompat.app.AppCompatActivity
import java.io.BufferedReader
import java.io.InputStreamReader
import java.net.MalformedURLException
import java.net.URL
object ContentScrapper {
fun getHTMLData(activity: AppCompatActivity,url: String, scrapListener: ScrapListener) {
Thread(Runnable {
val google: URL?
val `in`: BufferedReader?
var input: String?
val stringBuffer = StringBuffer()
try {
google = URL(url)
`in` = BufferedReader(InputStreamReader(google.openStream()))
while (true) {
if (`in`.readLine().also { input = it } == null)
break
stringBuffer.append(input)
}
`in`.close()
activity.runOnUiThread {
scrapListener.onResponse(stringBuffer.toString())
}
} catch (e: MalformedURLException) {
e.printStackTrace()
activity.runOnUiThread {
scrapListener.onResponse(null)
}
}
}).start()
}
interface ScrapListener {
fun onResponse(html: String?)
}
}
Like this in my Main Activity
ContentScrapper.getHTMLData(this,"http://google.com/",object : ContentScrapper.ScrapListener{
override fun onResponse(html: String?) {
if(html != null) {
editTexttest.setText(html)
} else {
Toast.makeText(this#SplashActivity,"Not found",Toast.LENGTH_LONG).show()
}
}
})
And I did, but initialising the EditText like this so seemed to return a null error
val testingbox: EditText = findViewById(R.id.editTexttest)
So I looked around and found that I didn't need to initialise and I could just do editTexttest.setText?
But then I received these errors
java.lang.NullPointerException: Attempt to invoke virtual method 'void android.widget.EditText.setText(java.lang.CharSequence)' on a null object reference
at com.example.myapplication.MainActivity$onCreate$1.onResponse(MainActivity.kt:41)
at com.example.myapplication.ContentScrapper$getHTMLData$1$2.run(Util.kt:31)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:223)
at android.app.ActivityThread.main(ActivityThread.java:7656)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
Line 41 was the editTexttest.setText and line 31 was scrapListener.onResponse(stringBuffer.toString()).
If anyone could explain what is wrong that would be great. Thanks.
EDIT: I tunnel visioned and was looking at the wrong activity the entire time. Dumb mistake on my part.
Related
I'm attempting to use kotlin Coroutines for fetching data from a Database using androidx.room artifact. I've analysed the code and I'm yet to find a solution to the problem.
I'm getting a null exception on tonight object which I already set to be nullable. I shouldn't be getting a null exception on a nullable object.
This is the ViewModel class from where I'm writing logic for fetching data
SleepTrackerViewModel.kt
package com.google.samples.apps.trackmysleepquality.sleeptracker
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations
import androidx.lifecycle.viewModelScope
import com.google.samples.apps.trackmysleepquality.database.SleepDatabaseDao
import com.google.samples.apps.trackmysleepquality.database.SleepNight
import com.google.samples.apps.trackmysleepquality.formatNights
import kotlinx.coroutines.launch
/**
* ViewModel for SleepTrackerFragment.
*/
class SleepTrackerViewModel(
val database: SleepDatabaseDao,
application: Application
) : AndroidViewModel(application) {
init {
initializeTonight()
}
/**
* [tonight] is the object that holds the most recent [SleepNight]
*/
private var tonight = MutableLiveData<SleepNight?>()
/**
* Get all the nights from the database
*/
private val nights = database.getAllNights()
val nightsString = Transformations.map(nights) { nights ->
formatNights(nights, application.resources)
}
private fun initializeTonight() {
viewModelScope.launch {
tonight.value = getTonightFromDatabase()
}
}
private suspend fun getTonightFromDatabase(): SleepNight? {
var night = database.getTonight()
if (night?.endTimeMilli != night?.startTimeMilli) {
// If the start and end times are not the same, meaning that the night has already been completed
night = null
}
return night
}
/**
* Function to start tracking a new SleepNight
*/
fun onStartTracking() {
viewModelScope.launch {
val newNight = SleepNight()
insert(newNight)
//assign newNight to tonight as the most recent SleepNight
tonight.value = getTonightFromDatabase()
}
}
private suspend fun insert(night: SleepNight) {
database.insert(night)
}
fun onStopTracking() {
viewModelScope.launch {
val oldNight = tonight.value ?: return#launch
oldNight.endTimeMilli = System.currentTimeMillis()
update(oldNight)
}
}
private suspend fun update(night: SleepNight) {
database.update(night)
}
fun onClear() {
viewModelScope.launch {
clear()
tonight.value = null
}
}
suspend fun clear() {
database.clear()
}
}
The Error Message
E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.google.samples.apps.trackmysleepquality, PID: 21352
java.lang.NullPointerException: Attempt to invoke virtual method 'void
androidx.lifecycle.MutableLiveData.setValue(java.lang.Object)' on a null
object reference
at
com.google.samples.apps.trackmysleepquality.sleeptracker.SleepTrack
erViewModel$initializeTonight$1.invokeSuspend(SleepTrackerViewMod
el.kt:56)
at
kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(Contin
uationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at android.os.Handler.handleCallback(Handler.java:873)
at android.os.Handler.dispatchMessage(Handler.java:99)
This is one of a couple of unexpected ways you can get a NullPointerException in Kotlin. You have an init block that calls the function initializeTonight() before tonight has been initialized. When a class is instantiated, all the property initializations and init blocks are called in order from top to bottom.
You might think it's safe because the value of tonight is set inside a coroutine, but by default viewModelScope synchronously starts running part of the launched coroutine because it uses Dispatchers.Main.immediate. Your getTonightFromDatabase() is calling a suspend function too, but that database might also be using Dispatchers.Main.immediate and be capable of returning a result without actually suspending.
I would change your code as follows. Remove the init block and initializeTonight functions. Declare tonight like this:
private var tonight = MutableLiveData<SleepNight?>().also {
viewModelScope.launch {
it.value = getTonightFromDatabase()
}
}
Also, I would make it val instead of var. There shouldn't be a reason to ever replace it, so making it var is error-prone.
#Tenfour04 covered the issue that's causing it, but I just wanted to point out you're reading the error message wrong, and it probably sent you in the wrong direction:
java.lang.NullPointerException: Attempt to invoke virtual method 'void
androidx.lifecycle.MutableLiveData.setValue(java.lang.Object)' on a null
object reference
That means you're trying to call setValue on a null object - i.e. null.setValue(whatever). It's not your MutableLiveData contents that are null (as you said, the value has a nullable type so that should be fine) - it's the MutableLiveData itself, which is what you call setValue on.
You don't see this one too often in Kotlin (since it does its own null checking and throws a different "oi this shouldn't be null" message) but it can happen!
I have a MainActivity using a DrawerLayout and tabs with 2 fragments.
My first fragment contains a list of elements in a RecyclerView, and I can click on each element to "select" it (which calls a SDK function to login to a hardware device). When selected, this triggers a change on the Fragment's ViewModel:
// Selected device changes when an item is clicked
private val _devices = MutableLiveData<List<DeviceListItemViewModel>>()
private val _selectedDevice = MutableLiveData<ConnectedDevice>()
val devices: LiveData<List<DeviceListItemViewModel>> by this::_devices
val selectedDevice: LiveData<ConnectedDevice> by this::_selectedDevice
Then I have a shared ViewModel between both fragments, which also has a currentDevice variable like this:
private val _currentDevice = MutableLiveData<ConnectedDevice>()
val currentDevice: LiveData<ConnectedDevice> by this::_currentDevice
So in the Fragment that contains the list, I have the following code to update the shared ViewModel variable:
private val mViewModel: DeviceManagementViewModel by viewModels()
private val mSharedViewModel: MainActivityViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
Log.d(classTag, "Fragment view created")
val binding = ActivityMainDevicesManagementFragmentBinding.bind(view)
binding.apply {
viewModel = mViewModel
lifecycleOwner = viewLifecycleOwner
}
// Observe fragment ViewModel
// If any device is clicked on the list, do the login on the shared ViewModel
mViewModel.selectedDevice.observe(this, {
mSharedViewModel.viewModelScope.launch {
if (it != null) {
mSharedViewModel.setCurrentDevice(videoDevice = it)
} else mSharedViewModel.unsetCurrentDevice()
}
})
}
The problem is that if the shared ViewModel's currentDevice variable is set, I get exceptions whenever I try to open a Dialog or start a new activity. If I modify the setCurrentDevice function in the shared ViewModel, then it works fine (or if I don't select any device).
The exceptions I see are this when starting a new Activity:
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.placeholder.easyview/com.example.myapp.activities.settings.SettingsActivity}: java.lang.IllegalArgumentException: display must not be null
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3430)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3594)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:85)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2067)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:223)
at android.app.ActivityThread.main(ActivityThread.java:7698)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:952)
Caused by: java.lang.IllegalArgumentException: display must not be null
at android.app.ContextImpl.createDisplayContext(ContextImpl.java:2386)
at android.content.ContextWrapper.createDisplayContext(ContextWrapper.java:977)
at com.android.internal.policy.DecorContext.<init>(DecorContext.java:50)
at com.android.internal.policy.PhoneWindow.generateDecor(PhoneWindow.java:2348)
at com.android.internal.policy.PhoneWindow.installDecor(PhoneWindow.java:2683)
at com.android.internal.policy.PhoneWindow.getDecorView(PhoneWindow.java:2116)
at androidx.appcompat.app.AppCompatActivity.initViewTreeOwners(AppCompatActivity.java:219)
at androidx.appcompat.app.AppCompatActivity.setContentView(AppCompatActivity.java:194)
at com.example.myapp.activities.settings.SettingsActivity.onCreate(SettingsActivity.kt:34)
at android.app.Activity.performCreate(Activity.java:8000)
at android.app.Activity.performCreate(Activity.java:7984)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1310)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3403)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3594)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:85)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2067)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:223)
at android.app.ActivityThread.main(ActivityThread.java:7698)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:952)
And this if I try to open a Dialog:
java.lang.ArrayIndexOutOfBoundsException: length=16; index=2448
at android.view.InsetsState.peekSource(InsetsState.java:374)
at android.view.InsetsSourceConsumer.updateSource(InsetsSourceConsumer.java:291)
at android.view.InsetsController.updateState(InsetsController.java:654)
at android.view.InsetsController.onStateChanged(InsetsController.java:621)
at android.view.ViewRootImpl.setView(ViewRootImpl.java:1058)
at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:409)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:110)
at android.app.Dialog.show(Dialog.java:340)
at android.app.AlertDialog$Builder.show(AlertDialog.java:1131)
at com.example.myapp.activities.main.fragments.DeviceManagementFragment.showAddDeviceMethodDialog(DeviceManagementFragment.kt:151)
at com.example.myapp.activities.main.fragments.DeviceManagementFragment.access$showAddDeviceMethodDialog(DeviceManagementFragment.kt:33)
at com.example.myapp.activities.main.fragments.DeviceManagementFragment$onViewCreated$$inlined$apply$lambda$1.onClick(DeviceManagementFragment.kt:52)
at android.view.View.performClick(View.java:7448)
at android.view.View.performClickInternal(View.java:7425)
at android.view.View.access$3600(View.java:810)
at android.view.View$PerformClick.run(View.java:28309)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:223)
at android.app.ActivityThread.main(ActivityThread.java:7698)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:952)
EDIT: Looks like the problem actually lies in the other fragment, where I have the following code (in onViewCreated method):
// If the shared view model device changes, this must change too
mSharedViewModel.currentDevice.observe(this, {
if (it != null) {
mViewModel.setCurrentDevice(it)
} else mViewModel.unsetCurrentDevice()
})
mViewModel.currentDevice.observe(this, {
if (it != null) {
mViewModel.fetchChannels()
}
})
If I comment out the second part (where the fetchChannels occurs), it works well. Even if I comment out the fetchChannels call only, it works.
This is the code of the fetchChannels function:
fun fetchChannels() = viewModelScope.launch {
Log.d(classTag, "Getting channels for device ${currentDevice.value}")
currentDevice.value?.let {
val fetchedChannels = deviceLibManager.getChannels(it.videoDevice)
_currentDevice.value?.channels?.clear()
_currentDevice.value?.channels?.addAll(fetchedChannels)
if (fetchedChannels.isNotEmpty()) {
_currentDevice.value?.currentChannel = fetchedChannels[0]
}
}
}
The following line is the one giving me trouble:
val fetchedChannels = deviceLibManager.getChannels(it.videoDevice)
That function is just this:
suspend fun getChannels(videoDevice: VideoDevice): List<VideoChannel> {
try {
Log.i(classTag, "Getting channels from device ${videoDevice}")
val channels = videoDevice.getChannelsAsync()
return channels
} catch (exception: Exception) {
when (exception) {
is UnknownVendorException -> {
Log.w(classTag, "Device ${videoDevice} cannot get channels because the vendor is unknown")
}
is NetworkException -> {
Log.w(classTag, "Device ${videoDevice} cannot get channels because it is unreachable")
}
else -> {
Log.w(classTag, "Device ${videoDevice} cannot get channels, reason: ${exception.message}")
}
}
return emptyList()
}
}
And the implementation in the SDK is this:
override suspend fun getChannelsAsync(): List<VideoChannel> = withContext(Dispatchers.IO) {
Log.i(classTag, "Trying to get channels for device: $logName")
val channels = ArrayList<VideoChannel>()
getZeroChannel()?.let {
channels.add(it)
}
channels.addAll(getAllChannels())
if (channels.isNotEmpty()) {
Log.i(classTag, "Successfully retrieved ${channels.size} channels for device: $logName")
return#withContext channels
} else {
Log.w(classTag, "Error retrieving channels for device $logName or no channels exist")
throw Exception()
}
}
The other functions just make a network call and retrieve some data, it should not be messing with the UI at all.
I am testing with a Xiaomi Mi A3 using Android 10.
Can someone help me? Thank you.
So I don't really know why but I found the answer.
In the SDK, the functions getZeroChannel and getAllChannels were not suspending functions, although they make a network call. So what I did is:
Move the withContext(Dispatchers.IO) part to those two functions (the ones who actually make the network call), and make them suspend functions.
Remove the withContext(Dispatchers.IO) part from getChannelsAsync function. Keep it as a suspend function though.
After these changes everything works as expected. I still don't know why, so if someone could shed some light, that would be very much appreciated.
I am implementing Twilio Chat SDK where I do add members in a private Channel. The flow goes like this that First I check whether is there a channel in my list or not. If it exists then I join it. And if there is no channel in my list I First create it and then join and it and then add member with it's unique name. When I run my application the Client is created and after the channel process is done it crashes with the following error:
com.twilio.chat.ListenerException: Exception thrown by a listener. Your application might have a problem in listener implementation. Listeners must never throw uncaught exceptions. See 'Caused by:' below for more details.
at java.lang.reflect.Constructor.newInstance0(Native Method)
at java.lang.reflect.Constructor.newInstance(Constructor.java:343)
at com.twilio.chat.internal.RethrowingForwarder$RethrowingProxy.invoke(RethrowingForwarder.java:123)
at java.lang.reflect.Proxy.invoke(Proxy.java:1006)
at $Proxy17.onError(Unknown Source)
at com.twilio.chat.internal.StatusListenerForwarder$2.run(StatusListenerForwarder.java:46)
at android.os.Handler.handleCallback(Handler.java:883)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loop(Looper.java:237)
at android.app.ActivityThread.main(ActivityThread.java:8167)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:496)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1100)
Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'void com.twilio.chat.Members.addByIdentity(java.lang.String, com.twilio.chat.StatusListener)' on a null object reference
at com.zofeur.customer.views.fragments.SupportFragment.addMemberInChannel(SupportFragment.kt:259)
at com.zofeur.customer.views.fragments.SupportFragment.access$addMemberInChannel(SupportFragment.kt:22)
at com.zofeur.customer.views.fragments.SupportFragment$joinChannel$1.onError(SupportFragment.kt:249)
I have been looking for several solutions but unable to do so.
Below is the code for my Fragment:
Support Fragment:
private fun createChannel(channelUniqueName: String) {
mViewModel.chatClient?.channels?.getChannel(channelUniqueName,
object : CallbackListener<Channel>() {
override fun onSuccess(channel: Channel?) {
if (channel != null) {
mViewModel.channel = channel
joinChannel(channel)
}
}
override fun onError(errorInfo: ErrorInfo?) {
super.onError(errorInfo)
if (errorInfo?.code == Constants.ERROR_CHANNEL_NOT_FOUND) {
mViewModel.chatClient?.channels?.channelBuilder()
?.withUniqueName(channelUniqueName)
?.withType(Channel.ChannelType.PRIVATE)
?.build(object : CallbackListener<Channel>() {
override fun onSuccess(channel: Channel?) {
requireContext().T("Channel Created $channel")
if (channel != null) {
mViewModel.channel = channel
joinChannel(channel)
}
}
})
} else {
requireContext().T("Channel not created" + errorInfo.toString())
}
}
})
}
private fun joinChannel(channel: Channel) {
channel.join(object : StatusListener() {
override fun onSuccess() {
requireContext().T("Channel Joined" + channel.uniqueName)
addMemberInChannel(channel, identity)
}
override fun onError(errorInfo: ErrorInfo) {
if (errorInfo.code == Constants.ERROR_MEMBER_ALREADY_EXISTS) {
// already join member
addMemberInChannel(channel, identity)
} else {
requireContext().T("Error joining channel$errorInfo")
}
}
})
}
private fun addMemberInChannel(channel: Channel, identity: String) {
if (mViewModel.channel.createdBy == mViewModel.chatClient.myIdentity) {
channel.members.addByIdentity(identity, object : StatusListener() {
override fun onSuccess() {
requireContext().T("Member added successfully" + channel.uniqueName)
}
override fun onError(errorInfo: ErrorInfo?) {
super.onError(errorInfo)
requireContext().T("Channel member added error" + errorInfo.toString())
}
})
} else {
requireContext().T("You cannot add member, You don't have rights.")
}
}
Any sort of help would be very helpful. Thanks in advance.
Hi I was facing the same issue When I debugged a deep longer I found out that when you create a channel it has 3 States:
SynchronizationStatus_STARTED
SynchronizationStatus_CHANNELS_COMPLETED
SyncronizationStatus_COMPLETED
You need to perform any channel related operation after the client SyncronizationStatus_COMPLETED Hope it works thanks just like that
client.addListener(object :ChatClientListener{
override fun onClientSynchronization(p0:
ChatClient.SynchronizationStatus?) {
if (p0 == ChatClient.SynchronizationStatus.COMPLETED) {
// Client is now ready for business, start working
mViewModel.chatClient = client
}
}
}
Context: It is my first project in Android and I fill I am asking some silly question but I can't find any direction on it (certainly I am messing up some previous knowledge of Angular/Srping with Android/Kotlin)
Goal: Android App will get a Firestore customtoken from certain backend microservice and then start to listen a document. So far so good. I read about good practices of how close/detach the listen and I believe I have successfully done this by passing Android activity as first argument to snapshot. So far also so good. But in my case, I have to close/detach the snapshot listen either after 10 minutes or after a especific value from document is received. Now I really got stuck.
I tried the simplest first step as imagined and I am getting the naive warnning form this topic. So my straght question is: why it is complaining as ALWAYS TRUE condition? A complementaty comment from someone expert on Android would be how to close/detach the snapshot after 10 minutes and if I receive a specific value from the listened document. Please accept the idea that either one of these two conditions needs to stops listen and still keep in same MainActivity.kt.
Here is the code with warning when trying to check during onStop cycle phase
package com.mycomp.appfirestore
import android.os.Bundle
import android.util.Log
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.firestore.DocumentSnapshot
import com.google.firebase.firestore.EventListener
import com.google.firebase.firestore.FirebaseFirestore
import com.mycomp.appfirestore.data.service.Endpoint
import com.mycomp.appfirestore.data.service.NetworkUtils
import com.mycomp.appfirestore.model.Transfer
import kotlinx.android.synthetic.main.activity_main.*
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class MainActivity : AppCompatActivity() {
lateinit var auth: FirebaseAuth
lateinit var listenerReg : FirebaseFirestore
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val btnTransfer = findViewById(R.id.btnTransfer) as Button
val textViewTransfer = findViewById(R.id.textViewTransfer) as TextView
btnTransfer.setOnClickListener {
getToken()
}
}
fun getToken() {
val retrofitClient = NetworkUtils
.getRetrofitInstance("http://192.168.15.13:8080/")
val endpoint = retrofitClient.create(Endpoint::class.java)
...
callback.enqueue(object : Callback<Transfer> {
override fun onFailure(call: Call<Transfer>, t: Throwable) {
Toast.makeText(baseContext, t.message, Toast.LENGTH_SHORT).show()
}
override fun onResponse(call: Call<Transfer>, response: Response<Transfer>) {
listenStatus()
}
})
}
fun listenStatus() {
val TAG = "ListenStatus"
auth = FirebaseAuth.getInstance()
// to make simple for this question let's say the backend returned a valid customtoken used here
auth.signInWithCustomToken("eyJ **** gXsQ")
.addOnCompleteListener(this) { task ->
if (task.isSuccessful) {
Log.d(TAG, "*** signInWithCustomToken:success")
startSnapshot()
} else {
// If sign in fails, display a message to the user.
Log.w(TAG, "signInWithCustomToken:failure", task.exception)
Toast.makeText(
baseContext, "Authentication failed.",
Toast.LENGTH_SHORT
).show()
}
}
}
fun startSnapshot() {
val TAG = "StartSnapshot"
//Try to pass this(activity context) as first parameter.It will automatically handle acivity life cycle.
// Example if you are calling this listener in onCreate() and passing this as a first parameter then
// it will remove this listener in onDestroy() method of activity.
listenerReg = FirebaseFirestore.getInstance()
listenerReg.collection("transfer")
.document("sDme6IRIi4ezfeyfrU7y")
.addSnapshotListener(
this,
EventListener<DocumentSnapshot?> { snapshot, e ->
if (e != null) {
Log.w(TAG, "Listen failed.", e)
return#EventListener
}
if (snapshot != null && snapshot.exists()) {
textViewTransfer.text = snapshot.data!!["status"].toString()
//Log.d(TAG, snapshot.data!!["status"].toString())
} else {
Log.d(TAG, "Current data: null")
}
})
}
//here I get the warnning mentioned in my question topic
fun stopSnapshot() {
if (listenerReg != null) {
listenerReg.remove()
}
}
}
I am aware that since I added the activity as first argument of snapshot soon the activity is left it will detached automatically the listen. But I have two more condition to stop listen:
1 - after 10 minutes
2 - if I get a specific returned value
So as imaginary solution I would try more or less
...
EventListener<DocumentSnapshot?> { snapshot, e ->
if (e != null) {
Log.w(TAG, "Listen failed.", e)
return#EventListener
}
if (snapshot != null && snapshot.exists()) {
textViewTransfer.text = snapshot.data!!["status"].toString()
**** imagined solution ****
if (snapshot.data!!["status"].toString() == "FINISH"){
stopSnapshot()
}
//Log.d(TAG, snapshot.data!!["status"].toString())
} else {
Log.d(TAG, "Current data: null")
}
})
...
**** imagined solution ***
listenerReg.timeout(10000, stopSnapshot())
To answer your questions:
Why it is complaining as ALWAYS TRUE condition?
Your FirebaseFirestore object is initialized as lateinit var listenerReg : FirebaseFirestore, which means you've marked your listenerReg variable as non-null and to be initialized later. lateinit is used to mark the variable as not yet initialized, but basically promising that it will be initialized when first referenced in code. Kotlin will throw an error at runtime if trying to access this variable and it's not initialized.
If you want to make it nullable, you'd need to initialize it like this:
var listenerReg : FirebaseFirestore? = null
Then you could have your stop method looking something like this:
fun stopSnapshot() {
if (listenerReg != null) {
listenerReg.remove()
listenerReg.terminate()
listenerRef = null
}
}
But I have two more condition to stop listen:
1 - after 10 minutes
There are many ways to set certain timeouts on Android. The most straightforward way would be to post a handler that will invoke stopSnapshot() method, e.g.:
Handler().postDelayed({
//Do something after 10mins
stopSnapshot();
}, 1000 * 60 * 10)
2 - if I get a specific returned value
Just call stopSnapshot() when you receive this value.
Note: I assumed all the Firestore calls are correct. I don't have the API and this usage on top of my head. Hope my answer helps.
Using a simple RxKotlin Single, I'm receiving either a android.view.ViewRootImpl$CalledFromWrongThreadException exception, or by adding .observeOn(AndroidSchedulers.mainThread()), I'm getting a NetworkOnMainThread exception.
fun loadStaffCalendar() {
var calendarParser = CalendarParser()
calendarParser.getSingleBearCal()
.subscribeOn(Schedulers.io())
.subscribeBy(
onError ={error("Error loading calendar\n${it.message}")},
onSuccess = { responseBody ->
println("ResponseBody retrieved")
var staffList = calendarParser.parseStringIntoSchedule(responseBody.string())
view.loadToAdapter(staffList)
println(staffList)
}
)
I can get the staffList to print in console, but as soon as I try to load it into the View's adapter, it crashes with an CalledFromWrongThread exception.
So here's the crash when I add .observeOn(AndroidSchedulers.mainThread()):
Process: com.offbroadwaystl.archdemo, PID: 21809
io.reactivex.exceptions.UndeliverableException: The exception could not be delivered to the consumer because it has already canceled/disposed the flow or the exception has nowhere to go to begin with. Further reading: https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0#error-handling | android.os.NetworkOnMainThreadException
at io.reactivex.plugins.RxJavaPlugins.onError(RxJavaPlugins.java:367)
at io.reactivex.android.schedulers.HandlerScheduler$ScheduledRunnable.run(HandlerScheduler.java:126)
at android.os.Handler.handleCallback(Handler.java:873)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:193)
at android.app.ActivityThread.main(ActivityThread.java:6669)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
Caused by: android.os.NetworkOnMainThreadException
at android.os.StrictMode$AndroidBlockGuardPolicy.onNetwork(StrictMode.java:1513)
at com.android.org.conscrypt.Platform.blockGuardOnNetwork(Platform.java:415)
at com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLInputStream.read(ConscryptFileDescriptorSocket.java:527)
at okio.InputStreamSource.read(Okio.kt:102)
No additional network calls are made anywhere. Here's the rest:
class CalendarParser : AnkoLogger {
fun getSingleBearCal(): Single<ResponseBody> {
val retrofit: Retrofit = Retrofit.Builder()
.baseUrl("https://www.brownbearsw.com/")
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build()
val bearApi: BearApi = retrofit.create(BearApi::class.java)
return bearApi.file
}
fun parseStringIntoSchedule(wholeSchedule: String): ArrayList<StaffModel> {
var dateMap: HashMap<LocalDate, String> = HashMap()
var endDelim = "END:VEVENT"
var events: List<String> = wholeSchedule.split(endDelim)
var parsedStaffCal: ArrayList<StaffModel> = ArrayList()
var today = LocalDate.now()
// :: Pull event date from event data, pull staff list from "SUMMARY" line :: //
events.forEach {
var tempString = (it.substringAfterLast("DATE:", "FAIL").take(8))
var dateTime: LocalDate = eightIntoDateTime(tempString)
var summary: String = it.substringAfter("SUMMARY:", "FAIL")
.let { it.substringBefore("UID").replace("\\n", "\n") }
dateMap.put(dateTime, summary)
}
// ::Filter out all days before today:: //
dateMap.forEach {
if (!it.key.isBefore(today)) {
val staffModel = StaffModel(it.key, it.value)
parsedStaffCal.add(staffModel)
}
}
//:: Sort chronologically :://
parsedStaffCal.sortBy { it.localDate }
return parsedStaffCal
}
fun eightIntoDateTime(s: String): LocalDate {
return if (s.length == 8 && s.isDigitsOnly()) { // <-=-=-=-=-=- avoid potential formatting exceptions
val dateString = ("${s.subSequence(0, 4)}-${s.subSequence(4, 6)}-${s.subSequence(6, 8)}")
LocalDate.parse(dateString)
} else LocalDate.parse("1999-12-31")
}
Retrofit API:
package com.offbroadwaystl.archdemo.schedule;
import io.reactivex.Single;
import okhttp3.ResponseBody;
import retrofit2.http.GET;
import retrofit2.http.Streaming;
public interface BearApi {
#Streaming
#GET("url.goes.here.ics")
Single<ResponseBody> getFile();
}
subscribeOn tells the observerable where to perform the work on, then observeOn is where the result of this work will be returned to. In your case, you need :
calendarParser.getSingleBearCal()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()).
......
There was an RxJava gradle dependency that was conflicting with the RxKotlin dependency, I think. Removing it fixed the problem. I also took some of the work from onSuccess and added an operator, which is probably better practice anyway:
fun loadStaffCalendar() {
var calendarParser = CalendarParser()
calendarParser.getSingleBearCal()
.subscribeOn(Schedulers.io())
.map { calendarParser.parseStringIntoSchedule(it.string()) }
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy(
onError = {error(it.localizedMessage.toString())},
onSuccess = {view.loadToAdapter(it)})
}
Gradle looks like:
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
implementation 'io.reactivex.rxjava2:rxkotlin:2.4.0'