Jetpack Compose

Internet Speed Test app Ui made using Jetpack Compose

Internet Speed Test

Yes, it is definitely possible to create an Internet Speed Test UI app using Jetpack Compose. Jetpack Compose is a modern toolkit for building native Android user interfaces using a declarative approach. It is a powerful and flexible tool that allows developers to create beautiful, responsive UIs with less code.

To create an Internet Speed Test UI app using Jetpack Compose, you would first need to define the layout of your app using Compose UI components. You could create a simple layout with a start button to initiate the speed test, a progress indicator to show the progress of the test, and a result display to show the download and upload speeds.

Next, you would need to implement the speed test logic. You could use a third-party library or API to perform the speed test and get the download and upload speeds. Once you have the results, you can update the UI to display the speeds and any other relevant information.

Overall, Jetpack Compose is a great choice for building UIs, and it can be used to create a wide range of apps, including an Internet Speed Test UI app.

Internet Speed Test app Example source code

lets create a new android project using with jetpack compose activity. follow the below steps.

Add below dependency

dependencies {

    implementation 'androidx.core:core-ktx:1.8.0'
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
    implementation 'androidx.activity:activity-compose:1.5.1'
    implementation "androidx.compose.ui:ui:$compose_ui_version"
    implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version"
    implementation 'androidx.compose.material:material:1.2.1'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
    androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version"
    debugImplementation "androidx.compose.ui:ui-tooling:$compose_ui_version"
    debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_ui_version"

    //Internet speed checker
    implementation 'com.github.oatrice:internet-speed-testing:1.0.1'

}

 

MainActivity.kt Source code Internet Speed Test 

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.ui.Modifier
import com.laboontech.internetspeedtestui.presentation.feature_speedtest.SpeedTestScreen
import com.laboontech.internetspeedtestui.presentation.ui.theme.InternetSpeedTesterTheme



class MainActivity : ComponentActivity() {


        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)

            setContent {
                InternetSpeedTesterTheme {
                    // A surface container using the 'background' color from the theme
                    Surface(
                            modifier = Modifier.fillMaxSize(),
                            color = MaterialTheme.colors.background
                    ) {
                        SpeedTestScreen()
                    }
                }
            }
        }


    }

 

Step 2: Create a Utils folder and create kotlin file BottomMenuContent.

import androidx.annotation.DrawableRes

data class BottomMenuContent(
        val title: String,
        @DrawableRes val iconId: Int
)

Step 3 : Create a Constants kotlin file in Utils. Internet Speed Test 

object Constants {

const val DEFAULT_NUMBER_OF_LINES = 40
const val MAX_VALUE_FOR_LINES = 240f
const val TAG_TEST = "TestMyAndroidApp"

}

Step 4: Create a kotlin file with name SpeedTestScreen

in this kotlin file we are design the all UI and function of the main screen.

import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.keyframes
import androidx.compose.animation.core.spring
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.BottomNavigation
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedButton
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.rotate
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.laboontech.internetspeedtestui.R
import com.laboontech.internetspeedtestui.presentation.ui.theme.ArcColorPrimary
import com.laboontech.internetspeedtestui.presentation.ui.theme.DarkGradient
import com.laboontech.internetspeedtestui.presentation.ui.theme.LightColor
import com.laboontech.internetspeedtestui.presentation.ui.theme.ArcGradient
import com.laboontech.internetspeedtestui.presentation.ui.theme.DarkColor
import com.laboontech.internetspeedtestui.presentation.ui.theme.DarkColor2
import com.laboontech.internetspeedtestui.presentation.ui.theme.LightColor2
import com.laboontech.internetspeedtestui.presentation.ui.theme.TitleColor
import com.laboontech.internetspeedtestui.presentation.uistate.UiState
import com.laboontech.internetspeedtestui.utils.BottomMenuContent
import com.laboontech.internetspeedtestui.utils.Constants
import kotlinx.coroutines.launch
import kotlin.math.floor
import kotlin.math.roundToInt


/**
 * SpeedTest Main Screen
 * */
    @Composable
    fun SpeedTestScreen() {


        val coroutineScope = rememberCoroutineScope()

        val animation = remember {
            Animatable(0f)
        }

        val maxSpeed = remember { mutableStateOf(0f) }
        maxSpeed.value = java.lang.Float.max(maxSpeed.value, animation.value * 100f)



        Box(modifier = Modifier.fillMaxSize()) {
            Column(
                    modifier = Modifier
                            .fillMaxSize()
                            .background(DarkGradient)
            ) {
                Header()
                Spacer(modifier = Modifier.height(30.dp))
                SpeedIndicatorContent(state = animation.toUiState(maxSpeed.value)) {
                    coroutineScope.launch {
                        maxSpeed.value = 0f
                        startAnimation(animation)
                    }
                }
                Spacer(modifier = Modifier.height(40.dp))
                SpeedInformation(state = animation.toUiState(maxSpeed.value))
            }

            BottomMenu(
                    items = listOf(
                            BottomMenuContent(
                                    stringResource(id = R.string.network),
                                    R.drawable.baseline_signal_wifi_statusbar_connected_no_internet_4_24
                            ),
                            BottomMenuContent(
                                    stringResource(id = R.string.speed),
                                    R.drawable.baseline_speed_24
                            ),
                            BottomMenuContent(
                                    stringResource(id = R.string.location),
                                    R.drawable.baseline_person_pin_circle_24
                            )
                    ),
                    modifier = Modifier.align(Alignment.BottomCenter)
            )
        }


    }


    suspend fun startAnimation(animation: Animatable<Float, AnimationVector1D>) {
        animation.animateTo(0.65f, keyframes {
            durationMillis = 9000
            0f at 0 with CubicBezierEasing(0f, 1.5f, 0.8f, 1f)
            0.12f at 1000 with CubicBezierEasing(0.2f, -1.5f, 0f, 1f)
            0.20f at 2000 with CubicBezierEasing(0.2f, -2f, 0f, 1f)
            0.35f at 3000 with CubicBezierEasing(0.2f, -1.5f, 0f, 1f)
            0.62f at 4000 with CubicBezierEasing(0.2f, -2f, 0f, 1f)
            0.75f at 5000 with CubicBezierEasing(0.2f, -2f, 0f, 1f)
            0.89f at 6000 with CubicBezierEasing(0.2f, -1.2f, 0f, 1f)
            0.82f at 7500 with LinearOutSlowInEasing
        })
    }


    fun Animatable<Float, AnimationVector1D>.toUiState(maxSpeed: Float) = UiState(
            arcValue = value,
            speed = "%.1f".format(value * 100),
    uploadSpeed = "%.1f".format(value * 50),
    ping = if (value > 0.2f) "${(value * 15).roundToInt()} ms" else "0.0 ms",
    maxSpeed = if (maxSpeed > 0f) "%.1f mbps".format(maxSpeed) else "0.0 mbps",
    inProgress = isRunning
)

    /**
     * Header View - Header Title and Setting Icon
     * */
    @Composable
    fun Header() {
        Row(
                modifier = Modifier
                        .fillMaxWidth()
                        .padding(20.dp),
                horizontalArrangement = Arrangement.SpaceBetween,
                verticalAlignment = Alignment.CenterVertically
        ) {

            Text(
                    text = stringResource(id = R.string.app_name),
                    color = Color.White,
                    fontSize = 24.sp,
                    fontFamily = FontFamily.Serif
            )
            Image(
                    painter = painterResource(id = R.drawable.settings),
                    contentDescription = stringResource(id = R.string.settings)
            )

        }
    }


    /**
     * Download Speed, Upload Speed, Max Speed and Ping Information
     * */
    @Composable
    fun SpeedInformation(
            state: UiState
    ) {

        @Composable
        fun RowScope.InfoColumn(title: String, value: String) {
            Column(
                    horizontalAlignment = Alignment.CenterHorizontally,
                    modifier = Modifier.weight(1f)
            ) {
                Text(
                        text = title,
                        color = TitleColor,
                        fontFamily = FontFamily.Monospace,
                        fontSize = 12.sp
                )
                Text(
                        text = value,
                        style = MaterialTheme.typography.body2,
                        color = Color.White,
                        modifier = Modifier.padding(vertical = 8.dp),
                        fontSize = 16.sp,
                        fontWeight = FontWeight.Bold
                )
            }
        }

        // Download Speed
        // Upload Speed
        Row(
                Modifier
                        .fillMaxWidth()
                        .height(IntrinsicSize.Min)
        ) {
            InfoColumn(
                    title = stringResource(id = R.string.download_speed),
                    value = "${state.speed} mbps"
            )
            VerticalDivider()
            InfoColumn(
                    title = stringResource(id = R.string.upload_speed),
                    value = "${state.uploadSpeed} mbps"
            )
        }

        Spacer(modifier = Modifier.height(40.dp))

        // Ping
        // Max Speed
        Row(
                Modifier
                        .fillMaxWidth()
                        .height(IntrinsicSize.Min)
        ) {
            InfoColumn(title = stringResource(id = R.string.ping), value = state.ping)
            VerticalDivider()
            InfoColumn(
                    title = stringResource(id = R.string.max_speed),
                    value = state.maxSpeed
            )
        }
    }


    /**
     * Vertical Divider
     * */
    @Composable
    fun VerticalDivider() {
        Box(
                modifier = Modifier
                        .fillMaxHeight()
                        .background(Color(0xFF414D66))
                        .width(1.dp)
        )
    }

    /**
     * This function contains - Speed Indicator, Speed Value text, Start/Stop button
     * */
    @Composable
    fun SpeedIndicatorContent(
            state: UiState,
            onclick: () -> Unit
) {

        val buttonHorizontalPadding by animateDpAsState(
                targetValue = if (state.inProgress) 40.dp else 20.dp,
                animationSpec = spring(
                        dampingRatio = Spring.DampingRatioMediumBouncy,
                        stiffness = Spring.StiffnessLow
                )
    )

        Box(
                contentAlignment = Alignment.BottomCenter,
                modifier = Modifier
                        .fillMaxWidth()
                        .aspectRatio(1f)
        ) {


            /* Circular speed indicator */
            Canvas(
                    modifier = Modifier
                            .fillMaxSize()
                            .padding(40.dp)
            ) {

                // Draw Scale lines
                drawLines(state.arcValue, Constants.MAX_VALUE_FOR_LINES)

                // Draw Arc
                drawArcs(state.arcValue, Constants.MAX_VALUE_FOR_LINES)
            }


            /* Speed value Texts */
            Column(
                    modifier = Modifier
                            .fillMaxSize(),
                    verticalArrangement = Arrangement.Center,
                    horizontalAlignment = Alignment.CenterHorizontally
            ) {

                // Download Text
                Text(
                        text = stringResource(id = R.string.download),
                        style = MaterialTheme.typography.caption,
                        color = Color.White
                )


                // Speed Value
                Text(
                        text = state.speed,
                        fontSize = 45.sp,
                        color = Color.White,
                        fontWeight = FontWeight.Bold,
                        fontFamily = FontFamily.SansSerif
                )

                // mbps text
                Text(
                        text = stringResource(id = R.string.mbps),
                        style = MaterialTheme.typography.caption,
                        color = Color.White
                )


            }





            /* Start check speed value Button */
            OutlinedButton(
                    onClick = {
            if (!state.inProgress) onclick()
            },
            modifier = Modifier.padding(bottom = 24.dp),
                    colors = ButtonDefaults.buttonColors(
                            backgroundColor = Color.Transparent,
                            contentColor = Color.White
                    ),
                    shape = RoundedCornerShape(24.dp),
                    border = BorderStroke(width = 1.dp, color = LightColor2),
        ) {

                // Start button text
                Text(
                        modifier = Modifier
                                .padding(horizontal = buttonHorizontalPadding, vertical = 4.dp),
                        text = if (!state.inProgress) {
                    stringResource(id = R.string.start)
                } else {
                    stringResource(id = R.string.checking_internet_speed)
                },
                color = if (state.inProgress) {
                    Color.White.copy(alpha = 0.5f)
                } else {
                    Color.White
                }

            )

            }
        }


    }


    /**
     * Draw arcs extension function to draw an speed indicator arc above the lines
     * */
    fun DrawScope.drawArcs(progress: Float, maxValue: Float) {
        val startAngle = 270 - maxValue / 2
        val sweepAngle = maxValue * progress

        val topLeft = Offset(50f, 50f)
        val size = Size(size.width - 100f, size.height - 100f)

        fun drawBlur() {
            for (i in 0..20) {
                drawArc(
                        color = ArcColorPrimary.copy(alpha = i / 900f),
                        startAngle = startAngle,
                        sweepAngle = sweepAngle,
                        useCenter = false,
                        topLeft = topLeft,
                        size = size,
                        style = Stroke(width = 80f + (20 - i) * 20, cap = StrokeCap.Round)
                )
            }
        }

        fun drawStroke() {
            drawArc(
                    color = ArcColorPrimary,
                    startAngle = startAngle,
                    sweepAngle = sweepAngle,
                    useCenter = false,
                    topLeft = topLeft,
                    size = size,
                    style = Stroke(width = 86f, cap = StrokeCap.Round)
            )
        }

        fun drawGradient() {
            drawArc(
                    brush = ArcGradient,
                    startAngle = startAngle,
                    sweepAngle = sweepAngle,
                    useCenter = false,
                    topLeft = topLeft,
                    size = size,
                    style = Stroke(width = 80f, cap = StrokeCap.Round)
            )
        }

        drawBlur()
        drawStroke()
        drawGradient()
    }

    /**
     * Draw lines extension function to draw lines which look like a round scale
     * */
    fun DrawScope.drawLines(
            progress: Float,
            maxValue: Float,
            numberOfLines: Int = Constants.DEFAULT_NUMBER_OF_LINES
    ) {

        // To calculate rotation size
        val oneRotation = maxValue / numberOfLines
        // Start value of indicator, it can start from 0 or from current progress
        val startValue = if (progress == 0f) 0 else floor(x = progress * numberOfLines).toInt() + 1


        // Loop starts from start value to the number of lines
        for (i in startValue..numberOfLines) {
            val rotationDegree = i * oneRotation + (180 - maxValue) / 2
            rotate(degrees = rotationDegree) {
                drawLine(
                        color = LightColor,
                        start = Offset(if (i % 5 == 0) 80f else 30f, size.height / 2),
                end = Offset(0f, size.height / 2),
                        strokeWidth = 8f,
                        cap = StrokeCap.Round
            )
            }
        }


    }


    @Composable
    fun BottomMenu(
            items: List<BottomMenuContent>,
            modifier: Modifier = Modifier,
            activeHighlightColor: Color = LightColor,
            activeTextColor: Color = Color.White,
            inactiveTextColor: Color = LightColor2,
            initialSelectedItemIndex: Int = 1
    ) {
        var selectedItemIndex by remember {
            mutableStateOf(initialSelectedItemIndex)
        }
        Row(
                horizontalArrangement = Arrangement.SpaceAround,
                verticalAlignment = Alignment.CenterVertically,
                modifier = modifier
                        .fillMaxWidth()
                        .background(DarkColor)
                        .padding(15.dp)
        ) {
            items.forEachIndexed { index, item ->
                    BottomMenuItem(
                            item = item,
                            isSelected = index == selectedItemIndex,
                            activeHighlightColor = activeHighlightColor,
                            activeTextColor = activeTextColor,
                            inactiveTextColor = inactiveTextColor
                    ) {
                selectedItemIndex = index
            }
            }
        }
    }

    @Composable
    fun BottomMenuItem(
            item: BottomMenuContent,
            isSelected: Boolean = false,
            activeHighlightColor: Color = LightColor,
            activeTextColor: Color = Color.White,
            inactiveTextColor: Color = LightColor2,
            onItemClick: () -> Unit
) {
        Column(
                horizontalAlignment = Alignment.CenterHorizontally,
                verticalArrangement = Arrangement.Center,
                modifier = Modifier
                        .clickable {
            onItemClick()
        }
    ) {
            Box(
                    contentAlignment = Alignment.Center,
                    modifier = Modifier
                            .clip(RoundedCornerShape(10.dp))
                            .background(if (isSelected) activeHighlightColor else Color.Transparent)
                .padding(10.dp)
        ) {
                Icon(
                        painter = painterResource(id = item.iconId),
                        contentDescription = item.title,
                        tint = if (isSelected) activeTextColor else inactiveTextColor,
                        modifier = Modifier.size(20.dp)
            )
            }
            Text(
                    text = item.title,
                    color = if (isSelected) activeTextColor else inactiveTextColor
        )
        }

    }

    @Preview(device = Devices.PIXEL)
    @Composable
    fun SpeedTestScreenPreview() {
        SpeedTestScreen()
    }

 

Create a File name State 

class UiState(
        val speed: String = "",
        val uploadSpeed: String = "",
        val ping: String = "-",
        val maxSpeed: String = "-",
        val arcValue: Float = 0f,
        val inProgress: Boolean = false
)

 

Internet Speed Test  Full Souce code on Github

Read More Tutorial