Search code examples
androidkotlinandroid-jetpack-composekotlin-coroutinesandroid-jetpack

Kotlin throws NullPointerException, pointing not to code but to funcion name, when reading data with Datastore and Gson


In my mobile app, written with Kotlin and Android Jetpack Compose, I am saving some user data from web service to DataStore. For user data I have UserModel data class, it's content is not important here, and DataStore is implemented as Repository.

DataStoreRepository.kt:

interface DataStoreRepository {

    fun saveUserCache(user: UserModel)
    fun readUserCache(): UserModel
}

DataStoreRepositoryImpl.kt:

const val DATASTORE_NAME = "settings"
const val USER_CACHE = "USER_CACHE"

private val Context.dataStore : DataStore<Preferences> by preferencesDataStore(name = DATASTORE_NAME)

class DataStoreRepositoryImpl @Inject constructor(
    private val context : Context,
    private val gson: Gson
): DataStoreRepository {

    override fun saveUserCache(user: UserModel) = runBlocking {

        val json = gson.toJson(user)
        saveString(USER_CACHE, json)
    }

    override fun readUserCache(): UserModel = runBlocking {

        val json = readString(USER_CACHE) ?: "" // Android Studio shows json variable as String -> OK

        // tried also this without success, using below code didn't help, but what was interesting for me json variable was shown as... String? -> why?
        //val json = try {
        //    readString(USER_CACHE)
        //} catch (_: Exception) {
        //    ""
        //}

        return@runBlocking try {
            gson.fromJson(json, UserModel::class.java)
        } catch (_: Exception) {
            UserModel()
        }
    }
    
    private suspend fun saveString(key: String, value: String) {

        val prefsKey = stringPreferencesKey(key)
        context.dataStore.edit {
            it[prefsKey] = value
        }
    }
    
    private suspend fun readString(key: String): String? {

        return try {

            val prefsKey = stringPreferencesKey(key)
            val prefs = context.dataStore.data.first()
            prefs[prefsKey]

        } catch (e: Exception) {

            Timber.e(e)
            return null
        }
    }
}

Here is my AppModule.kt (just in case):

@Module
@InstallIn(SingletonComponent::class)
object AppModule {

    // GSON

    @Provides
    @Singleton
    fun provideGson(): Gson {

        return GsonBuilder()
            .registerTypeAdapter(LocalDate::class.java, LocalDateAdapter())
            .registerTypeAdapter(LocalDateTime::class.java, LocalDateTimeAdapter())
            .create()
    }

    @Provides
    @Singleton
    fun provideDatstoreRepository(
        @ApplicationContext context: Context,
        gson: Gson
    ): DataStoreRepository {
        return DataStoreRepositoryImpl(context, gson)
    }
}

When I save data and then try to read it in some viewmodel, then it works perfect.

Problems start when I am trying to read data before something was saved there. I was given error which I do not understand (yes formatting of this curly braces is as in Android Studio Logcat):

java.lang.NullPointerException: override fun readUserCac…erModel()
    }
} must not be null
at REMOVED_MY_PACKAGE_NAME.data.repository.DataStoreRepositoryImpl.readUserCache(DataStoreRepositoryImpl.kt:170)
at REMOVED_MY_PACKAGE_NAME.presentation.components.PwNavigationViewModel.<init>(PwNavigation.kt:132)
at REMOVED_MY_PACKAGE_NAME.DaggerPwApplication_HiltComponents_SingletonC$ViewModelCImpl$SwitchingProvider.get(DaggerPwApplication_HiltComponents_SingletonC.java:712)
at dagger.hilt.android.internal.lifecycle.HiltViewModelFactory$1.create(HiltViewModelFactory.java:102)
at androidx.lifecycle.AbstractSavedStateViewModelFactory.create(AbstractSavedStateViewModelFactory.kt:90)
at dagger.hilt.android.internal.lifecycle.HiltViewModelFactory.create(HiltViewModelFactory.java:114)

Line DataStoreImpl.kt:170 points exactly to this:

override fun readUserCache(): UserModel = runBlocking {

I have feeling that error occurs when doing gson.fromJson(json, UserModel::class.java), but I surrounded it with try-catch to return empty UserModel() in case of some problems (i.e. when there is no data under USER_CACHE key or when data is malformed.

What can cause such problem? How can I fix it? Every help is appreciated :)


Solution

  • gson.fromJson returns null from an empty String. But the Kotlin compiler does not know that, and treat it as the platform type.

    runBlocking does not care about the return type and pass the null up. But readUserCache expect a non-null value, so it throws.


    Rewriting a bit to make the point clear.

    fun readUserCache(): UserModel {
        // implicit null check here fails
        //              vvvvvvvvvvv
        val tempVariable: UserModel = runBlocking {
            // runBlocking does not mind that a null is returned here
            return@runBlocking gson.fromJson("", UserModel::class.java)
        }
        return tempVariable
    
    }
    

    An ugly solution is to throw early

    gson.fromJson(json, UserModel::class.java)!!
    

    Then in your code it will be caught, and a empty UserModel() returned.