Пишем без Retrofit’а, json’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: ExecutorService,     private val handler: Handler ) {      fun execute(onSuccess: (json: String) -> Unit, onError: (error: GetError) -> Unit) {         executor.execute {              var conn : HttpsURLConnection? = null              try {                 val connection = URL(url).openConnection() as HttpsURLConnection                 conn = connection                  connection.requestMethod = "GET"                 connection.setRequestProperty("Content-Type", "application/json; utf-8")                 connection.connectTimeout = 5000                 connection.readTimeout = 5000                  val json = connection.inputStream.bufferedReader().readText()                  handler.post { onSuccess(json) }              } catch (error: Exception) {                 conn?.disconnect()                 handler.post {                     if (error is UnknownHostException) {                         onError(GetError.MISSING_INTERNET)                     } else {                         onError(GetError.OTHER)                     }                 }             }         }     }  }

Мы прокидываем ExecucotService, чтобы выполнить наш запрос в background потоке.

Handler используется для возвращения результата на UI поток

HttpsURLConnection входит во встроенный пакет java.net.* и предназначен для выполнения сетевых запросов.

Параметры HttpsURLConnection я думаю вам понятны.

Затем мы читаем все данные через BufferedReader и отправляем результат дальше через функции обратного вызова, которые передаются в метод execute().

Обратите внимание, наш класс может иметь наследников.

Я сделал это лишь для небольшого удобства (можно смело юзать только GetRequest)

В моем тестовом приложении это DictGetRequest:

class DictGetRequest(word: String, executor: ExecutorService, handler: Handler)     : GetRequest("https://api.dictionaryapi.dev/api/v2/entries/ru/$word",                   executor, handler)

Страшный парсинг JSON’а вручную ?

Пожалуй это выглядит очень страшно:

sealed interface DictResultData {      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 interface потому что запрос может вернуть разные результаты ответа (ошибка или успех).

Логика работы метода 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(private val executor: ExecutorService, private val handler: Handler) :     DictRepository {     override fun infoAboutWordBy(word: String, onSuccess: (dict: DictResultData) -> Unit) {         val request = DictGetRequest(word, executor, handler)         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 searchWordDefinition(word: String) {         if (word.isEmpty()) {             return         }          wordUi.value = DictResultUi.Loading         repo.infoAboutWordBy(word) { result ->             wordUi.value = result.toUi()         }     }  }

Здесь все очевидно: в репозитории мы делаем GET запрос на сервер и через функции обратного вызова получаем результат, который далее передаем во ViewModel.

Репозиторий возвращает объект DictResultData класса, который мы маппим в DictResultUi:

sealed interface 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 executor = Executors.newSingleThreadExecutor()         val handler = Handler(Looper.getMainLooper())         val viewModel = ViewModelProvider(this, DictViewModelFactory(DictRepositoryImpl(executor, handler)))             .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              when (dictResult) {                 is DictResultUi.Error -> {                     dictResult.text(binding.errorText)                 }                 is DictResultUi.Success -> {                     dictResult.word(binding.wordText)                     dictResult.definitions(binding.definitionsLayout)                 }                 else -> {}             }          }          val debounce = Debounce(Handler(Looper.getMainLooper()))         val runnable = Runnable { viewModel.searchWordDefinition(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.

  • конечно же рефакторинг, я написал не идеальный код и поэтому для его улучшения придется потратить определенное время (внедрение ExecutorService и других зависимостей через конструктор, разбиение на более мелкие части и создание интерфейсов)

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

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


ссылка на оригинал статьи https://habr.com/ru/post/599657/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *