Fatal Exception when iterating over firebase real-time database keys caused by a blank secondary database key. Possible fixes included ensuring the secondary key and the date string are not null, as well as logging errors with a toast.
Logcat output:
2022-04-18 09:41:43.007 21423-21423/com.jonmacpherson.newspathfinder D/FirebaseRepository: UserID 2022-04-18 09:36:30.613 20444-20444/com.jonmacpherson.newspathfinder D/FirebaseRepository: Index is M1649259297 2022-04-18 09:36:30.615 20444-20444/com.jonmacpherson.newspathfinder D/AndroidRuntime: Shutting down VM 2022-04-18 09:36:30.617 20444-20444/com.jonmacpherson.newspathfinder E/AndroidRuntime: FATAL EXCEPTION: main Process: com.jonmacpherson.newspathfinder, PID: 20444 java.lang.RuntimeException: java.lang.reflect.InvocationTargetException at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:504) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:964) Caused by: java.lang.reflect.InvocationTargetException at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:494) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:964) Caused by: java.text.ParseException: Unparseable date: ""
The method call that is generating the error:
fun getLastSubscriptionExpirationForUserID( database: DatabaseReference, userId: String) { Log.d(TAG, "UserID ${userId}") database.child("subscriptions").child(userId).get().addOnSuccessListener { // generates a list of one subscription. val lastExpirationEpoch = it.children.mapNotNull { snapShot -> snapShot.getValue(Subscription::class.java).also{ subscription -> subscription?.index = snapShot.key ?: "None" Log.d(TAG, "Index is ${snapShot.key}") subscription?.nextExpirationAsEpoch = SimpleDateFormat("MM/dd/yyyy").parse(subscription?.nextExpiration).time } }.sortedByDescending { it.nextExpirationAsEpoch }.take(1)
Likely cause, and possible solutions:
The error was generated when reading subscription records from a firebase real-time database. Just before the error is generated, I wrote the index of the authenticated subscriber out to the log file to ensure there was an actual record to derive the expiration from. The index was “UserID ” (note the lack of an index) as noted in the logcat snippet. I then wrote the “Index is ${snapShot.key}” to the log file and noted that the index matched what should have been the userID.
So, here’s what happened, When I supplied the (blank) userId in this line:
database.child(“subscriptions").child(userId).get().addOnSuccessListener {
The database ignored the child key specified by the second .child method call and instead returned the whole parent key of “subscriptions”, which is what it then tried to iterate with “it.children.mapNotNull”:
val lastExpirationEpoch = it.children.mapNotNull
This caused the children of “subscriptions” to be mapped, rather than the children of “subscriptions/M1649259297“.
To fix this error, I’ll need to supply the correct userId, as well as ensure that userId is not null or empty:
if (!userId.isNullOrEmpty()) {
I’d also like to ensure the date field is not null or empty before proceeding:
if (!subscription?.nextExpiration.isNullOrEmpty()) {
As I close up this bug report, here is the code I’m left with:
I’ve added a toast message that will be displayed to the user when there is a problem with a subscription record. It shouldn’t be too annoying if it is encountered, but more importantly, if there is an issue with the user’s subscription, they will be notified of an error. The notification would be useful to the subscription people who eventually would be helping the user. Also, it’s never good form to hide errors, as that is one of the worst Anti-Patterns there is.
fun getLastSubscriptionExpirationForUserID( database: DatabaseReference, userId: String) { Log.d(TAG, "UserID ${userId}") if (!userId.isNullOrEmpty()) { database.child("subscriptions").child(userId).get().addOnSuccessListener { // generates a list of one subscription. val lastExpirationEpoch = it.children.mapNotNull { snapShot -> snapShot.getValue(Subscription::class.java).also { subscription -> subscription?.index = snapShot.key ?: "None" Log.d(TAG, "Index is ${snapShot.key}") if (!subscription?.nextExpiration.isNullOrEmpty()) { subscription?.nextExpirationAsEpoch = SimpleDateFormat("MM/dd/yyyy").parse(subscription?.nextExpiration).time } else { subscriptionViewModel.apiStatus.postValue("Unable to parse Subscription Expiration for $userId at subscription record ${subscription?.nextExpiration}") } } }.sortedByDescending { it.nextExpirationAsEpoch }.take(1) //More code later. }.addOnFailureListener { // not defined yet } } }
//SubscriptionViewModel.kt (snippet showing the definition of the mutable text string used to send errors to the user) // in the viewmodel, there is a mutableLiveData string that is public. I write errors here, and observe the string from my fragments. Any changes to this string are sent to the user as a Toast // Store any error Message Generated: var apiStatus = MutableLiveData("")
// FragmentSubscriptionOffers.kt (snippet showing the display of error messages) // show Error Messages: subsViewModel.apiStatus.observe(viewLifecycleOwner) { errorMessage -> if (! errorMessage.isNullOrEmpty() ){ Toast.makeText(context,errorMessage, Toast.LENGTH_LONG).show() // once the error is displayed set it to null subsViewModel.resetErrorMessage() } }
Note: All code is a work in progress. This is the code I’m working on, and it’s posted as I run into errors. This work is not suitable for reuse, and no guarantees are offered to its suitability for any function. If you do find something of interest, feel free to reuse it in whole or part.