We write without Retrofit, Gson and Kotlin Coroutines Android application

Sometimes we want to write code without unnecessary libraries in order to better understand the basic principles or just for fun.

As an example, I wrote a simple Android application that allows users to find the meaning of a Russian word:

In this example, the GET request is implemented through the Java built-in tools that are in the package java.net.*

JSON parsing is done through a package built into Android org.json.*

And to execute a request on a background thread, I use callback functions and the Java package java.util.concurrent.*

Debounce effect with a delay of 500 ms is also implemented for searching.

Well, let’s go through all the parts in more detail.

We make a GET request without Retrofit)

I’ll show you the code right away:

open class GetRequest(private val url: String) {

    private val executor = Executors.newSingleThreadExecutor()
    private val handler = Handler(Looper.getMainLooper())

    fun execute(onSuccess: (json: String) -> Unit, onError: (error: GetError) -> Unit) {
        executor.execute {
            try {
                val connection = URL(url).openConnection() as HttpsURLConnection
                connection.requestMethod = "GET"
                connection.setRequestProperty("Content-Type", "application/json; utf-8")
                connection.connectTimeout = 5000
                connection.readTimeout = 5000

                val reader = BufferedReader(InputStreamReader(connection.inputStream))
                val content = StringBuffer()
                var inputLine = reader.readLine()
                while (inputLine != null) {
                    content.append(inputLine)
                    inputLine = reader.readLine()
                }

                connection.disconnect()

                handler.post { onSuccess(content.toString()) }

            } catch (error: Exception) {
                handler.post {
                    if (error is UnknownHostException) {
                        onError(GetError.MISSING_INTERNET)
                    } else {
                        onError(GetError.OTHER)
                    }
                }
            }
        }
    }

}

We are using a static method Executors.newSingleThreadExecutor() to create a single pool of threads that we use to execute our request on a background thread.

Handler used to return the result to the UI thread

HttpsURLConnection included in the built-in package java.net.* and is designed to fulfill network requests.

Parameters HttpsURLConnection I think you understand.

We then read all the data through BufferedReader and send the result further through the callback functions that are passed to the method execute()

Please note that our class can have descendants.

In my test application, this is DictGetRequest:

class DictGetRequest(word: String) : 
	GetRequest("https://api.dictionaryapi.dev/api/v2/entries/ru/$word")

Scary manual JSON parsing 😱

Perhaps it looks very scary:

sealed class DictResultData {

    abstract fun toUi() : DictResultUi

    data class Success(private val word: String, private val definitions: List<DictDefinition>) : DictResultData() {
        override fun toUi(): DictResultUi {
            return DictResultUi.Success(word, definitions)
        }
    }

    data class Error(@StringRes private val resId: Int) : DictResultData() {
        override fun toUi(): DictResultUi {
            return DictResultUi.Error(resId)
        }
    }

    companion object {
        fun fromJson(json: String) : DictResultData {

            if (json.isJsonObject()) {
                return Error(R.string.nothing_found)
            }

            val jsonObject = json.toJsonArray().firstObject()

            val word = jsonObject.str("word")
            val jsonDefinitions = jsonObject.array("meanings")
                .firstObject()
                .array("definitions")

            val definitions = mutableListOf<DictDefinition>()

            for (i in 0 until jsonDefinitions.length()) {

                val jsonDefinition = jsonDefinitions.jsonObject(i)

                val definition = jsonDefinition.str("definition")
                val example = jsonDefinition.str("example")

                definitions.add(DictDefinition(definition, example))
            }

            return Success(word, definitions)
        }
    }

}

I use sealed class because the request may return different response results (error or success).

The logic of the method fromJson() may not seem obvious.

First, Kotlin extensions are used here, which I rendered separately:

fun String.isJsonObject() : Boolean {
    return JSONTokener(this).nextValue() is JSONObject
}

fun String.toJsonArray() : JSONArray {
    return JSONArray(this)
}

fun JSONObject.str(key: String, default: String = "") : String {
    return if (has(key))  getString(key) else default
}

fun JSONArray.firstObject() : JSONObject {
    return if (length() == 0) JSONObject() else getJSONObject(0)
}


fun JSONArray.jsonObject(index: Int) : JSONObject {
    return getJSONObject(index)
}

fun JSONObject.array(key: String, default: JSONArray = JSONArray()) : JSONArray {
    return if (has(key)) getJSONArray(key) else default
}

Secondly, fromJson() can return either an error or success, and therefore I check if JSON is an object, then this is an error (a feature of the response from the server, in case of success it will be an array).

Repository and our ViewModel 🥺

Let’s take a look at the repository and the ViewModel, they are so cute:

// Repository
class DictRepositoryImpl : DictRepository {
    override fun infoAboutWordBy(word: String, onSuccess: (dict: DictResultData) -> Unit) {
        val request = DictGetRequest(word)
        request.execute(
            { json -> onSuccess(DictResultData.fromJson(json)) },
            { error -> onSuccess(DictResultData.Error(error.resId)) }
        )
    }
}

// ViewModel
class DictViewModel(private val repo: DictRepository) : ViewModel() {

    private val wordUi = MutableLiveData<DictResultUi>()

    fun observe(lifecycleOwner: LifecycleOwner, observer: Observer<DictResultUi>) = wordUi.observe(lifecycleOwner, observer)

    fun found(word: String) {
        if (word.isEmpty()) {
            return
        }

        wordUi.value = DictResultUi.Loading
        repo.infoAboutWordBy(word) { result ->
            wordUi.value = result.toUi()
        }
    }

}

Everything is obvious here: in the repository we make a GET request to the server and through the callback functions we get the result, which we then pass to ViewModel

The repository returns an object DictResultData the class that we map to DictResultUi:

sealed class DictResultUi {
    object Loading: DictResultUi()
    data class Error(@StringRes private val textResId: Int): DictResultUi() {
        fun text(view: TextView) {
            view.setText(textResId)
        }
    }
    data class Success(private val word: String, private val definitions: List<DictDefinition>) : DictResultUi() {

        fun word(view: TextView) {
            view.text = word
        }

        fun definitions(layout: LinearLayoutCompat) {
            layout.removeAllViews()
            definitions.mapIndexed { index, definition -> definition.str(index + 1) }
                .forEach { str ->
                    layout.addView(AppCompatTextView(layout.context).apply {
                        text = str
                        setTextSize(TypedValue.COMPLEX_UNIT_SP, 14f)
                        setTextColor(ContextCompat.getColor(context, R.color.grey_300))
                        layoutParams = LinearLayoutCompat.LayoutParams(
                            LinearLayoutCompat.LayoutParams.MATCH_PARENT,
                            LinearLayoutCompat.LayoutParams.WRAP_CONTENT
                        ).apply {
                            bottomMargin = 8.dp(context)
                        }
                    })
                }
        }

    }
}

Do not be alarmed, I did not bother too much and completely refactor the code.

Well, I just love to create UI code 😍

MainActivity and our favorite Debounce effect

Let’s take a look at MainActiivty:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        val viewModel = ViewModelProvider(this, DictViewModelFactory(DictRepositoryImpl()))
            .get(DictViewModel::class.java)

        viewModel.observe(this) { dictResult ->

            val isError = dictResult is DictResultUi.Error
            val isSuccess = dictResult is DictResultUi.Success
            val isLoading = dictResult is DictResultUi.Loading

            binding.frameLayout.isVisible = isLoading or isError
            binding.progress.isVisible = isLoading
            binding.errorText.isVisible = isError
            binding.definitionsLayout.isVisible = isSuccess
            binding.wordText.isVisible = isSuccess

            if (dictResult is DictResultUi.Error) {
                dictResult.text(binding.errorText)
            }

            if (dictResult is DictResultUi.Success) {
                dictResult.word(binding.wordText)
                dictResult.definitions(binding.definitionsLayout)
            }

        }

        val debounce = Debounce(Handler(Looper.getMainLooper()))
        val runnable = Runnable { viewModel.found(binding.searchEdit.text.toString()) }
        binding.searchEdit.onTextChange { debounce.run(runnable) }
        binding.searchBox.setEndIconOnClickListener { runnable.run() }
    }

}

Here we create ViewModel, subscribe to the change LiveData and we make a request when we type text or click on the search button.

Class Debounce as follows:

class Debounce(private val handler: Handler) {

    fun run(runnable: Runnable, delay: Long = 500) {
        handler.removeCallbacks(runnable)
        handler.postDelayed(runnable, delay)
    }

}

Here we cancel the execution of the previous request and start a new one, which will be executed in 500 ms if we do not write anything in the search field.

Conclusion

Unfortunately, I could not, and it is impossible, to disassemble all the subtleties in one article.

I advise you to pay attention to the following points:

  • GET request parameters, transfer of the request body, Headers and Cookies, and other types of requests such as POST, PUT, UPDATE and DELETE

  • how the thread pool works, and Handler

  • and of course refactoring, improving the code and breaking it down into smaller reusable parts

Leave a link per working application

I wish everyone who does not have diabetes warm and sweet winter evenings (just kidding) 😉

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *