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!
Related
I am trying to unit test the following class:
class UserProfileDetailsAnalyticUseCaseImp #Inject constructor(private val analyticsProvider: AnalyticsProvider) : UserProfileDetailsAnalyticUseCase {
override fun execute(cdsCustomer: CDSCustomer) {
with(analyticsProvider) {
log(AnalyticEvent.UserId(cdsCustomer.id.toString()))
log(AnalyticEvent.UserEmail(cdsCustomer.email))
}
}
}
And this is my unit test:
class UserProfileDetailsAnalyticUseCaseImpTest {
private lateinit var userProfileDetailsAnalyticUseCaseImp: UserProfileDetailsAnalyticUseCaseImp
private val analyticsProviders: AnalyticsProvider = mock()
#Before
fun setUp() {
userProfileDetailsAnalyticUseCaseImp = UserProfileDetailsAnalyticUseCaseImp(analyticsProviders)
}
#Test
fun `should send analytic event`() {
// Arrange
val cdsCustomer = CDSCustomer(
id = Random.nextInt(0, 100000),
email = UUID.randomUUID().toString())
val userIdCapture= argumentCaptor<AnalyticEvent.UserId>()
val userEmailCapture= argumentCaptor<AnalyticEvent.UserEmail>()
// Act
userProfileDetailsAnalyticUseCaseImp.execute(cdsCustomer)
// Assert
verify(analyticsProviders, atLeastOnce()).log(userIdCapture.capture())
verify(analyticsProviders, atLeastOnce()).log(userEmailCapture.capture())
assertThat(userIdCapture.firstValue.userId).isEqualTo(cdsCustomer.id.toString())
assertThat(userEmailCapture.firstValue.email).isEqualTo(cdsCustomer.email)
}
}
The error I get is the following:
AnalyticEvent$UserId cannot be cast to AnalyticEvent$UserEmail
I am suspecting that because class under test is creating a new object for each log method they will not be the same for the verified methods in the unit test
i.e log(AnalyticEvent.UserId(cdsCustomer.id.toString()))
As a new AnaltyicEvent.UserId will be created and just for the same AnalyticProvider mock
Many thanks for any suggetions
In the documentation of ArgumentCaptor we can read that:
This utility class doesn't do any type checks. The generic
signatures are only there to avoid casting in your code.
Moreover CapturingMatcher which is used for collecting captured arguments has a method which matches all objects:
public boolean matches(Object argument) {
return true;
}
It means that it is normal behaviour and even when we specify concrete type of captor it will record all arguments passed.
Of course all these arguments have to inherit from the same base class because in other case capture method will cause compilation error.
So, both your captors record two arguments.
To fix class cast exception for your test you can assert secondValue for email.
assertThat(userEmailCapture.secondValue.email).isEqualTo(cdsCustomer.email)
You can also stop using argument captors and simply verify invocations of log method.
verify(analyticsProviders).log(AnalyticEvent.UserId(cdsCustomer.id.toString()))
verify(analyticsProviders).log(AnalyticEvent.UserEmail(cdsCustomer.email))
I have the following code which i think is valid, because the recursion happens as a result of a callback. It's not called directly as a result of the function call. But the compiler seems to think there is a recursion issue
class Model(callBack: CallBack) {
interface CallBack {
fun onSomething()
}
}
class SomeClass {
fun createModel() = Model(callBack)
val callBack = object : Model.CallBack {
override fun onSomething() {
val anotherModel = createModel()
// Use model for something
}
}
}
Type checking has run into a recursive problem. Easiest workaround: specify types of your declarations explicitly
Is there a workaround for this?
EDIT
I also tried changing callBack to a function so that the same instance is not referenced by multiple models, but I get the same error
The recursive problem mentioned is not about function calls, it's about the compiler trying to find out the types of the declaration and it has stuck in a recursive type checking. It wants to find the output type of createModel which depends on the type of val callback and it depends on createModel again. As it says, declare their types to fix the issue.
class Model(callBack: CallBack)
{
interface CallBack {
fun onSomething()
}
}
class SomeClass {
fun createModel() : Model = Model(callBack)
val callBack : Model.CallBack = object : Model.CallBack {
override fun onSomething() {
val anotherModel : Model = createModel()
// Use model for something
}
}
}
I'm trying my hand at TDD with an Android app. I'm writing it in Kotlin, and because of that I've turned to MockK for testing, but there's one thing (for now) that I haven't been able to find out how to do: test a suspend call.
I wrote a test for a LiveData value in a ViewModel, and made it work. However, when I added coroutines to the mix, I started getting the "Method getMainLooper not mocked" message.
Here's my code:
ToDoListViewModelTest.kt
class ToDoListViewModelTest {
#get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
#MockK
private lateinit var toDoListLiveDataObserver: Observer<List<ToDoItem>>
#MockK
private lateinit var getToDoItemsUseCase: GetToDoItemsUseCase
#Before
fun setUp() {
MockKAnnotations.init(this)
every { toDoListLiveDataObserver.onChanged(any()) } answers { nothing }
}
#Test
fun toDoList_listItems_noItems() = runBlocking {
coEvery { getToDoItemsUseCase() } coAnswers { emptyList<ToDoItem>() }
val toDoListViewModel = ToDoListViewModel(getToDoItemsUseCase)
toDoListViewModel.toDoItemList.observeForever(toDoListLiveDataObserver)
toDoListViewModel.updateItemList()
assertEquals(0, toDoListViewModel.toDoItemList.value?.size)
}
}
ToDoListViewModel.kt
class ToDoListViewModel(private val getToDoItemsUseCase: GetToDoItemsUseCase) : ViewModel() {
private val _toDoItemList: MutableLiveData<List<ToDoItem>> = MutableLiveData()
val toDoItemList : LiveData<List<ToDoItem>> = _toDoItemList
fun updateItemList() {
viewModelScope.launch(Dispatchers.IO) {
_toDoItemList.value = getToDoItemsUseCase()
}
}
}
GetToDoItemsUseCase.kt
class GetToDoItemsUseCase {
suspend operator fun invoke(): List<ToDoItem> {
return listOf()
}
}
Things I've tried:
Adding "#RunWith(BlockJUnit4ClassRunner::class)": No change
Adding "testOptions { unitTests.returnDefaultValues = true }" to the Gradle file: The Looper error goes away, but the value coming from the LiveData is null, instead of the empty list specified in the "coEvery" call.
Calling "Dispatchers.setMain(newSingleThreadContext("UI Thread"))": Same as previous case, getting null from LiveData.
I'm not very experienced with testing, and I've run out of options. I feel I definitely need some help from the community ;)
Also, if for some reason my setup isn't the right one (should use something other than MockK, or some other testing framework...), please comment on that too. I still have much to learn regarding this.
Use postValue _toDoItemList.postValue(getToDoItemsUseCase())
Based on the documentation:
setValue():
Sets the value. If there are active observers, the value will be
dispatched to them. This method must be called from the main thread.
postValue():
Posts a task to a main thread to set the given value. If you called
this method multiple times before a main thread executed a posted
task, only the last value would be dispatched.
I am really confused and I need your help! This is only my second App and my first time to work with REST API's. I am simply trying to display some User Information like name and profile picture. It is working perfectly fine when I am using the code in the main Activity, but as soon as I am using a different class for it the API call fails and the code is pretty similar, so I do not know any further. Since Twitter uses Retrofit in their own tutorial I am using it as well.
My Class extending TwitterApiClient, the file is also including the Interface for the custom service:
import android.util.Log
import com.twitter.sdk.android.core.*
import com.twitter.sdk.android.core.models.User
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Query
class MyTwitterApiClient(session: TwitterSession) : TwitterApiClient(session) {
fun getCustomService() : GetUsersShowAPICustomService {
return getService(GetUsersShowAPICustomService::class.java)
}
}
interface GetUsersShowAPICustomService {
#GET("/1.1/users/show.json")
fun show(#Query("user_id") userId: Long) : Call<User>
}
My Method in the MainActivity looks like this:
private fun loadTwitterAPI(userID: Long) {
MyTwitterApiClient(session).getCustomService().show(userID).enqueue(object : Callback<User>() {
override fun success(result: Result<User>?) {
text.text = (
"Name: "+result!!.data.name
+"\nLocation: "+result!!.data.location
+"\nFriends: "+result!!.data.friendsCount
)
Picasso.with(baseContext).load(result!!.data.profileImageUrl).resize(250, 250).into(imageView)
}
override fun failure(exception: TwitterException?) {
}
})
}
This works perfectly fine, but I do not want to have the call itself in my main activity and I created a companion Object in my Class extending the TwitterApi which should simply get called with the TwitterSession as parameter and it should return an object of the class User which contains all the important data.
The companion Object inside the MyTwitterApiClient class looks like this:
companion object {
fun start(session: TwitterSession): User {
val userID = session.userId
var data: User? = null
MyTwitterApiClient(session).getCustomService().show(userID).enqueue(object : Callback<User>() {
override fun success(result: Result<User>?) {
data = result!!.data
}
override fun failure(exception: TwitterException?) {
throw exception!!
}
})
return data!!
}
}
The new Method in the MainActivity looks like this:
private fun loadTwitterAPI(userID: Long) {
val t = MyTwitterApiClient.start(session)
text.text = (
"Name: "+t.name
+"\nLocation: "+t.location
+"\nFriends: "+t.friendsCount
)
Picasso.with(baseContext).load(t.profileImageUrl).resize(250, 250).into(imageView)
}
Through testing, I found out, that neither the success Method nor the failure Method gets called. And I do not understand at all why it does not call any Method and just fails.
If anyone here already worked with something like this or has a Tip for me it would be super helpful!
Greetings
Btw: The error that crashes my app in the end is a NullPointerException as the Success Method is not called and null gets returned in the end.
Pastebin to my files:
MainActivity: https://pastebin.com/hWByYUFT
MyTwitterApiClient: https://pastebin.com/85xH284K
activity_main.xml: https://pastebin.com/vkzbkL81
depencies in build.gradle: https://pastebin.com/CpX7cwkS
Ok, starting from your code:
fun start(session: TwitterSession): User {
val userID = session.userId
var data: User? = null
MyTwitterApiClient(session).getCustomService().show(userID).enqueue(object : Callback<User>() {
override fun success(result: Result<User>?) {
data = result!!.data
}
override fun failure(exception: TwitterException?) {
throw exception!!
}
})
return data!!
}
Here you are returning data as if it was assigned. You TwitterApiClient does asynchronous task and so the data from data = result!!.data wont be read correctly from
text.text = (
"Name: "+t.name
+"\nLocation: "+t.location
+"\nFriends: "+t.friendsCount
)
Because t is null then. Its data is not yet set. It will be, sometime in the futur, in the asynchronous callback success().
Your main issue seems to be with how to work with asynchronous tasks and how to notify results. Plenty of sources about it. LiveData, RxJava, EventBus might all be leads.
BTW, the reason why your code worked in MainActivity was because you were setting the text after the result came (in success()), so t was good to read.
Good luck and happy learning!
I have certain memory leaks happening in my custom handler class ,but not sure how to fix it. checkedout a couple of examples online but nothing is specific to my code so not sure how to go about it :
private val startupCallback = object: RetryCallback(NUMBER, DELAY) {
override fun onRetry(retryCount: Int) {
mySdkApi.applicationStartup(this)
}
override fun onCompleted(): Boolean {
updateStatus(Callback.Status.StartUpSDK)
return true
}
override fun onFailed(e: MyException?) {
updateStatus(Callback.Status.StartUpSDK, "", e)
}
}
Android studio keeps prompting "This handler class should be static or leaks might occur".Any ideas how to go about it?
The Android Studio complaining is pretty reasonable. The problem is that anonymous classes capture reference to the parent class that they were created in.
There are basically two solutions the "not pretty" and the ugly.) Both of them are about WeakReference.
#1 The not pretty solution is to make a class that will take a weak ref
class ApiRetryCallback(activity: Activity): RetryCallback(NUMBER, DELAY) {
private val weakActivity = WeakReference(activity)
override fun onRetry(retryCount: Int) {
weakActivity.get()!!.mySdkApi.applicationStartup(this) //or weakThis.get()? to swallow null cases
}
override fun onCompleted(): Boolean {
weakActivity.get()!!.updateStatus(Callback.Status.StartUpSDK)
return true
}
override fun onFailed(e: MyException?) {
weakActivity.get()!!.updateStatus(Callback.Status.StartUpSDK, "", e)
}
}
In activity:
private val startupCallback = ApiRetryCallback(this) //this is MainActivity here
#2 The ugly solution is based on a fact that lambdas should capture parent reference, only where there is a direct usage of it. So I came up with this substitution and I didn't see strong references in a debugger but you should check that:
private val startupCallback = {
val weakActivity = WeakReference(this#MainActivity)
object : RetryCallback(NUMBER, DELAY) { //returned as last expression
override fun onRetry(retryCount: Int) {
weakActivity.get()!!.mySdkApi.applicationStartup(this) //or weakThis.get()? to swallow null cases
}
//....else methods....
}
}()
Here the lambda will be called immediately and will capture only the weak reference inside the object, also it will return the last expression wich is object.
#3 While I was writing, I came up with a third solution, which is close to #2
private val startupCallback = WeakReference(this).let { //this here is MainActivity
val weakActivity = it //it of let scope wich is WeakReference
object : RetryCallback(NUMBER, DELAY) { //returned as last expression
override fun onRetry(retryCount: Int) {
weakActivity.get()!!.mySdkApi.applicationStartup(this) //or weakThis.get()? to swallow null cases
}
//....else methods....
}
}
Anonymous classes (like yours) are non static. You can replace anonymous class with the normal class (just create class extending RetryCallback) and pass all needed objects as constructor arguments.