I'm trying to learn Kotlin and so far i was managing on my own, but alas i need help with understanding DataStore.
I want to save and load a simple String value. I did that with SharedPreferences with no issues but DataStore just isn't working no matter what i try and which "guide" i use.
What i'm currently stuck with:
private val Context.dataStore by preferencesDataStore("savedData")
class DataStorePrefs (context: Context){
private val dataStore = context.dataStore
companion object {
val USER_ID = stringPreferencesKey("USER_ID")
}
suspend fun saveLogin(userId: String){
dataStore.edit { it[USER_ID] = userId }
}
suspend fun restoreLogin(): String{
val result = dataStore.data.map { it[USER_ID]?: "no id" }
Log.e("result", result.toString())
return result.toString()
}
I`m getting this: E/result: com.evolve.recyclerview.data.DataStorePrefs$restoreLogin$$inlined$map$1#7dde190
So how do i actually get the value from a Flow? I tried to use dataStore.data.collect but it just gets stuck on it never returning anything.
I found out that
dataStore.data.map { it[USER_ID]?: "no id" }.first()
gets me the String.
private val Context.dataStore by preferencesDataStore("savedData")
class DataStorePrefs(context: Context){
companion object {
val USER_ID = stringPreferencesKey("USER_ID")
}
suspend fun saveLogin(userId: String){
dataStore.edit { it[USER_ID] = userId }
}
val myFlow: Flow<String> =
dataStore.data.map { it[USER_ID]?: "no id" }
}
Then somewhere else:
myFlow.collect {
doSomething(it)
}
Related
I have a problem with android datastore.
I dont know that I can't write or I can't read but it doesn't work any way
here is my code:
class DataStoreProvider(private val context: Context) {
private val Context.dataStore: DataStore<Preferences> by
preferencesDataStore("settings")
private val phoneNumberKey = stringPreferencesKey("phoneNumberPreferencesKey")
private val passwordKey = stringPreferencesKey("passwordPreferencesKey")
fun readPhoneNumber(): String? {
var phoneNumber: String? = null
context.dataStore.data
.map { preferences ->
phoneNumber = preferences[phoneNumberKey]
}
return phoneNumber
}
suspend fun savePhoneNumber(phoneNumber: String) {
context.dataStore.edit { setting ->
setting[phoneNumberKey] = phoneNumber
}
}
}
I call these functions from viewModelScope.launch function on Dispatchers.IO.
and I use 1.0.0 version of data store
any idea what should I do?
Refactor your readPhoneNumber like this:
suspend fun readPhoneNumber(): String? {
return context.dataStore.data
.map { preferences -> preferences[phoneNumberKey] ?: null }
.first()
}
I want to use coroutines in my project only when I use coroutines I get the error :Unable to invoke no-args constructor. I don't know why it's given this error. I am also new to coroutines.
here is my apiclient class:
class ApiClient {
val retro = Retrofit.Builder()
.baseUrl(Constants.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
Here is my endpoint class:
#GET("v2/venues/search")
suspend fun get(
#Query("near") city: String,
#Query("limit") limit: String = Constants.limit,
#Query("radius") radius: String = Constants.radius,
#Query("client_id") id: String = Constants.clientId,
#Query("client_secret") secret: String = Constants.clientSecret,
#Query("v") date: String
): Call<VenuesMainResponse>
my Repository class:
class VenuesRepository() {
private val _data: MutableLiveData<VenuesMainResponse?> = MutableLiveData(null)
val data: LiveData<VenuesMainResponse?> get() = _data
suspend fun fetch(city: String, date: String) {
val retrofit = ApiClient()
val api = retrofit.retro.create(VenuesEndpoint::class.java)
api.get(
city = city,
date = date
).enqueue(object : Callback<VenuesMainResponse>{
override fun onResponse(call: Call<VenuesMainResponse>, response: Response<VenuesMainResponse>) {
val res = response.body()
if (response.code() == 200 && res != null) {
_data.value = res
} else {
_data.value = null
}
}
override fun onFailure(call: Call<VenuesMainResponse>, t: Throwable) {
_data.value = null
}
})
}
}
my ViewModel class:
class VenueViewModel( ) : ViewModel() {
private val repository = VenuesRepository()
fun getData(city: String, date: String): LiveData<VenuesMainResponse?> {
viewModelScope.launch {
try {
repository.fetch(city, date)
} catch (e: Exception) {
Log.d("Hallo", "Exception: " + e.message)
}
}
return repository.data
}
}
part of activity class:
class MainActivity : AppCompatActivity(){
private lateinit var venuesViewModel: VenueViewModel
private lateinit var adapter: HomeAdapter
private var searchData: List<Venue>? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val editText = findViewById<EditText>(R.id.main_search)
venuesViewModel = ViewModelProvider(this)[VenueViewModel::class.java]
venuesViewModel.getData(
city = "",
date = ""
).observe(this, Observer {
it?.let { res ->
initAdapter()
rv_home.visibility = View.VISIBLE
adapter.setData(it.response.venues)
searchData = it.response.venues
println(it.response.venues)
}
})
this is my VenuesMainResponse data class
data class VenuesMainResponse(
val response: VenuesResponse
)
I think the no-args constructor warning should be related to your VenuesMainResponse, is it a data class? You should add the code for it as well and the complete Log details
Also, with Coroutines you should the change return value of the get() from Call<VenuesMainResponse> to VenuesMainResponse. You can then use a try-catch block to get the value instead of using enqueue on the Call.
Check this answer for knowing about it and feel free to ask if this doesn't solve the issue yet :)
UPDATE
Ok so I just noticed that it seems that you are trying to use the foursquare API. I recently helped out someone on StackOverFlow with the foursquare API so I kinda recognize those Query parameters and the Venue response in the code you provided above.
I guided the person on how to fetch the Venues from the Response using the MVVM architecture as well. You can find the complete code for getting the response after the UPDATE block in the answer here.
This answer by me has code with detailed explanation for ViewModel, Repository, MainActivity, and all the Model classes that you will need for fetching Venues from the foursquare API.
Let me know if you are unable to understand it, I'll help you out! :)
RE: UPDATE
So here is the change that will allow you to use this code with Coroutines as well.
Repository.kt
class Repository {
private val _data: MutableLiveData<mainResponse?> = MutableLiveData(null)
val data: LiveData<mainResponse?> get() = _data
suspend fun fetch(longlat: String, date: String) {
val retrofit = Retro()
val api = retrofit.retro.create(api::class.java)
try {
val response = api.get(
longLat = longlat,
date = date
)
_data.value = response
} catch (e: Exception) {
_data.value = null
}
}
}
ViewModel.kt
class ViewModel : ViewModel() {
private val repository = Repository()
val data: LiveData<mainResponse?> = repository.data
fun getData(longLat: String, date: String) {
viewModelScope.launch {
repository.fetch(longLat, date)
}
}
}
api.kt
interface api {
#GET("v2/venues/search")
suspend fun get(
#Query("ll") longLat: String,
#Query("client_id") id: String = Const.clientId,
#Query("client_secret") secret: String = Const.clientSecret,
#Query("v") date: String
): mainResponse
}
MainActivity.kt
private val viewModel by viewModels<ViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel.getData(
longLat = "40.7,-74",
date = "20210718" // date format is: YYYYMMDD
)
viewModel.data
.observe(this, Observer {
it?.let { res ->
res.response.venues.forEach { venue ->
val name = venue.name
Log.d("name ",name)
}
}
})
}
}
As told in the title i try to create a recyclerview on fragment open with a variable.
Here is a working version without variable:
Fragment:
viewModel.lists.observe(viewLifecycleOwner) {
listAdapter.submitList(it)
}
ViewHolder:
val lists = shishaDao.getList(HARDCODED_INT).asLiveData()
As you can see, there is an hardcoded integer. This integer can hold different values, which is changing the lists value.
Here is my try with a variable:
Fragment:
viewModel.lists(title).observe(viewLifecycleOwner) {
listAdapter.submitList(it)
}
Instead of accessing a variable of the viewholder I am now wanna access a function, which needs the neccessary variable.
ViewHolder:
fun lists(title: String): LiveData<List<Tabak>> {
val nr = dao.getNr(title)
return dao.getList(nr).asLiveData()
}
The App is crashing with following error:
Cannot access database on the main thread since it may potentially lock the UI for a long period of time.
Here are also tried ways:
fun lists(title: String): LiveData<List<Tabak>> {
val nr: Int = 0
viewModelScope.launch {
nr = dao.getNr(title)
Log.e("NR", nr.toString())
}
return dao.getList(nr).asLiveData()
}
fun lists(title: String): LiveData<List<Tabak>> {
val nr: MutableLiveData<Int> = MutableLiveData(0)
viewModelScope.launch {
nr.value = dao.getNr(title)
Log.e("NR", nr.value.toString())
}
return dao.getList(nr.value!!).asLiveData()
}
Both methods do not crash. The Log.e display the right number, but the last line still uses the 0.
My actual question: How can i get thi nr value from dao.getNr(title) to use it in the last line getList(nr)?
i found a way using LiveData. This is a big way, but a useful as well.
new PreferenceManager.kt
class PreferencesManager #Inject constructor(context: Context) {
private val dataStore = context.createDataStore("user_preferences")
val preferencesFlow = dataStore.data
.catch { exception ->
if (exception is IOException) {
Log.e(TAG, "Error reading preferences", exception)
emit(emptyPreferences())
} else {
throw exception
}
}
.map { preferences ->
val choseMarke = preferences[PreferencesKeys.CHOSE_TITLE] ?: 2
FilterPreference(choseTitle)
}
suspend fun updateChoseTitle (choseTitle: Int) {
dataStore.edit { preferences ->
preferences[PreferencesKeys.CHOSE_TITLE] = choseTitle
}
}
private object PreferencesKeys {
val CHOSE_TITLE = preferencesKey<Int>("chosen_title")
}
}
Fragment.kt looks again like this
viewModel.lists.observe(viewLifecycleOwner) {
listAdapter.submitList(it)
}
And the ViewModel.kt
class ListsViewModel #ViewModelInject constructor(private val dao: Dao, private val preferencesManager: PreferencesManager, #Assisted private val state: SavedStateHandle) : ViewModel() {
val searchQuery = state.getLiveData("searchTitle", "")
val preferencesFlow = preferencesManager.preferencesFlow
...
private val listsFlow = combine(searchQuery.asFlow(), preferencesFlow) { query, filterPreference ->
Pair(query, filterPreference)
}.flatMapLatest { (_, filterPreference) ->
dao.getList(filterPreference.choseMarke)
}
I am not more trying to use the title inside of the fragment. Before opening the fragment, i need to press a button which this title, and then I already save the title (as int from the dao getNr) inside the mainActivity.
MainActivity.kt
onNavigationItemSelected(item: MenuItem): Boolean {
viewModel.onNavigationClicked(item)
}
MainViewModel.kt
fun onNavigationClicked(item: MenuItem) = viewModelScope.launch {
val choseMarkeNr = shishaDao.getMarkeNr(item.title.toString())
preferencesManager.updateChoseMarke(choseMarkeNr)
}
This way is working. :)
I Am using MVVM architecture to simple project. Then i stack in this case, when i have to return value from Model DataSource (Lambda function) to Repository then ViewModel will observe this repository. Please correct me if this not ideally and give me some advise for the true MVVM in android. i want to use LiveData only instead of RxJava in this case, because many sample in Github using RxJava.
In my Model i have class UserDaoImpl, code snippet like below
class UserDaoImpl : UserDao {
private val resultCreateUser = MutableLiveData<AppResponse>()
private val mAuth : FirebaseAuth by lazy {
FirebaseAuth.getInstance()
}
override fun createUser(user: User) {
mAuth.createUserWithEmailAndPassword(user.email, user.password)
.addOnCompleteListener {
//I DID NOT REACH THIS LINE
println("hasilnya ${it.isSuccessful} ")
if(it.isSuccessful){
val appResponse = AppResponse(true, "oke")
resultCreateUser.postValue(appResponse)
}else{
val appResponse = AppResponse(false, "not oke -> ${it.result.toString()}")
resultCreateUser.postValue(appResponse)
}
}
.addOnFailureListener {
println("hasilnya ${it.message}")
val appResponse = AppResponse(false, "not oke -> ${it.message}")
resultCreateUser.postValue(appResponse)
}
}
override fun getResultCreateUser() = resultCreateUser
}
And this is my Repository snippet code
class RegisterRepositoryImpl private constructor(private val userDao: UserDao) : RegisterRepository{
companion object{
#Volatile private var instance : RegisterRepositoryImpl? = null
fun getInstance(userDao: UserDao) = instance ?: synchronized(this){
instance ?: RegisterRepositoryImpl(userDao).also {
instance = it
}
}
}
override fun registerUser(user: User) : LiveData<AppResponse> {
userDao.createUser(user)
return userDao.getResultCreateUser() as LiveData<AppResponse>
}
}
Then this is my ViewModel
class RegisterViewModel (private val registerRepository: RegisterRepository) : ViewModel() {
val signUpResult = MutableLiveData<AppResponse>()
fun registerUser(user: User){
println(user.toString())
val response = registerRepository.registerUser(user)
signUpResult.value = response.value
}
}
If i execute the snippet code above, the result always nullpointer in signUpResult
This is my Activity
lateinit var viewModel: RegisterViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_register)
initializeUI()
}
private fun initializeUI() {
val factory = InjectorUtils.provideRegisterViewModelFactory()
viewModel = ViewModelProviders.of(this, factory).get(RegisterViewModel::class.java)
viewModel.signUpResult.observe(this, Observer {
//IT always null
if(it.success){
// to HomeActivity
Toast.makeText(this, "Success! ${it.msg}", Toast.LENGTH_SHORT).show()
}else{
Toast.makeText(this, "FALSE! ${it.msg}", Toast.LENGTH_SHORT).show()
}
})
register_btn.setOnClickListener {
val username = name.text.toString()
val email = email.text.toString()
val password = password.text.toString()
val phone = number.text.toString()
val user = User(0, username,"disana", email, password, "disana")
viewModel.registerUser(user)
}
}
Crash occured when i press register button
I'm not 100% sure, but I think the problem is in your ViewModel, where you are trying to pass by reference MutableLiveData. Your Activity is observing signUpResult MutableLiveData, but you are never posting new value, you are trying to change reference of that LiveData to one in Repository.
val signUpResult = MutableLiveData<AppResponse>()
fun registerUser(user: User){
println(user.toString())
val response = registerRepository.registerUser(user)
signUpResult.value = response.value
}
I think that the solution here is to let your ViewModel return LiveData, which is returned from Repository.
fun registerUser(user: User): MutableLiveData<AppResponse> {
println(user.toString())
return registerRepository.registerUser(user)
}
And you need to observe function registerUser(user) in your Activity.
viewModel.registerUser(user).observe(this, Observer {
But now you encountered another problem. By this example you will trigger observe method every time your button is clicked. So you need to split in repository your function, you need to make one only for returning userDao.getResultCreateUser() as LiveData<AppResponse>, and the other to trigger userDao.create(user) .
So you can make two functions in your repository
override fun observeRegistrationResponse() : LiveData<AppResponse> {
return userDao.getResultCreateUser() as LiveData<AppResponse>
}
override fun registerUser(user: User) {
userDao.createUser(user)
}
Now also in ViewModel you need to make separate function for observing result and for sending request for registration.
fun observeRegistrationResponse(): LiveData<AppResponse> {
return registerRepository.observeRegistrationResponse()
}
fun registerUser(user: User){
println(user.toString())
registerRepository.registerUser(user)
}
And finally you can observe in your function initializeUI
viewModel.observeRegistrationResponse().observe(this, Observer {
And send registration request on button click
viewModel.registerUser(user)
Sorry for long response, but I tried to explain why you need to change your approach. I hope I helped you a bit to understand how LiveData works.
i've facing a problem to test sharedpreference in datastore. in actual datastore i implement three arguments, those include sharedpreference.
in this case i want to store value, and get that value. mocking not help here.
mocking cannot propagate actual value, that will be used by code. in second part.
class FooDataStoreTest : Spek({
given("a foo data store") {
val schedulerRule = TestSchedulerRule()
val service: FooRestService = mock()
val context: Context = mock()
val gson: Gson = mock()
val appFooPreference: SharedPreferences = mock()
var appFooSessionStoreService: AppFooSessionStoreService? = null
var fooStoredLocationService: FooStoredLocationService? = null
beforeEachTest {
appFooSessionStoreService = AppFooSessionStoreService.Builder()
.context(context)
.gson(gson)
.preference(appFooPreference)
.build()
fooStoredLocationService = FooStoredLocationService(appFooSessionStoreService)
}
val longitude = 106.803090
val latitude = -6.244285
on("should get foo service with request longitude $longitude and latitude $latitude") {
it("should return success") {
with(fooStoredLocationService) {
val location = Location()
location.latitude = latitude
location.longitude = longitude
// i want to store location in this
fooStoredLocationService?.saveLastKnownLocation(location)
// and retrieve in below code
val l = fooStoredLocationService?.lastKnownLocation
val dataStore = FooDataStore(service, preference, fooStoredLocationService!!)
service.getFooService(longitude, longitude) willReturnJust
load(FooResponse::class.java, "foo_response.json")
val testObserver = dataStore.getFooService().test()
schedulerRule.testScheduler.advanceTimeBy(2, TimeUnit.SECONDS)
testObserver.assertNoErrors()
testObserver.awaitTerminalEvent()
testObserver.assertComplete()
testObserver.assertValue { actual ->
actual == load(FooResponse::class.java, "foo_response.json")
}
}
}
}
afterEachTest {
appFooSessionStoreService?.clear()
fooStoredLocationService?.clear()
}
}})
and this datastore looks like
open class FooDataStore #Inject constructor(private val fooRestService: FooRestService,
private val fooPreference: FooPreference,
private val fooLocation: fooStoredLocationService) : FooRepository {
private val serviceLocation by lazy {
fooLocation.lastKnownLocation
}
override fun getFooService(): Single<FooResponse> {
safeWith(serviceLocation, {
return getFooLocal(it).flatMap({ (code, message, data) ->
if (data != null) {
Single.just(FooResponse(code, message, data))
} else {
restService.getFooService(it.longitude, it.latitude).compose(singleIo())
}
})
})
return Single.error(httpExceptionFactory(GPS_NOT_SATISFYING))
}
}
Actually i want to get value in from this field serviceLocation. Anyone has approach to do some test for that?, any advise very welcome.
thanks!
I would recommend you not to depend on SharedPreferences directly, but to have some interface LocalStorage, so you can have your SharedPrefsLocalStorage being used in the code and TestLocalStorage in the tests. SharedPrefsLocalStorage will use SharedPreferences under the hood, and TestLocalStorage some Map implementation.
Just a simple example:
// You may add other type, not Int only, or use the String and convert everything to String and back
interface LocalStorage {
fun save(key: String, value: Int)
fun get(key: String): Int?
}
class SharedPrefsLocalStorage(val prefs: SharedPreferences) : LocalStorage {
override fun save(key: String, value: Int) {
with(prefs.edit()){
putInt(key, value)
commit()
}
}
override fun get(key: String): Int? = prefs.getInteger(key)
}
class TestLocalStorage : LocalStorage {
val values = mutableMapOf<String, Any>()
override fun save(key: String, value: Int) {
values[key] = value
}
override fun get(key: String): Int? = map[value] as Int?
}