Евгений Колесников 3 år sedan
förälder
incheckning
5fa7d575cf
2 ändrade filer med 134 tillägg och 102 borttagningar
  1. 132 100
      articles/weather.md
  2. 2 2
      readme.md

+ 132 - 100
articles/weather.md

@@ -8,7 +8,7 @@
 
 # Проект погода (начало): геолокация, http(s)-запросы, разбор json, ImageView.
 
-На примере "Калькулятора" мы познакомились с интерфейсом *Android Studio*, более-менее разобрались со структурой проекта андроид.
+На примере проекта *Калькулятор* мы познакомились с интерфейсом **Android Studio**, более-менее разобрались со структурой проекта андроид.
 
 На примере проекта "Погода" мы научимся:
 
@@ -19,7 +19,7 @@
 * выбор города из списка и получение погоды для выбранного города (во второй части)
 * получение погоды за несколько дней и вывод списка погодных данных (во второй части)
 
-Создайте новый проект
+**Создайте новый проект**
 
 >Если ругается на несовместимость версии, то попробуйте откатить версии зависимостей:
 >
@@ -91,21 +91,21 @@ private fun checkPermission(){
 // в нём мы снова вызываем метод checkPermission
 override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>,
                                                 grantResults: IntArray) {
-when (requestCode) {
-    1 -> {
-        if (grantResults.isNotEmpty() && grantResults[0] ==
-            PackageManager.PERMISSION_GRANTED) {
-            if ((ContextCompat.checkSelfPermission(this@MainActivity, Manifest.permission.ACCESS_FINE_LOCATION) === PackageManager.PERMISSION_GRANTED))
-            {
-                checkPermission()
+    when (requestCode) {
+        1 -> {
+            if (grantResults.isNotEmpty() && grantResults[0] ==
+                PackageManager.PERMISSION_GRANTED) {
+                if ((ContextCompat.checkSelfPermission(this@MainActivity, Manifest.permission.ACCESS_FINE_LOCATION) === PackageManager.PERMISSION_GRANTED))
+                {
+                    checkPermission()
+                }
+            } else {
+                Toast.makeText(this, "Permission Denied", Toast.LENGTH_SHORT).show()
             }
-        } else {
-            Toast.makeText(this, "Permission Denied", Toast.LENGTH_SHORT).show()
+            return
         }
-        return
     }
 }
-}
 
 
 // этот метод будет вызван, когда клиент геолокации получит координаты
@@ -140,13 +140,15 @@ fun onGetCoordinates(lat: Double, lon: Double){
 
 ## Получение информации о погоде
 
+[Раздел из ПМ 05.01 про HTTP](api_php.md)
+
 Для получения информации о погоде воспользуемся открытым АПИ [openweathermap](https://openweathermap.org/api)
 
-В АПИ есть несколько вариантов: текущая погода, почасовая, на несколько дней, на месяц...
+В АПИ есть несколько вариантов: текущая погода, почасовая, на несколько дней, на месяц... Но для бесплатного использования подходят не все.
 
 Для начала получим данные о [текущей](https://openweathermap.org/current) погоде по координатам (By geographic coordinates)
 
-Базовый формат выглядит так:
+Формат запроса:
 
 ```
 api.openweathermap.org/data/2.5/weather?lat={lat}&lon={lon}&appid={API key}
@@ -164,24 +166,24 @@ api.openweathermap.org/data/2.5/weather?lat={lat}&lon={lon}&appid={API key}
 
 В итоге получается такой URL:
 
-```
-https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lon}&units=metric&appid=${token}&lang=ru
+```kt
+val url = "https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lon}&units=metric&appid=${token}&lang=ru"
 ```
 
 Проверить правильность запроса и посмотреть на результат можно запустив этот URL в **Postman**-е (он будет на демо-экзамене). Но лично мне больше нравится плагин **REST Client** для **VSCode**
 
-Можно описать переменные и запрос в отдельном файле (api.http) и выполнять запросы прямо из **VSCode**
+Можно описать переменные и запрос в отдельном файле (например, `api.http`) и выполнять запросы прямо из **VSCode**
 
 ```
 @lat=56.638372
 @lon=47.892991
 @token=d4c9eea0d00fb43230b479793d6aa78f
 
-# Запрос текущей погоды
+### Запрос текущей погоды
 GET https://api.openweathermap.org/data/2.5/weather?lat={{lat}}&lon={{lon}}&units=metric&appid={{token}}&lang=ru
 ```
 
-В ответ на этот запрос должно прийти что-то подобное
+В ответ на этот запрос должно прийти что-то подобное:
 
 ```json
 {
@@ -232,80 +234,90 @@ GET https://api.openweathermap.org/data/2.5/weather?lat={{lat}}&lon={{lon}}&unit
 }
 ```
 
-Для http-запросов в Андроиде есть встроенный клиент. Он сильно устарел и вообще монстрообразный, в реальной разработке обычно используют библиотеки типа **okhttp**. Но т.к. на демо-экзамене не будет доступа в интернет, то придется использовать то что есть.
+В **Android**-е есть встроенные функции работы с **http**-запросами, но стандартный код для сетевых запросов сложен, излишен и в реальном мире почти не используется. Используются библиотеки. Самые популярные: [OkHttp](https://square.github.io/okhttp/) и Retrofit.
 
-Я нашел и адаптировал для вас синглтон (объект), для запросов. Лежит он в [каталоге](../shpora/HttpHelper.kt) `shpora` этого репозитория (тут я текст не привожу, т.к. он ещё не до конца отлажен). Его же я положу и в публичный репозиторий админа на демо-экзамене.
+Рассмотрим работу к **OkHttp**
 
-Создайте у себя в проекте аналогичный файл, **обратите вснимание** на комментарии - в манифест надо добавить разрешения для работы с интернетом.
+>[Примеры синхронных и асинхронных запросов на котлине](https://square.github.io/okhttp/recipes/)
 
-Итак, в методе *onGetCoordinates* вместо вывода координат на экран вставьте http-запрос:
+Перед использованием не забудьте добавить в манифест разрешение на работу с интернетом
 
->Токен объявите переменной в начале класса
->```kt
->private val token = "d4c9eea0d00fb43230b479793d6aa78f"
->```
+```
+<uses-permission android:name="android.permission.INTERNET" />
+```
 
+И, если на сайте нет сертификата, атрибут в тег **application**:
 
-```kt
-val url = "https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lon}&units=metric&appid=${token}&lang=RU"
-HTTP.requestGET(url) {result, error ->
-    if(result != null) {
-        val json = JSONObject(result)
-        val wheather = json.getJSONArray("weather")
-        val icoName = wheather.getJSONObject(0).getString("icon")
-
-        runOnUiThread {
-            textView.text = json.getString("name")
-        }
-    }
-}
+```
+android:usesCleartextTraffic="true"
 ```
 
-Что здесь происходит?
+Ещё в зависимости проекта нужно добавить билиотеку (в файл `build.gradle(:app)` в раздел *dependencies*):
 
-**Во-первых**, андроид не разрешает запускать http-запросы из основного потока. Их нужно заворачивать в потоки или корутины. Я заворачитваю запрос в поток, т.к. для поддержки корутин нужно добавлять библиотеку, вам дополнительно с потоками разбираться не нужно. 
+```
+implementation 'com.squareup.okhttp3:okhttp:4.10.0'
+```
+
+>Токен объявите константой в свойствах класса
+>```kt
+>private val appid = "d4c9eea0d00fb43230b479793d6aa78f"
+>```
+
+Итак, в методе *onGetCoordinates* вместо вывода координат на экран вставьте http-запрос:
+
+>Неизвестные методы **Android Studio** показывает красным цветом. Чтобы добавить пакет, в котором описан такой метод, нужно поместить курсор на этот метод и, либо через контекстное меню, либо нажатием **Alt+Enter** добавить пакет в импортируемые (если вариантов импорта несколько, то смотрите по контексту - в нашем случае в названии пакета должно быть что-то про **okhttp**)
 
 ```kt
-fun requestGET(
-    r_url: String, 
-    callback: (result: String?, error: String)->Unit
-){
-    Thread( Runnable {
-        var error = ""
-        var result: String? = null
-        try {
-            ...
-        }
-        catch (e: Exception){
-            error = e.message.toString()
+// в классе объявите свойство client
+private val client = OkHttpClient()
+...
+
+fun onGetCoordinates(lat: Double, lon: Double){
+    ...
+
+    // сформируйте запрос
+    val request = Request.Builder()
+        .url("https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lon}&appid=${appid}&lang=ru&units=metric")
+        .build()
+
+    // и выполните его
+    client.newCall(request).enqueue(object : Callback {
+        // метод будет вызван при ошибке
+        override fun onFailure(call: Call, e: IOException) {
+            e.printStackTrace()
         }
 
-        callback.invoke(result, error)
-    }).start()
-}
-```
+        // при успешном запросе
+        override fun onResponse(call: Call, response: Response) {
+            response.use {
+                if (!response.isSuccessful) 
+                    throw IOException("Unexpected code $response")
 
-Функция принимает на вход URL, с которого нужно получить данные и callback-функцию в виде лямбда-выражения, которая возвращает текст ответа типа **String?** (нуллабельная строка, т.е. при ошибке вернет **null**) и текст ошибки. 
+                // for ((name, value) in response.headers) {
+                //     Log.d("weather","$name: $value")
+                // }
+                // Log.d("weather", response.body!!.string())
 
-Но в коде главного окна вызов этой функции выглядит иначе - один параметр и какой-то блок кода за функцией
+                val json = JSONObject(response.body!!.string())
+                val wheather = json.getJSONArray("weather")
+                val icoName = wheather.getJSONObject(0).getString("icon")
 
-```kt
-HTTP.requestGET(url) {result, error ->
-    //
+                runOnUiThread {
+                    textView.text = json.getString("name")
+                }
+            }
+        }
+    })
 }
 ```
 
-Это фича котлина. Если лямбда выражение объявлено последним параметром функции, то его можно вынести за скобки. В принципе более привычный аналог выглядит так
+Что здесь происходит?
 
-```kt
-HTTP.requestGET(url, {result, error ->
-    //
-})
-```
+**Во-первых**, андроид не разрешает запускать http(s)-запросы из основного потока. Их нужно запускать асинхронно. В **OkHttp** это делает метод **enqueue**.
 
-Но вы должны знать о такой фиче, т.к. она достаточно часто используется.
+В параметрах метода указывается *callback*-функция в виде лямбда-выражения, в котором должны быть реализованы методы для успешного и неуспешного запросов (тут нужно учитывать, что успех или не успех относятся к транспортному уровню, смог **OkHttp** получить ответ сервера или нет). 
 
-После разбора принятых данных их обычно выводят на экран. Тут надо учитывать что наша функция обратного вызова всё ещё находится в потоке, а к визуальным элементам можно обращаться только из основного потока. Для работы с визуальными элементами заворачиваем кусок кода в конструкцию:
+После разбора принятых данных их обычно выводят на экран. Тут надо учитывать что функция обратного вызова всё ещё находится в потоке, а к визуальным элементам можно обращаться только из основного потока. Для работы с визуальными элементами заворачиваем кусок кода в конструкцию:
 
 ```kt
 runOnUiThread {
@@ -313,16 +325,16 @@ runOnUiThread {
 }
 ```
 
-Тут я вывел на экран название города (разбор JSON будет ниже)
+Тут я вывел на экран только название города (разбор JSON будет ниже)
 
 ## Разбор JSON
 
-Мы получили результат в виде строки, теперь нужно преобразовать её в JSON-объект и достать нужные нам данные
+Мы получили результат в виде JSON-строки, теперь нужно преобразовать её в JSON-объект и достать нужные нам данные
 
 Для преобразования строки в JSON-объект используется конструктор JSONObject
 
 ```kt
-val json = JSONObject(result)
+val json = JSONObject(someJsonString)
 ```
 
 Теперь в переменной json у нас экземпляр JSON-объекта
@@ -330,24 +342,24 @@ val json = JSONObject(result)
 Для получения данных из JSON-объекта есть get методы
 
 * **getJSONArray** - получить массив
-* **getJSONObject** - получение объекта (по индексу из из массива или по имени из объекта)
+* **getJSONObject** - получение объекта (по индексу из массива или по имени из объекта)
 * **getString** - получить строку
 
 Также есть **getInt**, **getDouble**..., доступные методы будут видны в контекстном меню.
 
-Из полученного объекта (листинг был выше) нам нужны:
+Из полученного объекта (листинг был выше) нам нужны (эти данные вы будете выводить на экран по итогам этой лекции):
 
-* название иконки погоды: weather[0].icon
-* описание погоды: weather[0].description
-* температура: main.temp
-* влажность: main.humidity
-* скорость (wind.speed) и направление (wind.deg) ветра
-* название населенного пункта: name
+* название иконки погоды: **weather[0].icon**
+* описание погоды: **weather[0].description**
+* температура: **main.temp**
+* влажность: **main.humidity**
+* скорость (**wind.speed**) и направление (**wind.deg**) ветра
+* название населенного пункта: **name**
 
 Приведу несколько примеров
 
 ```kt
-val json = JSONObject(result)
+val json = JSONObject(response.body!!.string())
 
 // получение массива
 val wheather = json.getJSONArray("weather")
@@ -361,16 +373,14 @@ val temp = json.getJSONObject("main").getDouble("temp")
 
 ## Отображение иконки
 
-В разметку главного окна добавьте элемент **ImageView**
+В разметку окна добавьте элемент **ImageView**
 
 ```xml
 <ImageView
     android:id="@+id/ico"
     android:layout_width="100dp"
     android:layout_height="100dp"
-    app:layout_constraintStart_toStartOf="parent"
-    app:layout_constraintTop_toTopOf="parent"
-    />
+/>
 ```
 
 Для программного отображения изображения нужно вызвать метод **setImageBitmap(Bitmap)**
@@ -379,19 +389,44 @@ val temp = json.getJSONObject("main").getDouble("temp")
 
 В JSON-ответе название иконки погоды находится в массиве погоды. Я его доставал в примере выше (переменная icoName)
 
-Урл иконки выглядит так
+Урл иконки выглядит так (описано в АПИ):
 
-```
-https://openweathermap.org/img/w/${icoName}.png
+```kt
+val icoUrl = "https://openweathermap.org/img/w/${icoName}.png"
 ```
 
-И в моей библиотеке есть отдельный метод для загрузки картинок
+Для загрузки картинок напишем отдельный метод:
 
 ```
-fun getImage(url: String, callback: (result: Bitmap?, error: String)->Unit)
+fun loadImage(icoUrl: String, callback: (result: Bitmap?)->Unit) {
+    val request = Request.Builder()
+        .url(icoUrl)
+        .build()
+
+    client.newCall(request).enqueue(object : Callback {
+        override fun onFailure(call: Call, e: IOException) {
+            e.printStackTrace()
+        }
+
+        override fun onResponse(call: Call, response: Response) {
+            response.use {
+                if (!response.isSuccessful) 
+                    throw IOException("Unexpected code $response")
+
+                // тело запроса считваем как поток байт и передаем его в построитель изображений (BitmapFactory)
+                val bitmap = BitmapFactory
+                    .decodeStream(
+                        response.body!!.byteStream()
+                    )
+                    
+                callback.invoke(bitmap)
+            }
+        }
+    })
+}
 ```
 
-Первый параметр URL изображения, второй лямбда выражение для функции обратного вызова, которое возвращает Bitmap или ошибку
+Первый параметр URL изображения, второй - лямбда выражение для функции обратного вызова, которое возвращает Bitmap
 
 Вызов этого метода можно вставить в предыдущий callback
 
@@ -401,12 +436,9 @@ runOnUiThread {
     textView.text = json.getString("name")
 }
 
-HTTP.getImage("https://openweathermap.org/img/w/${icoName}.png") { bitmap, error ->
-    if (bitmap != null) {
-        var imageView = findViewById<ImageView>(R.id.ico)
-        runOnUiThread {
-            imageView.setImageBitmap(bitmap)
-        }
+loadImage("https://openweathermap.org/img/w/${icoName}.png") {
+    runOnUiThread {
+        ico.setImageBitmap(it)
     }
 }
 ```

+ 2 - 2
readme.md

@@ -477,9 +477,9 @@ tablayout
 10. Лабораторная работа «Тестирование установки»
 
 
-## [Учебная практика](articles/praktika_I.md)
+# [Учебная практика](articles/praktika_I.md)
 
-## [Курсовой проект](articles/kp2.md)
+# [Курсовой проект](articles/kp2.md)
 
 <!-- ПООП