Edit Text and pass string to the ViewModel? - android

I'm creating the app uses weather API and I need to get name of the place from Edit Text in UI to the ViewModel and there is val which gets method from Repository. How to correctly communicate with ViewModel in my case? There is some magic spell in LiveData or I need Databinding?
ViewModel:
class MainViewModel(
private val weatherRepository: WeatherRepository
) : ViewModel() {
val metric: String = "metric"
val currentWeatherByCoordinates by lazyDeferred {
weatherRepository.getCurrentWeather() }
val forecastByCoordinates by lazyDeferred {
weatherRepository.getForecast() }
val currentWeatherByCity by lazyDeferred { //This is what I'm writing about
weatherRepository.getCurrentWeatherByCity("London", metric)
}
}

use live data. observe some live data method1() in ui -> change the livedata state in viewmodel by method2(city)

Related

How can I generate a String in a function in a viewmodel and return to a fragment in Kotlin?

I want to generate a String in a function in my viewmodel and send it to the fragment associated to this viewmodel as a LiveData, how can I do it?
For example, my viewmodel:
class MaskViewModel : ViewModel() {
var mask: MutableLiveData<String> = newQuestion()
fun newQuestion(): MutableLiveData<String>{
mask.value="255"
return mask
}
}
And the observer in my fragment:
maskviewModel.mask.observe(viewLifecycleOwner){ result ->
binding.textView3.text=result
}
Your function should not return a LiveData or replace the existing LiveData. You should only have a single LiveData instance so when the Fragment observes it, it will get all future changes.
class MaskViewModel : ViewModel() {
val mask = MutableLiveData<String>()
fun newQuestion() {
mask.value="255"
}
}
But it is better encapsulation not to expose your LiveData as being mutable outside the class, so this is better:
class MaskViewModel : ViewModel() {
private val _mask = MutableLiveData<String>()
val mask: LiveData<String> get() = _mask
fun newQuestion() {
_mask.value="255"
}
}
You appear to be all set up to observe any changes to your mask variable within your fragment. To set a new String to it, simply call mask.postValue(myString) within your function. This will trigger your observer, which will receive the new value of mask as result.
Additionally, you are not assigning an instance of MutableLiveData to mask. Your newQuestion() never creates an instance of MutableLiveData, but rather tries to access it while it is still null.
Instantiate it this way: val mask: MutableLiveData<String> = MutableLiveData(). Then you can call .postValue() on it. I've changed it to val because you can keep it as the same reference but still change the value within it. Keep it as var only if you wish to reassign it to a new MutableLiveData at some point, which is unlikely.
As #Tenfour04 mentions in his answer, your function should not return LiveData.
instead of returning the string from your viewModel , you could return it's id , and call the string from fragment.
in the ViewModel
private val _mask = MutableLiveData<Int>()
val mask: LiveData<Int> get() = _mask
fun newQuestion() {
_mask.value = R.string.mask_value
}
in the Fragment
maskviewModel.mask.observe(viewLifecycleOwner){ id ->
binding.textView3.text = requireContext().getString(id)
}

How can I have different states with different viewmodels?

I am making an app where the user first need to login to be able to get alot of different data from a backend. (many endpoints)
So I have one viewmodel for the login, and I have alot of viewmodels for all the other data.
The other viewmodels require the token from the first viewmodel to be able to get data from the backend.
I don't know how I can do this.
I was thinking that I can have my login screen in a kind of state manager which will direct the UI to the correct screen like this
#ExperimentalComposeUiApi
#Composable
fun LoginState(vm: AuthViewModel, nc: NavController) {
val token by vm.token.collectAsState()
when (token) {
is Resource.Loading -> {
LoadingScreen()
}
is Resource.Success -> {
Scaffold(vm = vm)
}
is Resource.Error -> {
LoginScreen(vm = vm)
}
}
}
But then I would have to create the viewmodels inside the Scaffold which is a composable function, and that is not possible.
Another thought was to use Hilt to do some kind of magic dependency injection, and then put all the viewmodels into a ViewModelManager in the MainActivity and then inject the Token into the repositories of each viewmodel when login is successfull.
#AndroidEntryPoint
class MainActivity : ComponentActivity() {
private val authViewModel: AuthViewModel by viewModels()
private val userViewModel: UserViewModel by viewModels()
private val venueViewModel: VenueViewModel by viewModels()
private val eventViewModel: EventViewModel by viewModels()
private val viewModelManager = ViewModelManager(
userViewModel = userViewModel,
authViewModel = authViewModel,
venueViewModel = venueViewModel,
eventViewModel = eventViewModel,
)
#ExperimentalMaterialApi
#ExperimentalComposeUiApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MoroAdminTheme {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
ScaffoldExample(viewModelManager)
}
}
}
}
}
However I have no idea how to do this or if it is even possible - or a good solution.
Problem: you want to share a value (token) to all of your view model
your token retrieved in AuthViewModel and need to share it to the other viewModels
you can make your data in the other viewModels changes when the token changes
by using datastore Preferences see implementation
Datastore preferences provides you with a flow of values whenever the value changes
Create a DatastoreManager Class
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
#Singleton
class DatastoreManager #Inject constructor(#ApplicationContext context: Context) {
private val dataStore = context.dataStore
val dataFlow = dataStore.data
.map { preferences ->
val token = preferences[PreferencesKeys.TOKEN]
}
suspend fun updateToken(token: String) {
dataStore.edit { preferences ->
preferences[PreferencesKeys.TOKEN] = token
}
}
private object PreferencesKeys {
val TOKEN = preferencesKey<String>("token")
}
}
In AuthViewModel
Inject the DatastoreManager and set the token after login
datastore.updateToken(newToken)
In other ViewModels
Inject the DatastoreManager and use it's value
//this is a flow of tokens and will recive the token when you set it
val token = datastore.token
// if you are not familiar with flows and using only LiveData
val token = datastore.token.asLiveData()
// use the token to get the data from backend
val data = token.map {
// this code will trigger every time the token changes
yourGetDataFromBackendFunction(it)
}
But then I would have to create the viewmodels inside the Scaffold which is a composable function, and that is not possible.
This is not true. You don't have to create view models in your Activity.
In any composable you can use viewModel()
Returns an existing ViewModel or creates a new one in the given owner (usually, a fragment or an activity), defaulting to the owner provided by LocalViewModelStoreOwner.
So you don't need any ViewModelManager. Inside any composable you can use viewModel() with the corresponding class. In your case you're using Hilt, you should use hiltViewModel() instead: it'll also initialize your injections.
#Composable
fun AuthScreen(viewModel: AuthViewModel = hiltViewModel()) {
}
Or like this:
#Composable
fun VenueScreen() {
val viewModel: VenueViewModel = hiltViewModel()
}
First approach will allow you to easily test your screen with mock view model, without passing any arguments in your production code.
Check out more about view models in view models documentation and hilt documentation
As to your token question, you can pass it with injections. I don't think that your view model really needs the token, probably you should have some network manager which will use the token to make requests. And this network manager should use injection of some token provider.

Pass arguments from fragment to viewmodel function

Can you tell me if my approach is right? It works but I don't know if it's correct architecture. I read somewhere that we should avoid calling viewmodel function on function responsible for creating fragments/activities mainly because of screen orientation change which recall network request but I really need to pass arguments from one viewmodel to another one. Important thing is I'm using Dagger Hilt dependency injection so creating factory for each viewmodel isn't reasonable?
Assume I have RecyclerView of items and on click I want to launch new fragment with details - common thing. Because logic of these screens is complicated I decided to separate single viewmodel to two - one for list fragment, one for details fragment.
ItemsFragment has listener and launches details fragment using following code:
fun onItemSelected(item: Item) {
val args = Bundle().apply {
putInt(KEY_ITEM_ID, item.id)
}
findNavController().navigate(R.id.action_listFragment_to_detailsFragment, args)
}
Then in ItemDetailsFragment class in onViewCreated function I receive passed argument, saves it in ItemDetailsViewModel itemId variable and then launch requestItemDetails() function to make api call which result is saved to LiveData which is observed by ItemDetailsFragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
//...
val itemId = arguments?.getInt(KEY_ITEM_ID, -1) ?: -1
viewModel.itemId = itemId
viewModel.requestItemDetails()
//...
}
ItemDetailsViewModel
class ItemDetailsViewModel #ViewModelInject constructor(val repository: Repository) : ViewModel() {
var itemId: Int = -1
private val _item = MutableLiveData<Item>()
val item: LiveData<Item> = _item
fun requestItemDetails() {
if (itemId == -1) {
// return error state
return
}
viewModelScope.launch {
val response = repository.getItemDetails(itemId)
//...
_item.postValue(response.data)
}
}
}
Good news is that this is what SavedStateHandle is for, which automatically receives the arguments as its initial map.
#HiltViewModel
class ItemDetailsViewModel #Inject constructor(
private val repository: Repository,
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
private val itemId = savedStateHandle.getLiveData(KEY_ITEM_ID)
val item: LiveData<Item> = itemId.switchMap { itemId ->
liveData(viewModelScope.coroutineContext) {
emit(repository.getItemDetails(itemId).data)
}
}
we should avoid calling viewmodel function on function responsible for creating fragments/activities mainly because of screen orientation change which recall network request
Yes, in your example a request will be executed whenever ItemDetailsFragment's view is created.
Take a look at this GitHub issue about assisted injection support in Hilt. The point of assisted injection is to pass additional dependencies at object's creation time.
This will allow you to pass itemId through the constructor, which then will allow you to access it in ViewModel's init block.
class ItemDetailsViewModel #HiltViewModel constructor(
private val repository: Repository,
#Assisted private val itemId: Int
) : ViewModel() {
init {
requestItemDetails()
}
private fun requestItemDetails() {
// Do stuff with itemId.
}
}
This way the network request will be executed just once when ItemDetailsViewModel is created.
By the time the feature is available you can either try workarounds suggested in the GitHub issue or simulate the init block with a flag:
class ItemDetailsViewModel #ViewModelInject constructor(
private val repository: Repository
) : ViewModel() {
private var isInitialized = false
fun initialize(itemId: Int) {
if (isInitialized) return
isInitialized = true
requestItemDetails(itemId)
}
private fun requestItemDetails(itemId: Int) {
// Do stuff with itemId.
}
}

How to observe the return value from a Repository class in a ViewModel?

I have an android application using an MVVM architecture. On a button click, I launch a coroutine that calls a ViewModel method to make a network request. In my ViewModel, I have a LiveData observable for the return of that request, but I'm not seeing it update. It seems that my repository method isn't being called and I'm not sure why.
UI Click Listener
searchButton.setOnClickListener{
CoroutineScope(IO).launch{
viewModel.getUser(username.toString())
}
}
ViewModel - Observables and invoked method
private var _user: MutableLiveData<User?> = MutableLiveData<User?>()
val user: LiveData <User?>
get() = _user
...
suspend fun getUser(userId:String) {
_user = liveData{
emit(repository.getUser(userId))
} as MutableLiveData<User?>
}
...
When I debug through, execution goes into the getUser method of the ViewModel but doesn't go into the liveData scope to update my _user MutableLiveData observable and I'm not sure what I'm doing wrong.
There is no need to use liveData coroutine builder because the getUser is a suspended function and you are already calling it in a coroutine. Just post the result simply on _user.
suspend fun getUser(userId: String) {
_user.postValue(repository.getUser(userId))
}
What you did on your code caused assigning a new instance of LiveData to _user, while the observer in the fragment is observing on previous LiveData which is instantiated by private var _user: MutableLiveData<User?> = MutableLiveData<User?>(). So, the update gets lost.
A better solution is to handle the creation of coroutines in your ViewModel class to keep track of them and prevent execution leak.
fun getUser(userId: String) {
viewModelScope.launch(IO) {
_user.postValue(repository.getUser(userId))
}
}
And in the fragment:
searchButton.setOnClickListener{
viewModel.getUser(username.toString())
}
It doesn't work because your "MVVM structure" is not following the MVVM recommendations, nor the structured concurrency guidelines provided for coroutines.
searchButton.setOnClickListener{
CoroutineScope(IO).launch{ // <-- should be using a controlled scope
viewModel.getUser(username.toString()) // <-- state belongs in the viewModel
}
}
Instead, it is supposed to look like this
searchButton.setOnClickListener {
viewModel.onSearchButtonClicked()
}
username.doAfterTextChanged {
viewModel.updateUsername(it)
}
And
class MyViewModel(
private val application: Application,
private val savedStateHandle: SavedStateHandle
): AndroidViewModel(application) {
private val repository = (application as CustomApplication).repository
private val username = savedStateHandle.getLiveData("username", "")
fun updateUsername(username: String) {
username.value = username
}
val user: LiveData<User?> = username.switchMap { userId ->
liveData(viewModelScope + Dispatchers.IO) {
emit(repository.getUser(userId))
}
}
}
Now you can do user.observe(viewLifecycleOwner) { user -> ... } and it should work. If you really do need to fetch only when the button is clicked, you might want to replace the liveData { with a regular suspend fun call, calling from viewModelScope.launch {, and save the value to a LiveData.

Observing database object changes made from the background thread

I've met an issue with observing data in my app.
For the testing purposes, I have an activity with a single text view where I show the user's name. This is the code:
#Entity(tableName = "User")
data class User(
var name: String,
var surname: String,
#PrimaryKey(autoGenerate = true)
val internalID: Long = 0)
in the dao I've got just one method:
#Query("SELECT * FROM User WHERE surname LIKE :surname")
abstract suspend fun getUserForSurname(surname: String): User
in the activity onCreate's method:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: ActivityMainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)
val model = ViewModelProvider(this).get(MainViewModel::class.java)
binding.viewmodel = model
model.user.observe(this, Observer {
binding.textTest.setText(it.name)
})
}
and finally, view model:
class MainViewModel(application: Application) : AndroidViewModel(application) {
private val surname = "Doe"
val user: MutableLiveData<User> = MutableLiveData()
private val userDao: UserDao =
MyRoomDatabase.getDatabase(application).clientDao()
init {
viewModelScope.launch {
user.value = userDao.getUserForSurname(surname)
}
}
}
That specific user's name is changed in the background thread. When I check for the value in db itself, the name is indeed different. After restarting activity, the text view is changed too. In other words: the db value is changed but the observer is never called. I know that I am asking for the value only once during viewmodel's init method and it may be a problem. Is it possible to see the actual change without restarting activity?
I suggest you have a look at this Codelab
Room exposes different wrappers around the returned Entities such as:
RxJava
Flow Coroutines
LiveData
So you can changed your Dao as such:
#Query("SELECT * FROM User WHERE surname LIKE :surname")
abstract fun getUserForSurname(surname: String): LiveData<User>
The above means that any changes to the user entry will emit an observation to the listeners of the LiveData.
ViewModel
class MainViewModel(application: Application) : AndroidViewModel(application) {
private val surname = "Doe"
lateinit val client: LiveData<User>
private val userDao: UserDao =
MyRoomDatabase.getDatabase(application).clientDao()
init {
viewModelScope.launch {
user = userDao.getUserForSurname(surname)
}
}
}
Read more at:
- https://developer.android.com/training/data-storage/room/index.html
Disclaimer: Didn't test the above solution but it should give you an idea.
EDIT: Ideally LiveData should only be used in your view model as they were designed for such cases and not to observe DB transactions. I will suggest to replace the Dao's with Coroutine's Flow and use the extension to convert to LiveData.

Categories

Resources