Пишем без Retrofit’а, Gson’a и Kotlin Coroutines Android приложение

Порой нам хочется написать код без лишних библиотек, чтобы более глубоко понять основные принципы или просто ради интереса.

В качестве примера я написал простое 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.

  • ну и конечно же рефакторинг, улучшение кода и разбиение его на более мелкие переиспользуемые части

Оставляю ссылку на рабочее приложение

Желаю всем, у кого не диабет теплых и сладких зимних вечеров (шутка) 😉

Читайте так же:

  • На 440% быстрее, чем Apple MagSafe: Realme дразнит быстрой магнитной зарядкой MagDartНа 440% быстрее, чем Apple MagSafe: Realme дразнит быстрой магнитной зарядкой MagDart Компания Realme опубликовала новые тизеры собственного магнитного беспроводного зарядного устройства под названием MagDart. Которое будет изначально поддерживаться смартфоном Realme Flash, а затем и другими устройствами. Генеральный директор Realme Мадхав Шет (Madhav Sheth) заявил, что […]
  • [Перевод] Звездные войны, сторителлинг и веб-дизайн[Перевод] Звездные войны, сторителлинг и веб-дизайн Вселенная «Звездных войн» — отличный пример того, как фантастический мир вовлекает миллионы фанатов-адептов наподобие религиозного культа. Духовное учение Йоды, джедаизм, оставило существенный след в реальном мире. Психолог Дерек Тогерсон приводит результаты переписи населения […]
  • Как самостоятельно сделать сайт, если ничего не понимаешь в программировании Если вам нужен сайт, у вас есть три варианта: заказать у профессионалов. Собрать на конструкторе или создать самостоятельно с нуля. Первый способ – простой, почти не требует вашего участия, но стоит денег. В этой статье мы расскажем о втором и третьем способах – как […]
  • Вакансии по наполнению сайтовВакансии по наполнению сайтов Компании в области естественных наук в значительной степени зависят от найма квалифицированных специалистов для исследований и разработки инновационных лекарств и устройств. Которые помогают им расти и оставаться конкурентоспособными. Таким образом, чем дольше в вашей компании […]