Embed an http web server in Android
Do you want to embed an HTTP web server in Android to create Rest Api's (GET, POST, PUT, DELETE, etc.)? This is the right place. In this article, we are going to learn how to do it. The tools that we are going to use are:
The architecture of our project will look like this:
Embedded server architecture
We divide the code into three sections:
Controller: Contains the Rest Api's and is the entry point for the request made by the client. Service Layer: Contains all the business logic. Repository: Here, we read & write data to the database
Important
In this tutorial, we will not create a real database. We will store the data in a List inside the class UserRepository. The reason is that I want to keep this tutorial simple.
Dependencies
We start by adding all the dependencies we will need in this project to the build.gradle
file.
// Ktor
def ktor_version = "1.6.1"
implementation "io.ktor:ktor:$ktor_version"
implementation "io.ktor:ktor-server-netty:$ktor_version"
implementation "io.ktor:ktor-gson:$ktor_version"
// Dependency Injection
implementation "io.insert-koin:koin-ktor:3.1.2"
// Test Ktor
testImplementation "io.ktor:ktor-server-tests:$ktor_version"
testImplementation "io.mockk:mockk:1.12.0"
Models
These classes will help us to represent the objects inside our project. First, we have the User
class:
data class User(
val id: Int? = null,
val name: String? = null,
val age: Int? = null
)
Also, we have a class that I will call ResponseBase.
This class will help us to build the JSON response for each request made by the client
data class ResponseBase<T>(
val status: Int = 0,
val data: T? = null,
val message: String = "Success"
)
If you look carefully, you will notice that the class ResponseBase
will create a JSON that will look like this:
{
"message": "Success",
"status": 999,
"data": "La información en data puede cambiar"
}
Repository
Usually, in this layer, we will read and write into the database, but in this tutorial, we will not create a database. All the data will be stored inside a List.
We start by creating an interface, UserRepository.
It will help abstract the repository's implementation and make our code easier to test.
interface UserRepository {
fun personList(): ArrayList<User>
fun addPerson(user: User): User
fun removePerson(id: Int): User
}
Then we create the class UserRepositoryImp.
In this class, we will implement the interface's functions.
class UserRepositoryImp : UserRepository {
private var idCount = 0;
private val userList = ArrayList<User>()
override fun userList(): ArrayList<User> = userList
override fun addUser(user: User): User {
val newUser = user.copy(id = ++idCount);
userList.add(newUser)
return newUser
}
override fun removeUser(id: Int): User {
userList.find { it.id == id }?.let {
userList.remove(it)
return it
}
throw GeneralException("Cannot remove user: $id")
}
}
Some things to notice are:
- We have a counter
idCount
that will increase each time we add a user. This counter is the ID of a new user. - We store all the users in the list
userList.
- When we want to delete a user, and we cannot find him, we will throw a
GeneralException
that we will look at in detail later.
Service Layer
In this layer, we will write our web server's business logic. In this tutorial, we will only check if the user's name and age are correct. If they are not right, we will throw an exception.
class UserService : KoinComponent {
private val userRepository by inject<UserRepository>()
fun userList(): List<User> = userRepository.userList()
fun addUser(user: User): User {
if (user.name == null)
throw MissingParamsException("name")
if (user.age == null)
throw MissingParamsException("age")
if (user.age < 0)
throw GeneralException("Age cannot be negative number")
return userRepository.addUser(user)
}
fun removeUser(id: Int): User = userRepository.removeUser(id)
}
Also, we can see that we are using dependency injection with Koin to add UserRepository,
so our class must implement KoinComponent.
Controller
In this section, we will write the code to create the Rest Apis:
fun Route.userController() {
val userService by inject<UserService>()
get("/user") {
call.respond(ResponseBase(data = userService.userList()))
}
post("/user") {
val person = call.receive<User>()
call.respond(ResponseBase(data = userService.addUser(person)))
}
delete("/user/{id}") {
val id = call.parameters["id"]?.toInt()!! // Force just for this example
call.respond(ResponseBase(data = userService.removeUser(id)))
}
}
We just created three Api 's GET, POST & DELETE. This class depends on UserService,
so we use Koin to inject it. As you can see, the response we send to the client is made in the function call.respond()
, and this response is always of the type ResponseBase.
The only thing that changes is the property ResponseBase.data
.
Exception handling
Because we want to throw custom exceptions wherever we are in our code, we can use the plugin StatusPages. Now everytime we throw an exception, we can catch it and send an appropriate response to the client. We create a file CustomExceptions
and the content will be:
val handleException: Application.() -> Unit = {
install(StatusPages) {
exception<CustomExceptions> {
call.respond(ResponseBase(it.status, null, it.description))
}
exception<Throwable> {
it.printStackTrace()
call.respond(ResponseBase(9999, null, "Unknown error"))
}
}
}
open class CustomExceptions(val status: Int, val description: String) : Exception(description)
class MissingParamsException(param: String) : CustomExceptions(100, "Missing parameter: $param")
class GeneralException(description: String) : CustomExceptions(999, description)
We have two custom exceptions MissingParamsException
& GeneralException
both extends from CustomExceptions
. Now when we throw an exception the next code will handle it and will send the appropriate response to the client:
install(StatusPages) {
exception<CustomExceptions> {
call.respond(ResponseBase(it.status, null, it.description))
}
exception<Throwable> {
it.printStackTrace()
call.respond(ResponseBase(9999, null, "Unknown error"))
}
}
Initialize the server
We will create the server inside an Android Service
and start the service every time the device boots. We create a file HttpService
with the next code:
const val PORT = 8080
class HttpService : Service() {
override fun onCreate() {
super.onCreate()
Thread {
InternalLoggerFactory.setDefaultFactory(JdkLoggerFactory.INSTANCE)
embeddedServer(Netty, PORT) {
install(ContentNegotiation) { gson {} }
handleException()
install(Koin) {
modules(
module {
single<UserRepository> { UserRepositoryImp() }
single { UserService() }
}
)
}
install(Routing) {
userController()
}
}.start(wait = true)
}.start()
}
override fun onBind(intent: Intent): IBinder? = null
}
class BootCompletedReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
Log.d("BootCompletedReceiver", "starting service HttpService...")
context.startService(Intent(context, HttpService::class.java))
}
}
}
Android Manifest.
We update the manifest to add the permissions, services & receivers. After updating the manifest will look like:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.nopalsoft.http.server">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AndroidHttpServer">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".server.HttpService"
android:enabled="true" />
<receiver android:name=".server.BootCompletedReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
</intent-filter>
</receiver>
</application>
</manifest>
Final steps
If you want to start the service as soon as the app is launched. You can start the service in the MainActivity
like this:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
startService(Intent(this, HttpService::class.java))
}
If you want to test with postman and the Android Emulator, you must forward the requests. Look at this answer in stackoverflow to get more information.
Postman
We use postman to call our Rest Api's, and you can see the results in the following images:
POST: Adding a new user
GET: Retrieve a list of all the users
Unit testing
Finally, these two classes will help you to understand how to add unit tests to the code. Use the ./gradlew clean test --info
command to run the unit tests.
Class BaseModuleTest
:
abstract class BaseModuleTest {
private val gson = Gson()
protected var koinModules: Module? = null
protected var moduleList: Application.() -> Unit = { }
init {
stopKoin()
}
fun <R> withBaseTestApplication(test: TestApplicationEngine.() -> R) {
withTestApplication({
install(ContentNegotiation) { gson { } }
handleException()
koinModules?.let {
install(Koin) {
modules(it)
}
}
moduleList()
}) {
test()
}
}
fun toJsonBody(obj: Any): String = gson.toJson(obj)
fun <T> TestApplicationResponse.parseBody(clazz: Class<T>): ResponseBase<T> {
val typeOfT: Type = TypeToken.getParameterized(ResponseBase::class.java, clazz).type
return gson.fromJson(content, typeOfT)
}
fun <T> TestApplicationResponse.parseListBody(clazz: Class<T>): ResponseBase<List<T>> {
val typeList = TypeToken.getParameterized(List::class.java, clazz).type
val typeOfT: Type = TypeToken.getParameterized(ResponseBase::class.java, typeList).type
return gson.fromJson(content, typeOfT)
}
}
Class UserModuleTest
:
class UserModuleTest : BaseModuleTest() {
private val userRepositoryMock: UserRepository = mockk()
init {
koinModules = module {
single { userRepositoryMock }
single { UserService() }
}
moduleList = {
install(Routing) {
userController()
}
}
}
@Test
fun `Get users return successfully`() = withBaseTestApplication {
coEvery { userRepositoryMock.userList() } returns arrayListOf(User(1, "Yayo", 28))
val call = handleRequest(HttpMethod.Get, "/user")
val response = call.response.parseListBody(User::class.java)
assertEquals(call.response.status(), HttpStatusCode.OK)
assertEquals(response.data?.get(0)?.name, "Yayo")
assertEquals(response.data?.get(0)?.age, 28)
}
@Test
fun `Missing name parameter`() = withBaseTestApplication {
val call = handleRequest(HttpMethod.Post, "/user") {
addHeader(HttpHeaders.ContentType, ContentType.Application.Json.toString())
setBody(toJsonBody(User(age = 27)))
}
val response = call.response.parseBody(User::class.java)
assertEquals(call.response.status(), HttpStatusCode.OK)
assertEquals(response.data, null)
assertEquals(response.status, 100)
assertEquals(response.message.contains("name"), true)
}
@Test
fun `Missing age parameter`() = withBaseTestApplication {
val call = handleRequest(HttpMethod.Post, "/user") {
addHeader(HttpHeaders.ContentType, ContentType.Application.Json.toString())
setBody(toJsonBody(User(name = "Yayo")))
}
val response = call.response.parseBody(User::class.java)
assertEquals(call.response.status(), HttpStatusCode.OK)
assertEquals(response.data, null)
assertEquals(response.status, 100)
assertEquals(response.message.contains("age"), true)
}
@Test
fun `Age under zero error`() = withBaseTestApplication {
val call = handleRequest(HttpMethod.Post, "/user") {
addHeader(HttpHeaders.ContentType, ContentType.Application.Json.toString())
setBody(toJsonBody(User(name = "Yayo", age = -5)))
}
val response = call.response.parseBody(User::class.java)
assertEquals(call.response.status(), HttpStatusCode.OK)
assertEquals(response.data, null)
assertEquals(response.status, 999)
assertEquals(response.message.contains("Age cannot be negative number"), true)
}
}
Source code
The source code can be download from github.