How to use the keyword also in kotlin android - android

Am learning android kotlin follow this:
https://developer.android.com/topic/libraries/architecture/viewmodel#kotlin
class MyViewModel : ViewModel() {
private val users: MutableLiveData<List<User>> by lazy {
MutableLiveData<List<User>>().also {
loadUsers(it)
}
}
fun getUsers(): LiveData<List<User>> {
return users
}
private fun loadUsers() {
// Do an asynchronous operation to fetch users.
}
}
Dont know how to write the fun loadUsers()
Here is my User:
class User {
constructor(name: String?) {
this.name = name
}
var name:String? = null
}
If dont use the keyword 'also' , i know how to do it.
But if use 'also' , it seems not work.
Here is how i try to write the fun loadUsers:
private fun loadUsers( it: MutableLiveData<List<User>>){
val users: MutableList<User> = ArrayList()
for (i in 0..9) {
users.add(User("name$i"))
}
it = MutableLiveData<List<User>>(users)
}
Error tips near it : Val cant be ressigned

Part 1: According to the Kotlin documentation, also provides the object in question to the function block as a this parameter. So, every function call and property object you access is implied to refer to your MutableLiveData<List<User>>() object. also returns this from the function block when you are done.
Thus, another way of writing your MutableLiveData<> would be like this:
val users = MutableLiveData<List<User>>()
users.loadUsers()
Part 2: As far as how to implement loadUsers(), that is a separate issue (your question is not clear). You can use Retrofit + RxJava to load the data asynchronously, and that operation is totally outside of the realm of ViewModel or also.
Part 3: With your approach, you have conflicting things going on. Instead of doing a loadUsers() from your lazy {} operation, I would remove your lazy {} operation and create a MutableLiveData<> directly. Then, you can load users later on and update the users property any time new data is loaded. Here is a similar example I worked on a while ago. It uses state flows, but the idea is similar. Also use a data class to model the User instead of a regular class. Another example.

It is solved change to code:
private fun loadUsers( it: MutableLiveData<List<User>>){
val users: MutableList<User> = ArrayList()
for (i in 0..9) {
users.add(User("name$i"))
}
it.value = users
}
it can't be reassigned , but it.value could .

Related

Expected methods do not exists MutableStateFlow<List<T>>() for manipulation

I have this MutableStateFlow<>() declaration:
private val _books = MutableStateFlow<List<Book>>(emptyList())
I am trying to append/add results from the database:
fun fetchAllBooks(user_id: Long) = viewModelScope.launch(Dispatchers.IO) {
dbRepository.getAllUsersBooks(user_id).collect{ books ->
_books.add() // Does not exist, nor does the 'postValue' method exists
}
}
But, this does not work as I though, non of the expected methods exists.
If you need to update the state of a MutableStateFlow, you can set the value property:
fun fetchAllBooks(user_id: Long) = viewModelScope.launch(Dispatchers.IO) {
dbRepository.getAllUsersBooks(user_id).collect{ books ->
_books.value = books
}
}
It will trigger events on collectors if the new value is different from the previous one.
But if getAllUsersBooks already returns a Flow<List<Book>>, you could also simply use it directly instead of updating a state flow.
If you really want a StateFlow, you can also use stateIn:
fun fetchAllBooks(user_id: Long) = dbRepository.getAllUsersBooks(user_id)
.flowOn(Dispatchers.IO) // likely unnecessary if your DB has its own dispatcher anyway
.stateIn(viewModelScope)
You are actually declaring the immutable list and trying to add and remove data instade of that use mutable list to add or remove data from list like here :-
private var _bookState = MutableStateFlow<MutableList<Book>>(mutableListOf())
private var books=_bookState.asStateFlow()
var bookList= books.value
and to send the data to the state use this:-
viewModelScope.launch {
_bookState.value.add(BookItem)
}
viewModelScope.launch {
_bookState.value.remove(BookItem)
}
I hope this will work out for you if you have any query pls tell me in comment.

Wait with launching coroutine as long as first one is not finished. Synchronization mechanism

I am learning android development and I decided to build a weather app using api that comes from service named open water map. Unfortunately I’ve got the following problem:
In order to get the weather data for wanted city, I first need to perform request to get the geographical coordinates. So what I need to do is to create one request, wait until it is finished, and after that do another request with data that has been received from the first one.
This is how my view model for location looks like:
class LocationViewModel constructor(private val repository: WeatherRepository): ViewModel() {
val location = MutableLiveData<List<GeocodingModel>>()
private val API_KEY = „xxxxxxxxxxxxxxxxxxxxxxxxx”
fun refresh() {
CoroutineScope(Dispatchers.IO).launch {
// call fetch location here in coroutine
}
}
private suspend fun fetchLocation(): Response<GeocodingModel> {
return repository.getCoordinates(
"Szczecin",
API_KEY
)
}
}
And this is how my view model for weather looks like”
class WeatherSharedViewModel constructor(private val repository: WeatherRepository): ViewModel() {
private val API_KEY = „xxxxxxxxxxxxxxxxxxxxxxxxx”
val weather = MutableLiveData<List<SharedWeatherModel>>()
val weatherLoadError = MutableLiveData<Boolean>()
val loading = MutableLiveData<Boolean>()
fun refresh(lat: String, lon: String) {
loading.value = true
CoroutineScope(Dispatchers.IO).launch {
// call fetchWeather here in coroutine
}
loading.value = false
}
private suspend fun fetchWeather(lat: String, lon: String): Response<SharedWeatherModel> {
return repository.getWeather(
lat,
lon,
"minutely,hourly,alerts",
"metric",
API_KEY
)
}
}
I am using both view models in a fragment in such way:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
val weatherService = WeatherApi.getInstance()
val repository = WeatherRepository(weatherService)
locationViewModel = ViewModelProvider(requireActivity(), ViewModelFactory(repository)).get(LocationViewModel::class.java)
weatherViewModel = ViewModelProvider(requireActivity(), ViewModelFactory(repository)).get(WeatherSharedViewModel::class.java)
locationViewModel.refresh()
Log.d(TAG, "lat: ${locationViewModel.location.value?.get(0)?.get(0)?.lat.toString()}, lon: ${locationViewModel.location.value?.get(0)?.get(0)?.lon.toString()}")
weatherViewModel.refresh(
locationViewModel.location.value?.get(0)?.get(0)?.lat.toString(),
locationViewModel.location.value?.get(0)?.get(0)?.lon.toString()
)
val weatherList = view?.findViewById<RecyclerView>(R.id.currentWeatherList)
weatherList?.apply {
layoutManager = LinearLayoutManager(context)
adapter = currentWeatherAdapter
}
val cityList = view?.findViewById<RecyclerView>(R.id.currentCityList)
cityList?.apply {
layoutManager = LinearLayoutManager(context)
adapter = currentLocationAdapter
}
observerLocationViewModel()
observeWeatherViewModel()
}
So on a startup both models are refreshed, which means that requests are made. I was trying to somehow synchronize those calls but my last attempt ended that data passed to the refresh method of weather view model was null. So problem is that both coroutine are launched one after another, first one is not waiting for second.
The main question: is there any synchronisation mechanism in coroutines? That I can launch one coroutine and wait with launching second one as long as first is not finished?
You are violating the "Single Responsibilty Principle" you need to learn how to write CLEAN code. that is why you are running into such problems. A member of stackoverflow has explained it in depth: single responsibility
A few tips:
Your general design is somewhat convoluted because you are trying to update LiveData with coroutines, but one LiveData's exposed data is something determined by the other LiveData. This is theoretically OK if you need to be able to access the city even after you already have the weather for that city, but since you've split this behavior between two ViewModels, you end up having to manage that interaction externally with your Fragment, which is very messy. You cannot control it from a single coroutine unless you use the fragment's lifecycle scope, but then the fetch tasks restart if the screen rotates before they're done. So I would use a single ViewModel for this.
In a ViewModel, you should use viewModelScope for your coroutines instead of creating an ad hoc CoroutineScope that you never cancel. viewModelScope will automatically cancel your coroutines when the ViewModel goes out of scope.
Coroutines make it extremely easy to sequentially do background work. You just need to call suspend functions in sequence within a single coroutine. But to do that, once again, you really need a single ViewModel.
It's convoluted to have separate LiveDatas for the loading and error states. If you use a sealed class wrapper, it will be much simpler for the Fragment to treat the three possible states (loading, error, have data).
Putting this together gives the following. I don't really know what your repo is doing and how you convert Response<GeocodingModel> to List<GeocodingModel> (or why), so I am just using a placeholder function for that. Same for the weather.
sealed class WeatherState {
object Loading: WeatherState()
object Error: WetaherState()
data class LoadedData(val data: List<SharedWeatherModel>)
}
class WeatherViewModel constructor(private val repository: WeatherRepository): ViewModel() {
val location = MutableLiveData<List<GeocodingModel>>()
private val API_KEY = „xxxxxxxxxxxxxxxxxxxxxxxxx”
val weather = MutableLiveData<LoadedData>().apply {
value = WeatherState.Loading
}
fun refreshLocation() = viewModelScope.launch {
weather.value = WeatherState.Loading
val locationResponse = fetchLocation() //Response<GeocodingModel>
val locationList = unwrapLocation(location) //List<GeocodingModel>
location.value = locationList
val latitude = locationList.get(0).get(0).lat.toString()
val longitude = locationList.get(0).get(0).lon.toString()
try {
val weatherResponse = fetchWeather(latitude, longitude) //Response<SharedWeatherModel>
val weatherList = unwrapWeather(weatherResponse) //List<SharedWeatherModel>
weather.value = WeatherState.LoadedData(weatherList)
} catch (e: Exception) {
weather.value = WeatherState.Error
}
}
private suspend fun fetchLocation(): Response<GeocodingModel> {
return repository.getCoordinates(
"Szczecin",
API_KEY
)
}
private suspend fun fetchWeather(lat: String, lon: String): Response<SharedWeatherModel> {
return repository.getWeather(
lat,
lon,
"minutely,hourly,alerts",
"metric",
API_KEY
)
}
}
And in your Fragment you can observe either LiveData. The weather live data will always have one of the three states, so you have only one place where you can use a when statement to handle the three possible ways your UI should look.
Without referring to your actual code only to the question itself:
By default code inside coroutines is sequential.
scope.launch(Dispatcher.IO) {
val coordinates = repository.getCoordinates(place)
val forecast = repository.getForecast(coordinates)
}
Both getCoordinates(place) and getForecast(coordinates) are suspend functions since they're making network requests and waiting for the result.
getForecast(coordinates) won't execute until getCoordinates(place) is done and returned the coordinates.

How to pass different value when switchMap is not executed? Android + Kotlin

I have a huge understanding problem here, I have a ecommerce app and I cannot properly calculate value of users cart.
The problem is, my solution works well to the point but I have an issue when there are no products in the cart. Obviously LiveData observer or switchMap will not get executed when it's value is empty.
It seems like something trivial, only thing I want to do here is handle the situation when user have no products in the cart. Is the livedata and switchMap a wrong approach here?
I get userCart from the repo -> I calculate its value in the viewModel and expose it to the view with dataBinding.
#HiltViewModel
class CartFragmentViewModel
#Inject
constructor(
private val repository: ProductRepository,
private val userRepository: UserRepository,
private val priceFormatter: PriceFormatter
) : ViewModel() {
private val user = userRepository.currentUser
val userCart = user.switchMap {
repository.getProductsFromCart(it.cart)
}
val cartValue = userCart.switchMap {
calculateCartValue(it)
}
private fun calculateCartValue(list: List<Product>?): LiveData<String> {
val cartVal = MutableLiveData<String>()
var cartValue = 0L
list?.let { prods ->
prods.forEach {
cartValue += it.price
}
cartVal.postValue(priceFormatter.formatPrice(cartValue))
} ?: cartVal.postValue(priceFormatter.formatPrice(0))
return cartVal
}
fun removeFromCart(product: Product) {
userRepository.removeFromCart(product)
getUserData()
}
private fun getUserData() {
userRepository.getUserData()
}
init {
getUserData()
}
}
Default value is to solve the "initial" empty cart.
Now if you need to trigger it when there's no data... (aka: after you remove items and the list is now empty), I'd use a sealed class to wrap the actual value.
(names and code are pseudo-code, so please don't copy-paste)
Something like this:
Your Repository should expose the cart, user, etc. wrapped in a sealed class:
sealed class UserCartState {
object Empty : UserCartState()
data class HasItems(items: List<things>)
object Error(t: Throwable) :UserCartState() //hypotetical state to signal problems
}
In your CartFragmentViewModel, you observe and use when (for example), to determine what did the repo responded with.
repo.cartState.observe(...) {
when (state) {
is Empty -> //deal with it
is HasItems -> // do what it takes to convert it, calculate it, etc.
is Error -> // handle it
}
}
When the user removes the last item in the cart, your repo should emit Empty.
The VM doesn't care how that happened, it simply reacts to the new state.
The UI cares even less. :)
You get the idea (I hope).
That's how I would look into it.
You can even use a flow of cart items, or the new "FlowState" thingy (see the latest Google I/O 21) to conserve resources when the lifecycle owner is not ready.
I suppose that this part of code creates the problem
list?.let { prods ->
prods.forEach {
cartValue += it.price
}
cartVal.postValue(priceFormatter.formatPrice(cartValue))
} ?: cartVal.postValue(priceFormatter.formatPrice(0))
Probably, list is not null but is empty. Please try this:
if (list.isNullOrEmpty) {
list.forEach {
cartValue += it.price
}
cartVal.postValue(priceFormatter.formatPrice(cartValue))
} else {
cartVal.postValue(priceFormatter.formatPrice(0))
}

How to use LiveData for searching?

I have a ViewModel that has a MutableLiveData of an arraylist of class Course
private var coursesList: MutableLiveData<ArrayList<Course>> = MutableLiveData()
This coursesList is filled with data got from an API (by Retrofit): coursesList.postValue(response.body())
Now, a user can search for a course by its name. The function that I have for searching is that I iterate through the elements of the coursesList and check if its name is equal to what a user typed. It returns an arrayList with the courses that start with the name typed (this list is later sent to a fragment which passes it to an adapter to be shown in a recyclerview):
fun getCoursesList(): MutableLiveData<ArrayList<Course>> {
return coursesList
}
fun searchCourses(searchString: String): ArrayList<Course> {
val resultsList: ArrayList<Course> = ArrayList()
if (getCoursesList().value == null) return resultsList
if (getCoursesList().value!!.size > 0) {
for (course in getCoursesList().value!!.iterator()) {
if (course.name.toLowerCase(Locale.ROOT).startsWith(searchString)) {
resultsList.add(course)
}
}
}
resultsList.sortBy { it.price }
return resultsList
}
This function works and all but my instructor asked me to use LiveData for searching without giving any additional hints on how to do that.
So my question is how to use LiveData for searching? I tried to search for answers, I saw that some used LiveDataTransformations.switchMap but they were all using RoomDAOs and I couldn't adapt it to the code that I have.
Any help would be appreciated very much. Thanks in advance.
Maybe that can help you a little bit,
class YourViewModel(
private val courcesRepository: CourcesRepository
) : ViewModel() {
// Private access - mutableLiveData!
private val _coursesList = MutableLiveData<ArrayList<Course>>()
// Public access - immutableLiveData
val coursesList: LiveData<ArrayList<Course>>
get() = _coursesList
init {
// mutableLiveData initialize, automatic is immutable also initialize
_coursesList.postValue(getCourses())
}
// Here you get your data from repository
private fun getCourses(): ArrayList<Course> {
return courcesRepository.getCources()
}
// Search function
fun searchCourses(searchString: String) {
// you hold your data into this methode
val list: ArrayList<Course> = getCources()
if (searchString.isEmpty()) {
// here you reset the data if search string is empty
_coursesList.postValue(list)
} else {
// here you can search the list and post the new one to your LiveData
val filterList = list.filter {
it.name.toLowerCase(Locale.ROOT).startsWith(searchString)
}
filterList.sortedBy { it.price }
_coursesList.postValue(filterList)
}
}
}
The first tip is you should use LiveData like below, that is also recommended from google's jet pack team. The reason is so you can encapsulate the LivaData.
The second tip is you should use kotlin's idiomatic way to filter a list. Your code is readable and faster.
At least is a good idea to make a repository class to separate the concerns in your app.
And some useful links for you:
https://developer.android.com/jetpack/guide
https://developer.android.com/topic/libraries/architecture/livedata
I hope that's helpful for you
Ii is hard to guess the desired outcome, but a possible solution is to use live data for searched string also. And then combine them with coursesList live data into live data for searched courses, like this for example.
val searchStringLiveData: MutableLiveData<String> = MutableLiveData()
val coursesListLiveData: MutableLiveData<ArrayList<Course>> = MutableLiveData()
val searchedCourses: MediatorLiveData<ArrayList<Course>> = MediatorLiveData()
init {
searchedCourses.addSource(searchStringLiveData) {
searchedCourses.value = combineLiveData(searchStringLiveData, coursesListLiveData)
}
searchedCourses.addSource(coursesListLiveData) {
searchedCourses.value = combineLiveData(searchStringLiveData, coursesListLiveData)
}
}
fun combineLiveData(searchStringLiveData: LiveData<String>, coursesListLiveData: LiveData<ArrayList<Course>> ): ArrayList<Course> {
// your logic here to filter courses
return ArrayList()
}
I haven't run the code so I am not 100% sure that it works, but the idea is that every time either of the two live data changes value, searched string or courses, the combine function is executed and the result is set as value of the searchedCourses mediator live data. Also I omitted the logic of the filtering for simplicity.

Need help Kotlin Coroutines, Architecture Component and Retrofit

I'm trying to wrap my head around the mentioned components and I can't get it right. I want to do something very simple: Fetch data from the network and present it to the user. Currently am not yet caching it as am still learning new Coroutine features in Architecture components. Every time app loads I get an empty model posted, which seems weird.
My API is get hit fine and response is 200 which is OK.
Below is what I have attempted:
POJO
data class Profile(#SerializedName("fullname") val fullName : String.....)
Repository
class UserRepo(val context: Context, val api: Api) {
suspend fun getProfile(): Profile
{
val accessToken = ....
return api.getUserProfile(accessToken)
}
}
API
interface GatewayApi {
#GET("users/profile")
suspend fun getUserProfile(#Query("access-token") accessToken: String?): Profile
}
ViewModel
class UserViewModel(application: Application) : AndroidViewModel(application) {
private val usersRepo = UserRepo(application.applicationContext, Apifactory.Api)
val userProfileData = liveData{
emit(usersRepo.getProfile())
}
fun getProfile() = viewModelScope.launch {
usersRepo.getProfile()
}
}
Finally my fragment's relevant code
val viewModel = ViewModelProviders.of(activity!!).get(UserViewModel::class.java)
viewModel.userProfileData.observe(this, Observer<UserProfile> {
//it is having nulls
})
//trigger change
viewModel.getProfile()
So I added HTTP requests and responses (thanks to #CommonsWare for pointing that out) and it happened I had used a different model than I was supposed to use. The correct model that mapped the JSON response was ProfileResponse and as you can see in my posted code, I used Profile instead. So all fields were empty as Gson could not correctly serialize JSON into Profile object.
All the credit goes to #CommonsWare for pointing that out in comment.

Categories

Resources