Using async func in Compose Web - kotlin

I should use Ktor client in Compose Web. But, It can't be called in Compose Web due to async/non-async problem.
Environment is template project made by IntelliJ IDEA.
First, I use this:
val client=HttpClient(Js){
install(ContentNegotiation){
json()
}
}
suspend fun shorterCall(url:String):String{
val response = client.get(SERVER_NAME) {
contentType(ContentType.Application.Json)
parameter("url", url)
}
return response.bodyAsText()
}
suspend fun main() {
var i by mutableStateOf("")
renderComposable(rootElementId = "root") {
Div({ style {
height(100.vh)
margin(0.px)
width(100.vw)
} }) {
Input(type = InputType.Url) {
onInput {
val input=it.value.trim()
if(input.startsWith("http"))
i=shorterCall(input)
else
i="NaN"
}
}
Text(i)
}
}
}
Then, I got that error:
Suspend function can be called only within a coroutine body.
So, I tried another one:
import kotlinx.coroutines.*
fun shorterCall(url:String):String{
var ret:String
suspend fun t():String {
val response = client.get(SERVER_NAME) {
contentType(ContentType.Application.Json)
parameter("url", url)
}
return response.bodyAsText()
}
runBlocking{
ret=t()
}
return ret
}
//main func is same as upper one.
Then, I got that error:
Unresolved reference: runBlocking
+editing body 1: When I use GlobalScope.launch or js("JSCode"), It raise that error:
e: Could not find "kotlin" in [/home/user/.local/share/kotlin/daemon]
e: java.lang.IllegalStateException: FATAL ERROR: Could not find "kotlin" in [/home/user/.local/share/kotlin/daemon]
(a lot of internal errors bellow)

You can use the GlobalScope.launch() method to launch a job for a request in a browser environment:
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import io.ktor.client.*
import io.ktor.client.engine.js.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.jetbrains.compose.web.attributes.InputType
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.*
import org.jetbrains.compose.web.renderComposable
fun main() {
val client = HttpClient(Js) {}
var i by mutableStateOf("")
renderComposable(rootElementId = "root") {
Div({
style {
height(100.vh)
margin(0.px)
width(100.vw)
}
}) {
Input(type = InputType.Url) {
onInput {
val input = it.value.trim()
if (input.startsWith("http")) {
GlobalScope.launch {
i = client.shorterCall(input)
}
} else {
i = "NaN"
}
}
}
Text(i)
}
}
}
suspend fun HttpClient.shorterCall(url: String): String {
val response = get(url) {
contentType(ContentType.Application.Json)
}
return response.bodyAsText()
}

Related

How to use firebase admin with ktor 2.0

Does anyone have an idea how to convert these codes to ktor 2.0.
https://gist.github.com/togisoft/d1113a83eeb1d6b52031f77fe780ce48
If someone needs to see a complete sample of Aleksei's answer, I created a sample repository.
I did make some slight tweaks to the other answer as the implementation error was missing the correct messaging from the original gist.
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.FirebaseToken
import io.ktor.http.auth.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class FirebaseAuthProvider(config: FirebaseConfig): AuthenticationProvider(config) {
val authHeader: (ApplicationCall) -> HttpAuthHeader? = config.authHeader
private val authFunction = config.firebaseAuthenticationFunction
override suspend fun onAuthenticate(context: AuthenticationContext) {
val token = authHeader(context.call)
if (token == null) {
context.challenge(FirebaseJWTAuthKey, AuthenticationFailedCause.InvalidCredentials) { challengeFunc, call ->
challengeFunc.complete()
call.respond(UnauthorizedResponse(HttpAuthHeader.bearerAuthChallenge(realm = FIREBASE_AUTH)))
}
return
}
try {
val principal = verifyFirebaseIdToken(context.call, token, authFunction)
if (principal != null) {
context.principal(principal)
}
} catch (cause: Throwable) {
val message = cause.message ?: cause.javaClass.simpleName
context.error(FirebaseJWTAuthKey, AuthenticationFailedCause.Error(message))
}
}
}
class FirebaseConfig(name: String?) : AuthenticationProvider.Config(name) {
internal var authHeader: (ApplicationCall) -> HttpAuthHeader? =
{ call -> call.request.parseAuthorizationHeaderOrNull() }
var firebaseAuthenticationFunction: AuthenticationFunction<FirebaseToken> = {
throw NotImplementedError(FirebaseImplementationError)
}
fun validate(validate: suspend ApplicationCall.(FirebaseToken) -> User?) {
firebaseAuthenticationFunction = validate
}
}
public fun AuthenticationConfig.firebase(name: String? = FIREBASE_AUTH, configure: FirebaseConfig.() -> Unit) {
val provider = FirebaseAuthProvider(FirebaseConfig(name).apply(configure))
register(provider)
}
suspend fun verifyFirebaseIdToken(
call: ApplicationCall,
authHeader: HttpAuthHeader,
tokenData: suspend ApplicationCall.(FirebaseToken) -> Principal?
): Principal? {
val token: FirebaseToken = try {
if (authHeader.authScheme == "Bearer" && authHeader is HttpAuthHeader.Single) {
withContext(Dispatchers.IO) {
FirebaseAuth.getInstance().verifyIdToken(authHeader.blob)
}
} else {
null
}
} catch (ex: Exception) {
ex.printStackTrace()
return null
} ?: return null
return tokenData(call, token)
}
private fun HttpAuthHeader.Companion.bearerAuthChallenge(realm: String): HttpAuthHeader {
return HttpAuthHeader.Parameterized("Bearer", mapOf(HttpAuthHeader.Parameters.Realm to realm))
}
private fun ApplicationRequest.parseAuthorizationHeaderOrNull() = try {
parseAuthorizationHeader()
} catch (ex: IllegalArgumentException) {
println("failed to parse token")
null
}
const val FIREBASE_AUTH = "FIREBASE_AUTH"
private const val FirebaseJWTAuthKey: String = "FirebaseAuth"
private const val FirebaseImplementationError =
"Firebase auth validate function is not specified, use firebase { validate { ... } }to fix"
Then to actually use in your project created an extension function on Application. Be sure that the Firebase Admin SDK has been initialized with credentials before installing the Firebase authentication plugin on Ktor.
fun Application.configureFirebaseAuth() {
FirebaseAdmin.init()
install(Authentication) {
firebase {
validate {
// TODO look up user profile to fill in any additional information on top of firebase user profile
User(it.uid, it.name)
}
}
}
}
Finally wrap a route with the authentication function:
authenticate(FIREBASE_AUTH) {
get("/authenticated") {
val user: User = call.principal() ?: return#get call.respond(HttpStatusCode.Unauthorized)
call.respond("User is authenticated: $user")
}
}
The converted to Ktor 2.0.* code is the following:
import io.ktor.http.auth.*
import io.ktor.serialization.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.FirebaseToken
class FirebaseAuthProvider(config: FirebaseConfig): AuthenticationProvider(config) {
val authHeader: (ApplicationCall) -> HttpAuthHeader? = config.authHeader
private val authFunction = config.firebaseAuthenticationFunction
override suspend fun onAuthenticate(context: AuthenticationContext) {
val token = authHeader(context.call)
if (token == null) {
context.challenge(FirebaseJWTAuthKey, AuthenticationFailedCause.InvalidCredentials) { challengeFunc, call ->
challengeFunc.complete()
call.respond(UnauthorizedResponse(HttpAuthHeader.bearerAuthChallenge(realm = "firebaseAuth")))
}
return
}
try {
val principal = verifyFirebaseIdToken(context.call, token, authFunction)
if (principal != null) {
context.principal(principal)
}
} catch (cause: Throwable) {
val message = cause.message ?: cause.javaClass.simpleName
context.error(FirebaseJWTAuthKey, AuthenticationFailedCause.Error(message))
}
}
}
class FirebaseConfig(name: String?) : AuthenticationProvider.Config(name) {
internal var authHeader: (ApplicationCall) -> HttpAuthHeader? =
{ call -> call.request.parseAuthorizationHeaderOrNull() }
var firebaseAuthenticationFunction: AuthenticationFunction<FirebaseToken> = {
throw NotImplementedError(FirebaseImplementationError)
}
fun validate(validate: suspend ApplicationCall.(FirebaseToken) -> User?) {
firebaseAuthenticationFunction = validate
}
}
public fun AuthenticationConfig.firebase(name: String? = "firebaseAuth", configure: FirebaseConfig.() -> Unit) {
val provider = FirebaseAuthProvider(FirebaseConfig(name).apply(configure))
register(provider)
}
suspend fun verifyFirebaseIdToken(
call: ApplicationCall,
authHeader: HttpAuthHeader,
tokenData: suspend ApplicationCall.(FirebaseToken) -> Principal?
): Principal? {
val token: FirebaseToken = try {
if (authHeader.authScheme == "Bearer" && authHeader is HttpAuthHeader.Single) {
withContext(Dispatchers.IO) {
FirebaseAuth.getInstance().verifyIdToken(authHeader.blob)
}
} else {
null
}
} catch (ex: Exception) {
ex.printStackTrace()
return null
} ?: return null
return tokenData(call, token)
}
private fun HttpAuthHeader.Companion.bearerAuthChallenge(realm: String): HttpAuthHeader {
return HttpAuthHeader.Parameterized("Bearer", mapOf(HttpAuthHeader.Parameters.Realm to realm))
}
private fun ApplicationRequest.parseAuthorizationHeaderOrNull() = try {
parseAuthorizationHeader()
} catch (ex: IllegalArgumentException) {
println("failed to parse token")
null
}
private const val FirebaseJWTAuthKey: String = "FirebaseAuth"
private const val FirebaseImplementationError =
"Firebase auth validate function is not specified, use firebase { { ... } }to fix"

Display asset text file in Composable function using Kotlin Jetpack Compose

I'm trying to read a text file using Kotlin from my Assets folder and display it to a Compose text widget. Android Studio Arctic Fox 2020.3
The following code runs successfully and displays the text file to the Output console, however I can't figure out how to get the text file and pass it to a Compose text widget.
You'll notice that I have 2 text() calls inside ReadDataFile(). The first text() is outside of try{} and it works fine, but the text() inside the try{} causes an error: "Try catch is not supported around composable function invocations"
How can I make this work?
Thanks!
package com.learning.kotlinreadfile
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import com.learning.kotlinreadfile.ui.theme.KotlinReadFileTheme
import java.io.InputStream
import java.io.IOException
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
KotlinReadFileTheme {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
ReadDataFile()
}
}
}
}
}
#Composable
fun ReadDataFile() {
println("Read Data File")
Text("Read Data File")
val context = LocalContext.current
try {
val inputStream: InputStream = context.assets.open("data.txt")
val size: Int = inputStream.available()
val buffer = ByteArray(size)
inputStream.read(buffer)
var string = String(buffer)
println(string)
//Text(string) // ERROR: Try catch is not supported around composable function invocations
} catch (e: IOException) {
e.printStackTrace()
println("Error")
}
}
Caution
File read (I/O) operations can be long, so it is not recommended to use the UI scope to read files. But that's not what's causing the problem, I'm just warning you that if it reads very large files, it can make your app crash because it does a very long processing in the UI thread. I recommend checking this link if you're not familiar with this type of problem.
Problem solving following best practices
Fortunately Jetpack compose works very well with reactive programming, so we can take advantage of that to write reactive code that doesn't face the aforementioned problems.
I made an example very similar to yours, I hope you can understand:
UiState file
As stated earlier, reading a file can be a long process, so let's imagine 3 possible states, "loading", "message successful" and "error". In the case of "successful message" we will have a possibly null string that will no longer be null when the message is actually read from the txt file:
package com.example.kotlinreadfile
data class UiState(
val isLoading: Boolean,
val isOnError: Boolean,
val fileMessage: String?
)
MainActivity.kt file
Here will be just our implementation of the UI, in your case the text messages arranged in the application. As soon as we want to read these messages on the screen we will make a request to our ViewModel:
package com.example.kotlinreadfile
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.layout.*
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.example.kotlinreadfile.ui.theme.KotlinReadFileTheme
class MainActivity : ComponentActivity() {
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
KotlinReadFileTheme {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
val context = LocalContext.current
viewModel.loadData(context)
ScreenContent(viewModel.uiState.value)
}
}
}
}
#Composable
fun ScreenContent(uiState: UiState) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(text = "Read Data File")
Spacer(modifier = Modifier.height(8.dp))
when {
uiState.isLoading -> CircularProgressIndicator()
uiState.isOnError -> Text(text = "Error when try load data from txt file")
else -> Text(text = "${uiState.fileMessage}")
}
}
}
}
MainViewModel.kt file
If you are unfamiliar with the ViewModel class I recommend this official documentation link.
Here we will focus on "our business rule", what we will actually do to get the data. Since we are dealing with an input/output (I/O) operation we will do this in an appropriate scope using viewModelScope.launch(Dispatchers.IO):
package com.example.kotlinreadfile
import android.content.Context
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.IOException
import java.io.InputStream
class MainViewModel : ViewModel() {
private val _uiState = mutableStateOf(
UiState(
isLoading = true,
isOnError = false,
fileMessage = null
)
)
val uiState: State<UiState> = _uiState
fun loadData(context: Context) {
viewModelScope.launch(Dispatchers.IO) {
try {
val inputStream: InputStream = context.assets.open("data.txt")
val size: Int = inputStream.available()
val buffer = ByteArray(size)
inputStream.read(buffer)
val string = String(buffer)
launch(Dispatchers.Main) {
_uiState.value = uiState.value.copy(
isLoading = false,
isOnError = false,
fileMessage = string
)
}
} catch (e: IOException) {
e.printStackTrace()
launch(Dispatchers.Main) {
_uiState.value = uiState.value.copy(
isLoading = false,
isOnError = true,
fileMessage = null
)
}
}
}
}
}
jetpack compose by state refresh, please try
#Preview
#Composable
fun ReadDataFile() {
var dataText by remember {
mutableStateOf("asd")
}
println("Read Data File")
Column {
Text("Read Data File")
Text(dataText)
}
val context = LocalContext.current
LaunchedEffect(true) {
kotlin.runCatching {
val inputStream: InputStream = context.assets.open("data.txt")
val size: Int = inputStream.available()
val buffer = ByteArray(size)
inputStream.read(buffer)
String(buffer)
}.onSuccess {
it.logE()
dataText = it
}.onFailure {
dataText = "error"
}
}
}

How to restrict route access in ktor framework?

How to restrict route access in ktor framework?
//only admin
post("/add") {
call.respondText { "add" }
}
post("/delete") {
call.respondText { "delete" }
}
You can write a method that creates a route that restricts access for admins only. Inside that method, the newly created route is intercepted to inject the code for validation. In the following example, if the header admin has the value 1 then a request is made from an admin otherwise for the /add and /delete routes the response with the 401 Unauthorized status will be returned.
import io.ktor.application.*
import io.ktor.auth.*
import io.ktor.http.*
import io.ktor.request.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.util.pipeline.*
fun main() {
embeddedServer(Netty, port = 5555, host = "0.0.0.0") {
routing {
admin {
post("/add") {
call.respondText { "add" }
}
post("/delete") {
call.respondText { "delete" }
}
}
post("/show") {
call.respondText { "show" }
}
}
}.start(wait = false)
}
private val validationPhase = PipelinePhase("Validate")
fun Route.admin(build: Route.() -> Unit): Route {
val route = createChild(AdminSelector())
route.insertPhaseAfter(ApplicationCallPipeline.Features, Authentication.ChallengePhase)
route.insertPhaseAfter(Authentication.ChallengePhase, validationPhase)
route.intercept(validationPhase) {
if (!isAdmin(call.request)) {
call.respond(HttpStatusCode.Forbidden)
finish()
}
}
route.build()
return route
}
class AdminSelector: RouteSelector() {
override fun evaluate(context: RoutingResolveContext, segmentIndex: Int) = RouteSelectorEvaluation.Transparent
}
fun isAdmin(request: ApplicationRequest): Boolean {
return request.headers["admin"] == "1"
}
For Ktor 2.x the solution proposed by Alexsei does not work anymore because ChallengePhase is now marked as internal as they completely restructured the plugin system.
This code snippet seems to be working for me.
fun Route.authorization(build: Route.() -> Unit): Route {
val route = createChild(CustomSelector())
val plugin = createRouteScopedPlugin("CustomAuthorization") {
on(AuthenticationChecked) { call ->
val principal = call.authentication.principal
// custom logic
}
}
route.install(plugin)
route.build()
return route
}
private class CustomSelector : RouteSelector() {
override fun evaluate(context: RoutingResolveContext, segmentIndex: Int) = RouteSelectorEvaluation.Transparent
}
Of course, you can add parameters to the function specifying the restriction like required roles.
To secure a route...
fun Route.myRoute() = route("test") {
authorization {
get { ... }
}
}
For more details: https://ktor.io/docs/custom-plugins.html

Where should i place the code to observe internet connection so that the user is notified if the device is online or offline?

I have the code to monitor if internet is available. It returns a LiveData and it is observed in the MainActivity . The code is given below.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding=DataBindingUtil.setContentView(this,R.layout.activity_main)
NetworkStatusHelper(this#MainActivity).observe(this, Observer {
when(it){
NetworkStatus.Available-> Snackbar.make(binding.root, "Back online", Snackbar.LENGTH_LONG).show()
NetworkStatus.Unavailable-> Snackbar.make(binding.root, "No Internet connection", Snackbar.LENGTH_LONG).show()
}
})
}
NetworkHelper
package com.todo.utils.networkhelper
import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.os.Build
import android.util.Log
import androidx.lifecycle.LiveData
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.net.InetAddress
import java.net.InetSocketAddress
import java.net.Socket
class NetworkStatusHelper(private val context: Context): LiveData<NetworkStatus>() {
var connectivityManager: ConnectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
private lateinit var connectivityManagerCallback: ConnectivityManager.NetworkCallback
val validNetworkConnections: ArrayList<Network> = ArrayList()
fun getConnectivityCallbacks() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
super.onAvailable(network)
val networkCapability =
connectivityManager.getNetworkCapabilities(network)
val hasNetworkConnection =
networkCapability?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
?: false
if (hasNetworkConnection) {
determineInternetAccess(network)
}
}
override fun onLost(network: Network) {
super.onLost(network)
validNetworkConnections.remove(network)
announceNetworkStatus()
}
// override fun onCapabilitiesChanged(
// network: Network,
// networkCapabilities: NetworkCapabilities
// ) {
// super.onCapabilitiesChanged(network, networkCapabilities)
//
// Log.d("validNetworkConnection","onCapabilitiesChanged size "+validNetworkConnections.size)
//
//
// if (networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
// determineInternetAccess(network)
// } else {
// validNetworkConnections.remove(network)
// }
// announceNetworkStatus()
// }
private fun determineInternetAccess(network: Network) {
CoroutineScope(Dispatchers.IO).launch {
if (InternetAvailability.check()) {
withContext(Dispatchers.Main) {
validNetworkConnections.add(network)
announceNetworkStatus()
}
}
}
}
fun announceNetworkStatus() {
if (validNetworkConnections.isNotEmpty()) {
postValue(NetworkStatus.Available)
} else {
postValue(NetworkStatus.Unavailable)
}
}
}
} else {
TODO("VERSION.SDK_INT < LOLLIPOP")
}
override fun onActive() {
super.onActive()
connectivityManagerCallback = getConnectivityCallbacks()
val networkRequest = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
NetworkRequest
.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.build()
} else {
TODO("VERSION.SDK_INT < LOLLIPOP")
}
connectivityManager.registerNetworkCallback(networkRequest, connectivityManagerCallback)
}
override fun onInactive() {
super.onInactive()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
connectivityManager.unregisterNetworkCallback(connectivityManagerCallback)
}
}
object InternetAvailability {
fun check() : Boolean {
return try {
val socket = Socket()
socket.connect(InetSocketAddress("8.8.8.8",53))
socket.close()
true
} catch ( e: Exception){
e.printStackTrace()
false
}
}
}
}
The problem is here is , the Snackbar is displayed even when the app is opened for the first time .I don't want the Snackbar to be displayed when the app is opened for the first time when network is available. If network is unavailable, then the Snackbar should be displayed even when the app is opened for the first time.
Can someone help to improve the code with correct logic to implement the same.
If your helper class is a Flow, then you can use Flow operators to easily customize its behavior. You should keep the instance of your helper class in a ViewModel so it can maintain its state when there are configuration changes.
Here's a Flow version of your class's functionality. I actually just made it into a function, because I think that's simpler.
I removed the List<Network> but you can add it back in if you think it's necessary. I don't think it makes sense to keep a List that can only ever hold at most one item. A device cannot have multiple simultaneous network connections. If you do need it, it won't work for pre-Lollipop, so you will have to juggle differing functionality and probably do need a class instead of just a function.
I think you can probably remove the checkAvailability() function as it is redundant, but I put it in because you have it.
I added a pre-Lollipop version based on a broadcast receiver, since you seem to want to add support for that.
#get:RequiresPermission("android.permission.ACCESS_NETWORK_STATE")
val Context.networkStatus: Flow<NetworkStatus> get() = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP -> getNetworkStatusLollipop(this)
else -> getNetworkStatusPreLollipop(this)
}
#RequiresApi(Build.VERSION_CODES.LOLLIPOP)
#RequiresPermission("android.permission.ACCESS_NETWORK_STATE")
private fun getNetworkStatusLollipop(context: Context): Flow<NetworkStatus> = callbackFlow {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val callback = object : ConnectivityManager.NetworkCallback() {
private var availabilityCheckJob: Job? = null
override fun onUnavailable() {
availabilityCheckJob?.cancel()
trySend(NetworkStatus.Unavailable)
}
override fun onAvailable(network: Network) {
availabilityCheckJob = launch {
send(if(checkAvailability()) NetworkStatus.Available else NetworkStatus.Unavailable)
}
}
override fun onLost(network: Network) {
availabilityCheckJob?.cancel()
trySend(NetworkStatus.Unavailable)
}
}
val request = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.build()
connectivityManager.registerNetworkCallback(request, callback)
awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
}
#RequiresPermission("android.permission.ACCESS_NETWORK_STATE")
private fun getNetworkStatusPreLollipop(context: Context): Flow<NetworkStatus> = callbackFlow {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val receiver = object: BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
launch {
if (connectivityManager.activeNetworkInfo?.isConnectedOrConnecting == true) {
send(if(checkAvailability()) NetworkStatus.Available else NetworkStatus.Unavailable)
} else {
send(NetworkStatus.Unavailable)
}
}
}
}
context.registerReceiver(receiver, IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION))
awaitClose { context.unregisterReceiver(receiver) }
}
private suspend fun checkAvailability() : Boolean = withContext(Dispatchers.IO) {
try {
Socket().use {
it.connect(InetSocketAddress("8.8.8.8", 53))
}
true
} catch (e: Exception){
e.printStackTrace()
false
}
}
Then in your ViewModel, you can use Flow operators to easily expose a Flow that skips initial NetworkStatus.Available values:
class MyViewModel(application: Application): AndroidViewModel(application) {
val changedNetworkStatus = application.context.networkStatus
.dropWhile { it == NetworkStatus.Available } // ignore initial available status
.shareIn(viewModelScope, SharingStarted.Eagerly, 1) // or .asLiveData() if preferred
}

Squareup Retrofit/Moshi example process not shutting down

I have a working "hello world" App for Squareup's Retrofit2 http client (https://square.github.io/retrofit/) using JSON unmarschalling with Moshi (https://github.com/square/moshi) ...
Everything works fine, but without the System.exit(0) the App will run forever.
Any idea why this is??
import com.squareup.moshi.JsonClass
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.http.GET
import retrofit2.http.Path
fun main() {
MainApp().doIt()
println("main READY!")
System.exit(0)
}
interface GitHubService {
#GET("users/{user}/repos")
fun listRepos(#Path("user") user: String): Call<List<Repo>>
}
#JsonClass(generateAdapter = true)
data class Repo(val full_name: String, val private: Boolean)
class MainApp {
fun doIt() {
val moshi = Moshi.Builder()
.addLast(KotlinJsonAdapterFactory())
.build()
val retrofitGithub = Retrofit.Builder()
.baseUrl("https://api.github.com/")
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()
val githubService = retrofitGithub.create(GitHubService::class.java)
val reposCall: Call<List<Repo>> = githubService.listRepos("HoffiMuc")
val response = reposCall.execute()
if (response.isSuccessful) {
val repos = response.body() ?: ArrayList<Repo>()
if ( repos.isNotEmpty() ) {
for (repo in repos) {
println(repo)
}
} else {
println("Response Body is null or no repo in result list")
}
} else {
println(response.headers())
}
}
}
What happens if you add an explicit dependency on the latest release of OkHttp? The old version Retrofit depends on used non-daemon threads.