I switched from SharedPreferences to Jetpack DataStore.
I am unable to mock the data from the datastore call in the Instrumentation test
In declaration
val dataStore = context.createDataStore(name = "App Name")
In the declaration get a string from preferences
suspend fun getString(
key: Preferences.Key<String>,
defaultVal: String,
context: Context
): String {
return dataStore?.data?.catch {
if (it is IOException) {
it.printStackTrace()
emit(emptyPreferences())
} else {
throw it
}
}?.map {
it[key] ?: defaultVal
}?.first() ?: ""
}
In the usage
SharedPreferenceHelper.getString(
Preferences.Key<T>,
"",
requireContext()
)
Manipulate datastore preferences string key to get mocked value in instrumentation test as the desired value.
Thank you in advance.
createDataStore returns DataStore<Preference>, this will help you to create mock of Class with generic type
Or you can use mockito-kotlin to create mock like this in kotlin
val mockDataStore = mock<DataStore<Preference>>()
Related
In a Jetpack Compose component I'm subscribing to Room LiveData object using observeAsState.
The initial composition goes fine, data is received from ViewModel/LiveData/Room.
val settings by viewModel.settings.observeAsState(initial = AppSettings()) // Works fine the first time
A second composition is initiated, where settings - A non nullable variable is set to null, and the app crashed with an NPE.
DAO:
#Query("select * from settings order by id desc limit 1")
fun getSettings(): LiveData<AppSettings>
Repository:
fun getSettings(): LiveData<AppSettings> {
return dao.getSettings()
}
ViewModel:
#HiltViewModel
class SomeViewModel #Inject constructor(
private val repository: AppRepository
) : ViewModel() {
val settings = repository.getSettings()
}
Compose:
#OptIn(ExperimentalFoundationApi::class)
#Composable
fun ItemsListScreen(viewModel: AppViewModel = hiltViewModel()) {
val settings by viewModel.settings.observeAsState(initial = AppSettings())
Edit:
Just to clearify, the DB data does not change. the first time settings is fetched within the composable, a valid instance is returned.
Then the component goes into recomposition, when ItemsListScreen is invoked for the second time, then settings is null (the variable in ItemsListScreen).
Once the LiveData<Appsettings> is subscribed to will have a default value of null. So you get the default value required by a State<T> object, when you call LiveData<T>::observeAsState, followed by the default LiveData<T> value, this being null
LiveData<T> is a Java class that allows nullable objects. If your room database doesn't have AppSettings it will set it a null object on the LiveData<AppSettings> instance. As Room is also a Java library and not aware of kotlin language semantics.
Simply put this is an interop issue.
You should use LiveData<AppSettings?> in kotlin code and handle null objects, or use some sort of MediatorLiveData<T> that can filter null values for example some extensions functions like :
#Composable
fun <T> LiveData<T?>.observeAsNonNullState(initial : T & Any, default : T & Any) : State<T> =
MediatorLiveData<T>().apply {
addSource(this) { t -> value = t ?: default }
}.observeAsState(initial = initial)
#Composable
fun <T> LiveData<T?>.observeAsNonNullState(initial : T & Any) : State<T> =
MediatorLiveData<T>().apply {
addSource(this) { t -> t?.run { value = this } }
}.observeAsState(initial = initial)
If you only need to fetch settings when viewModel is initialised, you can try putting it in an init block inside your ViewModel.
I'm having an issue trying to display the data saved in my DataStore on startup in Jetpack Compose.
I have a data store set using protocol buffers to serialize the data. I create the datastore
val Context.networkSettingsDataStore: DataStore<NetworkSettings> by dataStore(
fileName = "network_settings.pb",
serializer = NetworkSettingsSerializer
)
and turn it into a livedata object in the view model
val networkSettingsLive = dataStore.data.catch { e ->
if (e is IOException) { // 2
emit(NetworkSettings.getDefaultInstance())
} else {
throw e
}
}.asLiveData()
Then in my #Composable I try observing this data asState
#Composable
fun mycomposable(viewModel: MyViewModel) {
val networkSettings by viewModel.networkSettingsLive.observeAsState(initial = NetworkSettings.getDefaultInstance())
val address by remember { mutableStateOf(networkSettings.address) }
Text(text = address)
}
I've confirmed that the data is in the datastore, and saving properly. I've put some print statements in the composible and the data from the datastore makes it, eventually, but never actually displays in my view. I want to say I'm not properly setting my data as Stateful the right way, but I think it could also be not reading from the data store the right way.
Is there a display the data from the datastore in the composable, while displaying the initial data on start up as well as live changes?
I've figured it out.
What I had to do is define the state variables in the composable, and later set them via a state controlled variable in the view model, then set that variable with what's in the dataStore sometime after initilization.
class MyActivity(): Activity {
private val viewModel: MyViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
MainScope.launch {
val networkSettings = viewModel.networkSettingsFlow.firstOrNull()
if (networkSettings != null) {
viewModel.mutableNetworkSettings.value = networkSettings
}
}
}
}
class MyViewModel(): ViewModel {
val networkSettingsFlow = dataStore.data
val mutableNetworkSettings = mutableStateOf(NetworkSettings.getInstance()
}
#Composable
fun NetworkSettings(viewModel: MyViewModel) {
val networkSettings by viewModel.mutableNetworkSettings
var address by remember { mutableStateOf(networkSettings.address) }
address = networkSettings.address
Text(text = address)
}
I have this Kotlin class that is a wrapper around SharedPreferences in an Android app.
class Preferences(private val context: Context) {
private val preferences: SharedPreferences =
context.getSharedPreferences("name_of_file", Context.MODE_PRIVATE)
// Integers
var coins: Int
get() = preferences.getInt(KEY_COINS, 0)
set(value) = preferences.edit { putInt(KEY_COINS, value) }
var pressure: Int
get() = preferences.getInt(KEY_PRESSURE, DEFAULT_PRESSURE)
set(value) = preferences.edit { putInt(KEY_PRESSURE, value) }
}
I need to mock this class in order to be able to use it in some unit tests for my viewmodels. I tried mocking the get/set methods of the properties but for some reason I'm getting some errors and I need a bit of help.
This is how I try to mock the Preferences class:
private val sharedPreferences = mutableMapOf<String, Any>()
...
val preferences = mockk<Preferences>()
listOf("coins", "pressure").forEach { key ->
every { preferences getProperty key } returns sharedPreferences[key]
every { preferences setProperty key } answers { // exception on this line
sharedPreferences[key] = fieldValue
fieldValue
}
}
And I get this exception when running any of the tests that involves this mock
io.mockk.MockKException: Missing mocked calls inside every { ... } block: make sure the object inside the block is a mock
I think the error is quite cryptic. Or, is there any way to mock these fields using mockk?
I have also read the examples from here where I got the inspiration for this solution but seems like there is something I'm missing.
I'm trying to implement the new typed DataStore API in Java and I'm having some issues. All the documentation seems to be in Kotlin only and trying to create a new data store is not as straight forward from the Java side it seems.
Calling DataStoreFactoryKt.createDataStore() from Java requires me to provide all the arguments including the ones with default values in the Kotlin implementation. There doesnt seem to be any #JvmOverloads annotation for that function, resulting in my predicament.
fun <T> Context.createDataStore(
fileName: String,
serializer: Serializer<T>,
corruptionHandler: ReplaceFileCorruptionHandler<T>? = null,
migrations: List<DataMigration<T>> = listOf(),
scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
): DataStore<T> =
DataStoreFactory.create(
produceFile = { File(this.filesDir, "datastore/$fileName") },
serializer = serializer,
corruptionHandler = corruptionHandler,
migrations = migrations,
scope = scope
)
What's the better way around this, if there is any? Or is the Data Store api simple designed to be used with Kotlin only? I have no idea how I would go about providing a CoroutineScope argument from Java.
After updating dataStore dependency to '1.0.0-alpha08' as below.
// DataStore
implementation "androidx.datastore:datastore-preferences:1.0.0-alpha08"
You can have preferences implementation as follow:
private val Context.dataStore by preferencesDataStore("app_preferences")
After that if you like create some preference key:
private object Keys {
val HIDE_VISITED = booleanPreferencesKey("hide_visited")
}
other options can be stringPreferencesKey, intPreferencesKey, etc.
Saving value example:
context.dataStore.edit { prefs -> prefs[Keys.HIDE_VISITED] = hideVisited }
Reading saved value example:
val hideVisited = preferences[Keys.HIDE_VISITED] ?: false
You need to add to your Grade build file the dependency for DataStore preferences:
implementation "androidx.datastore:datastore-preferences:1.0.0-alpha04"
and not the one for Types, that way you will be able to resolve the androidx.datastore.preferences.Context.createDataStore method that you are expecting:
public fun Context.createDataStore(
name: String,
corruptionHandler: ReplaceFileCorruptionHandler<Preferences>? = null,
migrations: List<DataMigration<Preferences>> = listOf(),
scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
): DataStore<Preferences> =
PreferenceDataStoreFactory.create(
corruptionHandler = corruptionHandler,
migrations = migrations,
scope = scope
) {
File(this.filesDir, "datastore/$name.preferences_pb")
}
If you need to use proto dataStore from version 1.0.0-beta01, you can:
implementation("androidx.datastore:datastore-core:1.0.0-beta01")
initialize with Data Store Factory
val data: DataStore<SomeMessage> = DataStoreFactory.create(
serializer = SessionSerializer, // your Serializer
corruptionHandler = null,
migrations = emptyList(),
scope = CoroutineScope(Dispatchers.IO + Job())
And continue as before
data.updateData {
it.toBuilder().setAddress("address").build()
}
data.collect { ChargingSession ->
ChargingSession.address
}
This is only valid for dependency version 1.0.0-alpha08 and above
#Ercan approach is correct but requires context every time we need access to the dataStore.
Below is a better approach for the same.
private val Context._dataStore: DataStore<Preferences> by preferencesDataStore(APP_PREFERENCES)
private val dataStore : DataStore<Preferences> = context._dataStore
companion object {
const val APP_PREFERENCES = "app_preferences"
}
reference: https://issuetracker.google.com/issues/173726702
I'm using a pattern that I've used a few times before to instantiate a ViewModel object. In this case, the data is saved as a string in SharedPreferences. I just need to read that string, parse it to the correct object, and assign that object as the value to my view model.
But when I do the assignment, I create an infinite loop.
class UserDataViewModel(private val prefs: SharedPreferences): ViewModel() {
val userData: MutableLiveData<UserData> by lazy {
MutableLiveData<UserData>().also {
val userDataString = prefs.getString(Authenticator.USER_DATA, "")
val ud = Gson().fromJson(userDataString, UserData::class.java)
userData.value = ud // infinite loop is here
}
}
fun getUserData(): LiveData<UserData> {
return userData
}
}
This is in onCreateView() of the fragment that keeps the reference to the ViewModel:
userDataViewModel = activity?.run {
ViewModelProviders
.of(this, UserDataViewModelFactory(prefs))
.get(UserDataViewModel::class.java)
} ?: throw Exception("Invalid Activity")
userDataViewModel
.getUserData()
.observe(this, Observer {
binding.userData = userDataViewModel.userData.value
})
FWIW, in the fragment, I have break points on both getUserData() and on binding.userData.... The last break point that gets hit is on getUserData().
I don't see where the loop is created. Thanks for any help.
The userData field is only initialized once the by lazy {} block returns. You're accessing the userData field from within the by lazy {} block and that's what is creating the loop - the inner access sees that it hasn't finishing initializing, so it runs the block again..and again and again.
Instead, you can access the MutableLiveData you're modifying in the also block by using it instead of userData, breaking the cycle:
val userData: MutableLiveData<UserData> by lazy {
MutableLiveData<UserData>().also {
val userDataString = prefs.getString(Authenticator.USER_DATA, "")
val ud = Gson().fromJson(userDataString, UserData::class.java)
it.value = ud
}
}