kotlin multiplaform using HashMap in javascript - kotlin

I'm using kotlin multiplatform to export a library to ios, android and possibly js.
the kotlin class is like this:
#JsExport
class CoreClient (private val httpClient: HTTPClient, private val socket: SocketClient, eventDelegate:EventEngineDelegate? = null): SocketClientDelegate {
...
fun sessionLogin(data: Map<String, Any>, callback: ((Exception?, String?) -> Unit)) {
Logger.i { "CoreClient - sessionLogin"}
val body = data["body"] as Map<String, Any?>
// create an event for connecting the socket, store the token for the login to happen after connected
val event = SocketConnectEvent(
body,
null,
null,
null,
null,
"SocketConnect"
)
invokeEngine(event, eventCallback(callback))
}
...
}
that's how I'm trying to use it:
const lib = require("../build/js/packages/clientsdk-core-js-legacy");
const kotlin = require("../build/js/packages_imported/kotlin/1.6.10/kotlin");
const HashMap = kotlin.kotlin.collections.HashMap
HashMap.prototype.get = HashMap.prototype.get_11rb$
HashMap.prototype.put = HashMap.prototype.put_xwzc9p$
const {core, middleware, api } = lib.com.nexmo.clientsdk
const {CoreClient} = core
const {VoiceClient, MediaClient} = api
const coreClient = new CoreClient(HTTPClient(), SocketClient())
const mediaClient = new MediaClient()
const client = new VoiceClient(coreClient, mediaClient)
loginEventBody = new HashMap()
loginEventBody.put('token', 'TOKEN')
loginEventBody.put('device_id', 'js1')
loginEventBody.put('device_type', 'js')
loginEventBody.put('SDK_version', 'foo')
const loginEvent = new HashMap()
loginEvent.put('body', loginEventBody)
// console.log('sessionLogin_wt4221$')
coreClient.sessionLogin(loginEvent)
if I do this, I got the following error:
/Users/jboemo/WorkspaceNexmo/nexmoclient-sdk-core-kmp/build/js/packages_imported/kotlin/1.6.10/kotlin.js:41851
return this.internalMap_uxhen5$_0.put_xwzc9p$(key, value);
^
TypeError: Cannot read property 'put_xwzc9p$' of null
my gradle configuration for js is the following:
js(BOTH){
browser {
commonWebpackConfig{
cssSupport.enabled = false
}
}
}
so my questions are:
why the kotlin hashmap got his method called in that wired name?
how am I supposed to use the kotlin hashmap in js?
is there a better way to expose this without using the expect and actual mechanism?
this was to do things is working great in both android and ios

See an issue in Kotlin Youtrack - https://youtrack.jetbrains.com/issue/KT-34995.

Related

Javalin Migration

I am new to Kotlin and Javalin. While migrating from Javalin 3 to 4, Javalinjackson.configure() function is deprecated. Below is the part of the code
import io.javalin.plugin.json.JavalinJackson
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
val om = jacksonObjectMapper().apply { registerModule(JavaTimeModule()) }
JavalinJackson.configure(om)
I read in the documentation is that config.jsonMapper() is used now. Any pointers would be helpful.
JavalinJackson is not a singleton any more. To "configure" it just pass your ObjectMapper as a constructor parameter:
val om = jacksonObjectMapper().apply { registerModule(JavaTimeModule()) }
val jacksonMapper = JavalinJackson(om)
and then pass resulting instance of JsonMapper into Javalin config:
val app = Javalin.create { config: JavalinConfig ->
config.jsonMapper(jacksonMapper)
}.start()

Cannot access 'number': it is internal in 'CardParams'

I have been trying to compile my app in android studio but I am getting this error "Cannot access 'number': it is internal in 'CardParams'"
val cardNumber = binding.cardInput.cardParams!!.number
val expiryYear = binding.cardInput.cardParams!!.expYear
val expiryMonth = binding.cardInput.cardParams!!.expMonth
val cvc = binding.cardInput.cardParams!!.cvc
val token = "{\"cardNumber\":${cardNumber},\"cvv\":${cvc},\"expiryMonth\":${expiryMonth},\"expiryYear\":${expiryYear}}"
chargeAccount(amountDouble, token)
}
else -> {
binding.waitingForPayment = true
GetPaymentLink(selectedPayment!!.id, amount = amountDouble, currency = currency!!, serverUrl = Config.Backend).execute<GetPaymentLinkResult> {
binding.waitingForPayment = false
when (it) {
is RemoteResponse.Success -> {
val intent = Intent(this#ChargeAccountActivity, PaymentActivity::class.java)
intent.putExtra("redirectionUrl", it.body.url)
startActivityForResult(intent, WEB_PAYMENT_CALLBACK)
}
is RemoteResponse.Error -> {
it.error.showAlert(this)
}
internal means it can only be accessed from within the same module. Is cardInput in another module, or some library you're using?
You're not meant to be touching cardParams directly, basically. If it's under your control you could make it public, or add a public getter function

How to inject scopeId into Koin to get the dependency?

In https://github.com/InsertKoinIO/koin/blob/master/koin-projects/docs/reference/koin-android/scope.md#sharing-instances-between-components-with-scopes it is shown the below example
module {
// Shared user session data
scope(named("session")) {
scoped { UserSession() }
}
// Inject UserSession instance from "session" Scope
factory { (scopeId : ScopeID) -> Presenter(getScope(scopeId).get())}
}
But I don't even know how to get presenter?
I try
val nameScope = getKoin().createScope("SomeName", named("session"))
val presenter = get<Presenter>(nameScope.id)
but it's not the correct. How to get my presenter?
After tracing the code, the way to do it is to use parameter to pass over the scopeId
For the above example, it will be
val nameScope = getKoin().createScope("SomeName", named("session"))
val presenter = get<Presenter>(parameters = { parametersOf(nameScope.id) )
If there's qualifier, we just need to send through them as well
One Example as below where we need a parameter of the lambda to send through scopeId and name of the qualifier. (the argument is self definable through the parameters of any type).
module {
scope(named("AScopeName")) {
scoped(qualifier = named("scopedName")) { Dependency() }
factory(qualifier = named("factoryName")) { Dependency() }
}
factory { (scopeId: ScopeID, name: String) ->
Environment(getScope(scopeId).get(qualifier = named(name)))
}
}
Then the calling is as simple as below
val nameScope = getKoin().createScope("SomeName", named("AScopeName"))
val environment = get<Environment>(parameters = { parametersOf(nameScope.id, "scopedName") })
Or we could also
val nameScope = getKoin().createScope("SomeName", named("AScopeName"))
val environment = get<Environment>(parameters = { parametersOf("SomeName", "scopedName") })

Testing a Retrofit + Moshi ApiService which returns a Deferred Object

I've been trying to build tests for the following API Service, but I can't figure how to build a mock interface for it:
package com.example.themovieapp.network
import com.jakewharton.retrofit2.adapter.kotlin.coroutines.CoroutineCallAdapterFactory
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import kotlinx.coroutines.Deferred
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.http.GET
import retrofit2.http.Query
private const val BASE_URL = "https://api.themoviedb.org/3/"
private const val API_key = ""
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
private val retrofit = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.baseUrl(BASE_URL)
.build()
interface MovieApiService{
//https://developers.themoviedb.org/3/movies/get-top-rated-movies
//https://square.github.io/retrofit/2.x/retrofit/index.html?retrofit2/http/Query.html
#GET("movie/top_rated")
fun getMoviesAsync(
#Query("api_key") apiKey: String = API_key,
#Query("language") language: String = "en-US",
#Query("page") page: Int
): Deferred<ResponseObject>
}
/*
Because this call is expensive, and the app only needs
one Retrofit service instance, you expose the service to the rest of the app using
a public object called MovieApi, and lazily initialize the Retrofit service there
*/
object MovieApi {
val retrofitService: MovieApiService by lazy {
retrofit.create(MovieApiService::class.java)
}
}
I want to create Unit tests which:
Verify HTTPS status &
Verify that the JSON response is appropriate.
If it's any help, this is used in another file to create the API request:
coroutineScope.launch {
val getMoviesDeferred = MovieApi.retrofitService.getMoviesAsync(page = pageNumber)
//...
val responseObject = getMoviesDeferred.await()
//...
}
data class ResponseObject(
val page: Int,
val results: List<Movie>,
val total_results: Int,
val total_pages: Int
)
You can achieve that with MockWebServer
class MovieApiTest {
private var mockWebServer = MockWebServer()
private lateinit var apiService: MovieApiService
#Before
fun setUp() {
// checkthis blogpost for more details about mock server
// https://medium.com/#hanru.yeh/unit-test-retrofit-and-mockwebserver-a3e4e81fd2a2
mockWebServer.start()
apiService = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.baseUrl(mockWebServer.url("/")) // note the URL is different from production one
.build()
.create(MovieApiService::class.java)
}
#After
fun teardown() {
mockWebServer.shutdown()
}
#Test
fun testCompleteIntegration() = runBlocking { // that will allow to wait for coroutine
mockWebServer.enqueue(MockResponse()
.setResponseCode(HttpURLConnection.HTTP_OK)
.setBody("""{
"page":0,
"total_results":1,
"total_pages":1,
"results": [{"id": "movie_id"}]
}"""))
val response = apiService.getMoviesAsync(page = 1).await()
assertEquals(0, response.page)
assertEquals(1, response.total_results)
assertEquals(1, response.total_pages)
assertEquals("movie_id", response.results.first().id)
}
}
That way you can avoid calling the real server that won't work well in unit tests because of latency and non-deterministic network state.
Also, I do recommend splitting code in pieces so that you can test them independently if you have complex parsing logic: make the fields optional and define separate mapper that can check which parts of JSON are required, and which are mandatory, and test that mapper in isolation.

How to log requests in ktor http client?

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.