меню

RSS Reader на Kotlin под Android

Пролог

Чтение RSS - одна из самых важных и широко используемых функций в мобильных приложениях, на веб сайтах.

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

Поэтому сначала мы создадим очень простое приложение для изучения основ функции чтения RSS на Kotlin. В следующих статьях мы будем использовать MVVM для улучшения структуры кода и перепишим наш RSS Reader.

Во время нашего погружения в разработку, мы будем использовать несколько вспомогательных библиотек, таких как: CardView, RecyclerView, Glide, Jsoup.

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

Разработка RSS Reader

Итак, наш проект состоит из:

  • Активити MainActivity
  • Фрагмента под названием RSSFragment (тип фрагмента список - "Fragment (List)");
  • Адаптера Recyclerview с названием MyItemRecylcerviewAdapter;
  • Модель с названием класса RSSItem для хранения определенных данных элемента RSS;
  • Класс парсера для разбора входящих потоков;
  • Класс AppGlideModule, необходимый для Glide.

Ниже приведен код для каждого из файлов, мы рассмотрим их один за другим.

MainActivity.kt

 
package com.relsellglobal.kotlinrssreading

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        supportFragmentManager.beginTransaction().replace(R.id.fragment_root,RSSFragment()).commit();
    }
}
  

В методе onCreate вызываем RSSFragment.

Распределение кода по фрагментам.

Фрагменты - это легкие, повторно используемые компоненты пользовательского интерфейса, которые можно использовать в разработке пользовательского интерфейса в Android. Вызовы активити - тяжелый процесс для ОС Android по сравнению с фрагментом. ОС Android может отклонять действия, вызывающие запросы приложения.

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

Согласно официальной документации Android:

Вы можете думать о фрагменте, как о модульном разделе действий, который имеет свой собственный жизненный цикл, получает свои собственные входные события и который вы можете добавлять или удалять во время выполнения действия (что-то вроде «вспомогательного действия», которое вы можно повторно использовать в различных действиях).

Поэтому мы будем использовать фрагменты в нашем приложении.

  
package com.relsellglobal.kotlinrssreading

import android.os.AsyncTask
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import java.io.IOException
import java.io.InputStream
import java.lang.ref.WeakReference
import java.net.HttpURLConnection
import java.net.URL


/**
 * A fragment representing a list of Items.
 * Activities containing this fragment MUST implement the
 * [RSSFragment.OnListFragmentInteractionListener] interface.
 */
class RSSFragment : Fragment() {

    // TODO: Customize parameters
    private var columnCount = 1

    private var listener: OnListFragmentInteractionListener? = null

    val RSS_FEED_LINK = "https://proweb63.ru/feed.xml";

    var adapter: MyItemRecyclerViewAdapter? = null
    var rssItems = ArrayList<RssItem>()

    var listV : RecyclerView ?= null

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = inflater.inflate(R.layout.fragment_item_list, container, false)

        listV = view.findViewById(R.id.listV)
        return view
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        adapter = MyItemRecyclerViewAdapter(rssItems, listener,activity)
        listV?.layoutManager = LinearLayoutManager(activity,LinearLayoutManager.VERTICAL,false)
        listV?.adapter = adapter

        val url = URL(RSS_FEED_LINK)
        RssFeedFetcher(this).execute(url)

    }

    fun updateRV(rssItemsL: List<RssItem>) {
        if (rssItemsL != null && !rssItemsL.isEmpty()) {
            rssItems.addAll(rssItemsL)
            adapter?.notifyDataSetChanged()
        }
    }


    class RssFeedFetcher(val context: RSSFragment) : AsyncTask<URL, Void, List<RssItem>>() {
        val reference = WeakReference(context)
        private var stream: InputStream? = null;
        override fun doInBackground(vararg params: URL?): List<RssItem>? {
            val connect = params[0]?.openConnection() as HttpURLConnection
            connect.readTimeout = 8000
            connect.connectTimeout = 8000
            connect.requestMethod = "GET"
            connect.connect();

            val responseCode: Int = connect.responseCode;
            var rssItems: List<RssItem>? = null
            if (responseCode == 200) {
                stream = connect.inputStream;


                try {
                    val parser = RssParser()
                    rssItems = parser.parse(stream!!)

                } catch (e: IOException) {
                    e.printStackTrace()
                }


            }

            return rssItems

        }

        override fun onPostExecute(result: List<RssItem>?) {
            super.onPostExecute(result)
            if (result != null && !result.isEmpty()) {
                reference.get()?.updateRV(result)
            }

        }

    }

    interface OnListFragmentInteractionListener {
        // TODO: Update argument type and name
        fun onListFragmentInteraction(item: RssItem?)
    }

}
  

Изучив выше код, Вы обнаружите, что это очень просто. Никаких сложных задач не происходит.

В методе onActivityCreated мы вызываем AsyncTask, чтобы запросить данные RSS-канала из сети, чтобы поток пользовательского интерфейса оставался свободным для выполнения некоторых вещей, таких как показ анимации. Более конкретно, ниже представлен класс asynctask.

Ничего сложного здесь нет, мы просто делаем запрос на получение потока с заданного URL. Убедитесь, что вы предоставили необходимые разрешения в Android Manifest.xml для доступа в Интернет.

 
class RssFeedFetcher(val context: RSSFragment) : AsyncTask<URL, Void, List<RssItem>>() {
        val reference = WeakReference(context)
        private var stream: InputStream? = null;
        override fun doInBackground(vararg params: URL?): List<RssItem>? {
            val connect = params[0]?.openConnection() as HttpURLConnection
            connect.readTimeout = 8000
            connect.connectTimeout = 8000
            connect.requestMethod = "GET"
            connect.connect();

            val responseCode: Int = connect.responseCode;
            var rssItems: List<RssItem>? = null
            if (responseCode == 200) {
                stream = connect.inputStream;


                try {
                    val parser = RssParser()
                    rssItems = parser.parse(stream!!)

                } catch (e: IOException) {
                    e.printStackTrace()
                }


            }

            return rssItems
        }

        override fun onPostExecute(result: List<RssItem>?) {
            super.onPostExecute(result)
            if (result != null && !result.isEmpty()) {
                reference.get()?.updateRV(result)
            }

        }

    }
  

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

Как только мы получаем поток ввода, мы передаем его нашему классу парсера

  
package com.relsellglobal.kotlinrssreading

import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserException
import org.xmlpull.v1.XmlPullParserFactory
import java.io.IOException
import java.io.InputStream


class RssParser {
    private val rssItems = ArrayList<RssItem>()
    private var rssItem : RssItem ?= null
    private var text: String? = null

    fun parse(inputStream: InputStream):List<RssItem> {
        try {
            val factory = XmlPullParserFactory.newInstance()
            factory.isNamespaceAware = true
            val parser = factory.newPullParser()
            parser.setInput(inputStream, null)
            var eventType = parser.eventType
            var foundItem = false
            while (eventType != XmlPullParser.END_DOCUMENT) {
                val tagname = parser.name
                when (eventType) {
                    XmlPullParser.START_TAG -> if (tagname.equals("item", ignoreCase = true)) {
                        // create a new instance of employee
                        foundItem = true
                        rssItem = RssItem()
                    }
                    XmlPullParser.TEXT -> text = parser.text
                    XmlPullParser.END_TAG -> if (tagname.equals("item", ignoreCase = true)) {
                        // add employee object to list
                        rssItem?.let { rssItems.add(it) }
                        foundItem = false
                    } else if ( foundItem && tagname.equals("title", ignoreCase = true)) {
                        rssItem!!.title = text.toString()
                    } else if (foundItem && tagname.equals("link", ignoreCase = true)) {
                        rssItem!!.link = text.toString()
                    } else if (foundItem && tagname.equals("pubDate", ignoreCase = true)) {
                        rssItem!!.pubDate = text.toString()
                    } else if (foundItem && tagname.equals("category", ignoreCase = true)) {
                        rssItem!!.category = text.toString()
                    } else if (foundItem && tagname.equals("description", ignoreCase = true)) {
                        rssItem!!.description = text.toString()
                    }
                }
                eventType = parser.next()
            }

        } catch (e: XmlPullParserException) {
            e.printStackTrace()
        } catch (e: IOException) {
            e.printStackTrace()
        }
        return rssItems
    }
}
  

Наш парсер RSS основан на XMLPullparser в android.

Итак, теперь вызов RSSParser будет анализировать входной поток и помещать все элементы в ArrayList. Если мы сосредоточимся на методе синтаксического анализа, мы увидим, что XMLPullparser вызвал события для начального и конечного тегов XML, присутствующих в RSS-канале.

 
while (eventType != XmlPullParser.END_DOCUMENT) {
    val tagname = parser.name
    when (eventType) {
        XmlPullParser.START_TAG -> if (tagname.equals("item", ignoreCase = true)) {
            // create a new instance of employee
            foundItem = true
            rssItem = RssItem()
        }
        XmlPullParser.TEXT -> text = parser.text
        XmlPullParser.END_TAG -> if (tagname.equals("item", ignoreCase = true)) {
            // add employee object to list
            rssItem?.let { rssItems.add(it) }
            foundItem = false
        } else if ( foundItem && tagname.equals("title", ignoreCase = true)) {
            rssItem!!.title = text.toString()
        } else if (foundItem && tagname.equals("link", ignoreCase = true)) {
            rssItem!!.link = text.toString()
        } else if (foundItem && tagname.equals("pubDate", ignoreCase = true)) {
            rssItem!!.pubDate = text.toString()
        } else if (foundItem && tagname.equals("category", ignoreCase = true)) {
            rssItem!!.category = text.toString()
        } else if (foundItem && tagname.equals("description", ignoreCase = true)) {
            rssItem!!.description = text.toString()
        }
    }
    eventType = parser.next()
}
 

Наконец, вот файлы Gradle, которые показывают зависимости, используемые в нашем приложении. Ниже файл Gradle корневого уровня.

 
projects/modules.

buildscript {
    ext.kotlin_version = '1.3.61'
    repositories {
        google()
        jcenter()
        
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.5.3'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        google()
        jcenter()
        
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}
 

Файл Gradle

 
apply plugin: 'com.android.application'

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-android-extensions'

apply plugin: 'kotlin-kapt'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.2"
    defaultConfig {
        applicationId "com.relsellglobal.kotlinrssreading"
        minSdkVersion 22
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.core:core-ktx:1.0.2'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation 'androidx.legacy:legacy-support-v4:1.0.0'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.0'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'

    // gson
    implementation 'com.google.code.gson:gson:2.8.6'

    //cardview
    implementation 'androidx.cardview:cardview:1.0.0'

    // recyclerview
    implementation "androidx.recyclerview:recyclerview:1.1.0"

    // glide
    implementation 'com.github.bumptech.glide:glide:4.9.0'
    kapt 'com.github.bumptech.glide:compiler:4.9.0'

    // parsing html
    implementation 'org.jsoup:jsoup:1.11.2'


}
 

Улучшение кода с использованием MVVM будет объяснено в следующей статье нашего блога.

Смотреть проект RssReading на GitHub

Удачного кодинга.


Возможно, вам будет интересно

RSS Reader на Kotlin под Android

Чтение RSS - одна из самых важных и широко используемых функций в мобильных приложениях и на веб сайтах.

Оформление заявки

Документы на создание сайта

Изучите наше коммерческое предложение, заполните БРИФ и отправьте его на почту maxidebox@list.ru. Изучив все пожелания из БРИФ-а, обратным ответом оповестим Вас по стоимости разработке, ответим на вопросы.

КП на создание сайта Коммерческое предложение на созданеи сайта

Мы берем на себя ответственность за все стадии работы и полностью избавляем клиентов от забот и необходимости вникать в тонкости.

Скачать БРИФ (акета) на создание сайта Скачать БРИФ (акета) на создание сайта

Зополните у БРИФ-а все необходимые поля. Сделайте краткое описание к каждому из пунктов анкеты, привидите примеры в соответсвующий пунктах - это позволит лучше понять Ваши ожидания и требования к сайту