I would like to check whether the bundle has the specified key.
Is there any modern way using kotlin to check it?
For now I was using
if(bundle.containsKey(Extras.PRODUCT){
bundle.getParcelable<Product>(Extras.PRODUCT)?.let{
mpresenter.mProduct = it
}
}
if(bundle.containsKey(Extras.ANIMAL){
bundle.getParcelable<ANIMAL>(Extras.ANIMAL)?.let{
mpresenter.mAnimal = it
}
}
... an so on
its okay if I only check one value of the Extras. But What if I have 10 or more variable in presenter. Is there any simpler solution for my case?
You could make some extension functions like
fun <T : Parcelable?> Bundle.tryGetParcelable(key: String): T? =
// getParcelable would return null anyway, but this is a general example
if (containsKey(key)) getParcelable<T>(key) else null
bundle.tryGetParcelable<Product>(Extras.PRODUCT)?.let { mPresenter.mProduct = it }
If that's still too wordy, you can pass property references and call set on those, like this:
// Upper bound isn't nullable now, since we're only assigning if the value is non-null
fun <T : Parcelable> Bundle.tryAssign(key: String, property: KMutableProperty0<T>) {
tryGetParcelable<T>(key)?.let { property.set(it) } // or let(property::set)
}
bundle.tryAssign<Product>(Extras.PRODUCT, mPresenter::mProduct)
but you might want to make the property the receiver instead, so it reads more like the usual thing = whatever
fun <T : Parcelable> KMutableProperty0<T>.tryAssign(bundle: Bundle, key: String) {
bundle.tryGetParcelable<T>(key)?.let { set(it) } // or run(::set)
}
mPresenter::mProduct.tryAssign<Product>(bundle, Extras.PRODUCT)
You'll need to make tryGetX functions for each type of Bundle getter you need, getString returns nulls but getInt always returns an Int, so it helps to have handlers that do the containsKey check so you don't need to worry about coming up with suitable, reserved default values for "not present".
If you make all those basic getter functions the same way (returning nullables) you can reuse that tryAssign function if you like, passing in the appropriate getter:
// Not using this here but it's the same getter signature, (Bundle, String) -> T?
// Note that because we're going to be passing references to these functions, we can't
// define them as extension functions in the same file - so the Bundle is a parameter now
fun tryGetString(bundle: Bundle, key: String): String? {
return bundle.getString(key)
}
// Now we're passing in the getter function we want to use, which returns a T?
// T doesn't have a Parcelable upper bound anymore
fun <T> KMutableProperty0<T>.tryAssign(bundle: Bundle, key: String, tryGet: (Bundle, String) -> T?) {
tryGet(bundle, key)?.run(::set)
}
// you won't need the type in diamond brackets, it's just for illustration
mPresenter::mProduct.tryAssign<Product>(bundle, Extras.PRODUCT, ::tryGetParcelable)
I mean, this is starting to get a bit intense, but if you have a lot of stuff to assign it might be worth having it cleanly ordered like this? Some stuff to try anyway!
Jetpack Navigation with Safe Args is now the recommended for navigating and passing data
Related Codelab Android Navigation
The navigation component has a Gradle plugin, called safe args, that
generates simple object and builder classes for type-safe access to
arguments specified for destinations and actions.
Safe args allows you to get rid of code like this when passing values
between destinations:
val username = arguments?.getString("usernameKey") And, instead,
replace it with code that has generated setters and getters.
val username = args.username
Related
Bundle.get(key: String) is deprecated:
Use the type-safe specific APIs depending on the type of the item to be retrieved, eg. getString(String).
So how to solve this in this method? Don't think I can use generic T here because the Bundle can contain multiple different types. I feel like I have to know the type of the key I'm trying to retrieve before retrieving it, is that even possible?
private fun Bundle.toMap(): Map<String?, *> {
val map = mutableMapOf<String?, Any?>()
for (key in keySet()) {
map[key] = get(key)
}
return map
}
In the code below, i'd like to generalize it so I instead of viewBinding.editText.text and viewModel.property.price can use the same method for e.g viewBinding.secondEditText.text and viewModel.property.income.
I'm thinking exchanging viewBinding.editText.text for a variable defined in the primary constructor, but then I'd need the variable to contain a reference to viewBinding.editText.text/viewBinding.secondEditText.text etc. instead of containing a value.
Is this possible? I've looked at lengths for this but can't find anything useful.
fun updateProperty() {
//... other irrelevant code
if (viewBinding.editText.text.toString() != "") {
viewModel.property.price = viewBinding.editText.text.toString().toDouble()
}
//... other irrelevant code
}
You can pass parameters into a function, yeah!
This is the easy one:
fun updateProperty(editText: EditText) {
val contents = editText.text.toString()
}
simple enough, you just pass in whatever instance of an EditText and the function does something with it.
If you're just using objects with setters and getters, you can just define the type you're going to be using and pass them in. Depending on what viewmodel.property is, you might be able to pass that in as well, and access price and income on it. Maybe use an interface or a sealed class if there are other types you want to use - they need some commonality if you're going to be using a generalised function that works with them all.
Properties are a bit tricker - assuming viewmodel.property contains a var price: Double, and you didn't want to pass in property itself, just a Double that exists somewhere, you can do it like this:
import kotlin.reflect.KMutableProperty0
var wow: Double = 1.2
fun main() {
println(wow)
setVar(::wow, 6.9)
println(wow)
}
fun setVar(variable: KMutableProperty0<Double>, value: Double) {
variable.set(value)
}
>> 1.2
>> 6.9
(see Property references if you're not familiar with the :: syntax)
KMutableProperty0 represents a reference to a mutable property (a var) which doesn't have any receivers - just a basic var. And don't worry about the reflect import, this is basic reflection stuff like function references, it's part of the base Kotlin install
Yes, method parameters can also be references to classes or interfaces. And method parameters can also be references to other methods/functions/lambdas.
If you are dealing with cases that are hard to generalize, consider using some kind of inversion of control (function as parameter or lambda).
You add a lambda parameter to your updateProperty function
fun updateProperty(onUpdate: (viewBinding: YourViewBindingType, viewModel: YourViewModelType) -> Unit) {
//... other irrelevant code
// here you just call the lambda, with any parameters that might be useful 'on the other side'
onUpdate(viewBinding, viewModel)
//... other irrelevant code
}
Elsewhere in code - case 1:
updateProperty() { viewBinding, viewModel ->
if (viewBinding.editText.text.toString() != "") {
viewModel.property.price = viewBinding.editText.text.toString().toDouble()
}
}
Elsewhere in code - case 2:
updateProperty() { viewBinding, viewModel ->
if (viewBinding.secondEditText.text.toString() != "") {
viewModel.property.income = viewBinding.secondEditText.text.toString().toDouble()
}
}
Elsewhere in code - case 3:
updateProperty() { viewBinding, viewModel ->
// I am a totally different case, because I have to update two properties at once!
viewModel.property.somethingElse1 = viewBinding.thirdEditText.text.toString().toBoolean()
viewModel.property.somethingElse2 = viewBinding.fourthEditText.text
.toString().replaceAll("[- ]*", "").toInt()
}
You could then go even further and define a function for the first 2 cases, since those 2 can be generalized, and then call it inside the lambda (or even pass it as the lambda), which would save you some amount of code, if you call updateProperty() in many places in your code or simply define a simple function for each of them, and call that instead, like this
fun updatePrice() = updateProperty() { viewBinding, viewModel ->
if (viewBinding.editText.text.toString() != "") {
viewModel.property.price = viewBinding.editText.text.toString().toDouble()
}
}
fun updateIncome() = updateProperty() { viewBinding, viewModel ->
if (viewBinding.secondEditText.text.toString() != "") {
viewModel.property.income = viewBinding.secondEditText.text.toString().toDouble()
}
}
Then elsewhere in code you just call it in a really simple way
updatePrice()
updateIncome()
I am making a list of observable LiveData objects, that should contain Resource object (https://developer.android.com/topic/libraries/architecture/guide.html#addendum). I don't care what type of data that Resource object is containing.
abstract class LiveResources : LiveData<Resource<Any>>() {
private val mediatorLiveData = MediatorLiveData<Resource<Any>>()
protected val resources = HashSet<LiveData<Resource<Any>>>()
fun addResource(source: LiveData<Resource<Any>>) {
resources.add(source)
mediatorLiveData.addSource(source, resourceObserver)
}
fun removeResource(source: LiveData<Resource<Any>>) {
resources.remove(source)
mediatorLiveData.removeSource(source)
}
private val resourceObserver = Observer<Resource<Any>> {
onSourceChange()
}
abstract fun onSourceChange()
}
Unfortunately when I try to use LiveResources.addResource() with LiveData<Resource<List<String>>> I get TypeMismatch error in my IDE, saying that LiveData<Resource<Any>> was expected.
Your Resource (and/or LiveData) class should be defined with generic covariance in order to make it work. Like so:
class Resource<out T> // <- out marks generic type as covariant
Haven't tried it, but I think this would work
fun <T:Any> addResource(source: LiveData<Resource<T>>)
You should generify the classes to accept Resource<T> i.e LiveData<Resource<T>>. Any is the covariance of any object passed, but I think you are not trying to achieve that.
Another friendly advice is that you don't need to add another abstraction on top of MediatorLiveData that solely does the same you have implemented.
Given an interface method like this (Android Retrofit), how do I read the URL path specified in the annotation argument from Kotlin code at runtime?
ApiDefinition interface:
#GET("/api/somepath/objects/")
fun getObjects(...)
Read the annotation value:
val method = ApiDefinition::getObjects.javaMethod
val verb = method!!.annotations[0].annotationClass.simpleName ?: ""
// verb contains "GET" as expected
// But how to get the path specified in the annotation?
val path = method!!.annotations[0].????????
UPDATE 1
Thanks for answers. I'm still struggling as I can't see what type to use to do the following:
val apiMethod = ApiDefinition::getObjects
.... then to pass that function reference into a method like this (it's reused)
private fun getHttpPathFromAnnotation(method: Method?) : String {
val a = method!!.annotations[0].message
}
IntelliJ IDE is suggesting I use KFunction5<> as a function parameter type (it doesn't exist as far as I can see) and seems to be requiring I specify all the parameter types for the method too, which makes a generic call to get the annotation attribute impossible. Isn't there a Kotlin equivalent of "Method"?, a type that will accept any method? I tried KFunction, without success.
UPDATE 2
Thanks for clarifying things. I've got to this point:
ApiDefinition (Retrofit interface)
#GET(API_ENDPOINT_LOCATIONS)
fun getLocations(#Header(API_HEADER_TIMESTAMP) timestamp: String,
#Header(API_HEADER_SIGNATURE) encryptedSignature: String,
#Header(API_HEADER_TOKEN) token: String,
#Header(API_HEADER_USERNAME) username: String
): Call<List<Location>>
Method to retrieve annotation argument:
private fun <T> getHttpPathFromAnnotation(method: KFunction<T>) : String {
return method.annotations.filterIsInstance<GET>().get(0).value
}
Call to get the path argument for a specific method:
val path = getHttpPathFromAnnotation<ApiDefinition>(ApiDefinition::getLocations as KFunction<ApiDefinition>)
The implicit cast seems to be necessary or the type parameter demands I provide a KFunction5 type.
This code works, but it has the GET annotation hard-coded, is there a way to make it more generic? I suspect I might need to look for GET, POST and PUT and return the first match.
Use the Kotlin KFunction directly instead of javaMethod (you're using Kotlin anyway!), and findAnnotation for concise, idiomatic code.
This will also work if the annotation happens to not be the first, where annotations[0] may break.
val method = ApiDefinition::getObjects
val annotation = method.findAnnotation<GET>() // Will be null if it doesn't exist
val path = annotation?.path
Basically all findAnnotation does is return
annotations.filterIsInstance<T>().firstOrNull()
I'm new to Kotlin and I don't know why compiler complains about this code:
data class Test(var data : String = "data")
fun test(){
var test: Test? = Test("")
var size = test?.data.length
}
Compiler complains with test?.data.length, it says that I should do: test?.data?.length. But data variable is String, not String?, so I don't understand why I have to put the ? when I want to check the length.
The expression test?.data.length is equivalent to (test?.data).length, and the test?.data part is nullable: it is either test.data or null. Therefore it is not null-safe to get its length, but instead you should use the safe call operator again: test?.data?.length.
The nullability is propagated through the whole calls chain: you have to write these chains as a?.b?.c?.d?.e (which is, again, equivalent to (((a?.b)?.c)?.d)?.e), because, if one of the left parts is null, the rest of the calls cannot be performed as if the value is not-null.
If you don't want to use safe call before each non-nullable component of the call chain, you can get the result of the first safe call into a new variable with the standard extension functions run or let:
// `this` is non-nullable `Test` inside lambda
val size = test?.run { data.length }
// or: `it` is non-nullable `Test` inside lambda
val size = test?.let { it.data.length }
Note that size is still nullable Int? here.