I want to implement search functionality among lists of banks in my app.
So somehow I need to use stringResId() but you can't call it without composable func(). Also by using Resources.getSystem().getString() is giving me resources not found exception.
This is my viewModel code
class BankViewModel: ViewModel() {
private val _bankAccount = MutableStateFlow(BankAccount())
val bankAccount: StateFlow<BankAccount> = _bankAccount.asStateFlow()
var bankList = mutableStateOf(Banks)
private var cachedBankList = listOf<Bank>()
private var isSearchStarting = true
var isSearching = mutableStateOf(false)
fun updateBankSearch(searchName: String) {
_bankAccount.update { bankAccount ->
bankAccount.copy(bankName = searchName)
}
}
fun searchBankName(query: String) {
val listToSearch = if(isSearchStarting) {
bankList.value
}else {
cachedBankList
}
viewModelScope.launch {
if (query.isEmpty()) {
bankList.value = cachedBankList
isSearching.value = false
isSearchStarting = true
return#launch
}
val results = listToSearch.filter {
Resources.getSystem().getString(it.bankName).contains(query.trim(), ignoreCase = true)
}
if (isSearchStarting) {
cachedBankList = bankList.value
isSearchStarting = false
}
bankList.value = results
isSearching.value = true
}
}
}
This is my Bank
data class Bank (
#StringRes val bankName: Int,
#DrawableRes val bankLogo: Int = R.drawable.bank_image_2
)
So my question is how can I get a string by using id so that I can compare it with the query??
You need to use the Application Context to retrieve resources like these.
If you're not using any DI framework, or you don't want to use custom ViewModelProvider.Factory, extend AndroidViewModel (which holds a reference to Application) instead of regular ViewModel.
Ideally ViewModels shouldn't contain any Android-specific objects, and you would use some custom StringProvider, or something similar, but that's not a topic of your question.
Related
I have already created the search bar for the app, but the logic is
not working.
Here's the "error". I am getting each time i type into the search bar
W/RecordingIC: requestCursorUpdates is not supported
I don't know why I am getting that call or what is causing it to do that.
Here's the code for the search logic.
#HiltViewModel
class AnyViewModel #Inject constructor(
private val repository: AnywhereRepository
): ViewModel() {
private var _anyInfoResults = MutableStateFlow<List<AnywhereListEntity>>(emptyList())
val anyInfoResults = _anyInfoResults.asStateFlow()
private var cachedSearch = listOf<AnywhereListEntity>()
private var isSearchStarting = true
var isSearching = mutableStateOf(false)
init {
getAnyList()
}
fun searchAnywhereList(query: String){
val listToSearch = if(isSearchStarting){ _anyInfoResults.value }
else { cachedSearch }
viewModelScope.launch(Dispatchers.Default){
if (query.isEmpty()){
_anyInfoResults.value = cachedSearch
isSearching.value = false
isSearchStarting = true
return#launch
}
val results = listToSearch.filter {
it.name.contains(query, ignoreCase = true)
}
if (isSearchStarting){
cachedSearch = _anyInfoResults.value
isSearchStarting = false
}
_anyInfoResults.value = results
isSearching.value = true
}
}
}
I am looking for the search bar to filter out a list as the user is typing. For example, if the user types "a", the list will filter to only show names with the letter "a" in it.
I'll leave my GitHub link for further explanation and for testing it out.
GitHub link: https://github.com/OEThe11/AnywhereCE
I'm doing a small project to learn flow and the latest Android features, and I'm currently facing the viewModel's testing, which I don't know if I'm performing correctly. can you help me with it?
Currently, I am using a use case to call the repository which calls a remote data source that gets from an API service a list of strings.
I have created a State to control the values in the view model:
data class StringItemsState(
val isLoading: Boolean = false,
val items: List<String> = emptyList(),
val error: String = ""
)
and the flow:
private val stringItemsState = StringtemsState()
private val _stateFlow = MutableStateFlow(stringItemsState)
val stateFlow = _stateFlow.asStateFlow()
and finally the method that performs all the logic in the viewModel:
fun fetchStringItems() {
try {
_stateFlow.value = stringItemsState.copy(isLoading = true)
viewModelScope.launch(Dispatchers.IO) {
val result = getStringItemsUseCase.execute()
if (result.isEmpty()) {
_stateFlow.value = stringItemsState
} else {
_stateFlow.value = stringItemsState.copy(items = result)
}
}
} catch (e: Exception) {
e.localizedMessage?.let {
_stateFlow.value = stringItemsState.copy(error = it)
}
}
}
I am trying to perform the test following the What / Where / Then pattern, but the result is always an empty list and the assert verification always fails:
private val stringItems = listOf<String>("A", "B", "C")
#Test
fun `get string items - not empty`() = runBlocking {
// What
coEvery {
useCase.execute()
} returns stringItems
// Where
viewModel.fetchStringItems()
// Then
assert(viewModel.stateFlow.value.items == stringItems)
coVerify(exactly = 1) { viewModel.fetchStringItems() }
}
Can someone help me and tell me if I am doing it correctly? Thanks.
Did anyone implement google autocomplete suggestion text field or fragment in a jetpack compose project? If so kindly guide or share code snippets as I'm having difficulty in implementing it.
Update
Here is the intent that I'm triggering to open full-screen dialog, but when I start typing within it gets closed, and also I'm unable to figure out what the issue is and need a clue about handling on activity result for reading the result of the predictions within this compose function.
Places.initialize(context, "sa")
val fields = listOf(Place.Field.ID, Place.Field.NAME)
val intent = Autocomplete.IntentBuilder(
AutocompleteActivityMode.FULLSCREEN,fields).build(context)
startActivityForResult(context as MainActivity,intent, AUTOCOMPLETE_REQUEST_CODE, Bundle.EMPTY)
I am using the MVVM architecture and this is how I implemented it:
GooglePlacesApi
I've created an api for reaching google api named GooglePlacesApi
interface GooglePlacesApi {
#GET("maps/api/place/autocomplete/json")
suspend fun getPredictions(
#Query("key") key: String = <GOOGLE_API_KEY>,
#Query("types") types: String = "address",
#Query("input") input: String
): GooglePredictionsResponse
companion object{
const val BASE_URL = "https://maps.googleapis.com/"
}
}
The #Query("types") field is for specifiying what are you looking for in the query, you can look for establishments etc.
Types can be found here
Models
So I created 3 models for this implementation:
GooglePredictionsResponse
The way the response looks if you are doing a GET request with postman is:
Google Prediction Response
You can see that we have an object with "predictions" key so this is our first model.
data class GooglePredictionsResponse(
val predictions: ArrayList<GooglePrediction>
)
GooglePredictionTerm
data class GooglePredictionTerm(
val offset: Int,
val value: String
)
GooglePrediction
data class GooglePrediction(
val description: String,
val terms: List<GooglePredictionTerm>
)
I only needed that information, if you need anything else, feel free to modify the models or create your own.
GooglePlacesRepository
And finally we create the repository to get the information (I'm using hilt to inject my dependencies, you can ignore those annotations if not using it)
#ActivityScoped
class GooglePlacesRepository #Inject constructor(
private val api: GooglePlacesApi,
){
suspend fun getPredictions(input: String): Resource<GooglePredictionsResponse>{
val response = try {
api.getPredictions(input = input)
} catch (e: Exception) {
Log.d("Rently", "Exception: ${e}")
return Resource.Error("Failed prediction")
}
return Resource.Success(response)
}
}
Here I've used an extra class I've created to handle the response, called Resource
sealed class Resource<T>(val data: T? = null, val message: String? = null){
class Success<T>(data: T): Resource<T>(data)
class Error<T>(message: String, data:T? = null): Resource<T>(data = data, message = message)
class Loading<T>(data: T? = null): Resource<T>(data = data)
}
View Model
Again I'm using hilt so ignore annotations if not using it.
#HiltViewModel
class AddApartmentViewModel #Inject constructor(private val googleRepository: GooglePlacesRepository): ViewModel(){
val isLoading = mutableStateOf(false)
val predictions = mutableStateOf(ArrayList<GooglePrediction>())
fun getPredictions(address: String) {
viewModelScope.launch {
isLoading.value = true
val response = googleRepository.getPredictions(input = address)
when(response){
is Resource.Success -> {
predictions.value = response.data?.predictions!!
}
}
isLoading.value = false
}
}
fun onSearchAddressChange(address: String){
getPredictions(address)
}
}
If you need any further help let me know
I didn't include UI implementation because I assume it is individual but this is the easier part ;)
#Composable
fun MyComponent() {
val context = LocalContext.current
val intentLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) {
when (it.resultCode) {
Activity.RESULT_OK -> {
it.data?.let {
val place = Autocomplete.getPlaceFromIntent(it)
Log.i("MAP_ACTIVITY", "Place: ${place.name}, ${place.id}")
}
}
AutocompleteActivity.RESULT_ERROR -> {
it.data?.let {
val status = Autocomplete.getStatusFromIntent(it)
Log.i("MAP_ACTIVITY", "Place: ${place.name}, ${place.id}")
}
}
Activity.RESULT_CANCELED -> {
// The user canceled the operation.
}
}
}
val launchMapInputOverlay = {
Places.initialize(context, YOUR_API_KEY)
val fields = listOf(Place.Field.ID, Place.Field.NAME)
val intent = Autocomplete
.IntentBuilder(AutocompleteActivityMode.OVERLAY, fields)
.build(context)
intentLauncher.launch(intent)
}
Column {
Button(onClick = launchMapInputOverlay) {
Text("Select Location")
}
}
}
I'm trying to do a search system.
In my code I have a list of objects, each object has a name and also has a list of keywords like this:
data class Sample(
val name: String,
val keywords: List<String>,
// etc
)
Now jumping to where I do the search, I have the following code:
private lateinit var samples: List<Sample>
// etc
fun search(keyword: String) {
val samplesSearch: MutableList<Sample> = mutableListOf()
samples.forEach { sample ->
val sampleName = sample.name.toLowerCase(Locale.ROOT)
val sampleKeywords = sample.keywords.joinToString(
separator = ",",
transform = { it.toLowerCase(Locale.ROOT) }
)
if (sampleName.contains(keyword) || sampleKeywords.contains(keyword))
samplesSearch.add(sample)
}
}
That way, it works the way I want. But I have a doubt if it would be possible to get the same result, without using joinToString()...
Without transforming the list of keywords into a single string, the result is not the same because contains() works differently between string and list...
fun search(keyword: String) {
val samplesSearch: MutableList<Sample> = mutableListOf()
samples.forEach { sample ->
val sampleName = sample.name.toLowerCase(Locale.ROOT)
val sampleKeywords = sample.keywords.onEach { it.toLowerCase(Locale.ROOT) }
if (sampleName.contains(keyword) || sampleKeywords.contains(keyword))
samplesSearch.add(sample)
}
}
Using String.contains() just need to have a sequence of characters, without needing the full word.
Using List.contains() need to have the full word.
Update
After milgner's answer I got what I wanted as follows:
fun search(keyword: String) {
val samplesSearch: MutableList<Sample> = mutableListOf()
samples.forEach { sample ->
val sampleName = sample.name
val sampleKeywords = sample.keywords
if (sampleName.contains(keyword, ignoreCase = true) ||
sampleKeywords.any { it.contains(keyword, ignoreCase = true) }
) samplesSearch.add(sample)
}
}
List.contains will check every element in the list against the argument using the equals implementation. If you're looking for a case-insensitive variant, you can use a construct like
sampleKeywords.any { it.equals(keyword, ignoreCase = true) }
In ViewModel:
val drillerCatList: List<Int> = emptyList()
val shownCategoriesFlow = wordDao.getShownCategories() // which returns type Flow<List<CategoryItem>>
Catigory object:
data class CategoryItem(
val categoryName: String,
val categoryNumber: Int,
val categoryShown: Boolean = false,
#PrimaryKey(autoGenerate = true) var id: Int = 0
) : Parcelable {
}
How can I retrieve all categoryNumber values from shownCategoriesFlow: FLow<List> and fill drillerCatList: List with these values in ViewModel?
First make drillerCatList mutable as shown below:
val drillerCatList: ArrayList<Int> = ArrayList()
Now collect the list from the shownCategoriesFlow:
shownCategoriesFlow.collect {
it.forEach{ categoryItem ->
drillerCatList.add(categoryItem.categoryNumber)
}
}
First, your property either needs to be a MutableList or a var. Usually, var with a read-only List is preferable since it is less error-prone. Then you call collect on your Flow in a coroutine. collect behaves sort of like forEach does on an Iterable, except that it doesn't block the thread in between when elements are ready.
val drillerCatList: List<Int> = emptyList()
val shownCategoriesFlow = wordDao.getShownCategories()
init { // or you could put this in a function do do it passively instead of eagerly
viewModelScope.launch {
shownCategoriesFlow.collect { drillerCatList = it.map(CategoryItem::categoryNumber) }
}
}
Alternate syntax:
init {
shownCategoriesFlow
.onEach { drillerCatList = it.map(CategoryItem::categoryNumber) }
.launchIn(viewModelScope)
}