Порой нам хочется написать код без лишних библиотек, чтобы более глубоко понять основные принципы или просто ради интереса.
В качестве примера я написал простое Android приложение, которое позволяет юзерам найти значение русского слова:
В данном примере GET запрос реализован через встроенные средства Java, которые находятся в пакете java.net.*
Парсинг JSON осуществляется через встроенный в Android пакет org.json.*
А для выполнения запроса на background потоке я использую функции обратного вызова и Java пакет java.util.concurrent.*
Также для поиска реализован Debounce эффект с задержкой в 500 мс.
Ну что ж пройдемся по всем частям более подробно
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)
}
}
}
}
}
}
Мы используем статический метод Executors.newSingleThreadExecutor() для создания одиночного пула потоков, который мы юзаем, чтобы выполнить наш запрос в background потоке.
Handler используется для возвращения результата на UI поток
HttpsURLConnection входит во встроенный пакет java.net.* и предназначен для выполнения сетевых запросов.
Параметры HttpsURLConnection я думаю вам понятны.
Затем мы читаем все данные через BufferedReader и отправляем результат дальше через функции обратного вызова, которые передаются в метод execute().
Обратите внимание, наш класс может иметь наследников.
В моем тестовом приложении это DictGetRequest:
class DictGetRequest(word: String) :
GetRequest("https://api.dictionaryapi.dev/api/v2/entries/ru/$word")
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)
}
}
}
Я юзаю sealed class потому что запрос может вернуть разные результаты ответа (ошибка или успех).
Логика работы метода fromJson() может показаться неочевидной.
Во-первых, здесь используются Kotlin расширения, которые я вынес отдельно:
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
}
Во-вторых, fromJson() может вернуть либо ошибку либо успех и поэтому я проверяю, если JSON является объектом, то это ошибка (особенность ответа от сервера, в случае успеха это будет массив).
// 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()
}
}
}
Здесь все очевидно: в репозитории мы делаем GET запрос на сервер и через функции обратного вызова получаем результат, который далее передаем во ViewModel.
Репозиторий возвращает объект DictResultData класса, который мы маппим в 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)
}
})
}
}
}
}
Не пугайтесь, я не стал сильно заморачиваться и полностью рефакторить код.
Ну и я просто обожаю создавать UI кодом
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() }
}
}
Здесь мы создаем ViewModel, подписываемся на изменение LiveData и делаем запрос, когда набираем текст или нажимаем на кнопку поиска.
Класс Debounce выглядит следующим образом:
class Debounce(private val handler: Handler) {
fun run(runnable: Runnable, delay: Long = 500) {
handler.removeCallbacks(runnable)
handler.postDelayed(runnable, delay)
}
}
Здесь мы отменяем выполнение предыдущего запроса и запускаем новый, который выполнится через 500 мс, если мы не будем ничего писать в поле поиска.
Советую вам обратить внимание на следующие моменты:
Желаю всем, у кого не диабет теплых и сладких зимних вечеров (шутка)
В качестве примера я написал простое Android приложение, которое позволяет юзерам найти значение русского слова:
В данном примере GET запрос реализован через встроенные средства Java, которые находятся в пакете java.net.*
Парсинг JSON осуществляется через встроенный в Android пакет org.json.*
А для выполнения запроса на background потоке я использую функции обратного вызова и Java пакет java.util.concurrent.*
Также для поиска реализован Debounce эффект с задержкой в 500 мс.
Ну что ж пройдемся по всем частям более подробно
Делаем GET запрос без Retrofit'а)
Покажу сразу код: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)
}
}
}
}
}
}
Мы используем статический метод Executors.newSingleThreadExecutor() для создания одиночного пула потоков, который мы юзаем, чтобы выполнить наш запрос в background потоке.
Handler используется для возвращения результата на UI поток
HttpsURLConnection входит во встроенный пакет java.net.* и предназначен для выполнения сетевых запросов.
Параметры HttpsURLConnection я думаю вам понятны.
Затем мы читаем все данные через BufferedReader и отправляем результат дальше через функции обратного вызова, которые передаются в метод execute().
Обратите внимание, наш класс может иметь наследников.
В моем тестовом приложении это DictGetRequest:
class DictGetRequest(word: String) :
GetRequest("https://api.dictionaryapi.dev/api/v2/entries/ru/$word")
Страшный парсинг JSON'а вручную
Пожалуй это выглядит очень страшно: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)
}
}
}
Я юзаю sealed class потому что запрос может вернуть разные результаты ответа (ошибка или успех).
Логика работы метода fromJson() может показаться неочевидной.
Во-первых, здесь используются Kotlin расширения, которые я вынес отдельно:
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
}
Во-вторых, fromJson() может вернуть либо ошибку либо успех и поэтому я проверяю, если JSON является объектом, то это ошибка (особенность ответа от сервера, в случае успеха это будет массив).
Репозиторий и наша ViewModel'ка
Давайте посмотрим на репозиторий и ViewModel'ку, они такие милые:// 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()
}
}
}
Здесь все очевидно: в репозитории мы делаем GET запрос на сервер и через функции обратного вызова получаем результат, который далее передаем во ViewModel.
Репозиторий возвращает объект DictResultData класса, который мы маппим в 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)
}
})
}
}
}
}
Не пугайтесь, я не стал сильно заморачиваться и полностью рефакторить код.
Ну и я просто обожаю создавать UI кодом
MainActivity и наш любимый Debounce эффект
Взглянем на 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() }
}
}
Здесь мы создаем ViewModel, подписываемся на изменение LiveData и делаем запрос, когда набираем текст или нажимаем на кнопку поиска.
Класс Debounce выглядит следующим образом:
class Debounce(private val handler: Handler) {
fun run(runnable: Runnable, delay: Long = 500) {
handler.removeCallbacks(runnable)
handler.postDelayed(runnable, delay)
}
}
Здесь мы отменяем выполнение предыдущего запроса и запускаем новый, который выполнится через 500 мс, если мы не будем ничего писать в поле поиска.
Заключение
Я к сожалению не смог, да это и невозможно, разобрать все тонкости в одной статье.Советую вам обратить внимание на следующие моменты:
- параметры GET запроса, передача тела запроса, Headers и Cookies, ну и другие типы запросов, такие как POST, PUT, UPDATE и DELETE
- принципы работы пула потоков и Handler.
- ну и конечно же рефакторинг, улучшение кода и разбиение его на более мелкие переиспользуемые части
Желаю всем, у кого не диабет теплых и сладких зимних вечеров (шутка)
Пишем без Retrofit'а, json'a и Kotlin Coroutines Android приложение
Порой нам хочется написать код без лишних библиотек, чтобы более глубоко понять основные принципы или просто ради интереса. В качестве примера я написал простое Android приложение, которое позволяет...
habr.com