Replace query parameters in Uri.Builder in Android? - android

I'm passing around a Uri.Builder object as a mechanism for subclasses to fill in whatever parameters necessary into a Uri before it is executed in Android.
Problem is, one of the parameters that the base class adds using builder.appendQueryParameter("q",searchPhrase); needs to be replaced in the sub-class, but I can only find appendQueryParameter(), there is no replace or set method. appendQueryParameter() with the same parameter name adds another instance of the parameter, doesn't replace it.
Should I give up and try another way? Or is there a way to replace query parameters that I haven't found yet?

Since there is no in-built method, the best way I have found is to build a new Uri. You iterate over all the query parameters of the old Uri and then replace the desired key with the new value.
private static Uri replaceUriParameter(Uri uri, String key, String newValue) {
final Set<String> params = uri.getQueryParameterNames();
final Uri.Builder newUri = uri.buildUpon().clearQuery();
for (String param : params) {
newUri.appendQueryParameter(param,
param.equals(key) ? newValue : uri.getQueryParameter(param));
}
return newUri.build();
}

This will add a parameter, or replace an existing parameter's value in Kotlin.
Extension of Uri:
fun Uri.addUriParameter(key: String, newValue: String): Uri {
val params = queryParameterNames
val newUri = buildUpon().clearQuery()
var isSameParamPresent = false
for (param in params) {
// if same param is present override it, otherwise add the old param back
newUri.appendQueryParameter(param,
if (param == key) newValue else getQueryParameter(param))
if (param == key) {
// make sure we do not add new param again if already overridden
isSameParamPresent = true
}
}
if (!isSameParamPresent) {
// never overrode same param so add new passed value now
newUri.appendQueryParameter(key,
newValue)
}
return newUri.build()
}
Implementation:
val appendedURL = originalUri.addUriParameter("UID","123456")

Somewhat more concise way of doing what #bmjohns is suggesting.
fun Uri.addUriParameter(key: String, newValue: String): Uri =
with(buildUpon()) {
clearQuery()
queryParameterNames.forEach {
if (it != key) appendQueryParameter(it, getQueryParameter(it))
}
appendQueryParameter(key, newValue)
build()
}

/*
* Append or replace query parameters
*/
fun Uri.Builder.addQueryParameters(uri: Uri, params: Map<String, String>) = apply {
if (uri.query == null) {
appendQueryParameters(params)
} else {
clearQuery()
appendQueryParameters(params)
val names = params.keys
uri.queryParameterNames.forEach {
if (it !in names) appendQueryParameter(it, uri.getQueryParameter(it))
}
}
}
fun Uri.Builder.appendQueryParameters(params: Map<String, String>) = apply {
params.forEach { name, value ->
appendQueryParameter(name, value)
}
}

I have same problem ago, and decide to go another way.
Make an wrapper class and store queries (say setQueryParameter(key, value)) in Map (or ArrayMap, something like that).
Then wrapper instance's build() method processes original Builder's appendQueryParameter() and build()

With UrlQuerySanitizer -
val sanitizer = UrlQuerySanitizer(serviceUrl)
val paramVal = sanitizer.getValue("byCustomValue")
val replacedUrl = serviceUrl.replace(paramVal, "REPLACE_HERE")
Here is a simple method to encode a specific param -
fun encodeParameter(url: String, key: String): String {
val sanitizer = UrlQuerySanitizer(url)
return if (sanitizer.hasParameter(key)) {
val paramValue = sanitizer.getValue(key)
val encodedValue = try {
URLEncoder.encode(paramValue, "utf-8")
} catch (e: UnsupportedEncodingException) {
paramValue
}
url.replace(paramValue, encodedValue)
} else url
}

Related

How to handle two data type in the same API android

I have an API response. When data is purchased it gives as a JSONObject else a null string.
How do I process both the data types.
If I try to specify Any as the data type is model class, I am not able to retrieve the data in the JSONObject.
If it's just a null, you can simply check if the key exists before calling getString:
private fun JSONObject.getStringOrNull(key: String): String? {
return when {
this.has(key) -> try { getString(key) } catch (e: Exception) { null }
else -> null
}
}
#Test
fun runTest() {
val json = """
{ "name":"Bob Ross", "email":null }
""".trimIndent()
val obj = JSONObject(json)
val name = obj.getStringOrNull("name")
val email = obj.getStringOrNull("email")
println(name)
println(email)
}
You can use JsonElement to store the data in the model class.Since primitive data types also extend JsonElement

Search system using contains() in Kotlin and the result difference with string and list

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) }

Is there some function in Android SDK which is an analogue of jQuery.param()?

There is a function param() in jQuery library
It serializes JS-object into a string set of HTTP parameters
Example: object {a:'x',b:{c:[0,1]}}
$.param({a:'x',b:{c:[0,1]}})
"a=x&b%5Bc%5D%5B%5D=0&b%5Bc%5D%5B%5D=1"
This string is encoded by encodeURIComponent()
It could be decoded by decodeURIComponent()
decodeURIComponent($.param({a:'x',b:{c:[0,1]}}))
"a=x&b[c][]=0&b[c][]=1"
We see that the object is converted into a set of parameters:
a=x
b[c][]=0
b[c][]=1
I need to do the same serialization in my Android application
I have to send data in the same format as is being done by jQuery
So the question is:
Is there some function in Android SDK which is an analogue of jQuery.param() ?
While people stays silent I offer my handmade solution on Kotlin:
fun serialize(data: JSONObject): String {
var result = arrayListOf<String>()
fun parse(value: Any, name: String, alt: String? = null) {
if (value is JSONObject) {
val keys = value.keys()
while (keys.hasNext())
keys.next().let {
parse(value[it], "$name[$it]")
}
} else if (value is JSONArray) {
for (i in 0 until value.length())
parse(value[i], "$name[$i]", "$name[]")
} else {
result.add(Uri.encode(alt ?: name) + "=" + Uri.encode("$value"))
}
}
val keys = data.keys()
while (keys.hasNext())
keys.next().let {
parse(data[it], it)
}
return result.joinToString("&")
}
This variant works identically to jQuery. It would be nice to avoid such homework though :-)

How to combine two live data one after the other?

I have next use case: User comes to registration form, enters name, email and password and clicks on register button. After that system needs to check if email is taken or not and based on that show error message or create new user...
I am trying to do that using Room, ViewModel and LiveData. This is some project that on which I try to learn these components and I do not have remote api, I will store everything in local database
So I have these classes:
RegisterActivity
RegisterViewModel
User
UsersDAO
UsersRepository
UsersRegistrationService
So the idea that I have is that there will be listener attached to register button which will call RegisterViewModel::register() method.
class RegisterViewModel extends ViewModel {
//...
public void register() {
validationErrorMessage.setValue(null);
if(!validateInput())
return;
registrationService.performRegistration(name.get(), email.get(), password.get());
}
//...
}
So that is the basic idea, I also want for performRegistration to return to me newly created user.
The thing that bothers me the most is I do not know how to implement performRegistration function in the service
class UsersRegistrationService {
private UsersRepository usersRepo;
//...
public LiveData<RegistrationResponse<Parent>> performRegistration(String name, String email, String password) {
// 1. check if email exists using repository
// 2. if user exists return RegistrationResponse.error("Email is taken")
// 3. if user does not exists create new user and return RegistrationResponse(newUser)
}
}
As I understand, methods that are in UsersRepository should return LiveData because UsersDAO is returning LiveData
#Dao
abstract class UsersDAO {
#Query("SELECT * FROM users WHERE email = :email LIMIT 1")
abstract LiveData<User> getUserByEmail(String email);
}
class UsersRepository {
//...
public LiveData<User> findUserByEmail(String email) {
return this.usersDAO.getUserByEmail(email);
}
}
So my problem is how to implement performRegistration() function and how to pass value back to view model and then how to change activity from RegisterActivity to MainActivity...
You can use my helper method:
val profile = MutableLiveData<ProfileData>()
val user = MutableLiveData<CurrentUser>()
val title = profile.combineWith(user) { profile, user ->
"${profile.job} ${user.name}"
}
fun <T, K, R> LiveData<T>.combineWith(
liveData: LiveData<K>,
block: (T?, K?) -> R
): LiveData<R> {
val result = MediatorLiveData<R>()
result.addSource(this) {
result.value = block(this.value, liveData.value)
}
result.addSource(liveData) {
result.value = block(this.value, liveData.value)
}
return result
}
With the help of MediatorLiveData, you can combine results from multiple sources. Here an example of how would I combine two sources:
class CombinedLiveData<T, K, S>(source1: LiveData<T>, source2: LiveData<K>, private val combine: (data1: T?, data2: K?) -> S) : MediatorLiveData<S>() {
private var data1: T? = null
private var data2: K? = null
init {
super.addSource(source1) {
data1 = it
value = combine(data1, data2)
}
super.addSource(source2) {
data2 = it
value = combine(data1, data2)
}
}
override fun <S : Any?> addSource(source: LiveData<S>, onChanged: Observer<in S>) {
throw UnsupportedOperationException()
}
override fun <T : Any?> removeSource(toRemove: LiveData<T>) {
throw UnsupportedOperationException()
}
}
here is the gist for above, in case it is updated on the future:
https://gist.github.com/guness/0a96d80bc1fb969fa70a5448aa34c215
One approach is to use flows for this.
val profile = MutableLiveData<ProfileData>()
val user = MutableLiveData<CurrentUser>()
val titleFlow = profile.asFlow().combine(user.asFlow()){ profile, user ->
"${profile.job} ${user.name}"
}
And then your Fragment/Activity:
viewLifecycleOwner.lifecycleScope.launch {
viewModel.titleFlow.collectLatest { title ->
Log.d(">>", title)
}
}
One advantage to this approach is that titleFlow will only emit value when both live datas have emitted at least one value. This interactive diagram will help you understand this https://rxmarbles.com/#combineLatest
Alternative syntax:
val titleFlow = combine(profile.asFlow(), user.asFlow()){ profile, user ->
"${profile.job} ${user.name}"
}
Jose Alcérreca has probably the best answer for this:
fun blogpostBoilerplateExample(newUser: String): LiveData<UserDataResult> {
val liveData1 = userOnlineDataSource.getOnlineTime(newUser)
val liveData2 = userCheckinsDataSource.getCheckins(newUser)
val result = MediatorLiveData<UserDataResult>()
result.addSource(liveData1) { value ->
result.value = combineLatestData(liveData1, liveData2)
}
result.addSource(liveData2) { value ->
result.value = combineLatestData(liveData1, liveData2)
}
return result
}
without custom class
MediatorLiveData<Pair<Foo?, Bar?>>().apply {
addSource(fooLiveData) { value = it to value?.second }
addSource(barLiveData) { value = value?.first to it }
}.observe(this) { pair ->
// TODO
}
I did an approach based on #guness answer. I found that being limited to two LiveDatas was not good. What if we want to use 3? We need to create different classes for every case. So, I created a class that handles an unlimited amount of LiveDatas.
/**
* CombinedLiveData is a helper class to combine results from multiple LiveData sources.
* #param liveDatas Variable number of LiveData arguments.
* #param combine Function reference that will be used to combine all LiveData data results.
* #param R The type of data returned after combining all LiveData data.
* Usage:
* CombinedLiveData<SomeType>(
* getLiveData1(),
* getLiveData2(),
* ... ,
* getLiveDataN()
* ) { datas: List<Any?> ->
* // Use datas[0], datas[1], ..., datas[N] to return a SomeType value
* }
*/
class CombinedLiveData<R>(vararg liveDatas: LiveData<*>,
private val combine: (datas: List<Any?>) -> R) : MediatorLiveData<R>() {
private val datas: MutableList<Any?> = MutableList(liveDatas.size) { null }
init {
for(i in liveDatas.indices){
super.addSource(liveDatas[i]) {
datas[i] = it
value = combine(datas)
}
}
}
}
You can define a method that would combine multiple LiveDatas using a MediatorLiveData, then expose this combined result as a tuple.
public class CombinedLiveData2<A, B> extends MediatorLiveData<Pair<A, B>> {
private A a;
private B b;
public CombinedLiveData2(LiveData<A> ld1, LiveData<B> ld2) {
setValue(Pair.create(a, b));
addSource(ld1, (a) -> {
if(a != null) {
this.a = a;
}
setValue(Pair.create(a, b));
});
addSource(ld2, (b) -> {
if(b != null) {
this.b = b;
}
setValue(Pair.create(a, b));
});
}
}
If you need more values, then you can create a CombinedLiveData3<A,B,C> and expose a Triple<A,B,C> instead of the Pair, etc. Just like in https://stackoverflow.com/a/54292960/2413303 .
EDIT: hey look, I even made a library for you that does that from 2 arity up to 16: https://github.com/Zhuinden/livedata-combinetuple-kt
Many of these answers work, but also it is assumed the LiveData generic types are not-nullable.
But what if one or more of the given input types are nullable types (given the default Kotlin upper bound for generics is Any?, which is nullable)?
The result would be even though the LiveData emitter would emit a value (null), the MediatorLiveData will ignore it, thinking it's his own child live data value not being set.
This solution, instead, takes care of it by forcing the upper bound of the types passed to the mediator to be not null. Lazy but needed.
Also, this implementation avoids same-value after the combiner function has been called, which might or might not be what you need, so feel free to remove the equality check there.
fun <T1 : Any, T2 : Any, R> combineLatest(
liveData1: LiveData<T1>,
liveData2: LiveData<T2>,
combiner: (T1, T2) -> R,
): LiveData<R> = MediatorLiveData<R>().apply {
var first: T1? = null
var second: T2? = null
fun updateValueIfNeeded() {
value = combiner(
first ?: return,
second ?: return,
)?.takeIf { it != value } ?: return
}
addSource(liveData1) {
first = it
updateValueIfNeeded()
}
addSource(liveData2) {
second = it
updateValueIfNeeded()
}
}
LiveData liveData1 = ...;
LiveData liveData2 = ...;
MediatorLiveData liveDataMerger = new MediatorLiveData<>();
liveDataMerger.addSource(liveData1, value -> liveDataMerger.setValue(value));
liveDataMerger.addSource(liveData2, value -> liveDataMerger.setValue(value));
if you want both value not null
fun <T, V, R> LiveData<T>.combineWithNotNull(
liveData: LiveData<V>,
block: (T, V) -> R
): LiveData<R> {
val result = MediatorLiveData<R>()
result.addSource(this) {
this.value?.let { first ->
liveData.value?.let { second ->
result.value = block(first, second)
}
}
}
result.addSource(liveData) {
this.value?.let { first ->
liveData.value?.let { second ->
result.value = block(first, second)
}
}
}
return result
}
If you want to create a field and setup at construction time (use also):
val liveData1 = MutableLiveData(false)
val liveData2 = MutableLiveData(false)
// Return true if liveData1 && liveData2 are true
val liveDataCombined = MediatorLiveData<Boolean>().also {
// Initial value
it.value = false
// Observing changes
it.addSource(liveData1) { newValue ->
it.value = newValue && liveData2.value!!
}
it.addSource(selectedAddOn) { newValue ->
it.value = liveData1.value!! && newValue
}
}
Solved with LiveData extensions
fun <T, R> LiveData<T>.map(action: (t: T) -> R): LiveData<R> =
Transformations.map(this, action)
fun <T1, T2, R> LiveData<T1>.combine(
liveData: LiveData<T2>,
action: (t1: T1?, t2: T2?) -> R
): LiveData<R> =
MediatorLiveData<Pair<T1?, T2?>>().also { med ->
med.addSource(this) { med.value = it to med.value?.second }
med.addSource(liveData) { med.value = med.value?.first to it }
}.map { action(it.first, it.second) }
Java version, if anyone else is stuck working on some old project
var fullNameLiveData = LiveDataCombiner.combine(
nameLiveData,
surnameLiveData,
(name, surname) -> name + surname
)
public class LiveDataCombiner<First, Second, Combined> {
private First first;
private Second second;
private final MediatorLiveData<Combined> combined = new MediatorLiveData<>();
private final BiFunction<First, Second, Combined> combine;
public LiveData<Combined> getCombined() {
return combined;
}
public static <First, Second, Combined>LiveDataCombiner<First, Second, Combined> combine(
LiveData<First> firstData,
LiveData<Second> secondData,
BiFunction<First, Second, Combined> combine
) {
return new LiveDataCombiner<>(firstData, secondData, combine);
}
private LiveDataCombiner(
LiveData<First> firstData,
LiveData<Second> secondData,
BiFunction<First, Second, Combined> combine
) {
this.combine = combine;
addSource(firstData, value -> first = value);
addSource(secondData, value -> second = value);
}
private <T> void addSource(LiveData<T> source, Consumer<T> setValue) {
combined.addSource(source, second -> {
setValue.accept(second);
emit(combine());
});
}
private Combined combine() {
return combine.apply(first, second);
}
private void emit(Combined value) {
if (combined.getValue() != value)
combined.setValue(value);
}
}

returning value no set

I'm very new to Kotlin\Android so this is probably an obvious mistake but I am stuck and need some help.
What I am doing is getting a user to enter their name, taking that name and check against a mySQL DB (this works fine), and then depending on their authorization level return a 0 or 1, this is the part I'm having issues with.
I have tried using return, creating a global class, adding to a mutable list and setting it but nothing works.
In Main I have this.
var useName:String = editText.text.toString()
verifiedID = verifyUser(useName) //Call the function here
// verifiedID = Global.validLogin //Was used to test Global class
ET_HelloW.setText("$useName ID $verifiedID")
In the verify user class
var ucounter = 0
fun verifyUser(userName: String) :Int {
//getting the record values
val userName = userName
var uname: String = ""
var uauth: String = ""
//creating volley string request
// Checking to see if the Username entered exists in the DBEd
val stringRequest = object : StringRequest(Request.Method.POST, PHPPost.URL_CHECK_LOGIN,
Response.Listener<String> { response ->
try {
val obj = JSONObject(response)
if (!obj.getBoolean("error")){
val array = obj.getJSONArray("credens")
for (i in 0..array.length() - 1) {
val objectArtist = array.getJSONObject(i)
uname = objectUser.getString("name")
uauth = objectUser.getString("auth_level")
}
if (uauth.toInt()<2)
{
Global.validLogin = 0 //This sets
ucounter = 0 //This sets
}else{
Global.validLogin = 1 //This sets when checking code but is not picked up when called in Main
ucounter = 1 //Debugger shows that I do reach here and that the var is set, but resets to 0 at the point of returning.
}
}else{
}
} catch (e: JSONException) {
e.printStackTrace()
}
},
object : Response.ErrorListener {
override fun onErrorResponse(volleyError: VolleyError) {
}
}) {
#Throws(AuthFailureError::class)
override fun getParams(): Map<String, String> {
val params = HashMap<String, String>()
params.put("name", userName)
return params
}
}
//adding request to queue
VolleySingleton.instance?.addToRequestQueue(stringRequest)
return ucounter //always returns zero
// return 1 // If set manually this will be returned.
}
I'm pretty sure that the value is being reset to 0 before it gets to the return at the end but even when I set it as a global it did not work.
Again am sure its a rookie mistake but would appreciate some help.

Categories

Resources