just started learning Kotlin. I'm trying to use okhttp to send a simple get request to a URL that contains only text.
I want the output of the request stored in a liveData variable, but when I run it, it crashes. Here's the class:
// gradle dependency added to build.gradle:
// implementation("com.squareup.okhttp3:okhttp:4.5.0")
//
// added this permission to AndroidManifest.xml just above the "application" section
// <uses-permission android:name="android.permission.INTERNET" />
//
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.IOException
class GetExample {
private val client = OkHttpClient()
private val _theResult = MutableLiveData<String?>()
val theResult: LiveData<String?> = _theResult
#Throws(IOException::class)
fun getText(url: String) {
val request = Request.Builder().url(url).build()
try {
client.newCall(request).execute()
.use { response -> _theResult.value = response.body?.string() }
} catch (e: IOException) {
_theResult.value = e.message
}
}
}
And to call this I'm using
val url = "https://raw.github.com/square/okhttp/master/README.md"
GetExample().getText(url)
and accessing the result with
var thisString: String? = GetExample().theResult.value
Help greatly appreciated
Lets break down a little what your code does, shall we?
val url = "https://raw.github.com/square/okhttp/master/README.md"
GetExample().getText(url)
var thisString: String? = GetExample().theResult.value
You first assign the url variable to be a github link. Then, you construct a new GetExample object and call getText on it, with the url parameter.
But now, you are assigning thisString to a new instance of GetExample, which means it doesn't contain the data from the object you called getText on.
To fix this problem, one might write something like this:
val url = "https://raw.github.com/square/okhttp/master/README.md"
val getter = GetExample()
getter.getText(url)
var thisString: String? = getter.theResult.value
What george said is true as well, but I haven't tested that so you need to take a look if that is a problem as well.
You are trying to execute this on the UI thread. That will not work.
Just try to run it on another thread, like the IO Thread,
and use postValue in liveData. Otherwise, you need to set the value on the UI thread.
E.g.,
try {
runBlocking(IO) {
client.newCall(request).execute()
.use { response -> _theResult.postValue(response.body?.string()) }
}
} catch (e: IOException) {
_theResult.value = e.message
}
Related
I'm making an app which gets the (pseudo) latency values by making a request to some urls and recording how long that will take.
First, I use retrofit to get a JSON response from a web server. This response contains: the name of the host (e.g. Ebay UK), the url of the host (e.g. www.ebay.co.uk), and an image url. I map this response onto my data class which looks like the following:
data class(
val name: String,
var url: String,
val icon: String,
var averagePing: Long = -1
)
url is a var property as before making the calls to get the latency values, I need to add https:// in order to make the request.
I'm doing all of this like so:
fun getHostsLiveData() {
viewModelScope.launch(Dispatchers.IO) {
val hostList = repo.getHosts()
for (host in hostList) {
host.url = "https://" + host.url
host.averagePing = -1
}
hostListLiveData.postValue(hostList)//updated the recyclerview with initial values
//with default (-1) value of averagePing
for (host in hostList) {
async { pingHostAndUpdate(host.url, hostList) }
}
}
}
The first for loop prepares my data. The line after the for loop submits the data to the recycler adapter, in order to show the host name, url and icon straight away (this all works i.e. I have a working observer for the LiveData), while I'm waiting for the latency values.
The second for loop calls the function to calculate the latency values for each host and the updateHostList() function updates the LiveData.
This is how the functions look:
suspend fun pingHostAndUpdate(url: String, hostList: MutableList<Host>) {
try {
val before = Calendar.getInstance().timeInMillis
val connection = URL(url).openConnection() as HttpURLConnection //Need error handling
connection.connectTimeout = 5*1000
connection.connect()
val after = Calendar.getInstance().timeInMillis
connection.disconnect()
val diff = after - before
updateHostList(url, diff, hostList)
} catch (e: MalformedURLException) {
Log.e("MalformedURLExceptionTAG", "MalformedURLException")
} catch (e: IOException) {
Log.e("IOExceptionTAG", "IOException")
}
}
fun updateHostList(url: String, pingResult: Long, hostList: MutableList<Host>) {
//All this on mainThread
var foundHost: Host? = null
var index = 0
for (host in hostListLiveData.value!!) {
if (host.url == url) {
foundHost = host
break
}
index++
}
if (foundHost != null) {
viewModelScope.launch(Dispatchers.Main) {
val host = Host(foundHost.name, foundHost.url, foundHost.icon, pingResult)
Log.d("TAAAG", "$host")
hostList[index] = host
hostListLiveData.value = hostList
}
}
}
All of this happens in the viewModel. Currently I'm updating my list by submitting the entire list again when I change one property of one element of the list, which seems horrible to me.
My question is: How can I update only one property of host and have it refresh the UI automatically?
Thanks in advance
Edit: My observer looks like this:
viewModel.hostListLiveData.observe(this, Observer { adapter.updateData(it) })
And updateData() looks like this:
fun updateData(freshHostList: List<Host>) {
hostList.clear()
hostList.addAll(freshHostList)
notifyDataSetChanged()
}
#ArpitShukla, do you suggest I would have 2 update functions? one for showing the initial list and another to update on item of the list? Or would I just put both notifyDataSetChanged() and notifyItemChanged() in updateData()?
Edit2: changed my function call to make it async.
You can consider to update items observed from hostListLiveData using notifyItemChanged(position) instead notifyDataSetChanged() in your adapter.
notifyItemChanged(position) is an item change event, which update only the content of the item.
EDIT:
You're using notifyDataSetChanged() on updating the content of data which causing to relayout and rebind the RecyclerView which you're not expecting. Therefore you should update the content of your data using notifyItemChanged(position).
I think you may create a new function for updating your RecyclerView in the adapter e.g.
fun updateHostAndPing(updatedHost: Host, position: Int) {
hostList[position].apply {
url = updatedHost.url
averagePing = updatedHost.averagePing
}
notifyItemChanged(position)
}
and in your observer, you may need to check whether it is fresh list or and updated list
viewModel.hostListLiveData.observe(this, Observer {
if (adapter.itemCount == ZERO) {
adapter.updateData(it)
} else {
it.forEachIndexed { index, host ->
adapter.updateHostAndPing(host, index)
}
}
})
In my multi-verticle application, I would like to load the config once and then inject the resulting JsonObject into each verticle using koin. The problem is that the ConfigRetriever doesn't really behave the way I would expect it to. Consider the following example:
class MainVerticle : AbstractVerticle() {
override fun start() {
val retriever = ConfigRetriever.create(vertx)
val config = ConfigRetriever.getConfigAsFuture(retriever).result()
println(config)
}
}
Intuitively I would expect this to load the config file under /resources/conf/config.json and print all the key/value pairs. Instead of doing that, it prints null. However, if I change the third line to:
val retriever = ConfigRetriever.create(Vertx.vertx())
then the JsonObject gets populated with the properties of my config.json file.
The docs of Future#result state the following
The result of the operation. This will be null if the operation failed.
So the operation succeeds but no config is loaded?
I don't really understand why I have to create a new vertx instance for the config to be loaded properly. What am I missing here?
Found a solution: there is a method which returns a cached version of the config https://vertx.io/docs/vertx-config/java/#_retrieving_the_last_retrieved_configuration
So all I had to do is to load the config once in #Provides method:
#Provides
#Singleton
public ConfigRetriever config(Vertx vertx) {
final ConfigRetriever retriever = ConfigRetriever.create(vertx);
// Retrieving the config here is not just to print it,
// but also to create a cached config which can be later used in Configuration
try {
final var cfg = retriever.getConfig().toCompletionStage().toCompletableFuture().get();
cfg.stream()
.filter(entry -> entry.getKey().startsWith("backend") && !entry.getKey().contains("pass"))
.forEach(entry -> log.info("{} = {}", entry.getKey(), entry.getValue()));
return retriever;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
I got something like this:
private val client = HttpClient {
install(JsonFeature) {
serializer = GsonSerializer()
}
install(ExpectSuccess)
}
and make request like
private fun HttpRequestBuilder.apiUrl(path: String, userId: String? = null) {
header(HttpHeaders.CacheControl, "no-cache")
url {
takeFrom(endPoint)
encodedPath = path
}
}
but I need to check request and response body, is there any way to do it? in console/in file?
You can achieve this with the Logging feature.
First add the dependency:
implementation "io.ktor:ktor-client-logging-native:$ktor_version"
Then install the feature:
private val client = HttpClient {
install(Logging) {
logger = Logger.DEFAULT
level = LogLevel.ALL
}
}
Bonus:
If you need to have multiple HttpClient instances throughout your application and you want to reuse some of the configuration, then you can create an extension function and add the common logic in there. For example:
fun HttpClientConfig<*>.default() {
install(Logging) {
logger = Logger.DEFAULT
level = LogLevel.ALL
}
// Add all the common configuration here.
}
And then initialize your HttpClient like this:
private val client = HttpClient {
default()
}
I ran into this as well. I switched to using the Ktor OkHttp client as I'm familiar with the logging mechanism there.
Update your pom.xml or gradle.build to include that client (copy/paste from the Ktor site) and also add the OkHttp Logging Interceptor (again, copy/paste from that site). Current version is 3.12.0.
Now configure the client with
val client = HttpClient(OkHttp) {
engine {
val loggingInterceptor = HttpLoggingInterceptor()
loggingInterceptor.level = Level.BODY
addInterceptor(loggingInterceptor)
}
}
Regardless of which client you use or framework you are on, you can implement your own logger like so:
private val client = HttpClient {
// Other configurations...
install(Logging) {
logger = CustomHttpLogger()
level = LogLevel.BODY
}
}
Where CustomHttpLogger is any class that implements the ktor Logger interface, like so:
import io.ktor.client.features.logging.Logger
class CustomHttpLogger(): Logger {
override fun log(message: String) {
Log.d("loggerTag", message) // Or whatever logging system you want here
}
}
You can read more about the Logger interface in the documentation here or in the source code here
It looks like we should handle the response in HttpReceivePipeline. We could clone the origin response and use it for logging purpose:
scope.receivePipeline.intercept(HttpReceivePipeline.Before) { response ->
val (loggingContent, responseContent) = response.content.split(scope)
launch {
val callForLog = DelegatedCall(loggingContent, context, scope, shouldClose = false)
....
}
...
}
The example implementation could be found here: https://github.com/ktorio/ktor/blob/00369bf3e41e91d366279fce57b8f4c97f927fd4/ktor-client/ktor-client-core/src/io/ktor/client/features/observer/ResponseObserver.kt
and would be available in next minor release as a client feature.
btw: we could implement the same scheme for the request.
A custom structured log can be created with the HttpSend plugin
Ktor 2.x:
client.plugin(HttpSend).intercept { request ->
val call = execute(request)
val response = call.response
val durationMillis = response.responseTime.timestamp - response.requestTime.timestamp
Log.i("NETWORK", "[${response.status.value}] ${request.url.build()} ($durationMillis ms)")
call
}
Ktor 1.x:
client.config {
install(HttpSend) {
intercept { call, _ ->
val request = call.request
val response = call.response
val durationMillis = response.responseTime.timestamp - response.requestTime.timestamp
Log.i("NETWORK", "[${response.status.value}] ${request.url} ($durationMillis ms)")
call
}
}
}
Check out Kotlin Logging, https://github.com/MicroUtils/kotlin-logging it isused by a lot of open source frameworks and takes care of all the prety printing.
You can use it simply like this:
private val logger = KotlinLogging.logger { }
logger.info { "MYLOGGER INFO" }
logger.warn { "MYLOGGER WARNING" }
logger.error { "MYLOGGER ERROR" }
This will print the messages on the console.
Am building an API and using intercept(ApplicationCallPipeline.Call){} to run some logic before each route execution. I need to pass data from the intercept() method to the called route and
am setting data by using call.attributes.put() in the intercept() like this:
val userKey= AttributeKey<User>("userK")
call.attributes.put(userKey, userData)
And retrieve userData with call.attributes[userKey] .
What happens is that call.attributes[userKey] only works in the intercept() method where I have set the attribute. It doesn't work in the route where I need it.
It throws me
java.lang.IllegalStateException: No instance for key AttributeKey: userK
I wonder if am doing things in the right way
Here is the simplest code reproducing what you describe:
class KtorTest {
data class User(val name: String)
private val userKey = AttributeKey<User>("userK")
private val expected = "expected name"
private val module = fun Application.() {
install(Routing) {
intercept(ApplicationCallPipeline.Call) {
println("intercept")
call.attributes.put(userKey, User(expected))
}
get {
println("call")
val user = call.attributes[userKey]
call.respond(user.name)
}
}
}
#Test fun `pass data`() {
withTestApplication(module) {
handleRequest {}.response.content.shouldNotBeNull() shouldBeEqualTo expected
}
}
}
I intercept the call, put the user in the attributes, and finally respond with the user in the get request.
The test passes.
What ktor version are you using and which engine?
I want to write a Spek test in Kotlin.
How to read an HTML file from the src/test/resources folder?
class MySpec : Spek(
{
describe("blah blah") {
given("blah blah") {
var fileContent: String = ""
beforeEachTest {
// How to read the file.html in src/test/resources/html/
fileContent = ...
}
it("should blah blah") {
...
}
}
}
}
)
val fileContent = MySpec::class.java.getResource("/html/file.html").readText()
No idea why this is so hard, but the simplest way I've found (without having to refer to a particular class) is:
fun getResourceAsText(path: String): String? =
object {}.javaClass.getResource(path)?.readText()
It returns null if no resource with this name is found (as documented).
And then passing in an absolute URL, e.g.
val html = getResourceAsText("/www/index.html")!!
another slightly different solution:
#Test
fun basicTest() {
"/html/file.html".asResource {
// test on `it` here...
println(it)
}
}
fun String.asResource(work: (String) -> Unit) {
val content = this.javaClass::class.java.getResource(this).readText()
work(content)
}
A slightly different solution:
class MySpec : Spek({
describe("blah blah") {
given("blah blah") {
var fileContent = ""
beforeEachTest {
html = this.javaClass.getResource("/html/file.html").readText()
}
it("should blah blah") {
...
}
}
}
})
Kotlin + Spring way:
#Autowired
private lateinit var resourceLoader: ResourceLoader
fun load() {
val html = resourceLoader.getResource("classpath:html/file.html").file
.readText(charset = Charsets.UTF_8)
}
Using Google Guava library Resources class:
import com.google.common.io.Resources;
val fileContent: String = Resources.getResource("/html/file.html").readText()
private fun loadResource(file: String) = {}::class.java.getResource(file).readText()
val fileContent = javaClass.getResource("/html/file.html").readText()
This is the way that I prefer to do it:
fun getResourceText(path: String): String {
return File(ClassLoader.getSystemResource(path).file).readText()
}
this top-level kotlin function will do the job in any case
fun loadResource(path: String): URL {
return Thread.currentThread().contextClassLoader.getResource(path)
}
or if you want a more robust function
fun loadResource(path: String): URL {
val resource = Thread.currentThread().contextClassLoader.getResource(path)
requireNotNull(resource) { "Resource $path not found" }
return resource
}
FYI: In all the above cases. getResource() is unsafe way of using nullable.
Haven't tried locally but I prefer this way:
fun readFile(resourcePath: String) = String::class.java.getResource(resourcePath)?.readText() ?: "<handle default. or handle custom exception>"
Or even as custom datatype function
private fun String.asResource() = this::class.java.getResource(resourcePath)?.readText() ?: "<handle default. or handle custom exception>"
and then you can call directly on path like:
// For suppose
val path = "/src/test/resources"
val content = path.asResource()
I prefer reading resources in this way:
object {}.javaClass.getResourceAsStream("/html/file.html")?.use { it.reader(Charsets.UTF_8).readText() }
Explenation:
getResourceAsStream instead getResource. The resource on classpath can be basically anywhere. e.g. packed inside another .jar file.
In these situations accessing resource via URL class returned from getResource method will fail. But accessing via method getResourceAsStream works in every situation.
object {} - This is not nice syntax, but it is not dependent on name of your class MyClass and works even in static (compenion object) block.
use to close stream - in most cases it is not necessary, but there can be some special classloaders, which may need it.
reader(Charsets.UTF_8) - UTF_8 is default encoding, but I prefer to be explicit. If you will encode your resource files in other encoding e.g. ISO-8859-2 you will not overlook it.
Another variation that handles null resource in place:
val content = object {}.javaClass
.getResource("/html/file.html")
?.let(URL::readText)
?: error("Cannot open/find the file")
// ?: "default text" // Instead of error()
You might find the File class useful:
import java.io.File
fun main(args: Array<String>) {
val content = File("src/main/resources/input.txt").readText()
print(content)
}