Для чтения данных из интернет есть множество библиотек и классов, потому не стоит делать так, как это описано ниже в статье. Я буду использовать базовые библиотеки, и будет много кода (kotlin).
Задача — есть BASE_URL, надо прочитать текстовые данные. Вы можете написать вот такой код функции, как часть вашего активити:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
private fun loadData() { Thread { try { val url = URL(BASE_URL) val urlConnection: URLConnection = url.openConnection() val inputSteam = urlConnection.getInputStream() val inputStreamReader = InputStreamReader(inputSteam) val bufferedInputStreamReader = BufferedReader(inputStreamReader) val result = bufferedInputStreamReader.readLines().joinToString("") Log.d("MainActivity", result.toString()) } catch (e: Exception) { Log.d("MainActivity", e.toString()) } }.start() } companion object { private val BASE_URL: String = "https://shra.ru" } |
Разберем основные моменты.
Сперва нужно убедиться, что у вашего приложения есть необходимые разрешения. Мы читаем данные из сети Интернет. В корень манифеста добавьте строки с разрешениями:
1 2 3 4 |
<manifest ...> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> ... |
Загрузку данных приходится производить в другом потоке, потому используется класс Thread с неявной реализацией Runnable интерфейса. Обычно так не делают, так как с реализацией на Thread много возни, чтобы реализовать всё действительно правильно.
Матрешка из объявлений потоков сделана для оптимизации чтения данных — inputStream — читает данные по байтам, а нам нужен текст (символы), затем мы обертываем это в буферизированный ввод, чтобы читать не символами, я сразу кусками по 8кб:
1 2 3 |
val inputSteam = urlConnection.getInputStream() val inputStreamReader = InputStreamReader(inputSteam) val bufferedInputStreamReader = BufferedReader(inputStreamReader) |
В итоге данные выводятся в лог в другом потоке выполнения кода. А хотелось бы подписаться на источник данных, и что то сделать с ними, когда данные будут готовы, например вывести их уже в основном потоке. И лучше придерживаться шаблона MVVM, т.е. данная функция не должна быть в Activity, а находиться в классе поставщике данных (модели).
MVVM подход
Так наш код распадается на три класса — MainActivity — это класс с активити нашего приложения, MainActivityViewModel — бизнес логика и поставщик LiveData объектов, на которые может подписываться активити. И вся грязная работа падает в модель — класс GetFromInternet, которая поставляет данные из интернета.
Начнем с активити (VIEW составляющая)
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class MainActivity : AppCompatActivity() { private var mainViewModel: MainActivityViewModel? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) mainViewModel = ViewModelProvider(this).get(MainActivityViewModel::class.java) mainViewModel!!.getData().observe(this, { dataString -> Log.d("MainActivity", dataString) }) } } |
Мы создаём экземпляр MainActivityViewModel, от которого требуется поставить нам LiveData объект. Мы на него подписываемся, используя метод observe. И когда данные получены, выводим их в консоль.
Таким образом мы ничего здесь не знаем ни о Модели, ни о бизнес логике.
ViewModel компонент
Этот класс очень простой, т.к. никакой бизнес-логики в нашем приложении нет, реализует только один метод — getData, который вернет LiveData<String> объект.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import android.app.Application import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData class MainActivityViewModel(application: Application) : AndroidViewModel(application) { private val data = MutableLiveData<String>("") fun getData(): LiveData<String> { data.value = GetFromInternet.receiveData() return data } } |
Данные мы получаем из источника GetFromInternet, это наша модель данных.
Источник данных, Модель
Для синхронного получения данных я воспользуюсь сервисом Executors.newSingleThreadExecutor(), который создаёт пул потоков из одного потока. При этом я могу передавать задачи в этот пул и ожидать результат.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
import android.util.Log import java.io.BufferedReader import java.io.InputStreamReader import java.net.URL import java.net.URLConnection import java.util.concurrent.Callable import java.util.concurrent.Executors class GetFromInternet { companion object { private val BASE_URL: String = "https://shra.ru" private val executor = Executors.newSingleThreadExecutor() fun receiveData(): String { val callable = object: Callable<String> { override fun call(): String { Log.d("GetFromInternet", "Starting....") try { val url = URL(BASE_URL) val urlConnection: URLConnection = url.openConnection() val inputSteam = urlConnection.getInputStream() val inputStreamReader = InputStreamReader(inputSteam) val bufferedInputStreamReader = BufferedReader(inputStreamReader) return bufferedInputStreamReader.readText() } catch (e: Exception) { Log.d("GetFromInternet", e.toString()) return "" } } } return executor.submit(callable).get().toString() } } } |
Чтобы задача вернула результат, приходится реализовывать Callable интерфейс, куда мы вставим уже знакомый код.
Метод submit вернет объект типа Future, а его синхронный метод get дождется результаты работы, и его можно будет вернуть уже как результат работы источника данных.
Запрос данных выполняется в дополнительном потоке, потому мы не блокируем основной поток приложения. Когда данные будут считаны, они будут переданы в LiveData и наш наблюдатель инициирует функцию заданную в observe — вывод строки в консоль.