diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml
index a00b8662..8b80ad42 100644
--- a/.github/workflows/android.yml
+++ b/.github/workflows/android.yml
@@ -3,12 +3,10 @@ name: Android CI
 on:
   push:
     branches:
-      - master
-      - develop
+      - '**'
   pull_request:
     branches:
-      - master
-      - develop
+      - '**'
 
 jobs:
   build:
@@ -24,7 +22,7 @@ jobs:
     - name: Build with Gradle
       run: ./gradlew clean build
     - name: Android Emulator Runner
-      uses: ReactiveCircus/android-emulator-runner@v2.5.0
+      uses: ReactiveCircus/android-emulator-runner@v2.19.0
       with:
         api-level: 29
         script: ./gradlew connectedCheck
diff --git a/api/build.gradle b/api/build.gradle
index a254b5f5..72abc1e1 100644
--- a/api/build.gradle
+++ b/api/build.gradle
@@ -1,6 +1,5 @@
 apply plugin: 'com.android.library'
 apply plugin: 'kotlin-android'
-apply plugin: 'kotlin-android-extensions'
 apply plugin: 'kotlin-kapt'
 
 android {
@@ -18,6 +17,10 @@ android {
         abortOnError false
     }
 
+    sourceSets {
+        androidTest.assets.srcDirs += files("$projectDir/androidTest/assets".toString())
+    }
+
     buildTypes {
         release {
             minifyEnabled true
@@ -33,37 +36,45 @@ android {
         sourceCompatibility JavaVersion.VERSION_1_8
         targetCompatibility JavaVersion.VERSION_1_8
     }
+    kotlinOptions {
+        jvmTarget = '1.8'
+    }
 }
 
 dependencies {
     implementation fileTree(dir: 'libs', include: ['*.jar'])
     implementation project(':db')
 
-    // xpp3 has a conflict with kxml when running connectedCheck task
-    configurations {
-        all*.exclude group: 'xpp3', module: 'xpp3'
+    testImplementation 'junit:junit:4.13'
+    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
+    androidTestImplementation 'androidx.test:runner:1.4.0'
+    androidTestImplementation 'androidx.test:rules:1.4.0'
+    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
+    testImplementation 'com.squareup.okhttp3:mockwebserver:4.9.0'
+    androidTestImplementation "io.insert-koin:koin-test:2.2.3"
+    testImplementation "io.insert-koin:koin-test:2.2.3"
+
+    implementation 'com.gitlab.mvysny.konsume-xml:konsume-xml:1.0'
+
+    implementation 'com.squareup.okhttp3:okhttp:4.9.1'
+
+    implementation('com.squareup.retrofit2:retrofit:2.9.0') {
+        exclude group: 'okhttp3', module: 'okhttp3'
     }
-
-    implementation "androidx.core:core-ktx:1.2.0"
-    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
-
-    testImplementation 'junit:junit:4.12'
-    androidTestImplementation 'androidx.test:runner:1.2.0'
-    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
-
-    implementation 'com.squareup.retrofit2:retrofit:2.7.1'
-    implementation('com.squareup.retrofit2:converter-moshi:2.7.1') {
+    implementation('com.squareup.retrofit2:converter-moshi:2.9.0') {
         exclude group: 'moshi', module: 'moshi' // moshi converter uses moshi 1.8.0 which breaks codegen 1.9.2
     }
 
-    implementation 'com.squareup.retrofit2:converter-simplexml:2.7.1'
-    implementation 'com.squareup.retrofit2:adapter-rxjava2:2.7.1'
+    implementation ('com.squareup.retrofit2:converter-simplexml:2.9.0')
 
-    implementation 'com.squareup.moshi:moshi:1.9.2'
-    kapt 'com.squareup.moshi:moshi-kotlin-codegen:1.9.2'
+    implementation 'com.squareup.retrofit2:adapter-rxjava2:2.9.0'
 
-    implementation 'com.squareup.okhttp3:logging-interceptor:4.2.0'
+    implementation 'com.squareup.moshi:moshi:1.12.0'
+    kapt 'com.squareup.moshi:moshi-kotlin-codegen:1.12.0'
 
     api 'io.reactivex.rxjava2:rxandroid:2.1.1'
-    api 'org.jsoup:jsoup:1.12.1'
+    api 'org.jsoup:jsoup:1.13.1'
+
+    debugApi 'com.chimerapps.niddler:niddler:1.5.5'
+    releaseApi 'com.chimerapps.niddler:niddler-noop:1.5.5'
 }
diff --git a/api/src/androidTest/assets/opml/lite_subscriptions.opml b/api/src/androidTest/assets/opml/lite_subscriptions.opml
new file mode 100644
index 00000000..cd4d40ba
--- /dev/null
+++ b/api/src/androidTest/assets/opml/lite_subscriptions.opml
@@ -0,0 +1,9 @@
+<opml version="2.0">
+	<body>
+		<outline>
+            <outline title="The Verge" xmlUrl='http://www.theverge.com/rss/index.xml' htmlUrl="http://www.theverge.com" />
+		</outline>
+
+        <outline title="TechCrunch" xmlUrl='https://techcrunch.com/feed/' htmlUrl="https://techcrunch.com/" />
+	</body>
+</opml>
\ No newline at end of file
diff --git a/api/src/androidTest/assets/opml/subscriptions.opml b/api/src/androidTest/assets/opml/subscriptions.opml
new file mode 100755
index 00000000..c2998c87
--- /dev/null
+++ b/api/src/androidTest/assets/opml/subscriptions.opml
@@ -0,0 +1,27 @@
+<opml version="2.0">
+	<body>
+		<outline text="Folder 1" title="Folder 1">
+		    <outline text="Subfolder 1" title="Subfolder 1">
+                <outline title="The Verge" xmlUrl='http://www.theverge.com/rss/index.xml' htmlUrl="http://www.theverge.com" />
+                <outline title="TechCrunch" xmlUrl='https://techcrunch.com/feed/' htmlUrl="https://techcrunch.com/" />
+                <outline xmlUrl='http://feeds.mashable.com/Mashable' />
+                <outline xmlUrl='http://www.engadget.com/rss.xml' />
+            </outline>
+
+            <outline text="Subfolder 2" title="Subfolder 2">
+                <outline text="Sub subfolder 1" title="Sub subfolder 1">
+                    <outline title="The Verge" xmlUrl='http://www.theverge.com/rss/index.xml' htmlUrl="http://www.theverge.com" />
+                    <outline title="TechCrunch" xmlUrl='https://techcrunch.com/feed/' htmlUrl="https://techcrunch.com/" />
+                </outline>
+                <outline text="Sub subfolder 2" title="Sub subfolder 2">
+                </outline>
+                <outline xmlUrl='http://www.engadget.com/rss.xml' />
+            </outline>
+            <outline xmlUrl='http://feeds.mashable.com/Mashable' />
+            <outline xmlUrl='http://www.engadget.com/rss.xml' />
+		</outline>
+
+		<outline title="The Verge" xmlUrl='http://www.theverge.com/rss/index.xml' htmlUrl="http://www.theverge.com" />
+        <outline title="TechCrunch" xmlUrl='https://techcrunch.com/feed/' htmlUrl="https://techcrunch.com/" />
+	</body>
+</opml>
\ No newline at end of file
diff --git a/api/src/androidTest/assets/opml/wrong_version.opml b/api/src/androidTest/assets/opml/wrong_version.opml
new file mode 100644
index 00000000..da6af325
--- /dev/null
+++ b/api/src/androidTest/assets/opml/wrong_version.opml
@@ -0,0 +1,6 @@
+<opml version="1.0">
+	<body>
+		<outline title="The Verge" xmlUrl='http://www.theverge.com/rss/index.xml' htmlUrl="http://www.theverge.com" />
+        <outline title="TechCrunch" xmlUrl='https://techcrunch.com/feed/' htmlUrl="https://techcrunch.com/" />
+	</body>
+</opml>
\ No newline at end of file
diff --git a/api/src/androidTest/java/com/readrops/api/ExampleInstrumentedTest.java b/api/src/androidTest/java/com/readrops/api/ExampleInstrumentedTest.java
deleted file mode 100644
index 3c0ef7cf..00000000
--- a/api/src/androidTest/java/com/readrops/api/ExampleInstrumentedTest.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package com.readrops.api;
-
-import android.content.Context;
-import androidx.test.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import static org.junit.Assert.*;
-
-/**
- * Instrumented test, which will execute on an Android device.
- *
- * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
- */
-@RunWith(AndroidJUnit4.class)
-public class ExampleInstrumentedTest {
-    @Test
-    public void useAppContext() {
-        // Context of the app under test.
-        Context appContext = InstrumentationRegistry.getTargetContext();
-
-        assertEquals("com.readrops.api.test", appContext.getPackageName());
-    }
-}
diff --git a/api/src/androidTest/java/com/readrops/api/OPMLParserTest.kt b/api/src/androidTest/java/com/readrops/api/OPMLParserTest.kt
new file mode 100644
index 00000000..2fd10595
--- /dev/null
+++ b/api/src/androidTest/java/com/readrops/api/OPMLParserTest.kt
@@ -0,0 +1,112 @@
+package com.readrops.api
+
+import android.Manifest
+import android.content.Context
+import android.os.Environment
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.rule.GrantPermissionRule
+import com.readrops.api.opml.OPMLParser
+import com.readrops.api.utils.exceptions.ParseException
+import com.readrops.db.entities.Feed
+import com.readrops.db.entities.Folder
+import io.reactivex.schedulers.Schedulers
+import junit.framework.TestCase.assertEquals
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.io.File
+import java.io.FileOutputStream
+import java.io.OutputStream
+
+@RunWith(AndroidJUnit4::class)
+class OPMLParserTest {
+
+    private val context: Context = InstrumentationRegistry.getInstrumentation().context
+
+    @get:Rule
+    val permissionRule: GrantPermissionRule = GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+
+    @Test
+    fun readOpmlTest() {
+        val stream = context.resources.assets.open("opml/subscriptions.opml")
+
+        var foldersAndFeeds: Map<Folder?, List<Feed>>? = null
+
+        OPMLParser.read(stream)
+                .observeOn(Schedulers.trampoline())
+                .subscribeOn(Schedulers.trampoline())
+                .subscribe { result -> foldersAndFeeds = result }
+
+        assertEquals(foldersAndFeeds?.size, 6)
+
+        assertEquals(foldersAndFeeds?.get(Folder("Folder 1"))?.size, 2)
+        assertEquals(foldersAndFeeds?.get(Folder("Subfolder 1"))?.size, 4)
+        assertEquals(foldersAndFeeds?.get(Folder("Subfolder 2"))?.size, 1)
+        assertEquals(foldersAndFeeds?.get(Folder("Sub subfolder 1"))?.size, 2)
+        assertEquals(foldersAndFeeds?.get(Folder("Sub subfolder 2"))?.size, 0)
+        assertEquals(foldersAndFeeds?.get(null)?.size, 2)
+
+        stream.close()
+    }
+
+    @Test
+    fun readLiteSubscriptionsTest() {
+        val stream = context.resources.assets.open("opml/lite_subscriptions.opml")
+
+        var foldersAndFeeds: Map<Folder?, List<Feed>>? = null
+
+        OPMLParser.read(stream)
+                .subscribe { result -> foldersAndFeeds = result }
+
+        assertEquals(foldersAndFeeds?.values?.first()?.size, 2)
+        assertEquals(foldersAndFeeds?.values?.first()?.first()?.url, "http://www.theverge.com/rss/index.xml")
+        assertEquals(foldersAndFeeds?.values?.first()?.get(1)?.url, "https://techcrunch.com/feed/")
+
+        stream.close()
+    }
+
+    @Test
+    fun opmlVersionTest() {
+        val stream = context.resources.assets.open("opml/wrong_version.opml")
+
+        OPMLParser.read(stream)
+                .test()
+                .assertError(ParseException::class.java)
+
+        stream.close()
+    }
+
+    @Test
+    fun writeOpmlTest() {
+        val filePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath
+        val file = File(filePath, "subscriptions.opml")
+
+        val outputStream: OutputStream = FileOutputStream(file)
+        val foldersAndFeeds: Map<Folder?, List<Feed>> = HashMap<Folder?, List<Feed>>().apply {
+            put(null, listOf(Feed("Feed1", "", "https://feed1.com"),
+                    Feed("Feed2", "", "https://feed2.com")))
+            put(Folder("Folder1"), listOf())
+            put(Folder("Folder2"), listOf(Feed("Feed3", "", "https://feed3.com"),
+                    Feed("Feed4", "", "https://feed4.com")))
+        }
+
+        OPMLParser.write(foldersAndFeeds, outputStream)
+                .subscribeOn(Schedulers.trampoline())
+                .subscribe()
+
+        outputStream.flush()
+        outputStream.close()
+
+        val inputStream = file.inputStream()
+        var foldersAndFeeds2: Map<Folder?, List<Feed>>? = null
+        OPMLParser.read(inputStream).subscribe { result -> foldersAndFeeds2 = result }
+
+        assertEquals(foldersAndFeeds.size, foldersAndFeeds2?.size)
+        assertEquals(foldersAndFeeds[Folder("Folder1")]?.size, foldersAndFeeds2?.get(Folder("Folder1"))?.size)
+        assertEquals(foldersAndFeeds[Folder("Folder2")]?.size, foldersAndFeeds2?.get(Folder("Folder2"))?.size)
+        assertEquals(foldersAndFeeds[null]?.size, foldersAndFeeds2?.get(null)?.size)
+
+        inputStream.close()
+    }
+}
\ No newline at end of file
diff --git a/api/src/debug/AndroidManifest.xml b/api/src/debug/AndroidManifest.xml
new file mode 100644
index 00000000..bc80eafd
--- /dev/null
+++ b/api/src/debug/AndroidManifest.xml
@@ -0,0 +1,12 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.readrops.api">
+
+    <!-- for tests only -->
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.INTERNET" />
+
+    <application
+        android:requestLegacyExternalStorage="true"
+        android:usesCleartextTraffic="true" />
+
+</manifest>
\ No newline at end of file
diff --git a/api/src/main/java/com/readrops/api/ApiModule.kt b/api/src/main/java/com/readrops/api/ApiModule.kt
new file mode 100644
index 00000000..fe8e9960
--- /dev/null
+++ b/api/src/main/java/com/readrops/api/ApiModule.kt
@@ -0,0 +1,100 @@
+package com.readrops.api
+
+import com.chimerapps.niddler.interceptor.okhttp.NiddlerOkHttpInterceptor
+import com.readrops.api.localfeed.LocalRSSDataSource
+import com.readrops.api.services.Credentials
+import com.readrops.api.services.freshrss.FreshRSSDataSource
+import com.readrops.api.services.freshrss.FreshRSSService
+import com.readrops.api.services.freshrss.adapters.FreshRSSFeedsAdapter
+import com.readrops.api.services.freshrss.adapters.FreshRSSFoldersAdapter
+import com.readrops.api.services.freshrss.adapters.FreshRSSItemsAdapter
+import com.readrops.api.services.freshrss.adapters.FreshRSSItemsIdsAdapter
+import com.readrops.api.services.nextcloudnews.NextNewsDataSource
+import com.readrops.api.services.nextcloudnews.NextNewsService
+import com.readrops.api.services.nextcloudnews.adapters.NextNewsFeedsAdapter
+import com.readrops.api.services.nextcloudnews.adapters.NextNewsFoldersAdapter
+import com.readrops.api.services.nextcloudnews.adapters.NextNewsItemsAdapter
+import com.readrops.api.utils.AuthInterceptor
+import com.readrops.db.entities.Item
+import com.squareup.moshi.Moshi
+import com.squareup.moshi.Types
+import okhttp3.OkHttpClient
+import org.koin.core.qualifier.named
+import org.koin.dsl.module
+import retrofit2.Retrofit
+import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
+import retrofit2.converter.moshi.MoshiConverterFactory
+import java.util.concurrent.TimeUnit
+
+val apiModule = module {
+
+    single(createdAtStart = true) {
+        OkHttpClient.Builder()
+                .callTimeout(1, TimeUnit.MINUTES)
+                .readTimeout(1, TimeUnit.HOURS)
+                .addInterceptor(get<AuthInterceptor>())
+                .addInterceptor(NiddlerOkHttpInterceptor(get(), "niddler"))
+                .build()
+    }
+
+    single { AuthInterceptor() }
+
+    single { LocalRSSDataSource(get()) }
+
+    //region freshrss
+
+    factory { params -> FreshRSSDataSource(get(parameters = { params })) }
+
+    factory { params ->
+        get<Retrofit>(named("freshrssRetrofit"), parameters = { params })
+                .create(FreshRSSService::class.java)
+    }
+
+    factory(named("freshrssRetrofit")) { (credentials: Credentials) ->
+        Retrofit.Builder()
+                .baseUrl(credentials.url)
+                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
+                .client(get())
+                .addConverterFactory(MoshiConverterFactory.create(get(named("freshrssMoshi"))))
+                .build()
+    }
+
+    single(named("freshrssMoshi")) {
+        Moshi.Builder()
+                .add(Types.newParameterizedType(List::class.java, Item::class.java), FreshRSSItemsAdapter())
+                .add(Types.newParameterizedType(List::class.java, String::class.java), FreshRSSItemsIdsAdapter())
+                .add(FreshRSSFeedsAdapter())
+                .add(FreshRSSFoldersAdapter())
+                .build()
+    }
+
+    //endregion freshrss
+
+    //region nextcloud news
+
+    factory { params -> NextNewsDataSource(get(parameters = { params })) }
+
+    factory { params ->
+        get<Retrofit>(named("nextcloudNewsRetrofit"), parameters = { params })
+                .create(NextNewsService::class.java)
+    }
+
+    factory(named("nextcloudNewsRetrofit")) { (credentials: Credentials) ->
+        Retrofit.Builder()
+                .baseUrl(credentials.url)
+                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
+                .client(get())
+                .addConverterFactory(MoshiConverterFactory.create(get(named("nextcloudNewsMoshi"))))
+                .build()
+    }
+
+    single(named("nextcloudNewsMoshi")) {
+        Moshi.Builder()
+                .add(NextNewsFeedsAdapter())
+                .add(NextNewsFoldersAdapter())
+                .add(Types.newParameterizedType(List::class.java, Item::class.java), NextNewsItemsAdapter())
+                .build()
+    }
+
+    //endregion nextcloud news
+}
\ No newline at end of file
diff --git a/api/src/main/java/com/readrops/api/localfeed/AFeed.kt b/api/src/main/java/com/readrops/api/localfeed/AFeed.kt
deleted file mode 100644
index 3cbd7653..00000000
--- a/api/src/main/java/com/readrops/api/localfeed/AFeed.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.readrops.api.localfeed
-
-/*
- A simple class to give an abstract level to rss/atom/json feed classes
- */
-abstract class AFeed {
-    var etag: String? = null
-    var lastModified: String? = null
-}
\ No newline at end of file
diff --git a/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt b/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt
new file mode 100644
index 00000000..d0900cec
--- /dev/null
+++ b/api/src/main/java/com/readrops/api/localfeed/LocalRSSDataSource.kt
@@ -0,0 +1,152 @@
+package com.readrops.api.localfeed
+
+import android.accounts.NetworkErrorException
+import androidx.annotation.WorkerThread
+import com.readrops.api.localfeed.json.JSONFeedAdapter
+import com.readrops.api.localfeed.json.JSONItemsAdapter
+import com.readrops.api.utils.ApiUtils
+import com.readrops.api.utils.AuthInterceptor
+import com.readrops.api.utils.exceptions.ParseException
+import com.readrops.api.utils.exceptions.UnknownFormatException
+import com.readrops.db.entities.Feed
+import com.readrops.db.entities.Item
+import com.squareup.moshi.Moshi
+import com.squareup.moshi.Types
+import okhttp3.Headers
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.Response
+import okio.Buffer
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.get
+import java.io.ByteArrayInputStream
+import java.io.IOException
+import java.io.InputStream
+import java.net.HttpURLConnection
+
+class LocalRSSDataSource(private val httpClient: OkHttpClient): KoinComponent {
+
+    /**
+     * Query RSS url
+     * @param url url to query
+     * @param headers request headers
+     * @return a Feed object with its items
+     */
+    @Throws(ParseException::class, UnknownFormatException::class, NetworkErrorException::class, IOException::class)
+    @WorkerThread
+    fun queryRSSResource(url: String, headers: Headers?): Pair<Feed, List<Item>>? {
+        get<AuthInterceptor>().credentials = null
+        val response = queryUrl(url, headers)
+
+        return when {
+            response.isSuccessful -> {
+                val header = response.header(ApiUtils.CONTENT_TYPE_HEADER)
+                        ?: throw UnknownFormatException("Unable to get $url content-type")
+
+                val contentType = ApiUtils.parseContentType(header)
+                        ?: throw ParseException("Unable to parse $url content-type")
+
+                var type = LocalRSSHelper.getRSSType(contentType)
+
+                val bodyArray = response.peekBody(Long.MAX_VALUE).bytes()
+
+                // if we can't guess type based on content-type header, we use the content
+                if (type == LocalRSSHelper.RSSType.UNKNOWN)
+                    type = LocalRSSHelper.getRSSContentType(ByteArrayInputStream(bodyArray))
+                // if we can't guess type even with the content, we are unable to go further
+                if (type == LocalRSSHelper.RSSType.UNKNOWN) throw UnknownFormatException("Unable to guess $url RSS type")
+
+                val feed = parseFeed(ByteArrayInputStream(bodyArray), type, response)
+                val items = parseItems(ByteArrayInputStream(bodyArray), type)
+
+                response.body?.close()
+                Pair(feed, items)
+            }
+            response.code == HttpURLConnection.HTTP_NOT_MODIFIED -> null
+            else -> throw NetworkErrorException("$url returned ${response.code} code : ${response.message}")
+        }
+    }
+
+    /**
+     * Checks if the provided url is a RSS resource
+     * @param url url to check
+     * @return true if [url] is a RSS resource, false otherwise
+     */
+    @WorkerThread
+    fun isUrlRSSResource(url: String): Boolean {
+        val response = queryUrl(url, null)
+
+        return if (response.isSuccessful) {
+            val header = response.header(ApiUtils.CONTENT_TYPE_HEADER)
+                    ?: return false
+
+            val contentType = ApiUtils.parseContentType(header)
+                    ?: return false
+
+            var type = LocalRSSHelper.getRSSType(contentType)
+
+            if (type == LocalRSSHelper.RSSType.UNKNOWN)
+                type = LocalRSSHelper.getRSSContentType(response.body?.byteStream()!!) // stream is closed in helper method
+
+            type != LocalRSSHelper.RSSType.UNKNOWN
+        } else false
+    }
+
+    @Throws(IOException::class)
+    private fun queryUrl(url: String, headers: Headers?): Response {
+        val requestBuilder = Request.Builder().url(url)
+        headers?.let { requestBuilder.headers(it) }
+
+        return httpClient.newCall(requestBuilder.build()).execute()
+    }
+
+    private fun parseFeed(stream: InputStream, type: LocalRSSHelper.RSSType, response: Response): Feed {
+        val feed = if (type != LocalRSSHelper.RSSType.JSONFEED) {
+            val adapter = XmlAdapter.xmlFeedAdapterFactory(type)
+
+            adapter.fromXml(stream)
+        } else {
+            val adapter = Moshi.Builder()
+                    .add(JSONFeedAdapter())
+                    .build()
+                    .adapter(Feed::class.java)
+
+            adapter.fromJson(Buffer().readFrom(stream))!!
+        }
+
+        handleSpecialCases(feed, type, response)
+
+        feed.etag = response.header(ApiUtils.ETAG_HEADER)
+        feed.lastModified = response.header(ApiUtils.LAST_MODIFIED_HEADER)
+
+        return feed
+    }
+
+    private fun parseItems(stream: InputStream, type: LocalRSSHelper.RSSType): List<Item> {
+        return if (type != LocalRSSHelper.RSSType.JSONFEED) {
+            val adapter = XmlAdapter.xmlItemsAdapterFactory(type)
+
+            adapter.fromXml(stream)
+        } else {
+            val adapter = Moshi.Builder()
+                    .add(Types.newParameterizedType(MutableList::class.java, Item::class.java), JSONItemsAdapter())
+                    .build()
+                    .adapter<List<Item>>(Types.newParameterizedType(MutableList::class.java, Item::class.java))
+
+            adapter.fromJson(Buffer().readFrom(stream))!!
+        }
+    }
+
+    private fun handleSpecialCases(feed: Feed, type: LocalRSSHelper.RSSType, response: Response) {
+        with(feed) {
+            if (type == LocalRSSHelper.RSSType.RSS_2) {
+                // if an atom:link element was parsed, we still replace its value as it is unreliable,
+                // otherwise we just add the rss url
+                url = response.request.url.toString()
+            } else if (type == LocalRSSHelper.RSSType.ATOM || type == LocalRSSHelper.RSSType.RSS_1) {
+                if (url == null) url = response.request.url.toString()
+                if (siteUrl == null) siteUrl = response.request.url.scheme + "://" + response.request.url.host
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/api/src/main/java/com/readrops/api/localfeed/LocalRSSHelper.kt b/api/src/main/java/com/readrops/api/localfeed/LocalRSSHelper.kt
new file mode 100644
index 00000000..3651ebe9
--- /dev/null
+++ b/api/src/main/java/com/readrops/api/localfeed/LocalRSSHelper.kt
@@ -0,0 +1,66 @@
+package com.readrops.api.localfeed
+
+import java.io.InputStream
+
+object LocalRSSHelper {
+
+    private const val RSS_1_CONTENT_TYPE = "application/rdf+xml"
+    private const val RSS_2_CONTENT_TYPE = "application/rss+xml"
+    private const val ATOM_CONTENT_TYPE = "application/atom+xml"
+    private const val JSONFEED_CONTENT_TYPE = "application/feed+json"
+    private const val JSON_CONTENT_TYPE = "application/json"
+
+    private const val RSS_1_REGEX = "<rdf:RDF.*xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\""
+    private const val RSS_2_REGEX = "rss.*version=\"2.0\""
+    private const val ATOM_REGEX = "<feed.* xmlns=\"http://www.w3.org/2005/Atom\""
+
+    /**
+     * Guess RSS type based on content-type header
+     */
+    fun getRSSType(contentType: String): RSSType {
+        return when (contentType) {
+            RSS_1_CONTENT_TYPE -> RSSType.RSS_1
+            RSS_2_CONTENT_TYPE -> RSSType.RSS_2
+            ATOM_CONTENT_TYPE -> RSSType.ATOM
+            JSON_CONTENT_TYPE, JSONFEED_CONTENT_TYPE -> RSSType.JSONFEED
+            else -> RSSType.UNKNOWN
+        }
+    }
+
+    /**
+     * Guess RSS type based on xml content
+     */
+    fun getRSSContentType(content: InputStream): RSSType {
+        val stringBuffer = StringBuffer()
+        val reader = content.bufferedReader()
+
+        // we get the first 10 lines which should be sufficient to get the type,
+        // otherwise iterating over the whole file could be too slow
+        for (i in 0..9) stringBuffer.append(reader.readLine())
+
+        val string = stringBuffer.toString()
+        val type = when {
+            RSS_1_REGEX.toRegex().containsMatchIn(string) -> RSSType.RSS_1
+            RSS_2_REGEX.toRegex().containsMatchIn(string) -> RSSType.RSS_2
+            ATOM_REGEX.toRegex().containsMatchIn(string) -> RSSType.ATOM
+            else -> RSSType.UNKNOWN
+        }
+
+        reader.close()
+        content.close()
+        return type
+    }
+
+    @JvmStatic
+    fun isRSSType(type: String?): Boolean {
+        return if (type != null) getRSSType(type) != RSSType.UNKNOWN else false
+    }
+
+    enum class RSSType {
+        RSS_1,
+        RSS_2,
+        ATOM,
+        JSONFEED,
+        UNKNOWN
+    }
+}
\ No newline at end of file
diff --git a/api/src/main/java/com/readrops/api/localfeed/RSSQuery.java b/api/src/main/java/com/readrops/api/localfeed/RSSQuery.java
deleted file mode 100644
index 0700a02e..00000000
--- a/api/src/main/java/com/readrops/api/localfeed/RSSQuery.java
+++ /dev/null
@@ -1,197 +0,0 @@
-package com.readrops.api.localfeed;
-
-import android.accounts.NetworkErrorException;
-import android.util.Log;
-
-import com.readrops.api.localfeed.atom.ATOMFeed;
-import com.readrops.api.localfeed.json.JSONFeed;
-import com.readrops.api.localfeed.rss.RSSFeed;
-import com.readrops.api.localfeed.rss.RSSLink;
-import com.readrops.api.utils.HttpManager;
-import com.readrops.api.utils.LibUtils;
-import com.readrops.api.utils.UnknownFormatException;
-import com.squareup.moshi.JsonAdapter;
-import com.squareup.moshi.Moshi;
-
-import org.simpleframework.xml.Serializer;
-import org.simpleframework.xml.core.Persister;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-import okhttp3.OkHttpClient;
-import okhttp3.Request;
-import okhttp3.Response;
-
-public class RSSQuery {
-
-    private static final String TAG = RSSQuery.class.getSimpleName();
-
-    private static final String RSS_CONTENT_TYPE_REGEX = "([^;]+)";
-
-    private static final String RSS_2_REGEX = "rss.*version=\"2.0\"";
-
-    private static final String ATOM_REGEX = "<feed.* xmlns=\"http://www.w3.org/2005/Atom\"";
-
-    /**
-     * Request the url given in parameter.
-     * This method is synchronous, it <b>has</b> to be called from another thread than the main one.
-     *
-     * @param url url to request
-     * @throws Exception
-     */
-    public RSSQueryResult queryUrl(String url, Map<String, String> headers) throws Exception {
-        Response response = query(url, headers);
-
-        if (response.isSuccessful()) {
-            String header = response.header(LibUtils.CONTENT_TYPE_HEADER);
-            RSSType type = getRSSType(header);
-
-            if (type == null)
-                return new RSSQueryResult(new UnknownFormatException("bad content type : " + header + "for " + url));
-
-            return parseFeed(response.body().byteStream(), type, response);
-        } else if (response.code() == 304)
-            return null;
-        else
-            return new RSSQueryResult(new NetworkErrorException("Error " + response.code() + " when requesting url " + url));
-    }
-
-    public boolean isUrlFeedLink(String url) throws IOException {
-        Response response = query(url, new HashMap<>());
-
-        if (response.isSuccessful()) {
-            String header = response.header(LibUtils.CONTENT_TYPE_HEADER);
-            RSSType type = getRSSType(header);
-
-            if (type == RSSType.RSS_UNKNOWN) {
-                RSSType contentType = getContentRSSType(response.body().string());
-                return contentType != RSSType.RSS_UNKNOWN;
-            } else return true;
-        } else
-            return false;
-    }
-
-    private Response query(String url, Map<String, String> headers) throws IOException {
-        OkHttpClient okHttpClient = HttpManager.getInstance().getOkHttpClient();
-        HttpManager.getInstance().setCredentials(null);
-
-        Request.Builder builder = new Request.Builder().url(url);
-        for (String header : headers.keySet()) {
-            String value = headers.get(header);
-            builder.addHeader(header, value);
-        }
-
-        Request request = builder.build();
-        return okHttpClient.newCall(request).execute();
-    }
-
-    private RSSType getRSSType(String contentType) {
-        Pattern pattern = Pattern.compile(RSS_CONTENT_TYPE_REGEX);
-        Matcher matcher = pattern.matcher(contentType);
-
-        String header;
-        if (matcher.find())
-            header = matcher.group(0);
-        else
-            header = contentType;
-
-        switch (header) {
-            case LibUtils.RSS_DEFAULT_CONTENT_TYPE:
-                return RSSType.RSS_2;
-            case LibUtils.ATOM_CONTENT_TYPE:
-                return RSSType.RSS_ATOM;
-            case LibUtils.JSON_CONTENT_TYPE:
-                return RSSType.RSS_JSON;
-            case LibUtils.RSS_TEXT_CONTENT_TYPE:
-            case LibUtils.HTML_CONTENT_TYPE:
-            case LibUtils.RSS_APPLICATION_CONTENT_TYPE:
-            default:
-                Log.d(TAG, "bad content type : " + contentType);
-                return RSSType.RSS_UNKNOWN;
-        }
-    }
-
-    /**
-     * Parse input feed
-     *
-     * @param stream   source to parse
-     * @param type     rss type, important to know the feed format
-     * @param response query response
-     * @throws Exception
-     */
-    private RSSQueryResult parseFeed(InputStream stream, RSSType type, Response response) throws Exception {
-        String xml = LibUtils.inputStreamToString(stream);
-        Serializer serializer = new Persister();
-
-        if (type == RSSType.RSS_UNKNOWN) {
-            RSSType contentType = getContentRSSType(xml);
-            if (contentType == RSSType.RSS_UNKNOWN) {
-                return new RSSQueryResult(new UnknownFormatException("Unknown content format"));
-            } else
-                type = contentType;
-        }
-
-        String etag = response.header(LibUtils.ETAG_HEADER);
-        String lastModified = response.header(LibUtils.LAST_MODIFIED_HEADER);
-        AFeed feed = null;
-        RSSQueryResult queryResult = new RSSQueryResult();
-
-        switch (type) {
-            case RSS_2:
-                feed = serializer.read(RSSFeed.class, xml);
-
-                // workaround if the channel does not have any atom:link tag
-                if (((RSSFeed) feed).getChannel().getFeedUrl() == null) {
-                    ((RSSFeed) feed).getChannel().getLinks().add(new RSSLink(null, response.request().url().toString()));
-                }
-                break;
-            case RSS_ATOM:
-                feed = serializer.read(ATOMFeed.class, xml);
-                ((ATOMFeed) feed).setWebsiteUrl(response.request().url().scheme() + "://" + response.request().url().host());
-                ((ATOMFeed) feed).setUrl(response.request().url().toString());
-                break;
-            case RSS_JSON:
-                Moshi moshi = new Moshi.Builder()
-                        .build();
-
-                JsonAdapter<JSONFeed> jsonFeedAdapter = moshi.adapter(JSONFeed.class);
-                feed = jsonFeedAdapter.fromJson(xml);
-                break;
-        }
-
-        queryResult.setFeed(feed);
-        queryResult.setRssType(type);
-
-        feed.setEtag(etag);
-        feed.setLastModified(lastModified);
-
-        return queryResult;
-    }
-
-    private RSSType getContentRSSType(String content) {
-        RSSType type;
-
-        if (Pattern.compile(RSS_2_REGEX).matcher(content).find())
-            type = RSSType.RSS_2;
-        else if (Pattern.compile(ATOM_REGEX).matcher(content).find())
-            type = RSSType.RSS_ATOM;
-        else
-            type = RSSType.RSS_UNKNOWN;
-
-        return type;
-    }
-
-    public enum RSSType {
-        RSS_2,
-        RSS_ATOM,
-        RSS_JSON,
-        RSS_UNKNOWN
-    }
-
-
-}
diff --git a/api/src/main/java/com/readrops/api/localfeed/RSSQueryResult.java b/api/src/main/java/com/readrops/api/localfeed/RSSQueryResult.java
deleted file mode 100644
index 449e162a..00000000
--- a/api/src/main/java/com/readrops/api/localfeed/RSSQueryResult.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package com.readrops.api.localfeed;
-
-public class RSSQueryResult {
-
-    private AFeed feed;
-
-    private RSSQuery.RSSType rssType;
-
-    private Exception exception;
-
-    public RSSQueryResult(Exception exception) {
-        this.exception = exception;
-    }
-
-    public RSSQueryResult() {
-
-    }
-
-    public AFeed getFeed() {
-        return feed;
-    }
-
-    public void setFeed(AFeed feed) {
-        this.feed = feed;
-    }
-
-    public RSSQuery.RSSType getRssType() {
-        return rssType;
-    }
-
-    public void setRssType(RSSQuery.RSSType rssType) {
-        this.rssType = rssType;
-    }
-
-    public void setException(Exception exception) {
-        this.exception = exception;
-    }
-
-    public Exception getException() {
-        return exception;
-    }
-}
diff --git a/api/src/main/java/com/readrops/api/localfeed/XmlAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/XmlAdapter.kt
new file mode 100644
index 00000000..732d495a
--- /dev/null
+++ b/api/src/main/java/com/readrops/api/localfeed/XmlAdapter.kt
@@ -0,0 +1,36 @@
+package com.readrops.api.localfeed
+
+import com.readrops.api.localfeed.atom.ATOMFeedAdapter
+import com.readrops.api.localfeed.atom.ATOMItemsAdapter
+import com.readrops.api.localfeed.rss1.RSS1FeedAdapter
+import com.readrops.api.localfeed.rss1.RSS1ItemsAdapter
+import com.readrops.api.localfeed.rss2.RSS2FeedAdapter
+import com.readrops.api.localfeed.rss2.RSS2ItemsAdapter
+import com.readrops.db.entities.Feed
+import com.readrops.db.entities.Item
+import java.io.InputStream
+
+interface XmlAdapter<T> {
+
+    fun fromXml(inputStream: InputStream): T
+
+    companion object {
+        fun xmlFeedAdapterFactory(type: LocalRSSHelper.RSSType): XmlAdapter<Feed> = when (type) {
+            LocalRSSHelper.RSSType.RSS_1 -> RSS1FeedAdapter()
+            LocalRSSHelper.RSSType.RSS_2 -> RSS2FeedAdapter()
+            LocalRSSHelper.RSSType.ATOM -> ATOMFeedAdapter()
+            else -> throw IllegalArgumentException("Unknown RSS type : $type")
+        }
+
+        fun xmlItemsAdapterFactory(type: LocalRSSHelper.RSSType): XmlAdapter<List<Item>> =
+                when (type) {
+                    LocalRSSHelper.RSSType.RSS_1 -> RSS1ItemsAdapter()
+                    LocalRSSHelper.RSSType.RSS_2 -> RSS2ItemsAdapter()
+                    LocalRSSHelper.RSSType.ATOM -> ATOMItemsAdapter()
+                    else -> throw IllegalArgumentException("Unknown RSS type : $type")
+                }
+
+        const val AUTHORS_MAX = 4
+    }
+}
+
diff --git a/api/src/main/java/com/readrops/api/localfeed/atom/ATOMAuthor.java b/api/src/main/java/com/readrops/api/localfeed/atom/ATOMAuthor.java
deleted file mode 100644
index 302d06c3..00000000
--- a/api/src/main/java/com/readrops/api/localfeed/atom/ATOMAuthor.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package com.readrops.api.localfeed.atom;
-
-import org.simpleframework.xml.Element;
-import org.simpleframework.xml.Root;
-
-@Root(name = "author", strict = false)
-public class ATOMAuthor {
-
-    @Element(required = false)
-    private String name;
-
-    @Element(required = false)
-    private String email;
-
-    public String getName() {
-        return name;
-    }
-
-    public String getEmail() {
-        return email;
-    }
-}
diff --git a/api/src/main/java/com/readrops/api/localfeed/atom/ATOMEntry.java b/api/src/main/java/com/readrops/api/localfeed/atom/ATOMEntry.java
deleted file mode 100644
index 08671f1e..00000000
--- a/api/src/main/java/com/readrops/api/localfeed/atom/ATOMEntry.java
+++ /dev/null
@@ -1,98 +0,0 @@
-package com.readrops.api.localfeed.atom;
-
-import org.simpleframework.xml.Attribute;
-import org.simpleframework.xml.Element;
-import org.simpleframework.xml.ElementList;
-import org.simpleframework.xml.Root;
-
-import java.util.List;
-
-@Root(name = "entry", strict = false)
-public class ATOMEntry {
-
-    @Element(required = false)
-    private String title;
-
-    @ElementList(name = "link", inline = true, required = false)
-    private List<ATOMLink> links;
-
-    @Element(required = false)
-    private String updated;
-
-    @Element(required = false)
-    private String summary;
-
-    @Element(required = false)
-    private String id;
-
-    @Element(required = false)
-    private String content;
-
-    @Attribute(name = "type", required = false)
-    private String contentType;
-
-    public String getTitle() {
-        return title;
-    }
-
-    public void setTitle(String title) {
-        this.title = title;
-    }
-
-    public List<ATOMLink> getLinks() {
-        return links;
-    }
-
-    public void setLinks(List<ATOMLink> links) {
-        this.links = links;
-    }
-
-    public String getUpdated() {
-        return updated;
-    }
-
-    public void setUpdated(String updated) {
-        this.updated = updated;
-    }
-
-    public String getSummary() {
-        return summary;
-    }
-
-    public void setSummary(String summary) {
-        this.summary = summary;
-    }
-
-    public String getId() {
-        return id;
-    }
-
-    public void setId(String id) {
-        this.id = id;
-    }
-
-    public String getContent() {
-        return content;
-    }
-
-    public void setContent(String content) {
-        this.content = content;
-    }
-
-    public String getContentType() {
-        return contentType;
-    }
-
-    public void setContentType(String contentType) {
-        this.contentType = contentType;
-    }
-
-    public String getUrl() {
-        for (ATOMLink link : links) {
-            if (link.getRel() == null || link.getRel().equals("self") || link.getRel().equals("alternate"))
-                return link.getHref();
-        }
-
-        return null;
-    }
-}
diff --git a/api/src/main/java/com/readrops/api/localfeed/atom/ATOMFeed.java b/api/src/main/java/com/readrops/api/localfeed/atom/ATOMFeed.java
deleted file mode 100644
index e83e3c1f..00000000
--- a/api/src/main/java/com/readrops/api/localfeed/atom/ATOMFeed.java
+++ /dev/null
@@ -1,128 +0,0 @@
-package com.readrops.api.localfeed.atom;
-
-import com.readrops.api.localfeed.AFeed;
-
-import org.simpleframework.xml.Element;
-import org.simpleframework.xml.ElementList;
-import org.simpleframework.xml.Root;
-
-import java.util.List;
-
-@Root(name = "feed", strict = false)
-public class ATOMFeed extends AFeed {
-
-    @Element(required = false)
-    private String title;
-
-    @ElementList(name = "link", inline = true, required = false)
-    private List<ATOMLink> links;
-
-    private String url;
-
-    private String websiteUrl;
-
-    @Element(required = false)
-    private String id;
-
-    @Element(required = false)
-    private String subtitle;
-
-    @Element(required = false)
-    private String updated;
-
-    @Element(required = false)
-    private ATOMAuthor author;
-
-    @ElementList(inline = true, required = false)
-    private List<ATOMEntry> entries;
-
-    public String getTitle() {
-        return title;
-    }
-
-    public void setTitle(String title) {
-        this.title = title;
-    }
-
-    public List<ATOMLink> getLinks() {
-        return links;
-    }
-
-    public void setLinks(List<ATOMLink> links) {
-        this.links = links;
-    }
-
-    public String getSubtitle() {
-        return subtitle;
-    }
-
-    public void setSubtitle(String subtitle) {
-        this.subtitle = subtitle;
-    }
-
-    public String getUpdated() {
-        return updated;
-    }
-
-    public void setUpdated(String updated) {
-        this.updated = updated;
-    }
-
-    public ATOMAuthor getAuthor() {
-        return author;
-    }
-
-    public void setAuthor(ATOMAuthor author) {
-        this.author = author;
-    }
-
-    public List<ATOMEntry> getEntries() {
-        return entries;
-    }
-
-    public void setEntries(List<ATOMEntry> entries) {
-        this.entries = entries;
-    }
-
-    public String getId() {
-        return id;
-    }
-
-    public void setId(String id) {
-        this.id = id;
-    }
-
-    public String getUrl() {
-        return url;
-    }
-
-    public void setUrl(String url) {
-        this.url = url;
-    }
-
-    public String getWebsiteUrl() {
-        return websiteUrl;
-    }
-
-    public void setWebsiteUrl(String websiteUrl) {
-        this.websiteUrl = websiteUrl;
-    }
-
-    /*public String getWebSiteUrl() {
-        return id;
-    }
-
-    public String getUrl() {
-        if (links.size() > 0) {
-            if (links.get(0).getRel() != null)
-                return links.get(0).getHref();
-            else {
-                if (links.size() > 1)
-                    return links.get(1).getHref();
-                else
-                    return null;
-            }
-        } else
-            return null;
-    }*/
-}
diff --git a/api/src/main/java/com/readrops/api/localfeed/atom/ATOMFeedAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/atom/ATOMFeedAdapter.kt
new file mode 100644
index 00000000..7497083c
--- /dev/null
+++ b/api/src/main/java/com/readrops/api/localfeed/atom/ATOMFeedAdapter.kt
@@ -0,0 +1,52 @@
+package com.readrops.api.localfeed.atom
+
+import com.gitlab.mvysny.konsumexml.Konsumer
+import com.gitlab.mvysny.konsumexml.Names
+import com.gitlab.mvysny.konsumexml.allChildrenAutoIgnore
+import com.gitlab.mvysny.konsumexml.konsumeXml
+import com.readrops.api.localfeed.XmlAdapter
+import com.readrops.api.utils.exceptions.ParseException
+import com.readrops.api.utils.extensions.nonNullText
+import com.readrops.api.utils.extensions.nullableText
+import com.readrops.db.entities.Feed
+import java.io.InputStream
+
+class ATOMFeedAdapter : XmlAdapter<Feed> {
+
+    override fun fromXml(inputStream: InputStream): Feed {
+        val konsume = inputStream.konsumeXml()
+        val feed = Feed()
+
+        return try {
+            konsume.child("feed") {
+                allChildrenAutoIgnore(names) {
+                    with(feed) {
+                        when (tagName) {
+                            "title" -> name = nonNullText()
+                            "link" -> parseLink(this@allChildrenAutoIgnore, feed)
+                            "subtitle" -> description = nullableText()
+                        }
+                    }
+                }
+            }
+
+            konsume.close()
+            feed
+        } catch (e: Exception) {
+            throw ParseException(e.message)
+        }
+    }
+
+    private fun parseLink(konsume: Konsumer, feed: Feed) = with(konsume) {
+        val rel = attributes.getValueOpt("rel")
+
+        if (rel == "self")
+            feed.url = attributes["href"]
+        else if (rel == "alternate")
+            feed.siteUrl = attributes["href"]
+    }
+
+    companion object {
+        val names = Names.of("title", "link", "subtitle")
+    }
+}
\ No newline at end of file
diff --git a/api/src/main/java/com/readrops/api/localfeed/atom/ATOMItemsAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/atom/ATOMItemsAdapter.kt
new file mode 100644
index 00000000..151bf382
--- /dev/null
+++ b/api/src/main/java/com/readrops/api/localfeed/atom/ATOMItemsAdapter.kt
@@ -0,0 +1,75 @@
+package com.readrops.api.localfeed.atom
+
+import com.gitlab.mvysny.konsumexml.Konsumer
+import com.gitlab.mvysny.konsumexml.Names
+import com.gitlab.mvysny.konsumexml.allChildrenAutoIgnore
+import com.gitlab.mvysny.konsumexml.konsumeXml
+import com.readrops.api.localfeed.XmlAdapter
+import com.readrops.api.utils.*
+import com.readrops.api.utils.exceptions.ParseException
+import com.readrops.api.utils.extensions.nonNullText
+import com.readrops.api.utils.extensions.nullableText
+import com.readrops.api.utils.extensions.nullableTextRecursively
+import com.readrops.db.entities.Item
+import org.joda.time.LocalDateTime
+import java.io.InputStream
+
+class ATOMItemsAdapter : XmlAdapter<List<Item>> {
+
+    override fun fromXml(inputStream: InputStream): List<Item> {
+        val konsumer = inputStream.konsumeXml()
+        val items = arrayListOf<Item>()
+
+        return try {
+            konsumer.child("feed") {
+                allChildrenAutoIgnore("entry") {
+                    val item = Item().apply {
+                        allChildrenAutoIgnore(names) {
+                            when (tagName) {
+                                "title" -> title = nonNullText()
+                                "id" -> guid = nullableText()
+                                "updated" -> pubDate = DateUtils.parse(nullableText())
+                                "link" -> parseLink(this, this@apply)
+                                "author" -> allChildrenAutoIgnore("name") { author = nullableText() }
+                                "summary" -> description = nullableTextRecursively()
+                                "content" -> content = nullableTextRecursively()
+                                else -> skipContents()
+                            }
+                        }
+                    }
+
+                    validateItem(item)
+                    if (item.pubDate == null) item.pubDate = LocalDateTime.now()
+                    if (item.guid == null) item.guid = item.link
+
+                    items += item
+                }
+            }
+
+            konsumer.close()
+            items
+        } catch (e: Exception) {
+            throw ParseException(e.message)
+        }
+    }
+
+    private fun parseLink(konsumer: Konsumer, item: Item) {
+        konsumer.apply {
+            if (attributes.getValueOpt("rel") == null ||
+                    attributes["rel"] == "alternate")
+                item.link = attributes.getValueOpt("href")
+        }
+
+    }
+
+    private fun validateItem(item: Item) {
+        when {
+            item.title == null -> throw ParseException("Item title is required")
+            item.link == null -> throw ParseException("Item link is required")
+        }
+    }
+
+    companion object {
+        val names = Names.of("title", "id", "updated", "link", "author", "summary", "content")
+    }
+}
\ No newline at end of file
diff --git a/api/src/main/java/com/readrops/api/localfeed/atom/ATOMLink.java b/api/src/main/java/com/readrops/api/localfeed/atom/ATOMLink.java
deleted file mode 100644
index 024a137f..00000000
--- a/api/src/main/java/com/readrops/api/localfeed/atom/ATOMLink.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package com.readrops.api.localfeed.atom;
-
-import org.simpleframework.xml.Attribute;
-import org.simpleframework.xml.Root;
-
-@Root(name = "link", strict = false)
-public class ATOMLink {
-
-    @Attribute(name = "href", required = false)
-    private String href;
-
-    @Attribute(name = "rel", required = false)
-    private String rel;
-
-    public String getHref() {
-        return href;
-    }
-
-    public void setHref(String href) {
-        this.href = href;
-    }
-
-    public String getRel() {
-        return rel;
-    }
-
-    public void setRel(String rel) {
-        this.rel = rel;
-    }
-}
diff --git a/api/src/main/java/com/readrops/api/localfeed/json/JSONAuthor.kt b/api/src/main/java/com/readrops/api/localfeed/json/JSONAuthor.kt
deleted file mode 100644
index 125e69a1..00000000
--- a/api/src/main/java/com/readrops/api/localfeed/json/JSONAuthor.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.readrops.api.localfeed.json
-
-import com.squareup.moshi.Json
-import com.squareup.moshi.JsonClass
-
-@JsonClass(generateAdapter = true)
-data class JSONAuthor(val name: String,
-                      val url: String,
-                      @Json(name = "avatar") val avatarUrl: String?)
\ No newline at end of file
diff --git a/api/src/main/java/com/readrops/api/localfeed/json/JSONFeed.kt b/api/src/main/java/com/readrops/api/localfeed/json/JSONFeed.kt
deleted file mode 100644
index 7d83d834..00000000
--- a/api/src/main/java/com/readrops/api/localfeed/json/JSONFeed.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.readrops.api.localfeed.json
-
-import com.readrops.api.localfeed.AFeed
-import com.squareup.moshi.Json
-import com.squareup.moshi.JsonClass
-
-@JsonClass(generateAdapter = true)
-data class JSONFeed(val version: String,
-                    val title: String,
-                    @Json(name = "home_page_url") val homePageUrl: String?,
-                    @Json(name = "feed_url") val feedUrl: String?,
-                    val description: String?,
-                    @Json(name = "icon") val iconUrl: String?,
-                    @Json(name = "favicon") val faviconUrl: String?,
-                    val items: List<JSONItem>) : AFeed()
\ No newline at end of file
diff --git a/api/src/main/java/com/readrops/api/localfeed/json/JSONFeedAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/json/JSONFeedAdapter.kt
new file mode 100644
index 00000000..f95772eb
--- /dev/null
+++ b/api/src/main/java/com/readrops/api/localfeed/json/JSONFeedAdapter.kt
@@ -0,0 +1,43 @@
+package com.readrops.api.localfeed.json
+
+import com.readrops.api.utils.exceptions.ParseException
+import com.readrops.api.utils.extensions.nextNonEmptyString
+import com.readrops.api.utils.extensions.nextNullableString
+import com.readrops.db.entities.Feed
+import com.squareup.moshi.FromJson
+import com.squareup.moshi.JsonReader
+import com.squareup.moshi.ToJson
+
+class JSONFeedAdapter {
+
+    @ToJson
+    fun toJson(feed: Feed) = ""
+
+    @FromJson
+    fun fromJson(reader: JsonReader): Feed = try {
+        val feed = Feed()
+        reader.beginObject()
+
+        while (reader.hasNext()) {
+            with(feed) {
+                when (reader.selectName(names)) {
+                    0 -> name = reader.nextNonEmptyString()
+                    1 -> siteUrl = reader.nextNullableString()
+                    2 -> url = reader.nextNullableString()
+                    3 -> description = reader.nextNullableString()
+                    else -> reader.skipValue()
+                }
+            }
+        }
+
+        reader.endObject()
+        feed
+    } catch (e: Exception) {
+        throw ParseException(e.message)
+    }
+
+    companion object {
+        val names: JsonReader.Options = JsonReader.Options.of("title", "home_page_url",
+                "feed_url", "description")
+    }
+}
\ No newline at end of file
diff --git a/api/src/main/java/com/readrops/api/localfeed/json/JSONItem.kt b/api/src/main/java/com/readrops/api/localfeed/json/JSONItem.kt
deleted file mode 100644
index 94e50b51..00000000
--- a/api/src/main/java/com/readrops/api/localfeed/json/JSONItem.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package com.readrops.api.localfeed.json
-
-import com.squareup.moshi.Json
-import com.squareup.moshi.JsonClass
-
-@JsonClass(generateAdapter = true)
-data class JSONItem(val id: String,
-                    val title: String?,
-                    val summary: String?,
-                    @Json(name = "content_text") val contentText: String?,
-                    @Json(name = "content_html") val contentHtml: String?,
-                    val url: String?,
-                    @Json(name = "image") val imageUrl: String?,
-                    @Json(name = "date_published") val pubDate: String,
-                    @Json(name = "date_modified") val modDate: String?,
-                    val author: JSONAuthor?) {
-
-    fun getContent(): String? {
-        return contentHtml ?: contentText
-    }
-}
\ No newline at end of file
diff --git a/api/src/main/java/com/readrops/api/localfeed/json/JSONItemsAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/json/JSONItemsAdapter.kt
new file mode 100644
index 00000000..e4ca8f2f
--- /dev/null
+++ b/api/src/main/java/com/readrops/api/localfeed/json/JSONItemsAdapter.kt
@@ -0,0 +1,114 @@
+package com.readrops.api.localfeed.json
+
+import com.readrops.api.localfeed.XmlAdapter.Companion.AUTHORS_MAX
+import com.readrops.api.utils.DateUtils
+import com.readrops.api.utils.exceptions.ParseException
+import com.readrops.api.utils.extensions.nextNonEmptyString
+import com.readrops.api.utils.extensions.nextNullableString
+import com.readrops.db.entities.Item
+import com.squareup.moshi.JsonAdapter
+import com.squareup.moshi.JsonReader
+import com.squareup.moshi.JsonWriter
+import org.joda.time.LocalDateTime
+
+class JSONItemsAdapter : JsonAdapter<List<Item>>() {
+
+    override fun toJson(writer: JsonWriter, value: List<Item>?) {
+        // not useful
+    }
+
+    override fun fromJson(reader: JsonReader): List<Item> = try {
+        val items = arrayListOf<Item>()
+        reader.beginObject()
+
+        while (reader.hasNext()) {
+            when (reader.nextName()) {
+                "items" -> parseItems(reader, items)
+                else -> reader.skipValue()
+            }
+        }
+
+        items
+    } catch (e: Exception) {
+        throw ParseException(e.message)
+    }
+
+    private fun parseItems(reader: JsonReader, items: MutableList<Item>) = with(reader) {
+        beginArray()
+
+        while (hasNext()) {
+            beginObject()
+            val item = Item()
+
+            var contentText: String? = null
+            var contentHtml: String? = null
+
+            while (hasNext()) {
+                with(item) {
+                    when (selectName(names)) {
+                        0 -> guid = nextNonEmptyString()
+                        1 -> link = nextNonEmptyString()
+                        2 -> title = nextNonEmptyString()
+                        3 -> contentHtml = nextNullableString()
+                        4 -> contentText = nextNullableString()
+                        5 -> description = nextNullableString()
+                        6 -> imageLink = nextNullableString()
+                        7 -> pubDate = DateUtils.parse(nextNullableString())
+                        8 -> author = parseAuthor(reader) // jsonfeed 1.0
+                        9 -> author = parseAuthors(reader) // jsonfeed 1.1
+                        else -> skipValue()
+                    }
+                }
+            }
+
+            validateItem(item)
+            item.content = if (contentHtml != null) contentHtml else contentText
+            if (item.pubDate == null) item.pubDate = LocalDateTime.now()
+
+            endObject()
+            items += item
+        }
+
+        endArray()
+    }
+
+    private fun parseAuthor(reader: JsonReader): String? {
+        var author: String? = null
+        reader.beginObject()
+
+        while (reader.hasNext()) {
+            when (reader.nextName()) {
+                "name" -> author = reader.nextNullableString()
+                else -> reader.skipValue()
+            }
+        }
+
+        reader.endObject()
+        return author
+    }
+
+    private fun parseAuthors(reader: JsonReader): String? {
+        val authors = arrayListOf<String?>()
+        reader.beginArray()
+
+        while (reader.hasNext()) {
+            authors += parseAuthor(reader)
+        }
+
+        reader.endArray()
+
+        return if (authors.filterNotNull().isNotEmpty())
+            authors.filterNotNull().joinToString(limit = AUTHORS_MAX) else null
+    }
+
+    private fun validateItem(item: Item): Boolean = when {
+        item.title == null -> throw ParseException("Item title is required")
+        item.link == null -> throw ParseException("Item link is required")
+        else -> true
+    }
+
+    companion object {
+        val names: JsonReader.Options = JsonReader.Options.of("id", "url", "title",
+                "content_html", "content_text", "summary", "image", "date_published", "author", "authors")
+    }
+}
\ No newline at end of file
diff --git a/api/src/main/java/com/readrops/api/localfeed/rss/RSSChannel.java b/api/src/main/java/com/readrops/api/localfeed/rss/RSSChannel.java
deleted file mode 100644
index 32c88be3..00000000
--- a/api/src/main/java/com/readrops/api/localfeed/rss/RSSChannel.java
+++ /dev/null
@@ -1,87 +0,0 @@
-package com.readrops.api.localfeed.rss;
-
-import org.simpleframework.xml.Element;
-import org.simpleframework.xml.ElementList;
-import org.simpleframework.xml.Root;
-
-import java.util.List;
-
-@Root(name = "channel", strict = false)
-public class RSSChannel {
-
-    @Element(name = "title", required = false)
-    private String title;
-
-    @Element(name = "description", required = false)
-    private String description;
-
-    // workaround to get the two links (feed and regular)
-    @ElementList(name = "link", inline = true, required = false)
-    private List<RSSLink> links;
-
-    @Element(name = "lastBuildDate", required = false)
-    private String lastUpdated;
-
-    @ElementList(inline = true, required = false)
-    private List<RSSItem> items;
-
-    public String getTitle() {
-        return title;
-    }
-
-    public void setTitle(String title) {
-        this.title = title;
-    }
-
-    public String getDescription() {
-        return description;
-    }
-
-    public void setDescription(String description) {
-        this.description = description;
-    }
-
-    public List<RSSLink> getLinks() {
-        return links;
-    }
-
-    public void setLinks(List<RSSLink> links) {
-        this.links = links;
-    }
-
-    public List<RSSItem> getItems() {
-        return items;
-    }
-
-    public void setItems(List<RSSItem> items) {
-        this.items = items;
-    }
-
-    public String getLastUpdated() {
-        return lastUpdated;
-    }
-
-    public void setLastUpdated(String lastUpdated) {
-        this.lastUpdated = lastUpdated;
-    }
-    
-    public String getFeedUrl() {
-        if (links.size() > 1) {
-            if (links.get(0).getHref() != null)
-                return links.get(0).getHref();
-            else
-                return links.get(1).getHref();
-        } else
-            return null;
-    }
-
-    public String getUrl() {
-        if (links.size() > 1) {
-            if (links.get(1).getText() != null)
-                return links.get(1).getText();
-            else
-                return links.get(0).getText();
-        } else
-            return null;
-    }
-}
diff --git a/api/src/main/java/com/readrops/api/localfeed/rss/RSSEnclosure.java b/api/src/main/java/com/readrops/api/localfeed/rss/RSSEnclosure.java
deleted file mode 100644
index 45a25dbd..00000000
--- a/api/src/main/java/com/readrops/api/localfeed/rss/RSSEnclosure.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package com.readrops.api.localfeed.rss;
-
-import org.simpleframework.xml.Attribute;
-import org.simpleframework.xml.Root;
-
-@Root(name = "enclosure", strict = false)
-public class RSSEnclosure {
-
-    @Attribute(required = false)
-    private String type;
-
-    @Attribute(required = false)
-    private String url;
-
-    public String getType() {
-        return type;
-    }
-
-    public void setType(String type) {
-        this.type = type;
-    }
-
-    public String getUrl() {
-        return url;
-    }
-
-    public void setUrl(String url) {
-        this.url = url;
-    }
-}
diff --git a/api/src/main/java/com/readrops/api/localfeed/rss/RSSFeed.java b/api/src/main/java/com/readrops/api/localfeed/rss/RSSFeed.java
deleted file mode 100644
index e52ab253..00000000
--- a/api/src/main/java/com/readrops/api/localfeed/rss/RSSFeed.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package com.readrops.api.localfeed.rss;
-
-import com.readrops.api.localfeed.AFeed;
-
-import org.simpleframework.xml.Element;
-import org.simpleframework.xml.Root;
-
-@Root(name = "rss", strict = false)
-public class RSSFeed extends AFeed {
-
-    @Element(name = "channel", required = false)
-    private RSSChannel channel;
-
-    public RSSChannel getChannel() {
-        return channel;
-    }
-
-    public void setChannel(RSSChannel channel) {
-        this.channel = channel;
-    }
-}
diff --git a/api/src/main/java/com/readrops/api/localfeed/rss/RSSItem.java b/api/src/main/java/com/readrops/api/localfeed/rss/RSSItem.java
deleted file mode 100644
index 7cd15406..00000000
--- a/api/src/main/java/com/readrops/api/localfeed/rss/RSSItem.java
+++ /dev/null
@@ -1,154 +0,0 @@
-package com.readrops.api.localfeed.rss;
-
-import org.simpleframework.xml.Element;
-import org.simpleframework.xml.ElementList;
-import org.simpleframework.xml.Namespace;
-import org.simpleframework.xml.Root;
-
-import java.util.List;
-
-@Root(name = "item", strict = false)
-public class RSSItem {
-
-    @Element
-    private String title;
-
-    @Element(name = "link", required = false)
-    private String link;
-    
-    @Element(name = "imageLink", required = false)
-    private String imageLink;
-
-    @ElementList(name = "content", inline = true, required = false)
-    @Namespace(prefix = "media")
-    private List<RSSMediaContent> mediaContents;
-
-    @ElementList(name = "enclosure", inline = true, required = false)
-    private List<RSSEnclosure> enclosures;
-
-    @ElementList(name = "creator", inline = true, required = false)
-    @Namespace(prefix = "dc", reference = "http://purl.org/dc/elements/1.1/")
-    private List<String> creator;
-
-    @Element(required = false)
-    private String author;
-
-    @Element(name = "pubDate", required = false)
-    private String pubDate;
-
-    @Element(name = "date", required = false)
-    @Namespace(prefix = "dc")
-    private String date;
-
-    @Element(name = "description", required = false)
-    private String description;
-
-    @Element(name = "encoded", required = false)
-    @Namespace(prefix = "content")
-    private String content;
-
-    @Element(required = false)
-    private String guid;
-
-    public String getTitle() {
-        return title;
-    }
-
-    public void setTitle(String title) {
-        this.title = title;
-    }
-
-    public String getDescription() {
-        return description;
-    }
-
-    public void setDescription(String description) {
-        this.description = description;
-    }
-
-    public String getLink() {
-        return link;
-    }
-
-    public void setLink(String link) {
-        this.link = link;
-    }
-
-    public String getImageLink() {
-        return imageLink;
-    }
-
-    public void setImageLink(String imageLink) {
-        this.imageLink = imageLink;
-    }
-
-    public List<String> getCreator() {
-        return creator;
-    }
-
-    public void setCreator(List<String> creator) {
-        this.creator = creator;
-    }
-
-    public String getPubDate() {
-        return pubDate;
-    }
-
-    public void setPubDate(String pubDate) {
-        this.pubDate = pubDate;
-    }
-
-    public String getContent() {
-        return content;
-    }
-
-    public void setContent(String content) {
-        this.content = content;
-    }
-
-    public String getGuid() {
-        return guid;
-    }
-
-    public void setGuid(String guid) {
-        this.guid = guid;
-    }
-
-    public List<RSSMediaContent> getMediaContents() {
-        return mediaContents;
-    }
-
-    public void setMediaContents(List<RSSMediaContent> mediaContents) {
-        this.mediaContents = mediaContents;
-    }
-
-    public List<RSSEnclosure> getEnclosures() {
-        return enclosures;
-    }
-
-    public void setEnclosures(List<RSSEnclosure> enclosures) {
-        this.enclosures = enclosures;
-    }
-
-    public String getDate() {
-        if (pubDate != null)
-            return pubDate;
-        else
-            return date;
-    }
-
-    public void setDate(String date) {
-        this.date = date;
-    }
-
-    public String getAuthor() {
-        if (creator != null && !creator.isEmpty())
-            return creator.get(0);
-        else
-            return author;
-    }
-
-    public void setAuthor(String author) {
-        this.author = author;
-    }
-}
diff --git a/api/src/main/java/com/readrops/api/localfeed/rss/RSSLink.java b/api/src/main/java/com/readrops/api/localfeed/rss/RSSLink.java
deleted file mode 100644
index e6511c33..00000000
--- a/api/src/main/java/com/readrops/api/localfeed/rss/RSSLink.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package com.readrops.api.localfeed.rss;
-
-import org.simpleframework.xml.Attribute;
-import org.simpleframework.xml.Root;
-import org.simpleframework.xml.Text;
-
-@Root(name = "link", strict = false)
-public class RSSLink {
-
-    @Text(required = false)
-    private String text;
-
-    @Attribute(name = "href", required = false)
-    private String href;
-
-    public RSSLink() {
-
-    }
-
-    public RSSLink(String text, String href) {
-        this.text = text;
-        this.href = href;
-    }
-
-    public String getHref() {
-        return href;
-    }
-
-    public void setHref(String href) {
-        this.href = href;
-    }
-
-    public String getText() {
-        return text;
-    }
-
-    public void setText(String text) {
-        this.text = text;
-    }
-}
diff --git a/api/src/main/java/com/readrops/api/localfeed/rss/RSSMediaContent.java b/api/src/main/java/com/readrops/api/localfeed/rss/RSSMediaContent.java
deleted file mode 100644
index 4e7c38e1..00000000
--- a/api/src/main/java/com/readrops/api/localfeed/rss/RSSMediaContent.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package com.readrops.api.localfeed.rss;
-
-import org.simpleframework.xml.Attribute;
-import org.simpleframework.xml.Root;
-
-@Root(name = "content", strict = false)
-public class RSSMediaContent {
-
-    @Attribute(required = false)
-    private String url;
-
-    @Attribute(required = false)
-    private String medium;
-
-    public String getUrl() {
-        return url;
-    }
-
-    public void setUrl(String url) {
-        this.url = url;
-    }
-
-    public String getMedium() {
-        return medium;
-    }
-
-    public void setMedium(String medium) {
-        this.medium = medium;
-    }
-}
diff --git a/api/src/main/java/com/readrops/api/localfeed/rss1/RSS1FeedAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/rss1/RSS1FeedAdapter.kt
new file mode 100644
index 00000000..cbbdbe2a
--- /dev/null
+++ b/api/src/main/java/com/readrops/api/localfeed/rss1/RSS1FeedAdapter.kt
@@ -0,0 +1,48 @@
+package com.readrops.api.localfeed.rss1
+
+import com.gitlab.mvysny.konsumexml.Names
+import com.gitlab.mvysny.konsumexml.allChildrenAutoIgnore
+import com.gitlab.mvysny.konsumexml.konsumeXml
+import com.readrops.api.localfeed.XmlAdapter
+import com.readrops.api.utils.exceptions.ParseException
+import com.readrops.api.utils.extensions.nonNullText
+import com.readrops.api.utils.extensions.nullableText
+import com.readrops.db.entities.Feed
+import java.io.InputStream
+
+class RSS1FeedAdapter : XmlAdapter<Feed> {
+
+    override fun fromXml(inputStream: InputStream): Feed {
+        val konsume = inputStream.konsumeXml()
+        val feed = Feed()
+
+        return try {
+            konsume.child("RDF") {
+                allChildrenAutoIgnore("channel") {
+                    feed.url = attributes.getValueOpt("about",
+                            namespace = "http://www.w3.org/1999/02/22-rdf-syntax-ns#")
+
+                    allChildrenAutoIgnore(names) {
+                        with(feed) {
+                            when (tagName) {
+                                "title" -> name = nonNullText()
+                                "link" -> siteUrl = nonNullText()
+                                "description" -> description = nullableText()
+                            }
+                        }
+                    }
+                }
+            }
+
+            konsume.close()
+            feed
+        } catch (e: Exception) {
+            throw ParseException(e.message)
+        }
+
+    }
+
+    companion object {
+        val names = Names.of("title", "link", "description")
+    }
+}
\ No newline at end of file
diff --git a/api/src/main/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapter.kt
new file mode 100644
index 00000000..416de501
--- /dev/null
+++ b/api/src/main/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapter.kt
@@ -0,0 +1,72 @@
+package com.readrops.api.localfeed.rss1
+
+import com.gitlab.mvysny.konsumexml.Names
+import com.gitlab.mvysny.konsumexml.allChildrenAutoIgnore
+import com.gitlab.mvysny.konsumexml.konsumeXml
+import com.readrops.api.localfeed.XmlAdapter
+import com.readrops.api.localfeed.XmlAdapter.Companion.AUTHORS_MAX
+import com.readrops.api.utils.*
+import com.readrops.api.utils.exceptions.ParseException
+import com.readrops.api.utils.extensions.nonNullText
+import com.readrops.api.utils.extensions.nullableText
+import com.readrops.api.utils.extensions.nullableTextRecursively
+import com.readrops.db.entities.Item
+import org.joda.time.LocalDateTime
+import java.io.InputStream
+
+class RSS1ItemsAdapter : XmlAdapter<List<Item>> {
+
+    override fun fromXml(inputStream: InputStream): List<Item> {
+        val konsumer = inputStream.konsumeXml()
+        val items = arrayListOf<Item>()
+
+        return try {
+            konsumer.child("RDF") {
+                allChildrenAutoIgnore("item") {
+                    val authors = arrayListOf<String?>()
+                    val about = attributes.getValueOpt("about",
+                            namespace = "http://www.w3.org/1999/02/22-rdf-syntax-ns#")
+
+                    val item = Item().apply {
+                        allChildrenAutoIgnore(names) {
+                            when (tagName) {
+                                "title" -> title = nonNullText()
+                                "link" -> link = nullableText()
+                                "dc:date" -> pubDate = DateUtils.parse(nullableText())
+                                "dc:creator" -> authors += nullableText()
+                                "description" -> description = nullableTextRecursively()
+                                "content:encoded" -> content = nullableTextRecursively()
+                                else -> skipContents()
+                            }
+                        }
+                    }
+
+                    if (item.pubDate == null) item.pubDate = LocalDateTime.now()
+                    if (item.link == null) item.link = about
+                            ?: throw ParseException("RSS1 link or about element is required")
+                    item.guid = item.link
+
+                    if (authors.filterNotNull().isNotEmpty()) item.author = authors.filterNotNull()
+                            .joinToString(limit = AUTHORS_MAX)
+
+                    validateItem(item)
+
+                    items += item
+                }
+            }
+
+            konsumer.close()
+            items
+        } catch (e: Exception) {
+            throw ParseException(e.message)
+        }
+    }
+
+    private fun validateItem(item: Item) {
+        if (item.title == null) throw ParseException("Item title is required")
+    }
+
+    companion object {
+        val names = Names.of("title", "description", "date", "link", "creator", "encoded")
+    }
+}
\ No newline at end of file
diff --git a/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2FeedAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2FeedAdapter.kt
new file mode 100644
index 00000000..e28c1478
--- /dev/null
+++ b/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2FeedAdapter.kt
@@ -0,0 +1,50 @@
+package com.readrops.api.localfeed.rss2
+
+import com.gitlab.mvysny.konsumexml.Names
+import com.gitlab.mvysny.konsumexml.allChildrenAutoIgnore
+import com.gitlab.mvysny.konsumexml.konsumeXml
+import com.readrops.api.localfeed.XmlAdapter
+import com.readrops.api.utils.exceptions.ParseException
+import com.readrops.api.utils.extensions.nonNullText
+import com.readrops.api.utils.extensions.nullableText
+import com.readrops.db.entities.Feed
+import org.jsoup.Jsoup
+import java.io.InputStream
+
+class RSS2FeedAdapter : XmlAdapter<Feed> {
+
+    override fun fromXml(inputStream: InputStream): Feed {
+        val konsume = inputStream.konsumeXml()
+        val feed = Feed()
+
+        return try {
+            konsume.child("rss") {
+                child("channel") {
+                    allChildrenAutoIgnore(names) {
+                        with(feed) {
+                            when (tagName) {
+                                "title" -> name = Jsoup.parse(nonNullText()).text()
+                                "description" -> description = nullableText()
+                                "link" -> siteUrl = nullableText()
+                                "atom:link" -> {
+                                    if (attributes.getValueOpt("rel") == "self")
+                                        url = attributes.getValueOpt("href")
+                                }
+                                else -> skipContents()
+                            }
+                        }
+                    }
+                }
+            }
+
+            konsume.close()
+            feed
+        } catch (e: Exception) {
+            throw ParseException(e.message)
+        }
+    }
+
+    companion object {
+        val names = Names.of("title", "description", "link")
+    }
+}
\ No newline at end of file
diff --git a/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapter.kt b/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapter.kt
new file mode 100644
index 00000000..1f73875a
--- /dev/null
+++ b/api/src/main/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapter.kt
@@ -0,0 +1,111 @@
+package com.readrops.api.localfeed.rss2
+
+import com.gitlab.mvysny.konsumexml.*
+import com.readrops.api.localfeed.XmlAdapter
+import com.readrops.api.localfeed.XmlAdapter.Companion.AUTHORS_MAX
+import com.readrops.api.utils.*
+import com.readrops.api.utils.exceptions.ParseException
+import com.readrops.api.utils.extensions.nonNullText
+import com.readrops.api.utils.extensions.nullableText
+import com.readrops.api.utils.extensions.nullableTextRecursively
+import com.readrops.db.entities.Item
+import org.joda.time.LocalDateTime
+import java.io.InputStream
+
+class RSS2ItemsAdapter : XmlAdapter<List<Item>> {
+
+    override fun fromXml(inputStream: InputStream): List<Item> {
+        val konsumer = inputStream.konsumeXml()
+        val items = mutableListOf<Item>()
+
+        return try {
+            konsumer.child("rss") {
+                child("channel") {
+                    allChildrenAutoIgnore("item") {
+                        val creators = arrayListOf<String?>()
+
+                        val item = Item().apply {
+                            allChildrenAutoIgnore(names) {
+                                when (tagName) {
+                                    "title" -> title = ApiUtils.cleanText(nonNullText())
+                                    "link" -> link = nonNullText()
+                                    "author" -> author = nullableText()
+                                    "dc:creator" -> creators += nullableText()
+                                    "pubDate" -> pubDate = DateUtils.parse(nullableText())
+                                    "dc:date" -> pubDate = DateUtils.parse(nullableText())
+                                    "guid" -> guid = nullableText()
+                                    "description" -> description = nullableTextRecursively()
+                                    "content:encoded" -> content = nullableTextRecursively()
+                                    "enclosure" -> parseEnclosure(this, item = this@apply)
+                                    "media:content" -> parseMediaContent(this, item = this@apply)
+                                    "media:group" -> parseMediaGroup(this, item = this@apply)
+                                    else -> skipContents() // for example media:description
+                                }
+                            }
+                        }
+
+                        finalizeItem(item, creators)
+
+                        items += item
+                    }
+                }
+            }
+
+            konsumer.close()
+            items
+        } catch (e: KonsumerException) {
+            throw ParseException(e.message)
+        }
+    }
+
+    private fun parseEnclosure(konsumer: Konsumer, item: Item) = with(konsumer) {
+        if (attributes.getValueOpt("type") != null
+                && ApiUtils.isMimeImage(attributes["type"]) && item.imageLink == null)
+            item.imageLink = attributes.getValueOpt("url")
+    }
+
+    private fun isMediumImage(konsumer: Konsumer) = with(konsumer) {
+        attributes.getValueOpt("medium") != null && ApiUtils.isMimeImage(attributes["medium"])
+    }
+
+    private fun isTypeImage(konsumer: Konsumer) = with(konsumer) {
+        attributes.getValueOpt("type") != null && ApiUtils.isMimeImage(attributes["type"])
+    }
+
+    private fun parseMediaContent(konsumer: Konsumer, item: Item) {
+        if ((isMediumImage(konsumer) || isTypeImage(konsumer)) && item.imageLink == null)
+            item.imageLink = konsumer.attributes.getValueOpt("url")
+
+        konsumer.skipContents() // ignore media content sub elements
+    }
+
+    private fun parseMediaGroup(konsumer: Konsumer, item: Item) {
+        konsumer.allChildrenAutoIgnore("content") {
+            when (tagName) {
+                "media:content" -> parseMediaContent(this, item)
+                else -> skipContents()
+            }
+        }
+    }
+
+    private fun finalizeItem(item: Item, creators: List<String?>) = with(item) {
+        validateItem(this)
+
+        if (pubDate == null) pubDate = LocalDateTime.now()
+        if (guid == null) guid = link
+        if (author == null && creators.filterNotNull().isNotEmpty())
+            author = creators.filterNotNull().joinToString(limit = AUTHORS_MAX)
+    }
+
+    private fun validateItem(item: Item) {
+        when {
+            item.title == null -> throw ParseException("Item title is required")
+            item.link == null -> throw ParseException("Item link is required")
+        }
+    }
+
+    companion object {
+        val names = Names.of("title", "link", "author", "creator", "pubDate", "date",
+                "guid", "description", "encoded", "enclosure", "content", "group")
+    }
+}
\ No newline at end of file
diff --git a/api/src/main/java/com/readrops/api/opml/OPMLHelper.kt b/api/src/main/java/com/readrops/api/opml/OPMLHelper.kt
new file mode 100644
index 00000000..60d6f750
--- /dev/null
+++ b/api/src/main/java/com/readrops/api/opml/OPMLHelper.kt
@@ -0,0 +1,27 @@
+package com.readrops.api.opml
+
+import android.app.Activity
+import android.content.Intent
+import androidx.fragment.app.Fragment
+
+object OPMLHelper {
+
+    const val OPEN_OPML_FILE_REQUEST = 1
+
+    @JvmStatic
+    fun openFileIntent(activity: Activity) =
+            activity.startActivityForResult(createIntent(), OPEN_OPML_FILE_REQUEST)
+
+    @JvmStatic
+    fun openFileIntent(fragment: Fragment) =
+            fragment.startActivityForResult(createIntent(), OPEN_OPML_FILE_REQUEST)
+
+
+    private fun createIntent(): Intent {
+        return Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
+            addCategory(Intent.CATEGORY_OPENABLE)
+            type = "*/*"
+            putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("application/*", "text/*"))
+        }
+    }
+}
\ No newline at end of file
diff --git a/api/src/main/java/com/readrops/api/opml/OPMLParser.kt b/api/src/main/java/com/readrops/api/opml/OPMLParser.kt
index 92d7b883..8b55bba0 100644
--- a/api/src/main/java/com/readrops/api/opml/OPMLParser.kt
+++ b/api/src/main/java/com/readrops/api/opml/OPMLParser.kt
@@ -2,35 +2,52 @@ package com.readrops.api.opml
 
 import android.content.Context
 import android.net.Uri
+import android.util.Log
 import com.readrops.api.opml.model.Body
 import com.readrops.api.opml.model.Head
 import com.readrops.api.opml.model.OPML
 import com.readrops.api.opml.model.Outline
-import com.readrops.api.utils.LibUtils
+import com.readrops.api.utils.exceptions.ParseException
 import com.readrops.db.entities.Feed
 import com.readrops.db.entities.Folder
 import io.reactivex.Completable
 import io.reactivex.Single
+import io.reactivex.SingleOnSubscribe
 import org.simpleframework.xml.Serializer
 import org.simpleframework.xml.core.Persister
+import java.io.InputStream
 import java.io.OutputStream
 
 object OPMLParser {
 
+    val TAG = OPMLParser.javaClass.simpleName
+
     @JvmStatic
-    fun read(uri: Uri, context: Context): Single<Map<Folder, List<Feed>>> {
+    fun read(uri: Uri, context: Context): Single<Map<Folder?, List<Feed>>> {
+        return Single.create(SingleOnSubscribe<InputStream> {
+            val stream = context.contentResolver.openInputStream(uri)
+            it.onSuccess(stream!!)
+        }).flatMap { stream -> read(stream) }
+    }
+
+    @JvmStatic
+    fun read(stream: InputStream): Single<Map<Folder?, List<Feed>>> {
         return Single.create { emitter ->
-            val fileString = LibUtils.fileToString(uri, context)
-            val serializer: Serializer = Persister()
+            try {
+                val serializer: Serializer = Persister()
 
-            val opml: OPML = serializer.read(OPML::class.java, fileString)
+                val opml: OPML = serializer.read(OPML::class.java, stream)
 
-            emitter.onSuccess(opmltoFoldersAndFeeds(opml))
+                emitter.onSuccess(opmlToFoldersAndFeeds(opml))
+            } catch (e: Exception) {
+                Log.d(TAG, e.message, e)
+                emitter.onError(e)
+            }
         }
     }
 
     @JvmStatic
-    fun write(foldersAndFeeds: Map<Folder, List<Feed>>, outputStream: OutputStream): Completable {
+    fun write(foldersAndFeeds: Map<Folder?, List<Feed>>, outputStream: OutputStream): Completable {
         return Completable.create { emitter ->
             val serializer: Serializer = Persister()
             serializer.write(foldersAndFeedsToOPML(foldersAndFeeds), outputStream)
@@ -39,44 +56,83 @@ object OPMLParser {
         }
     }
 
-    private fun opmltoFoldersAndFeeds(opml: OPML): Map<Folder, List<Feed>> {
-        val foldersAndFeeds: MutableMap<Folder, List<Feed>> = HashMap()
+    private fun opmlToFoldersAndFeeds(opml: OPML): Map<Folder?, List<Feed>> {
+        if (opml.version != "2.0")
+            throw ParseException("Only 2.0 OPML specification is supported")
+
+        val foldersAndFeeds: MutableMap<Folder?, List<Feed>> = HashMap()
         val body = opml.body!!
 
         body.outlines?.forEach { outline ->
-            val folder = Folder(outline.title)
+            val outlineParsing = parseOutline(outline)
+            associateOrphanFeedsToFolder(foldersAndFeeds, outlineParsing, null)
 
-            val feeds = arrayListOf<Feed>()
-            outline.outlines?.forEach { feedOutline ->
-                val feed = Feed().apply {
-                    name = feedOutline.title
-                    url = feedOutline.xmlUrl
-                    siteUrl = feedOutline.htmlUrl
-                }
-
-                feeds.add(feed)
-            }
-
-            foldersAndFeeds[folder] = feeds
+            foldersAndFeeds.putAll(outlineParsing)
         }
 
         return foldersAndFeeds
     }
 
-    private fun foldersAndFeedsToOPML(foldersAndFeeds: Map<Folder, List<Feed>>): OPML {
-        val outlines = arrayListOf<Outline>()
-        for (folderAndFeeds in foldersAndFeeds) {
-            val outline = Outline(folderAndFeeds.key.name)
+    /**
+     * Parse outline and its children recursively
+     * @param outline node to parse
+     */
+    private fun parseOutline(outline: Outline): MutableMap<Folder?, List<Feed>> {
+        val foldersAndFeeds: MutableMap<Folder?, List<Feed>> = HashMap()
 
-            val feedOutlines = arrayListOf<Outline>()
-            folderAndFeeds.value.forEach { feed ->
-                val feedOutline = Outline(feed.name, feed.url, feed.siteUrl)
+        // The outline is a folder/category
+        if ((outline.outlines != null && !outline.outlines?.isEmpty()!!) || outline.xmlUrl.isNullOrEmpty()) {
+            // if the outline doesn't have text or title value but contains sub outlines,
+            // those sub outlines will be considered as not belonging to any folder and join the others at the top level of the hierarchy
+            val folder = if (outline.name != null) Folder(outline.name) else null
 
-                feedOutlines.add(feedOutline)
+            outline.outlines?.forEach {
+                val recursiveFeedsFolders = parseOutline(it)
+
+                // Treat feeds without folder, so belonging to the current folder
+                associateOrphanFeedsToFolder(foldersAndFeeds, recursiveFeedsFolders, folder)
+                foldersAndFeeds.putAll(recursiveFeedsFolders.toMap())
             }
 
-            outline.outlines = feedOutlines
-            outlines.add(outline)
+            // empty outline
+            if (!foldersAndFeeds.containsKey(folder)) foldersAndFeeds[folder] = listOf()
+
+        } else { // the outline is a feed
+            if (!outline.xmlUrl.isNullOrEmpty()) {
+                val feed = Feed().apply {
+                    name = outline.name
+                    url = outline.xmlUrl
+                    siteUrl = outline.htmlUrl
+                }
+                // parsed feed is linked to null to be assigned to the previous level folder
+                foldersAndFeeds[null] = listOf(feed)
+            }
+        }
+
+        return foldersAndFeeds
+    }
+
+    private fun foldersAndFeedsToOPML(foldersAndFeeds: Map<Folder?, List<Feed>>): OPML {
+        val outlines = arrayListOf<Outline>()
+
+        for (folderAndFeeds in foldersAndFeeds) {
+            if (folderAndFeeds.key != null) {
+                val outline = Outline(folderAndFeeds.key?.name)
+
+                val feedOutlines = arrayListOf<Outline>()
+                for (feed in folderAndFeeds.value) {
+                    val feedOutline = Outline(feed.name, feed.url, feed.siteUrl)
+
+                    feedOutlines += feedOutline
+                }
+
+                outline.outlines = feedOutlines
+                outlines += outline
+            } else {
+                for (feed in folderAndFeeds.value) {
+                    outlines += Outline(feed.name, feed.url, feed.siteUrl)
+                }
+            }
         }
 
         val head = Head("Subscriptions")
@@ -85,4 +141,21 @@ object OPMLParser {
         return OPML("2.0", head, body)
     }
 
+    /**
+     * Associate parsed feeds without folder to the previous level folder.
+     * @param foldersAndFeeds final result
+     * @param parsingResult current level parsing
+     * @param folder the folder feeds will be associated to
+     *
+     */
+    private fun associateOrphanFeedsToFolder(foldersAndFeeds: MutableMap<Folder?, List<Feed>>,
+                                             parsingResult: MutableMap<Folder?, List<Feed>>, folder: Folder?) {
+        val feeds = parsingResult[null]
+        if (feeds != null && feeds.isNotEmpty()) {
+            if (foldersAndFeeds[folder] == null) foldersAndFeeds[folder] = feeds
+            else foldersAndFeeds[folder] = foldersAndFeeds[folder]?.plus(feeds)!!
+
+            parsingResult.remove(null)
+        }
+    }
 }
\ No newline at end of file
diff --git a/api/src/main/java/com/readrops/api/opml/model/OPML.kt b/api/src/main/java/com/readrops/api/opml/model/OPML.kt
index cc6de1f9..6cdc7087 100644
--- a/api/src/main/java/com/readrops/api/opml/model/OPML.kt
+++ b/api/src/main/java/com/readrops/api/opml/model/OPML.kt
@@ -8,7 +8,7 @@ import org.simpleframework.xml.Root
 @Order(elements = ["head", "body"])
 @Root(name = "opml", strict = false)
 data class OPML(@field:Attribute(required = true) var version: String?,
-                @field:Element(required = true) var head: Head?,
+                @field:Element(required = false) var head: Head?,
                 @field:Element(required = true) var body: Body?) {
 
     /**
diff --git a/api/src/main/java/com/readrops/api/opml/model/Outline.kt b/api/src/main/java/com/readrops/api/opml/model/Outline.kt
index 9a261069..2b0635ef 100644
--- a/api/src/main/java/com/readrops/api/opml/model/Outline.kt
+++ b/api/src/main/java/com/readrops/api/opml/model/Outline.kt
@@ -5,8 +5,8 @@ import org.simpleframework.xml.ElementList
 import org.simpleframework.xml.Root
 
 @Root(name = "outline", strict = false)
-data class Outline(@field:Attribute(required = false) var title: String?,
-                   @field:Attribute(required = false) var text: String?,
+data class Outline(@field:Attribute(required = false) private var title: String?,
+                   @field:Attribute(required = false) private var text: String?,
                    @field:Attribute(required = false) var type: String?,
                    @field:Attribute(required = false) var xmlUrl: String?,
                    @field:Attribute(required = false) var htmlUrl: String?,
@@ -23,7 +23,10 @@ data class Outline(@field:Attribute(required = false) var title: String?,
             null,
             null)
 
-    constructor(title: String) : this(title, null, null, null, null, null)
+    constructor(title: String?) : this(title, title, null, null, null, null)
 
-    constructor(title: String, xmlUrl: String, htmlUrl: String) : this(title, title, "rss", xmlUrl, htmlUrl, null)
+    constructor(title: String?, xmlUrl: String, htmlUrl: String?) : this(title, title, "rss", xmlUrl, htmlUrl, null)
+
+    val name: String?
+        get() = title ?: text
 }
\ No newline at end of file
diff --git a/api/src/main/java/com/readrops/api/services/API.java b/api/src/main/java/com/readrops/api/services/API.java
deleted file mode 100644
index d86e938a..00000000
--- a/api/src/main/java/com/readrops/api/services/API.java
+++ /dev/null
@@ -1,61 +0,0 @@
-package com.readrops.api.services;
-
-import androidx.annotation.NonNull;
-
-import com.readrops.api.utils.HttpManager;
-import com.squareup.moshi.Moshi;
-
-import retrofit2.Retrofit;
-import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
-import retrofit2.converter.moshi.MoshiConverterFactory;
-
-/**
- * Abstraction level for services APIs
- *
- * @param <T> an API service interface
- */
-public abstract class API<T> {
-
-    protected static final int MAX_ITEMS = 5000;
-
-    protected T api;
-    private Retrofit retrofit;
-
-    private Class<T> clazz;
-    private String endPoint;
-
-    public API(Credentials credentials, @NonNull Class<T> clazz, @NonNull String endPoint) {
-        this.clazz = clazz;
-        this.endPoint = endPoint;
-
-        api = createAPI(credentials);
-    }
-
-    protected abstract Moshi buildMoshi();
-
-    protected Retrofit getConfiguredRetrofitInstance() {
-        return new Retrofit.Builder()
-                .baseUrl(HttpManager.getInstance().getCredentials().getUrl() + endPoint)
-                .addConverterFactory(MoshiConverterFactory.create(buildMoshi()))
-                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
-                .client(HttpManager.getInstance().getOkHttpClient())
-                .build();
-    }
-
-    private T createAPI(@NonNull Credentials credentials) {
-        HttpManager.getInstance().setCredentials(credentials);
-        retrofit = getConfiguredRetrofitInstance();
-
-        return retrofit.create(clazz);
-    }
-
-    public void setCredentials(@NonNull Credentials credentials) {
-        HttpManager.getInstance().setCredentials(credentials);
-
-        retrofit = retrofit.newBuilder()
-                .baseUrl(credentials.getUrl() + endPoint)
-                .build();
-
-        api = retrofit.create(clazz);
-    }
-}
diff --git a/api/src/main/java/com/readrops/api/services/Credentials.java b/api/src/main/java/com/readrops/api/services/Credentials.java
index 53fee513..755ff918 100644
--- a/api/src/main/java/com/readrops/api/services/Credentials.java
+++ b/api/src/main/java/com/readrops/api/services/Credentials.java
@@ -1,14 +1,15 @@
 package com.readrops.api.services;
 
-import androidx.annotation.Nullable;
-
-import com.readrops.db.entities.account.Account;
 import com.readrops.api.services.freshrss.FreshRSSCredentials;
+import com.readrops.api.services.freshrss.FreshRSSService;
 import com.readrops.api.services.nextcloudnews.NextNewsCredentials;
+import com.readrops.api.services.nextcloudnews.NextNewsService;
+import com.readrops.db.entities.account.Account;
+import com.readrops.db.entities.account.AccountType;
 
 public abstract class Credentials {
 
-    private String authorization;
+    private final String authorization;
 
     private String url;
 
@@ -25,15 +26,31 @@ public abstract class Credentials {
         return url;
     }
 
-    @Nullable
+    public void setUrl(String url) {
+        this.url = url;
+    }
+
     public static Credentials toCredentials(Account account) {
+        String endPoint = getEndPoint(account.getAccountType());
+
         switch (account.getAccountType()) {
             case NEXTCLOUD_NEWS:
-                return new NextNewsCredentials(account.getLogin(), account.getPassword(), account.getUrl());
+                return new NextNewsCredentials(account.getLogin(), account.getPassword(), account.getUrl() + endPoint);
             case FRESHRSS:
-                return new FreshRSSCredentials(account.getToken(), account.getUrl());
+                return new FreshRSSCredentials(account.getToken(), account.getUrl() + endPoint);
             default:
-                return null;
+                throw new IllegalArgumentException("Unknown account type");
+        }
+    }
+
+    private static String getEndPoint(AccountType accountType) {
+        switch (accountType) {
+            case FRESHRSS:
+                return FreshRSSService.END_POINT;
+            case NEXTCLOUD_NEWS:
+                return NextNewsService.END_POINT;
+            default:
+                throw new IllegalArgumentException("Unknown account type");
         }
     }
 }
diff --git a/api/src/main/java/com/readrops/api/services/SyncResult.kt b/api/src/main/java/com/readrops/api/services/SyncResult.kt
index daf97a5d..6252affa 100644
--- a/api/src/main/java/com/readrops/api/services/SyncResult.kt
+++ b/api/src/main/java/com/readrops/api/services/SyncResult.kt
@@ -4,13 +4,11 @@ import com.readrops.db.entities.Feed
 import com.readrops.db.entities.Folder
 import com.readrops.db.entities.Item
 
-class SyncResult {
-
-    var items: List<Item> = mutableListOf()
-
-    var feeds: List<Feed> = listOf()
-
-    var folders: List<Folder> = listOf()
-
-    var isError: Boolean = false
-}
+class SyncResult(var items: List<Item> = mutableListOf(),
+                 var starredItems: List<Item> = mutableListOf(),
+                 var feeds: List<Feed> = listOf(),
+                 var folders: List<Folder> = listOf(),
+                 var unreadIds: List<String>? = null,
+                 var starredIds: List<String>? = null,
+                 var isError: Boolean = false
+)
diff --git a/api/src/main/java/com/readrops/api/services/freshrss/FreshRSSAPI.java b/api/src/main/java/com/readrops/api/services/freshrss/FreshRSSDataSource.java
similarity index 62%
rename from api/src/main/java/com/readrops/api/services/freshrss/FreshRSSAPI.java
rename to api/src/main/java/com/readrops/api/services/freshrss/FreshRSSDataSource.java
index 86fccf7d..16f4c602 100644
--- a/api/src/main/java/com/readrops/api/services/freshrss/FreshRSSAPI.java
+++ b/api/src/main/java/com/readrops/api/services/freshrss/FreshRSSDataSource.java
@@ -3,21 +3,15 @@ package com.readrops.api.services.freshrss;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.readrops.api.services.SyncResult;
+import com.readrops.api.services.SyncType;
+import com.readrops.api.services.freshrss.json.FreshRSSUserInfo;
 import com.readrops.db.entities.Feed;
 import com.readrops.db.entities.Folder;
 import com.readrops.db.entities.Item;
-import com.readrops.api.services.API;
-import com.readrops.api.services.Credentials;
-import com.readrops.api.services.SyncResult;
-import com.readrops.api.services.SyncType;
-import com.readrops.api.services.freshrss.adapters.FreshRSSFeedsAdapter;
-import com.readrops.api.services.freshrss.adapters.FreshRSSFoldersAdapter;
-import com.readrops.api.services.freshrss.adapters.FreshRSSItemsAdapter;
-import com.readrops.api.services.freshrss.json.FreshRSSUserInfo;
-import com.squareup.moshi.Moshi;
-import com.squareup.moshi.Types;
 
 import java.io.StringReader;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Properties;
 
@@ -26,23 +20,21 @@ import io.reactivex.Single;
 import okhttp3.MultipartBody;
 import okhttp3.RequestBody;
 
-public class FreshRSSAPI extends API<FreshRSSService> {
+public class FreshRSSDataSource {
+
+    private static final int MAX_ITEMS = 5000;
+    private static final int MAX_STARRED_ITEMS = 1000;
 
     public static final String GOOGLE_READ = "user/-/state/com.google/read";
+    public static final String GOOGLE_STARRED = "user/-/state/com.google/starred";
+    public static final String GOOGLE_READING_LIST = "user/-/state/com.google/reading-list";
 
     private static final String FEED_PREFIX = "feed/";
 
-    public FreshRSSAPI(Credentials credentials) {
-        super(credentials, FreshRSSService.class, FreshRSSService.END_POINT);
-    }
+    private final FreshRSSService api;
 
-    @Override
-    protected Moshi buildMoshi() {
-        return new Moshi.Builder()
-                .add(Types.newParameterizedType(List.class, Item.class), new FreshRSSItemsAdapter())
-                .add(new FreshRSSFeedsAdapter())
-                .add(new FreshRSSFoldersAdapter())
-                .build();
+    public FreshRSSDataSource(FreshRSSService api) {
+        this.api = api;
     }
 
     /**
@@ -99,6 +91,7 @@ public class FreshRSSAPI extends API<FreshRSSService> {
         SyncResult syncResult = new SyncResult();
 
         return setItemsReadState(syncData, writeToken)
+                .andThen(setItemsStarState(syncData, writeToken))
                 .andThen(getFolders()
                         .flatMap(freshRSSFolders -> {
                             syncResult.setFolders(freshRSSFolders);
@@ -109,7 +102,7 @@ public class FreshRSSAPI extends API<FreshRSSService> {
                             syncResult.setFeeds(freshRSSFeeds);
 
                             if (syncType == SyncType.INITIAL_SYNC) {
-                                return getItems(GOOGLE_READ, MAX_ITEMS, null);
+                                return getItems(Arrays.asList(GOOGLE_READ, GOOGLE_STARRED), MAX_ITEMS, null);
                             } else {
                                 return getItems(null, MAX_ITEMS, syncData.getLastModified());
                             }
@@ -117,7 +110,23 @@ public class FreshRSSAPI extends API<FreshRSSService> {
                         .flatMap(freshRSSItems -> {
                             syncResult.setItems(freshRSSItems);
 
-                            return Single.just(syncResult);
+                            return getItemsIds(GOOGLE_READ, GOOGLE_READING_LIST, MAX_ITEMS);
+                        }).flatMap(unreadItemsIds -> {
+                            syncResult.setUnreadIds(unreadItemsIds);
+
+                            return getItemsIds(null, GOOGLE_STARRED, MAX_STARRED_ITEMS);
+                        }).flatMap(starredItemsIds -> {
+                            syncResult.setStarredIds(starredItemsIds);
+
+                            if (syncType == SyncType.INITIAL_SYNC) {
+                                return getStarredItems(MAX_STARRED_ITEMS).flatMap(starredItems -> {
+                                    syncResult.setStarredItems(starredItems);
+
+                                    return Single.just(syncResult);
+                                });
+                            } else {
+                                return Single.just(syncResult);
+                            }
                         }));
     }
 
@@ -142,13 +151,27 @@ public class FreshRSSAPI extends API<FreshRSSService> {
     /**
      * Fetch the items
      *
-     * @param excludeTarget type of items to exclude (currently only read items)
-     * @param max           max number of items to fetch
-     * @param lastModified  fetch only items created after this timestamp
+     * @param excludeTargets type of items to exclude (read items and starred items)
+     * @param max            max number of items to fetch
+     * @param lastModified   fetch only items created after this timestamp
      * @return the items
      */
-    public Single<List<Item>> getItems(@Nullable String excludeTarget, int max, @Nullable Long lastModified) {
-        return api.getItems(excludeTarget, max, lastModified);
+    public Single<List<Item>> getItems(@Nullable List<String> excludeTargets, int max, @Nullable Long lastModified) {
+        return api.getItems(excludeTargets, max, lastModified);
+    }
+
+    /**
+     * Fetch starred items
+     *
+     * @param max max number of items to fetch
+     * @return items
+     */
+    public Single<List<Item>> getStarredItems(int max) {
+        return api.getStarredItems(max);
+    }
+
+    public Single<List<String>> getItemsIds(String excludeTarget, String includeTarget, int max) {
+        return api.getItemsIds(excludeTarget, includeTarget, max);
     }
 
 
@@ -161,10 +184,27 @@ public class FreshRSSAPI extends API<FreshRSSService> {
      * @return Completable
      */
     public Completable markItemsReadUnread(boolean read, @NonNull List<String> itemIds, @NonNull String token) {
-        if (read)
-            return api.setItemsReadState(token, GOOGLE_READ, null, itemIds);
-        else
-            return api.setItemsReadState(token, null, GOOGLE_READ, itemIds);
+        if (read) {
+            return api.setItemsState(token, GOOGLE_READ, null, itemIds);
+        } else {
+            return api.setItemsState(token, null, GOOGLE_READ, itemIds);
+        }
+    }
+
+    /**
+     * Mark items as starred or unstarred
+     *
+     * @param starred true for starred, false for unstarred
+     * @param itemIds items ids to mark
+     * @param token   token for modifications
+     * @return Completable
+     */
+    public Completable markItemsStarredUnstarred(boolean starred, @NonNull List<String> itemIds, @NonNull String token) {
+        if (starred) {
+            return api.setItemsState(token, GOOGLE_STARRED, null, itemIds);
+        } else {
+            return api.setItemsState(token, null, GOOGLE_STARRED, itemIds);
+        }
     }
 
     /**
@@ -237,7 +277,7 @@ public class FreshRSSAPI extends API<FreshRSSService> {
     }
 
     /**
-     * Set the state of items
+     * Set items star state
      *
      * @param syncData data containing items to mark
      * @param token    token for modifications
@@ -245,17 +285,44 @@ public class FreshRSSAPI extends API<FreshRSSService> {
      */
     private Completable setItemsReadState(@NonNull FreshRSSSyncData syncData, @NonNull String token) {
         Completable readItemsCompletable;
-        if (syncData.getReadItemsIds().isEmpty())
+        if (syncData.getReadItemsIds().isEmpty()) {
             readItemsCompletable = Completable.complete();
-        else
+        } else {
             readItemsCompletable = markItemsReadUnread(true, syncData.getReadItemsIds(), token);
+        }
 
         Completable unreadItemsCompletable;
-        if (syncData.getUnreadItemsIds().isEmpty())
+        if (syncData.getUnreadItemsIds().isEmpty()) {
             unreadItemsCompletable = Completable.complete();
-        else
+        } else {
             unreadItemsCompletable = markItemsReadUnread(false, syncData.getUnreadItemsIds(), token);
+        }
 
         return readItemsCompletable.concatWith(unreadItemsCompletable);
     }
+
+    /**
+     * Set items star state
+     *
+     * @param syncData data containing items to mark
+     * @param token    token for modifications
+     * @return A concatenation of two completable (starred and unstarred completable)
+     */
+    private Completable setItemsStarState(@NonNull FreshRSSSyncData syncData, @NonNull String token) {
+        Completable starredItemsCompletable;
+        if (syncData.getStarredItemsIds().isEmpty()) {
+            starredItemsCompletable = Completable.complete();
+        } else {
+            starredItemsCompletable = markItemsStarredUnstarred(true, syncData.getStarredItemsIds(), token);
+        }
+
+        Completable unstarredItemsCompletable;
+        if (syncData.getUnstarredItemsIds().isEmpty()) {
+            unstarredItemsCompletable = Completable.complete();
+        } else {
+            unstarredItemsCompletable = markItemsStarredUnstarred(false, syncData.getUnstarredItemsIds(), token);
+        }
+
+        return starredItemsCompletable.concatWith(unstarredItemsCompletable);
+    }
 }
diff --git a/api/src/main/java/com/readrops/api/services/freshrss/FreshRSSService.java b/api/src/main/java/com/readrops/api/services/freshrss/FreshRSSService.java
index 321a4164..43184f7f 100644
--- a/api/src/main/java/com/readrops/api/services/freshrss/FreshRSSService.java
+++ b/api/src/main/java/com/readrops/api/services/freshrss/FreshRSSService.java
@@ -1,9 +1,9 @@
 package com.readrops.api.services.freshrss;
 
+import com.readrops.api.services.freshrss.json.FreshRSSUserInfo;
 import com.readrops.db.entities.Feed;
 import com.readrops.db.entities.Folder;
 import com.readrops.db.entities.Item;
-import com.readrops.api.services.freshrss.json.FreshRSSUserInfo;
 
 import java.util.List;
 
@@ -35,14 +35,20 @@ public interface FreshRSSService {
     Single<List<Feed>> getFeeds();
 
     @GET("reader/api/0/stream/contents/user/-/state/com.google/reading-list")
-    Single<List<Item>> getItems(@Query("xt") String excludeTarget, @Query("n") int max, @Query("ot") Long lastModified);
+    Single<List<Item>> getItems(@Query("xt") List<String> excludeTarget, @Query("n") int max, @Query("ot") Long lastModified);
+
+    @GET("reader/api/0/stream/contents/user/-/state/com.google/starred")
+    Single<List<Item>> getStarredItems(@Query("n") int max);
+
+    @GET("reader/api/0/stream/items/ids")
+    Single<List<String>> getItemsIds(@Query("xt") String excludeTarget, @Query("s") String includeTarget, @Query("n") int max);
 
     @GET("reader/api/0/tag/list?output=json")
     Single<List<Folder>> getFolders();
 
     @FormUrlEncoded
     @POST("reader/api/0/edit-tag")
-    Completable setItemsReadState(@Field("T") String token, @Field("a") String readAction, @Field("r") String unreadAction, @Field("i") List<String> itemIds);
+    Completable setItemsState(@Field("T") String token, @Field("a") String addAction, @Field("r") String removeAction, @Field("i") List<String> itemIds);
 
     @FormUrlEncoded
     @POST("reader/api/0/subscription/edit")
diff --git a/api/src/main/java/com/readrops/api/services/freshrss/FreshRSSSyncData.java b/api/src/main/java/com/readrops/api/services/freshrss/FreshRSSSyncData.java
index 072b11bf..dde0c8b2 100644
--- a/api/src/main/java/com/readrops/api/services/freshrss/FreshRSSSyncData.java
+++ b/api/src/main/java/com/readrops/api/services/freshrss/FreshRSSSyncData.java
@@ -11,6 +11,10 @@ public class FreshRSSSyncData {
 
     private List<String> unreadItemsIds;
 
+    private List<String> starredItemsIds;
+
+    private List<String> unstarredItemsIds;
+
     public FreshRSSSyncData() {
         readItemsIds = new ArrayList<>();
         unreadItemsIds = new ArrayList<>();
@@ -39,4 +43,20 @@ public class FreshRSSSyncData {
     public void setUnreadItemsIds(List<String> unreadItemsIds) {
         this.unreadItemsIds = unreadItemsIds;
     }
+
+    public List<String> getStarredItemsIds() {
+        return starredItemsIds;
+    }
+
+    public void setStarredItemsIds(List<String> starredItemsIds) {
+        this.starredItemsIds = starredItemsIds;
+    }
+
+    public List<String> getUnstarredItemsIds() {
+        return unstarredItemsIds;
+    }
+
+    public void setUnstarredItemsIds(List<String> unstarredItemsIds) {
+        this.unstarredItemsIds = unstarredItemsIds;
+    }
 }
diff --git a/api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSFeedsAdapter.kt b/api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSFeedsAdapter.kt
index e0c19853..365d7c71 100644
--- a/api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSFeedsAdapter.kt
+++ b/api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSFeedsAdapter.kt
@@ -1,6 +1,7 @@
 package com.readrops.api.services.freshrss.adapters
 
 import android.annotation.SuppressLint
+import com.readrops.api.utils.exceptions.ParseException
 import com.readrops.db.entities.Feed
 import com.squareup.moshi.FromJson
 import com.squareup.moshi.JsonReader
@@ -16,36 +17,40 @@ class FreshRSSFeedsAdapter {
     fun fromJson(reader: JsonReader): List<Feed> {
         val feeds = mutableListOf<Feed>()
 
-        reader.beginObject()
-        reader.nextName() // "subscriptions", beginning of the feed array
-        reader.beginArray()
-
-        while (reader.hasNext()) {
+        return try {
             reader.beginObject()
+            reader.nextName() // "subscriptions", beginning of the feed array
+            reader.beginArray()
 
-            val feed = Feed()
             while (reader.hasNext()) {
-                with(feed) {
-                    when (reader.selectName(NAMES)) {
-                        0 -> name = reader.nextString()
-                        1 -> url = reader.nextString()
-                        2 -> siteUrl = reader.nextString()
-                        3 -> iconUrl = reader.nextString()
-                        4 -> remoteId = reader.nextString()
-                        5 -> remoteFolderId = getCategoryId(reader)
-                        else -> reader.skipValue()
+                reader.beginObject()
+
+                val feed = Feed()
+                while (reader.hasNext()) {
+                    with(feed) {
+                        when (reader.selectName(NAMES)) {
+                            0 -> name = reader.nextString()
+                            1 -> url = reader.nextString()
+                            2 -> siteUrl = reader.nextString()
+                            3 -> iconUrl = reader.nextString()
+                            4 -> remoteId = reader.nextString()
+                            5 -> remoteFolderId = getCategoryId(reader)
+                            else -> reader.skipValue()
+                        }
                     }
                 }
+
+                feeds += feed
+                reader.endObject()
             }
 
-            feeds += feed
+            reader.endArray()
             reader.endObject()
+
+            feeds
+        } catch (e: Exception) {
+            throw ParseException(e.message)
         }
-
-        reader.endArray()
-        reader.endObject()
-
-        return feeds
     }
 
     private fun getCategoryId(reader: JsonReader): String? {
@@ -72,6 +77,7 @@ class FreshRSSFeedsAdapter {
     }
 
     companion object {
-        val NAMES: JsonReader.Options = JsonReader.Options.of("title", "url", "htmlUrl", "iconUrl", "id", "categories")
+        val NAMES: JsonReader.Options = JsonReader.Options.of("title", "url", "htmlUrl",
+                "iconUrl", "id", "categories")
     }
 }
\ No newline at end of file
diff --git a/api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSFoldersAdapter.kt b/api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSFoldersAdapter.kt
index 8574f990..20c687c3 100644
--- a/api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSFoldersAdapter.kt
+++ b/api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSFoldersAdapter.kt
@@ -1,6 +1,7 @@
 package com.readrops.api.services.freshrss.adapters
 
 import android.annotation.SuppressLint
+import com.readrops.api.utils.exceptions.ParseException
 import com.readrops.db.entities.Folder
 import com.squareup.moshi.FromJson
 import com.squareup.moshi.JsonReader
@@ -17,42 +18,46 @@ class FreshRSSFoldersAdapter {
     fun fromJson(reader: JsonReader): List<Folder> {
         val folders = mutableListOf<Folder>()
 
-        reader.beginObject()
-        reader.nextName() // "tags", beginning of folder array
-        reader.beginArray()
-
-        while (reader.hasNext()) {
+        return try {
             reader.beginObject()
-
-            val folder = Folder()
-            var type: String? = null
+            reader.nextName() // "tags", beginning of folder array
+            reader.beginArray()
 
             while (reader.hasNext()) {
-                with(folder) {
-                    when (reader.selectName(NAMES)) {
-                        0 -> {
-                            val id = reader.nextString()
-                            name = StringTokenizer(id, "/")
-                                    .toList()
-                                    .last() as String
-                            remoteId = id
+                reader.beginObject()
+
+                val folder = Folder()
+                var type: String? = null
+
+                while (reader.hasNext()) {
+                    with(folder) {
+                        when (reader.selectName(NAMES)) {
+                            0 -> {
+                                val id = reader.nextString()
+                                name = StringTokenizer(id, "/")
+                                        .toList()
+                                        .last() as String
+                                remoteId = id
+                            }
+                            1 -> type = reader.nextString()
+                            else -> reader.skipValue()
                         }
-                        1 -> type = reader.nextString()
-                        else -> reader.skipValue()
                     }
                 }
+
+                if (type == "folder") // add only folders and avoid tags
+                    folders += folder
+
+                reader.endObject()
             }
 
-            if (type == "folder") // add only folders and avoid tags
-                folders += folder
-
+            reader.endArray()
             reader.endObject()
+
+            folders
+        } catch (e: Exception) {
+            throw ParseException(e.message)
         }
-
-        reader.endArray()
-        reader.endObject()
-
-        return folders
     }
 
     companion object {
diff --git a/api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSItemsAdapter.kt b/api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSItemsAdapter.kt
index 8667fa51..a68f0f88 100644
--- a/api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSItemsAdapter.kt
+++ b/api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSItemsAdapter.kt
@@ -1,8 +1,10 @@
 package com.readrops.api.services.freshrss.adapters
 
 import android.util.TimingLogger
+import com.readrops.api.services.freshrss.FreshRSSDataSource.GOOGLE_READ
+import com.readrops.api.services.freshrss.FreshRSSDataSource.GOOGLE_STARRED
+import com.readrops.api.utils.exceptions.ParseException
 import com.readrops.db.entities.Item
-import com.readrops.api.services.freshrss.FreshRSSAPI.GOOGLE_READ
 import com.squareup.moshi.JsonAdapter
 import com.squareup.moshi.JsonReader
 import com.squareup.moshi.JsonWriter
@@ -19,17 +21,21 @@ class FreshRSSItemsAdapter : JsonAdapter<List<Item>>() {
         val logger = TimingLogger(TAG, "item parsing")
         val items = mutableListOf<Item>()
 
-        reader.beginObject()
-        while (reader.hasNext()) {
-            if (reader.nextName() == "items") parseItems(reader, items) else reader.skipValue()
+        return try {
+            reader.beginObject()
+            while (reader.hasNext()) {
+                if (reader.nextName() == "items") parseItems(reader, items) else reader.skipValue()
+            }
+
+            reader.endObject()
+
+            logger.addSplit("item parsing done")
+            logger.dumpToLog()
+
+            items
+        } catch (e: Exception) {
+            throw ParseException(e.message)
         }
-
-        reader.endObject()
-
-        logger.addSplit("item parsing done")
-        logger.dumpToLog()
-
-        return items
     }
 
     private fun parseItems(reader: JsonReader, items: MutableList<Item>) {
@@ -48,7 +54,7 @@ class FreshRSSItemsAdapter : JsonAdapter<List<Item>>() {
                         2 -> title = reader.nextString()
                         3 -> content = getContent(reader)
                         4 -> link = getLink(reader)
-                        5 -> isRead = getReadState(reader)
+                        5 -> getStates(reader, this)
                         6 -> feedRemoteId = getRemoteFeedId(reader)
                         7 -> author = reader.nextString()
                         else -> reader.skipValue()
@@ -97,18 +103,17 @@ class FreshRSSItemsAdapter : JsonAdapter<List<Item>>() {
         return href
     }
 
-    private fun getReadState(reader: JsonReader): Boolean {
-        var isRead = false
+    private fun getStates(reader: JsonReader, item: Item) {
         reader.beginArray()
 
         while (reader.hasNext()) {
             when (reader.nextString()) {
-                GOOGLE_READ -> isRead = true
+                GOOGLE_READ -> item.isRead = true
+                GOOGLE_STARRED -> item.isStarred = true
             }
         }
 
         reader.endArray()
-        return isRead
     }
 
     private fun getRemoteFeedId(reader: JsonReader): String? {
@@ -127,7 +132,8 @@ class FreshRSSItemsAdapter : JsonAdapter<List<Item>>() {
     }
 
     companion object {
-        val NAMES: JsonReader.Options = JsonReader.Options.of("id", "published", "title", "summary", "alternate", "categories", "origin", "author")
+        val NAMES: JsonReader.Options = JsonReader.Options.of("id", "published", "title",
+                "summary", "alternate", "categories", "origin", "author")
 
         val TAG = FreshRSSItemsAdapter::class.java.simpleName
     }
diff --git a/api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSItemsIdsAdapter.kt b/api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSItemsIdsAdapter.kt
new file mode 100644
index 00000000..988507c1
--- /dev/null
+++ b/api/src/main/java/com/readrops/api/services/freshrss/adapters/FreshRSSItemsIdsAdapter.kt
@@ -0,0 +1,58 @@
+package com.readrops.api.services.freshrss.adapters
+
+import android.annotation.SuppressLint
+import com.readrops.api.utils.exceptions.ParseException
+import com.readrops.api.utils.extensions.nextNonEmptyString
+import com.squareup.moshi.JsonAdapter
+import com.squareup.moshi.JsonReader
+import com.squareup.moshi.JsonWriter
+
+class FreshRSSItemsIdsAdapter : JsonAdapter<List<String>>() {
+
+    override fun toJson(writer: JsonWriter, value: List<String>?) {
+        // not useful here
+    }
+
+    @SuppressLint("CheckResult")
+    override fun fromJson(reader: JsonReader): List<String>? = with(reader) {
+        val ids = arrayListOf<String>()
+
+        return try {
+            beginObject()
+            nextName()
+            beginArray()
+
+            while (hasNext()) {
+                beginObject()
+
+                when (nextName()) {
+                    "id" -> {
+                        val value = nextNonEmptyString()
+                        ids += "tag:google.com,2005:reader/item/${
+                            value.toLong()
+                                    .toString(16).padStart(value.length, '0')
+                        }"
+                    }
+                    else -> skipValue()
+                }
+
+                endObject()
+            }
+
+            endArray()
+
+            // skip continuation
+            if (hasNext()) {
+                skipName()
+                skipValue()
+            }
+
+            endObject()
+
+            ids
+        } catch (e: Exception) {
+            throw ParseException(e.message)
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsCredentials.java b/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsCredentials.java
index 40644621..24c44ab7 100644
--- a/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsCredentials.java
+++ b/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsCredentials.java
@@ -5,6 +5,6 @@ import com.readrops.api.services.Credentials;
 public class NextNewsCredentials extends Credentials {
 
     public NextNewsCredentials(String login, String password, String url) {
-        super(okhttp3.Credentials.basic(login, password), url);
+        super(login != null && password != null ? okhttp3.Credentials.basic(login, password) : null, url);
     }
 }
diff --git a/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsAPI.java b/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsDataSource.java
similarity index 63%
rename from api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsAPI.java
rename to api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsDataSource.java
index eb9d8d16..fd8c57ee 100644
--- a/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsAPI.java
+++ b/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsDataSource.java
@@ -5,56 +5,52 @@ import android.content.res.Resources;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.readrops.api.services.SyncResult;
+import com.readrops.api.services.SyncType;
+import com.readrops.api.services.nextcloudnews.adapters.NextNewsUserAdapter;
+import com.readrops.api.utils.ApiUtils;
+import com.readrops.api.utils.exceptions.ConflictException;
+import com.readrops.api.utils.exceptions.UnknownFormatException;
 import com.readrops.db.entities.Feed;
 import com.readrops.db.entities.Folder;
 import com.readrops.db.entities.Item;
-import com.readrops.api.services.API;
-import com.readrops.api.services.Credentials;
-import com.readrops.api.services.SyncResult;
-import com.readrops.api.services.SyncType;
-import com.readrops.api.services.nextcloudnews.adapters.NextNewsFeedsAdapter;
-import com.readrops.api.services.nextcloudnews.adapters.NextNewsFoldersAdapter;
-import com.readrops.api.services.nextcloudnews.adapters.NextNewsItemsAdapter;
-import com.readrops.api.services.nextcloudnews.json.NextNewsUser;
-import com.readrops.api.utils.ConflictException;
-import com.readrops.api.utils.LibUtils;
-import com.readrops.api.utils.UnknownFormatException;
-import com.squareup.moshi.Moshi;
-import com.squareup.moshi.Types;
+import com.readrops.db.pojo.StarItem;
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
+import okhttp3.ResponseBody;
 import retrofit2.Response;
 
-public class NextNewsAPI extends API<NextNewsService> {
+public class NextNewsDataSource {
 
-    private static final String TAG = NextNewsAPI.class.getSimpleName();
+    private static final String TAG = NextNewsDataSource.class.getSimpleName();
 
-    public NextNewsAPI(Credentials credentials) {
-        super(credentials, NextNewsService.class, NextNewsService.END_POINT);
-    }
+    private static final int MAX_ITEMS = 5000;
+    private static final int MAX_STARRED_ITEMS = 1000;
 
-    @Override
-    protected Moshi buildMoshi() {
-        return new Moshi.Builder()
-                .add(new NextNewsFeedsAdapter())
-                .add(new NextNewsFoldersAdapter())
-                .add(Types.newParameterizedType(List.class, Item.class), new NextNewsItemsAdapter())
-                .build();
+    private NextNewsService api;
+
+    public NextNewsDataSource(NextNewsService api) {
+        this.api = api;
     }
 
     @Nullable
-    public NextNewsUser login() throws IOException {
-        Response<NextNewsUser> response = api.getUser().execute();
+    public String login(String user) throws IOException {
+        Response<ResponseBody> response = api.getUser(user).execute();
 
-        if (!response.isSuccessful())
+        if (!response.isSuccessful()) {
             return null;
+        }
 
-        return response.body();
+        String displayName = new NextNewsUserAdapter().fromXml(response.body().byteStream());
+        response.body().close();
+
+        return displayName;
     }
 
     @Nullable
@@ -62,7 +58,7 @@ public class NextNewsAPI extends API<NextNewsService> {
         Response<List<Feed>> response = api.createFeed(url, folderId).execute();
 
         if (!response.isSuccessful()) {
-            if (response.code() == LibUtils.HTTP_UNPROCESSABLE)
+            if (response.code() == ApiUtils.HTTP_UNPROCESSABLE)
                 throw new UnknownFormatException();
             else
                 return null;
@@ -91,7 +87,8 @@ public class NextNewsAPI extends API<NextNewsService> {
     private void initialSync(SyncResult syncResult) throws IOException {
         getFeedsAndFolders(syncResult);
 
-        Response<List<Item>> itemsResponse = api.getItems(3, false, MAX_ITEMS).execute();
+        // unread items
+        Response<List<Item>> itemsResponse = api.getItems(ItemQueryType.ALL.value, false, MAX_ITEMS).execute();
         List<Item> itemList = itemsResponse.body();
 
         if (!itemsResponse.isSuccessful())
@@ -99,13 +96,23 @@ public class NextNewsAPI extends API<NextNewsService> {
 
         if (itemList != null)
             syncResult.setItems(itemList);
+
+        // starred items
+        Response<List<Item>> starredItemsResponse = api.getItems(ItemQueryType.STARRED.value, true, MAX_STARRED_ITEMS).execute();
+        List<Item> starredItems = starredItemsResponse.body();
+
+        if (!itemsResponse.isSuccessful())
+            syncResult.setError(true);
+
+        if (itemList != null)
+            syncResult.setStarredItems(starredItems);
     }
 
     private void classicSync(SyncResult syncResult, NextNewsSyncData data) throws IOException {
         putModifiedItems(data, syncResult);
         getFeedsAndFolders(syncResult);
 
-        Response<List<Item>> itemsResponse = api.getNewItems(data.getLastModified(), 3).execute();
+        Response<List<Item>> itemsResponse = api.getNewItems(data.getLastModified(), ItemQueryType.ALL.value).execute();
         List<Item> itemList = itemsResponse.body();
 
         if (!itemsResponse.isSuccessful())
@@ -137,27 +144,11 @@ public class NextNewsAPI extends API<NextNewsService> {
     }
 
     private void putModifiedItems(NextNewsSyncData data, SyncResult syncResult) throws IOException {
-        if (!data.getReadItems().isEmpty()) {
-            Map<String, List<String>> itemIdsMap = new HashMap<>();
-            itemIdsMap.put("items", data.getReadItems());
+        setReadState(data.getReadItems(), syncResult, StateType.READ);
+        setReadState(data.getUnreadItems(), syncResult, StateType.UNREAD);
 
-            Response readItemsResponse = api.setArticlesState(StateType.READ.name().toLowerCase(),
-                    itemIdsMap).execute();
-
-            if (!readItemsResponse.isSuccessful())
-                syncResult.setError(true);
-        }
-
-        if (!data.getUnreadItems().isEmpty()) {
-            Map<String, List<String>> itemIdsMap = new HashMap<>();
-            itemIdsMap.put("items", data.getUnreadItems());
-
-            Response unreadItemsResponse = api.setArticlesState(StateType.UNREAD.toString().toLowerCase(),
-                    itemIdsMap).execute();
-
-            if (!unreadItemsResponse.isSuccessful())
-                syncResult.setError(true);
-        }
+        setStarState(data.getStarredItems(), syncResult, StateType.STAR);
+        setStarState(data.getUnstarredItems(), syncResult, StateType.UNSTAR);
     }
 
     public List<Folder> createFolder(Folder folder) throws IOException, UnknownFormatException, ConflictException {
@@ -168,9 +159,9 @@ public class NextNewsAPI extends API<NextNewsService> {
 
         if (foldersResponse.isSuccessful())
             return foldersResponse.body();
-        else if (foldersResponse.code() == LibUtils.HTTP_UNPROCESSABLE)
+        else if (foldersResponse.code() == ApiUtils.HTTP_UNPROCESSABLE)
             throw new UnknownFormatException();
-        else if (foldersResponse.code() == LibUtils.HTTP_CONFLICT)
+        else if (foldersResponse.code() == ApiUtils.HTTP_CONFLICT)
             throw new ConflictException();
         else
             return new ArrayList<>();
@@ -181,7 +172,7 @@ public class NextNewsAPI extends API<NextNewsService> {
 
         if (response.isSuccessful())
             return true;
-        else if (response.code() == LibUtils.HTTP_NOT_FOUND)
+        else if (response.code() == ApiUtils.HTTP_NOT_FOUND)
             throw new Resources.NotFoundException();
         else
             return false;
@@ -197,11 +188,11 @@ public class NextNewsAPI extends API<NextNewsService> {
             return true;
         else {
             switch (response.code()) {
-                case LibUtils.HTTP_NOT_FOUND:
+                case ApiUtils.HTTP_NOT_FOUND:
                     throw new Resources.NotFoundException();
-                case LibUtils.HTTP_UNPROCESSABLE:
+                case ApiUtils.HTTP_UNPROCESSABLE:
                     throw new UnknownFormatException();
-                case LibUtils.HTTP_CONFLICT:
+                case ApiUtils.HTTP_CONFLICT:
                     throw new ConflictException();
                 default:
                     return false;
@@ -214,7 +205,7 @@ public class NextNewsAPI extends API<NextNewsService> {
 
         if (response.isSuccessful())
             return true;
-        else if (response.code() == LibUtils.HTTP_NOT_FOUND)
+        else if (response.code() == ApiUtils.HTTP_NOT_FOUND)
             throw new Resources.NotFoundException();
         else
             return false;
@@ -228,7 +219,7 @@ public class NextNewsAPI extends API<NextNewsService> {
 
         if (response.isSuccessful())
             return true;
-        else if (response.code() == LibUtils.HTTP_NOT_FOUND)
+        else if (response.code() == ApiUtils.HTTP_NOT_FOUND)
             throw new Resources.NotFoundException();
         else
             return false;
@@ -242,16 +233,63 @@ public class NextNewsAPI extends API<NextNewsService> {
 
         if (response.isSuccessful())
             return true;
-        else if (response.code() == LibUtils.HTTP_NOT_FOUND)
+        else if (response.code() == ApiUtils.HTTP_NOT_FOUND)
             throw new Resources.NotFoundException();
         else
             return false;
     }
 
+    private void setReadState(List<String> items, SyncResult syncResult, StateType stateType) throws IOException {
+        if (!items.isEmpty()) {
+            Map<String, List<String>> itemIdsMap = new HashMap<>();
+            itemIdsMap.put("items", items);
+
+            Response readItemsResponse = api.setReadState(stateType.name().toLowerCase(),
+                    itemIdsMap).execute();
+
+            if (!readItemsResponse.isSuccessful())
+                syncResult.setError(true);
+        }
+    }
+
+    private void setStarState(List<StarItem> items, SyncResult syncResult, StateType stateType) throws IOException {
+        if (!items.isEmpty()) {
+            List<Map<String, String>> body = new ArrayList<>();
+            for (StarItem item : items) {
+                Map<String, String> itemBody = new HashMap<>();
+                itemBody.put("feedId", item.getFeedRemoteId());
+                itemBody.put("guidHash", item.getGuidHash());
+
+                body.add(itemBody);
+            }
+
+            Response response = api.setStarState(stateType.name().toLowerCase(),
+                    Collections.singletonMap("items", body)).execute();
+            if (!response.isSuccessful()) {
+                syncResult.setError(true);
+            }
+        }
+    }
+
     public enum StateType {
         READ,
         UNREAD,
-        STARRED,
-        UNSTARRED
+        STAR,
+        UNSTAR
+    }
+
+    public enum ItemQueryType {
+        ALL(3),
+        STARRED(2);
+
+        private int value;
+
+         ItemQueryType(int value) {
+            this.value = value;
+        }
+
+        public int getValue() {
+            return value;
+        }
     }
 }
diff --git a/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsService.java b/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsService.java
index ad37b4fa..cf1ce098 100644
--- a/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsService.java
+++ b/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsService.java
@@ -3,7 +3,6 @@ package com.readrops.api.services.nextcloudnews;
 import com.readrops.db.entities.Feed;
 import com.readrops.db.entities.Folder;
 import com.readrops.db.entities.Item;
-import com.readrops.api.services.nextcloudnews.json.NextNewsUser;
 
 import java.util.List;
 import java.util.Map;
@@ -13,6 +12,7 @@ import retrofit2.Call;
 import retrofit2.http.Body;
 import retrofit2.http.DELETE;
 import retrofit2.http.GET;
+import retrofit2.http.Headers;
 import retrofit2.http.POST;
 import retrofit2.http.PUT;
 import retrofit2.http.Path;
@@ -21,9 +21,10 @@ import retrofit2.http.Query;
 public interface NextNewsService {
 
     String END_POINT = "/index.php/apps/news/api/v1-2/";
-
-    @GET("user")
-    Call<NextNewsUser> getUser();
+    
+    @GET("/ocs/v1.php/cloud/users/{userId}")
+    @Headers("OCS-APIRequest: true")
+    Call<ResponseBody> getUser(@Path("userId") String userId);
 
     @GET("folders")
     Call<List<Folder>> getFolders();
@@ -38,7 +39,10 @@ public interface NextNewsService {
     Call<List<Item>> getNewItems(@Query("lastModified") long lastModified, @Query("type") int type);
 
     @PUT("items/{stateType}/multiple")
-    Call<ResponseBody> setArticlesState(@Path("stateType") String stateType, @Body Map<String, List<String>> itemIdsMap);
+    Call<ResponseBody> setReadState(@Path("stateType") String stateType, @Body Map<String, List<String>> itemIdsMap);
+
+    @PUT("items/{starType}/multiple")
+    Call<ResponseBody> setStarState(@Path("starType") String starType, @Body Map<String, List<Map<String, String>>> body);
 
     @POST("feeds")
     Call<List<Feed>> createFeed(@Query("url") String url, @Query("folderId") int folderId);
diff --git a/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsSyncData.java b/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsSyncData.java
index cc1fc8ba..0e59a982 100644
--- a/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsSyncData.java
+++ b/api/src/main/java/com/readrops/api/services/nextcloudnews/NextNewsSyncData.java
@@ -1,5 +1,7 @@
 package com.readrops.api.services.nextcloudnews;
 
+import com.readrops.db.pojo.StarItem;
+
 import java.util.ArrayList;
 import java.util.List;
 
@@ -9,9 +11,9 @@ public class NextNewsSyncData {
 
     private List<String> readItems;
 
-    private List<Integer> starredItems;
+    private List<StarItem> starredItems;
 
-    private List<Integer> unstarredItems;
+    private List<StarItem> unstarredItems;
 
     private long lastModified;
 
@@ -38,19 +40,19 @@ public class NextNewsSyncData {
         this.readItems = readItems;
     }
 
-    public List<Integer> getStarredItems() {
+    public List<StarItem> getStarredItems() {
         return starredItems;
     }
 
-    public void setStarredItems(List<Integer> starredItems) {
+    public void setStarredItems(List<StarItem> starredItems) {
         this.starredItems = starredItems;
     }
 
-    public List<Integer> getUnstarredItems() {
+    public List<StarItem> getUnstarredItems() {
         return unstarredItems;
     }
 
-    public void setUnstarredItems(List<Integer> unstarredItems) {
+    public void setUnstarredItems(List<StarItem> unstarredItems) {
         this.unstarredItems = unstarredItems;
     }
 
diff --git a/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsFeedsAdapter.kt b/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsFeedsAdapter.kt
index 16ad2e79..a5ccc6e9 100644
--- a/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsFeedsAdapter.kt
+++ b/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsFeedsAdapter.kt
@@ -1,12 +1,15 @@
 package com.readrops.api.services.nextcloudnews.adapters
 
 import android.annotation.SuppressLint
-import com.readrops.api.utils.nextNullableInt
+import com.readrops.api.utils.exceptions.ParseException
+import com.readrops.api.utils.extensions.nextNonEmptyString
+import com.readrops.api.utils.extensions.nextNullableInt
+import com.readrops.api.utils.extensions.nextNullableString
 import com.readrops.db.entities.Feed
-import com.readrops.api.utils.nextNullableString
 import com.squareup.moshi.FromJson
 import com.squareup.moshi.JsonReader
 import com.squareup.moshi.ToJson
+import java.net.URI
 
 class NextNewsFeedsAdapter {
 
@@ -18,15 +21,19 @@ class NextNewsFeedsAdapter {
     fun fromJson(reader: JsonReader): List<Feed> {
         val feeds = mutableListOf<Feed>()
 
-        reader.beginObject()
+        return try {
+            reader.beginObject()
 
-        while (reader.hasNext()) {
-            if (reader.nextName() == "feeds") parseFeeds(reader, feeds) else reader.skipValue()
+            while (reader.hasNext()) {
+                if (reader.nextName() == "feeds") parseFeeds(reader, feeds) else reader.skipValue()
+            }
+
+            reader.endObject()
+
+            feeds
+        } catch (e: Exception) {
+            throw ParseException(e.message)
         }
-
-        reader.endObject()
-
-        return feeds
     }
 
     private fun parseFeeds(reader: JsonReader, feeds: MutableList<Feed>) {
@@ -39,10 +46,10 @@ class NextNewsFeedsAdapter {
             while (reader.hasNext()) {
                 with(feed) {
                     when (reader.selectName(NAMES)) {
-                        0 -> remoteId = reader.nextString()
-                        1 -> url = reader.nextString()
-                        2 -> name = reader.nextString()
-                        3 -> iconUrl = reader.nextString()
+                        0 -> remoteId = reader.nextNonEmptyString()
+                        1 -> url = reader.nextNonEmptyString()
+                        2 -> name = reader.nextNullableString()
+                        3 -> iconUrl = reader.nextNullableString()
                         4 -> {
                             val nextInt = reader.nextNullableInt()
                             remoteFolderId = if (nextInt != null && nextInt > 0) nextInt.toString() else null
@@ -53,6 +60,8 @@ class NextNewsFeedsAdapter {
                 }
             }
 
+            if (feed.name == null) feed.name = URI.create(feed.url).host
+
             feeds += feed
             reader.endObject()
         }
@@ -61,6 +70,7 @@ class NextNewsFeedsAdapter {
     }
 
     companion object {
-        val NAMES: JsonReader.Options = JsonReader.Options.of("id", "url", "title", "faviconLink", "folderId", "link")
+        val NAMES: JsonReader.Options = JsonReader.Options.of("id", "url", "title",
+                "faviconLink", "folderId", "link")
     }
 }
\ No newline at end of file
diff --git a/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsFoldersAdapter.kt b/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsFoldersAdapter.kt
index e3f70f7b..ceefc80a 100644
--- a/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsFoldersAdapter.kt
+++ b/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsFoldersAdapter.kt
@@ -1,6 +1,7 @@
 package com.readrops.api.services.nextcloudnews.adapters
 
 import android.annotation.SuppressLint
+import com.readrops.api.utils.exceptions.ParseException
 import com.readrops.db.entities.Folder
 import com.squareup.moshi.FromJson
 import com.squareup.moshi.JsonReader
@@ -16,32 +17,36 @@ class NextNewsFoldersAdapter {
     fun fromJson(reader: JsonReader): List<Folder> {
         val folders = mutableListOf<Folder>()
 
-        reader.beginObject()
-        reader.nextName() // "folders", beginning of folders array
-        reader.beginArray()
-
-        while (reader.hasNext()) {
-            val folder = Folder()
+        return try {
             reader.beginObject()
+            reader.nextName() // "folders", beginning of folders array
+            reader.beginArray()
 
             while (reader.hasNext()) {
-                with(folder) {
-                    when (reader.selectName(NAMES)) {
-                        0 -> remoteId = reader.nextInt().toString()
-                        1 -> name = reader.nextString()
-                        else -> reader.skipValue()
+                val folder = Folder()
+                reader.beginObject()
+
+                while (reader.hasNext()) {
+                    with(folder) {
+                        when (reader.selectName(NAMES)) {
+                            0 -> remoteId = reader.nextInt().toString()
+                            1 -> name = reader.nextString()
+                            else -> reader.skipValue()
+                        }
                     }
                 }
+
+                folders += folder
+                reader.endObject()
             }
 
-            folders += folder
+            reader.endArray()
             reader.endObject()
+
+            folders
+        } catch (e: Exception) {
+            throw ParseException(e.message)
         }
-
-        reader.endArray()
-        reader.endObject()
-
-        return folders
     }
 
     companion object {
diff --git a/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsItemsAdapter.kt b/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsItemsAdapter.kt
index 992a1e44..2667b46f 100644
--- a/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsItemsAdapter.kt
+++ b/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsItemsAdapter.kt
@@ -2,8 +2,10 @@ package com.readrops.api.services.nextcloudnews.adapters
 
 import android.annotation.SuppressLint
 import com.readrops.db.entities.Item
-import com.readrops.api.utils.LibUtils
-import com.readrops.api.utils.nextNullableString
+import com.readrops.api.utils.ApiUtils
+import com.readrops.api.utils.exceptions.ParseException
+import com.readrops.api.utils.extensions.nextNonEmptyString
+import com.readrops.api.utils.extensions.nextNullableString
 import com.squareup.moshi.JsonAdapter
 import com.squareup.moshi.JsonReader
 import com.squareup.moshi.JsonWriter
@@ -21,50 +23,56 @@ class NextNewsItemsAdapter : JsonAdapter<List<Item>>() {
     override fun fromJson(reader: JsonReader): List<Item> {
         val items = mutableListOf<Item>()
 
-        reader.beginObject()
-        reader.nextName() // "items", beginning of items array
-        reader.beginArray()
-
-        while (reader.hasNext()) {
-            val item = Item()
+        return try {
             reader.beginObject()
-
-            var enclosureMime: String? = null
-            var enclosureLink: String? = null
+            reader.nextName() // "items", beginning of items array
+            reader.beginArray()
 
             while (reader.hasNext()) {
-                with(item) {
-                    when (reader.selectName(NAMES)) {
-                        0 -> remoteId = reader.nextInt().toString()
-                        1 -> link = reader.nextNullableString()
-                        2 -> title = reader.nextString()
-                        3 -> author = reader.nextString()
-                        4 -> pubDate = LocalDateTime(reader.nextLong() * 1000L, DateTimeZone.getDefault())
-                        5 -> content = reader.nextString()
-                        6 -> enclosureMime = reader.nextNullableString()
-                        7 -> enclosureLink = reader.nextNullableString()
-                        8 -> feedRemoteId = reader.nextInt().toString()
-                        9 -> isRead = !reader.nextBoolean()
-                        else -> reader.skipValue()
+                val item = Item()
+                reader.beginObject()
+
+                var enclosureMime: String? = null
+                var enclosureLink: String? = null
+
+                while (reader.hasNext()) {
+                    with(item) {
+                        when (reader.selectName(NAMES)) {
+                            0 -> remoteId = reader.nextInt().toString()
+                            1 -> link = reader.nextNullableString()
+                            2 -> title = reader.nextString()
+                            3 -> author = reader.nextString()
+                            4 -> pubDate = LocalDateTime(reader.nextLong() * 1000L, DateTimeZone.getDefault())
+                            5 -> content = reader.nextString()
+                            6 -> enclosureMime = reader.nextNullableString()
+                            7 -> enclosureLink = reader.nextNullableString()
+                            8 -> feedRemoteId = reader.nextInt().toString()
+                            9 -> isRead = !reader.nextBoolean() // the negation is important here
+                            10 -> isStarred = reader.nextBoolean()
+                            11 -> guid = reader.nextNullableString()
+                            else -> reader.skipValue()
+                        }
                     }
                 }
+
+                if (enclosureMime != null && ApiUtils.isMimeImage(enclosureMime!!))
+                    item.imageLink = enclosureLink
+
+                items += item
+                reader.endObject()
             }
 
-            if (enclosureMime != null && LibUtils.isMimeImage(enclosureMime!!))
-                item.imageLink = enclosureLink
-
-            items += item
+            reader.endArray()
             reader.endObject()
+
+            items
+        } catch (e: Exception) {
+            throw ParseException(e.message)
         }
-
-        reader.endArray()
-        reader.endObject()
-
-        return items
     }
 
     companion object {
         val NAMES: JsonReader.Options = JsonReader.Options.of("id", "url", "title", "author",
-                "pubDate", "body", "enclosureMime", "enclosureLink", "feedId", "unread")
+                "pubDate", "body", "enclosureMime", "enclosureLink", "feedId", "unread", "starred", "guidHash")
     }
 }
\ No newline at end of file
diff --git a/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsUserAdapter.kt b/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsUserAdapter.kt
new file mode 100644
index 00000000..992db5ba
--- /dev/null
+++ b/api/src/main/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsUserAdapter.kt
@@ -0,0 +1,31 @@
+package com.readrops.api.services.nextcloudnews.adapters
+
+import com.gitlab.mvysny.konsumexml.allChildrenAutoIgnore
+import com.gitlab.mvysny.konsumexml.konsumeXml
+import com.readrops.api.localfeed.XmlAdapter
+import com.readrops.api.utils.exceptions.ParseException
+import com.readrops.api.utils.extensions.nonNullText
+import java.io.InputStream
+
+class NextNewsUserAdapter : XmlAdapter<String> {
+
+    override fun fromXml(inputStream: InputStream): String {
+        val konsumer = inputStream.konsumeXml()
+        var displayName: String? = null
+
+        return try {
+            konsumer.child("ocs") {
+                allChildrenAutoIgnore("data") {
+                    allChildrenAutoIgnore("displayname") {
+                        displayName = nonNullText()
+                    }
+                }
+            }
+
+            konsumer.close()
+            displayName!!
+        } catch (e: Exception) {
+            throw ParseException(e.message)
+        }
+    }
+}
\ No newline at end of file
diff --git a/api/src/main/java/com/readrops/api/services/nextcloudnews/json/NextNewsUser.kt b/api/src/main/java/com/readrops/api/services/nextcloudnews/json/NextNewsUser.kt
deleted file mode 100644
index bc50ae6f..00000000
--- a/api/src/main/java/com/readrops/api/services/nextcloudnews/json/NextNewsUser.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.readrops.api.services.nextcloudnews.json
-
-import com.squareup.moshi.JsonClass
-
-@JsonClass(generateAdapter = true)
-data class NextNewsUser(val userId: String,
-                        val displayName: String,
-                        val lastLoginTimestamp: Long,
-                        val avatar: Avatar?) {
-
-    @JsonClass(generateAdapter = true)
-    data class Avatar(val data: String,
-                      val mime: String)
-}
\ No newline at end of file
diff --git a/api/src/main/java/com/readrops/api/utils/ApiUtils.java b/api/src/main/java/com/readrops/api/utils/ApiUtils.java
new file mode 100644
index 00000000..65209555
--- /dev/null
+++ b/api/src/main/java/com/readrops/api/utils/ApiUtils.java
@@ -0,0 +1,53 @@
+package com.readrops.api.utils;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import org.jsoup.Jsoup;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public final class ApiUtils {
+
+    public static final String HTML_CONTENT_TYPE = "text/html";
+
+    public static final String CONTENT_TYPE_HEADER = "content-type";
+    public static final String ETAG_HEADER = "ETag";
+    public static final String IF_NONE_MATCH_HEADER = "If-None-Match";
+    public static final String LAST_MODIFIED_HEADER = "Last-Modified";
+    public static final String IF_MODIFIED_HEADER = "If-Modified-Since";
+
+    public static final int HTTP_UNPROCESSABLE = 422;
+    public static final int HTTP_NOT_FOUND = 404;
+    public static final int HTTP_CONFLICT = 409;
+
+    private static final String RSS_CONTENT_TYPE_REGEX = "([^;]+)";
+
+    public static boolean isMimeImage(@NonNull String type) {
+        return type.equals("image") || type.equals("image/jpeg") || type.equals("image/jpg")
+                || type.equals("image/png");
+    }
+
+    @Nullable
+    public static String parseContentType(String header) {
+        Matcher matcher = Pattern.compile(RSS_CONTENT_TYPE_REGEX)
+                .matcher(header);
+
+        if (matcher.find()) {
+            return matcher.group(0);
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Remove html tags and trim the text
+     *
+     * @param text string to clean
+     * @return cleaned text
+     */
+    public static String cleanText(String text) {
+        return Jsoup.parse(text).text().trim();
+    }
+}
diff --git a/api/src/main/java/com/readrops/api/utils/AuthInterceptor.kt b/api/src/main/java/com/readrops/api/utils/AuthInterceptor.kt
new file mode 100644
index 00000000..06f9e3df
--- /dev/null
+++ b/api/src/main/java/com/readrops/api/utils/AuthInterceptor.kt
@@ -0,0 +1,21 @@
+package com.readrops.api.utils
+
+import com.readrops.api.services.Credentials
+import okhttp3.Interceptor
+import okhttp3.Response
+import java.net.URI
+
+class AuthInterceptor(var credentials: Credentials? = null) : Interceptor {
+
+    override fun intercept(chain: Interceptor.Chain): Response {
+        val requestBuilder = chain.request().newBuilder()
+
+        credentials?.let {
+            if (it.authorization != null) {
+                requestBuilder.addHeader("Authorization", it.authorization)
+            }
+        }
+
+        return chain.proceed(requestBuilder.build())
+    }
+}
\ No newline at end of file
diff --git a/api/src/main/java/com/readrops/api/utils/DateUtils.java b/api/src/main/java/com/readrops/api/utils/DateUtils.java
new file mode 100644
index 00000000..f681d2ad
--- /dev/null
+++ b/api/src/main/java/com/readrops/api/utils/DateUtils.java
@@ -0,0 +1,76 @@
+package com.readrops.api.utils;
+
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import org.joda.time.LocalDateTime;
+import org.joda.time.format.DateTimeFormat;
+import org.joda.time.format.DateTimeFormatter;
+import org.joda.time.format.DateTimeFormatterBuilder;
+
+import java.util.Locale;
+
+public final class DateUtils {
+
+    private static final String TAG = DateUtils.class.getSimpleName();
+
+    /**
+     * Base of common RSS 2 date formats.
+     * Examples :
+     * Fri, 04 Jan 2019 22:21:46 GMT
+     * Fri, 04 Jan 2019 22:21:46 +0000
+     */
+    private static final String RSS_2_BASE_PATTERN = "EEE, dd MMM yyyy HH:mm:ss";
+
+    private static final String GMT_PATTERN = "ZZZ";
+
+    private static final String OFFSET_PATTERN = "Z";
+
+    private static final String ISO_PATTERN = ".SSSZZ";
+
+    private static final String EDT_PATTERN = "zzz";
+
+    /**
+     * Date pattern for format : 2019-01-04T22:21:46+00:00
+     */
+    private static final String ATOM_JSON_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss";
+
+    @Nullable
+    public static LocalDateTime parse(String value) {
+        if (value == null) {
+            return null;
+        }
+
+        try {
+            DateTimeFormatter formatter = new DateTimeFormatterBuilder()
+                    .appendOptional(DateTimeFormat.forPattern(RSS_2_BASE_PATTERN + " ").getParser()) // with timezone
+                    .appendOptional(DateTimeFormat.forPattern(RSS_2_BASE_PATTERN).getParser()) // no timezone, important order here
+                    .appendOptional(DateTimeFormat.forPattern(ATOM_JSON_DATE_FORMAT).getParser())
+                    .appendOptional(DateTimeFormat.forPattern(GMT_PATTERN).getParser())
+                    .appendOptional(DateTimeFormat.forPattern(OFFSET_PATTERN).getParser())
+                    .appendOptional(DateTimeFormat.forPattern(ISO_PATTERN).getParser())
+                    .appendOptional(DateTimeFormat.forPattern(EDT_PATTERN).getParser())
+                    .toFormatter()
+                    .withLocale(Locale.ENGLISH)
+                    .withOffsetParsed();
+
+            return formatter.parseLocalDateTime(value);
+        } catch (Exception e) {
+            Log.d(TAG, e.getMessage());
+            return null;
+        }
+    }
+
+    public static String formattedDateByLocal(LocalDateTime dateTime) {
+        return DateTimeFormat.mediumDate()
+                .withLocale(Locale.getDefault())
+                .print(dateTime);
+    }
+
+    public static String formattedDateTimeByLocal(LocalDateTime dateTime) {
+        return DateTimeFormat.forPattern("dd MMM yyyy · HH:mm")
+                .withLocale(Locale.getDefault())
+                .print(dateTime);
+    }
+}
diff --git a/api/src/main/java/com/readrops/api/utils/HttpManager.java b/api/src/main/java/com/readrops/api/utils/HttpManager.java
deleted file mode 100644
index 0f4605e0..00000000
--- a/api/src/main/java/com/readrops/api/utils/HttpManager.java
+++ /dev/null
@@ -1,77 +0,0 @@
-package com.readrops.api.utils;
-
-import androidx.annotation.Nullable;
-
-import com.readrops.api.services.Credentials;
-
-import java.io.IOException;
-import java.util.concurrent.TimeUnit;
-
-import okhttp3.Interceptor;
-import okhttp3.OkHttpClient;
-import okhttp3.Request;
-import okhttp3.Response;
-
-public class HttpManager {
-
-    private OkHttpClient okHttpClient;
-    private Credentials credentials;
-
-    public HttpManager() {
-        buildOkHttp();
-    }
-
-    private void buildOkHttp() {
-        okHttpClient = new OkHttpClient.Builder()
-                .callTimeout(1, TimeUnit.MINUTES)
-                .readTimeout(1, TimeUnit.HOURS)
-                .addInterceptor(new AuthInterceptor())
-                .build();
-    }
-
-    public OkHttpClient getOkHttpClient() {
-        return okHttpClient;
-    }
-
-    public void setCredentials(@Nullable Credentials credentials) {
-        this.credentials = credentials;
-    }
-
-    public Credentials getCredentials() {
-        return credentials;
-    }
-
-    private static HttpManager instance;
-
-    public static HttpManager getInstance() {
-        if (instance == null) {
-            instance = new HttpManager();
-        }
-
-        return instance;
-    }
-
-    public static void setInstance(OkHttpClient client) {
-        instance.okHttpClient = client;
-    }
-
-    public class AuthInterceptor implements Interceptor {
-
-        public AuthInterceptor() {
-            // empty constructor
-        }
-
-        @Override
-        public Response intercept(Chain chain) throws IOException {
-            Request request = chain.request();
-
-            if (credentials != null && credentials.getAuthorization() != null) {
-                request = request.newBuilder()
-                        .addHeader("Authorization", credentials.getAuthorization())
-                        .build();
-            }
-
-            return chain.proceed(request);
-        }
-    }
-}
diff --git a/api/src/main/java/com/readrops/api/utils/JsonReaderExtensions.kt b/api/src/main/java/com/readrops/api/utils/JsonReaderExtensions.kt
deleted file mode 100644
index f03553cb..00000000
--- a/api/src/main/java/com/readrops/api/utils/JsonReaderExtensions.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.readrops.api.utils
-
-import com.squareup.moshi.JsonReader
-
-fun JsonReader.nextNullableString(): String? =
-        if (peek() != JsonReader.Token.NULL) nextString() else nextNull()
-
-fun JsonReader.nextNullableInt(): Int? =
-        if (peek() != JsonReader.Token.NULL) nextInt() else nextNull()
\ No newline at end of file
diff --git a/api/src/main/java/com/readrops/api/utils/LibUtils.java b/api/src/main/java/com/readrops/api/utils/LibUtils.java
deleted file mode 100644
index d838e68e..00000000
--- a/api/src/main/java/com/readrops/api/utils/LibUtils.java
+++ /dev/null
@@ -1,47 +0,0 @@
-package com.readrops.api.utils;
-
-import android.content.Context;
-import android.net.Uri;
-
-import androidx.annotation.NonNull;
-
-import java.io.FileNotFoundException;
-import java.io.InputStream;
-import java.util.Scanner;
-
-public final class LibUtils {
-
-    public static final String RSS_DEFAULT_CONTENT_TYPE = "application/rss+xml";
-    public static final String RSS_TEXT_CONTENT_TYPE = "text/xml";
-    public static final String RSS_APPLICATION_CONTENT_TYPE = "application/xml";
-    public static final String ATOM_CONTENT_TYPE = "application/atom+xml";
-    public static final String JSON_CONTENT_TYPE = "application/json";
-    public static final String HTML_CONTENT_TYPE = "text/html";
-
-    public static final String CONTENT_TYPE_HEADER = "content-type";
-    public static final String ETAG_HEADER = "ETag";
-    public static final String IF_NONE_MATCH_HEADER = "If-None-Match";
-    public static final String LAST_MODIFIED_HEADER = "Last-Modified";
-    public static final String IF_MODIFIED_HEADER = "If-Modified-Since";
-
-    public static final int HTTP_UNPROCESSABLE = 422;
-    public static final int HTTP_NOT_FOUND = 404;
-    public static final int HTTP_CONFLICT = 409;
-
-
-    public static String inputStreamToString(InputStream input) {
-        Scanner scanner = new Scanner(input).useDelimiter("\\A");
-        return scanner.hasNext() ? scanner.next() : "";
-    }
-
-    public static String fileToString(Uri uri, Context context) throws FileNotFoundException {
-        InputStream  inputStream = context.getContentResolver().openInputStream(uri);
-
-        return inputStreamToString(inputStream);
-    }
-
-    public static boolean isMimeImage(@NonNull String type) {
-        return type.equals("image") || type.equals("image/jpeg") || type.equals("image/jpg")
-                || type.equals("image/png");
-    }
-}
diff --git a/api/src/main/java/com/readrops/api/utils/ParseException.java b/api/src/main/java/com/readrops/api/utils/ParseException.java
deleted file mode 100644
index 12db280c..00000000
--- a/api/src/main/java/com/readrops/api/utils/ParseException.java
+++ /dev/null
@@ -1,5 +0,0 @@
-package com.readrops.api.utils;
-
-public class ParseException extends Exception {
-
-}
diff --git a/api/src/main/java/com/readrops/api/utils/ConflictException.java b/api/src/main/java/com/readrops/api/utils/exceptions/ConflictException.java
similarity index 80%
rename from api/src/main/java/com/readrops/api/utils/ConflictException.java
rename to api/src/main/java/com/readrops/api/utils/exceptions/ConflictException.java
index 83467a44..eda1d4d6 100644
--- a/api/src/main/java/com/readrops/api/utils/ConflictException.java
+++ b/api/src/main/java/com/readrops/api/utils/exceptions/ConflictException.java
@@ -1,4 +1,4 @@
-package com.readrops.api.utils;
+package com.readrops.api.utils.exceptions;
 
 public class ConflictException extends Exception {
 
diff --git a/api/src/main/java/com/readrops/api/utils/exceptions/ParseException.java b/api/src/main/java/com/readrops/api/utils/exceptions/ParseException.java
new file mode 100644
index 00000000..9ff5e50d
--- /dev/null
+++ b/api/src/main/java/com/readrops/api/utils/exceptions/ParseException.java
@@ -0,0 +1,12 @@
+package com.readrops.api.utils.exceptions;
+
+public class ParseException extends Exception {
+
+    public ParseException() {
+        super();
+    }
+
+    public ParseException(String message) {
+        super(message);
+    }
+}
diff --git a/api/src/main/java/com/readrops/api/utils/UnknownFormatException.java b/api/src/main/java/com/readrops/api/utils/exceptions/UnknownFormatException.java
similarity index 81%
rename from api/src/main/java/com/readrops/api/utils/UnknownFormatException.java
rename to api/src/main/java/com/readrops/api/utils/exceptions/UnknownFormatException.java
index 3e76fe56..8e6da51b 100644
--- a/api/src/main/java/com/readrops/api/utils/UnknownFormatException.java
+++ b/api/src/main/java/com/readrops/api/utils/exceptions/UnknownFormatException.java
@@ -1,4 +1,4 @@
-package com.readrops.api.utils;
+package com.readrops.api.utils.exceptions;
 
 public class UnknownFormatException extends Exception {
 
diff --git a/api/src/main/java/com/readrops/api/utils/extensions/JsonReaderExtensions.kt b/api/src/main/java/com/readrops/api/utils/extensions/JsonReaderExtensions.kt
new file mode 100644
index 00000000..9eb49679
--- /dev/null
+++ b/api/src/main/java/com/readrops/api/utils/extensions/JsonReaderExtensions.kt
@@ -0,0 +1,15 @@
+package com.readrops.api.utils.extensions
+
+import com.readrops.api.utils.exceptions.ParseException
+import com.squareup.moshi.JsonReader
+
+fun JsonReader.nextNullableString(): String? =
+        if (peek() != JsonReader.Token.NULL) nextString().ifEmpty { null }?.trim() else nextNull()
+
+fun JsonReader.nextNonEmptyString(): String {
+    val text = nextString()
+    return if (text.isNotEmpty()) text.trim() else throw ParseException("Json value can't be null")
+}
+
+fun JsonReader.nextNullableInt(): Int? =
+        if (peek() != JsonReader.Token.NULL) nextInt() else nextNull()
diff --git a/api/src/main/java/com/readrops/api/utils/extensions/KonsumerExtensions.kt b/api/src/main/java/com/readrops/api/utils/extensions/KonsumerExtensions.kt
new file mode 100644
index 00000000..25a8ba81
--- /dev/null
+++ b/api/src/main/java/com/readrops/api/utils/extensions/KonsumerExtensions.kt
@@ -0,0 +1,21 @@
+package com.readrops.api.utils.extensions
+
+import com.gitlab.mvysny.konsumexml.Konsumer
+import com.gitlab.mvysny.konsumexml.Whitespace
+import com.gitlab.mvysny.konsumexml.textRecursively
+import com.readrops.api.utils.exceptions.ParseException
+
+fun Konsumer.nonNullText(): String {
+    val text = text(whitespace = Whitespace.preserve)
+    return if (text.isNotEmpty()) text.trim() else throw ParseException("$name text can't be null")
+}
+
+fun Konsumer.nullableText(): String? {
+    val text = text(whitespace = Whitespace.preserve)
+    return if (text.isNotEmpty()) text.trim() else null
+}
+
+fun Konsumer.nullableTextRecursively(): String? {
+    val text = textRecursively()
+    return if (text.isNotEmpty()) text.trim() else null
+}
\ No newline at end of file
diff --git a/api/src/test/java/com/readrops/api/ExampleUnitTest.java b/api/src/test/java/com/readrops/api/ExampleUnitTest.java
deleted file mode 100644
index 79e5f257..00000000
--- a/api/src/test/java/com/readrops/api/ExampleUnitTest.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.readrops.api;
-
-import org.junit.Test;
-
-import static org.junit.Assert.*;
-
-/**
- * Example local unit test, which will execute on the development machine (host).
- *
- * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
- */
-public class ExampleUnitTest {
-    @Test
-    public void addition_isCorrect() {
-        assertEquals(4, 2 + 2);
-    }
-}
\ No newline at end of file
diff --git a/api/src/test/java/com/readrops/api/TestUtils.kt b/api/src/test/java/com/readrops/api/TestUtils.kt
new file mode 100644
index 00000000..dac26c4c
--- /dev/null
+++ b/api/src/test/java/com/readrops/api/TestUtils.kt
@@ -0,0 +1,9 @@
+package com.readrops.api
+
+import java.io.InputStream
+
+object TestUtils {
+
+    fun loadResource(path: String): InputStream =
+        javaClass.classLoader?.getResourceAsStream(path)!!
+}
\ No newline at end of file
diff --git a/api/src/test/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt b/api/src/test/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt
new file mode 100644
index 00000000..495ba5e3
--- /dev/null
+++ b/api/src/test/java/com/readrops/api/localfeed/LocalRSSDataSourceTest.kt
@@ -0,0 +1,206 @@
+package com.readrops.api.localfeed
+
+import android.accounts.NetworkErrorException
+import com.readrops.api.TestUtils
+import com.readrops.api.apiModule
+import com.readrops.api.utils.ApiUtils
+import com.readrops.api.utils.AuthInterceptor
+import com.readrops.api.utils.exceptions.ParseException
+import com.readrops.api.utils.exceptions.UnknownFormatException
+import junit.framework.TestCase.*
+import okhttp3.Headers
+import okhttp3.HttpUrl
+import okhttp3.OkHttpClient
+import okhttp3.mockwebserver.MockResponse
+import okhttp3.mockwebserver.MockWebServer
+import okio.Buffer
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.koin.dsl.module
+import org.koin.test.KoinTest
+import org.koin.test.KoinTestRule
+import org.koin.test.inject
+import java.net.HttpURLConnection
+import java.util.concurrent.TimeUnit
+
+
+class LocalRSSDataSourceTest : KoinTest {
+
+    private lateinit var url: HttpUrl
+
+    private val mockServer: MockWebServer = MockWebServer()
+    private val localRSSDataSource by inject<LocalRSSDataSource>()
+
+    @get:Rule
+    val koinTestRule = KoinTestRule.create {
+        modules(apiModule, module {
+            single(override = true) {
+                OkHttpClient.Builder()
+                        .callTimeout(1, TimeUnit.MINUTES)
+                        .readTimeout(1, TimeUnit.HOURS)
+                        .addInterceptor(get<AuthInterceptor>())
+                        .build()
+            }
+        })
+    }
+
+    @Before
+    fun before() {
+        mockServer.start(8080)
+        url = mockServer.url("/rss")
+    }
+
+    @After
+    fun tearDown() {
+        mockServer.close()
+    }
+
+    @Test
+    fun successfulQueryTest() {
+        val stream = TestUtils.loadResource("localfeed/rss_feed.xml")
+
+        mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK)
+                .addHeader(ApiUtils.CONTENT_TYPE_HEADER, "application/xml; charset=UTF-8")
+                .addHeader(ApiUtils.ETAG_HEADER, "ETag-value")
+                .addHeader(ApiUtils.LAST_MODIFIED_HEADER, "Last-Modified")
+                .setBody(Buffer().readFrom(stream)))
+
+        val pair = localRSSDataSource.queryRSSResource(url.toString(), null)
+        val feed = pair?.first!!
+
+        assertEquals(feed.name, "Hacker News")
+        assertEquals(feed.url, "http://localhost:8080/rss")
+        assertEquals(feed.siteUrl, "https://news.ycombinator.com/")
+        assertEquals(feed.description, "Links for the intellectually curious, ranked by readers.")
+
+        assertEquals(feed.etag, "ETag-value")
+        assertEquals(feed.lastModified, "Last-Modified")
+
+        assertEquals(pair.second.size, 7)
+    }
+
+    @Test
+    fun headersTest() {
+        val stream = TestUtils.loadResource("localfeed/rss_feed.xml")
+
+        mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK)
+                .addHeader("Content-Type", "application/rss+xml; charset=UTF-8")
+                .setBody(Buffer().readFrom(stream)))
+
+        val headers = Headers.headersOf(ApiUtils.ETAG_HEADER, "ETag", ApiUtils.LAST_MODIFIED_HEADER, "Last-Modified")
+        localRSSDataSource.queryRSSResource(url.toString(), headers)
+
+        val request = mockServer.takeRequest()
+
+        assertEquals(request.headers[ApiUtils.ETAG_HEADER], "ETag")
+        assertEquals(request.headers[ApiUtils.LAST_MODIFIED_HEADER], "Last-Modified")
+    }
+
+    @Test
+    fun jsonFeedTest() {
+        val stream = TestUtils.loadResource("localfeed/json/json_feed.json")
+
+        mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK)
+                .addHeader(ApiUtils.CONTENT_TYPE_HEADER, "application/feed+json")
+                .setBody(Buffer().readFrom(stream)))
+
+        val pair = localRSSDataSource.queryRSSResource(url.toString(), null)!!
+
+        assertEquals(pair.first.name, "News from Flying Meat")
+        assertEquals(pair.second.size, 10)
+    }
+
+    @Test
+    fun specialCasesAtomTest() {
+        val stream = TestUtils.loadResource("localfeed/atom/atom_feed_no_url_siteurl.xml")
+
+        mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK)
+                .addHeader(ApiUtils.CONTENT_TYPE_HEADER, "application/atom+xml")
+                .setBody(Buffer().readFrom(stream)))
+
+        val pair = localRSSDataSource.queryRSSResource(url.toString(), null)!!
+
+        assertEquals(pair.first.url, "http://localhost:8080/rss")
+        assertEquals(pair.first.siteUrl, "http://localhost")
+    }
+
+    @Test
+    fun specialCasesRSS1Test() {
+        val stream = TestUtils.loadResource("localfeed/rss1/rss1_feed_no_url_siteurl.xml")
+
+        mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK)
+                .addHeader(ApiUtils.CONTENT_TYPE_HEADER, "application/rdf+xml")
+                .setBody(Buffer().readFrom(stream)))
+
+        val pair = localRSSDataSource.queryRSSResource(url.toString(), null)!!
+
+        assertEquals(pair.first.url, "http://localhost:8080/rss")
+        assertEquals(pair.first.siteUrl, "http://localhost")
+    }
+
+    @Test
+    fun response304Test() {
+        mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED))
+
+        val pair = localRSSDataSource.queryRSSResource(url.toString(), null)
+
+        assertNull(pair)
+    }
+
+    @Test(expected = NetworkErrorException::class)
+    fun response404Test() {
+        mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND))
+
+        localRSSDataSource.queryRSSResource(url.toString(), null)
+    }
+
+    @Test(expected = UnknownFormatException::class)
+    fun noContentTypeTest() {
+        mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK))
+
+        localRSSDataSource.queryRSSResource(url.toString(), null)
+    }
+
+    @Test(expected = ParseException::class)
+    fun badContentTypeTest() {
+        mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK)
+                .addHeader("Content-Type", ""))
+
+        localRSSDataSource.queryRSSResource(url.toString(), null)
+    }
+
+    @Test(expected = UnknownFormatException::class)
+    fun badContentTest() {
+        mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK)
+                .addHeader("Content-Type", "application/xml")
+                .setBody("<html>  </html>"))
+
+        localRSSDataSource.queryRSSResource(url.toString(), null)
+    }
+
+    @Test
+    fun isUrlResourceSuccessfulTest() {
+        mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK)
+                .addHeader("Content-Type", "application/atom+xml; charset=UTF-8"))
+
+        assertTrue(localRSSDataSource.isUrlRSSResource(url.toString()))
+    }
+
+    @Test
+    fun isUrlRSSResourceFailureTest() {
+        mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND))
+
+        assertFalse(localRSSDataSource.isUrlRSSResource(url.toString()))
+    }
+
+    @Test
+    fun isUrlRSSResourceBadContentTypeTest() {
+        mockServer.enqueue(MockResponse().setResponseCode(HttpURLConnection.HTTP_OK)
+                .addHeader("Content-Type", "application/xml; charset=UTF-8")
+                .setBody("<html> </html>"))
+
+        assertFalse(localRSSDataSource.isUrlRSSResource(url.toString()))
+    }
+}
\ No newline at end of file
diff --git a/api/src/test/java/com/readrops/api/localfeed/LocalRSSHelperTest.kt b/api/src/test/java/com/readrops/api/localfeed/LocalRSSHelperTest.kt
new file mode 100644
index 00000000..74414ce6
--- /dev/null
+++ b/api/src/test/java/com/readrops/api/localfeed/LocalRSSHelperTest.kt
@@ -0,0 +1,91 @@
+package com.readrops.api.localfeed
+
+import junit.framework.TestCase.*
+import org.junit.Test
+import java.io.ByteArrayInputStream
+
+class LocalRSSHelperTest {
+
+    @Test
+    fun standardContentTypesTest() {
+        assertEquals(LocalRSSHelper.getRSSType("application/rdf+xml"),
+                LocalRSSHelper.RSSType.RSS_1)
+        assertEquals(LocalRSSHelper.getRSSType("application/rss+xml"),
+                LocalRSSHelper.RSSType.RSS_2)
+        assertEquals(LocalRSSHelper.getRSSType("application/atom+xml"),
+                LocalRSSHelper.RSSType.ATOM)
+        assertEquals(LocalRSSHelper.getRSSType("application/json"),
+                LocalRSSHelper.RSSType.JSONFEED)
+        assertEquals(LocalRSSHelper.getRSSType("application/feed+json"),
+                LocalRSSHelper.RSSType.JSONFEED)
+    }
+
+    @Test
+    fun nonSupportedContentTypesTest() {
+        assertEquals(LocalRSSHelper.getRSSType("application/xml"),
+                LocalRSSHelper.RSSType.UNKNOWN)
+        assertEquals(LocalRSSHelper.getRSSType("text/xml"),
+                LocalRSSHelper.RSSType.UNKNOWN)
+        assertEquals(LocalRSSHelper.getRSSType("text/html"),
+                LocalRSSHelper.RSSType.UNKNOWN)
+    }
+
+    @Test
+    fun rss1ContentTest() {
+        assertEquals(LocalRSSHelper.getRSSContentType(ByteArrayInputStream(
+                """<?xml-stylesheet type="text/xsl" media="screen" href="/~d/styles/rss1full.xsl"?>
+<?xml-stylesheet type="text/css" media="screen" href="http://rss.slashdot.org/~d/styles/itemcontent.css"?>
+<rdf:RDF xmlns:admin="http://webns.net/mvcb/" xmlns:content="http://purl.org/rss/1.0/modules/content/" 
+xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://purl.org/rss/1.0/"
+                """.trimIndent().toByteArray()
+        )), LocalRSSHelper.RSSType.RSS_1)
+    }
+
+
+    @Test
+    fun rss2ContentTest() {
+        assertEquals(LocalRSSHelper.getRSSContentType(ByteArrayInputStream(
+                """<rss 
+                    xmlns:content="http://purl.org/rss/1.0/modules/content/"
+                    xmlns:wfw="http://wellformedweb.org/CommentAPI/"
+                    xmlns:dc="http://purl.org/dc/elements/1.1/"
+                    version="2.0"
+                    xmlns:atom="http://www.w3.org/2005/Atom"
+                    xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
+                    xmlns:slash="http://purl.org/rss/1.0/modules/slash/">
+                </rss>""".toByteArray()
+        )), LocalRSSHelper.RSSType.RSS_2)
+    }
+
+    @Test
+    fun atomContentTest() {
+        assertEquals(LocalRSSHelper.getRSSContentType(ByteArrayInputStream(
+                """<feed     xmlns="http://www.w3.org/2005/Atom">
+                    
+                </feed>""".toByteArray()
+        )), LocalRSSHelper.RSSType.ATOM)
+    }
+
+    @Test
+    fun unknownContentTest() {
+        assertEquals(LocalRSSHelper.getRSSContentType(ByteArrayInputStream(
+                """<html>
+                        <body>
+                        </body>
+               </html>""".trimMargin().toByteArray()
+        )), LocalRSSHelper.RSSType.UNKNOWN)
+
+    }
+
+    @Test
+    fun isRSSTypeTest() {
+        assertTrue(LocalRSSHelper.isRSSType("application/rss+xml"))
+    }
+
+    @Test
+    fun isRSSTypeNullCaseTest() {
+        assertFalse(LocalRSSHelper.isRSSType(null))
+    }
+
+
+}
\ No newline at end of file
diff --git a/api/src/test/java/com/readrops/api/localfeed/XmlAdapterTest.kt b/api/src/test/java/com/readrops/api/localfeed/XmlAdapterTest.kt
new file mode 100644
index 00000000..bc3f729d
--- /dev/null
+++ b/api/src/test/java/com/readrops/api/localfeed/XmlAdapterTest.kt
@@ -0,0 +1,38 @@
+package com.readrops.api.localfeed
+
+import com.readrops.api.localfeed.atom.ATOMFeedAdapter
+import com.readrops.api.localfeed.atom.ATOMItemsAdapter
+import com.readrops.api.localfeed.rss1.RSS1FeedAdapter
+import com.readrops.api.localfeed.rss1.RSS1ItemsAdapter
+import com.readrops.api.localfeed.rss2.RSS2FeedAdapter
+import com.readrops.api.localfeed.rss2.RSS2ItemsAdapter
+import junit.framework.TestCase.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.ExpectedException
+
+class XmlAdapterTest {
+
+    @get:Rule
+    val expectedException: ExpectedException = ExpectedException.none()
+
+    @Test
+    fun xmlFeedAdapterFactoryTest() {
+        assertTrue(XmlAdapter.xmlFeedAdapterFactory(LocalRSSHelper.RSSType.RSS_1) is RSS1FeedAdapter)
+        assertTrue(XmlAdapter.xmlFeedAdapterFactory(LocalRSSHelper.RSSType.RSS_2) is RSS2FeedAdapter)
+        assertTrue(XmlAdapter.xmlFeedAdapterFactory(LocalRSSHelper.RSSType.ATOM) is ATOMFeedAdapter)
+
+        expectedException.expect(IllegalArgumentException::class.java)
+        XmlAdapter.xmlFeedAdapterFactory(LocalRSSHelper.RSSType.UNKNOWN)
+    }
+
+    @Test
+    fun xmlItemsAdapterFactoryTest() {
+        assertTrue(XmlAdapter.xmlItemsAdapterFactory(LocalRSSHelper.RSSType.RSS_1) is RSS1ItemsAdapter)
+        assertTrue(XmlAdapter.xmlItemsAdapterFactory(LocalRSSHelper.RSSType.RSS_2) is RSS2ItemsAdapter)
+        assertTrue(XmlAdapter.xmlItemsAdapterFactory(LocalRSSHelper.RSSType.ATOM) is ATOMItemsAdapter)
+
+        expectedException.expect(IllegalArgumentException::class.java)
+        XmlAdapter.xmlItemsAdapterFactory(LocalRSSHelper.RSSType.UNKNOWN)
+    }
+}
\ No newline at end of file
diff --git a/api/src/test/java/com/readrops/api/localfeed/atom/ATOMFeedAdapterTest.kt b/api/src/test/java/com/readrops/api/localfeed/atom/ATOMFeedAdapterTest.kt
new file mode 100644
index 00000000..18cb0686
--- /dev/null
+++ b/api/src/test/java/com/readrops/api/localfeed/atom/ATOMFeedAdapterTest.kt
@@ -0,0 +1,22 @@
+package com.readrops.api.localfeed.atom
+
+import com.readrops.api.TestUtils
+import junit.framework.TestCase.assertEquals
+import org.junit.Test
+
+class ATOMFeedAdapterTest {
+
+    private val adapter = ATOMFeedAdapter()
+
+    @Test
+    fun normalCasesTest() {
+        val stream = TestUtils.loadResource("localfeed/atom/atom_feed.xml")
+
+        val feed = adapter.fromXml(stream)
+
+        assertEquals(feed.name, "Recent Commits to Readrops:develop")
+        assertEquals(feed.url, "https://github.com/readrops/Readrops/commits/develop.atom")
+        assertEquals(feed.siteUrl, "https://github.com/readrops/Readrops/commits/develop")
+        assertEquals(feed.description, "Here is a subtitle")
+    }
+}
\ No newline at end of file
diff --git a/api/src/test/java/com/readrops/api/localfeed/atom/ATOMItemsAdapterTest.kt b/api/src/test/java/com/readrops/api/localfeed/atom/ATOMItemsAdapterTest.kt
new file mode 100644
index 00000000..6b014126
--- /dev/null
+++ b/api/src/test/java/com/readrops/api/localfeed/atom/ATOMItemsAdapterTest.kt
@@ -0,0 +1,64 @@
+package com.readrops.api.localfeed.atom
+
+import com.readrops.api.TestUtils
+import com.readrops.api.utils.DateUtils
+import com.readrops.api.utils.exceptions.ParseException
+import junit.framework.TestCase.assertEquals
+import junit.framework.TestCase.assertNotNull
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.ExpectedException
+
+class ATOMItemsAdapterTest {
+
+    private val adapter = ATOMItemsAdapter()
+
+    @get:Rule
+    val expectedException: ExpectedException = ExpectedException.none()
+
+    @Test
+    fun normalCasesTest() {
+        val stream = TestUtils.loadResource("localfeed/atom/atom_items.xml")
+
+        val items = adapter.fromXml(stream)
+        val item = items[0]
+
+        assertEquals(items.size, 4)
+        assertEquals(item.title, "Add an option to open item url in custom tab")
+        assertEquals(item.link, "https://github.com/readrops/Readrops/commit/c15f093a1bc4211e85f8d1817c9073e307afe5ac")
+        assertEquals(item.pubDate, DateUtils.parse("2020-09-06T21:09:59Z"))
+        assertEquals(item.author, "Shinokuni")
+        assertEquals(item.description, "Summary")
+        assertEquals(item.guid, "tag:github.com,2008:Grit::Commit/c15f093a1bc4211e85f8d1817c9073e307afe5ac")
+        assertNotNull(item.content)
+    }
+
+    @Test
+    fun noDateTest() {
+        val stream = TestUtils.loadResource("localfeed/atom/atom_items_no_date.xml")
+
+        val item = adapter.fromXml(stream).first()
+        assertNotNull(item.pubDate)
+    }
+
+    @Test
+    fun noTitleTest() {
+        val stream = TestUtils.loadResource("localfeed/atom/atom_items_no_title.xml")
+
+        expectedException.expect(ParseException::class.java)
+        expectedException.expectMessage("Item title is required")
+
+        adapter.fromXml(stream)
+    }
+
+    @Test
+    fun noLinkTest() {
+        val stream = TestUtils.loadResource("localfeed/atom/atom_items_no_link.xml")
+
+        expectedException.expect(ParseException::class.java)
+        expectedException.expectMessage("Item link is required")
+
+        adapter.fromXml(stream)
+    }
+
+}
\ No newline at end of file
diff --git a/api/src/test/java/com/readrops/api/localfeed/json/JSONFeedAdapterTest.kt b/api/src/test/java/com/readrops/api/localfeed/json/JSONFeedAdapterTest.kt
new file mode 100644
index 00000000..bece7e27
--- /dev/null
+++ b/api/src/test/java/com/readrops/api/localfeed/json/JSONFeedAdapterTest.kt
@@ -0,0 +1,29 @@
+package com.readrops.api.localfeed.json
+
+import com.readrops.api.TestUtils
+import com.readrops.db.entities.Feed
+import com.squareup.moshi.Moshi
+import junit.framework.TestCase.assertEquals
+import okio.Buffer
+import org.junit.Test
+
+class JSONFeedAdapterTest {
+
+    private val adapter = Moshi.Builder()
+            .add(JSONFeedAdapter())
+            .build()
+            .adapter(Feed::class.java)
+
+    @Test
+    fun normalCasesTest() {
+        val stream = TestUtils.loadResource("localfeed/json/json_feed.json")
+
+        val feed = adapter.fromJson(Buffer().readFrom(stream))!!
+
+        assertEquals(feed.name, "News from Flying Meat")
+        assertEquals(feed.url, "http://flyingmeat.com/blog/feed.json")
+        assertEquals(feed.siteUrl, "http://flyingmeat.com/blog/")
+        assertEquals(feed.description, "News from your friends at Flying Meat.")
+    }
+
+}
\ No newline at end of file
diff --git a/api/src/test/java/com/readrops/api/localfeed/json/JSONItemsAdapterTest.kt b/api/src/test/java/com/readrops/api/localfeed/json/JSONItemsAdapterTest.kt
new file mode 100644
index 00000000..518520c2
--- /dev/null
+++ b/api/src/test/java/com/readrops/api/localfeed/json/JSONItemsAdapterTest.kt
@@ -0,0 +1,83 @@
+package com.readrops.api.localfeed.json
+
+import com.readrops.api.TestUtils
+import com.readrops.api.utils.DateUtils
+import com.readrops.api.utils.exceptions.ParseException
+import com.readrops.db.entities.Item
+import com.squareup.moshi.Moshi
+import com.squareup.moshi.Types
+import junit.framework.TestCase.assertEquals
+import junit.framework.TestCase.assertNotNull
+import okio.Buffer
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.ExpectedException
+
+
+class JSONItemsAdapterTest {
+
+    private val adapter = Moshi.Builder()
+            .add(Types.newParameterizedType(List::class.java, Item::class.java), JSONItemsAdapter())
+            .build()
+            .adapter<List<Item>>(Types.newParameterizedType(List::class.java, Item::class.java))
+
+    @get:Rule
+    val expectedException: ExpectedException = ExpectedException.none()
+
+    @Test
+    fun normalCasesTest() {
+        val stream = TestUtils.loadResource("localfeed/json/json_feed.json")
+
+        val items = adapter.fromJson(Buffer().readFrom(stream))!!
+        val item = items.first()
+
+        assertEquals(items.size, 10)
+        assertEquals(item.guid, "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html")
+        assertEquals(item.title, "Acorn and 10.13")
+        assertEquals(item.link, "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html")
+        assertEquals(item.pubDate, DateUtils.parse("2017-09-25T14:27:27-07:00"))
+        assertEquals(item.author, "Author 1")
+        assertNotNull(item.content)
+    }
+
+    @Test
+    fun otherCasesTest() {
+        val stream = TestUtils.loadResource("localfeed/json/json_items_other_cases.json")
+
+        val item = adapter.fromJson(Buffer().readFrom(stream))!!.first()
+
+        assertEquals(item.description, "This is a summary")
+        assertEquals(item.content, "content_html")
+        assertEquals(item.imageLink, "https://image.com")
+        assertEquals(item.author, "Author 1, Author 3, Author 4, Author 5, ...")
+    }
+
+    @Test
+    fun nullDateTest() {
+        val stream = TestUtils.loadResource("localfeed/json/json_items_no_date.json")
+
+        val item = adapter.fromJson(Buffer().readFrom(stream))!!.first()
+        assertNotNull(item.pubDate)
+    }
+
+    @Test
+    fun nullTitleTest() {
+        val stream = TestUtils.loadResource("localfeed/json/json_items_no_title.json")
+
+        expectedException.expect(ParseException::class.java)
+        expectedException.expectMessage("Item title is required")
+
+        adapter.fromJson(Buffer().readFrom(stream))
+    }
+
+    @Test
+    fun nullLinkTest() {
+        val stream = TestUtils.loadResource("localfeed/json/json_items_no_link.json")
+
+        expectedException.expect(ParseException::class.java)
+        expectedException.expectMessage("Item link is required")
+
+        adapter.fromJson(Buffer().readFrom(stream))
+    }
+
+}
\ No newline at end of file
diff --git a/api/src/test/java/com/readrops/api/localfeed/rss1/RSS1FeedAdapterTest.kt b/api/src/test/java/com/readrops/api/localfeed/rss1/RSS1FeedAdapterTest.kt
new file mode 100644
index 00000000..ddee8ead
--- /dev/null
+++ b/api/src/test/java/com/readrops/api/localfeed/rss1/RSS1FeedAdapterTest.kt
@@ -0,0 +1,22 @@
+package com.readrops.api.localfeed.rss1
+
+import com.readrops.api.TestUtils
+import junit.framework.Assert.assertEquals
+import org.junit.Test
+
+class RSS1FeedAdapterTest {
+
+    private val adapter = RSS1FeedAdapter()
+
+    @Test
+    fun normalCaseTest() {
+        val stream = TestUtils.loadResource("localfeed/rss1/rss1_feed.xml")
+
+        val feed = adapter.fromXml(stream)
+
+        assertEquals(feed.name, "Slashdot")
+        assertEquals(feed.url, "https://slashdot.org/")
+        assertEquals(feed.siteUrl, "https://slashdot.org/")
+        assertEquals(feed.description, "News for nerds, stuff that matters")
+    }
+}
\ No newline at end of file
diff --git a/api/src/test/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapterTest.kt b/api/src/test/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapterTest.kt
new file mode 100644
index 00000000..7efda78f
--- /dev/null
+++ b/api/src/test/java/com/readrops/api/localfeed/rss1/RSS1ItemsAdapterTest.kt
@@ -0,0 +1,76 @@
+package com.readrops.api.localfeed.rss1
+
+import com.readrops.api.TestUtils
+import com.readrops.api.utils.DateUtils
+import com.readrops.api.utils.exceptions.ParseException
+import junit.framework.TestCase.assertEquals
+import junit.framework.TestCase.assertNotNull
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.ExpectedException
+
+class RSS1ItemsAdapterTest {
+
+    private val adapter = RSS1ItemsAdapter()
+
+    @get:Rule
+    val expectedException: ExpectedException = ExpectedException.none()
+
+    @Test
+    fun normalCasesTest() {
+        val stream = TestUtils.loadResource("localfeed/rss1/rss1_feed.xml")
+
+        val items = adapter.fromXml(stream)
+        val item = items.first()
+
+        assertEquals(items.size, 4)
+        assertEquals(item.title, "Google Expands its Flutter Development Kit To Windows Apps")
+        assertEquals(item.link.trim(), "https://developers.slashdot.org/story/20/09/23/1616231/google-expands-" +
+                "its-flutter-development-kit-to-windows-apps?utm_source=rss1.0mainlinkanon&utm_medium=feed")
+        assertEquals(item.guid.trim(), "https://developers.slashdot.org/story/20/09/23/1616231/google-expands-" +
+                "its-flutter-development-kit-to-windows-apps?utm_source=rss1.0mainlinkanon&utm_medium=feed")
+        assertEquals(item.pubDate, DateUtils.parse("2020-09-23T16:15:00+00:00"))
+        assertEquals(item.author, "msmash")
+        assertNotNull(item.description)
+        assertEquals(item.content, "content:encoded")
+    }
+
+    @Test
+    fun specialCasesTest() {
+        val stream = TestUtils.loadResource("localfeed/rss1/rss1_items_special_cases.xml")
+
+        val item = adapter.fromXml(stream).first()
+
+        assertEquals(item.author, "msmash, creator 2, creator 3, creator 4, ...")
+        assertEquals(item.link, "https://news.slashdot.org/story/20/09/23/1420240/a-new-york-clock-" +
+                "that-told-time-now-tells-the-time-remaining?utm_source=rss1.0mainlinkanon&utm_medium=feed")
+    }
+
+    @Test
+    fun nullDateTest() {
+        val stream = TestUtils.loadResource("localfeed/rss1/rss1_items_no_date.xml")
+
+        val item = adapter.fromXml(stream).first()
+        assertNotNull(item.pubDate)
+    }
+
+    @Test
+    fun nullTitleTest() {
+        val stream = TestUtils.loadResource("localfeed/rss1/rss1_items_no_title.xml")
+
+        expectedException.expect(ParseException::class.java)
+        expectedException.expectMessage("Item title is required")
+
+        adapter.fromXml(stream)
+    }
+
+    @Test
+    fun nullLinkTest() {
+        val stream = TestUtils.loadResource("localfeed/rss1/rss1_items_no_link.xml")
+
+        expectedException.expect(ParseException::class.java)
+        expectedException.expectMessage("RSS1 link or about element is required")
+
+        adapter.fromXml(stream)
+    }
+}
\ No newline at end of file
diff --git a/api/src/test/java/com/readrops/api/localfeed/rss2/RSS2FeedAdapterTest.kt b/api/src/test/java/com/readrops/api/localfeed/rss2/RSS2FeedAdapterTest.kt
new file mode 100644
index 00000000..c16a1581
--- /dev/null
+++ b/api/src/test/java/com/readrops/api/localfeed/rss2/RSS2FeedAdapterTest.kt
@@ -0,0 +1,31 @@
+package com.readrops.api.localfeed.rss2
+
+import com.readrops.api.TestUtils
+import com.readrops.api.utils.exceptions.ParseException
+import junit.framework.TestCase.assertEquals
+import org.junit.Test
+
+class RSS2FeedAdapterTest {
+
+
+    private val adapter = RSS2FeedAdapter()
+
+    @Test
+    fun normalCasesTest() {
+        val stream = TestUtils.loadResource("localfeed/rss2/rss_full_feed.xml")
+
+        val feed = adapter.fromXml(stream)
+
+        assertEquals(feed.name, "Hacker News")
+        assertEquals(feed.url, "https://news.ycombinator.com/feed/")
+        assertEquals(feed.siteUrl, "https://news.ycombinator.com/")
+        assertEquals(feed.description, "Links for the intellectually curious, ranked by readers.")
+    }
+
+
+    @Test(expected = ParseException::class)
+    fun nullTitleTest() {
+        val stream = TestUtils.loadResource("localfeed/rss2/rss_feed_special_cases.xml")
+        adapter.fromXml(stream)
+    }
+}
\ No newline at end of file
diff --git a/api/src/test/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt b/api/src/test/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt
new file mode 100644
index 00000000..49862135
--- /dev/null
+++ b/api/src/test/java/com/readrops/api/localfeed/rss2/RSS2ItemsAdapterTest.kt
@@ -0,0 +1,98 @@
+package com.readrops.api.localfeed.rss2
+
+import com.readrops.api.TestUtils
+import com.readrops.api.utils.DateUtils
+import com.readrops.api.utils.exceptions.ParseException
+import junit.framework.TestCase.assertEquals
+import junit.framework.TestCase.assertNotNull
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.ExpectedException
+
+class RSS2ItemsAdapterTest {
+
+    private val adapter = RSS2ItemsAdapter()
+
+    @get:Rule
+    val expectedException: ExpectedException = ExpectedException.none()
+
+    @Test
+    fun normalCasesTest() {
+        val stream = TestUtils.loadResource("localfeed/rss_feed.xml")
+
+        val items = adapter.fromXml(stream)
+        val item = items.first()
+
+        assertEquals(items.size, 7)
+        assertEquals(item.title, "Africa declared free of wild polio")
+        assertEquals(item.link, "https://www.bbc.com/news/world-africa-53887947")
+        assertEquals(item.pubDate, DateUtils.parse("Tue, 25 Aug 2020 17:15:49 +0000"))
+        assertEquals(item.author, "Author 1")
+        assertEquals(item.description, "<a href=\"https://news.ycombinator.com/item?id=24273602\">Comments</a>")
+        assertEquals(item.guid, "https://www.bbc.com/news/world-africa-53887947")
+    }
+
+    @Test
+    fun otherNamespacesTest() {
+        val stream = TestUtils.loadResource("localfeed/rss2/rss_items_other_namespaces.xml")
+        val item = adapter.fromXml(stream).first()
+
+        assertEquals(item.guid, "guid")
+        assertEquals(item.author, "creator 1, creator 2, creator 3, creator 4")
+        assertEquals(item.pubDate, DateUtils.parse("2020-08-05T14:03:48Z"))
+        assertEquals(item.content, "content:encoded")
+    }
+
+    @Test
+    fun noDateTest() {
+        val stream = TestUtils.loadResource("localfeed/rss2/rss_items_no_date.xml")
+        val item = adapter.fromXml(stream).first()
+
+        assertNotNull(item.pubDate)
+    }
+
+    @Test
+    fun noTitleTest() {
+        val stream = TestUtils.loadResource("localfeed/rss2/rss_items_no_title.xml")
+
+        expectedException.expect(ParseException::class.java)
+        expectedException.expectMessage("Item title is required")
+
+        adapter.fromXml(stream)
+    }
+
+    @Test
+    fun noLinkTest() {
+        val stream = TestUtils.loadResource("localfeed/rss2/rss_items_no_link.xml")
+
+        expectedException.expect(ParseException::class.java)
+        expectedException.expectMessage("Item link is required")
+
+        adapter.fromXml(stream)
+    }
+
+    @Test
+    fun enclosureTest() {
+        val stream = TestUtils.loadResource("localfeed/rss2/rss_items_enclosure.xml")
+        val item = adapter.fromXml(stream).first()
+
+        assertEquals(item.imageLink, "https://image1.jpg")
+    }
+
+    @Test
+    fun mediaContentTest() {
+        val stream = TestUtils.loadResource("localfeed/rss2/rss_items_media_content.xml")
+        val items = adapter.fromXml(stream)
+
+        assertEquals(items.first().imageLink, "https://image1.jpg")
+        assertEquals(items[1].imageLink, "https://image2.jpg")
+    }
+
+    @Test
+    fun mediaGroupTest() {
+        val stream = TestUtils.loadResource("localfeed/rss2/rss_items_media_group.xml")
+        val item = adapter.fromXml(stream).first()
+
+        assertEquals(item.imageLink, "https://image1.jpg")
+    }
+}
\ No newline at end of file
diff --git a/api/src/test/java/com/readrops/api/services/freshrss/adapters/FreshRSSItemsIdsAdapterTest.kt b/api/src/test/java/com/readrops/api/services/freshrss/adapters/FreshRSSItemsIdsAdapterTest.kt
new file mode 100644
index 00000000..7831c213
--- /dev/null
+++ b/api/src/test/java/com/readrops/api/services/freshrss/adapters/FreshRSSItemsIdsAdapterTest.kt
@@ -0,0 +1,30 @@
+package com.readrops.api.services.freshrss.adapters
+
+import com.squareup.moshi.Moshi
+import com.squareup.moshi.Types
+import junit.framework.TestCase.assertEquals
+import okio.Buffer
+import org.junit.Test
+
+class FreshRSSItemsIdsAdapterTest {
+
+    private val adapter = Moshi.Builder()
+            .add(Types.newParameterizedType(List::class.java, String::class.java), FreshRSSItemsIdsAdapter())
+            .build()
+            .adapter<List<String>>(Types.newParameterizedType(List::class.java, String::class.java))
+
+    @Test
+    fun validIdsTest() {
+        val stream = javaClass.classLoader!!.getResourceAsStream("services/freshrss/adapters/items_starred_ids.json")
+
+        val ids = adapter.fromJson(Buffer().readFrom(stream))!!
+
+        assertEquals(ids, listOf(
+                "tag:google.com,2005:reader/item/0005b2c17277b383",
+                "tag:google.com,2005:reader/item/0005b2c12d328ae4",
+                "tag:google.com,2005:reader/item/0005b2c0781d0737",
+                "tag:google.com,2005:reader/item/0005b2bf3852c293",
+                "tag:google.com,2005:reader/item/0005b2bebeed9f7f"
+        ))
+    }
+}
\ No newline at end of file
diff --git a/api/src/test/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsFeedsAdapterTest.kt b/api/src/test/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsFeedsAdapterTest.kt
new file mode 100644
index 00000000..01a7c4ff
--- /dev/null
+++ b/api/src/test/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsFeedsAdapterTest.kt
@@ -0,0 +1,40 @@
+package com.readrops.api.services.nextcloudnews.adapters
+
+import com.readrops.api.TestUtils
+import com.readrops.db.entities.Feed
+import com.squareup.moshi.Moshi
+import com.squareup.moshi.Types
+import okio.Buffer
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Test
+
+class NextNewsFeedsAdapterTest {
+
+    private val adapter = Moshi.Builder()
+            .add(NextNewsFeedsAdapter())
+            .build()
+            .adapter<List<Feed>>(Types.newParameterizedType(List::class.java, Feed::class.java))
+
+    @Test
+    fun validFeedsTest() {
+        val stream = TestUtils.loadResource("services/nextcloudnews/feeds.json")
+
+        val feeds = adapter.fromJson(Buffer().readFrom(stream))!!
+        val feed1 = feeds[0]
+
+        assertEquals(feed1.name, "Krebs on Security")
+        assertEquals(feed1.url, "https://krebsonsecurity.com/feed/")
+        assertEquals(feed1.siteUrl, "https://krebsonsecurity.com/")
+        assertEquals(feed1.remoteId, "3")
+        assertNull(feed1.remoteFolderId)
+        assertEquals(feed1.iconUrl, "https://krebsonsecurity.com/favicon.ico")
+
+        val feed2 = feeds[1]
+        assertNull(feed2.remoteFolderId)
+
+        val feed3 = feeds[2]
+        assertEquals(feed3.name, "krebsonsecurity.com")
+        assertEquals(feed3.remoteFolderId, "5")
+    }
+}
\ No newline at end of file
diff --git a/api/src/test/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsUserAdapterTest.kt b/api/src/test/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsUserAdapterTest.kt
new file mode 100644
index 00000000..6638f16b
--- /dev/null
+++ b/api/src/test/java/com/readrops/api/services/nextcloudnews/adapters/NextNewsUserAdapterTest.kt
@@ -0,0 +1,17 @@
+package com.readrops.api.services.nextcloudnews.adapters
+
+import com.readrops.api.TestUtils
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class NextNewsUserAdapterTest {
+
+    private val adapter = NextNewsUserAdapter()
+
+    @Test
+    fun validXmlTest() {
+        val stream = TestUtils.loadResource("services/nextcloudnews/user.xml")
+
+        assertEquals(adapter.fromXml(stream), "Shinokuni")
+    }
+}
\ No newline at end of file
diff --git a/api/src/test/java/com/readrops/api/utils/ApiUtilsTest.kt b/api/src/test/java/com/readrops/api/utils/ApiUtilsTest.kt
new file mode 100644
index 00000000..8a8bcae4
--- /dev/null
+++ b/api/src/test/java/com/readrops/api/utils/ApiUtilsTest.kt
@@ -0,0 +1,25 @@
+package com.readrops.api.utils
+
+import junit.framework.TestCase.assertEquals
+import org.junit.Test
+
+class ApiUtilsTest {
+
+    @Test
+    fun contentTypeWithCharsetTest() {
+        assertEquals(ApiUtils.parseContentType("application/rss+xml; charset=UTF-8"),
+                "application/rss+xml")
+    }
+
+    @Test
+    fun contentTypeWithoutCharsetText() {
+        assertEquals(ApiUtils.parseContentType("text/xml"),
+                "text/xml")
+    }
+
+    @Test
+    fun cleanTextTest() {
+        val text = "    <p>This is a text<br/>to</p> clean    "
+        assertEquals("This is a text to clean", ApiUtils.cleanText(text))
+    }
+}
\ No newline at end of file
diff --git a/api/src/test/java/com/readrops/api/utils/AuthInterceptorTest.kt b/api/src/test/java/com/readrops/api/utils/AuthInterceptorTest.kt
new file mode 100644
index 00000000..7bd94878
--- /dev/null
+++ b/api/src/test/java/com/readrops/api/utils/AuthInterceptorTest.kt
@@ -0,0 +1,53 @@
+package com.readrops.api.utils
+
+import com.readrops.api.services.freshrss.FreshRSSCredentials
+import junit.framework.TestCase.assertEquals
+import junit.framework.TestCase.assertNull
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.mockwebserver.MockResponse
+import okhttp3.mockwebserver.MockWebServer
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+
+class AuthInterceptorTest {
+
+    private val interceptor = AuthInterceptor()
+    private val mockServer = MockWebServer()
+    private lateinit var okHttpClient: OkHttpClient
+
+    @Before
+    fun before() {
+        okHttpClient = OkHttpClient.Builder().addInterceptor(interceptor).build()
+        mockServer.start(8080)
+    }
+
+    @After
+    fun tearDown() {
+        mockServer.close()
+    }
+
+    @Test
+    fun credentialsUrlTest() {
+        mockServer.enqueue(MockResponse())
+        interceptor.credentials = FreshRSSCredentials("token", "http://localhost:8080/rss")
+
+        okHttpClient.newCall(Request.Builder().url(mockServer.url("/url")).build()).execute()
+        val request = mockServer.takeRequest()
+
+        assertEquals(request.headers["Authorization"], "GoogleLogin auth=token")
+    }
+
+    @Test
+    fun nullCredentialsTest() {
+        mockServer.enqueue(MockResponse())
+        interceptor.credentials = null
+
+        okHttpClient.newCall(Request.Builder().url(mockServer.url("/url")).build()).execute()
+        val request = mockServer.takeRequest()
+
+        assertEquals(request.requestUrl.toString(), "http://localhost:8080/url")
+        assertNull(request.headers["Authorization"])
+    }
+}
\ No newline at end of file
diff --git a/app/src/test/java/com/readrops/app/DateUtilsTest.java b/api/src/test/java/com/readrops/api/utils/DateUtilsTest.java
similarity index 55%
rename from app/src/test/java/com/readrops/app/DateUtilsTest.java
rename to api/src/test/java/com/readrops/api/utils/DateUtilsTest.java
index 95b9058f..af3d98c7 100644
--- a/app/src/test/java/com/readrops/app/DateUtilsTest.java
+++ b/api/src/test/java/com/readrops/api/utils/DateUtilsTest.java
@@ -1,6 +1,4 @@
-package com.readrops.app;
-
-import com.readrops.app.utils.DateUtils;
+package com.readrops.api.utils;
 
 import org.joda.time.LocalDateTime;
 import org.junit.Test;
@@ -13,48 +11,48 @@ public class DateUtilsTest {
     public void rssDateTest() {
         LocalDateTime dateTime = new LocalDateTime(2019, 1, 4, 22, 21, 46);
 
-        assertEquals(0, dateTime.compareTo(DateUtils.stringToLocalDateTime("Fri, 04 Jan 2019 22:21:46 GMT")));
+        assertEquals(0, dateTime.compareTo(DateUtils.parse("Fri, 04 Jan 2019 22:21:46 GMT")));
     }
 
     @Test
     public void rssDate2Test() {
         LocalDateTime dateTime = new LocalDateTime(2019, 1, 4, 22, 21, 46);
 
-        assertEquals(0, dateTime.compareTo(DateUtils.stringToLocalDateTime("Fri, 04 Jan 2019 22:21:46 +0000")));
+        assertEquals(0, dateTime.compareTo(DateUtils.parse("Fri, 04 Jan 2019 22:21:46 +0000")));
     }
 
     @Test
     public void rssDate3Test() {
         LocalDateTime dateTime = new LocalDateTime(2019, 1, 4, 22, 21, 46);
 
-        assertEquals(0, dateTime.compareTo(DateUtils.stringToLocalDateTime("Fri, 04 Jan 2019 22:21:46")));
+        assertEquals(0, dateTime.compareTo(DateUtils.parse("Fri, 04 Jan 2019 22:21:46")));
     }
 
     @Test
     public void atomJsonDateTest() {
         LocalDateTime dateTime = new LocalDateTime(2019, 1, 4, 22, 21, 46);
 
-        assertEquals(0, dateTime.compareTo(DateUtils.stringToLocalDateTime("2019-01-04T22:21:46+00:00")));
+        assertEquals(0, dateTime.compareTo(DateUtils.parse("2019-01-04T22:21:46+00:00")));
     }
 
     @Test
     public void atomJsonDate2Test() {
         LocalDateTime dateTime = new LocalDateTime(2019, 1, 4, 22, 21, 46);
 
-        assertEquals(0, dateTime.compareTo(DateUtils.stringToLocalDateTime("2019-01-04T22:21:46-0000")));
+        assertEquals(0, dateTime.compareTo(DateUtils.parse("2019-01-04T22:21:46-0000")));
     }
 
     @Test
     public void isoPatternTest() {
         LocalDateTime dateTime = new LocalDateTime(2020, 6, 30, 11, 39, 37, 206);
 
-        assertEquals(0, dateTime.compareTo(DateUtils.stringToLocalDateTime("2020-06-30T11:39:37.206-07:00")));
+        assertEquals(0, dateTime.compareTo(DateUtils.parse("2020-06-30T11:39:37.206-07:00")));
     }
 
     @Test
     public void edtPatternTest() {
         LocalDateTime dateTime = new LocalDateTime(2020, 7, 17, 16, 30, 0);
 
-        assertEquals(0, dateTime.compareTo(DateUtils.stringToLocalDateTime("Fri, 17 Jul 2020 16:30:00 EDT")));
+        assertEquals(0, dateTime.compareTo(DateUtils.parse("Fri, 17 Jul 2020 16:30:00 EDT")));
     }
 }
\ No newline at end of file
diff --git a/api/src/test/java/com/readrops/api/utils/JsonReaderExtensionsTest.kt b/api/src/test/java/com/readrops/api/utils/JsonReaderExtensionsTest.kt
new file mode 100644
index 00000000..92b95a8f
--- /dev/null
+++ b/api/src/test/java/com/readrops/api/utils/JsonReaderExtensionsTest.kt
@@ -0,0 +1,88 @@
+package com.readrops.api.utils
+
+import com.readrops.api.utils.exceptions.ParseException
+import com.readrops.api.utils.extensions.nextNonEmptyString
+import com.readrops.api.utils.extensions.nextNullableString
+import com.squareup.moshi.JsonReader
+import junit.framework.TestCase.assertEquals
+import junit.framework.TestCase.assertNull
+import okio.Buffer
+import org.junit.Test
+
+class JsonReaderExtensionsTest {
+
+    @Test
+    fun nextNullableStringNullCaseTest() {
+        val reader = JsonReader.of(Buffer().readFrom("""
+            {
+                "field": null
+            }
+        """.trimIndent().byteInputStream()))
+
+        reader.beginObject()
+        reader.nextName()
+
+        assertNull(reader.nextNullableString())
+        reader.endObject()
+    }
+
+    @Test
+    fun nextNullableStringEmptyCaseTest() {
+        val reader = JsonReader.of(Buffer().readFrom("""
+            {
+                "field": ""
+            }
+        """.trimIndent().byteInputStream()))
+
+        reader.beginObject()
+        reader.nextName()
+
+        assertNull(reader.nextNullableString())
+        reader.endObject()
+    }
+
+    @Test
+    fun nextNullableValueNormalCaseTest() {
+        val reader = JsonReader.of(Buffer().readFrom("""
+            {
+                "field": "value"
+            }
+        """.trimIndent().byteInputStream()))
+
+        reader.beginObject()
+        reader.nextName()
+
+        assertEquals(reader.nextNullableString(), "value")
+        reader.endObject()
+    }
+
+    @Test
+    fun nextNonEmptyStringTest() {
+        val reader = JsonReader.of(Buffer().readFrom("""
+            {
+                "field": "value"
+            }
+        """.trimIndent().byteInputStream()))
+
+        reader.beginObject()
+        reader.nextName()
+
+        assertEquals(reader.nextNonEmptyString(), "value")
+        reader.endObject()
+    }
+
+    @Test(expected = ParseException::class)
+    fun nextNonEmptyStringEmptyCaseTest() {
+        val reader = JsonReader.of(Buffer().readFrom("""
+            {
+                "field": ""
+            }
+        """.trimIndent().byteInputStream()))
+
+        reader.beginObject()
+        reader.nextName()
+
+        reader.nextNonEmptyString()
+    }
+
+}
\ No newline at end of file
diff --git a/api/src/test/java/com/readrops/api/utils/KonsumerExtensionsTest.kt b/api/src/test/java/com/readrops/api/utils/KonsumerExtensionsTest.kt
new file mode 100644
index 00000000..40699959
--- /dev/null
+++ b/api/src/test/java/com/readrops/api/utils/KonsumerExtensionsTest.kt
@@ -0,0 +1,90 @@
+package com.readrops.api.utils
+
+import com.gitlab.mvysny.konsumexml.KonsumerException
+import com.gitlab.mvysny.konsumexml.konsumeXml
+import com.readrops.api.utils.extensions.nonNullText
+import com.readrops.api.utils.extensions.nullableText
+import com.readrops.api.utils.extensions.nullableTextRecursively
+import junit.framework.TestCase.assertEquals
+import junit.framework.TestCase.assertNull
+import org.junit.Test
+
+class KonsumerExtensionsTest {
+
+    @Test(expected = KonsumerException::class)
+    fun nonNullTextNullCaseTest() {
+        val xml = """
+            <description></description>
+        """.trimIndent()
+
+        xml.konsumeXml().apply {
+            child("description") { nonNullText() }
+        }
+    }
+
+    @Test
+    fun nonNullTextNonNullCaseTest() {
+        val xml = """
+            <description>
+description
+</description>
+        """.trimIndent()
+
+        xml.konsumeXml().apply {
+            val description = child("description") { nonNullText() }
+            assertEquals(description, "description")
+        }
+    }
+
+    @Test
+    fun nullableTextNullCaseTest() {
+        val xml = """
+            <description></description>
+        """.trimIndent()
+
+        xml.konsumeXml().apply {
+            val description = child("description") { nullableText() }
+            assertNull(description)
+        }
+    }
+
+    @Test
+    fun nullableTextNonNullCaseTest() {
+        val xml = """
+            <description>
+description
+</description>
+        """.trimIndent()
+
+        xml.konsumeXml().apply {
+            val description = child("description") { nullableText() }
+            assertEquals(description, "description")
+        }
+    }
+
+    @Test
+    fun nullableTextRecursivelyNullCaseTest() {
+        val xml = """
+            <description></description>
+        """.trimIndent()
+
+        xml.konsumeXml().apply {
+            val description = child("description") { nullableTextRecursively() }
+            assertNull(description)
+        }
+    }
+
+    @Test
+    fun nullableTextRecursivelyNonNullCaseTest() {
+        val xml = """
+            <description>
+descrip<a>tion</a>
+</description>
+        """.trimIndent()
+
+        xml.konsumeXml().apply {
+            val description = child("description") { nullableTextRecursively() }
+            assertEquals(description, "description")
+        }
+    }
+}
\ No newline at end of file
diff --git a/api/src/test/resources/localfeed/atom/atom_feed.xml b/api/src/test/resources/localfeed/atom/atom_feed.xml
new file mode 100644
index 00000000..797a7917
--- /dev/null
+++ b/api/src/test/resources/localfeed/atom/atom_feed.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-US">
+    <id>tag:github.com,2008:/readrops/Readrops/commits/develop</id>
+    <link type="text/html" rel="alternate" href="https://github.com/readrops/Readrops/commits/develop"/>
+    <link type="application/atom+xml" rel="self" href="https://github.com/readrops/Readrops/commits/develop.atom"/>
+    <title>Recent Commits to Readrops:develop</title>
+    <updated>2020-09-06T21:09:59Z</updated>
+    <subtitle>Here is a subtitle</subtitle>
+</feed>
\ No newline at end of file
diff --git a/api/src/test/resources/localfeed/atom/atom_feed_no_url_siteurl.xml b/api/src/test/resources/localfeed/atom/atom_feed_no_url_siteurl.xml
new file mode 100644
index 00000000..6b2d1530
--- /dev/null
+++ b/api/src/test/resources/localfeed/atom/atom_feed_no_url_siteurl.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-US">
+    <id>tag:github.com,2008:/readrops/Readrops/commits/develop</id>
+    <title>Recent Commits to Readrops:develop</title>
+    <updated>2020-09-06T21:09:59Z</updated>
+    <subtitle>Here is a subtitle</subtitle>
+</feed>
\ No newline at end of file
diff --git a/api/src/test/resources/localfeed/atom/atom_items.xml b/api/src/test/resources/localfeed/atom/atom_items.xml
new file mode 100644
index 00000000..107a0950
--- /dev/null
+++ b/api/src/test/resources/localfeed/atom/atom_items.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<feed xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xml:lang="en-US">
+    <id>tag:github.com,2008:/readrops/Readrops/commits/develop</id>
+    <link type="text/html" rel="alternate" href="https://github.com/readrops/Readrops/commits/develop"/>
+    <link type="application/atom+xml" rel="self" href="https://github.com/readrops/Readrops/commits/develop.atom"/>
+    <title>Recent Commits to Readrops:develop</title>
+    <updated>2020-09-06T21:09:59Z</updated>
+    <entry>
+        <id>tag:github.com,2008:Grit::Commit/c15f093a1bc4211e85f8d1817c9073e307afe5ac</id>
+        <link type="text/html" rel="alternate" href="https://github.com/readrops/Readrops/commit/c15f093a1bc4211e85f8d1817c9073e307afe5ac"/>
+        <title>Add an option to open item url in custom tab</title>
+        <updated>2020-09-06T21:09:59Z</updated>
+        <media:thumbnail height="30" width="30" url="https://avatars2.githubusercontent.com/u/18555673?s=30&amp;u=c56b216e8d128d0ec217062feeace9faca4dc893&amp;v=4"/>
+        <author>
+            <name>Shinokuni</name>
+            <uri>https://github.com/Shinokuni</uri>
+        </author>
+        <summary>Summary</summary>
+        <content type="html">
+            &lt;pre style=&#39;white-space:pre-wrap;width:81ex&#39;&gt;Add an option to open item url in custom tab&lt;/pre&gt;
+        </content>
+    </entry>
+    <entry>
+        <id>tag:github.com,2008:Grit::Commit/e0945823eecf269e5beea646ac5d7e630e08afbf</id>
+        <link type="text/html" rel="alternate" href="https://github.com/readrops/Readrops/commit/e0945823eecf269e5beea646ac5d7e630e08afbf"/>
+        <title>
+            Use gradle parallel builds
+        </title>
+        <updated>2020-09-05T13:28:23Z</updated>
+        <media:thumbnail height="30" width="30" url="https://avatars2.githubusercontent.com/u/18555673?s=30&amp;u=c56b216e8d128d0ec217062feeace9faca4dc893&amp;v=4"/>
+        <author>
+            <name>Shinokuni</name>
+            <uri>https://github.com/Shinokuni</uri>
+        </author>
+        <content type="html">
+            &lt;pre style=&#39;white-space:pre-wrap;width:81ex&#39;&gt;Use gradle parallel builds&lt;/pre&gt;
+        </content>
+    </entry>
+    <entry>
+        <id>tag:github.com,2008:Grit::Commit/85fcf03e64d8b482e4d2af8c2bcd1509d946944f</id>
+        <link type="text/html" rel="alternate" href="https://github.com/readrops/Readrops/commit/85fcf03e64d8b482e4d2af8c2bcd1509d946944f"/>
+        <title>
+            Use clear text mode for the feed url text input in AddFeedActivity
+        </title>
+        <updated>2020-09-05T12:23:48Z</updated>
+        <media:thumbnail height="30" width="30" url="https://avatars2.githubusercontent.com/u/18555673?s=30&amp;u=c56b216e8d128d0ec217062feeace9faca4dc893&amp;v=4"/>
+        <author>
+            <name>Shinokuni</name>
+            <uri>https://github.com/Shinokuni</uri>
+        </author>
+        <content type="html">
+            &lt;pre style=&#39;white-space:pre-wrap;width:81ex&#39;&gt;Use clear text mode for the feed url text input in AddFeedActivity&lt;/pre&gt;
+        </content>
+    </entry>
+    <entry>
+        <id>tag:github.com,2008:Grit::Commit/d59e38ee9d11da186131b602425231eff0896956</id>
+        <link type="text/html" rel="alternate" href="https://github.com/readrops/Readrops/commit/d59e38ee9d11da186131b602425231eff0896956"/>
+        <title>
+            Use project level okhttp client with glide
+        </title>
+        <updated>2020-09-05T12:05:16Z</updated>
+        <media:thumbnail height="30" width="30" url="https://avatars2.githubusercontent.com/u/18555673?s=30&amp;u=c56b216e8d128d0ec217062feeace9faca4dc893&amp;v=4"/>
+        <author>
+            <name>Shinokuni</name>
+            <uri>https://github.com/Shinokuni</uri>
+        </author>
+        <content type="html">
+            &lt;pre style=&#39;white-space:pre-wrap;width:81ex&#39;&gt;Use project level okhttp client with glide&lt;/pre&gt;
+        </content>
+    </entry>
+</feed>
\ No newline at end of file
diff --git a/api/src/test/resources/localfeed/atom/atom_items_no_date.xml b/api/src/test/resources/localfeed/atom/atom_items_no_date.xml
new file mode 100644
index 00000000..08ae5692
--- /dev/null
+++ b/api/src/test/resources/localfeed/atom/atom_items_no_date.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<feed xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xml:lang="en-US">
+    <id>tag:github.com,2008:/readrops/Readrops/commits/develop</id>
+    <link type="text/html" rel="alternate" href="https://github.com/readrops/Readrops/commits/develop"/>
+    <link type="application/atom+xml" rel="self" href="https://github.com/readrops/Readrops/commits/develop.atom"/>
+    <title>Recent Commits to Readrops:develop</title>
+    <updated>2020-09-06T21:09:59Z</updated>
+    <entry>
+        <id>tag:github.com,2008:Grit::Commit/c15f093a1bc4211e85f8d1817c9073e307afe5ac</id>
+        <title>Add an option to open item url in custom tab</title>
+        <link type="text/html" rel="alternate" href="https://github.com/readrops/Readrops/commit/c15f093a1bc4211e85f8d1817c9073e307afe5ac"/>
+        <media:thumbnail height="30" width="30" url="https://avatars2.githubusercontent.com/u/18555673?s=30&amp;u=c56b216e8d128d0ec217062feeace9faca4dc893&amp;v=4"/>
+        <author>
+            <name>Shinokuni</name>
+            <uri>https://github.com/Shinokuni</uri>
+        </author>
+        <summary>Summary</summary>
+        <content type="html">
+            &lt;pre style=&#39;white-space:pre-wrap;width:81ex&#39;&gt;Add an option to open item url in custom tab&lt;/pre&gt;
+        </content>
+    </entry>
+</feed>
\ No newline at end of file
diff --git a/api/src/test/resources/localfeed/atom/atom_items_no_link.xml b/api/src/test/resources/localfeed/atom/atom_items_no_link.xml
new file mode 100644
index 00000000..c0d817d8
--- /dev/null
+++ b/api/src/test/resources/localfeed/atom/atom_items_no_link.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<feed xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xml:lang="en-US">
+    <id>tag:github.com,2008:/readrops/Readrops/commits/develop</id>
+    <link type="text/html" rel="alternate" href="https://github.com/readrops/Readrops/commits/develop"/>
+    <link type="application/atom+xml" rel="self" href="https://github.com/readrops/Readrops/commits/develop.atom"/>
+    <title>Recent Commits to Readrops:develop</title>
+    <updated>2020-09-06T21:09:59Z</updated>
+    <entry>
+        <id>tag:github.com,2008:Grit::Commit/c15f093a1bc4211e85f8d1817c9073e307afe5ac</id>
+        <title>Add an option to open item url in custom tab</title>
+        <updated>2020-09-06T21:09:59Z</updated>
+        <media:thumbnail height="30" width="30" url="https://avatars2.githubusercontent.com/u/18555673?s=30&amp;u=c56b216e8d128d0ec217062feeace9faca4dc893&amp;v=4"/>
+        <author>
+            <name>Shinokuni</name>
+            <uri>https://github.com/Shinokuni</uri>
+        </author>
+        <summary>Summary</summary>
+        <content type="html">
+            &lt;pre style=&#39;white-space:pre-wrap;width:81ex&#39;&gt;Add an option to open item url in custom tab&lt;/pre&gt;
+        </content>
+    </entry>
+</feed>
\ No newline at end of file
diff --git a/api/src/test/resources/localfeed/atom/atom_items_no_title.xml b/api/src/test/resources/localfeed/atom/atom_items_no_title.xml
new file mode 100644
index 00000000..8bf4adbd
--- /dev/null
+++ b/api/src/test/resources/localfeed/atom/atom_items_no_title.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<feed xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xml:lang="en-US">
+    <id>tag:github.com,2008:/readrops/Readrops/commits/develop</id>
+    <link type="text/html" rel="alternate" href="https://github.com/readrops/Readrops/commits/develop"/>
+    <link type="application/atom+xml" rel="self" href="https://github.com/readrops/Readrops/commits/develop.atom"/>
+    <title>Recent Commits to Readrops:develop</title>
+    <updated>2020-09-06T21:09:59Z</updated>
+    <entry>
+        <id>tag:github.com,2008:Grit::Commit/c15f093a1bc4211e85f8d1817c9073e307afe5ac</id>
+        <link type="text/html" rel="alternate" href="https://github.com/readrops/Readrops/commit/c15f093a1bc4211e85f8d1817c9073e307afe5ac"/>
+        <updated>2020-09-06T21:09:59Z</updated>
+        <media:thumbnail height="30" width="30" url="https://avatars2.githubusercontent.com/u/18555673?s=30&amp;u=c56b216e8d128d0ec217062feeace9faca4dc893&amp;v=4"/>
+        <author>
+            <name>Shinokuni</name>
+            <uri>https://github.com/Shinokuni</uri>
+        </author>
+        <summary>Summary</summary>
+        <content type="html">
+            &lt;pre style=&#39;white-space:pre-wrap;width:81ex&#39;&gt;Add an option to open item url in custom tab&lt;/pre&gt;
+        </content>
+    </entry>
+</feed>
\ No newline at end of file
diff --git a/api/src/test/resources/localfeed/json/json_feed.json b/api/src/test/resources/localfeed/json/json_feed.json
new file mode 100644
index 00000000..4e5b032f
--- /dev/null
+++ b/api/src/test/resources/localfeed/json/json_feed.json
@@ -0,0 +1,86 @@
+{
+  "version": "https://jsonfeed.org/version/1",
+  "title": "News from Flying Meat",
+  "home_page_url": "http://flyingmeat.com/blog/",
+  "feed_url": "http://flyingmeat.com/blog/feed.json",
+  "description": "News from your friends at Flying Meat.",
+  "author": {
+    "name": "Gus Mueller"
+  },
+  "items": [
+    {
+      "id": "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html",
+      "title": "Acorn and 10.13",
+      "content_html": "<p>Happy Mac OS High Sierra release day everyone.</p>\n<p>I&#39;m happy to say that there are no known issues with <a href=\"https://flyingmeat.com/acorn/\">Acorn</a> 6.0.3 or Acorn 5.6.6 when running on Mac OS 10.13 High Sierra. In fact, you might even notice that some things are actually faster and it can now open HEIF images. How awesome is that?</p>\n<p>I&#39;m also working on some 10.13 goodies for Acorn 6 folks later this year. I can&#39;t wait to share that with you, but you&#39;ll have to wait just a little bit.</p>\n",
+      "date_published": "2017-09-25T14:27:27-07:00",
+      "url": "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html",
+      "author": {
+        "url": "this is an url",
+        "name": "Author 1"
+      }
+    },
+    {
+      "id": "http://flyingmeat.com/blog/archives/2018/2/acorn_6.1_is_out.html",
+      "title": "Acorn 6.1 Is Out",
+      "content_html": "<p><a href=\"https://flyingmeat.com/acorn/\">Acorn 6.1 has been released</a>.</p>\n<p>You can <a href=\"http://shapeof.com/archives/2018/2/acorn_6.1_is_out.html\">read a longer post about it</a> over on Gus&#39;s blog, but the short of it is: Better, faster, smoother, stronger. And now with Metal 2 support.</p>\n",
+      "date_published": "2018-02-16T09:59:11-08:00",
+      "url": "http://flyingmeat.com/blog/archives/2018/2/acorn_6.1_is_out.html"
+    },
+    {
+      "id": "http://flyingmeat.com/blog/archives/2018/6/a_pair_of_updates.html",
+      "title": "A Pair of Updates",
+      "content_html": "<p>Happy summer solstice everybody! (at least for folks in the northern hemisphere, and for folks in the south… sorry. It&#39;s going to start getting brighter for you though).</p>\n<p>Today I&#39;ve got a pair of minor app updates to annouce for you.</p>\n<p>First up is <a href=\"https://flyingmeat.com/acorn/\">Acorn 6.1.3</a>, which <a href=\"https://flyingmeat.com/acorn/releasenotes.html\">fixes a number of bugs</a> including one that stemmed from trying to use QuickLook on a file that was created with Acorn 1.0. For the one or two of you that this was affecting, hurray!</p>\n<p>Next up is <a href=\"https://flyingmeat.com/retrobatch/\">Retrobatch</a>, which also <a href=\"https://flyingmeat.com/retrobatch/releasenotes.html\">includes some bug fixes</a>, the beginnings of Voice Over support, performance improvements, and more.</p>\n<p>What&#39;s next for these apps? Work on Acorn 6.2 will begin shortly, as will Retrobatch 1.1. WWDC introduced some great new APIs that I want to take advantage of (cool new machine learning things), so that&#39;ll be a focus- as well as Dark Mode for Acorn and one other major thing I&#39;ve got planned. Retrobatch will probably also get the Dark Mode treatment, but not until I&#39;ve done it for Acorn first.</p>\n<p>So it&#39;s going to be a busy summer, but I&#39;m looking forward to it.</p>\n",
+      "date_published": "2018-06-21T10:18:46-07:00",
+      "url": "http://flyingmeat.com/blog/archives/2018/6/a_pair_of_updates.html"
+    },
+    {
+      "id": "http://flyingmeat.com/blog/archives/2018/9/retrobatch_1.1_is_out.html",
+      "title": "Retrobatch 1.1 Is Out",
+      "content_html": "<p>Here&#39;s something new for your lazy <strike>August</strike> September* morning: <a href=\"https://flyingmeat.com/retrobatch/\">Retrobatch 1.1 is out</a>.</p>\n<p>What&#39;s new and awesome? Well, <a href=\"https://flyingmeat.com/retrobatch/\">Retrobatch</a> now has some great scripting goodness in the form of a new Automator action which will run a workflow for you (and create Automator droplets), a new JavaScript node*, and the ability to run Retrobatch workflows from the terminal.</p>\n<p>We&#39;ve added a handful of new nodes such as Dither, Auto Enhance, Instant Alpha, and Color Posterize. New options to existing nodes have also shown up, such as &quot;Only scale smaller&quot; for the Scale node.</p>\n<p>And an interesting idea that I&#39;ve had folks ask about a number of times- it&#39;s now possible to run an image through a machine learning classifier, and then have the classification written to metadata such as the image title, or keywords. This was done by adding token support to the Set Specific Metadata node. This also means you can use other tokens such as the Current Year in metadata fields. Awesome? We think so.</p>\n<p>The <a href=\"https://flyingmeat.com/retrobatch/releasenotes.html\">full release notes are available</a>, and if you have ideas or questions- make sure to <a href=\"https://forums.flyingmeat.com/\">poke around on the forums</a> or write us: <a href=\"mailto:support@flyingmeat.com\">support@flyingmeat.com</a>. We&#39;ve got lots of ideas for future releases, but if you&#39;d like something specific in there make sure to let us know.</p>\n<br/>\n\n<div style=\"color:#666\">\n* Whoa, it's September already?<br/><br/>\n\n<p>**I&#39;m calling the JavaScript node a &quot;preview&quot;. It works very well, but I&#39;m not 100% sold on the API that I&#39;ve provided to folks. So this is a disclaimer that it might change a little bit in the future.</div></p>\n",
+      "date_published": "2018-09-07T09:43:10-07:00",
+      "url": "http://flyingmeat.com/blog/archives/2018/9/retrobatch_1.1_is_out.html"
+    },
+    {
+      "id": "http://flyingmeat.com/blog/archives/2018/9/acorn_6.2_with_mojave_dark_mode_is_out.html",
+      "title": "Acorn 6.2 With Mojave Dark Mode Is Out",
+      "content_html": "<p>On Monday I flipped some switches on the FM servers and <a href=\"https://flyingmeat.com/acorn/\">Acorn 6.2</a> was released to the universe. You might also remember that Monday a little known operating system from Apple was updated, which includes a neat new feature known as Dark Mode.</p>\n<center><img src=\"https://shapeof.com/archives/2018/media/acorn62.jpeg\" width=\"800\" style=\"\" /></center>\n\n<p>I think Acorn looks pretty good in Dark Aqua, especially the icon refresh from <a href=\"http://www.matthewskiles.com/\">Matthew Skiles</a>.</p>\n<p>To celebrate the new release, we&#39;ve put <a href=\"https://flyingmeat.com/store/\">Acorn on sale for 50% off</a>. So go grab it at the insanely low price of $14.99. If you haven&#39;t already upgraded from previous versions of Acorn, now is a good time to do so.</p>\n<p>We&#39;ve also packed a bunch of little changes, bug fixes, and compatibility with Mojave in there. And of course, there&#39;s more to come in the future as always.</p>\n",
+      "date_published": "2018-09-27T14:37:21-07:00",
+      "url": "http://flyingmeat.com/blog/archives/2018/9/acorn_6.2_with_mojave_dark_mode_is_out.html"
+    },
+    {
+      "id": "http://flyingmeat.com/blog/archives/2019/1/acorn_6.3_is_out.html",
+      "title": "Acorn 6.3 Is Out",
+      "content_html": "<p><a href=\"https://flyingmeat.com/acorn/\">Acorn 6.3 is available</a>, and the full <a href=\"https://flyingmeat.com/acorn/releasenotes.html\">release notes</a> are up as well.</p>\n<p>Here&#39;s what I think is awesome in this release:</p>\n<p><strong>Portrait Mask Support</strong>. If you have an iPhone running iOS 12 (and can take Portrait photos), Acorn will now detect the Portrait Matte from those images and turn it into a layer mask. The Portrait Matte is the image data which enables blurring in the background, or other fancy camera tricks. This means you can use this matte to erase and add fancy backgrounds or custom blurs for your image, all within Acorn.</p>\n<p><strong>Other Mask Features</strong>. You can now drag and drop masks from the layers list into another layer, or copy it out as a new layer. When exporting layers you now have an option to apply the mask on export, or just write it as an additional image along with everything else. There are a number of new shortcuts when dealing with layer masks as well.</p>\n<p><strong>Brush Stuff</strong>. If you&#39;re running MacOS 10.13 or later, you get a performance boost when brushing (painting, smudging, cloning, etc…). This is especially noticeable when brusing on deep color images.</p>\n<p>I&#39;ve also added options to the brush palette for adjusting flow, softness and blending. In addition to all this, there&#39;s a bunch of new brushes under the &quot;Basic Round&quot; category which are designed for the new brush engine.</p>\n<p><strong>Other Stuff</strong>. There&#39;s other good things including improved PDF export, various MacOS Mojave UI fixes, additional speed improvements with with deep images, and more. And as always, it&#39;s a free upgrade for anyone who has already purchased Acorn 6.</p>\n",
+      "date_published": "2019-01-09T13:00:07-08:00",
+      "url": "http://flyingmeat.com/blog/archives/2019/1/acorn_6.3_is_out.html"
+    },
+    {
+      "id": "http://flyingmeat.com/blog/archives/2019/4/retrobatch_1.2_released.html",
+      "title": "Retrobatch 1.2 Released",
+      "content_html": "<p>We&#39;re happy to announce that Retrobatch 1.2 has now been released, which is a free update for all owners of Retrobatch. Highlights of this release include:</p>\n<ul>\n<li><p><strong>Create animated GIF and PNG</strong> images with the Animated Image node. When using Retrobatch you can load in a folder of images and produce an optimized animated image with options for setting the frame rate, format, as well as letting the image loop or not.</p>\n</li>\n<li><p><strong>New nodes</strong> including &quot;Round Corner&quot;, &quot;Image Grid&quot;, and &quot;Limit&quot;. We&#39;ve also added improvements to the Write node allowing you to write back to the original processed image.</p>\n</li>\n<li><p><strong>Droplet support</strong> (Retrobatch Pro). Turn your workflow into an an application which you can drag and drop images onto. The droplet can work anywhere an application normally would, even in the Dock.</p>\n</li>\n<li><p><strong>Write Plug-Ins using JavaScript</strong> (Retrobatch Pro). Using the combined power of JavaScript and the native to MacOS Cocoa APIs, you can <a href=\"https://flyingmeat.com/retrobatch/jsapi/\">make and distribute</a> new plugins for Retrobatch. Got an idea for a plug-in and you want to use Core Image to make it? Or maybe you want to use Core Graphics to add some funky text to your images? Now you can do this with JavaScript and Cocoa.</p>\n</li>\n</ul>\n<p>The <a href=\"https://flyingmeat.com/retrobatch/releasenotes.html\">full release notes are available</a>, as well as information on bug fixes we delivered in this update.</p>\n<p>As always, we&#39;re <a href=\"https://flyingmeat.com/retrobatch/releasenotes.html\">always listening for feedback</a> and feature requests. And don&#39;t forget to head over to the <a href=\"http://forums.flyingmeat.com/c/retrobatch\">Retrobatch community formus</a> to chat with us and other Retrobatch users. </p>\n",
+      "date_published": "2019-04-01T13:38:21-07:00",
+      "url": "http://flyingmeat.com/blog/archives/2019/4/retrobatch_1.2_released.html"
+    },
+    {
+      "id": "http://flyingmeat.com/blog/archives/2019/10/catalina_ready.html",
+      "title": "Catalina Ready",
+      "content_html": "<p>MacOS 10.15 Catalina was just released, and we&#39;re happy to let you know that both <a href=\"https://flyingmeat.com/acorn/\">Acorn 6.5.1</a> and <a href=\"https://flyingmeat.com/retrobatch/\">Retrobatch 1.2</a> are compatible with it.</p>\n<p>And to celebrate the release of Catalina, we&#39;re <a href=\"https://flyingmeat.com/store/\">discounting Acorn by 50% for a limited time</a>. So if you haven&#39;t upgraded yet, now is a good time.</p>\n",
+      "date_published": "2019-10-07T10:48:03-07:00",
+      "url": "http://flyingmeat.com/blog/archives/2019/10/catalina_ready.html"
+    },
+    {
+      "id": "http://flyingmeat.com/blog/archives/2020/3/retrobatch_1.4_is_out.html",
+      "title": "Retrobatch 1.4 Is Out",
+      "content_html": "<p>I&#39;ve just typed the magic commands* and let the servers do their thing and now <a href=\"https://flyingmeat.com/retrobatch/\">Retrobatch 1.4</a> is loose on the world.</p>\n<p>There&#39;s a couple of interesting new features in this update I&#39;d like to call out. First up is JavaScript expressions in Retrobatch Pro. Various nodes in Retrobatch which allow you to set the size or length of a value (such as the Crop, Border, Gradient, Adjust Margin nodes) now have an option of running a little snippet of JavaScript code to figure out the value. This is a super powerful feature, which you can read about in our <a href=\"https://flyingmeat.com/retrobatch/jsapi-1/jsexpressions/\">JavaScript Expressions documentation</a></p>\n<p>Let&#39;s say you have some images of varying sizes, which are all at 480 x 380 or smaller, and you want them to expand to meet that size. But- you only want it to grow evenly on either side of the image, but you want to keep a baseline so only transparent area is added to the top of the image, and the bottom stays in the same spot. This little picture of the new Adjust Margins node shows how this can be done:</p>\n<center><img src=\"https://flyingmeat.com/retrobatch/jsapi-1/images/javascript_expression_fields_shot.png\" width=\"444\" style=\"border: solid 1px #777;\" /></center>\n\n<p>Yes, this is an oddball (and very real) case- but there&#39;s a billion of these little oddball cases out there. With the new JavaScript expressions support, these small but hard to do scenarios are now super easy.</p>\n<p>And yes, all of the JavaScript support in Retrobatch now sits atop <a href=\"https://github.com/ccgus/fmjs\">FMJS</a>, which any developer can use to build similar support into their apps.</p>\n<p>What else is new?</p>\n<p>File numbers with leading zeros for the Write node. You can add (and it&#39;s case sensitive) $FileNumber04$ in the File name: field of the Write node to have the file number of your image written out as part of the name, with a padding of up to 4 zeros. If you&#39;d like to pad that number to 6, you would enter $FileNumber06$, and so on.</p>\n<p>The Mask to Alpha node got a new &quot;invert colors&quot; option. Normally Mask to Alpha will convert the black areas of your image to transparent, and the white to opaque (with gray somewhere inbetween). With the new Invert Colors option, Mask to Alpha will now convert the white areas of your image to transparent, and keep the black opaque. This is great if you are scanning in line drawings from your own artwork, and want to make the backgrounds transparent.</p>\n<p>This request comes up a lot in <a href=\"https://flyingmeat.com/acorn/\">Acorn</a> as well. Previously you&#39;d have to add an Invert Colors node (or filter for Acorn), then the Mask to Alpha, and then Invert Colors again. Now it&#39;s just a checkbox in Mask to Alpha, which is super easy. I&#39;ve also added an update to the same filter in Acorn for the next release. You can grab a preview of it <a href=\"http://flyingmeat.com/download/latest/\">from here</a>.</p>\n<p>And finally for my short list, you can now make a droplet which doesn&#39;t take any files. Why is this useful? Well, imagine you have a workflow that reads an image from the clipboard, resizes it to a specific width, and then writes it back to the clipboard. Now you can make a little droplet to do just this. Just a double click from the Finder (or a single click from the Dock) and your workflow is run.</p>\n<p>The full release notes for Retrobatch 1.4  are <a href=\"https://flyingmeat.com/retrobatch/releasenotes.html\">available in the usual place</a>.</p>\n<p>*  <code>./bin/otbuild.sh -e 1.4</code></p>\n",
+      "date_published": "2020-03-31T14:37:15-07:00",
+      "url": "http://flyingmeat.com/blog/archives/2020/3/retrobatch_1.4_is_out.html"
+    },
+    {
+      "id": "http://flyingmeat.com/blog/archives/2020/5/acorn_6.6_released.html",
+      "title": "Acorn 6.6 is out with new Shape Processors and more",
+      "content_html": "<p><a href=\"https://flyingmeat.com/acorn/\">Acorn 6.6</a> is out. You can update to this release via the <a href=\"https://flyingmeat.com/acorn/appstore/\">App Store</a> as or the Acorn ▸ Check for Updates… menu if you bought it directly from us.</p>\n<p>Originally this was going to be a bug fix release but I kept on adding useful things and it snowballed into a feature release. As usual, <a href=\"https://flyingmeat.com/acorn/releasenotes.html\">the full release notes</a> have all the details about what was updated.</p>\n<p>The main new features are with the <a href=\"https://flyingmeat.com/acorn/docs/shape_processor.html\">Shape Processor</a>. If you&#39;re not already familiar with the shape processor, it&#39;s a neat ability Acorn has to take shapes on vector layers and pipe them through a series of actions, similar to how Automator or Acorn&#39;s bitmap filters work. Only instead of working on pixels, the processors will alter the shapes by scaling them or moving them around, or changing colors or blend modes. There&#39;s even a processor which will generate shapes for you- so if you want your canvas to fill up with hundreds of stars, you can do that.</p>\n<p>Acorn 6.6 adds new processors which let you set the stroke, fill, and blend mode of your processed shapes. You can now also flip your shapes and even shift colors. </p>\n<img src=\"https://shapeof.com/archives/2020/5/proc_shape.png\" width=\"660\" height=\"510\" />\n\n<p>Chaining these processors together can get you some neat looking images. You can make interesting desktop backgrounds, as well as textures for your photos. Or if you just need a bunch of hexagons arranged in a circle, that&#39;s just two processors stacked together.</p>\n<p>Have you made something interesting with the Shape Processor? I&#39;d love to see it either via Twitter (I&#39;m <a href=\"https://twitter.com/ccgus/\">@ccgus</a>) or via <a href=\"mailto:support@flyingmeat.com\">email</a>.</p>\n<p>There are of course the usual bug fixes and other <a href=\"https://flyingmeat.com/acorn/releasenotes.html\">minor details</a>. And if you don&#39;t already have Acorn, a <a href=\"https://flyingmeat.com/acorn/\">no-strings attached free trial</a> is available on our website. Try it out, and we&#39;re  always looking to hear from you about feature requests, thoughts, and anything else.</p>\n",
+      "date_published": "2020-05-28T12:17:57-07:00",
+      "url": "http://flyingmeat.com/blog/archives/2020/5/acorn_6.6_released.html"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/api/src/test/resources/localfeed/json/json_items_no_date.json b/api/src/test/resources/localfeed/json/json_items_no_date.json
new file mode 100644
index 00000000..ce9aa1fc
--- /dev/null
+++ b/api/src/test/resources/localfeed/json/json_items_no_date.json
@@ -0,0 +1,10 @@
+{
+  "items": [
+    {
+      "id": "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html",
+      "title": "Acorn and 10.13",
+      "content_html": "<p>Happy Mac OS High Sierra release day everyone.</p>\n<p>I&#39;m happy to say that there are no known issues with <a href=\"https://flyingmeat.com/acorn/\">Acorn</a> 6.0.3 or Acorn 5.6.6 when running on Mac OS 10.13 High Sierra. In fact, you might even notice that some things are actually faster and it can now open HEIF images. How awesome is that?</p>\n<p>I&#39;m also working on some 10.13 goodies for Acorn 6 folks later this year. I can&#39;t wait to share that with you, but you&#39;ll have to wait just a little bit.</p>\n",
+      "url": "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/api/src/test/resources/localfeed/json/json_items_no_link.json b/api/src/test/resources/localfeed/json/json_items_no_link.json
new file mode 100644
index 00000000..e23377e6
--- /dev/null
+++ b/api/src/test/resources/localfeed/json/json_items_no_link.json
@@ -0,0 +1,10 @@
+{
+  "items": [
+    {
+      "id": "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html",
+      "title": "Acorn and 10.13",
+      "content_html": "<p>Happy Mac OS High Sierra release day everyone.</p>\n<p>I&#39;m happy to say that there are no known issues with <a href=\"https://flyingmeat.com/acorn/\">Acorn</a> 6.0.3 or Acorn 5.6.6 when running on Mac OS 10.13 High Sierra. In fact, you might even notice that some things are actually faster and it can now open HEIF images. How awesome is that?</p>\n<p>I&#39;m also working on some 10.13 goodies for Acorn 6 folks later this year. I can&#39;t wait to share that with you, but you&#39;ll have to wait just a little bit.</p>\n",
+      "date_published": "2017-09-25T14:27:27-07:00"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/api/src/test/resources/localfeed/json/json_items_no_title.json b/api/src/test/resources/localfeed/json/json_items_no_title.json
new file mode 100644
index 00000000..e63cae47
--- /dev/null
+++ b/api/src/test/resources/localfeed/json/json_items_no_title.json
@@ -0,0 +1,10 @@
+{
+  "items": [
+    {
+      "id": "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html",
+      "content_html": "<p>Happy Mac OS High Sierra release day everyone.</p>\n<p>I&#39;m happy to say that there are no known issues with <a href=\"https://flyingmeat.com/acorn/\">Acorn</a> 6.0.3 or Acorn 5.6.6 when running on Mac OS 10.13 High Sierra. In fact, you might even notice that some things are actually faster and it can now open HEIF images. How awesome is that?</p>\n<p>I&#39;m also working on some 10.13 goodies for Acorn 6 folks later this year. I can&#39;t wait to share that with you, but you&#39;ll have to wait just a little bit.</p>\n",
+      "date_published": "2017-09-25T14:27:27-07:00",
+      "url": "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/api/src/test/resources/localfeed/json/json_items_other_cases.json b/api/src/test/resources/localfeed/json/json_items_other_cases.json
new file mode 100644
index 00000000..8e54b640
--- /dev/null
+++ b/api/src/test/resources/localfeed/json/json_items_other_cases.json
@@ -0,0 +1,40 @@
+{
+  "items": [
+    {
+      "id": "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html",
+      "title": "Acorn and 10.13",
+      "summary": "This is a summary",
+      "content_html": "content_html",
+      "content_text": "content_text",
+      "date_published": "2017-09-25T14:27:27-07:00",
+      "url": "http://flyingmeat.com/blog/archives/2017/9/acorn_and_10.13.html",
+      "image": "https://image.com",
+      "authors": [
+        {
+          "url": "url 1",
+          "name": "Author 1"
+        },
+        {
+          "url": "url 2",
+          "name": ""
+        },
+        {
+          "url": "url 3",
+          "name": "Author 3"
+        },
+        {
+          "url": "url 4",
+          "name": "Author 4"
+        },
+        {
+          "url": "url 5",
+          "name": "Author 5"
+        },
+        {
+          "url": "url 6",
+          "name": "Author 6"
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/api/src/test/resources/localfeed/rss1/rss1_feed.xml b/api/src/test/resources/localfeed/rss1/rss1_feed.xml
new file mode 100644
index 00000000..3c60e50e
--- /dev/null
+++ b/api/src/test/resources/localfeed/rss1/rss1_feed.xml
@@ -0,0 +1,269 @@
+<rdf:RDF xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+    xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:syn="http://purl.org/rss/1.0/modules/syndication/"
+    xmlns="http://purl.org/rss/1.0/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
+
+    <channel rdf:about="https://slashdot.org/">
+        <title>Slashdot</title>
+        <link>https://slashdot.org/</link>
+        <description>News for nerds, stuff that matters</description>
+        <dc:language>en-us</dc:language>
+        <dc:rights>Copyright 1997-2016, SlashdotMedia. All Rights Reserved.</dc:rights>
+        <dc:date>2020-09-23T16:20:20+00:00</dc:date>
+        <dc:publisher>Dice</dc:publisher>
+        <dc:creator>help@slashdot.org</dc:creator>
+        <dc:subject>Technology</dc:subject>
+        <syn:updateBase>1970-01-01T00:00+00:00</syn:updateBase>
+        <syn:updateFrequency>1</syn:updateFrequency>
+        <syn:updatePeriod>hourly</syn:updatePeriod>
+        <items>
+            <rdf:Seq>
+                <rdf:li
+                    rdf:resource="https://developers.slashdot.org/story/20/09/23/1616231/google-expands-its-flutter-development-kit-to-windows-apps?utm_source=rss1.0mainlinkanon&amp;utm_medium=feed" />
+                <rdf:li
+                    rdf:resource="https://news.slashdot.org/story/20/09/23/1528219/firefox-usage-is-down-85-despite-mozillas-top-exec-pay-going-up-400?utm_source=rss1.0mainlinkanon&amp;utm_medium=feed" />
+                <rdf:li
+                    rdf:resource="https://news.slashdot.org/story/20/09/23/1451213/climate-disruption-is-now-locked-in-the-next-moves-will-be-crucial?utm_source=rss1.0mainlinkanon&amp;utm_medium=feed" />
+                <rdf:li
+                    rdf:resource="https://news.slashdot.org/story/20/09/23/1420240/a-new-york-clock-that-told-time-now-tells-the-time-remaining?utm_source=rss1.0mainlinkanon&amp;utm_medium=feed" />
+                <rdf:li
+                    rdf:resource="https://news.slashdot.org/story/20/09/23/0057256/jeff-bezos-is-opening-his-first-tuition-free-bezos-academy-preschool-where-each-child-will-be-the-customer?utm_source=rss1.0mainlinkanon&amp;utm_medium=feed" />
+                <rdf:li
+                    rdf:resource="https://news.slashdot.org/story/20/09/23/0050249/google-is-pulling-the-plug-on-paid-chrome-extensions-over-the-next-year?utm_source=rss1.0mainlinkanon&amp;utm_medium=feed" />
+                <rdf:li
+                    rdf:resource="https://entertainment.slashdot.org/story/20/09/22/2147243/old-tv-caused-village-broadband-outages-for-18-months?utm_source=rss1.0mainlinkanon&amp;utm_medium=feed" />
+                <rdf:li
+                    rdf:resource="https://mobile.slashdot.org/story/20/09/22/2316256/t-mobile-amassed-unprecedented-concentration-of-spectrum-att-complains?utm_source=rss1.0mainlinkanon&amp;utm_medium=feed" />
+                <rdf:li
+                    rdf:resource="https://yro.slashdot.org/story/20/09/22/2030223/dark-web-drugs-raid-leads-to-179-arrests?utm_source=rss1.0mainlinkanon&amp;utm_medium=feed" />
+                <rdf:li
+                    rdf:resource="https://hardware.slashdot.org/story/20/09/22/2055203/the-fairphone-3-is-a-repairable-dream-that-takes-beautiful-photos?utm_source=rss1.0mainlinkanon&amp;utm_medium=feed" />
+                <rdf:li
+                    rdf:resource="https://tech.slashdot.org/story/20/09/23/0039222/tesla-unveils-model-s-plaid-520-miles-200-mph-and-0-60-mph-in-less-than-2s?utm_source=rss1.0mainlinkanon&amp;utm_medium=feed" />
+                <rdf:li
+                    rdf:resource="https://apple.slashdot.org/story/20/09/22/211222/apple-ceo-impressed-by-remote-work-sees-permanent-changes?utm_source=rss1.0mainlinkanon&amp;utm_medium=feed" />
+                <rdf:li
+                    rdf:resource="https://hardware.slashdot.org/story/20/09/22/236228/tesla-announces-tabless-battery-cells-that-will-improve-range-of-its-electric-cars?utm_source=rss1.0mainlinkanon&amp;utm_medium=feed" />
+                <rdf:li
+                    rdf:resource="https://linux.slashdot.org/story/20/09/22/2243209/linux-journal-is-back?utm_source=rss1.0mainlinkanon&amp;utm_medium=feed" />
+                <rdf:li
+                    rdf:resource="https://hardware.slashdot.org/story/20/09/22/2026238/shell-reportedly-to-slash-oil-and-gas-production-costs-to-focus-more-on-renewables?utm_source=rss1.0mainlinkanon&amp;utm_medium=feed" />
+            </rdf:Seq>
+        </items>
+        <image rdf:resource="https://a.fsdn.com/sd/topics/topicslashdot.gif" />
+        <textinput rdf:resource="https://slashdot.org/search.pl" />
+        <atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" href="http://rss.slashdot.org/slashdot/slashdotMain"
+            rel="self" type="application/rdf+xml" />
+        <feedburner:info xmlns:feedburner="http://rssnamespace.org/feedburner/ext/1.0"
+            uri="slashdot/slashdotmain" />
+        <atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" href="http://pubsubhubbub.appspot.com/"
+            rel="hub" />
+    </channel>
+    <image rdf:about="https://a.fsdn.com/sd/topics/topicslashdot.gif">
+        <title>Slashdot</title>
+        <url>https://a.fsdn.com/sd/topics/topicslashdot.gif</url>
+        <link>https://slashdot.org/</link>
+    </image>
+    <item
+        rdf:about="https://developers.slashdot.org/story/20/09/23/1616231/google-expands-its-flutter-development-kit-to-windows-apps?utm_source=rss1.0mainlinkanon&amp;utm_medium=feed">
+        <title>Google Expands its Flutter Development Kit To Windows Apps</title>
+        <link>
+            https://developers.slashdot.org/story/20/09/23/1616231/google-expands-its-flutter-development-kit-to-windows-apps?utm_source=rss1.0mainlinkanon&amp;utm_medium=feed
+        </link>
+        <description>Google has announced that Flutter, its open source UI development kit for
+            building cross-platform software from the same codebase, is finally available for
+            Windows apps in alpha. From a report:For the world's leading desktop operating system
+            with some 1 billion installations of Windows 10 alone, this has been a long time coming.
+            Flutter's alpha incarnation was initially launched at Google's I/O developer conference
+            back in 2017, before arriving in beta less than a year later. In its original guise,
+            Flutter was designed for Android and iOS app development, but it has since expanded to
+            cover the web, MacOS, and Linux, which are currently available in various alpha or beta
+            iterations. Developers have had to consider unique platform-specific factors when
+            designing for the desktop or mobile phones, such as different screen sizes and how
+            people interact with their devices. On smartphones, people typically use touch and
+            swipe-based gestures, while keyboards and mice are commonly used on PCs and laptops.
+            This means Flutter has had to expand its support to cover the additional inputs.&lt;p&gt;&lt;div
+            class="share_submission" style="position:relative;"&gt; &lt;a class="slashpop"
+            href="http://twitter.com/home?status=Google+Expands+its+Flutter+Development+Kit+To+Windows+Apps%3A+https%3A%2F%2Fbit.ly%2F32X36MW"&gt;&lt;img
+            src="https://a.fsdn.com/sd/twitter_icon_large.png"&gt;&lt;/a&gt; &lt;a class="slashpop"
+            href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Fdevelopers.slashdot.org%2Fstory%2F20%2F09%2F23%2F1616231%2Fgoogle-expands-its-flutter-development-kit-to-windows-apps%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"&gt;&lt;img
+            src="https://a.fsdn.com/sd/facebook_icon_large.png"&gt;&lt;/a&gt;
+
+
+            &lt;/div&gt;&lt;/p&gt;&lt;p&gt;&lt;a
+            href="https://developers.slashdot.org/story/20/09/23/1616231/google-expands-its-flutter-development-kit-to-windows-apps?utm_source=rss1.0moreanon&amp;amp;utm_medium=feed"&gt;Read
+            more of this story&lt;/a&gt; at Slashdot.&lt;/p&gt;&lt;iframe
+            src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;amp;id=17251868&amp;amp;smallembed=1"
+            style="height: 300px; width: 100%; border: none;"&gt;&lt;/iframe&gt;
+        </description>
+        <content:encoded>content:encoded</content:encoded>
+        <dc:creator>msmash</dc:creator>
+        <dc:date>2020-09-23T16:15:00+00:00</dc:date>
+        <dc:subject>programming</dc:subject>
+        <slash:department>how-about-that</slash:department>
+        <slash:section>developers</slash:section>
+        <slash:comments>1</slash:comments>
+        <slash:hit_parade>1,1,1,1,0,0,0</slash:hit_parade>
+    </item>
+    <item
+        rdf:about="https://news.slashdot.org/story/20/09/23/1528219/firefox-usage-is-down-85-despite-mozillas-top-exec-pay-going-up-400?utm_source=rss1.0mainlinkanon&amp;utm_medium=feed">
+        <title>Firefox Usage is Down 85% Despite Mozilla's Top Exec Pay Going Up 400%</title>
+        <link>
+            https://news.slashdot.org/story/20/09/23/1528219/firefox-usage-is-down-85-despite-mozillas-top-exec-pay-going-up-400?utm_source=rss1.0mainlinkanon&amp;utm_medium=feed
+        </link>
+        <description>Software engineer Cal Paterson writes: Mozilla recently announced that they
+            would be dismissing 250 people. That's a quarter of their workforce so there are some
+            deep cuts to their work too. The victims include: the MDN docs (those are the web
+            standards docs everyone likes better than w3schools), the Rust compiler and even some
+            cuts to Firefox development. Like most people I want to see Mozilla do well but those
+            three projects comprise pretty much what I think of as the whole point of Mozilla, so
+            this news is a a big let down. The stated reason for the cuts is falling income. Mozilla
+            largely relies on "royalties" for funding. In return for payment, Mozilla allows big
+            technology companies to choose the default search engine in Firefox - the technology
+            companies are ultimately paying to increase the number of searches Firefox users make
+            with them. Mozilla haven't been particularly transparent about why these royalties are
+            being reduced, except to blame the coronavirus. I'm sure the coronavirus is not a great
+            help but I suspect the bigger problem is that Firefox's market share is now a tiny
+            fraction of its previous size and so the royalties will be smaller too - fewer users, so
+            fewer searches and therefore less money for Mozilla.
+
+            The real problem is not the royalty cuts, though. Mozilla has already received more than
+            enough money to set themselves up for financial independence. Mozilla received up to
+            half a billion dollars a year (each year!) for many years. The real problem is that
+            Mozilla didn't use that money to achieve financial independence and instead just spent
+            it each year, doing the organisational equivalent of living hand-to-mouth. Despite their
+            slightly contrived legal structure as a non-profit that owns a for-profit, Mozilla are
+            an NGO just like any other. In this article I want to apply the traditional measures
+            that are applied to other NGOs to Mozilla in order to show what's wrong. These three
+            measures are: overheads, ethics and results.&lt;p&gt;&lt;div class="share_submission"
+            style="position:relative;"&gt; &lt;a class="slashpop"
+            href="http://twitter.com/home?status=Firefox+Usage+is+Down+85%25+Despite+Mozilla's+Top+Exec+Pay+Going+Up+400%25%3A+https%3A%2F%2Fbit.ly%2F33M9FB2"&gt;&lt;img
+            src="https://a.fsdn.com/sd/twitter_icon_large.png"&gt;&lt;/a&gt; &lt;a class="slashpop"
+            href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Fnews.slashdot.org%2Fstory%2F20%2F09%2F23%2F1528219%2Ffirefox-usage-is-down-85-despite-mozillas-top-exec-pay-going-up-400%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"&gt;&lt;img
+            src="https://a.fsdn.com/sd/facebook_icon_large.png"&gt;&lt;/a&gt;
+
+
+            &lt;/div&gt;&lt;/p&gt;&lt;p&gt;&lt;a
+            href="https://news.slashdot.org/story/20/09/23/1528219/firefox-usage-is-down-85-despite-mozillas-top-exec-pay-going-up-400?utm_source=rss1.0moreanon&amp;amp;utm_medium=feed"&gt;Read
+            more of this story&lt;/a&gt; at Slashdot.&lt;/p&gt;&lt;iframe
+            src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;amp;id=17251650&amp;amp;smallembed=1"
+            style="height: 300px; width: 100%; border: none;"&gt;&lt;/iframe&gt;
+        </description>
+        <dc:creator>msmash</dc:creator>
+        <dc:date>2020-09-23T15:27:00+00:00</dc:date>
+        <dc:subject>firefox</dc:subject>
+        <slash:department>closer-look</slash:department>
+        <slash:section>news</slash:section>
+        <slash:comments>31</slash:comments>
+        <slash:hit_parade>31,29,24,21,3,1,0</slash:hit_parade>
+    </item>
+    <item
+        rdf:about="https://news.slashdot.org/story/20/09/23/1451213/climate-disruption-is-now-locked-in-the-next-moves-will-be-crucial?utm_source=rss1.0mainlinkanon&amp;utm_medium=feed">
+        <title>Climate Disruption Is Now Locked In. The Next Moves Will Be Crucial.</title>
+        <link>
+            https://news.slashdot.org/story/20/09/23/1451213/climate-disruption-is-now-locked-in-the-next-moves-will-be-crucial?utm_source=rss1.0mainlinkanon&amp;utm_medium=feed
+        </link>
+        <description>America is now under siege by climate change in ways that scientists have
+            warned about for years. But there is a second part to their admonition: Decades of
+            growing crisis are already locked into the global ecosystem and cannot be reversed. From
+            a report: This means the kinds of cascading disasters occurring today -- drought in the
+            West fueling historic wildfires that send smoke all the way to the East Coast, or
+            parades of tropical storms lining up across the Atlantic to march destructively toward
+            North America -- are no longer features of some dystopian future. They are the here and
+            now, worsening for the next generation and perhaps longer, depending on humanity's
+            willingness to take action. "I've been labeled an alarmist," said Peter Kalmus, a
+            climate scientist in Los Angeles, where he and millions of others have inhaled
+            dangerously high levels of smoke for weeks. "And I think it's a lot harder for people to
+            say that I'm being alarmist now." Last month, before the skies over San Francisco turned
+            a surreal orange, Death Valley reached 130 degrees Fahrenheit, the highest temperature
+            ever measured on the planet. Dozens of people have perished from the heat in Phoenix,
+            which in July suffered its hottest month on record, only to surpass that milestone in
+            August.
+
+            Conversations about climate change have broken into everyday life, to the top of the
+            headlines and to center stage in the presidential campaign. The questions are profound
+            and urgent. Can this be reversed? What can be done to minimize the looming dangers for
+            the decades ahead? Will the destruction of recent weeks become a moment of reckoning, or
+            just a blip in the news cycle? The Times spoke with two dozen climate experts, including
+            scientists, economists, sociologists and policymakers, and their answers were by turns
+            alarming, cynical and hopeful. "It's as if we've been smoking a pack of cigarettes a day
+            for decades" and the world is now feeling the effects, said Katharine Hayhoe, a climate
+            scientist at Texas Tech University. But, she said, "we're not dead yet." Their most
+            sobering message was that the world still hasn't seen the worst of it. Gone is the
+            climate of yesteryear, and there's no going back. The effects of climate change evident
+            today are the results of choices that countries made decades ago to keep pumping
+            heat-trapping greenhouse gases into the atmosphere at ever-increasing rates despite
+            warnings from scientists about the price to be paid.&lt;p&gt;&lt;div
+            class="share_submission" style="position:relative;"&gt; &lt;a class="slashpop"
+            href="http://twitter.com/home?status=Climate+Disruption+Is+Now+Locked+In.+The+Next+Moves+Will+Be+Crucial.%3A+https%3A%2F%2Fbit.ly%2F32TsNxO"&gt;&lt;img
+            src="https://a.fsdn.com/sd/twitter_icon_large.png"&gt;&lt;/a&gt; &lt;a class="slashpop"
+            href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Fnews.slashdot.org%2Fstory%2F20%2F09%2F23%2F1451213%2Fclimate-disruption-is-now-locked-in-the-next-moves-will-be-crucial%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"&gt;&lt;img
+            src="https://a.fsdn.com/sd/facebook_icon_large.png"&gt;&lt;/a&gt;
+
+
+            &lt;/div&gt;&lt;/p&gt;&lt;p&gt;&lt;a
+            href="https://news.slashdot.org/story/20/09/23/1451213/climate-disruption-is-now-locked-in-the-next-moves-will-be-crucial?utm_source=rss1.0moreanon&amp;amp;utm_medium=feed"&gt;Read
+            more of this story&lt;/a&gt; at Slashdot.&lt;/p&gt;&lt;iframe
+            src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;amp;id=17251462&amp;amp;smallembed=1"
+            style="height: 300px; width: 100%; border: none;"&gt;&lt;/iframe&gt;
+        </description>
+        <dc:creator>msmash</dc:creator>
+        <dc:date>2020-09-23T14:51:00+00:00</dc:date>
+        <dc:subject>earth</dc:subject>
+        <slash:department>closer-look</slash:department>
+        <slash:section>news</slash:section>
+        <slash:comments>67</slash:comments>
+        <slash:hit_parade>67,61,50,33,12,2,1</slash:hit_parade>
+    </item>
+    <item
+        rdf:about="https://news.slashdot.org/story/20/09/23/1420240/a-new-york-clock-that-told-time-now-tells-the-time-remaining?utm_source=rss1.0mainlinkanon&amp;utm_medium=feed">
+        <title>A New York Clock That Told Time Now Tells the Time Remaining</title>
+        <link>
+            https://news.slashdot.org/story/20/09/23/1420240/a-new-york-clock-that-told-time-now-tells-the-time-remaining?utm_source=rss1.0mainlinkanon&amp;utm_medium=feed
+        </link>
+        <description>For more than 20 years, Metronome, which includes a 62-foot-wide 15-digit
+            electronic clock that faces Union Square in Manhattan, has been one of the city's most
+            prominent and baffling public art projects. Its digital display once told the time in
+            its own unique way, counting the hours, minutes and seconds (and fractions thereof) to
+            and from midnight. But for years observers who did not understand how it worked
+            suggested that it was measuring the acres of rainforest destroyed each year, tracking
+            the world population or even that it had something to do with pi. On Saturday Metronome
+            adopted a new ecologically sensitive mission. From a report: Now, instead of measuring
+            24-hour cycles, it is measuring what two artists, Gan Golan and Andrew Boyd, present as
+            a critical window for action to prevent the effects of global warming from becoming
+            irreversible. On Saturday at 3:20 p.m., messages including "The Earth has a deadline"
+            began to appear on the display. Then numbers -- 7:103:15:40:07 -- showed up,
+            representing the years, days, hours, minutes and seconds until that deadline. As a
+            handful of supporters watched, the number -- which the artists said was based on
+            calculations by the Mercator Research Institute on Global Commons and Climate Change in
+            Berlin -- began ticking down, second by second.
+
+            "This is our way to shout that number from the rooftops." Mr. Golan said just before the
+            countdown began. "The world is literally counting on us." The Climate Clock, as the two
+            artists call their project, will be displayed on the 14th Street building, One Union
+            Square South, through Sept. 27, the end of Climate Week. The creators say their aim is
+            to arrange for the clock to be permanently displayed, there or elsewhere. Mr. Golan said
+            he came up with the idea to publicly illustrate the urgency of combating climate change
+            about two years ago, shortly after his daughter was born. He asked Mr. Boyd, an activist
+            from the Lower East Side, to work with him on the project.&lt;p&gt;&lt;div
+            class="share_submission" style="position:relative;"&gt; &lt;a class="slashpop"
+            href="http://twitter.com/home?status=A+New+York+Clock+That+Told+Time+Now+Tells+the+Time+Remaining%3A+https%3A%2F%2Fbit.ly%2F2HrAt2b"&gt;&lt;img
+            src="https://a.fsdn.com/sd/twitter_icon_large.png"&gt;&lt;/a&gt; &lt;a class="slashpop"
+            href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Fnews.slashdot.org%2Fstory%2F20%2F09%2F23%2F1420240%2Fa-new-york-clock-that-told-time-now-tells-the-time-remaining%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"&gt;&lt;img
+            src="https://a.fsdn.com/sd/facebook_icon_large.png"&gt;&lt;/a&gt;
+
+
+            &lt;/div&gt;&lt;/p&gt;&lt;p&gt;&lt;a
+            href="https://news.slashdot.org/story/20/09/23/1420240/a-new-york-clock-that-told-time-now-tells-the-time-remaining?utm_source=rss1.0moreanon&amp;amp;utm_medium=feed"&gt;Read
+            more of this story&lt;/a&gt; at Slashdot.&lt;/p&gt;&lt;iframe
+            src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;amp;id=17251272&amp;amp;smallembed=1"
+            style="height: 300px; width: 100%; border: none;"&gt;&lt;/iframe&gt;
+        </description>
+        <dc:creator>msmash</dc:creator>
+        <dc:date>2020-09-23T14:10:00+00:00</dc:date>
+        <dc:subject>news</dc:subject>
+        <slash:department>how-about-that</slash:department>
+        <slash:section>news</slash:section>
+        <slash:comments>43</slash:comments>
+        <slash:hit_parade>43,38,33,27,6,1,0</slash:hit_parade>
+    </item>
+</rdf:RDF>
\ No newline at end of file
diff --git a/api/src/test/resources/localfeed/rss1/rss1_feed_no_url_siteurl.xml b/api/src/test/resources/localfeed/rss1/rss1_feed_no_url_siteurl.xml
new file mode 100644
index 00000000..dd49bb0c
--- /dev/null
+++ b/api/src/test/resources/localfeed/rss1/rss1_feed_no_url_siteurl.xml
@@ -0,0 +1,31 @@
+<rdf:RDF xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+    xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:syn="http://purl.org/rss/1.0/modules/syndication/"
+    xmlns="http://purl.org/rss/1.0/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
+
+    <channel>
+        <title>Slashdot</title>
+        <description>News for nerds, stuff that matters</description>
+        <dc:language>en-us</dc:language>
+        <dc:rights>Copyright 1997-2016, SlashdotMedia. All Rights Reserved.</dc:rights>
+        <dc:date>2020-09-23T16:20:20+00:00</dc:date>
+        <dc:publisher>Dice</dc:publisher>
+        <dc:creator>help@slashdot.org</dc:creator>
+        <dc:subject>Technology</dc:subject>
+        <syn:updateBase>1970-01-01T00:00+00:00</syn:updateBase>
+        <syn:updateFrequency>1</syn:updateFrequency>
+        <syn:updatePeriod>hourly</syn:updatePeriod>
+        <image rdf:resource="https://a.fsdn.com/sd/topics/topicslashdot.gif" />
+        <textinput rdf:resource="https://slashdot.org/search.pl" />
+        <atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" href="http://rss.slashdot.org/slashdot/slashdotMain"
+            rel="self" type="application/rdf+xml" />
+        <feedburner:info xmlns:feedburner="http://rssnamespace.org/feedburner/ext/1.0"
+            uri="slashdot/slashdotmain" />
+        <atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" href="http://pubsubhubbub.appspot.com/"
+            rel="hub" />
+    </channel>
+    <image rdf:about="https://a.fsdn.com/sd/topics/topicslashdot.gif">
+        <title>Slashdot</title>
+        <url>https://a.fsdn.com/sd/topics/topicslashdot.gif</url>
+        <link>https://slashdot.org/</link>
+    </image>
+</rdf:RDF>
\ No newline at end of file
diff --git a/api/src/test/resources/localfeed/rss1/rss1_items_no_date.xml b/api/src/test/resources/localfeed/rss1/rss1_items_no_date.xml
new file mode 100644
index 00000000..b4aaab5c
--- /dev/null
+++ b/api/src/test/resources/localfeed/rss1/rss1_items_no_date.xml
@@ -0,0 +1,43 @@
+<rdf:RDF xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+    xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:syn="http://purl.org/rss/1.0/modules/syndication/"
+    xmlns="http://purl.org/rss/1.0/">
+    <item
+        rdf:about="https://developers.slashdot.org/story/20/09/23/1616231/google-expands-its-flutter-development-kit-to-windows-apps?utm_source=rss1.0mainlinkanon&amp;utm_medium=feed">
+        <title>Google Expands its Flutter Development Kit To Windows Apps</title>
+        <link>
+            https://developers.slashdot.org/story/20/09/23/1616231/google-expands-its-flutter-development-kit-to-windows-apps?utm_source=rss1.0mainlinkanon&amp;utm_medium=feed
+        </link>
+        <description>Google has announced that Flutter, its open source UI development kit for
+            building cross-platform software from the same codebase, is finally available for
+            Windows apps in alpha. From a report:For the world's leading desktop operating system
+            with some 1 billion installations of Windows 10 alone, this has been a long time coming.
+            Flutter's alpha incarnation was initially launched at Google's I/O developer conference
+            back in 2017, before arriving in beta less than a year later. In its original guise,
+            Flutter was designed for Android and iOS app development, but it has since expanded to
+            cover the web, MacOS, and Linux, which are currently available in various alpha or beta
+            iterations. Developers have had to consider unique platform-specific factors when
+            designing for the desktop or mobile phones, such as different screen sizes and how
+            people interact with their devices. On smartphones, people typically use touch and
+            swipe-based gestures, while keyboards and mice are commonly used on PCs and laptops.
+            This means Flutter has had to expand its support to cover the additional inputs.&lt;p&gt;&lt;div
+            class="share_submission" style="position:relative;"&gt; &lt;a class="slashpop"
+            href="http://twitter.com/home?status=Google+Expands+its+Flutter+Development+Kit+To+Windows+Apps%3A+https%3A%2F%2Fbit.ly%2F32X36MW"&gt;&lt;img
+            src="https://a.fsdn.com/sd/twitter_icon_large.png"&gt;&lt;/a&gt; &lt;a class="slashpop"
+            href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Fdevelopers.slashdot.org%2Fstory%2F20%2F09%2F23%2F1616231%2Fgoogle-expands-its-flutter-development-kit-to-windows-apps%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"&gt;&lt;img
+            src="https://a.fsdn.com/sd/facebook_icon_large.png"&gt;&lt;/a&gt;
+
+
+            &lt;/div&gt;&lt;/p&gt;&lt;p&gt;&lt;a
+            href="https://developers.slashdot.org/story/20/09/23/1616231/google-expands-its-flutter-development-kit-to-windows-apps?utm_source=rss1.0moreanon&amp;amp;utm_medium=feed"&gt;Read
+            more of this story&lt;/a&gt; at Slashdot.&lt;/p&gt;&lt;iframe
+            src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;amp;id=17251868&amp;amp;smallembed=1"
+            style="height: 300px; width: 100%; border: none;"&gt;&lt;/iframe&gt;
+        </description>
+        <dc:creator>msmash</dc:creator>
+        <dc:subject>programming</dc:subject>
+        <slash:department>how-about-that</slash:department>
+        <slash:section>developers</slash:section>
+        <slash:comments>1</slash:comments>
+        <slash:hit_parade>1,1,1,1,0,0,0</slash:hit_parade>
+    </item>
+</rdf:RDF>
\ No newline at end of file
diff --git a/api/src/test/resources/localfeed/rss1/rss1_items_no_link.xml b/api/src/test/resources/localfeed/rss1/rss1_items_no_link.xml
new file mode 100644
index 00000000..b927c248
--- /dev/null
+++ b/api/src/test/resources/localfeed/rss1/rss1_items_no_link.xml
@@ -0,0 +1,40 @@
+<rdf:RDF xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+    xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:syn="http://purl.org/rss/1.0/modules/syndication/"
+    xmlns="http://purl.org/rss/1.0/">
+    <item>
+        <title>Google Expands its Flutter Development Kit To Windows Apps</title>
+        <description>Google has announced that Flutter, its open source UI development kit for
+            building cross-platform software from the same codebase, is finally available for
+            Windows apps in alpha. From a report:For the world's leading desktop operating system
+            with some 1 billion installations of Windows 10 alone, this has been a long time coming.
+            Flutter's alpha incarnation was initially launched at Google's I/O developer conference
+            back in 2017, before arriving in beta less than a year later. In its original guise,
+            Flutter was designed for Android and iOS app development, but it has since expanded to
+            cover the web, MacOS, and Linux, which are currently available in various alpha or beta
+            iterations. Developers have had to consider unique platform-specific factors when
+            designing for the desktop or mobile phones, such as different screen sizes and how
+            people interact with their devices. On smartphones, people typically use touch and
+            swipe-based gestures, while keyboards and mice are commonly used on PCs and laptops.
+            This means Flutter has had to expand its support to cover the additional inputs.&lt;p&gt;&lt;div
+            class="share_submission" style="position:relative;"&gt; &lt;a class="slashpop"
+            href="http://twitter.com/home?status=Google+Expands+its+Flutter+Development+Kit+To+Windows+Apps%3A+https%3A%2F%2Fbit.ly%2F32X36MW"&gt;&lt;img
+            src="https://a.fsdn.com/sd/twitter_icon_large.png"&gt;&lt;/a&gt; &lt;a class="slashpop"
+            href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Fdevelopers.slashdot.org%2Fstory%2F20%2F09%2F23%2F1616231%2Fgoogle-expands-its-flutter-development-kit-to-windows-apps%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"&gt;&lt;img
+            src="https://a.fsdn.com/sd/facebook_icon_large.png"&gt;&lt;/a&gt;
+
+
+            &lt;/div&gt;&lt;/p&gt;&lt;p&gt;&lt;a
+            href="https://developers.slashdot.org/story/20/09/23/1616231/google-expands-its-flutter-development-kit-to-windows-apps?utm_source=rss1.0moreanon&amp;amp;utm_medium=feed"&gt;Read
+            more of this story&lt;/a&gt; at Slashdot.&lt;/p&gt;&lt;iframe
+            src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;amp;id=17251868&amp;amp;smallembed=1"
+            style="height: 300px; width: 100%; border: none;"&gt;&lt;/iframe&gt;
+        </description>
+        <dc:creator>msmash</dc:creator>
+        <dc:date>2020-09-23T16:15:00+00:00</dc:date>
+        <dc:subject>programming</dc:subject>
+        <slash:department>how-about-that</slash:department>
+        <slash:section>developers</slash:section>
+        <slash:comments>1</slash:comments>
+        <slash:hit_parade>1,1,1,1,0,0,0</slash:hit_parade>
+    </item>
+</rdf:RDF>
\ No newline at end of file
diff --git a/api/src/test/resources/localfeed/rss1/rss1_items_no_title.xml b/api/src/test/resources/localfeed/rss1/rss1_items_no_title.xml
new file mode 100644
index 00000000..96e0c123
--- /dev/null
+++ b/api/src/test/resources/localfeed/rss1/rss1_items_no_title.xml
@@ -0,0 +1,43 @@
+<rdf:RDF xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+    xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:syn="http://purl.org/rss/1.0/modules/syndication/"
+    xmlns="http://purl.org/rss/1.0/">
+    <item
+        rdf:about="https://developers.slashdot.org/story/20/09/23/1616231/google-expands-its-flutter-development-kit-to-windows-apps?utm_source=rss1.0mainlinkanon&amp;utm_medium=feed">
+        <link>
+            https://developers.slashdot.org/story/20/09/23/1616231/google-expands-its-flutter-development-kit-to-windows-apps?utm_source=rss1.0mainlinkanon&amp;utm_medium=feed
+        </link>
+        <description>Google has announced that Flutter, its open source UI development kit for
+            building cross-platform software from the same codebase, is finally available for
+            Windows apps in alpha. From a report:For the world's leading desktop operating system
+            with some 1 billion installations of Windows 10 alone, this has been a long time coming.
+            Flutter's alpha incarnation was initially launched at Google's I/O developer conference
+            back in 2017, before arriving in beta less than a year later. In its original guise,
+            Flutter was designed for Android and iOS app development, but it has since expanded to
+            cover the web, MacOS, and Linux, which are currently available in various alpha or beta
+            iterations. Developers have had to consider unique platform-specific factors when
+            designing for the desktop or mobile phones, such as different screen sizes and how
+            people interact with their devices. On smartphones, people typically use touch and
+            swipe-based gestures, while keyboards and mice are commonly used on PCs and laptops.
+            This means Flutter has had to expand its support to cover the additional inputs.&lt;p&gt;&lt;div
+            class="share_submission" style="position:relative;"&gt; &lt;a class="slashpop"
+            href="http://twitter.com/home?status=Google+Expands+its+Flutter+Development+Kit+To+Windows+Apps%3A+https%3A%2F%2Fbit.ly%2F32X36MW"&gt;&lt;img
+            src="https://a.fsdn.com/sd/twitter_icon_large.png"&gt;&lt;/a&gt; &lt;a class="slashpop"
+            href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Fdevelopers.slashdot.org%2Fstory%2F20%2F09%2F23%2F1616231%2Fgoogle-expands-its-flutter-development-kit-to-windows-apps%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"&gt;&lt;img
+            src="https://a.fsdn.com/sd/facebook_icon_large.png"&gt;&lt;/a&gt;
+
+
+            &lt;/div&gt;&lt;/p&gt;&lt;p&gt;&lt;a
+            href="https://developers.slashdot.org/story/20/09/23/1616231/google-expands-its-flutter-development-kit-to-windows-apps?utm_source=rss1.0moreanon&amp;amp;utm_medium=feed"&gt;Read
+            more of this story&lt;/a&gt; at Slashdot.&lt;/p&gt;&lt;iframe
+            src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;amp;id=17251868&amp;amp;smallembed=1"
+            style="height: 300px; width: 100%; border: none;"&gt;&lt;/iframe&gt;
+        </description>
+        <dc:creator>msmash</dc:creator>
+        <dc:date>2020-09-23T16:15:00+00:00</dc:date>
+        <dc:subject>programming</dc:subject>
+        <slash:department>how-about-that</slash:department>
+        <slash:section>developers</slash:section>
+        <slash:comments>1</slash:comments>
+        <slash:hit_parade>1,1,1,1,0,0,0</slash:hit_parade>
+    </item>
+</rdf:RDF>
\ No newline at end of file
diff --git a/api/src/test/resources/localfeed/rss1/rss1_items_special_cases.xml b/api/src/test/resources/localfeed/rss1/rss1_items_special_cases.xml
new file mode 100644
index 00000000..2624009b
--- /dev/null
+++ b/api/src/test/resources/localfeed/rss1/rss1_items_special_cases.xml
@@ -0,0 +1,120 @@
+<rdf:RDF xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+    xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:syn="http://purl.org/rss/1.0/modules/syndication/"
+    xmlns="http://purl.org/rss/1.0/">
+
+    <channel rdf:about="https://slashdot.org/">
+        <title>Slashdot</title>
+        <link>https://slashdot.org/</link>
+        <description>News for nerds, stuff that matters</description>
+        <dc:language>en-us</dc:language>
+        <dc:rights>Copyright 1997-2016, SlashdotMedia. All Rights Reserved.</dc:rights>
+        <dc:date>2020-09-23T16:20:20+00:00</dc:date>
+        <dc:publisher>Dice</dc:publisher>
+        <dc:creator>help@slashdot.org</dc:creator>
+        <dc:subject>Technology</dc:subject>
+        <syn:updateBase>1970-01-01T00:00+00:00</syn:updateBase>
+        <syn:updateFrequency>1</syn:updateFrequency>
+        <syn:updatePeriod>hourly</syn:updatePeriod>
+        <items>
+            <rdf:Seq>
+                <rdf:li
+                    rdf:resource="https://developers.slashdot.org/story/20/09/23/1616231/google-expands-its-flutter-development-kit-to-windows-apps?utm_source=rss1.0mainlinkanon&amp;utm_medium=feed" />
+                <rdf:li
+                    rdf:resource="https://news.slashdot.org/story/20/09/23/1528219/firefox-usage-is-down-85-despite-mozillas-top-exec-pay-going-up-400?utm_source=rss1.0mainlinkanon&amp;utm_medium=feed" />
+                <rdf:li
+                    rdf:resource="https://news.slashdot.org/story/20/09/23/1451213/climate-disruption-is-now-locked-in-the-next-moves-will-be-crucial?utm_source=rss1.0mainlinkanon&amp;utm_medium=feed" />
+                <rdf:li
+                    rdf:resource="https://news.slashdot.org/story/20/09/23/1420240/a-new-york-clock-that-told-time-now-tells-the-time-remaining?utm_source=rss1.0mainlinkanon&amp;utm_medium=feed" />
+                <rdf:li
+                    rdf:resource="https://news.slashdot.org/story/20/09/23/0057256/jeff-bezos-is-opening-his-first-tuition-free-bezos-academy-preschool-where-each-child-will-be-the-customer?utm_source=rss1.0mainlinkanon&amp;utm_medium=feed" />
+                <rdf:li
+                    rdf:resource="https://news.slashdot.org/story/20/09/23/0050249/google-is-pulling-the-plug-on-paid-chrome-extensions-over-the-next-year?utm_source=rss1.0mainlinkanon&amp;utm_medium=feed" />
+                <rdf:li
+                    rdf:resource="https://entertainment.slashdot.org/story/20/09/22/2147243/old-tv-caused-village-broadband-outages-for-18-months?utm_source=rss1.0mainlinkanon&amp;utm_medium=feed" />
+                <rdf:li
+                    rdf:resource="https://mobile.slashdot.org/story/20/09/22/2316256/t-mobile-amassed-unprecedented-concentration-of-spectrum-att-complains?utm_source=rss1.0mainlinkanon&amp;utm_medium=feed" />
+                <rdf:li
+                    rdf:resource="https://yro.slashdot.org/story/20/09/22/2030223/dark-web-drugs-raid-leads-to-179-arrests?utm_source=rss1.0mainlinkanon&amp;utm_medium=feed" />
+                <rdf:li
+                    rdf:resource="https://hardware.slashdot.org/story/20/09/22/2055203/the-fairphone-3-is-a-repairable-dream-that-takes-beautiful-photos?utm_source=rss1.0mainlinkanon&amp;utm_medium=feed" />
+                <rdf:li
+                    rdf:resource="https://tech.slashdot.org/story/20/09/23/0039222/tesla-unveils-model-s-plaid-520-miles-200-mph-and-0-60-mph-in-less-than-2s?utm_source=rss1.0mainlinkanon&amp;utm_medium=feed" />
+                <rdf:li
+                    rdf:resource="https://apple.slashdot.org/story/20/09/22/211222/apple-ceo-impressed-by-remote-work-sees-permanent-changes?utm_source=rss1.0mainlinkanon&amp;utm_medium=feed" />
+                <rdf:li
+                    rdf:resource="https://hardware.slashdot.org/story/20/09/22/236228/tesla-announces-tabless-battery-cells-that-will-improve-range-of-its-electric-cars?utm_source=rss1.0mainlinkanon&amp;utm_medium=feed" />
+                <rdf:li
+                    rdf:resource="https://linux.slashdot.org/story/20/09/22/2243209/linux-journal-is-back?utm_source=rss1.0mainlinkanon&amp;utm_medium=feed" />
+                <rdf:li
+                    rdf:resource="https://hardware.slashdot.org/story/20/09/22/2026238/shell-reportedly-to-slash-oil-and-gas-production-costs-to-focus-more-on-renewables?utm_source=rss1.0mainlinkanon&amp;utm_medium=feed" />
+            </rdf:Seq>
+        </items>
+        <image rdf:resource="https://a.fsdn.com/sd/topics/topicslashdot.gif" />
+        <textinput rdf:resource="https://slashdot.org/search.pl" />
+        <atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" href="http://rss.slashdot.org/slashdot/slashdotMain"
+            rel="self" type="application/rdf+xml" />
+        <feedburner:info xmlns:feedburner="http://rssnamespace.org/feedburner/ext/1.0"
+            uri="slashdot/slashdotmain" />
+        <atom10:link xmlns:atom10="http://www.w3.org/2005/Atom" href="http://pubsubhubbub.appspot.com/"
+            rel="hub" />
+    </channel>
+    <image rdf:about="https://a.fsdn.com/sd/topics/topicslashdot.gif">
+        <title>Slashdot</title>
+        <url>https://a.fsdn.com/sd/topics/topicslashdot.gif</url>
+        <link>https://slashdot.org/</link>
+    </image>
+    <item
+        rdf:about="https://news.slashdot.org/story/20/09/23/1420240/a-new-york-clock-that-told-time-now-tells-the-time-remaining?utm_source=rss1.0mainlinkanon&amp;utm_medium=feed">
+        <title>A New York Clock That Told Time Now Tells the Time Remaining</title>
+        <description>For more than 20 years, Metronome, which includes a 62-foot-wide 15-digit
+            electronic clock that faces Union Square in Manhattan, has been one of the city's most
+            prominent and baffling public art projects. Its digital display once told the time in
+            its own unique way, counting the hours, minutes and seconds (and fractions thereof) to
+            and from midnight. But for years observers who did not understand how it worked
+            suggested that it was measuring the acres of rainforest destroyed each year, tracking
+            the world population or even that it had something to do with pi. On Saturday Metronome
+            adopted a new ecologically sensitive mission. From a report: Now, instead of measuring
+            24-hour cycles, it is measuring what two artists, Gan Golan and Andrew Boyd, present as
+            a critical window for action to prevent the effects of global warming from becoming
+            irreversible. On Saturday at 3:20 p.m., messages including "The Earth has a deadline"
+            began to appear on the display. Then numbers -- 7:103:15:40:07 -- showed up,
+            representing the years, days, hours, minutes and seconds until that deadline. As a
+            handful of supporters watched, the number -- which the artists said was based on
+            calculations by the Mercator Research Institute on Global Commons and Climate Change in
+            Berlin -- began ticking down, second by second.
+
+            "This is our way to shout that number from the rooftops." Mr. Golan said just before the
+            countdown began. "The world is literally counting on us." The Climate Clock, as the two
+            artists call their project, will be displayed on the 14th Street building, One Union
+            Square South, through Sept. 27, the end of Climate Week. The creators say their aim is
+            to arrange for the clock to be permanently displayed, there or elsewhere. Mr. Golan said
+            he came up with the idea to publicly illustrate the urgency of combating climate change
+            about two years ago, shortly after his daughter was born. He asked Mr. Boyd, an activist
+            from the Lower East Side, to work with him on the project.&lt;p&gt;&lt;div
+            class="share_submission" style="position:relative;"&gt; &lt;a class="slashpop"
+            href="http://twitter.com/home?status=A+New+York+Clock+That+Told+Time+Now+Tells+the+Time+Remaining%3A+https%3A%2F%2Fbit.ly%2F2HrAt2b"&gt;&lt;img
+            src="https://a.fsdn.com/sd/twitter_icon_large.png"&gt;&lt;/a&gt; &lt;a class="slashpop"
+            href="http://www.facebook.com/sharer.php?u=https%3A%2F%2Fnews.slashdot.org%2Fstory%2F20%2F09%2F23%2F1420240%2Fa-new-york-clock-that-told-time-now-tells-the-time-remaining%3Futm_source%3Dslashdot%26utm_medium%3Dfacebook"&gt;&lt;img
+            src="https://a.fsdn.com/sd/facebook_icon_large.png"&gt;&lt;/a&gt;
+
+
+            &lt;/div&gt;&lt;/p&gt;&lt;p&gt;&lt;a
+            href="https://news.slashdot.org/story/20/09/23/1420240/a-new-york-clock-that-told-time-now-tells-the-time-remaining?utm_source=rss1.0moreanon&amp;amp;utm_medium=feed"&gt;Read
+            more of this story&lt;/a&gt; at Slashdot.&lt;/p&gt;&lt;iframe
+            src="https://slashdot.org/slashdot-it.pl?op=discuss&amp;amp;id=17251272&amp;amp;smallembed=1"
+            style="height: 300px; width: 100%; border: none;"&gt;&lt;/iframe&gt;
+        </description>
+        <dc:creator>msmash</dc:creator>
+        <dc:creator></dc:creator>
+        <dc:creator>creator 2</dc:creator>
+        <dc:creator>creator 3</dc:creator>
+        <dc:creator>creator 4</dc:creator>
+        <dc:creator>creator 5</dc:creator>
+        <dc:date>2020-09-23T14:10:00+00:00</dc:date>
+        <dc:subject>news</dc:subject>
+        <slash:department>how-about-that</slash:department>
+        <slash:section>news</slash:section>
+        <slash:comments>43</slash:comments>
+        <slash:hit_parade>43,38,33,27,6,1,0</slash:hit_parade>
+    </item>
+</rdf:RDF>
\ No newline at end of file
diff --git a/api/src/test/resources/localfeed/rss2/rss_feed_special_cases.xml b/api/src/test/resources/localfeed/rss2/rss_feed_special_cases.xml
new file mode 100644
index 00000000..55f74777
--- /dev/null
+++ b/api/src/test/resources/localfeed/rss2/rss_feed_special_cases.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
+    <channel>
+        <title></title>
+        <atom:link href="https://news.ycombinator.com/feed/" rel="self"/>
+        <link>https://news.ycombinator.com/</link>
+        <description>Links for the intellectually curious, ranked by readers.</description>
+    </channel>
+</rss>
\ No newline at end of file
diff --git a/api/src/test/resources/localfeed/rss2/rss_full_feed.xml b/api/src/test/resources/localfeed/rss2/rss_full_feed.xml
new file mode 100644
index 00000000..e56ad91b
--- /dev/null
+++ b/api/src/test/resources/localfeed/rss2/rss_full_feed.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
+    <channel>
+        <title>Hacker News</title>
+        <atom:link href="https://news.ycombinator.com/feed/" rel="self" />
+        <atom:link href="https://news.ycombinator.com/hub" rel="hub" />
+        <link>https://news.ycombinator.com/</link>
+        <description>Links for the intellectually curious, ranked by readers.</description>
+    </channel>
+</rss>
\ No newline at end of file
diff --git a/api/src/test/resources/localfeed/rss2/rss_items_enclosure.xml b/api/src/test/resources/localfeed/rss2/rss_items_enclosure.xml
new file mode 100644
index 00000000..58601828
--- /dev/null
+++ b/api/src/test/resources/localfeed/rss2/rss_items_enclosure.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<rss xmlns:content="http://purl.org/rss/1.0/modules/content/"
+    xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/"
+    version="2.0">
+    <channel>
+        <item>
+            <title>title</title>
+            <link>link</link>
+            <dc:creator><![CDATA[creator]]></dc:creator>
+            <dc:date>2020-08-05T14:03:48Z</dc:date>
+            <category><![CDATA[Category 1]]></category>
+            <category><![CDATA[Category 2]]></category>
+            <category><![CDATA[Category 3]]></category>
+            <category><![CDATA[Category 4]]></category>
+            <category><![CDATA[Category 5]]></category>
+            <category><![CDATA[Category 6]]></category>
+            <guid isPermaLink="false">guid</guid>
+
+            <description><![CDATA[description]]></description>
+            <content:encoded><![CDATA[content:encoded]]></content:encoded>
+            <enclosure length="0" type="image/jpg" url="https://image1.jpg" />
+            <media:content medium="image" url="https://image2.jpg"/>
+        </item>
+    </channel>
+</rss>
\ No newline at end of file
diff --git a/api/src/test/resources/localfeed/rss2/rss_items_media_content.xml b/api/src/test/resources/localfeed/rss2/rss_items_media_content.xml
new file mode 100644
index 00000000..fcefa3c4
--- /dev/null
+++ b/api/src/test/resources/localfeed/rss2/rss_items_media_content.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<rss xmlns:content="http://purl.org/rss/1.0/modules/content/"
+    xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/"
+    version="2.0">
+    <channel>
+        <item>
+            <title>title</title>
+            <link>link</link>
+            <dc:creator><![CDATA[creator]]></dc:creator>
+            <dc:date>2020-08-05T14:03:48Z</dc:date>
+            <category><![CDATA[Category 1]]></category>
+            <category><![CDATA[Category 2]]></category>
+            <category><![CDATA[Category 3]]></category>
+            <category><![CDATA[Category 4]]></category>
+            <category><![CDATA[Category 5]]></category>
+            <category><![CDATA[Category 6]]></category>
+            <guid isPermaLink="false">guid</guid>
+
+            <description><![CDATA[description]]></description>
+            <content:encoded><![CDATA[content:encoded]]></content:encoded>
+            <media:content medium="image" url="https://image1.jpg"><media:title>image1 title</media:title></media:content>
+        </item>
+        <item>
+            <title>title</title>
+            <link>link</link>
+            <dc:creator><![CDATA[creator]]></dc:creator>
+            <dc:date>2020-08-05T14:03:48Z</dc:date>
+            <category><![CDATA[Category 1]]></category>
+            <category><![CDATA[Category 2]]></category>
+            <category><![CDATA[Category 3]]></category>
+            <category><![CDATA[Category 4]]></category>
+            <category><![CDATA[Category 5]]></category>
+            <category><![CDATA[Category 6]]></category>
+            <guid isPermaLink="false">guid</guid>
+
+            <description><![CDATA[description]]></description>
+            <content:encoded><![CDATA[content:encoded]]></content:encoded>
+            <media:content type="image/jpeg" url="https://image2.jpg"><media:title>image2 title</media:title></media:content>
+        </item>
+    </channel>
+</rss>
\ No newline at end of file
diff --git a/api/src/test/resources/localfeed/rss2/rss_items_media_group.xml b/api/src/test/resources/localfeed/rss2/rss_items_media_group.xml
new file mode 100644
index 00000000..39f73f50
--- /dev/null
+++ b/api/src/test/resources/localfeed/rss2/rss_items_media_group.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<rss xmlns:content="http://purl.org/rss/1.0/modules/content/"
+    xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/"
+    version="2.0">
+    <channel>
+        <item>
+            <title>title</title>
+            <link>link</link>
+            <dc:creator><![CDATA[creator]]></dc:creator>
+            <dc:date>2020-08-05T14:03:48Z</dc:date>
+            <category><![CDATA[Category 1]]></category>
+            <category><![CDATA[Category 2]]></category>
+            <category><![CDATA[Category 3]]></category>
+            <category><![CDATA[Category 4]]></category>
+            <category><![CDATA[Category 5]]></category>
+            <category><![CDATA[Category 6]]></category>
+            <guid isPermaLink="false">guid</guid>
+
+            <description><![CDATA[description]]></description>
+            <content:encoded><![CDATA[content:encoded]]></content:encoded>
+            <media:group>
+                <media:content medium="image" url="https://image1.jpg" />
+                <media:content medium="image" url="https://image2.jpg">
+                    <media:title>image2 title</media:title>
+                </media:content>
+            </media:group>
+
+        </item>
+    </channel>
+</rss>
\ No newline at end of file
diff --git a/api/src/test/resources/localfeed/rss2/rss_items_no_date.xml b/api/src/test/resources/localfeed/rss2/rss_items_no_date.xml
new file mode 100644
index 00000000..3ee2e090
--- /dev/null
+++ b/api/src/test/resources/localfeed/rss2/rss_items_no_date.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<rss xmlns:content="http://purl.org/rss/1.0/modules/content/"
+    xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0">
+    <channel>
+        <item>
+            <title>title</title>
+            <link>link</link>
+            <dc:creator><![CDATA[creator]]></dc:creator>
+            <category><![CDATA[Category 1]]></category>
+            <category><![CDATA[Category 2]]></category>
+            <category><![CDATA[Category 3]]></category>
+            <category><![CDATA[Category 4]]></category>
+            <category><![CDATA[Category 5]]></category>
+            <category><![CDATA[Category 6]]></category>
+            <guid isPermaLink="false">guid</guid>
+
+            <description><![CDATA[description]]></description>
+            <content:encoded><![CDATA[content:encoded]]></content:encoded>
+        </item>
+    </channel>
+</rss>
\ No newline at end of file
diff --git a/api/src/test/resources/localfeed/rss2/rss_items_no_link.xml b/api/src/test/resources/localfeed/rss2/rss_items_no_link.xml
new file mode 100644
index 00000000..69c76fcd
--- /dev/null
+++ b/api/src/test/resources/localfeed/rss2/rss_items_no_link.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<rss xmlns:content="http://purl.org/rss/1.0/modules/content/"
+    xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0">
+    <channel>
+        <item>
+            <title>title</title>
+            <dc:creator><![CDATA[creator]]></dc:creator>
+            <dc:date>2020-08-05T14:03:48Z</dc:date>
+            <category><![CDATA[Category 1]]></category>
+            <category><![CDATA[Category 2]]></category>
+            <category><![CDATA[Category 3]]></category>
+            <category><![CDATA[Category 4]]></category>
+            <category><![CDATA[Category 5]]></category>
+            <category><![CDATA[Category 6]]></category>
+            <guid isPermaLink="false">guid</guid>
+
+            <description><![CDATA[description]]></description>
+            <content:encoded><![CDATA[content:encoded]]></content:encoded>
+        </item>
+    </channel>
+</rss>
\ No newline at end of file
diff --git a/api/src/test/resources/localfeed/rss2/rss_items_no_title.xml b/api/src/test/resources/localfeed/rss2/rss_items_no_title.xml
new file mode 100644
index 00000000..a602c8d5
--- /dev/null
+++ b/api/src/test/resources/localfeed/rss2/rss_items_no_title.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<rss xmlns:content="http://purl.org/rss/1.0/modules/content/"
+    xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0">
+    <channel>
+        <item>
+            <link>link</link>
+            <dc:creator><![CDATA[creator]]></dc:creator>
+            <dc:date>2020-08-05T14:03:48Z</dc:date>
+            <category><![CDATA[Category 1]]></category>
+            <category><![CDATA[Category 2]]></category>
+            <category><![CDATA[Category 3]]></category>
+            <category><![CDATA[Category 4]]></category>
+            <category><![CDATA[Category 5]]></category>
+            <category><![CDATA[Category 6]]></category>
+            <guid isPermaLink="false">guid</guid>
+
+            <description><![CDATA[description]]></description>
+            <content:encoded><![CDATA[content:encoded]]></content:encoded>
+        </item>
+    </channel>
+</rss>
\ No newline at end of file
diff --git a/api/src/test/resources/localfeed/rss2/rss_items_other_namespaces.xml b/api/src/test/resources/localfeed/rss2/rss_items_other_namespaces.xml
new file mode 100644
index 00000000..05bb2187
--- /dev/null
+++ b/api/src/test/resources/localfeed/rss2/rss_items_other_namespaces.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<rss xmlns:content="http://purl.org/rss/1.0/modules/content/"
+    xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0">
+    <channel>
+        <item>
+            <title>title</title>
+            <link>link</link>
+            <guid>guid</guid>
+
+            <dc:creator><![CDATA[creator 1]]></dc:creator>
+            <dc:creator><![CDATA[creator 2]]></dc:creator>
+            <dc:creator><![CDATA[creator 3]]></dc:creator>
+            <dc:creator><![CDATA[creator 4]]></dc:creator>
+
+            <dc:date>2020-08-05T14:03:48Z</dc:date>
+            <category><![CDATA[Category 1]]></category>
+            <category><![CDATA[Category 2]]></category>
+            <category><![CDATA[Category 3]]></category>
+            <category><![CDATA[Category 4]]></category>
+            <category><![CDATA[Category 5]]></category>
+            <category><![CDATA[Category 6]]></category>
+            <guid isPermaLink="false">guid</guid>
+
+            <description><![CDATA[description]]></description>
+            <content:encoded><![CDATA[content:encoded]]></content:encoded>
+        </item>
+    </channel>
+</rss>
\ No newline at end of file
diff --git a/api/src/test/resources/localfeed/rss_feed.xml b/api/src/test/resources/localfeed/rss_feed.xml
new file mode 100644
index 00000000..79d3b7a1
--- /dev/null
+++ b/api/src/test/resources/localfeed/rss_feed.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<rss version="2.0" xmlns:media="http://search.yahoo.com/mrss/">
+   <channel>
+      <title>Hacker News</title>
+      <link>https://news.ycombinator.com/</link>
+      <description>Links for the intellectually curious, ranked by readers.</description>
+      <item>
+         <title>Africa declared free of wild polio</title>
+         <link>https://www.bbc.com/news/world-africa-53887947</link>
+         <pubDate>Tue, 25 Aug 2020 17:15:49 +0000</pubDate>
+         <comments>https://news.ycombinator.com/item?id=24273602</comments>
+         <author>Author 1</author>
+         <description><![CDATA[<a href="https://news.ycombinator.com/item?id=24273602">Comments</a>]]></description>
+         <media:description>media description</media:description>
+      </item>
+      <item>
+         <title>Palantir S-1</title>
+         <link>https://www.sec.gov/Archives/edgar/data/1321655/000119312520230013/d904406ds1.htm</link>
+         <pubDate>Tue, 25 Aug 2020 21:03:42 +0000</pubDate>
+         <comments>https://news.ycombinator.com/item?id=24276086</comments>
+         <description><![CDATA[<a href="https://news.ycombinator.com/item?id=24276086">Comments</a>]]></description>
+      </item>
+      <item>
+         <title>Openwifi: Linux mac80211 compatible full-stack 802.11/Wi-Fi design based on SDR</title>
+         <link>https://github.com/open-sdr/openwifi</link>
+         <pubDate>Tue, 25 Aug 2020 17:45:19 +0000</pubDate>
+         <comments>https://news.ycombinator.com/item?id=24273919</comments>
+         <description><![CDATA[<a href="https://news.ycombinator.com/item?id=24273919">Comments</a>]]></description>
+      </item>
+      <item>
+         <title>Syllabus for Eric's PhD Students</title>
+         <link>https://docs.google.com/document/d/11D3kHElzS2HQxTwPqcaTnU5HCJ8WGE5brTXI4KLf4dM/edit</link>
+         <pubDate>Tue, 25 Aug 2020 18:55:12 +0000</pubDate>
+         <comments>https://news.ycombinator.com/item?id=24274699</comments>
+         <description><![CDATA[<a href="https://news.ycombinator.com/item?id=24274699">Comments</a>]]></description>
+      </item>
+      <item>
+         <title>WebBundles harmful to content blocking, security tools, and the open web</title>
+         <link>https://brave.com/webbundles-harmful-to-content-blocking-security-tools-and-the-open-web/</link>
+         <pubDate>Tue, 25 Aug 2020 19:18:50 +0000</pubDate>
+         <comments>https://news.ycombinator.com/item?id=24274968</comments>
+         <description><![CDATA[<a href="https://news.ycombinator.com/item?id=24274968">Comments</a>]]></description>
+      </item>
+      <item>
+         <title>Zappos CEO Tony Hsieh is stepping down after 21 years</title>
+         <link>https://footwearnews.com/2020/business/executive-moves/zappos-ceo-tony-hsieh-steps-down-1203045974/</link>
+         <pubDate>Tue, 25 Aug 2020 06:11:42 +0000</pubDate>
+         <comments>https://news.ycombinator.com/item?id=24268522</comments>
+         <description><![CDATA[<a href="https://news.ycombinator.com/item?id=24268522">Comments</a>]]></description>
+      </item>
+      <item>
+         <title>Evgeny Kuznetsov practices with Bauer stick that has hole in the blade</title>
+         <link>https://russianmachineneverbreaks.com/2020/07/17/evgeny-kuznetsov-practices-with-bauer-stick-that-has-hole-in-the-blade/</link>
+         <pubDate>Tue, 25 Aug 2020 19:38:09 +0000</pubDate>
+         <comments>https://news.ycombinator.com/item?id=24275159</comments>
+         <description><![CDATA[<a href="https://news.ycombinator.com/item?id=24275159">Comments</a>]]></description>
+      </item>
+   </channel>
+</rss>
\ No newline at end of file
diff --git a/api/src/test/resources/services/freshrss/adapters/items_starred_ids.json b/api/src/test/resources/services/freshrss/adapters/items_starred_ids.json
new file mode 100644
index 00000000..743b02a3
--- /dev/null
+++ b/api/src/test/resources/services/freshrss/adapters/items_starred_ids.json
@@ -0,0 +1,20 @@
+{
+  "itemRefs": [
+    {
+      "id": "1603918802432899"
+    },
+    {
+      "id": "1603917640272612"
+    },
+    {
+      "id": "1603914602186551"
+    },
+    {
+      "id": "1603909236998803"
+    },
+    {
+      "id": "1603907200327551"
+    }
+  ],
+  "continuation": 1600675234695337
+}
\ No newline at end of file
diff --git a/api/src/test/resources/services/nextcloudnews/feeds.json b/api/src/test/resources/services/nextcloudnews/feeds.json
new file mode 100644
index 00000000..8663bb33
--- /dev/null
+++ b/api/src/test/resources/services/nextcloudnews/feeds.json
@@ -0,0 +1,49 @@
+{
+  "feeds": [
+    {
+      "id": 3,
+      "url": "https://krebsonsecurity.com/feed/",
+      "title": "Krebs on Security",
+      "faviconLink": "https://krebsonsecurity.com/favicon.ico",
+      "added": 1490999780,
+      "folderId": null,
+      "unreadCount": 1,
+      "ordering": 0,
+      "link": "https://krebsonsecurity.com/",
+      "pinned": false,
+      "updateErrorCount": 0,
+      "lastUpdateError": null,
+      "items": []
+    },
+    {
+      "id": 3,
+      "url": "https://krebsonsecurity.com/feed/",
+      "title": "Krebs on Security",
+      "faviconLink": "https://krebsonsecurity.com/favicon.ico",
+      "added": 1490999780,
+      "folderId": 0,
+      "unreadCount": 1,
+      "ordering": 0,
+      "link": "https://krebsonsecurity.com/",
+      "pinned": false,
+      "updateErrorCount": 0,
+      "lastUpdateError": null,
+      "items": []
+    },
+    {
+      "id": 3,
+      "url": "https://krebsonsecurity.com/feed/",
+      "title": "",
+      "faviconLink": "https://krebsonsecurity.com/favicon.ico",
+      "added": 1490999780,
+      "folderId": 5,
+      "unreadCount": 1,
+      "ordering": 0,
+      "link": "https://krebsonsecurity.com/",
+      "pinned": false,
+      "updateErrorCount": 0,
+      "lastUpdateError": null,
+      "items": []
+    }
+  ]
+}
diff --git a/api/src/test/resources/services/nextcloudnews/user.xml b/api/src/test/resources/services/nextcloudnews/user.xml
new file mode 100644
index 00000000..4a661401
--- /dev/null
+++ b/api/src/test/resources/services/nextcloudnews/user.xml
@@ -0,0 +1,39 @@
+<ocs>
+    <meta>
+        <status>ok</status>
+        <statuscode>100</statuscode>
+        <message>OK</message>
+        <totalitems></totalitems>
+        <itemsperpage></itemsperpage>
+    </meta>
+    <data>
+        <enabled>1</enabled>
+        <storageLocation>/opt/nextcloud/data/Shinokuni</storageLocation>
+        <id>Shinokuni</id>
+        <lastLogin>1605289415000</lastLogin>
+        <backend>Database</backend>
+        <subadmin/>
+        <quota>
+            <free>6215059179</free>
+            <used>15259777301</used>
+            <total>21474836480</total>
+            <relative>71.06</relative>
+            <quota>21474836480</quota>
+        </quota>
+        <email>email@email.org</email>
+        <displayname>Shinokuni</displayname>
+        <phone>phone</phone>
+        <address></address>
+        <website>https://url.com</website>
+        <twitter></twitter>
+        <groups>
+            <element>admin</element>
+        </groups>
+        <language>fr</language>
+        <locale>fr_FR</locale>
+        <backendCapabilities>
+            <setDisplayName>1</setDisplayName>
+            <setPassword>1</setPassword>
+        </backendCapabilities>
+    </data>
+</ocs>
diff --git a/app/build.gradle b/app/build.gradle
index c3217697..3b00d304 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,6 +1,5 @@
 apply plugin: 'com.android.application'
 apply plugin: 'kotlin-android'
-apply plugin: 'kotlin-android-extensions'
 apply plugin: 'kotlin-kapt'
 
 android {
@@ -39,6 +38,8 @@ android {
         }
     }
     compileOptions {
+        coreLibraryDesugaringEnabled true
+
         sourceCompatibility JavaVersion.VERSION_1_8
         targetCompatibility JavaVersion.VERSION_1_8
     }
@@ -48,6 +49,7 @@ android {
 
     buildFeatures {
         viewBinding true
+        buildConfig true
     }
 }
 
@@ -56,46 +58,46 @@ dependencies {
     implementation project(':api')
     implementation project(':db')
 
-    implementation 'com.google.android.material:material:1.1.0'
+    coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
+
+    testImplementation 'junit:junit:4.13'
+    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
+    androidTestImplementation 'androidx.test:runner:1.4.0'
+    androidTestImplementation 'androidx.test:rules:1.4.0'
+    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
+
+    implementation 'com.google.android.material:material:1.4.0'
     implementation 'androidx.cardview:cardview:1.0.0'
-    implementation 'androidx.palette:palette:1.0.0'
-    implementation 'androidx.recyclerview:recyclerview:1.1.0'
-    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+    implementation 'androidx.palette:palette-ktx:1.0.0'
+    implementation 'androidx.recyclerview:recyclerview:1.2.1'
+    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
     implementation 'androidx.legacy:legacy-support-v4:1.0.0'
-    implementation 'androidx.preference:preference:1.1.0'
-    implementation "androidx.core:core-ktx:1.2.0"
-    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
-    implementation "androidx.work:work-runtime-ktx:2.4.0"
-    implementation "androidx.fragment:fragment-ktx:1.2.3"
+    implementation 'androidx.preference:preference:1.1.1'
+    implementation "androidx.work:work-runtime-ktx:2.5.0"
+    implementation "androidx.fragment:fragment-ktx:1.3.5"
+    implementation "androidx.browser:browser:1.3.0"
+    testImplementation "io.insert-koin:koin-test:2.2.3"
 
-
-    testImplementation 'junit:junit:4.12'
-    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
-    androidTestImplementation 'androidx.test:runner:1.2.0'
-    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
-
-    implementation 'com.github.bumptech.glide:glide:4.10.0'
-    kapt 'com.github.bumptech.glide:compiler:4.10.0'
-    implementation 'com.github.bumptech.glide:okhttp3-integration:4.10.0'
-    implementation('com.github.bumptech.glide:recyclerview-integration:4.10.0') {
-        // Excludes the support library because it's already included by Glide.
+    implementation 'com.github.bumptech.glide:glide:4.12.0'
+    kapt 'com.github.bumptech.glide:compiler:4.12.0'
+    implementation 'com.github.bumptech.glide:okhttp3-integration:4.12.0'
+    implementation('com.github.bumptech.glide:recyclerview-integration:4.12.0') {
         transitive = false
     }
 
     implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
-    kapt 'androidx.lifecycle:lifecycle-common-java8:2.2.0'
+    kapt 'androidx.lifecycle:lifecycle-common-java8:2.3.1'
 
     implementation 'com.afollestad.material-dialogs:core:0.9.6.0'
 
-    implementation 'com.mikepenz:fastadapter:3.3.1'
+    implementation 'com.mikepenz:fastadapter:3.2.9'
     implementation 'com.mikepenz:fastadapter-commons:3.3.0'
     implementation 'com.mikepenz:materialdrawer:6.1.2'
     implementation "com.mikepenz:aboutlibraries:6.2.3"
+    implementation "com.mikepenz:iconics-views:3.2.5"
+    implementation "com.mikepenz:iconics-core:3.2.5"
 
-    debugImplementation 'com.facebook.flipper:flipper:0.30.1'
-    debugImplementation 'com.facebook.soloader:soloader:0.8.0'
-    debugImplementation 'com.facebook.flipper:flipper-network-plugin:0.30.1'
-
-    debugImplementation 'com.icapps.niddler:niddler:1.2.0'
-    releaseImplementation 'com.icapps.niddler:niddler-noop:1.2.0'
+    debugImplementation 'com.facebook.flipper:flipper:0.96.1'
+    debugImplementation 'com.facebook.soloader:soloader:0.10.1'
+    debugImplementation 'com.facebook.flipper:flipper-network-plugin:0.96.1'
 }
diff --git a/app/src/androidTest/java/com/readrops/app/SyncResultAnalyserTest.kt b/app/src/androidTest/java/com/readrops/app/SyncResultAnalyserTest.kt
index 9b3d9b4f..782c724d 100644
--- a/app/src/androidTest/java/com/readrops/app/SyncResultAnalyserTest.kt
+++ b/app/src/androidTest/java/com/readrops/app/SyncResultAnalyserTest.kt
@@ -4,7 +4,7 @@ import android.content.Context
 import androidx.room.Room
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.platform.app.InstrumentationRegistry
-import com.readrops.app.utils.SyncResultAnalyser
+import com.readrops.app.notifications.sync.SyncResultAnalyser
 import com.readrops.db.Database
 import com.readrops.db.entities.Feed
 import com.readrops.db.entities.Item
diff --git a/app/src/debug/java/com/readrops/app/ReadropsDebugApp.java b/app/src/debug/java/com/readrops/app/ReadropsDebugApp.java
index 9e09bd2e..2ef2a039 100644
--- a/app/src/debug/java/com/readrops/app/ReadropsDebugApp.java
+++ b/app/src/debug/java/com/readrops/app/ReadropsDebugApp.java
@@ -13,13 +13,9 @@ import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin;
 import com.facebook.flipper.plugins.inspector.DescriptorMapping;
 import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin;
 import com.facebook.flipper.plugins.navigation.NavigationFlipperPlugin;
-import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor;
 import com.facebook.flipper.plugins.network.NetworkFlipperPlugin;
 import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin;
 import com.facebook.soloader.SoLoader;
-import com.icapps.niddler.core.AndroidNiddler;
-import com.icapps.niddler.interceptor.okhttp.NiddlerOkHttpInterceptor;
-import com.readrops.api.utils.HttpManager;
 
 public class ReadropsDebugApp extends ReadropsApp implements Configuration.Provider {
 
@@ -29,7 +25,6 @@ public class ReadropsDebugApp extends ReadropsApp implements Configuration.Provi
         SoLoader.init(this, false);
 
         initFlipper();
-        initNiddler();
     }
 
     private void initFlipper() {
@@ -40,13 +35,6 @@ public class ReadropsDebugApp extends ReadropsApp implements Configuration.Provi
             NetworkFlipperPlugin networkPlugin = new NetworkFlipperPlugin();
             client.addPlugin(networkPlugin);
 
-            HttpManager.setInstance(
-                    HttpManager.getInstance()
-                            .getOkHttpClient()
-                            .newBuilder()
-                            .addInterceptor(new FlipperOkhttpInterceptor(networkPlugin))
-                            .build());
-
             client.addPlugin(new DatabasesFlipperPlugin(this));
             client.addPlugin(CrashReporterPlugin.getInstance());
             client.addPlugin(NavigationFlipperPlugin.getInstance());
@@ -56,24 +44,6 @@ public class ReadropsDebugApp extends ReadropsApp implements Configuration.Provi
         }
     }
 
-    private void initNiddler() {
-        AndroidNiddler niddler = new AndroidNiddler.Builder()
-                .setNiddlerInformation(AndroidNiddler.fromApplication(this))
-                .setPort(0)
-                .setMaxStackTraceSize(10)
-                .build();
-
-        niddler.attachToApplication(this);
-
-        HttpManager.setInstance(HttpManager.getInstance().
-                getOkHttpClient().
-                newBuilder().
-                addInterceptor(new NiddlerOkHttpInterceptor(niddler, "default"))
-                .build());
-
-        niddler.start();
-    }
-
     @NonNull
     @Override
     public Configuration getWorkManagerConfiguration() {
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 13390561..fb1ff613 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -6,7 +6,9 @@
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
-    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission
+        android:name="android.permission.WRITE_EXTERNAL_STORAGE"
+        android:maxSdkVersion="28" />
 
     <application
         android:name=".ReadropsApp"
@@ -30,21 +32,21 @@
         </provider>
 
         <activity
-            android:name=".activities.NotificationPermissionActivity"
+            android:name=".notifications.NotificationPermissionActivity"
             android:theme="@style/AppTheme" />
 
         <activity
-            android:name=".activities.WebViewActivity"
+            android:name=".item.WebViewActivity"
             android:theme="@style/AppTheme.NoActionBar" />
 
         <service android:name=".utils.feedscolors.FeedsColorsIntentService" />
 
-        <receiver android:name=".utils.SyncWorker$MarkReadReceiver" />
-        <receiver android:name=".utils.SyncWorker$ReadLaterReceiver" />
+        <receiver android:name=".notifications.sync.SyncWorker$MarkReadReceiver" />
+        <receiver android:name=".notifications.sync.SyncWorker$ReadLaterReceiver" />
 
-        <activity android:name=".activities.SettingsActivity" />
+        <activity android:name=".settings.SettingsActivity" />
         <activity
-            android:name=".activities.SplashActivity"
+            android:name=".SplashActivity"
             android:theme="@style/SplashTheme">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -52,28 +54,28 @@
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
-        <activity android:name=".activities.AccountTypeListActivity" />
+        <activity android:name=".account.AccountTypeListActivity" />
         <activity
-            android:name=".activities.AddAccountActivity"
+            android:name=".account.AddAccountActivity"
             android:label="@string/add_account" />
         <activity
-            android:name=".activities.ManageFeedsFoldersActivity"
+            android:name=".feedsfolders.ManageFeedsFoldersActivity"
             android:label="@string/manage_feeds_folders"
-            android:parentActivityName=".activities.MainActivity"
+            android:parentActivityName=".itemslist.MainActivity"
             android:theme="@style/AppTheme.NoActionBar" />
         <activity
-            android:name=".activities.MainActivity"
+            android:name=".itemslist.MainActivity"
             android:label="@string/articles"
             android:launchMode="singleTask"
             android:theme="@style/AppTheme.NoActionBar" />
         <activity
-            android:name=".activities.ItemActivity"
-            android:parentActivityName=".activities.MainActivity"
+            android:name=".item.ItemActivity"
+            android:parentActivityName=".itemslist.MainActivity"
             android:theme="@style/AppTheme.NoActionBar" />
         <activity
-            android:name=".activities.AddFeedActivity"
+            android:name=".addfeed.AddFeedActivity"
             android:label="@string/add_feed_title"
-            android:parentActivityName=".activities.MainActivity">
+            android:parentActivityName=".itemslist.MainActivity">
             <intent-filter android:label="@string/new_feed">
                 <action android:name="android.intent.action.SEND" />
 
diff --git a/app/src/main/java/com/readrops/app/AppModule.kt b/app/src/main/java/com/readrops/app/AppModule.kt
new file mode 100644
index 00000000..8e9000b2
--- /dev/null
+++ b/app/src/main/java/com/readrops/app/AppModule.kt
@@ -0,0 +1,77 @@
+package com.readrops.app
+
+import androidx.preference.PreferenceManager
+import com.chimerapps.niddler.core.AndroidNiddler
+import com.chimerapps.niddler.core.Niddler
+import com.readrops.api.services.Credentials
+import com.readrops.app.account.AccountViewModel
+import com.readrops.app.addfeed.AddFeedsViewModel
+import com.readrops.app.feedsfolders.ManageFeedsFoldersViewModel
+import com.readrops.app.item.ItemViewModel
+import com.readrops.app.itemslist.MainViewModel
+import com.readrops.app.notifications.NotificationPermissionViewModel
+import com.readrops.app.repositories.FreshRSSRepository
+import com.readrops.app.repositories.LocalFeedRepository
+import com.readrops.app.repositories.NextNewsRepository
+import com.readrops.app.utils.GlideApp
+import com.readrops.db.entities.account.Account
+import com.readrops.db.entities.account.AccountType
+import org.koin.android.ext.koin.androidApplication
+import org.koin.android.ext.koin.androidContext
+import org.koin.androidx.viewmodel.dsl.viewModel
+import org.koin.core.parameter.parametersOf
+import org.koin.dsl.module
+
+val appModule = module {
+
+    factory { (account: Account) ->
+        when (account.accountType) {
+            AccountType.LOCAL -> LocalFeedRepository(get(), get(), androidContext(), account)
+            AccountType.NEXTCLOUD_NEWS -> NextNewsRepository(get(parameters = { parametersOf(Credentials.toCredentials(account)) }),
+                    get(), androidContext(), account)
+            AccountType.FRESHRSS -> FreshRSSRepository(get(parameters = { parametersOf(Credentials.toCredentials(account)) }),
+                    get(), androidContext(), account)
+            else -> throw IllegalArgumentException("Account type not supported")
+        }
+    }
+
+    viewModel {
+        MainViewModel(get())
+    }
+
+    viewModel {
+        AddFeedsViewModel(get(), get())
+    }
+
+    viewModel {
+        ItemViewModel(get())
+    }
+
+    viewModel {
+        ManageFeedsFoldersViewModel(get())
+    }
+
+    viewModel {
+        NotificationPermissionViewModel(get())
+    }
+
+    viewModel {
+        AccountViewModel(get())
+    }
+
+    single { GlideApp.with(androidApplication()) }
+
+    single { PreferenceManager.getDefaultSharedPreferences(androidContext()) }
+
+    single<Niddler> {
+        val niddler = AndroidNiddler.Builder()
+                .setNiddlerInformation(AndroidNiddler.fromApplication(get()))
+                .setPort(0)
+                .setMaxStackTraceSize(10)
+                .build()
+
+        niddler.attachToApplication(get())
+
+        niddler.apply { start() }
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/readrops/app/ReadropsApp.java b/app/src/main/java/com/readrops/app/ReadropsApp.java
deleted file mode 100644
index b2a9c58a..00000000
--- a/app/src/main/java/com/readrops/app/ReadropsApp.java
+++ /dev/null
@@ -1,61 +0,0 @@
-package com.readrops.app;
-
-import android.annotation.SuppressLint;
-import android.app.Application;
-import android.app.NotificationChannel;
-import android.app.NotificationManager;
-import android.os.Build;
-
-import androidx.appcompat.app.AppCompatDelegate;
-import androidx.preference.PreferenceManager;
-
-import com.readrops.app.utils.SharedPreferencesManager;
-
-import io.reactivex.plugins.RxJavaPlugins;
-
-@SuppressLint("Registered")
-public class ReadropsApp extends Application {
-
-    public static final String FEEDS_COLORS_CHANNEL_ID = "feedsColorsChannel";
-    public static final String OPML_EXPORT_CHANNEL_ID = "opmlExportChannel";
-    public static final String SYNC_CHANNEL_ID = "syncChannel";
-
-    @Override
-    public void onCreate() {
-        super.onCreate();
-
-        RxJavaPlugins.setErrorHandler(e -> {
-        });
-
-        createNotificationChannels();
-
-        PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
-
-        if (Boolean.valueOf(SharedPreferencesManager.readString(this, SharedPreferencesManager.SharedPrefKey.DARK_THEME)))
-            AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
-        else
-            AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
-    }
-
-    private void createNotificationChannels() {
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-            NotificationChannel feedsColorsChannel = new NotificationChannel(FEEDS_COLORS_CHANNEL_ID,
-                    getString(R.string.feeds_colors), NotificationManager.IMPORTANCE_DEFAULT);
-            feedsColorsChannel.setDescription(getString(R.string.get_feeds_colors));
-
-            NotificationChannel opmlExportChannel = new NotificationChannel(OPML_EXPORT_CHANNEL_ID,
-                    getString(R.string.opml_export), NotificationManager.IMPORTANCE_DEFAULT);
-            opmlExportChannel.setDescription(getString(R.string.opml_export_description));
-
-            NotificationChannel syncChannel = new NotificationChannel(SYNC_CHANNEL_ID,
-                    getString(R.string.auto_synchro), NotificationManager.IMPORTANCE_LOW);
-            syncChannel.setDescription(getString(R.string.account_synchro));
-
-            NotificationManager manager = getSystemService(NotificationManager.class);
-
-            manager.createNotificationChannel(feedsColorsChannel);
-            manager.createNotificationChannel(opmlExportChannel);
-            manager.createNotificationChannel(syncChannel);
-        }
-    }
-}
diff --git a/app/src/main/java/com/readrops/app/ReadropsApp.kt b/app/src/main/java/com/readrops/app/ReadropsApp.kt
new file mode 100644
index 00000000..559bfa33
--- /dev/null
+++ b/app/src/main/java/com/readrops/app/ReadropsApp.kt
@@ -0,0 +1,68 @@
+package com.readrops.app
+
+import android.app.Application
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.os.Build
+import androidx.appcompat.app.AppCompatDelegate
+import androidx.preference.PreferenceManager
+import com.readrops.api.apiModule
+import com.readrops.app.utils.SharedPreferencesManager
+import com.readrops.db.dbModule
+import io.reactivex.plugins.RxJavaPlugins
+import org.koin.android.ext.koin.androidContext
+import org.koin.android.ext.koin.androidLogger
+import org.koin.core.context.startKoin
+import org.koin.core.logger.Level
+
+open class ReadropsApp : Application() {
+
+    override fun onCreate() {
+        super.onCreate()
+        RxJavaPlugins.setErrorHandler { e: Throwable? -> }
+
+        createNotificationChannels()
+        PreferenceManager.setDefaultValues(this, R.xml.preferences, false)
+
+        startKoin {
+            androidLogger(Level.ERROR)
+            androidContext(this@ReadropsApp)
+
+            modules(apiModule, dbModule, appModule)
+        }
+
+        if (SharedPreferencesManager.readString(SharedPreferencesManager.SharedPrefKey.DARK_THEME).toBoolean())
+            AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
+        else
+            AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
+    }
+
+    private fun createNotificationChannels() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            val feedsColorsChannel = NotificationChannel(FEEDS_COLORS_CHANNEL_ID,
+                    getString(R.string.feeds_colors), NotificationManager.IMPORTANCE_DEFAULT)
+            feedsColorsChannel.description = getString(R.string.get_feeds_colors)
+
+            val opmlExportChannel = NotificationChannel(OPML_EXPORT_CHANNEL_ID,
+                    getString(R.string.opml_export), NotificationManager.IMPORTANCE_DEFAULT)
+            opmlExportChannel.description = getString(R.string.opml_export_description)
+
+            val syncChannel = NotificationChannel(SYNC_CHANNEL_ID,
+                    getString(R.string.auto_synchro), NotificationManager.IMPORTANCE_LOW)
+            syncChannel.description = getString(R.string.account_synchro)
+
+            val manager = getSystemService(NotificationManager::class.java)!!
+
+            manager.createNotificationChannel(feedsColorsChannel)
+            manager.createNotificationChannel(opmlExportChannel)
+            manager.createNotificationChannel(syncChannel)
+        }
+    }
+
+    companion object {
+        const val FEEDS_COLORS_CHANNEL_ID = "feedsColorsChannel"
+        const val OPML_EXPORT_CHANNEL_ID = "opmlExportChannel"
+        const val SYNC_CHANNEL_ID = "syncChannel"
+    }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/readrops/app/activities/SplashActivity.java b/app/src/main/java/com/readrops/app/SplashActivity.java
similarity index 79%
rename from app/src/main/java/com/readrops/app/activities/SplashActivity.java
rename to app/src/main/java/com/readrops/app/SplashActivity.java
index ef74650b..c28cd93c 100644
--- a/app/src/main/java/com/readrops/app/activities/SplashActivity.java
+++ b/app/src/main/java/com/readrops/app/SplashActivity.java
@@ -1,13 +1,16 @@
-package com.readrops.app.activities;
+package com.readrops.app;
 
 import android.content.Intent;
 import android.os.Bundle;
+import android.util.Log;
 
 import androidx.appcompat.app.AppCompatActivity;
-import androidx.lifecycle.ViewModelProvider;
 
-import com.readrops.app.R;
-import com.readrops.app.viewmodels.AccountViewModel;
+import com.readrops.app.account.AccountTypeListActivity;
+import com.readrops.app.account.AccountViewModel;
+import com.readrops.app.itemslist.MainActivity;
+
+import org.koin.androidx.viewmodel.compat.ViewModelCompat;
 
 import io.reactivex.android.schedulers.AndroidSchedulers;
 import io.reactivex.observers.DisposableSingleObserver;
@@ -15,6 +18,8 @@ import io.reactivex.schedulers.Schedulers;
 
 public class SplashActivity extends AppCompatActivity {
 
+    private static final String TAG = SplashActivity.class.getSimpleName();
+
     private AccountViewModel viewModel;
 
     @Override
@@ -22,7 +27,7 @@ public class SplashActivity extends AppCompatActivity {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.activity_splash);
 
-        viewModel = new ViewModelProvider(this).get(AccountViewModel.class);
+        viewModel = ViewModelCompat.getViewModel(this, AccountViewModel.class);
 
         viewModel.getAccountCount()
                 .subscribeOn(Schedulers.io())
@@ -44,7 +49,7 @@ public class SplashActivity extends AppCompatActivity {
 
                     @Override
                     public void onError(Throwable e) {
-                        
+                        Log.d(TAG, e.getMessage());
                     }
                 });
 
diff --git a/app/src/main/java/com/readrops/app/activities/AccountTypeListActivity.java b/app/src/main/java/com/readrops/app/account/AccountTypeListActivity.java
similarity index 91%
rename from app/src/main/java/com/readrops/app/activities/AccountTypeListActivity.java
rename to app/src/main/java/com/readrops/app/account/AccountTypeListActivity.java
index 5f971bed..c197eba7 100644
--- a/app/src/main/java/com/readrops/app/activities/AccountTypeListActivity.java
+++ b/app/src/main/java/com/readrops/app/account/AccountTypeListActivity.java
@@ -1,4 +1,4 @@
-package com.readrops.app.activities;
+package com.readrops.app.account;
 
 import android.content.Intent;
 import android.net.Uri;
@@ -11,19 +11,20 @@ import android.widget.LinearLayout;
 
 import androidx.annotation.Nullable;
 import androidx.appcompat.app.AppCompatActivity;
-import androidx.lifecycle.ViewModelProvider;
 import androidx.recyclerview.widget.DividerItemDecoration;
 import androidx.recyclerview.widget.LinearLayoutManager;
 
 import com.afollestad.materialdialogs.MaterialDialog;
+import com.readrops.api.opml.OPMLHelper;
 import com.readrops.app.R;
-import com.readrops.app.adapters.AccountTypeListAdapter;
 import com.readrops.app.databinding.ActivityAccountTypeListBinding;
+import com.readrops.app.itemslist.MainActivity;
 import com.readrops.app.utils.Utils;
-import com.readrops.app.viewmodels.AccountViewModel;
 import com.readrops.db.entities.account.Account;
 import com.readrops.db.entities.account.AccountType;
 
+import org.koin.androidx.viewmodel.compat.ViewModelCompat;
+
 import java.util.ArrayList;
 import java.util.List;
 
@@ -32,7 +33,7 @@ import io.reactivex.observers.DisposableCompletableObserver;
 import io.reactivex.observers.DisposableSingleObserver;
 import io.reactivex.schedulers.Schedulers;
 
-import static com.readrops.app.fragments.settings.AccountSettingsFragment.OPEN_OPML_FILE_REQUEST;
+import static com.readrops.api.opml.OPMLHelper.OPEN_OPML_FILE_REQUEST;
 import static com.readrops.app.utils.ReadropsKeys.ACCOUNT;
 import static com.readrops.app.utils.ReadropsKeys.ACCOUNT_TYPE;
 import static com.readrops.app.utils.ReadropsKeys.FROM_MAIN_ACTIVITY;
@@ -54,7 +55,7 @@ public class AccountTypeListActivity extends AppCompatActivity {
         binding = ActivityAccountTypeListBinding.inflate(getLayoutInflater());
         setContentView(binding.getRoot());
 
-        viewModel = new ViewModelProvider(this).get(AccountViewModel.class);
+        viewModel = ViewModelCompat.getViewModel(this, AccountViewModel.class);
 
         setTitle(R.string.new_account);
 
@@ -127,11 +128,7 @@ public class AccountTypeListActivity extends AppCompatActivity {
     }
 
     public void openOPMLFile(View view) {
-        Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
-        intent.addCategory(Intent.CATEGORY_OPENABLE);
-        intent.setType("application/*");
-
-        startActivityForResult(intent, OPEN_OPML_FILE_REQUEST);
+        OPMLHelper.openFileIntent(this);
     }
 
     @Override
@@ -161,7 +158,7 @@ public class AccountTypeListActivity extends AppCompatActivity {
                     account.setId(id.intValue());
                     viewModel.setAccount(account);
 
-                    return viewModel.parseOPMLFile(uri);
+                    return viewModel.parseOPMLFile(uri, this);
                 })
                 .subscribeOn(Schedulers.io())
                 .observeOn(AndroidSchedulers.mainThread())
diff --git a/app/src/main/java/com/readrops/app/adapters/AccountTypeListAdapter.java b/app/src/main/java/com/readrops/app/account/AccountTypeListAdapter.java
similarity index 98%
rename from app/src/main/java/com/readrops/app/adapters/AccountTypeListAdapter.java
rename to app/src/main/java/com/readrops/app/account/AccountTypeListAdapter.java
index 8eea5135..3b93d325 100644
--- a/app/src/main/java/com/readrops/app/adapters/AccountTypeListAdapter.java
+++ b/app/src/main/java/com/readrops/app/account/AccountTypeListAdapter.java
@@ -1,4 +1,4 @@
-package com.readrops.app.adapters;
+package com.readrops.app.account;
 
 import android.view.LayoutInflater;
 import android.view.ViewGroup;
diff --git a/app/src/main/java/com/readrops/app/viewmodels/AccountViewModel.java b/app/src/main/java/com/readrops/app/account/AccountViewModel.java
similarity index 55%
rename from app/src/main/java/com/readrops/app/viewmodels/AccountViewModel.java
rename to app/src/main/java/com/readrops/app/account/AccountViewModel.java
index b84b4340..7b4d5de3 100644
--- a/app/src/main/java/com/readrops/app/viewmodels/AccountViewModel.java
+++ b/app/src/main/java/com/readrops/app/account/AccountViewModel.java
@@ -1,11 +1,10 @@
-package com.readrops.app.viewmodels;
+package com.readrops.app.account;
 
-import android.app.Application;
+import android.content.Context;
 import android.net.Uri;
-import android.util.Log;
 
 import androidx.annotation.NonNull;
-import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.ViewModel;
 
 import com.readrops.api.opml.OPMLParser;
 import com.readrops.app.repositories.ARepository;
@@ -13,7 +12,9 @@ import com.readrops.db.Database;
 import com.readrops.db.entities.Feed;
 import com.readrops.db.entities.Folder;
 import com.readrops.db.entities.account.Account;
-import com.readrops.db.entities.account.AccountType;
+
+import org.koin.core.parameter.DefinitionParametersKt;
+import org.koin.java.KoinJavaComponent;
 
 import java.util.List;
 import java.util.Map;
@@ -21,32 +22,22 @@ import java.util.Map;
 import io.reactivex.Completable;
 import io.reactivex.Single;
 
-public class AccountViewModel extends AndroidViewModel {
-
-    private static final String TAG = AccountViewModel.class.getSimpleName();
+public class AccountViewModel extends ViewModel {
 
     private ARepository repository;
-    private Database database;
+    private final Database database;
 
-    public AccountViewModel(@NonNull Application application) {
-        super(application);
-
-        database = Database.getInstance(application);
-    }
-
-    public void setAccountType(AccountType accountType) throws Exception {
-        repository = ARepository.repositoryFactory(null, accountType, getApplication());
+    public AccountViewModel(@NonNull Database database) {
+        this.database = database;
     }
 
     public void setAccount(Account account) {
-        try {
-            repository = ARepository.repositoryFactory(account, getApplication());
-        } catch (Exception e) {
-            Log.e(TAG, e.getMessage());
-        }
+        repository = KoinJavaComponent.get(ARepository.class, null,
+                () -> DefinitionParametersKt.parametersOf(account));
     }
 
-    public Single<Boolean> login(Account account, boolean insert) {
+    public Completable login(Account account, boolean insert) {
+        setAccount(account);
         return repository.login(account, insert);
     }
 
@@ -66,12 +57,13 @@ public class AccountViewModel extends AndroidViewModel {
         return database.accountDao().getAccountCount();
     }
 
+    @SuppressWarnings("unchecked")
     public Single<Map<Folder, List<Feed>>> getFoldersWithFeeds() {
         return repository.getFoldersWithFeeds();
     }
 
-    public Completable parseOPMLFile(Uri uri) {
-        return OPMLParser.read(uri, getApplication())
+    public Completable parseOPMLFile(Uri uri, Context context) {
+        return OPMLParser.read(uri, context)
                 .flatMapCompletable(foldersAndFeeds -> repository.insertOPMLFoldersAndFeeds(foldersAndFeeds));
     }
 }
diff --git a/app/src/main/java/com/readrops/app/activities/AddAccountActivity.java b/app/src/main/java/com/readrops/app/account/AddAccountActivity.java
similarity index 68%
rename from app/src/main/java/com/readrops/app/activities/AddAccountActivity.java
rename to app/src/main/java/com/readrops/app/account/AddAccountActivity.java
index c9167725..23f11283 100644
--- a/app/src/main/java/com/readrops/app/activities/AddAccountActivity.java
+++ b/app/src/main/java/com/readrops/app/account/AddAccountActivity.java
@@ -1,26 +1,26 @@
-package com.readrops.app.activities;
+package com.readrops.app.account;
 
 import android.content.Intent;
 import android.os.Bundle;
+import android.util.Log;
 import android.util.Patterns;
 import android.view.KeyEvent;
 import android.view.MenuItem;
 import android.view.View;
 
 import androidx.appcompat.app.AppCompatActivity;
-import androidx.lifecycle.ViewModelProvider;
 
 import com.readrops.app.R;
 import com.readrops.app.databinding.ActivityAddAccountBinding;
+import com.readrops.app.itemslist.MainActivity;
 import com.readrops.app.utils.SharedPreferencesManager;
 import com.readrops.app.utils.Utils;
-import com.readrops.app.viewmodels.AccountViewModel;
 import com.readrops.db.entities.account.Account;
 import com.readrops.db.entities.account.AccountType;
 
-import io.reactivex.Completable;
+import org.koin.androidx.viewmodel.compat.ViewModelCompat;
+
 import io.reactivex.CompletableObserver;
-import io.reactivex.SingleObserver;
 import io.reactivex.android.schedulers.AndroidSchedulers;
 import io.reactivex.disposables.Disposable;
 import io.reactivex.schedulers.Schedulers;
@@ -31,6 +31,8 @@ import static com.readrops.app.utils.ReadropsKeys.EDIT_ACCOUNT;
 
 public class AddAccountActivity extends AppCompatActivity {
 
+    private static final String TAG = AddAccountActivity.class.getSimpleName();
+
     private ActivityAddAccountBinding binding;
     private AccountViewModel viewModel;
 
@@ -46,7 +48,7 @@ public class AddAccountActivity extends AppCompatActivity {
         binding = ActivityAddAccountBinding.inflate(getLayoutInflater());
         setContentView(binding.getRoot());
 
-        viewModel = new ViewModelProvider(this).get(AccountViewModel.class);
+        viewModel = ViewModelCompat.getViewModel(this, AccountViewModel.class);
 
         accountType = getIntent().getParcelableExtra(ACCOUNT_TYPE);
 
@@ -58,26 +60,18 @@ public class AddAccountActivity extends AppCompatActivity {
         if (forwardResult || accountToEdit != null)
             getSupportActionBar().setDisplayHomeAsUpEnabled(true);
 
-        try {
-            if (accountToEdit != null) {
-                viewModel.setAccountType(accountToEdit.getAccountType());
-                editAccount = true;
-                fillFields();
-            } else {
-                viewModel.setAccountType(accountType);
+        if (accountToEdit != null) {
+            editAccount = true;
+            fillFields();
+        } else {
+            binding.providerImage.setImageResource(accountType.getIconRes());
+            binding.providerName.setText(accountType.getName());
+            binding.addAccountName.setText(accountType.getName());
 
-                binding.providerImage.setImageResource(accountType.getIconRes());
-                binding.providerName.setText(accountType.getName());
-                binding.addAccountName.setText(accountType.getName());
-                if (accountType == AccountType.FRESHRSS) {
-                    binding.addAccountPasswordLayout.setHelperText(getString(R.string.password_helper));
-                }
+            if (accountType == AccountType.FRESHRSS) {
+                binding.addAccountPasswordLayout.setHelperText(getString(R.string.password_helper));
             }
-        } catch (Exception e) {
-            // TODO : see how to handle this exception
-            e.printStackTrace();
         }
-
     }
 
     public void createAccount(View view) {
@@ -106,7 +100,7 @@ public class AddAccountActivity extends AppCompatActivity {
                 viewModel.login(account, true)
                         .subscribeOn(Schedulers.io())
                         .observeOn(AndroidSchedulers.mainThread())
-                        .subscribe(new SingleObserver<Boolean>() {
+                        .subscribe(new CompletableObserver() {
 
                             @Override
                             public void onSubscribe(Disposable d) {
@@ -115,33 +109,25 @@ public class AddAccountActivity extends AppCompatActivity {
                             }
 
                             @Override
-                            public void onSuccess(Boolean success) {
-                                binding.addAccountLoading.setVisibility(View.GONE);
+                            public void onComplete() {
+                                saveLoginPassword(account);
 
-                                if (success) {
-                                    saveLoginPassword(account);
-
-                                    if (forwardResult) {
-                                        Intent intent = new Intent();
-                                        intent.putExtra(ACCOUNT, account);
-                                        setResult(RESULT_OK, intent);
-                                        finish();
-
-                                    } else {
-                                        Intent intent = new Intent(getApplicationContext(), MainActivity.class);
-                                        intent.putExtra(ACCOUNT, account);
-                                        startActivity(intent);
-                                    }
-
-                                    finish();
+                                if (forwardResult) {
+                                    Intent intent = new Intent();
+                                    intent.putExtra(ACCOUNT, account);
+                                    setResult(RESULT_OK, intent);
                                 } else {
-                                    binding.addAccountValidate.setEnabled(true);
-                                    Utils.showSnackbar(binding.addAccountRoot, getString(R.string.login_failed));
+                                    Intent intent = new Intent(getApplicationContext(), MainActivity.class);
+                                    intent.putExtra(ACCOUNT, account);
+                                    startActivity(intent);
                                 }
+
+                                finish();
                             }
 
                             @Override
                             public void onError(Throwable e) {
+                                Log.d(TAG, e.getMessage());
                                 binding.addAccountLoading.setVisibility(View.GONE);
                                 binding.addAccountValidate.setEnabled(true);
 
@@ -183,8 +169,8 @@ public class AddAccountActivity extends AppCompatActivity {
     }
 
     private void saveLoginPassword(Account account) {
-        SharedPreferencesManager.writeValue(this, account.getLoginKey(), account.getLogin());
-        SharedPreferencesManager.writeValue(this, account.getPasswordKey(), account.getPassword());
+        SharedPreferencesManager.writeValue(account.getLoginKey(), account.getLogin());
+        SharedPreferencesManager.writeValue(account.getPasswordKey(), account.getPassword());
 
         account.setLogin(null);
         account.setPassword(null);
@@ -196,27 +182,15 @@ public class AddAccountActivity extends AppCompatActivity {
 
         binding.addAccountUrl.setText(accountToEdit.getUrl());
         binding.addAccountName.setText(accountToEdit.getAccountName());
-        binding.addAccountLogin.setText(SharedPreferencesManager.readString(this, accountToEdit.getLoginKey()));
-        binding.addAccountPassword.setText(SharedPreferencesManager.readString(this, accountToEdit.getPasswordKey()));
+        binding.addAccountLogin.setText(SharedPreferencesManager.readString(accountToEdit.getLoginKey()));
+        binding.addAccountPassword.setText(SharedPreferencesManager.readString(accountToEdit.getPasswordKey()));
     }
 
     private void updateAccount() {
         viewModel.login(accountToEdit, false)
                 .doOnError(throwable -> Utils.showSnackbar(binding.addAccountRoot, throwable.getMessage()))
-                .flatMapCompletable(b -> {
-                    if (b) {
-                        saveLoginPassword(accountToEdit);
-                        return viewModel.update(accountToEdit);
-                    } else {
-                        runOnUiThread(() -> {
-                            binding.addAccountLoading.setVisibility(View.GONE);
-                            binding.addAccountValidate.setEnabled(true);
-                            Utils.showSnackbar(binding.addAccountRoot, getString(R.string.login_failed));
-                        });
-
-                        return Completable.never();
-                    }
-                })
+                .doAfterTerminate(() -> saveLoginPassword(accountToEdit))
+                .andThen(viewModel.update(accountToEdit))
                 .subscribeOn(Schedulers.io())
                 .observeOn(AndroidSchedulers.mainThread())
                 .subscribe(new CompletableObserver() {
diff --git a/app/src/main/java/com/readrops/app/adapters/AccountArrayAdapter.java b/app/src/main/java/com/readrops/app/addfeed/AccountArrayAdapter.java
similarity index 97%
rename from app/src/main/java/com/readrops/app/adapters/AccountArrayAdapter.java
rename to app/src/main/java/com/readrops/app/addfeed/AccountArrayAdapter.java
index d2369167..af594ea1 100644
--- a/app/src/main/java/com/readrops/app/adapters/AccountArrayAdapter.java
+++ b/app/src/main/java/com/readrops/app/addfeed/AccountArrayAdapter.java
@@ -1,4 +1,4 @@
-package com.readrops.app.adapters;
+package com.readrops.app.addfeed;
 
 import android.content.Context;
 import android.view.LayoutInflater;
diff --git a/app/src/main/java/com/readrops/app/activities/AddFeedActivity.java b/app/src/main/java/com/readrops/app/addfeed/AddFeedActivity.java
similarity index 89%
rename from app/src/main/java/com/readrops/app/activities/AddFeedActivity.java
rename to app/src/main/java/com/readrops/app/addfeed/AddFeedActivity.java
index b8dc7acf..a8b5cb33 100644
--- a/app/src/main/java/com/readrops/app/activities/AddFeedActivity.java
+++ b/app/src/main/java/com/readrops/app/addfeed/AddFeedActivity.java
@@ -1,4 +1,4 @@
-package com.readrops.app.activities;
+package com.readrops.app.addfeed;
 
 import android.annotation.SuppressLint;
 import android.content.Intent;
@@ -7,12 +7,10 @@ import android.os.Bundle;
 import android.util.Patterns;
 import android.view.KeyEvent;
 import android.view.MenuItem;
-import android.view.MotionEvent;
 import android.view.View;
 
 import androidx.annotation.Nullable;
 import androidx.appcompat.app.AppCompatActivity;
-import androidx.lifecycle.ViewModelProvider;
 import androidx.recyclerview.widget.DiffUtil;
 import androidx.recyclerview.widget.ItemTouchHelper;
 import androidx.recyclerview.widget.LinearLayoutManager;
@@ -23,17 +21,15 @@ import com.mikepenz.fastadapter.adapters.ItemAdapter;
 import com.mikepenz.fastadapter.commons.utils.DiffCallback;
 import com.mikepenz.fastadapter.commons.utils.FastAdapterDiffUtil;
 import com.readrops.app.R;
-import com.readrops.app.adapters.AccountArrayAdapter;
 import com.readrops.app.databinding.ActivityAddFeedBinding;
-import com.readrops.app.utils.FeedInsertionResult;
-import com.readrops.app.utils.ParsingResult;
-import com.readrops.app.utils.ReadropsItemTouchCallback;
+import com.readrops.app.utils.customviews.ReadropsItemTouchCallback;
 import com.readrops.app.utils.SharedPreferencesManager;
 import com.readrops.app.utils.Utils;
-import com.readrops.app.viewmodels.AddFeedsViewModel;
 import com.readrops.db.entities.Feed;
 import com.readrops.db.entities.account.Account;
 
+import org.koin.androidx.viewmodel.compat.ViewModelCompat;
+
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -70,33 +66,15 @@ public class AddFeedActivity extends AppCompatActivity implements View.OnClickLi
         binding.addFeedOk.setOnClickListener(this);
         binding.addFeedOk.setEnabled(false);
 
-        binding.addFeedTextInput.setOnTouchListener((v, event) -> {
-            final int DRAWABLE_RIGHT = 2;
-
-            int drawablePos = (binding.addFeedTextInput.getRight() -
-                    binding.addFeedTextInput.getCompoundDrawables()[DRAWABLE_RIGHT].getBounds().width());
-            if (event.getAction() == MotionEvent.ACTION_UP && event.getRawX() >= drawablePos) {
-                binding.addFeedTextInput.setText("");
-                return true;
-            }
-
-            return false;
-        });
-
-        viewModel = new ViewModelProvider(this).get(AddFeedsViewModel.class);
+        viewModel = ViewModelCompat.getViewModel(this, AddFeedsViewModel.class);
 
         parseItemsAdapter = new ItemAdapter<>();
         fastAdapter = FastAdapter.with(parseItemsAdapter);
         fastAdapter.withSelectable(true);
         fastAdapter.withOnClickListener((v, adapter, item, position) -> {
-            if (item.isChecked()) {
-                item.setChecked(false);
-                fastAdapter.notifyAdapterItemChanged(position);
-            } else {
-                item.setChecked(true);
-                fastAdapter.notifyAdapterItemChanged(position);
-            }
-
+            item.setChecked(!item.isChecked());
+            
+            fastAdapter.notifyAdapterItemChanged(position);
             binding.addFeedOk.setEnabled(recyclerViewHasCheckedItems());
 
             return true;
@@ -278,8 +256,8 @@ public class AddFeedActivity extends AppCompatActivity implements View.OnClickLi
 
         Account account = (Account) binding.addFeedAccountSpinner.getSelectedItem();
 
-        account.setLogin(SharedPreferencesManager.readString(this, account.getLoginKey()));
-        account.setPassword(SharedPreferencesManager.readString(this, account.getPasswordKey()));
+        account.setLogin(SharedPreferencesManager.readString(account.getLoginKey()));
+        account.setPassword(SharedPreferencesManager.readString(account.getPasswordKey()));
 
         viewModel.addFeeds(feedsToInsert, account)
                 .subscribeOn(Schedulers.io())
diff --git a/app/src/main/java/com/readrops/app/viewmodels/AddFeedsViewModel.java b/app/src/main/java/com/readrops/app/addfeed/AddFeedsViewModel.java
similarity index 50%
rename from app/src/main/java/com/readrops/app/viewmodels/AddFeedsViewModel.java
rename to app/src/main/java/com/readrops/app/addfeed/AddFeedsViewModel.java
index 562e370d..18e71d2d 100644
--- a/app/src/main/java/com/readrops/app/viewmodels/AddFeedsViewModel.java
+++ b/app/src/main/java/com/readrops/app/addfeed/AddFeedsViewModel.java
@@ -1,59 +1,47 @@
-package com.readrops.app.viewmodels;
-
-import android.app.Application;
-import android.util.Log;
+package com.readrops.app.addfeed;
 
 import androidx.annotation.NonNull;
-import androidx.lifecycle.AndroidViewModel;
 import androidx.lifecycle.LiveData;
+import androidx.lifecycle.ViewModel;
 
+import com.readrops.api.localfeed.LocalRSSDataSource;
+import com.readrops.app.repositories.ARepository;
+import com.readrops.app.utils.HtmlParser;
 import com.readrops.db.Database;
 import com.readrops.db.entities.account.Account;
-import com.readrops.app.repositories.ARepository;
-import com.readrops.app.utils.FeedInsertionResult;
-import com.readrops.app.utils.HtmlParser;
-import com.readrops.app.utils.ParsingResult;
-import com.readrops.api.localfeed.RSSQuery;
+
+import org.koin.core.parameter.DefinitionParametersKt;
+import org.koin.java.KoinJavaComponent;
 
 import java.util.ArrayList;
 import java.util.List;
 
 import io.reactivex.Single;
 
-public class AddFeedsViewModel extends AndroidViewModel {
+public class AddFeedsViewModel extends ViewModel {
 
-    private static final String TAG = AddFeedsViewModel.class.getSimpleName();
+    private final Database database;
+    private final LocalRSSDataSource localRSSDataSource;
 
-    private ARepository repository;
-    private Database database;
-
-    public AddFeedsViewModel(@NonNull Application application) {
-        super(application);
-
-        database = Database.getInstance(application);
+    public AddFeedsViewModel(@NonNull Database database, @NonNull LocalRSSDataSource localRSSDataSource) {
+        this.database = database;
+        this.localRSSDataSource = localRSSDataSource;
     }
 
     public Single<List<FeedInsertionResult>> addFeeds(List<ParsingResult> results, Account account) {
-        try {
-            repository = ARepository.repositoryFactory(account, getApplication());
+        ARepository repository = KoinJavaComponent.get(ARepository.class, null,
+                () -> DefinitionParametersKt.parametersOf(account));
 
-            return repository.addFeeds(results);
-        } catch (Exception e) {
-            Log.d(TAG, e.getMessage());
-        }
-
-        return null;
+        return repository.addFeeds(results);
     }
 
     public Single<List<ParsingResult>> parseUrl(String url) {
         return Single.create(emitter -> {
-            RSSQuery rssApi = new RSSQuery();
             List<ParsingResult> results = new ArrayList<>();
 
-            if (rssApi.isUrlFeedLink(url)) {
+            if (localRSSDataSource.isUrlRSSResource(url)) {
                 ParsingResult parsingResult = new ParsingResult(url, null);
                 results.add(parsingResult);
-
             } else {
                 results.addAll(HtmlParser.getFeedLink(url));
             }
diff --git a/app/src/main/java/com/readrops/app/utils/FeedInsertionResult.java b/app/src/main/java/com/readrops/app/addfeed/FeedInsertionResult.java
similarity index 99%
rename from app/src/main/java/com/readrops/app/utils/FeedInsertionResult.java
rename to app/src/main/java/com/readrops/app/addfeed/FeedInsertionResult.java
index 1ffb4976..fd2e8379 100644
--- a/app/src/main/java/com/readrops/app/utils/FeedInsertionResult.java
+++ b/app/src/main/java/com/readrops/app/addfeed/FeedInsertionResult.java
@@ -1,4 +1,4 @@
-package com.readrops.app.utils;
+package com.readrops.app.addfeed;
 
 import android.view.View;
 import android.widget.ImageView;
diff --git a/app/src/main/java/com/readrops/app/utils/ParsingResult.java b/app/src/main/java/com/readrops/app/addfeed/ParsingResult.java
similarity index 99%
rename from app/src/main/java/com/readrops/app/utils/ParsingResult.java
rename to app/src/main/java/com/readrops/app/addfeed/ParsingResult.java
index c5b594c1..09e05489 100644
--- a/app/src/main/java/com/readrops/app/utils/ParsingResult.java
+++ b/app/src/main/java/com/readrops/app/addfeed/ParsingResult.java
@@ -1,4 +1,4 @@
-package com.readrops.app.utils;
+package com.readrops.app.addfeed;
 
 import android.view.View;
 import android.widget.CheckBox;
diff --git a/app/src/main/java/com/readrops/app/activities/ManageFeedsFoldersActivity.java b/app/src/main/java/com/readrops/app/feedsfolders/ManageFeedsFoldersActivity.java
similarity index 91%
rename from app/src/main/java/com/readrops/app/activities/ManageFeedsFoldersActivity.java
rename to app/src/main/java/com/readrops/app/feedsfolders/ManageFeedsFoldersActivity.java
index 4b833b78..7b79f1a4 100644
--- a/app/src/main/java/com/readrops/app/activities/ManageFeedsFoldersActivity.java
+++ b/app/src/main/java/com/readrops/app/feedsfolders/ManageFeedsFoldersActivity.java
@@ -1,4 +1,4 @@
-package com.readrops.app.activities;
+package com.readrops.app.feedsfolders;
 
 import android.os.Bundle;
 import android.view.Menu;
@@ -9,19 +9,19 @@ import androidx.appcompat.app.AppCompatActivity;
 import androidx.fragment.app.Fragment;
 import androidx.fragment.app.FragmentManager;
 import androidx.fragment.app.FragmentPagerAdapter;
-import androidx.lifecycle.ViewModelProvider;
 
 import com.afollestad.materialdialogs.MaterialDialog;
+import com.readrops.api.utils.exceptions.ConflictException;
+import com.readrops.api.utils.exceptions.UnknownFormatException;
 import com.readrops.app.R;
 import com.readrops.app.databinding.ActivityManageFeedsFoldersBinding;
-import com.readrops.app.fragments.FeedsFragment;
-import com.readrops.app.fragments.FoldersFragment;
+import com.readrops.app.feedsfolders.feeds.FeedsFragment;
+import com.readrops.app.feedsfolders.folders.FoldersFragment;
 import com.readrops.app.utils.Utils;
-import com.readrops.app.viewmodels.ManageFeedsFoldersViewModel;
 import com.readrops.db.entities.Folder;
 import com.readrops.db.entities.account.Account;
-import com.readrops.api.utils.ConflictException;
-import com.readrops.api.utils.UnknownFormatException;
+
+import org.koin.androidx.viewmodel.compat.ViewModelCompat;
 
 import io.reactivex.android.schedulers.AndroidSchedulers;
 import io.reactivex.schedulers.Schedulers;
@@ -52,7 +52,7 @@ public class ManageFeedsFoldersActivity extends AppCompatActivity {
         binding.manageFeedsFoldersViewpager.setAdapter(pageAdapter);
         binding.manageFeedsFoldersTablayout.setupWithViewPager(binding.manageFeedsFoldersViewpager);
 
-        viewModel = new ViewModelProvider(this).get(ManageFeedsFoldersViewModel.class);
+        viewModel = ViewModelCompat.getViewModel(this, ManageFeedsFoldersViewModel.class);
         viewModel.setAccount(account);
     }
 
diff --git a/app/src/main/java/com/readrops/app/viewmodels/ManageFeedsFoldersViewModel.java b/app/src/main/java/com/readrops/app/feedsfolders/ManageFeedsFoldersViewModel.java
similarity index 67%
rename from app/src/main/java/com/readrops/app/viewmodels/ManageFeedsFoldersViewModel.java
rename to app/src/main/java/com/readrops/app/feedsfolders/ManageFeedsFoldersViewModel.java
index 7a357449..91bd9581 100644
--- a/app/src/main/java/com/readrops/app/viewmodels/ManageFeedsFoldersViewModel.java
+++ b/app/src/main/java/com/readrops/app/feedsfolders/ManageFeedsFoldersViewModel.java
@@ -1,48 +1,44 @@
-package com.readrops.app.viewmodels;
-
-import android.app.Application;
+package com.readrops.app.feedsfolders;
 
 import androidx.annotation.NonNull;
-import androidx.lifecycle.AndroidViewModel;
 import androidx.lifecycle.LiveData;
+import androidx.lifecycle.ViewModel;
 
+import com.readrops.app.repositories.ARepository;
 import com.readrops.db.Database;
 import com.readrops.db.entities.Feed;
 import com.readrops.db.entities.Folder;
 import com.readrops.db.entities.account.Account;
 import com.readrops.db.pojo.FeedWithFolder;
 import com.readrops.db.pojo.FolderWithFeedCount;
-import com.readrops.app.repositories.ARepository;
+
+import org.koin.core.parameter.DefinitionParametersKt;
+import org.koin.java.KoinJavaComponent;
 
 import java.util.List;
 
 import io.reactivex.Completable;
 import io.reactivex.Single;
 
-public class ManageFeedsFoldersViewModel extends AndroidViewModel {
+public class ManageFeedsFoldersViewModel extends ViewModel {
 
-    private Database db;
+    private final Database database;
     private LiveData<List<FeedWithFolder>> feedsWithFolder;
     private LiveData<List<Folder>> folders;
     private ARepository repository;
 
     private Account account;
 
-    public ManageFeedsFoldersViewModel(@NonNull Application application) {
-        super(application);
-
-        db = Database.getInstance(application);
+    public ManageFeedsFoldersViewModel(@NonNull Database database) {
+        this.database = database;
     }
 
     private void setup() {
-        try {
-            repository = ARepository.repositoryFactory(account, getApplication());
+        repository = KoinJavaComponent.get(ARepository.class, null,
+                () -> DefinitionParametersKt.parametersOf(account));
 
-            feedsWithFolder = db.feedDao().getAllFeedsWithFolder(account.getId());
-            folders = db.folderDao().getAllFolders(account.getId());
-        } catch (Exception e) {
-            e.printStackTrace();
-        }
+        feedsWithFolder = database.feedDao().getAllFeedsWithFolder(account.getId());
+        folders = database.folderDao().getAllFolders(account.getId());
     }
 
     public LiveData<List<FeedWithFolder>> getFeedsWithFolder() {
@@ -67,7 +63,7 @@ public class ManageFeedsFoldersViewModel extends AndroidViewModel {
     }
 
     public LiveData<List<FolderWithFeedCount>> getFoldersWithFeedCount() {
-        return db.folderDao().getFoldersWithFeedCount(account.getId());
+        return database.folderDao().getFoldersWithFeedCount(account.getId());
     }
 
     public Single<Long> addFolder(Folder folder) {
@@ -87,6 +83,6 @@ public class ManageFeedsFoldersViewModel extends AndroidViewModel {
     }
 
     public Single<Integer> getFeedCountByAccount() {
-        return db.feedDao().getFeedCount(account.getId());
+        return database.feedDao().getFeedCount(account.getId());
     }
 }
diff --git a/app/src/main/java/com/readrops/app/fragments/EditFeedDialogFragment.java b/app/src/main/java/com/readrops/app/feedsfolders/feeds/EditFeedDialogFragment.java
similarity index 94%
rename from app/src/main/java/com/readrops/app/fragments/EditFeedDialogFragment.java
rename to app/src/main/java/com/readrops/app/feedsfolders/feeds/EditFeedDialogFragment.java
index 16bfc365..c9819336 100644
--- a/app/src/main/java/com/readrops/app/fragments/EditFeedDialogFragment.java
+++ b/app/src/main/java/com/readrops/app/feedsfolders/feeds/EditFeedDialogFragment.java
@@ -1,4 +1,4 @@
-package com.readrops.app.fragments;
+package com.readrops.app.feedsfolders.feeds;
 
 import android.app.AlertDialog;
 import android.app.Dialog;
@@ -11,16 +11,17 @@ import android.widget.Spinner;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.fragment.app.DialogFragment;
-import androidx.lifecycle.ViewModelProvider;
 
 import com.google.android.material.textfield.TextInputEditText;
 import com.readrops.app.R;
-import com.readrops.app.viewmodels.ManageFeedsFoldersViewModel;
+import com.readrops.app.feedsfolders.ManageFeedsFoldersViewModel;
 import com.readrops.db.entities.Feed;
 import com.readrops.db.entities.Folder;
 import com.readrops.db.entities.account.Account;
 import com.readrops.db.pojo.FeedWithFolder;
 
+import org.koin.androidx.viewmodel.compat.SharedViewModelCompat;
+
 import java.util.ArrayList;
 import java.util.Map;
 import java.util.TreeMap;
@@ -59,7 +60,7 @@ public class EditFeedDialogFragment extends DialogFragment implements AdapterVie
     @NonNull
     @Override
     public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
-        viewModel = new ViewModelProvider(getActivity()).get(ManageFeedsFoldersViewModel.class);
+        viewModel = SharedViewModelCompat.getSharedViewModel(this, ManageFeedsFoldersViewModel.class);
 
         feedWithFolder = getArguments().getParcelable("feedWithFolder");
         account = getArguments().getParcelable(ACCOUNT);
diff --git a/app/src/main/java/com/readrops/app/fragments/FeedOptionsDialogFragment.kt b/app/src/main/java/com/readrops/app/feedsfolders/feeds/FeedOptionsDialogFragment.kt
similarity index 98%
rename from app/src/main/java/com/readrops/app/fragments/FeedOptionsDialogFragment.kt
rename to app/src/main/java/com/readrops/app/feedsfolders/feeds/FeedOptionsDialogFragment.kt
index ac7b9736..eea856e6 100644
--- a/app/src/main/java/com/readrops/app/fragments/FeedOptionsDialogFragment.kt
+++ b/app/src/main/java/com/readrops/app/feedsfolders/feeds/FeedOptionsDialogFragment.kt
@@ -1,4 +1,4 @@
-package com.readrops.app.fragments
+package com.readrops.app.feedsfolders.feeds
 
 import android.content.Intent
 import android.net.Uri
diff --git a/app/src/main/java/com/readrops/app/adapters/FeedsAdapter.java b/app/src/main/java/com/readrops/app/feedsfolders/feeds/FeedsAdapter.java
similarity index 58%
rename from app/src/main/java/com/readrops/app/adapters/FeedsAdapter.java
rename to app/src/main/java/com/readrops/app/feedsfolders/feeds/FeedsAdapter.java
index 6050ea3b..8df74301 100644
--- a/app/src/main/java/com/readrops/app/adapters/FeedsAdapter.java
+++ b/app/src/main/java/com/readrops/app/feedsfolders/feeds/FeedsAdapter.java
@@ -1,10 +1,8 @@
-package com.readrops.app.adapters;
+package com.readrops.app.feedsfolders.feeds;
 
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
-import android.widget.ImageView;
-import android.widget.TextView;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -14,12 +12,15 @@ import androidx.recyclerview.widget.RecyclerView;
 
 import com.bumptech.glide.load.engine.DiskCacheStrategy;
 import com.readrops.app.R;
+import com.readrops.app.databinding.FeedLayoutBinding;
+import com.readrops.app.utils.GlideRequests;
 import com.readrops.db.pojo.FeedWithFolder;
-import com.readrops.app.utils.GlideApp;
+
+import org.koin.java.KoinJavaComponent;
 
 import java.util.List;
 
-public class FeedsAdapter extends ListAdapter<FeedWithFolder, FeedsAdapter.ViewHolder> {
+public class FeedsAdapter extends ListAdapter<FeedWithFolder, FeedsAdapter.FeedViewHolder> {
 
     private ManageFeedsListener listener;
 
@@ -52,42 +53,38 @@ public class FeedsAdapter extends ListAdapter<FeedWithFolder, FeedsAdapter.ViewH
         this.listener = listener;
     }
 
-    public FeedWithFolder getItemAt(int position) {
-        return getItem(position);
-    }
-
     @NonNull
     @Override
-    public ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
-        View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.feed_layout, viewGroup, false);
+    public FeedViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
+        FeedLayoutBinding binding = FeedLayoutBinding.inflate(LayoutInflater.from(viewGroup.getContext()), viewGroup, false);
 
-        return new ViewHolder(view);
+        return new FeedViewHolder(binding);
     }
 
     @Override
-    public void onBindViewHolder(@NonNull ViewHolder viewHolder, int i) {
+    public void onBindViewHolder(@NonNull FeedViewHolder viewHolder, int i) {
         FeedWithFolder feedWithFolder = getItem(i);
 
         if (feedWithFolder.getFeed().getIconUrl() != null) {
-            GlideApp.with(viewHolder.itemView.getContext())
+            KoinJavaComponent.get(GlideRequests.class)
                     .load(feedWithFolder.getFeed().getIconUrl())
                     .diskCacheStrategy(DiskCacheStrategy.ALL)
                     .placeholder(R.drawable.ic_rss_feed_grey)
-                    .into(viewHolder.feedIcon);
+                    .into(viewHolder.binding.feedLayoutIcon);
         } else
-            viewHolder.feedIcon.setImageResource(R.drawable.ic_rss_feed_grey);
+            viewHolder.binding.feedLayoutIcon.setImageResource(R.drawable.ic_rss_feed_grey);
 
-        viewHolder.feedName.setText(feedWithFolder.getFeed().getName());
+        viewHolder.binding.feedLayoutName.setText(feedWithFolder.getFeed().getName());
         if (feedWithFolder.getFeed().getDescription() != null) {
-            viewHolder.feedDescription.setVisibility(View.VISIBLE);
-            viewHolder.feedDescription.setText(feedWithFolder.getFeed().getDescription());
+            viewHolder.binding.feedLayoutDescription.setVisibility(View.VISIBLE);
+            viewHolder.binding.feedLayoutDescription.setText(feedWithFolder.getFeed().getDescription());
         } else
-            viewHolder.feedDescription.setVisibility(View.GONE);
+            viewHolder.binding.feedLayoutDescription.setVisibility(View.GONE);
 
         if (feedWithFolder.getFolder() != null)
-            viewHolder.folderName.setText(feedWithFolder.getFolder().getName());
+            viewHolder.binding.feedLayoutFolder.setText(feedWithFolder.getFolder().getName());
         else
-            viewHolder.folderName.setText(R.string.no_folder);
+            viewHolder.binding.feedLayoutFolder.setText(R.string.no_folder);
 
         viewHolder.itemView.setOnClickListener(v -> listener.onEdit(feedWithFolder));
         viewHolder.itemView.setOnLongClickListener(v -> {
@@ -98,16 +95,16 @@ public class FeedsAdapter extends ListAdapter<FeedWithFolder, FeedsAdapter.ViewH
 
 
     @Override
-    public void onBindViewHolder(@NonNull ViewHolder holder, int position, @NonNull List<Object> payloads) {
+    public void onBindViewHolder(@NonNull FeedViewHolder holder, int position, @NonNull List<Object> payloads) {
         if (!payloads.isEmpty()) {
             FeedWithFolder feedWithFolder = (FeedWithFolder) payloads.get(0);
 
-            holder.feedName.setText(feedWithFolder.getFeed().getName());
+            holder.binding.feedLayoutName.setText(feedWithFolder.getFeed().getName());
 
             if (feedWithFolder.getFolder() != null)
-                holder.folderName.setText(feedWithFolder.getFolder().getName());
+                holder.binding.feedLayoutName.setText(feedWithFolder.getFolder().getName());
             else
-                holder.folderName.setText(R.string.no_folder);
+                holder.binding.feedLayoutName.setText(R.string.no_folder);
 
         } else
             onBindViewHolder(holder, position);
@@ -115,24 +112,19 @@ public class FeedsAdapter extends ListAdapter<FeedWithFolder, FeedsAdapter.ViewH
 
     public interface ManageFeedsListener {
         void onOpenLink(FeedWithFolder feedWithFolder);
+
         void onEdit(FeedWithFolder feedWithFolder);
     }
 
 
-    protected class ViewHolder extends RecyclerView.ViewHolder {
+    protected class FeedViewHolder extends RecyclerView.ViewHolder {
 
-        private ImageView feedIcon;
-        private TextView feedName;
-        private TextView feedDescription;
-        private TextView folderName;
+        private FeedLayoutBinding binding;
 
-        public ViewHolder(View itemView) {
-            super(itemView);
+        public FeedViewHolder(FeedLayoutBinding binding) {
+            super(binding.getRoot());
 
-            feedIcon = itemView.findViewById(R.id.feed_layout_icon);
-            feedName = itemView.findViewById(R.id.feed_layout_name);
-            feedDescription = itemView.findViewById(R.id.feed_layout_description);
-            folderName = itemView.findViewById(R.id.feed_layout_folder);
+            this.binding = binding;
         }
     }
 }
diff --git a/app/src/main/java/com/readrops/app/fragments/FeedsFragment.java b/app/src/main/java/com/readrops/app/feedsfolders/feeds/FeedsFragment.java
similarity index 93%
rename from app/src/main/java/com/readrops/app/fragments/FeedsFragment.java
rename to app/src/main/java/com/readrops/app/feedsfolders/feeds/FeedsFragment.java
index f07f66b9..50ac70a5 100644
--- a/app/src/main/java/com/readrops/app/fragments/FeedsFragment.java
+++ b/app/src/main/java/com/readrops/app/feedsfolders/feeds/FeedsFragment.java
@@ -1,4 +1,4 @@
-package com.readrops.app.fragments;
+package com.readrops.app.feedsfolders.feeds;
 
 
 import android.content.res.Resources;
@@ -10,20 +10,20 @@ import android.view.ViewGroup;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.fragment.app.Fragment;
-import androidx.lifecycle.ViewModelProvider;
 import androidx.recyclerview.widget.LinearLayoutManager;
 
 import com.afollestad.materialdialogs.MaterialDialog;
 import com.readrops.app.R;
-import com.readrops.app.adapters.FeedsAdapter;
 import com.readrops.app.databinding.FragmentFeedsBinding;
 import com.readrops.app.utils.SharedPreferencesManager;
 import com.readrops.app.utils.Utils;
-import com.readrops.app.viewmodels.ManageFeedsFoldersViewModel;
+import com.readrops.app.feedsfolders.ManageFeedsFoldersViewModel;
 import com.readrops.db.entities.Feed;
 import com.readrops.db.entities.account.Account;
 import com.readrops.db.pojo.FeedWithFolder;
 
+import org.koin.androidx.viewmodel.compat.SharedViewModelCompat;
+
 import io.reactivex.android.schedulers.AndroidSchedulers;
 import io.reactivex.observers.DisposableCompletableObserver;
 import io.reactivex.schedulers.Schedulers;
@@ -60,11 +60,11 @@ public class FeedsFragment extends Fragment {
         account = getArguments().getParcelable(ACCOUNT);
 
         if (account.getLogin() == null)
-            account.setLogin(SharedPreferencesManager.readString(getContext(), account.getLoginKey()));
+            account.setLogin(SharedPreferencesManager.readString(account.getLoginKey()));
         if (account.getPassword() == null)
-            account.setPassword(SharedPreferencesManager.readString(getContext(), account.getPasswordKey()));
+            account.setPassword(SharedPreferencesManager.readString(account.getPasswordKey()));
 
-        viewModel = new ViewModelProvider(this).get(ManageFeedsFoldersViewModel.class);
+        viewModel = SharedViewModelCompat.getSharedViewModel(this, ManageFeedsFoldersViewModel.class);
         viewModel.setAccount(account);
 
         viewModel.getFeedsWithFolder().observe(this, feedWithFolders -> {
diff --git a/app/src/main/java/com/readrops/app/fragments/FolderOptionsDialogFragment.kt b/app/src/main/java/com/readrops/app/feedsfolders/folders/FolderOptionsDialogFragment.kt
similarity index 97%
rename from app/src/main/java/com/readrops/app/fragments/FolderOptionsDialogFragment.kt
rename to app/src/main/java/com/readrops/app/feedsfolders/folders/FolderOptionsDialogFragment.kt
index ce04a350..853e6ce5 100644
--- a/app/src/main/java/com/readrops/app/fragments/FolderOptionsDialogFragment.kt
+++ b/app/src/main/java/com/readrops/app/feedsfolders/folders/FolderOptionsDialogFragment.kt
@@ -1,4 +1,4 @@
-package com.readrops.app.fragments
+package com.readrops.app.feedsfolders.folders
 
 import android.os.Bundle
 import android.view.LayoutInflater
diff --git a/app/src/main/java/com/readrops/app/adapters/FoldersAdapter.java b/app/src/main/java/com/readrops/app/feedsfolders/folders/FoldersAdapter.java
similarity index 98%
rename from app/src/main/java/com/readrops/app/adapters/FoldersAdapter.java
rename to app/src/main/java/com/readrops/app/feedsfolders/folders/FoldersAdapter.java
index f4a0f266..85266e92 100644
--- a/app/src/main/java/com/readrops/app/adapters/FoldersAdapter.java
+++ b/app/src/main/java/com/readrops/app/feedsfolders/folders/FoldersAdapter.java
@@ -1,4 +1,4 @@
-package com.readrops.app.adapters;
+package com.readrops.app.feedsfolders.folders;
 
 import android.text.TextUtils;
 import android.view.LayoutInflater;
diff --git a/app/src/main/java/com/readrops/app/fragments/FoldersFragment.java b/app/src/main/java/com/readrops/app/feedsfolders/folders/FoldersFragment.java
similarity index 93%
rename from app/src/main/java/com/readrops/app/fragments/FoldersFragment.java
rename to app/src/main/java/com/readrops/app/feedsfolders/folders/FoldersFragment.java
index 9019a09c..0fca4c42 100644
--- a/app/src/main/java/com/readrops/app/fragments/FoldersFragment.java
+++ b/app/src/main/java/com/readrops/app/feedsfolders/folders/FoldersFragment.java
@@ -1,4 +1,4 @@
-package com.readrops.app.fragments;
+package com.readrops.app.feedsfolders.folders;
 
 
 import android.content.res.Resources;
@@ -10,20 +10,20 @@ import android.view.ViewGroup;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.fragment.app.Fragment;
-import androidx.lifecycle.ViewModelProvider;
 import androidx.recyclerview.widget.LinearLayoutManager;
 
 import com.afollestad.materialdialogs.MaterialDialog;
+import com.readrops.api.utils.exceptions.ConflictException;
+import com.readrops.api.utils.exceptions.UnknownFormatException;
 import com.readrops.app.R;
-import com.readrops.app.adapters.FoldersAdapter;
 import com.readrops.app.databinding.FragmentFoldersBinding;
 import com.readrops.app.utils.SharedPreferencesManager;
 import com.readrops.app.utils.Utils;
-import com.readrops.app.viewmodels.ManageFeedsFoldersViewModel;
+import com.readrops.app.feedsfolders.ManageFeedsFoldersViewModel;
 import com.readrops.db.entities.Folder;
 import com.readrops.db.entities.account.Account;
-import com.readrops.api.utils.ConflictException;
-import com.readrops.api.utils.UnknownFormatException;
+
+import org.koin.androidx.viewmodel.compat.SharedViewModelCompat;
 
 import io.reactivex.android.schedulers.AndroidSchedulers;
 import io.reactivex.observers.DisposableSingleObserver;
@@ -60,12 +60,12 @@ public class FoldersFragment extends Fragment {
         account = getArguments().getParcelable(ACCOUNT);
 
         if (account.getLogin() == null)
-            account.setLogin(SharedPreferencesManager.readString(getContext(), account.getLoginKey()));
+            account.setLogin(SharedPreferencesManager.readString(account.getLoginKey()));
         if (account.getPassword() == null)
-            account.setPassword(SharedPreferencesManager.readString(getContext(), account.getPasswordKey()));
+            account.setPassword(SharedPreferencesManager.readString(account.getPasswordKey()));
 
         adapter = new FoldersAdapter(this::openFolderOptionsDialog);
-        viewModel = new ViewModelProvider(this).get(ManageFeedsFoldersViewModel.class);
+        viewModel = SharedViewModelCompat.getSharedViewModel(this, ManageFeedsFoldersViewModel.class);
 
         viewModel.setAccount(account);
         viewModel.getFeedCountByAccount()
diff --git a/app/src/main/java/com/readrops/app/activities/ItemActivity.java b/app/src/main/java/com/readrops/app/item/ItemActivity.java
similarity index 61%
rename from app/src/main/java/com/readrops/app/activities/ItemActivity.java
rename to app/src/main/java/com/readrops/app/item/ItemActivity.java
index 59b2add3..f45f5e1d 100644
--- a/app/src/main/java/com/readrops/app/activities/ItemActivity.java
+++ b/app/src/main/java/com/readrops/app/item/ItemActivity.java
@@ -1,4 +1,4 @@
-package com.readrops.app.activities;
+package com.readrops.app.item;
 
 import android.Manifest;
 import android.app.DownloadManager;
@@ -18,40 +18,39 @@ import android.view.Menu;
 import android.view.MenuItem;
 import android.view.View;
 import android.webkit.WebView;
-import android.widget.ImageView;
-import android.widget.RelativeLayout;
-import android.widget.TextView;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.appcompat.app.AppCompatActivity;
-import androidx.appcompat.widget.Toolbar;
-import androidx.coordinatorlayout.widget.CoordinatorLayout;
+import androidx.browser.customtabs.CustomTabsIntent;
 import androidx.core.app.ActivityCompat;
 import androidx.core.app.ShareCompat;
-import androidx.lifecycle.ViewModelProvider;
 
 import com.afollestad.materialdialogs.MaterialDialog;
 import com.bumptech.glide.load.engine.DiskCacheStrategy;
 import com.bumptech.glide.request.target.CustomTarget;
 import com.bumptech.glide.request.transition.Transition;
-import com.google.android.material.appbar.AppBarLayout;
-import com.google.android.material.appbar.CollapsingToolbarLayout;
-import com.google.android.material.floatingactionbutton.FloatingActionButton;
+import com.readrops.api.utils.DateUtils;
 import com.readrops.app.R;
-import com.readrops.app.utils.DateUtils;
-import com.readrops.app.utils.GlideApp;
+import com.readrops.app.databinding.ActivityItemBinding;
+import com.readrops.app.utils.GlideRequests;
 import com.readrops.app.utils.PermissionManager;
-import com.readrops.app.utils.ReadropsWebView;
 import com.readrops.app.utils.SharedPreferencesManager;
 import com.readrops.app.utils.Utils;
-import com.readrops.app.viewmodels.ItemViewModel;
 import com.readrops.db.entities.Item;
+import com.readrops.db.entities.account.Account;
 import com.readrops.db.pojo.ItemWithFeed;
 
+import org.koin.androidx.viewmodel.compat.ViewModelCompat;
+import org.koin.java.KoinJavaComponent;
+
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.schedulers.Schedulers;
+
+import static com.readrops.app.utils.ReadropsKeys.ACCOUNT;
 import static com.readrops.app.utils.ReadropsKeys.ACTION_BAR_COLOR;
 import static com.readrops.app.utils.ReadropsKeys.IMAGE_URL;
 import static com.readrops.app.utils.ReadropsKeys.ITEM_ID;
@@ -62,72 +61,50 @@ public class ItemActivity extends AppCompatActivity {
     private static final String TAG = ItemActivity.class.getSimpleName();
     private static final int WRITE_EXTERNAL_STORAGE_REQUEST = 1;
 
+    private ActivityItemBinding binding;
     private ItemViewModel viewModel;
-    private TextView date;
-    private TextView title;
-    private TextView author;
-    private TextView readTime;
-
-    private RelativeLayout readTimeLayout;
-    RelativeLayout dateLayout;
-
-    private CollapsingToolbarLayout toolbarLayout;
-    private Toolbar toolbar;
-    private FloatingActionButton actionButton;
-    private ReadropsWebView webView;
 
     private ItemWithFeed itemWithFeed;
 
     private boolean appBarCollapsed;
 
-    private CoordinatorLayout rootLayout;
     private String urlToDownload;
     private String imageTitle;
 
+    private boolean uiBinded;
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        setContentView(R.layout.activity_item);
+
+        binding = ActivityItemBinding.inflate(getLayoutInflater());
+        setContentView(binding.getRoot());
 
         Intent intent = getIntent();
         int itemId = intent.getIntExtra(ITEM_ID, 0);
         String imageUrl = intent.getStringExtra(IMAGE_URL);
+        Account account = intent.getParcelableExtra(ACCOUNT);
 
-        toolbar = findViewById(R.id.collapsing_layout_toolbar);
-        setSupportActionBar(toolbar);
+        setSupportActionBar(binding.collapsingLayoutToolbar);
 
-        if (getSupportActionBar() != null)
+        if (getSupportActionBar() != null) {
             getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+        }
 
-        toolbarLayout = findViewById(R.id.collapsing_layout);
-        AppBarLayout appBarLayout = findViewById(R.id.app_bar_layout);
-
-        ImageView imageView = findViewById(R.id.collapsing_layout_image);
-        View scrim = findViewById(R.id.collapsing_layout_scrim);
-        actionButton = findViewById(R.id.activity_item_fab);
-        webView = findViewById(R.id.item_webview);
-        date = findViewById(R.id.activity_item_date);
-        title = findViewById(R.id.activity_item_title);
-        author = findViewById(R.id.activity_item_author);
-        readTime = findViewById(R.id.activity_item_readtime);
-        readTimeLayout = findViewById(R.id.activity_item_readtime_layout);
-        dateLayout = findViewById(R.id.activity_item_date_layout);
-        rootLayout = findViewById(R.id.item_root);
-
-        registerForContextMenu(webView);
+        registerForContextMenu(binding.itemWebview);
 
         if (imageUrl == null) {
             getSupportActionBar().setDisplayShowTitleEnabled(false);
-            toolbarLayout.setTitleEnabled(false);
-            scrim.setVisibility(View.GONE);
+            binding.collapsingLayout.setTitleEnabled(false);
+            binding.collapsingLayoutScrim.setVisibility(View.GONE);
         } else {
-            appBarLayout.setExpanded(true);
-            toolbarLayout.setTitleEnabled(true);
+            binding.appBarLayout.setExpanded(true);
+            binding.collapsingLayout.setTitleEnabled(true);
 
-            GlideApp.with(this)
+            KoinJavaComponent.get(GlideRequests.class)
                     .load(imageUrl)
                     .diskCacheStrategy(DiskCacheStrategy.ALL)
-                    .into(imageView);
+                    .into(binding.collapsingLayoutImage);
         }
 
         final TypedArray styledAttributes = getTheme().obtainStyledAttributes(
@@ -135,83 +112,108 @@ public class ItemActivity extends AppCompatActivity {
         int actionBarSize = (int) styledAttributes.getDimension(0, 0);
         styledAttributes.recycle();
 
-        appBarLayout.addOnOffsetChangedListener(((appBarLayout1, i) -> {
+        binding.appBarLayout.addOnOffsetChangedListener(((appBarLayout1, i) -> {
+            appBarCollapsed = Math.abs(i) >= (binding.appBarLayout.getTotalScrollRange() -
+                    actionBarSize - ((8 * binding.appBarLayout.getTotalScrollRange()) / 100));
 
-            if (Math.abs(i) >= (appBarLayout.getTotalScrollRange() - actionBarSize - ((8 * appBarLayout.getTotalScrollRange()) / 100))) {
-                appBarCollapsed = true;
-                invalidateOptionsMenu();
-            } else {
-                appBarCollapsed = false;
-                invalidateOptionsMenu();
-            }
+            invalidateOptionsMenu();
         }));
 
-        viewModel = new ViewModelProvider(this).get(ItemViewModel.class);
-        viewModel.getItemById(itemId).observe(this, this::bindUI);
-        actionButton.setOnClickListener(v -> openInNavigator());
+        viewModel = ViewModelCompat.getViewModel(this, ItemViewModel.class);
+        viewModel.setAccount(account);
+        viewModel.getItemById(itemId).observe(this, itemWithFeed1 -> {
+            if (!uiBinded) {
+                bindUI(itemWithFeed1);
+                uiBinded = true;
+            }
+        });
+
+        binding.activityItemFab.setOnClickListener(v -> openInNavigator());
+
+        binding.itemStarFab.setOnClickListener(v -> {
+            Item item = itemWithFeed.getItem();
+
+            if (item.isStarred()) {
+                binding.itemStarFab.setImageResource(R.drawable.ic_empty_star);
+            } else {
+                binding.itemStarFab.setImageResource(R.drawable.ic_star);
+            }
+
+            item.setStarred(!item.isStarred());
+            viewModel.setStarState(item)
+                    .subscribeOn(Schedulers.io())
+                    .observeOn(AndroidSchedulers.mainThread())
+                    .doOnError(throwable -> Utils.showSnackbar(binding.itemRoot, throwable.getMessage()))
+                    .subscribe();
+        });
+
     }
 
     private void bindUI(ItemWithFeed itemWithFeed) {
         this.itemWithFeed = itemWithFeed;
         Item item = itemWithFeed.getItem();
 
-        date.setText(DateUtils.formattedDateTimeByLocal(item.getPubDate()));
-
-        if (item.getImageLink() == null)
-            toolbar.setTitle(itemWithFeed.getFeedName());
-        else
-            toolbarLayout.setTitle(itemWithFeed.getFeedName());
-
-        if (itemWithFeed.getFolder() != null) {
-            toolbar.setSubtitle(itemWithFeed.getFolder().getName());
+        if (item.isStarred()) {
+            binding.itemStarFab.setImageResource(R.drawable.ic_star);
         }
 
-        title.setText(item.getTitle());
+        binding.activityItemDate.setText(DateUtils.formattedDateTimeByLocal(item.getPubDate()));
+
+        if (item.getImageLink() == null)
+            binding.collapsingLayoutToolbar.setTitle(itemWithFeed.getFeedName());
+        else
+            binding.collapsingLayout.setTitle(itemWithFeed.getFeedName());
+
+        if (itemWithFeed.getFolder() != null) {
+            binding.collapsingLayoutToolbar.setSubtitle(itemWithFeed.getFolder().getName());
+        }
+
+        binding.activityItemTitle.setText(item.getTitle());
 
         if (itemWithFeed.getBgColor() != 0) {
-            title.setTextColor(itemWithFeed.getBgColor());
-            Utils.setDrawableColor(dateLayout.getBackground(), itemWithFeed.getBgColor());
+            binding.activityItemTitle.setTextColor(itemWithFeed.getBgColor());
+            Utils.setDrawableColor(binding.activityItemDateLayout.getBackground(), itemWithFeed.getBgColor());
         } else if (itemWithFeed.getColor() != 0) {
-            title.setTextColor(itemWithFeed.getColor());
-            Utils.setDrawableColor(dateLayout.getBackground(), itemWithFeed.getColor());
+            binding.activityItemTitle.setTextColor(itemWithFeed.getColor());
+            Utils.setDrawableColor(binding.activityItemDateLayout.getBackground(), itemWithFeed.getColor());
         }
 
         if (item.getAuthor() != null && !item.getAuthor().isEmpty()) {
-            author.setText(getString(R.string.by_author, item.getAuthor()));
-            author.setVisibility(View.VISIBLE);
+            binding.activityItemAuthor.setText(getString(R.string.by_author, item.getAuthor()));
+            binding.activityItemAuthor.setVisibility(View.VISIBLE);
         }
 
         if (item.getReadTime() > 0) {
             int minutes = (int) Math.round(item.getReadTime());
             if (minutes < 1)
-                readTime.setText(getResources().getString(R.string.read_time_lower_than_1));
+                binding.activityItemReadtime.setText(getResources().getString(R.string.read_time_lower_than_1));
             else if (minutes > 1)
-                readTime.setText(getResources().getString(R.string.read_time, String.valueOf(minutes)));
+                binding.activityItemReadtime.setText(getResources().getString(R.string.read_time, String.valueOf(minutes)));
             else
-                readTime.setText(getResources().getString(R.string.read_time_one_minute));
+                binding.activityItemReadtime.setText(getResources().getString(R.string.read_time_one_minute));
 
-            readTimeLayout.setVisibility(View.VISIBLE);
+            binding.activityItemReadtimeLayout.setVisibility(View.VISIBLE);
         }
 
         if (itemWithFeed.getBgColor() != 0) {
-            toolbarLayout.setBackgroundColor(itemWithFeed.getBgColor());
-            toolbarLayout.setContentScrimColor(itemWithFeed.getBgColor());
-            toolbarLayout.setStatusBarScrimColor(itemWithFeed.getBgColor());
+            binding.collapsingLayout.setBackgroundColor(itemWithFeed.getBgColor());
+            binding.collapsingLayout.setContentScrimColor(itemWithFeed.getBgColor());
+            binding.collapsingLayout.setStatusBarScrimColor(itemWithFeed.getBgColor());
 
             getWindow().setStatusBarColor(itemWithFeed.getBgColor());
-
-            actionButton.setBackgroundTintList(ColorStateList.valueOf(itemWithFeed.getBgColor()));
+            binding.activityItemFab.setBackgroundTintList(ColorStateList.valueOf(itemWithFeed.getBgColor()));
+            binding.itemStarFab.setBackgroundTintList(ColorStateList.valueOf(itemWithFeed.getBgColor()));
         } else if (itemWithFeed.getColor() != 0) {
-            toolbarLayout.setBackgroundColor(itemWithFeed.getColor());
-            toolbarLayout.setContentScrimColor(itemWithFeed.getColor());
-            toolbarLayout.setStatusBarScrimColor(itemWithFeed.getColor());
+            binding.collapsingLayout.setBackgroundColor(itemWithFeed.getColor());
+            binding.collapsingLayout.setContentScrimColor(itemWithFeed.getColor());
+            binding.collapsingLayout.setStatusBarScrimColor(itemWithFeed.getColor());
 
             getWindow().setStatusBarColor(itemWithFeed.getColor());
-
-            actionButton.setBackgroundTintList(ColorStateList.valueOf(itemWithFeed.getColor()));
+            binding.activityItemFab.setBackgroundTintList(ColorStateList.valueOf(itemWithFeed.getColor()));
+            binding.itemStarFab.setBackgroundTintList(ColorStateList.valueOf(itemWithFeed.getColor()));
         }
 
-        webView.setItem(itemWithFeed);
+        binding.itemWebview.setItem(itemWithFeed);
     }
 
     @Override
@@ -238,12 +240,7 @@ public class ItemActivity extends AppCompatActivity {
                 shareArticle();
                 return true;
             case R.id.item_open:
-                int value = Integer.valueOf(SharedPreferencesManager.readString(this,
-                        SharedPreferencesManager.SharedPrefKey.OPEN_ITEMS_IN));
-                if (value == 0)
-                    openInNavigator();
-                else
-                    openInWebView();
+                openUrl();
                 return true;
             default:
                 return super.onOptionsItemSelected(item);
@@ -256,6 +253,22 @@ public class ItemActivity extends AppCompatActivity {
         super.onBackPressed();
     }
 
+    private void openUrl() {
+        int value = Integer.parseInt(SharedPreferencesManager.readString(
+                SharedPreferencesManager.SharedPrefKey.OPEN_ITEMS_IN));
+        switch (value) {
+            case 0:
+                openInNavigator();
+                break;
+            case 1:
+                openInWebView();
+                break;
+            default:
+                openInCustomTab();
+                break;
+        }
+    }
+
     private void openInNavigator() {
         Intent urlIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(itemWithFeed.getItem().getLink()));
         startActivity(urlIntent);
@@ -264,11 +277,27 @@ public class ItemActivity extends AppCompatActivity {
     private void openInWebView() {
         Intent intent = new Intent(this, WebViewActivity.class);
         intent.putExtra(WEB_URL, itemWithFeed.getItem().getLink());
-        intent.putExtra(ACTION_BAR_COLOR, itemWithFeed.getColor() != 0 ? itemWithFeed.getColor() : itemWithFeed.getBgColor());
+        intent.putExtra(ACTION_BAR_COLOR, itemWithFeed.getBgColor() != 0 ? itemWithFeed.getBgColor() : itemWithFeed.getColor());
 
         startActivity(intent);
     }
 
+    private void openInCustomTab() {
+        boolean darkTheme = Boolean.parseBoolean(SharedPreferencesManager.readString(SharedPreferencesManager.SharedPrefKey.DARK_THEME));
+        int color = itemWithFeed.getBgColor() != 0 ? itemWithFeed.getBgColor() : itemWithFeed.getColor();
+
+        CustomTabsIntent customTabsIntent = new CustomTabsIntent.Builder()
+                .addDefaultShareMenuItem()
+                .setToolbarColor(color)
+                .setSecondaryToolbarColor(color)
+                .setColorScheme(darkTheme ? CustomTabsIntent.COLOR_SCHEME_DARK : CustomTabsIntent.COLOR_SCHEME_LIGHT)
+                .enableUrlBarHiding()
+                .setShowTitle(true)
+                .build();
+
+        customTabsIntent.launchUrl(this, Uri.parse(itemWithFeed.getItem().getLink()));
+    }
+
     private void shareArticle() {
         Intent shareIntent = new Intent(Intent.ACTION_SEND);
         shareIntent.setType("text/plain");
@@ -278,7 +307,7 @@ public class ItemActivity extends AppCompatActivity {
 
     @Override
     public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
-        WebView.HitTestResult hitTestResult = webView.getHitTestResult();
+        WebView.HitTestResult hitTestResult = binding.itemWebview.getHitTestResult();
 
         if (hitTestResult.getType() == WebView.HitTestResult.IMAGE_TYPE ||
                 hitTestResult.getType() == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) {
@@ -300,7 +329,7 @@ public class ItemActivity extends AppCompatActivity {
                                 break;
                             case 2:
                                 urlToDownload = hitTestResult.getExtra();
-                                String content = webView.getItemContent();
+                                String content = binding.itemWebview.getItemContent();
 
                                 Pattern p = Pattern.compile("(<img.*src=\"" + urlToDownload + "\".*>)");
                                 Matcher m = p.matcher(content);
@@ -334,12 +363,12 @@ public class ItemActivity extends AppCompatActivity {
                 downloadImage(urlToDownload);
             } else {
                 if (ActivityCompat.shouldShowRequestPermissionRationale(this, permissions[0])) {
-                    Utils.showSnackBarWithAction(rootLayout, getString(R.string.download_image_permission),
+                    Utils.showSnackBarWithAction(binding.itemRoot, getString(R.string.download_image_permission),
                             getString(R.string.try_again),
                             v -> PermissionManager.requestPermissions(this, WRITE_EXTERNAL_STORAGE_REQUEST,
                                     Manifest.permission.WRITE_EXTERNAL_STORAGE));
                 } else {
-                    Utils.showSnackBarWithAction(rootLayout, getString(R.string.download_image_permission),
+                    Utils.showSnackBarWithAction(binding.itemRoot, getString(R.string.download_image_permission),
                             getString(R.string.permissions), v -> {
                                 Intent intent = new Intent();
                                 intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
@@ -368,7 +397,7 @@ public class ItemActivity extends AppCompatActivity {
     }
 
     private void shareImage(String url) {
-        GlideApp.with(this)
+        KoinJavaComponent.get(GlideRequests.class)
                 .asBitmap()
                 .diskCacheStrategy(DiskCacheStrategy.ALL)
                 .load(url)
@@ -376,7 +405,7 @@ public class ItemActivity extends AppCompatActivity {
                     @Override
                     public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
                         try {
-                            Uri uri = viewModel.saveImageInCache(resource);
+                            Uri uri = viewModel.saveImageInCache(resource, ItemActivity.this);
                             Intent intent = ShareCompat.IntentBuilder.from(ItemActivity.this)
                                     .setType("image/png")
                                     .setStream(uri)
diff --git a/app/src/main/java/com/readrops/app/item/ItemViewModel.java b/app/src/main/java/com/readrops/app/item/ItemViewModel.java
new file mode 100644
index 00000000..9fddfe78
--- /dev/null
+++ b/app/src/main/java/com/readrops/app/item/ItemViewModel.java
@@ -0,0 +1,68 @@
+package com.readrops.app.item;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.net.Uri;
+
+import androidx.annotation.NonNull;
+import androidx.core.content.FileProvider;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.ViewModel;
+
+import com.readrops.app.repositories.ARepository;
+import com.readrops.db.Database;
+import com.readrops.db.entities.Item;
+import com.readrops.db.entities.account.Account;
+import com.readrops.db.pojo.ItemWithFeed;
+import com.readrops.db.queries.ItemSelectionQueryBuilder;
+
+import org.koin.core.parameter.DefinitionParametersKt;
+import org.koin.java.KoinJavaComponent;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+import io.reactivex.Completable;
+
+public class ItemViewModel extends ViewModel {
+
+    private final Database database;
+    private Account account;
+
+    public ItemViewModel(@NonNull Database database) {
+        this.database = database;
+    }
+
+    public void setAccount(Account account) {
+        this.account = account;
+    }
+
+    public LiveData<ItemWithFeed> getItemById(int id) {
+        return database.itemDao().getItemById(ItemSelectionQueryBuilder.buildQuery(id,
+                account.getConfig().useSeparateState()));
+    }
+
+    public Completable setStarState(Item item) {
+        ARepository repository = KoinJavaComponent.get(ARepository.class, null, () -> DefinitionParametersKt.parametersOf(account));
+
+        return repository.setItemStarState(item);
+    }
+
+    public Uri saveImageInCache(Bitmap bitmap, Context context) throws IOException {
+        File imagesFolder = new File(context.getCacheDir().getAbsolutePath(), "images");
+
+        if (!imagesFolder.exists())
+            imagesFolder.mkdirs();
+
+        File image = new File(imagesFolder, "shared_image.png");
+        OutputStream stream = new FileOutputStream(image);
+        bitmap.compress(Bitmap.CompressFormat.PNG, 90, stream);
+
+        stream.flush();
+        stream.close();
+
+        return FileProvider.getUriForFile(context, context.getPackageName(), image);
+    }
+}
diff --git a/app/src/main/java/com/readrops/app/activities/WebViewActivity.kt b/app/src/main/java/com/readrops/app/item/WebViewActivity.kt
similarity index 95%
rename from app/src/main/java/com/readrops/app/activities/WebViewActivity.kt
rename to app/src/main/java/com/readrops/app/item/WebViewActivity.kt
index 2a41a876..8e5f0eab 100644
--- a/app/src/main/java/com/readrops/app/activities/WebViewActivity.kt
+++ b/app/src/main/java/com/readrops/app/item/WebViewActivity.kt
@@ -1,4 +1,4 @@
-package com.readrops.app.activities
+package com.readrops.app.item
 
 import android.annotation.SuppressLint
 import android.content.Intent
@@ -7,7 +7,6 @@ import android.graphics.Bitmap
 import android.graphics.drawable.ColorDrawable
 import android.net.Uri
 import android.os.Bundle
-import android.view.LayoutInflater
 import android.view.Menu
 import android.view.MenuItem
 import android.view.View
@@ -98,8 +97,8 @@ class WebViewActivity : AppCompatActivity() {
             super.onBackPressed()
     }
 
-    override fun onOptionsItemSelected(item: MenuItem?): Boolean {
-        when (item?.itemId) {
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        when (item.itemId) {
             android.R.id.home -> {
                 if (binding.webView.canGoBack())
                     binding.webView.goBack()
@@ -115,7 +114,7 @@ class WebViewActivity : AppCompatActivity() {
             }
         }
 
-        return super.onOptionsItemSelected(item!!)
+        return super.onOptionsItemSelected(item)
     }
 
     private fun shareLink() {
diff --git a/app/src/main/java/com/readrops/app/utils/DrawerManager.java b/app/src/main/java/com/readrops/app/itemslist/DrawerManager.java
similarity index 73%
rename from app/src/main/java/com/readrops/app/utils/DrawerManager.java
rename to app/src/main/java/com/readrops/app/itemslist/DrawerManager.java
index 570acee8..a6805c8a 100644
--- a/app/src/main/java/com/readrops/app/utils/DrawerManager.java
+++ b/app/src/main/java/com/readrops/app/itemslist/DrawerManager.java
@@ -1,24 +1,29 @@
-package com.readrops.app.utils;
+package com.readrops.app.itemslist;
 
 import android.app.Activity;
 import android.graphics.drawable.Drawable;
+import android.view.View;
 import android.widget.ImageView;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.appcompat.widget.Toolbar;
+import androidx.recyclerview.widget.RecyclerView;
 
 import com.bumptech.glide.Glide;
 import com.bumptech.glide.load.engine.DiskCacheStrategy;
 import com.bumptech.glide.request.target.CustomTarget;
 import com.bumptech.glide.request.transition.Transition;
+import com.mikepenz.fastadapter.FastAdapter;
+import com.mikepenz.fastadapter.expandable.ExpandableExtension;
+import com.mikepenz.fastadapter.listeners.ClickEventHook;
+import com.mikepenz.fastadapter.select.SelectExtension;
 import com.mikepenz.materialdrawer.AccountHeader;
 import com.mikepenz.materialdrawer.AccountHeaderBuilder;
 import com.mikepenz.materialdrawer.Drawer;
 import com.mikepenz.materialdrawer.DrawerBuilder;
 import com.mikepenz.materialdrawer.holder.ImageHolder;
 import com.mikepenz.materialdrawer.model.DividerDrawerItem;
-import com.mikepenz.materialdrawer.model.ExpandableBadgeDrawerItem;
 import com.mikepenz.materialdrawer.model.PrimaryDrawerItem;
 import com.mikepenz.materialdrawer.model.ProfileDrawerItem;
 import com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem;
@@ -26,11 +31,14 @@ import com.mikepenz.materialdrawer.model.SecondaryDrawerItem;
 import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem;
 import com.mikepenz.materialdrawer.model.interfaces.IProfile;
 import com.readrops.app.R;
+import com.readrops.app.utils.customviews.CustomExpandableBadgeDrawerItem;
 import com.readrops.db.entities.Feed;
 import com.readrops.db.entities.Folder;
 import com.readrops.db.entities.account.Account;
 
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -41,6 +49,7 @@ public class DrawerManager {
 
     public static final int ARTICLES_ITEM_ID = -5;
     public static final int READ_LATER_ID = -6;
+    public static final int STARS_ID = -10;
     public static final int ADD_ACCOUNT_ID = -4;
     public static final int ABOUT_ID = -7;
     public static final int SETTINGS_ID = -8;
@@ -49,6 +58,7 @@ public class DrawerManager {
     private Activity activity;
     private Toolbar toolbar;
     private Drawer drawer;
+    private FastAdapter<IDrawerItem> adapter;
 
     private AccountHeader header;
     private Drawer.OnDrawerItemClickListener listener;
@@ -75,11 +85,76 @@ public class DrawerManager {
                 .withOnDrawerItemClickListener(listener)
                 .build();
 
+        adapter = drawer.getAdapter();
+        buildFastAdapter();
+
         addDefaultPlaces();
 
         return drawer;
     }
 
+    public void buildFastAdapter() {
+        // Folder click
+        adapter.withEventHook(new ClickEventHook<IDrawerItem>() {
+            @Override
+            public void onClick(@NonNull View v, int position, @NonNull FastAdapter<IDrawerItem> fastAdapter, @NonNull IDrawerItem item) {
+                SelectExtension selectExtension = adapter.getExtension(SelectExtension.class);
+
+                selectExtension.deselect(selectExtension.getSelections());
+
+                if (!item.isSelected()) {
+                    selectExtension.select(position);
+                }
+
+                listener.onItemClick(v, position, item);
+            }
+
+            @Override
+            public List<View> onBindMany(@NonNull RecyclerView.ViewHolder viewHolder) {
+                if (viewHolder instanceof CustomExpandableBadgeDrawerItem.ViewHolder) {
+                    CustomExpandableBadgeDrawerItem.ViewHolder expandableViewHolder = (CustomExpandableBadgeDrawerItem.ViewHolder) viewHolder;
+
+                    return Arrays.asList(new View[]{
+                            expandableViewHolder.itemView.findViewById(R.id.expandable_item_container),
+                            expandableViewHolder.itemView.findViewById(R.id.material_drawer_icon),
+                            expandableViewHolder.itemView.findViewById(R.id.material_drawer_name),
+                            expandableViewHolder.itemView.findViewById(R.id.material_drawer_description)
+                    }.clone());
+
+                } else {
+                    return Collections.emptyList();
+                }
+            }
+        });
+
+        // Expandable click
+        adapter.withEventHook(new ClickEventHook<IDrawerItem>() {
+            @Override
+            public void onClick(@NonNull View v, int position, @NonNull FastAdapter<IDrawerItem> fastAdapter, @NonNull IDrawerItem item) {
+                ExpandableExtension expandableExtension = adapter.getExtension(ExpandableExtension.class);
+
+                expandableExtension.toggleExpandable(position);
+            }
+
+            @Override
+            public List<View> onBindMany(@NonNull RecyclerView.ViewHolder viewHolder) {
+                if (viewHolder instanceof CustomExpandableBadgeDrawerItem.ViewHolder) {
+                    CustomExpandableBadgeDrawerItem.ViewHolder expandableViewHolder = (CustomExpandableBadgeDrawerItem.ViewHolder) viewHolder;
+
+                    return Arrays.asList(new View[]{
+                            expandableViewHolder.badge,
+                            expandableViewHolder.badgeContainer,
+                            expandableViewHolder.arrow,
+                            expandableViewHolder.itemView.findViewById(R.id.material_drawer_arrow_container)
+                    }.clone());
+
+                } else {
+                    return Collections.emptyList();
+                }
+            }
+        });
+    }
+
     public void updateDrawer(Map<Folder, List<Feed>> folderListMap) {
         drawer.removeAllItems();
         drawer.removeAllStickyFooterItems();
@@ -90,10 +165,9 @@ public class DrawerManager {
 
         for (Map.Entry<Folder, List<Feed>> entry : folderListMap.entrySet()) {
             Folder folder = entry.getKey();
-            if (folder.getId() != 0) {
-                // no identifier for badge items, but if needed, be aware of not getting conflicts
-                // with secondary item identifiers (folder and feed ids can be the same)
-                ExpandableBadgeDrawerItem badgeDrawerItem = new ExpandableBadgeDrawerItem()
+            if (folder != null) {
+                CustomExpandableBadgeDrawerItem badgeDrawerItem = new CustomExpandableBadgeDrawerItem()
+                        .withIdentifier(folder.getId() * 1000L) // to avoid any id conflict with other items
                         .withName(folder.getName())
                         .withIcon(R.drawable.ic_folder_grey);
 
@@ -208,6 +282,12 @@ public class DrawerManager {
                 .withSelectable(true)
                 .withIdentifier(READ_LATER_ID);
 
+        PrimaryDrawerItem favorites = new PrimaryDrawerItem()
+                .withName(R.string.favorites)
+                .withIcon(R.drawable.ic_star)
+                .withSelectable(true)
+                .withIdentifier(STARS_ID);
+
         PrimaryDrawerItem aboutItem = new PrimaryDrawerItem()
                 .withName(R.string.about)
                 .withIcon(R.drawable.ic_about_grey)
@@ -224,6 +304,7 @@ public class DrawerManager {
         drawer.addStickyFooterItem(aboutItem);
 
         drawer.addItem(articles);
+        drawer.addItem(favorites);
         drawer.addItem(toReadLater);
         drawer.addItem(new DividerDrawerItem());
     }
@@ -307,4 +388,8 @@ public class DrawerManager {
     public void setDrawerSelection(long identifier) {
         drawer.setSelection(identifier);
     }
+
+    public long getCurrentSelection() {
+        return drawer.getCurrentSelection();
+    }
 }
diff --git a/app/src/main/java/com/readrops/app/activities/MainActivity.java b/app/src/main/java/com/readrops/app/itemslist/MainActivity.java
similarity index 73%
rename from app/src/main/java/com/readrops/app/activities/MainActivity.java
rename to app/src/main/java/com/readrops/app/itemslist/MainActivity.java
index d419425e..627474f6 100644
--- a/app/src/main/java/com/readrops/app/activities/MainActivity.java
+++ b/app/src/main/java/com/readrops/app/itemslist/MainActivity.java
@@ -1,4 +1,4 @@
-package com.readrops.app.activities;
+package com.readrops.app.itemslist;
 
 import android.content.Intent;
 import android.graphics.drawable.Drawable;
@@ -9,20 +9,13 @@ import android.view.ActionMode;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.View;
-import android.widget.LinearLayout;
-import android.widget.ProgressBar;
-import android.widget.RelativeLayout;
-import android.widget.TextView;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.appcompat.app.AppCompatActivity;
-import androidx.appcompat.widget.Toolbar;
-import androidx.constraintlayout.widget.ConstraintLayout;
 import androidx.core.content.ContextCompat;
 import androidx.core.graphics.drawable.DrawableCompat;
 import androidx.drawerlayout.widget.DrawerLayout;
-import androidx.lifecycle.ViewModelProvider;
 import androidx.paging.PagedList;
 import androidx.recyclerview.widget.DividerItemDecoration;
 import androidx.recyclerview.widget.ItemTouchHelper;
@@ -34,7 +27,6 @@ import com.afollestad.materialdialogs.MaterialDialog;
 import com.bumptech.glide.Glide;
 import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader;
 import com.bumptech.glide.util.ViewPreloadSizeProvider;
-import com.google.android.material.floatingactionbutton.FloatingActionButton;
 import com.mikepenz.aboutlibraries.Libs;
 import com.mikepenz.aboutlibraries.LibsBuilder;
 import com.mikepenz.materialdrawer.Drawer;
@@ -42,21 +34,27 @@ import com.mikepenz.materialdrawer.model.PrimaryDrawerItem;
 import com.mikepenz.materialdrawer.model.SecondaryDrawerItem;
 import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem;
 import com.readrops.app.R;
-import com.readrops.app.adapters.MainItemListAdapter;
-import com.readrops.app.utils.DrawerManager;
-import com.readrops.app.utils.GlideApp;
-import com.readrops.app.utils.ReadropsItemTouchCallback;
+import com.readrops.app.account.AccountTypeListActivity;
+import com.readrops.app.addfeed.AddFeedActivity;
+import com.readrops.app.databinding.ActivityMainBinding;
+import com.readrops.app.item.ItemActivity;
+import com.readrops.app.settings.SettingsActivity;
+import com.readrops.app.utils.GlideRequests;
 import com.readrops.app.utils.SharedPreferencesManager;
 import com.readrops.app.utils.Utils;
-import com.readrops.app.viewmodels.MainViewModel;
+import com.readrops.app.utils.customviews.CustomExpandableBadgeDrawerItem;
+import com.readrops.app.utils.customviews.ReadropsItemTouchCallback;
 import com.readrops.db.entities.Feed;
 import com.readrops.db.entities.Folder;
+import com.readrops.db.entities.Item;
 import com.readrops.db.entities.account.Account;
 import com.readrops.db.filters.FilterType;
 import com.readrops.db.filters.ListSortType;
 import com.readrops.db.pojo.ItemWithFeed;
 
 import org.jetbrains.annotations.NotNull;
+import org.koin.androidx.viewmodel.compat.ViewModelCompat;
+import org.koin.java.KoinJavaComponent;
 
 import java.lang.ref.WeakReference;
 import java.util.Collections;
@@ -88,12 +86,9 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
     public static final int ITEM_REQUEST = 3;
     public static final int ADD_ACCOUNT_REQUEST = 4;
 
-    private RecyclerView recyclerView;
+    private ActivityMainBinding binding;
     private MainItemListAdapter adapter;
-    private SwipeRefreshLayout refreshLayout;
-    private ConstraintLayout rootLayout;
 
-    private Toolbar toolbar;
     private Drawer drawer;
 
     private PagedList<ItemWithFeed> allItems;
@@ -101,12 +96,6 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
     private MainViewModel viewModel;
     private DrawerManager drawerManager;
 
-    private LinearLayout emptyListLayout;
-    private RelativeLayout syncProgressLayout;
-    private TextView syncProgress;
-    private ProgressBar syncProgressBar;
-    private FloatingActionButton actionButton;
-
     private int feedCount;
     private int feedNb;
     private boolean scrollToTop;
@@ -121,42 +110,31 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-        setContentView(R.layout.activity_main);
+        binding = ActivityMainBinding.inflate(getLayoutInflater());
 
-        toolbar = findViewById(R.id.toolbar_main);
-        setSupportActionBar(toolbar);
+        setContentView(binding.getRoot());
+        setSupportActionBar(binding.toolbarMain);
 
-        emptyListLayout = findViewById(R.id.empty_list_layout);
-        refreshLayout = findViewById(R.id.swipe_refresh_layout);
-        refreshLayout.setOnRefreshListener(this);
-        rootLayout = findViewById(R.id.main_root);
-
-        syncProgressLayout = findViewById(R.id.sync_progress_layout);
-        syncProgress = findViewById(R.id.sync_progress_text_view);
-        syncProgressBar = findViewById(R.id.sync_progress_bar);
-        actionButton = findViewById(R.id.add_feed_fab);
+        binding.swipeRefreshLayout.setOnRefreshListener(this);
 
         feedCount = 0;
         initRecyclerView();
 
-        viewModel = new ViewModelProvider(this).get(MainViewModel.class);
-
-        viewModel.setShowReadItems(SharedPreferencesManager.readBoolean(this,
-                SharedPreferencesManager.SharedPrefKey.SHOW_READ_ARTICLES));
+        viewModel = ViewModelCompat.getViewModel(this, MainViewModel.class);
 
         viewModel.getItemsWithFeed().observe(this, itemWithFeeds -> {
             allItems = itemWithFeeds;
 
             if (!itemWithFeeds.isEmpty())
-                emptyListLayout.setVisibility(View.GONE);
+                binding.emptyListLayout.setVisibility(View.GONE);
             else
-                emptyListLayout.setVisibility(View.VISIBLE);
+                binding.emptyListLayout.setVisibility(View.VISIBLE);
 
-            if (!refreshLayout.isRefreshing())
+            if (!binding.swipeRefreshLayout.isRefreshing())
                 adapter.submitList(itemWithFeeds);
         });
 
-        drawerManager = new DrawerManager(this, toolbar, (view, position, drawerItem) -> {
+        drawerManager = new DrawerManager(this, binding.toolbarMain, (view, position, drawerItem) -> {
             handleDrawerClick(drawerItem);
 
             return true;
@@ -227,11 +205,11 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
             }
 
             if (accountWeakReference.get() != null && !accountWeakReference.get().isLocal()) {
-                refreshLayout.setRefreshing(true);
+                binding.swipeRefreshLayout.setRefreshing(true);
                 onRefresh();
                 accountWeakReference.clear();
             } else if (currentAccount == null && savedInstanceState != null && savedInstanceState.getBoolean(SYNCING)) {
-                refreshLayout.setRefreshing(true);
+                binding.swipeRefreshLayout.setRefreshing(true);
                 onRefresh();
                 savedInstanceState.clear();
             }
@@ -251,13 +229,18 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
         if (intent.hasExtra(ITEM_ID) && intent.hasExtra(IMAGE_URL)) {
             Intent itemIntent = new Intent(this, ItemActivity.class);
             itemIntent.putExtras(intent);
+            itemIntent.putExtra(ACCOUNT, viewModel.getCurrentAccount());
 
             startActivity(itemIntent);
 
-            viewModel.setItemReadState(intent.getIntExtra(ITEM_ID, 0), true, true)
+            Item item = new Item();
+            item.setId(intent.getIntExtra(ITEM_ID, 0));
+            item.setRead(true);
+
+            viewModel.setItemReadState(item)
                     .subscribeOn(Schedulers.io())
                     .observeOn(AndroidSchedulers.mainThread())
-                    .doOnError(throwable -> Utils.showSnackbar(rootLayout, throwable.getMessage()))
+                    .doOnError(throwable -> Utils.showSnackbar(binding.mainRoot, throwable.getMessage()))
                     .subscribe();
         }
     }
@@ -268,14 +251,22 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
             int id = (int) drawerItem.getIdentifier();
 
             switch (id) {
+                default:
                 case DrawerManager.ARTICLES_ITEM_ID:
                     viewModel.setFilterType(FilterType.NO_FILTER);
                     scrollToTop = true;
                     viewModel.invalidate();
+                    setTitle(R.string.articles);
                     break;
                 case DrawerManager.READ_LATER_ID:
                     viewModel.setFilterType(FilterType.READ_IT_LATER_FILTER);
                     viewModel.invalidate();
+                    setTitle(R.string.read_later);
+                    break;
+                case DrawerManager.STARS_ID:
+                    viewModel.setFilterType(FilterType.STARS_FILTER);
+                    viewModel.invalidate();
+                    setTitle(R.string.favorites);
                     break;
                 case DrawerManager.ABOUT_ID:
                     startAboutActivity();
@@ -293,6 +284,14 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
             viewModel.setFilterFeedId((int) drawerItem.getIdentifier());
             viewModel.setFilterType(FilterType.FEED_FILTER);
             viewModel.invalidate();
+            setTitle(((SecondaryDrawerItem) drawerItem).getName().getText());
+        } else if (drawerItem instanceof CustomExpandableBadgeDrawerItem) {
+            drawer.closeDrawer();
+
+            viewModel.setFilerFolderId((int) (drawerItem.getIdentifier() / 1000));
+            viewModel.setFilterType(FilterType.FOLDER_FILER);
+            viewModel.invalidate();
+            setTitle(((CustomExpandableBadgeDrawerItem) drawerItem).getName().getText());
         }
     }
 
@@ -308,7 +307,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
 
                     @Override
                     public void onError(Throwable e) {
-                        Utils.showSnackbar(rootLayout, e.getMessage());
+                        Utils.showSnackbar(binding.mainRoot, e.getMessage());
                     }
                 });
     }
@@ -322,10 +321,8 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
     }
 
     private void initRecyclerView() {
-        recyclerView = findViewById(R.id.items_recycler_view);
-
         ViewPreloadSizeProvider preloadSizeProvider = new ViewPreloadSizeProvider();
-        adapter = new MainItemListAdapter(GlideApp.with(this), preloadSizeProvider);
+        adapter = new MainItemListAdapter(KoinJavaComponent.get(GlideRequests.class), preloadSizeProvider);
         adapter.setOnItemClickListener(new MainItemListAdapter.OnItemClickListener() {
             @Override
             public void onItemClick(ItemWithFeed itemWithFeed, int position) {
@@ -334,15 +331,17 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
 
                     intent.putExtra(ITEM_ID, itemWithFeed.getItem().getId());
                     intent.putExtra(IMAGE_URL, itemWithFeed.getItem().getImageLink());
+                    intent.putExtra(ACCOUNT, viewModel.getCurrentAccount());
+
                     startActivityForResult(intent, ITEM_REQUEST);
 
-                    viewModel.setItemReadState(itemWithFeed, true)
+                    itemWithFeed.getItem().setRead(true);
+                    viewModel.setItemReadState(itemWithFeed)
                             .subscribeOn(Schedulers.io())
                             .observeOn(AndroidSchedulers.mainThread())
-                            .doOnError(throwable -> Utils.showSnackbar(rootLayout, throwable.getMessage()))
+                            .doOnError(throwable -> Utils.showSnackbar(binding.mainRoot, throwable.getMessage()))
                             .subscribe();
 
-                    itemWithFeed.getItem().setRead(true);
                     adapter.notifyItemChanged(position, itemWithFeed);
                     updateDrawerFeeds();
                 } else {
@@ -358,7 +357,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
 
             @Override
             public void onItemLongClick(ItemWithFeed itemWithFeed, int position) {
-                if (actionMode != null || refreshLayout.isRefreshing())
+                if (actionMode != null || binding.swipeRefreshLayout.isRefreshing())
                     return;
 
                 selectedItemWithFeed = itemWithFeed;
@@ -370,20 +369,20 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
         });
 
         RecyclerViewPreloader<String> preloader = new RecyclerViewPreloader<String>(Glide.with(this), adapter, preloadSizeProvider, 10);
-        recyclerView.addOnScrollListener(preloader);
+        binding.itemsRecyclerView.addOnScrollListener(preloader);
 
-        recyclerView.setRecyclerListener(viewHolder -> {
+        binding.itemsRecyclerView.addRecyclerListener(viewHolder -> {
             MainItemListAdapter.ItemViewHolder vh = (MainItemListAdapter.ItemViewHolder) viewHolder;
-            GlideApp.with(this).clear(vh.getItemImage());
+            KoinJavaComponent.get(GlideRequests.class).clear(vh.getItemImage());
         });
 
         LinearLayoutManager layoutManager = new LinearLayoutManager(this);
-        recyclerView.setLayoutManager(layoutManager);
+        binding.itemsRecyclerView.setLayoutManager(layoutManager);
 
         DividerItemDecoration decoration = new DividerItemDecoration(this, layoutManager.getOrientation());
-        recyclerView.addItemDecoration(decoration);
+        binding.itemsRecyclerView.addItemDecoration(decoration);
 
-        recyclerView.setAdapter(adapter);
+        binding.itemsRecyclerView.setAdapter(adapter);
 
 
         Drawable readLater = ContextCompat.getDrawable(this, R.drawable.ic_read_later).mutate();
@@ -396,34 +395,34 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
                         .leftDraw(ContextCompat.getColor(this, R.color.colorAccent), R.drawable.ic_read_later, readLater)
                         .rightDraw(ContextCompat.getColor(this, R.color.colorAccent), R.drawable.ic_read, null)
                         .build()))
-                .attachToRecyclerView(recyclerView);
+                .attachToRecyclerView(binding.itemsRecyclerView);
 
         adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
             @Override
             public void onItemRangeInserted(int positionStart, int itemCount) {
                 if (scrollToTop) {
-                    recyclerView.scrollToPosition(0);
+                    binding.itemsRecyclerView.scrollToPosition(0);
                     scrollToTop = false;
                 }
             }
 
             @Override
             public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
-                ;if (scrollToTop) {
-                    recyclerView.scrollToPosition(0);
+                if (scrollToTop) {
+                    binding.itemsRecyclerView.scrollToPosition(0);
                     scrollToTop = false;
                 } else
                     super.onItemRangeMoved(fromPosition, toPosition, itemCount);
             }
         });
 
-        recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
+        binding.itemsRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
             @Override
             public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
                 if (dy > 0) {
-                    actionButton.hide();
+                    binding.addFeedFab.hide();
                 } else {
-                    actionButton.show();
+                    binding.addFeedFab.show();
                 }
             }
         });
@@ -431,34 +430,34 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
 
     @Override
     public void onSwipe(@NotNull RecyclerView.ViewHolder viewHolder, int direction) {
+        Item item = adapter.getItemWithFeed(viewHolder.getBindingAdapterPosition()).getItem();
+
         if (direction == ItemTouchHelper.LEFT) { // set item read state
-            ItemWithFeed itemWithFeed = adapter.getItemWithFeed(viewHolder.getAdapterPosition());
+            item.setRead(!item.isRead());
 
-            viewModel.setItemReadState(itemWithFeed, !itemWithFeed.getItem().isRead())
+            viewModel.setItemReadState(item)
                     .subscribeOn(Schedulers.io())
                     .observeOn(AndroidSchedulers.mainThread())
-                    .doOnError(throwable -> Utils.showSnackbar(rootLayout, throwable.getMessage()))
+                    .doOnError(throwable -> Utils.showSnackbar(binding.mainRoot, throwable.getMessage()))
                     .subscribe();
 
-            itemWithFeed.getItem().setRead(!itemWithFeed.getItem().isRead());
+        } else { // set item read it later state
+            item.setReadItLater(!item.isReadItLater());
 
-            adapter.notifyItemChanged(viewHolder.getAdapterPosition());
-        } else { // add item to read it later section
-            viewModel.setItemReadItLater((int) adapter.getItemId(viewHolder.getAdapterPosition()))
+            viewModel.setItemReadItLater(item.isReadItLater(), item.getId())
                     .subscribeOn(Schedulers.io())
                     .observeOn(AndroidSchedulers.mainThread())
-                    .doOnError(throwable -> Utils.showSnackbar(rootLayout, throwable.getMessage()))
+                    .doOnError(throwable -> Utils.showSnackbar(binding.mainRoot, throwable.getMessage()))
                     .subscribe();
-
-            if (viewModel.getFilterType() == FilterType.READ_IT_LATER_FILTER)
-                adapter.notifyItemChanged(viewHolder.getAdapterPosition());
         }
+
+        adapter.notifyItemChanged(viewHolder.getBindingAdapterPosition());
     }
 
     @Override
     public boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
         drawer.getDrawerLayout().setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
-        refreshLayout.setEnabled(false);
+        binding.swipeRefreshLayout.setEnabled(false);
 
         actionMode.getMenuInflater().inflate(R.menu.item_list_contextual_menu, menu);
         getWindow().setStatusBarColor(ContextCompat.getColor(this, R.color.primary_dark));
@@ -476,23 +475,21 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
 
     @Override
     public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) {
-        switch (menuItem.getItemId()) {
-            case R.id.item_mark_read:
-                setReadState(true);
-                break;
-            case R.id.item_mark_unread:
-                setReadState(false);
-                break;
-            case R.id.item_select_all:
-                if (allItemsSelected) {
-                    adapter.unselectAll();
-                    allItemsSelected = false;
-                    actionMode.finish();
-                } else {
-                    adapter.selectAll();
-                    allItemsSelected = true;
-                }
-                break;
+        int itemId = menuItem.getItemId();
+
+        if (itemId == R.id.item_mark_read) {
+            setReadState(true);
+        } else if (itemId == R.id.item_mark_unread) {
+            setReadState(false);
+        } else if (itemId == R.id.item_select_all) {
+            if (allItemsSelected) {
+                adapter.unselectAll();
+                allItemsSelected = false;
+                actionMode.finish();
+            } else {
+                adapter.selectAll();
+                allItemsSelected = true;
+            }
         }
 
         return true;
@@ -504,7 +501,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
         actionMode = null;
 
         drawer.getDrawerLayout().setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
-        refreshLayout.setEnabled(true);
+        binding.swipeRefreshLayout.setEnabled(true);
 
         adapter.clearSelection();
     }
@@ -514,7 +511,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
             viewModel.setAllItemsReadState(read)
                     .subscribeOn(Schedulers.io())
                     .observeOn(AndroidSchedulers.mainThread())
-                    .doOnError(throwable -> Utils.showSnackbar(rootLayout, throwable.getMessage()))
+                    .doOnError(throwable -> Utils.showSnackbar(binding.mainRoot, throwable.getMessage()))
                     .subscribe();
 
             allItemsSelected = false;
@@ -522,7 +519,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
             viewModel.setItemsReadState(adapter.getSelectedItems(), read)
                     .subscribeOn(Schedulers.io())
                     .observeOn(AndroidSchedulers.mainThread())
-                    .doOnError(throwable -> Utils.showSnackbar(rootLayout, throwable.getMessage()))
+                    .doOnError(throwable -> Utils.showSnackbar(binding.mainRoot, throwable.getMessage()))
                     .subscribe();
         }
 
@@ -543,14 +540,14 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
                     .observeOn(AndroidSchedulers.mainThread())
                     .subscribe(new DisposableSingleObserver<Integer>() {
                         @Override
-                        public void onSuccess(Integer integer) {
+                        public void onSuccess(@NonNull Integer integer) {
                             feedNb = integer;
                             sync(null);
                         }
 
                         @Override
-                        public void onError(Throwable e) {
-                            Utils.showSnackbar(rootLayout, e.getMessage());
+                        public void onError(@NonNull Throwable e) {
+                            Utils.showSnackbar(binding.mainRoot, e.getMessage());
                         }
                     });
         } else {
@@ -570,7 +567,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
             List<Feed> feeds = data.getParcelableArrayListExtra(FEEDS);
 
             if (feeds != null && !feeds.isEmpty() && viewModel.isAccountLocal()) {
-                refreshLayout.setRefreshing(true);
+                binding.swipeRefreshLayout.setRefreshing(true);
                 feedNb = feeds.size();
                 sync(feeds);
             }
@@ -592,7 +589,7 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
 
                 // start syncing only if the account is not local
                 if (!viewModel.isAccountLocal()) {
-                    refreshLayout.setRefreshing(true);
+                    binding.swipeRefreshLayout.setRefreshing(true);
                     onRefresh();
                 }
 
@@ -611,50 +608,53 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
                 .observeOn(AndroidSchedulers.mainThread())
                 .subscribe(new Observer<Feed>() {
                     @Override
-                    public void onSubscribe(Disposable d) {
+                    public void onSubscribe(@NonNull Disposable d) {
                         syncDisposable = d;
 
                         if (viewModel.isAccountLocal() && feedNb > 0) {
-                            syncProgressLayout.setVisibility(View.VISIBLE);
-                            syncProgressBar.setProgress(0);
+                            binding.syncProgressLayout.setVisibility(View.VISIBLE);
+                            binding.syncProgressBar.setProgress(0);
                         }
                     }
 
                     @Override
-                    public void onNext(Feed feed) {
+                    public void onNext(@NonNull Feed feed) {
                         if (viewModel.isAccountLocal() && feedNb > 0) {
-                            syncProgress.setText(getString(R.string.updating_feed, feed.getName()));
+                            binding.syncProgressTextView.setText(getString(R.string.updating_feed, feed.getName()));
                             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
-                                syncProgressBar.setProgress((feedCount * 100) / feedNb, true);
+                                binding.syncProgressBar.setProgress((feedCount * 100) / feedNb, true);
                             } else
-                                syncProgressBar.setProgress((feedCount * 100) / feedNb);
+                                binding.syncProgressBar.setProgress((feedCount * 100) / feedNb);
                         }
 
                         feedCount++;
                     }
 
                     @Override
-                    public void onError(Throwable e) {
+                    public void onError(@NonNull Throwable e) {
                         e.printStackTrace();
-                        refreshLayout.setRefreshing(false);
-                        syncProgressLayout.setVisibility(View.GONE);
+                        binding.swipeRefreshLayout.setRefreshing(false);
+                        binding.syncProgressLayout.setVisibility(View.GONE);
 
-                        Utils.showSnackbar(rootLayout, e.getMessage());
+                        Utils.showSnackbar(binding.mainRoot, e.getMessage());
                         drawerManager.enableAccountSelection();
+                        updating = false;
                     }
 
                     @Override
                     public void onComplete() {
+                        viewModel.invalidate();
+
                         if (viewModel.isAccountLocal() && feedNb > 0) {
                             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
-                                syncProgressBar.setProgress(100, true);
+                                binding.syncProgressBar.setProgress(100, true);
                             else
-                                syncProgressBar.setProgress(100);
+                                binding.syncProgressBar.setProgress(100);
 
-                            syncProgressLayout.setVisibility(View.GONE);
+                            binding.syncProgressLayout.setVisibility(View.GONE);
                         }
 
-                        refreshLayout.setRefreshing(false);
+                        binding.swipeRefreshLayout.setRefreshing(false);
 
                         scrollToTop = true;
                         adapter.submitList(allItems);
@@ -679,31 +679,31 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
 
     @Override
     public boolean onOptionsItemSelected(MenuItem item) {
-        switch (item.getItemId()) {
-            case R.id.item_filter_read_items:
-                if (item.isChecked()) {
-                    item.setChecked(false);
-                    viewModel.setShowReadItems(false);
-                    SharedPreferencesManager.writeValue(this,
-                            SharedPreferencesManager.SharedPrefKey.SHOW_READ_ARTICLES, false);
-                } else {
-                    item.setChecked(true);
-                    viewModel.setShowReadItems(true);
-                    SharedPreferencesManager.writeValue(this,
-                            SharedPreferencesManager.SharedPrefKey.SHOW_READ_ARTICLES, true);
-                }
+        int itemId = item.getItemId();
 
-                viewModel.invalidate();
-                return true;
-            case R.id.item_sort:
-                displayFilterDialog();
-                return true;
-            case R.id.start_sync:
-                if (!viewModel.isAccountLocal()) {
-                    refreshLayout.setRefreshing(true);
-                }
-                onRefresh();
-                break;
+        if (itemId == R.id.item_filter_read_items) {
+            if (item.isChecked()) {
+                item.setChecked(false);
+                viewModel.setShowReadItems(false);
+                SharedPreferencesManager.writeValue(
+                        SharedPreferencesManager.SharedPrefKey.SHOW_READ_ARTICLES, false);
+            } else {
+                item.setChecked(true);
+                viewModel.setShowReadItems(true);
+                SharedPreferencesManager.writeValue(
+                        SharedPreferencesManager.SharedPrefKey.SHOW_READ_ARTICLES, true);
+            }
+
+            viewModel.invalidate();
+            return true;
+        } else if (itemId == R.id.item_sort) {
+            displayFilterDialog();
+            return true;
+        } else if (itemId == R.id.start_sync) {
+            if (!viewModel.isAccountLocal()) {
+                binding.swipeRefreshLayout.setRefreshing(true);
+            }
+            onRefresh();
         }
 
         return super.onOptionsItemSelected(item);
@@ -733,16 +733,16 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
     private void getAccountCredentials(List<Account> accounts) {
         for (Account account : accounts) {
             if (account.getLogin() == null)
-                account.setLogin(SharedPreferencesManager.readString(this, account.getLoginKey()));
+                account.setLogin(SharedPreferencesManager.readString(account.getLoginKey()));
 
             if (account.getPassword() == null)
-                account.setPassword(SharedPreferencesManager.readString(this, account.getPasswordKey()));
+                account.setPassword(SharedPreferencesManager.readString(account.getPasswordKey()));
         }
     }
 
     private void startAboutActivity() {
         Libs.ActivityStyle activityStyle;
-        if (Boolean.valueOf(SharedPreferencesManager.readString(this, SharedPreferencesManager.SharedPrefKey.DARK_THEME)))
+        if (Boolean.parseBoolean(SharedPreferencesManager.readString(SharedPreferencesManager.SharedPrefKey.DARK_THEME)))
             activityStyle = Libs.ActivityStyle.DARK;
         else
             activityStyle = Libs.ActivityStyle.LIGHT_DARK_TOOLBAR;
@@ -769,11 +769,10 @@ public class MainActivity extends AppCompatActivity implements SwipeRefreshLayou
     }
 
     @Override
-    protected void onSaveInstanceState(Bundle outState) {
-        if (refreshLayout.isRefreshing())
+    protected void onSaveInstanceState(@NonNull Bundle outState) {
+        if (binding.swipeRefreshLayout.isRefreshing())
             outState.putBoolean(SYNCING, true);
 
         super.onSaveInstanceState(outState);
     }
-
 }
diff --git a/app/src/main/java/com/readrops/app/adapters/MainItemListAdapter.java b/app/src/main/java/com/readrops/app/itemslist/MainItemListAdapter.java
similarity index 99%
rename from app/src/main/java/com/readrops/app/adapters/MainItemListAdapter.java
rename to app/src/main/java/com/readrops/app/itemslist/MainItemListAdapter.java
index a818387c..9fac99cf 100644
--- a/app/src/main/java/com/readrops/app/adapters/MainItemListAdapter.java
+++ b/app/src/main/java/com/readrops/app/itemslist/MainItemListAdapter.java
@@ -1,4 +1,4 @@
-package com.readrops.app.adapters;
+package com.readrops.app.itemslist;
 
 import android.content.Context;
 import android.content.res.Resources;
@@ -29,7 +29,7 @@ import com.readrops.app.R;
 import com.readrops.db.entities.Item;
 import com.readrops.db.pojo.ItemWithFeed;
 import com.readrops.app.databinding.ListItemBinding;
-import com.readrops.app.utils.DateUtils;
+import com.readrops.api.utils.DateUtils;
 import com.readrops.app.utils.GlideRequests;
 import com.readrops.app.utils.Utils;
 
diff --git a/app/src/main/java/com/readrops/app/viewmodels/MainViewModel.java b/app/src/main/java/com/readrops/app/itemslist/MainViewModel.java
similarity index 65%
rename from app/src/main/java/com/readrops/app/viewmodels/MainViewModel.java
rename to app/src/main/java/com/readrops/app/itemslist/MainViewModel.java
index 621f94b0..f9c46dd1 100644
--- a/app/src/main/java/com/readrops/app/viewmodels/MainViewModel.java
+++ b/app/src/main/java/com/readrops/app/itemslist/MainViewModel.java
@@ -1,24 +1,29 @@
-package com.readrops.app.viewmodels;
-
-import android.app.Application;
+package com.readrops.app.itemslist;
 
 import androidx.annotation.NonNull;
-import androidx.lifecycle.AndroidViewModel;
 import androidx.lifecycle.LiveData;
 import androidx.lifecycle.MediatorLiveData;
+import androidx.lifecycle.ViewModel;
+import androidx.paging.DataSource;
 import androidx.paging.LivePagedListBuilder;
 import androidx.paging.PagedList;
 
+import com.readrops.app.repositories.ARepository;
+import com.readrops.app.utils.SharedPreferencesManager;
 import com.readrops.db.Database;
-import com.readrops.db.ItemsListQueryBuilder;
+import com.readrops.db.queries.ItemsQueryBuilder;
+import com.readrops.db.queries.QueryFilters;
 import com.readrops.db.RoomFactoryWrapper;
 import com.readrops.db.entities.Feed;
 import com.readrops.db.entities.Folder;
+import com.readrops.db.entities.Item;
 import com.readrops.db.entities.account.Account;
 import com.readrops.db.filters.FilterType;
 import com.readrops.db.filters.ListSortType;
 import com.readrops.db.pojo.ItemWithFeed;
-import com.readrops.app.repositories.ARepository;
+
+import org.koin.core.parameter.DefinitionParametersKt;
+import org.koin.java.KoinJavaComponent;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -31,45 +36,43 @@ import io.reactivex.Single;
 import io.reactivex.android.schedulers.AndroidSchedulers;
 import io.reactivex.schedulers.Schedulers;
 
-public class MainViewModel extends AndroidViewModel {
+public class MainViewModel extends ViewModel {
 
-    private MediatorLiveData<PagedList<ItemWithFeed>> itemsWithFeed;
+    private final MediatorLiveData<PagedList<ItemWithFeed>> itemsWithFeed;
     private LiveData<PagedList<ItemWithFeed>> lastFetch;
     private ARepository repository;
-    private Database db;
+    private final Database database;
 
-    private ItemsListQueryBuilder queryBuilder;
+    private final QueryFilters queryFilters;
 
     private Account currentAccount;
     private List<Account> accounts;
 
-    public MainViewModel(@NonNull Application application) {
-        super(application);
-
-        queryBuilder = new ItemsListQueryBuilder();
-
-        queryBuilder.setFilterType(FilterType.NO_FILTER);
-        queryBuilder.setSortType(ListSortType.NEWEST_TO_OLDEST);
-
-        db = Database.getInstance(application);
+    public MainViewModel(@NonNull Database database) {
+        this.database = database;
         itemsWithFeed = new MediatorLiveData<>();
+
+        queryFilters = new QueryFilters();
+        queryFilters.setShowReadItems(SharedPreferencesManager.readBoolean(
+                SharedPreferencesManager.SharedPrefKey.SHOW_READ_ARTICLES));
     }
 
     //region main query
 
     private void setRepository() {
-        try {
-            repository = ARepository.repositoryFactory(currentAccount, getApplication());
-        } catch (Exception e) {
-            e.printStackTrace();
-        }
+        repository = KoinJavaComponent.get(ARepository.class, null,
+                () -> DefinitionParametersKt.parametersOf(currentAccount));
     }
 
     private void buildPagedList() {
-        if (lastFetch != null)
+        if (lastFetch != null) {
             itemsWithFeed.removeSource(lastFetch);
+        }
 
-        lastFetch = new LivePagedListBuilder<>(new RoomFactoryWrapper<>(db.itemDao().selectAll(queryBuilder.getQuery())),
+        DataSource.Factory<Integer, ItemWithFeed> items;
+        items = database.itemDao().selectAll(ItemsQueryBuilder.buildItemsQuery(queryFilters, currentAccount.getConfig().useSeparateState()));
+
+        lastFetch = new LivePagedListBuilder<>(new RoomFactoryWrapper<>(items),
                 new PagedList.Config.Builder()
                         .setPageSize(100)
                         .setPrefetchDistance(150)
@@ -77,7 +80,7 @@ public class MainViewModel extends AndroidViewModel {
                         .build())
                 .build();
 
-        itemsWithFeed.addSource(lastFetch, itemWithFeeds -> itemsWithFeed.setValue(itemWithFeeds));
+        itemsWithFeed.addSource(lastFetch, itemsWithFeed::setValue);
     }
 
     public void invalidate() {
@@ -85,31 +88,35 @@ public class MainViewModel extends AndroidViewModel {
     }
 
     public void setShowReadItems(boolean showReadItems) {
-        queryBuilder.setShowReadItems(showReadItems);
+        queryFilters.setShowReadItems(showReadItems);
     }
 
     public boolean showReadItems() {
-        return queryBuilder.showReadItems();
+        return queryFilters.getShowReadItems();
     }
 
     public void setFilterType(FilterType filterType) {
-        queryBuilder.setFilterType(filterType);
+        queryFilters.setFilterType(filterType);
     }
 
     public FilterType getFilterType() {
-        return queryBuilder.getFilterType();
+        return queryFilters.getFilterType();
     }
 
     public void setSortType(ListSortType sortType) {
-        queryBuilder.setSortType(sortType);
+        queryFilters.setSortType(sortType);
     }
 
     public ListSortType getSortType() {
-        return queryBuilder.getSortType();
+        return queryFilters.getSortType();
     }
 
     public void setFilterFeedId(int filterFeedId) {
-        queryBuilder.setFilterFeedId(filterFeedId);
+        queryFilters.setFilterFeedId(filterFeedId);
+    }
+
+    public void setFilerFolderId(int folderId) {
+        queryFilters.setFilterFolderId(folderId);
     }
 
     public MediatorLiveData<PagedList<ItemWithFeed>> getItemsWithFeed() {
@@ -117,6 +124,7 @@ public class MainViewModel extends AndroidViewModel {
     }
 
     public Observable<Feed> sync(List<Feed> feeds) {
+        itemsWithFeed.removeSource(lastFetch);
         return repository.sync(feeds);
     }
 
@@ -133,12 +141,12 @@ public class MainViewModel extends AndroidViewModel {
     //region Account
 
     public LiveData<List<Account>> getAllAccounts() {
-        return db.accountDao().selectAllAsync();
+        return database.accountDao().selectAllAsync();
     }
 
     private Completable deselectOldCurrentAccount(int accountId) {
         return Completable.create(emitter -> {
-            db.accountDao().deselectOldCurrentAccount(accountId);
+            database.accountDao().deselectOldCurrentAccount(accountId);
             emitter.onComplete();
         });
     }
@@ -164,12 +172,12 @@ public class MainViewModel extends AndroidViewModel {
     public void setCurrentAccount(Account currentAccount) {
         this.currentAccount = currentAccount;
         setRepository();
-        queryBuilder.setAccountId(currentAccount.getId());
+        queryFilters.setAccountId(currentAccount.getId());
         buildPagedList();
 
         // set the new account as the current one
         Completable setCurrentAccount = Completable.create(emitter -> {
-            db.accountDao().setCurrentAccount(currentAccount.getId());
+            database.accountDao().setCurrentAccount(currentAccount.getId());
             emitter.onComplete();
         });
 
@@ -195,13 +203,13 @@ public class MainViewModel extends AndroidViewModel {
                 currentAccountExists = true;
 
                 setRepository();
-                queryBuilder.setAccountId(currentAccount.getId());
+                queryFilters.setAccountId(currentAccount.getId());
                 buildPagedList();
                 break;
             }
         }
 
-        if (!currentAccountExists && accounts.size() > 0) {
+        if (!currentAccountExists && !accounts.isEmpty()) {
             setCurrentAccount(accounts.get(0));
             accounts.get(0).setCurrentAccount(true);
         }
@@ -215,33 +223,34 @@ public class MainViewModel extends AndroidViewModel {
 
     //region Item read state
 
-    public Completable setItemReadState(ItemWithFeed itemWithFeed, boolean read) {
-        return repository.setItemReadState(itemWithFeed.getItem(), read);
+    public Completable setItemReadState(ItemWithFeed itemWithFeed) {
+        return repository.setItemReadState(itemWithFeed.getItem());
     }
 
-    public Completable setItemReadState(int itemId, boolean read, boolean readChanged) {
-        return repository.setItemReadState(itemId, read, readChanged);
+    public Completable setItemReadState(Item item) {
+        return repository.setItemReadState(item);
     }
 
     public Completable setItemsReadState(List<ItemWithFeed> items, boolean read) {
         List<Completable> completableList = new ArrayList<>();
 
         for (ItemWithFeed itemWithFeed : items) {
-            completableList.add(setItemReadState(itemWithFeed, read));
+            itemWithFeed.getItem().setRead(read);
+            completableList.add(setItemReadState(itemWithFeed));
         }
 
         return Completable.concat(completableList);
     }
 
     public Completable setAllItemsReadState(boolean read) {
-        if (queryBuilder.getFilterType() == FilterType.FEED_FILTER)
-            return repository.setAllFeedItemsReadState(queryBuilder.getFilterFeedId(), read);
+        if (queryFilters.getFilterType() == FilterType.FEED_FILTER)
+            return repository.setAllFeedItemsReadState(queryFilters.getFilterFeedId(), read);
         else
             return repository.setAllItemsReadState(read);
     }
 
-    public Completable setItemReadItLater(int itemId) {
-        return db.itemDao().setReadItLater(itemId);
+    public Completable setItemReadItLater(boolean readLater, int itemId) {
+        return database.itemDao().setReadItLater(readLater, itemId);
     }
 
     //endregion
diff --git a/app/src/main/java/com/readrops/app/activities/NotificationPermissionActivity.kt b/app/src/main/java/com/readrops/app/notifications/NotificationPermissionActivity.kt
similarity index 94%
rename from app/src/main/java/com/readrops/app/activities/NotificationPermissionActivity.kt
rename to app/src/main/java/com/readrops/app/notifications/NotificationPermissionActivity.kt
index 511de722..46230e4e 100644
--- a/app/src/main/java/com/readrops/app/activities/NotificationPermissionActivity.kt
+++ b/app/src/main/java/com/readrops/app/notifications/NotificationPermissionActivity.kt
@@ -1,4 +1,4 @@
-package com.readrops.app.activities
+package com.readrops.app.notifications
 
 import android.content.Intent
 import android.os.Bundle
@@ -9,22 +9,22 @@ import androidx.lifecycle.Observer
 import androidx.recyclerview.widget.LinearLayoutManager
 import com.afollestad.materialdialogs.MaterialDialog
 import com.readrops.app.R
-import com.readrops.app.adapters.NotificationPermissionListAdapter
+import com.readrops.app.settings.SettingsActivity
 import com.readrops.app.databinding.ActivityNotificationPermissionBinding
 import com.readrops.app.utils.ReadropsKeys
 import com.readrops.app.utils.ReadropsKeys.ACCOUNT_ID
 import com.readrops.app.utils.SharedPreferencesManager
 import com.readrops.app.utils.Utils
-import com.readrops.app.viewmodels.NotificationPermissionViewModel
 import com.readrops.db.entities.Feed
 import com.readrops.db.entities.account.Account
 import io.reactivex.android.schedulers.AndroidSchedulers
 import io.reactivex.schedulers.Schedulers
+import org.koin.androidx.viewmodel.ext.android.getViewModel
 
 class NotificationPermissionActivity : AppCompatActivity() {
 
     private lateinit var binding: ActivityNotificationPermissionBinding
-    private val viewModel by viewModels<NotificationPermissionViewModel>()
+    private lateinit var viewModel: NotificationPermissionViewModel
     private var adapter: NotificationPermissionListAdapter? = null
 
     private var isFirstCheck = true
@@ -41,6 +41,7 @@ class NotificationPermissionActivity : AppCompatActivity() {
 
         val accountId = intent.getIntExtra(ACCOUNT_ID, 0)
 
+        viewModel = getViewModel<NotificationPermissionViewModel>()
         viewModel.getAccount(accountId).observe(this, Observer { account ->
             viewModel.account = account
 
@@ -120,7 +121,7 @@ class NotificationPermissionActivity : AppCompatActivity() {
     }
 
     private fun displayAutoSynchroPopup() {
-        val autoSynchroValue = SharedPreferencesManager.readString(this, SharedPreferencesManager.SharedPrefKey.AUTO_SYNCHRO)
+        val autoSynchroValue = SharedPreferencesManager.readString(SharedPreferencesManager.SharedPrefKey.AUTO_SYNCHRO)
 
         if (autoSynchroValue.toFloat() <= 0) {
             MaterialDialog.Builder(this)
diff --git a/app/src/main/java/com/readrops/app/adapters/NotificationPermissionListAdapter.kt b/app/src/main/java/com/readrops/app/notifications/NotificationPermissionListAdapter.kt
similarity index 90%
rename from app/src/main/java/com/readrops/app/adapters/NotificationPermissionListAdapter.kt
rename to app/src/main/java/com/readrops/app/notifications/NotificationPermissionListAdapter.kt
index 9d8e019c..01cd67f0 100644
--- a/app/src/main/java/com/readrops/app/adapters/NotificationPermissionListAdapter.kt
+++ b/app/src/main/java/com/readrops/app/notifications/NotificationPermissionListAdapter.kt
@@ -1,4 +1,4 @@
-package com.readrops.app.adapters
+package com.readrops.app.notifications
 
 import android.view.LayoutInflater
 import android.view.ViewGroup
@@ -8,11 +8,13 @@ import androidx.recyclerview.widget.RecyclerView
 import com.bumptech.glide.load.engine.DiskCacheStrategy
 import com.readrops.app.R
 import com.readrops.app.databinding.NotificationPermissionLayoutBinding
-import com.readrops.app.utils.GlideApp
+import com.readrops.app.utils.GlideRequests
 import com.readrops.db.entities.Feed
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.get
 
 class NotificationPermissionListAdapter(var enableAll: Boolean, val listener: (feed: Feed) -> Unit) :
-        ListAdapter<Feed, NotificationPermissionListAdapter.NotificationPermissionViewHolder>(DIFF_CALLBACK) {
+        ListAdapter<Feed, NotificationPermissionListAdapter.NotificationPermissionViewHolder>(DIFF_CALLBACK), KoinComponent {
 
     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NotificationPermissionViewHolder {
         val binding = NotificationPermissionLayoutBinding.inflate(LayoutInflater.from(parent.context))
@@ -30,7 +32,7 @@ class NotificationPermissionListAdapter(var enableAll: Boolean, val listener: (f
 
         holder.itemView.setOnClickListener { if (enableAll) listener(getItem(position)) }
 
-        GlideApp.with(holder.itemView.context)
+        get<GlideRequests>()
                 .load(feed.iconUrl)
                 .diskCacheStrategy(DiskCacheStrategy.ALL)
                 .placeholder(R.drawable.ic_rss_feed_grey)
diff --git a/app/src/main/java/com/readrops/app/viewmodels/NotificationPermissionViewModel.kt b/app/src/main/java/com/readrops/app/notifications/NotificationPermissionViewModel.kt
similarity index 77%
rename from app/src/main/java/com/readrops/app/viewmodels/NotificationPermissionViewModel.kt
rename to app/src/main/java/com/readrops/app/notifications/NotificationPermissionViewModel.kt
index b425e70e..d555f8a0 100644
--- a/app/src/main/java/com/readrops/app/viewmodels/NotificationPermissionViewModel.kt
+++ b/app/src/main/java/com/readrops/app/notifications/NotificationPermissionViewModel.kt
@@ -1,16 +1,14 @@
-package com.readrops.app.viewmodels
+package com.readrops.app.notifications
 
-import android.app.Application
-import androidx.lifecycle.AndroidViewModel
 import androidx.lifecycle.LiveData
+import androidx.lifecycle.ViewModel
 import com.readrops.db.Database
 import com.readrops.db.entities.Feed
 import com.readrops.db.entities.account.Account
 import io.reactivex.Completable
 
-class NotificationPermissionViewModel(application: Application) : AndroidViewModel(application) {
+class NotificationPermissionViewModel(val database: Database) : ViewModel() {
 
-    val database: Database = Database.getInstance(application)
     var account: Account? = null
 
     fun getAccount(accountId: Int): LiveData<Account> = database.accountDao().selectAsync(accountId)
diff --git a/app/src/main/java/com/readrops/app/utils/SyncResultAnalyser.kt b/app/src/main/java/com/readrops/app/notifications/sync/SyncResultAnalyser.kt
similarity index 93%
rename from app/src/main/java/com/readrops/app/utils/SyncResultAnalyser.kt
rename to app/src/main/java/com/readrops/app/notifications/sync/SyncResultAnalyser.kt
index 497c24fe..b957e24b 100644
--- a/app/src/main/java/com/readrops/app/utils/SyncResultAnalyser.kt
+++ b/app/src/main/java/com/readrops/app/notifications/sync/SyncResultAnalyser.kt
@@ -1,4 +1,4 @@
-package com.readrops.app.utils
+package com.readrops.app.notifications.sync
 
 import android.content.Context
 import androidx.core.content.ContextCompat
@@ -9,11 +9,15 @@ import com.readrops.db.entities.Feed
 import com.readrops.db.entities.Item
 import com.readrops.db.entities.account.Account
 import com.readrops.api.services.SyncResult
+import com.readrops.app.utils.GlideRequests
+import com.readrops.app.utils.Utils
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.get
 
 /**
  * Simple class to get synchro notification content (title, content and largeIcon) according to some rules
  */
-class SyncResultAnalyser(val context: Context, private val syncResults: Map<Account, SyncResult>, val database: Database) {
+class SyncResultAnalyser(val context: Context, private val syncResults: Map<Account, SyncResult>, val database: Database) : KoinComponent {
 
     private val notifContent = SyncResultNotifContent()
 
@@ -66,7 +70,7 @@ class SyncResultAnalyser(val context: Context, private val syncResults: Map<Acco
             notifContent.title = feed?.name
 
             feed?.iconUrl?.let {
-                val target = GlideApp.with(context)
+                val target = get<GlideRequests>()
                         .asBitmap()
                         .load(it)
                         .diskCacheStrategy(DiskCacheStrategy.ALL)
diff --git a/app/src/main/java/com/readrops/app/utils/SyncResultDebugData.kt b/app/src/main/java/com/readrops/app/notifications/sync/SyncResultDebugData.kt
similarity index 88%
rename from app/src/main/java/com/readrops/app/utils/SyncResultDebugData.kt
rename to app/src/main/java/com/readrops/app/notifications/sync/SyncResultDebugData.kt
index 5018b464..847a2730 100644
--- a/app/src/main/java/com/readrops/app/utils/SyncResultDebugData.kt
+++ b/app/src/main/java/com/readrops/app/notifications/sync/SyncResultDebugData.kt
@@ -1,20 +1,21 @@
-package com.readrops.app.utils
+package com.readrops.app.notifications.sync
 
-import android.content.Context
+import com.readrops.api.services.SyncResult
 import com.readrops.db.Database
 import com.readrops.db.entities.Item
 import com.readrops.db.entities.account.Account
 import com.readrops.db.entities.account.AccountType
-import com.readrops.api.services.SyncResult
 import org.jetbrains.annotations.TestOnly
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.get
 
 class SyncResultDebugData {
 
-    companion object {
+    companion object : KoinComponent {
 
         @TestOnly
-        fun oneAccountOneFeedOneItem(context: Context): Map<Account, SyncResult> {
-            val database = Database.getInstance(context)
+        fun oneAccountOneFeedOneItem(): Map<Account, SyncResult> {
+            val database = get<Database>()
             val account1 = database.accountDao().select(2)
 
 
@@ -27,14 +28,14 @@ class SyncResultDebugData {
         }
 
         @TestOnly
-        fun oneAccountOneFeedMultipleItems(context: Context): Map<Account, SyncResult> {
+        fun oneAccountOneFeedMultipleItems(): Map<Account, SyncResult> {
             val account1 = Account().apply {
                 id = 1
                 accountType = AccountType.FRESHRSS
                 isNotificationsEnabled = true
             }
 
-            val database = Database.getInstance(context)
+            val database = get<Database>()
             val item = database.itemDao().select(5055)
             database.feedDao().updateFeedNotificationState(item.feedId, false).subscribe()
 
diff --git a/app/src/main/java/com/readrops/app/utils/SyncResultNotifContent.kt b/app/src/main/java/com/readrops/app/notifications/sync/SyncResultNotifContent.kt
similarity index 85%
rename from app/src/main/java/com/readrops/app/utils/SyncResultNotifContent.kt
rename to app/src/main/java/com/readrops/app/notifications/sync/SyncResultNotifContent.kt
index 3552e1c6..0e1891cf 100644
--- a/app/src/main/java/com/readrops/app/utils/SyncResultNotifContent.kt
+++ b/app/src/main/java/com/readrops/app/notifications/sync/SyncResultNotifContent.kt
@@ -1,4 +1,4 @@
-package com.readrops.app.utils
+package com.readrops.app.notifications.sync
 
 import android.graphics.Bitmap
 import com.readrops.db.entities.Item
diff --git a/app/src/main/java/com/readrops/app/utils/SyncWorker.kt b/app/src/main/java/com/readrops/app/notifications/sync/SyncWorker.kt
similarity index 86%
rename from app/src/main/java/com/readrops/app/utils/SyncWorker.kt
rename to app/src/main/java/com/readrops/app/notifications/sync/SyncWorker.kt
index 474b189d..0ccd90c9 100644
--- a/app/src/main/java/com/readrops/app/utils/SyncWorker.kt
+++ b/app/src/main/java/com/readrops/app/notifications/sync/SyncWorker.kt
@@ -1,4 +1,4 @@
-package com.readrops.app.utils
+package com.readrops.app.notifications.sync
 
 import android.app.PendingIntent
 import android.content.BroadcastReceiver
@@ -12,20 +12,25 @@ import androidx.work.WorkerParameters
 import com.readrops.api.services.SyncResult
 import com.readrops.app.R
 import com.readrops.app.ReadropsApp
-import com.readrops.app.activities.MainActivity
+import com.readrops.app.itemslist.MainActivity
 import com.readrops.app.repositories.ARepository
+import com.readrops.app.utils.ReadropsKeys
+import com.readrops.app.utils.SharedPreferencesManager
 import com.readrops.db.Database
 import com.readrops.db.entities.Item
 import com.readrops.db.entities.account.Account
 import io.reactivex.disposables.Disposable
 import io.reactivex.schedulers.Schedulers
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.get
+import org.koin.core.parameter.parametersOf
 
-class SyncWorker(context: Context, parameters: WorkerParameters) : Worker(context, parameters) {
+class SyncWorker(context: Context, parameters: WorkerParameters) : Worker(context, parameters), KoinComponent {
 
     private var disposable: Disposable? = null
 
     private val notificationManager = NotificationManagerCompat.from(applicationContext)
-    private val database = Database.getInstance(applicationContext)
+    private val database = get<Database>()
 
     override fun doWork(): Result {
         var result = Result.success()
@@ -39,15 +44,15 @@ class SyncWorker(context: Context, parameters: WorkerParameters) : Worker(contex
                     .setProgress(0, 0, true)
                     .setSmallIcon(R.drawable.ic_notif)
                     .setOnlyAlertOnce(true)
-            
+
             accounts.forEach {
                 notificationBuilder.setContentText(it.accountName)
                 notificationManager.notify(SYNC_NOTIFICATION_ID, notificationBuilder.build())
 
-                it.login = SharedPreferencesManager.readString(applicationContext, it.loginKey)
-                it.password = SharedPreferencesManager.readString(applicationContext, it.passwordKey)
+                it.login = SharedPreferencesManager.readString(it.loginKey)
+                it.password = SharedPreferencesManager.readString(it.passwordKey)
 
-                val repository = ARepository.repositoryFactory(it, applicationContext)
+                val repository = get<ARepository>(parameters = { parametersOf(it) })
 
                 disposable = repository.sync(null)
                         .doOnError { throwable ->
@@ -138,13 +143,13 @@ class SyncWorker(context: Context, parameters: WorkerParameters) : Worker(contex
                 .build()
     }
 
-    class MarkReadReceiver : BroadcastReceiver() {
+    class MarkReadReceiver : BroadcastReceiver(), KoinComponent {
 
         override fun onReceive(context: Context?, intent: Intent?) {
             val itemId = intent?.getIntExtra(ReadropsKeys.ITEM_ID, 0)!!
 
-            with(Database.getInstance(context)) {
-                itemDao().setReadState(itemId, true, true)
+            with(get<Database>()) {
+                itemDao().setReadState(itemId, true)
                         .subscribeOn(Schedulers.io())
                         .subscribe()
             }
@@ -155,13 +160,16 @@ class SyncWorker(context: Context, parameters: WorkerParameters) : Worker(contex
         }
     }
 
-    class ReadLaterReceiver : BroadcastReceiver() {
+    class ReadLaterReceiver : BroadcastReceiver(), KoinComponent {
 
         override fun onReceive(context: Context?, intent: Intent?) {
             val itemId = intent?.getIntExtra(ReadropsKeys.ITEM_ID, 0)!!
 
-            with(Database.getInstance(context)) {
-                itemDao().setReadItLater(itemId)
+            with(get<Database>()) {
+                val item = itemDao().select(itemId)
+                item.isReadItLater = !item.isReadItLater
+
+                itemDao().setReadItLater(item.isReadItLater, itemId)
                         .subscribeOn(Schedulers.io())
                         .subscribe()
             }
diff --git a/app/src/main/java/com/readrops/app/repositories/ARepository.java b/app/src/main/java/com/readrops/app/repositories/ARepository.java
index 56b07c2e..45839309 100644
--- a/app/src/main/java/com/readrops/app/repositories/ARepository.java
+++ b/app/src/main/java/com/readrops/app/repositories/ARepository.java
@@ -6,19 +6,24 @@ import android.content.Intent;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
-import com.readrops.app.utils.FeedInsertionResult;
-import com.readrops.app.utils.ParsingResult;
+import com.readrops.api.services.Credentials;
+import com.readrops.api.services.SyncResult;
+import com.readrops.api.utils.AuthInterceptor;
+import com.readrops.app.addfeed.FeedInsertionResult;
+import com.readrops.app.addfeed.ParsingResult;
 import com.readrops.app.utils.feedscolors.FeedColorsKt;
 import com.readrops.app.utils.feedscolors.FeedsColorsIntentService;
 import com.readrops.db.Database;
 import com.readrops.db.entities.Feed;
 import com.readrops.db.entities.Folder;
 import com.readrops.db.entities.Item;
+import com.readrops.db.entities.ItemState;
 import com.readrops.db.entities.account.Account;
-import com.readrops.db.entities.account.AccountType;
-import com.readrops.api.services.SyncResult;
+
+import org.koin.java.KoinJavaComponent;
 
 import java.util.ArrayList;
+import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
 import java.util.TreeMap;
@@ -29,28 +34,28 @@ import io.reactivex.Single;
 
 import static com.readrops.app.utils.ReadropsKeys.FEEDS;
 
-public abstract class ARepository<T> {
+public abstract class ARepository {
 
     protected Context context;
     protected Database database;
     protected Account account;
 
-    protected T api;
-
     protected SyncResult syncResult;
 
-    protected ARepository(@NonNull Context context, @Nullable Account account) {
+    protected ARepository(Database database, @NonNull Context context, @Nullable Account account) {
         this.context = context;
-        this.database = Database.getInstance(context);
+        this.database = database;
         this.account = account;
 
-        api = createAPI();
+        setCredentials(account);
     }
 
-    protected abstract T createAPI();
+    protected void setCredentials(@Nullable Account account) {
+        KoinJavaComponent.get(AuthInterceptor.class)
+                .setCredentials(account != null && !account.isLocal() ? Credentials.toCredentials(account) : null);
+    }
 
-    // TODO : replace Single by Completable
-    public abstract Single<Boolean> login(Account account, boolean insert);
+    public abstract Completable login(Account account, boolean insert);
 
     public abstract Observable<Feed> sync(List<Feed> feeds);
 
@@ -109,20 +114,47 @@ public abstract class ARepository<T> {
         return database.folderDao().delete(folder);
     }
 
-    public Completable setItemReadState(Item item, boolean read) {
-        return setItemReadState(item.getId(), read, !item.isReadChanged());
-    }
+    public Completable setItemReadState(Item item) {
+        if (account.getConfig().useSeparateState()) {
+            return database.itemStateChangesDao().upsertItemReadStateChange(item, account.getId(), true)
+                    .andThen(database.itemStateDao().upsertItemReadState(new ItemState(0, item.isRead(),
+                            item.isStarred(), item.getRemoteId(), account.getId())));
+        } else if (account.isLocal()) {
+            return database.itemDao().setReadState(item.getId(), item.isRead());
+        } else { // nextcloud case
+            return database.itemStateChangesDao().upsertItemReadStateChange(item, account.getId(), false)
+                    .andThen(database.itemDao().setReadState(item.getId(), item.isRead()));
+        }
 
-    public Completable setItemReadState(int itemId, boolean read, boolean readChanged) {
-        return database.itemDao().setReadState(itemId, read, readChanged);
     }
 
     public Completable setAllItemsReadState(boolean read) {
-        return database.itemDao().setAllItemsReadState(read ? 1 : 0, account.getId());
+        if (account.isLocal()) { // TODO see if it's possible to implement for others accounts
+            return database.itemDao().setAllItemsReadState(read ? 1 : 0, account.getId());
+        } else {
+            return Completable.complete();
+        }
     }
 
     public Completable setAllFeedItemsReadState(int feedId, boolean read) {
-        return database.itemDao().setAllFeedItemsReadState(feedId, read ? 1 : 0);
+        if (account.isLocal()) {
+            return database.itemDao().setAllFeedItemsReadState(feedId, read ? 1 : 0);
+        } else {
+            return Completable.complete();
+        }
+    }
+
+    public Completable setItemStarState(Item item) {
+        if (account.getConfig().useSeparateState()) {
+            return database.itemStateChangesDao().upsertItemStarStateChange(item, account.getId(), true)
+                    .andThen(database.itemStateDao().upsertItemStarState(new ItemState(0, item.isRead(),
+                            item.isStarred(), item.getRemoteId(), account.getId())));
+        } else if (account.isLocal()) {
+            return database.itemDao().setStarState(item.getId(), item.isRead());
+        } else { // nextcloud case
+            return database.itemStateChangesDao().upsertItemStarStateChange(item, account.getId(), false)
+                    .andThen(database.itemDao().setStarState(item.getId(), item.isStarred()));
+        }
     }
 
     public Single<Integer> getFeedCount(int accountId) {
@@ -132,7 +164,7 @@ public abstract class ARepository<T> {
     public Single<Map<Folder, List<Feed>>> getFoldersWithFeeds() {
         return Single.create(emitter -> {
             List<Folder> folders = database.folderDao().getFolders(account.getId());
-            Map<Folder, List<Feed>> foldersWithFeeds = new TreeMap<>(Folder::compareTo);
+            Map<Folder, List<Feed>> foldersWithFeeds = new TreeMap<>(Comparator.nullsLast(Folder::compareTo));
 
             for (Folder folder : folders) {
                 List<Feed> feeds = database.feedDao().getFeedsByFolder(folder.getId());
@@ -145,14 +177,13 @@ public abstract class ARepository<T> {
                 foldersWithFeeds.put(folder, feeds);
             }
 
-            Folder noFolder = new Folder("no folder");
-
+            // feeds without folder
             List<Feed> feedsWithoutFolder = database.feedDao().getFeedsWithoutFolder(account.getId());
             for (Feed feed : feedsWithoutFolder) {
                 feed.setUnreadCount(database.itemDao().getUnreadCount(feed.getId()));
             }
 
-            foldersWithFeeds.put(noFolder, feedsWithoutFolder);
+            foldersWithFeeds.put(null, feedsWithoutFolder);
 
             emitter.onSuccess(foldersWithFeeds);
         });
@@ -171,23 +202,6 @@ public abstract class ARepository<T> {
         context.startService(intent);
     }
 
-    public static ARepository repositoryFactory(Account account, AccountType accountType, Context context) throws Exception {
-        switch (accountType) {
-            case LOCAL:
-                return new LocalFeedRepository(context, account);
-            case NEXTCLOUD_NEWS:
-                return new NextNewsRepository(context, account);
-            case FRESHRSS:
-                return new FreshRSSRepository(context, account);
-            default:
-                throw new Exception("account type not supported");
-        }
-    }
-
-    public static ARepository repositoryFactory(Account account, Context context) throws Exception {
-        return ARepository.repositoryFactory(account, account.getAccountType(), context);
-    }
-
     public SyncResult getSyncResult() {
         return syncResult;
     }
diff --git a/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java b/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java
index 744e80f6..5161c6ea 100644
--- a/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java
+++ b/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.java
@@ -7,77 +7,74 @@ import android.util.TimingLogger;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
-import com.readrops.app.utils.FeedInsertionResult;
-import com.readrops.app.utils.ParsingResult;
+import com.readrops.api.services.SyncType;
+import com.readrops.api.services.freshrss.FreshRSSDataSource;
+import com.readrops.api.services.freshrss.FreshRSSSyncData;
+import com.readrops.app.addfeed.FeedInsertionResult;
+import com.readrops.app.addfeed.ParsingResult;
 import com.readrops.app.utils.Utils;
+import com.readrops.db.Database;
 import com.readrops.db.entities.Feed;
 import com.readrops.db.entities.Folder;
 import com.readrops.db.entities.Item;
+import com.readrops.db.entities.ItemState;
 import com.readrops.db.entities.account.Account;
-import com.readrops.api.services.Credentials;
-import com.readrops.api.services.SyncType;
-import com.readrops.api.services.freshrss.FreshRSSAPI;
-import com.readrops.api.services.freshrss.FreshRSSCredentials;
-import com.readrops.api.services.freshrss.FreshRSSSyncData;
+import com.readrops.db.pojo.ItemReadStarState;
 
 import org.joda.time.DateTime;
 
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
 
 import io.reactivex.Completable;
 import io.reactivex.Observable;
 import io.reactivex.Single;
 
-public class FreshRSSRepository extends ARepository<FreshRSSAPI> {
+public class FreshRSSRepository extends ARepository {
 
     private static final String TAG = FreshRSSRepository.class.getSimpleName();
 
-    public FreshRSSRepository(@NonNull Context context, @Nullable Account account) {
-        super(context, account);
+    private final FreshRSSDataSource dataSource;
+
+    public FreshRSSRepository(FreshRSSDataSource dataSource, Database database, @NonNull Context context, @Nullable Account account) {
+        super(database, context, account);
+
+        this.dataSource = dataSource;
     }
 
     @Override
-    protected FreshRSSAPI createAPI() {
-        if (account != null)
-            return new FreshRSSAPI(Credentials.toCredentials(account));
+    public Completable login(Account account, boolean insert) {
+        setCredentials(account);
 
-        return null;
-    }
-
-    @Override
-    public Single<Boolean> login(Account account, boolean insert) {
-        if (api == null)
-            api = new FreshRSSAPI(Credentials.toCredentials(account));
-        else
-            api.setCredentials(Credentials.toCredentials(account));
-
-        return api.login(account.getLogin(), account.getPassword())
+        return dataSource.login(account.getLogin(), account.getPassword())
                 .flatMap(token -> {
                     account.setToken(token);
-                    api.setCredentials(new FreshRSSCredentials(token, account.getUrl()));
+                    setCredentials(account);
 
-                    return api.getWriteToken();
+                    return dataSource.getWriteToken();
                 })
                 .flatMap(writeToken -> {
                     account.setWriteToken(writeToken);
 
-                    return api.getUserInfo();
+                    return dataSource.getUserInfo();
                 })
-                .flatMap(userInfo -> {
+                .flatMapCompletable(userInfo -> {
                     account.setDisplayedName(userInfo.getUserName());
 
                     if (insert) {
                         return database.accountDao().insert(account)
-                                .flatMap(id -> {
+                                .flatMapCompletable(id -> {
                                     account.setId(id.intValue());
 
-                                    return Single.just(true);
+                                    return Completable.complete();
                                 });
                     }
 
-                    return Single.just(true);
+                    return Completable.complete();
                 });
     }
 
@@ -96,11 +93,32 @@ public class FreshRSSRepository extends ARepository<FreshRSSAPI> {
         TimingLogger logger = new TimingLogger(TAG, "FreshRSS sync timer");
 
         return Single.<FreshRSSSyncData>create(emitter -> {
-            syncData.setReadItemsIds(database.itemDao().getReadChanges(account.getId()));
-            syncData.setUnreadItemsIds(database.itemDao().getUnreadChanges(account.getId()));
+            List<ItemReadStarState> itemStateChanges = database
+                    .itemStateChangesDao()
+                    .getItemStateChanges(account.getId());
+
+            syncData.setReadItemsIds(itemStateChanges.stream()
+                    .filter(it -> it.getReadChange() && it.getRead())
+                    .map(ItemReadStarState::getRemoteId)
+                    .collect(Collectors.toList()));
+
+            syncData.setUnreadItemsIds(itemStateChanges.stream()
+                    .filter(it -> it.getReadChange() && !it.getRead())
+                    .map(ItemReadStarState::getRemoteId)
+                    .collect(Collectors.toList()));
+
+            syncData.setStarredItemsIds(itemStateChanges.stream()
+                    .filter(it -> it.getStarChange() && it.getStarred())
+                    .map(ItemReadStarState::getRemoteId)
+                    .collect(Collectors.toList()));
+
+            syncData.setUnstarredItemsIds(itemStateChanges.stream()
+                    .filter(it -> it.getStarChange() && !it.getStarred())
+                    .map(ItemReadStarState::getRemoteId)
+                    .collect(Collectors.toList()));
 
             emitter.onSuccess(syncData);
-        }).flatMap(syncData1 -> api.sync(syncType, syncData1, account.getWriteToken()))
+        }).flatMap(syncData1 -> dataSource.sync(syncType, syncData1, account.getWriteToken()))
                 .flatMapObservable(syncResult -> {
                     logger.addSplit("server queries");
 
@@ -109,14 +127,20 @@ public class FreshRSSRepository extends ARepository<FreshRSSAPI> {
                     insertFeeds(syncResult.getFeeds());
                     logger.addSplit("feeds insertion");
 
-                    insertItems(syncResult.getItems(), syncType == SyncType.INITIAL_SYNC);
+                    insertItems(syncResult.getItems(), false);
                     logger.addSplit("items insertion");
 
+                    insertItems(syncResult.getStarredItems(), true);
+                    logger.addSplit("starred items insertion");
+
+                    insertItemsIds(syncResult.getUnreadIds(), syncResult.getStarredIds());
+                    logger.addSplit("insert and update items ids");
+
                     account.setLastModified(newLastModified);
                     database.accountDao().updateLastModified(account.getId(), newLastModified);
 
-                    database.itemDao().resetReadChanges(account.getId());
-                    logger.addSplit("reset read changes");
+                    database.itemStateChangesDao().resetStateChanges(account.getId());
+
                     logger.dumpToLog();
 
                     this.syncResult = syncResult;
@@ -131,7 +155,7 @@ public class FreshRSSRepository extends ARepository<FreshRSSAPI> {
         List<FeedInsertionResult> insertionResults = new ArrayList<>();
 
         for (ParsingResult result : results) {
-            completableList.add(api.createFeed(account.getWriteToken(), result.getUrl())
+            completableList.add(dataSource.createFeed(account.getWriteToken(), result.getUrl())
                     .doOnComplete(() -> {
                         FeedInsertionResult feedInsertionResult = new FeedInsertionResult();
                         feedInsertionResult.setParsingResult(result);
@@ -159,26 +183,26 @@ public class FreshRSSRepository extends ARepository<FreshRSSAPI> {
             Folder folder = feed.getFolderId() == null ? null : database.folderDao().select(feed.getFolderId());
             emitter.onSuccess(folder);
 
-        }).flatMapCompletable(folder -> api.updateFeed(account.getWriteToken(),
+        }).flatMapCompletable(folder -> dataSource.updateFeed(account.getWriteToken(),
                 feed.getUrl(), feed.getName(), folder == null ? null : folder.getRemoteId())
                 .andThen(super.updateFeed(feed)));
     }
 
     @Override
     public Completable deleteFeed(Feed feed) {
-        return api.deleteFeed(account.getWriteToken(), feed.getUrl())
+        return dataSource.deleteFeed(account.getWriteToken(), feed.getUrl())
                 .andThen(super.deleteFeed(feed));
     }
 
     @Override
     public Single<Long> addFolder(Folder folder) {
-        return api.createFolder(account.getWriteToken(), folder.getName())
+        return dataSource.createFolder(account.getWriteToken(), folder.getName())
                 .andThen(super.addFolder(folder));
     }
 
     @Override
     public Completable updateFolder(Folder folder) {
-        return api.updateFolder(account.getWriteToken(), folder.getRemoteId(), folder.getName())
+        return dataSource.updateFolder(account.getWriteToken(), folder.getRemoteId(), folder.getName())
                 .andThen(Completable.create(emitter -> {
                     folder.setRemoteId("user/-/label/" + folder.getName());
                     emitter.onComplete();
@@ -188,14 +212,12 @@ public class FreshRSSRepository extends ARepository<FreshRSSAPI> {
 
     @Override
     public Completable deleteFolder(Folder folder) {
-        return api.deleteFolder(account.getWriteToken(), folder.getRemoteId())
+        return dataSource.deleteFolder(account.getWriteToken(), folder.getRemoteId())
                 .andThen(super.deleteFolder(folder));
     }
 
     private void insertFeeds(List<Feed> freshRSSFeeds) {
-        for (Feed feed : freshRSSFeeds) {
-            feed.setAccountId(account.getId());
-        }
+        freshRSSFeeds.stream().forEach(feed -> feed.setAccountId(account.getId()));
 
         List<Long> insertedFeedsIds = database.feedDao().feedsUpsert(freshRSSFeeds, account);
 
@@ -206,27 +228,62 @@ public class FreshRSSRepository extends ARepository<FreshRSSAPI> {
     }
 
     private void insertFolders(List<Folder> freshRSSFolders) {
-        for (Folder folder : freshRSSFolders) {
-            folder.setAccountId(account.getId());
-        }
+        freshRSSFolders.stream().forEach(folder -> folder.setAccountId(account.getId()));
 
         database.folderDao().foldersUpsert(freshRSSFolders, account);
     }
 
-    private void insertItems(List<Item> items, boolean initialSync) {
-        for (Item item : items) {
-            int feedId = database.feedDao().getFeedIdByRemoteId(item.getFeedRemoteId(), account.getId());
+    private void insertItems(List<Item> items, boolean starredItems) {
+        List<Item> itemsToInsert = new ArrayList<>();
+        Map<String, Integer> itemsFeedsIds = new HashMap<>();
 
-            if (!initialSync && feedId > 0 && database.itemDao().remoteItemExists(item.getRemoteId(), feedId)) {
-                database.itemDao().setReadState(item.getRemoteId(), item.isRead());
-                continue;
+        for (Item item : items) {
+            Integer feedId;
+            if (itemsFeedsIds.containsKey(item.getFeedRemoteId())) {
+                feedId = itemsFeedsIds.get(item.getFeedRemoteId());
+            } else {
+                feedId = database.feedDao().getFeedIdByRemoteId(item.getFeedRemoteId(), account.getId());
+                itemsFeedsIds.put(item.getFeedRemoteId(), feedId);
             }
 
             item.setFeedId(feedId);
             item.setReadTime(Utils.readTimeFromString(item.getContent()));
+
+            // workaround to avoid inserting starred items coming from the main item call
+            // as the API exclusion filter doesn't seem to work
+            if (!starredItems) {
+                if (!item.isStarred()) {
+                    itemsToInsert.add(item);
+                }
+            } else {
+                itemsToInsert.add(item);
+            }
         }
 
-        Collections.sort(items, Item::compareTo);
-        database.itemDao().insert(items);
+        if (!itemsToInsert.isEmpty()) {
+            Collections.sort(itemsToInsert, Item::compareTo);
+            database.itemDao().insert(itemsToInsert);
+        }
+    }
+
+    private void insertItemsIds(List<String> unreadIds, List<String> starredIds) {
+        database.itemStateDao().deleteItemsStates(account.getId());
+
+        database.itemStateDao().insertItemStates(unreadIds.stream().map(id -> {
+                    boolean starred = starredIds.stream().filter(starredId -> starredId.equals(id)).count() == 1;
+                    if (starred) {
+                        starredIds.remove(id);
+                    }
+
+                    return new ItemState(0, false, starred, id, account.getId());
+                }
+        ).collect(Collectors.toList()));
+
+        // insert starred items ids which are read
+        if (!starredIds.isEmpty()) {
+            database.itemStateDao().insertItemStates(starredIds.stream().map(id ->
+                    new ItemState(0, true, true, id, account.getId()))
+                    .collect(Collectors.toList()));
+        }
     }
 }
diff --git a/app/src/main/java/com/readrops/app/repositories/LocalFeedRepository.java b/app/src/main/java/com/readrops/app/repositories/LocalFeedRepository.java
index e3d9651d..724c7afa 100644
--- a/app/src/main/java/com/readrops/app/repositories/LocalFeedRepository.java
+++ b/app/src/main/java/com/readrops/app/repositories/LocalFeedRepository.java
@@ -2,30 +2,24 @@ package com.readrops.app.repositories;
 
 import android.accounts.NetworkErrorException;
 import android.content.Context;
+import android.util.Log;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
-import com.readrops.app.utils.FeedInsertionResult;
-import com.readrops.app.utils.HtmlParser;
-import com.readrops.app.utils.ParsingResult;
+import com.readrops.api.localfeed.LocalRSSDataSource;
+import com.readrops.api.services.SyncResult;
+import com.readrops.api.utils.ApiUtils;
+import com.readrops.api.utils.exceptions.ParseException;
+import com.readrops.api.utils.exceptions.UnknownFormatException;
+import com.readrops.app.addfeed.FeedInsertionResult;
+import com.readrops.app.addfeed.ParsingResult;
 import com.readrops.app.utils.SharedPreferencesManager;
 import com.readrops.app.utils.Utils;
-import com.readrops.app.utils.matchers.FeedMatcher;
-import com.readrops.app.utils.matchers.ItemMatcher;
+import com.readrops.db.Database;
 import com.readrops.db.entities.Feed;
 import com.readrops.db.entities.Item;
 import com.readrops.db.entities.account.Account;
-import com.readrops.api.localfeed.AFeed;
-import com.readrops.api.localfeed.RSSQuery;
-import com.readrops.api.localfeed.RSSQueryResult;
-import com.readrops.api.localfeed.atom.ATOMFeed;
-import com.readrops.api.localfeed.json.JSONFeed;
-import com.readrops.api.localfeed.rss.RSSFeed;
-import com.readrops.api.services.SyncResult;
-import com.readrops.api.utils.LibUtils;
-import com.readrops.api.utils.ParseException;
-import com.readrops.api.utils.UnknownFormatException;
 
 import org.jsoup.Jsoup;
 
@@ -33,29 +27,29 @@ import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.List;
 
+import io.reactivex.Completable;
 import io.reactivex.Observable;
 import io.reactivex.Single;
+import kotlin.Pair;
+import okhttp3.Headers;
 
-public class LocalFeedRepository extends ARepository<Void> {
+public class LocalFeedRepository extends ARepository {
 
     private static final String TAG = LocalFeedRepository.class.getSimpleName();
 
-    public LocalFeedRepository(@NonNull Context context, @Nullable Account account) {
-        super(context, account);
+    private LocalRSSDataSource dataSource;
+
+    public LocalFeedRepository(LocalRSSDataSource dataSource, Database database, @NonNull Context context, @Nullable Account account) {
+        super(database, context, account);
 
         syncResult = new SyncResult();
+        this.dataSource = dataSource;
     }
 
     @Override
-    protected Void createAPI() {
-        return null;
-    }
-
-    @Override
-    public Single<Boolean> login(Account account, boolean insert) {
+    public Completable login(Account account, boolean insert) {
         return null;
     }
 
@@ -64,47 +58,31 @@ public class LocalFeedRepository extends ARepository<Void> {
         return Observable.create(emitter -> {
             List<Feed> feedList;
 
-            if (feeds == null || feeds.size() == 0)
+            if (feeds == null || feeds.isEmpty()) {
                 feedList = database.feedDao().getFeeds(account.getId());
-            else
-                feedList = new ArrayList<>(feeds);
-
-            RSSQuery rssQuery = new RSSQuery();
-            List<FeedInsertionResult> syncErrors = new ArrayList<>();
+            } else {
+                feedList = feeds;
+            }
 
             for (Feed feed : feedList) {
                 emitter.onNext(feed);
-                FeedInsertionResult syncError = new FeedInsertionResult();
 
                 try {
-                    HashMap<String, String> headers = new HashMap<>();
-                    if (feed.getEtag() != null)
-                        headers.put(LibUtils.IF_NONE_MATCH_HEADER, feed.getEtag());
-                    if (feed.getLastModified() != null)
-                        headers.put(LibUtils.IF_MODIFIED_HEADER, feed.getLastModified());
+                    Headers.Builder headers = new Headers.Builder();
+                    if (feed.getEtag() != null) {
+                        headers.add(ApiUtils.IF_NONE_MATCH_HEADER, feed.getEtag());
+                    }
+                    if (feed.getLastModified() != null) {
+                        headers.add(ApiUtils.IF_MODIFIED_HEADER, feed.getLastModified());
+                    }
 
-                    RSSQueryResult queryResult = rssQuery.queryUrl(feed.getUrl(), headers);
-                    if (queryResult != null && queryResult.getException() == null)
-                        insertNewItems(queryResult.getFeed(), queryResult.getRssType());
-                    else if (queryResult != null && queryResult.getException() != null) {
-                        Exception e = queryResult.getException();
+                    Pair<Feed, List<Item>> pair = dataSource.queryRSSResource(feed.getUrl(), headers.build());
 
-                        if (e instanceof UnknownFormatException)
-                            syncError.setInsertionError(FeedInsertionResult.FeedInsertionError.FORMAT_ERROR);
-                        else if (e instanceof NetworkErrorException)
-                            syncError.setInsertionError(FeedInsertionResult.FeedInsertionError.NETWORK_ERROR);
-
-                        syncError.setFeed(feed);
-                        syncErrors.add(syncError);
+                    if (pair != null) {
+                        insertNewItems(feed, pair.getSecond());
                     }
                 } catch (Exception e) {
-                    if (e instanceof IOException)
-                        syncError.setInsertionError(FeedInsertionResult.FeedInsertionError.NETWORK_ERROR);
-                    else
-                        syncError.setInsertionError(FeedInsertionResult.FeedInsertionError.PARSE_ERROR);
-
-                    syncError.setFeed(feed);
-                    syncErrors.add(syncError);
+                    Log.d(TAG, "sync: " + e.getMessage());
                 }
             }
 
@@ -121,28 +99,26 @@ public class LocalFeedRepository extends ARepository<Void> {
                 FeedInsertionResult insertionResult = new FeedInsertionResult();
 
                 try {
-                    RSSQuery rssNet = new RSSQuery();
-                    RSSQueryResult queryResult = rssNet.queryUrl(parsingResult.getUrl(), new HashMap<>());
+                    Pair<Feed, List<Item>> pair = dataSource.queryRSSResource(parsingResult.getUrl(),
+                            null);
+                    Feed feed = insertFeed(pair.getFirst(), parsingResult);
 
-                    if (queryResult != null && queryResult.getException() == null) {
-                        Feed feed = insertFeed(queryResult.getFeed(), queryResult.getRssType(), parsingResult);
-                        if (feed != null) {
-                            insertionResult.setFeed(feed);
-                            insertionResult.setParsingResult(parsingResult);
-                            insertionResults.add(insertionResult);
-                        }
-                    } else if (queryResult != null && queryResult.getException() != null) {
-                        insertionResult.setParsingResult(parsingResult);
-                        insertionResult.setInsertionError(getErrorFromException(queryResult.getException()));
-
-                        insertionResults.add(insertionResult);
+                    if (feed != null) {
+                        insertionResult.setFeed(feed);
                     }
+                } catch (ParseException e) {
+                    Log.d(TAG, "addFeeds: " + e.getMessage());
+                    insertionResult.setInsertionError(FeedInsertionResult.FeedInsertionError.PARSE_ERROR);
+                } catch (UnknownFormatException e) {
+                    Log.d(TAG, "addFeeds: " + e.getMessage());
+                    insertionResult.setInsertionError(FeedInsertionResult.FeedInsertionError.FORMAT_ERROR);
+                } catch (NetworkErrorException | IOException e) {
+                    Log.d(TAG, "addFeeds: " + e.getMessage());
+                    insertionResult.setInsertionError(FeedInsertionResult.FeedInsertionError.NETWORK_ERROR);
                 } catch (Exception e) {
-                    if (e instanceof IOException)
-                        insertionResult.setInsertionError(FeedInsertionResult.FeedInsertionError.NETWORK_ERROR);
-                    else
-                        insertionResult.setInsertionError(FeedInsertionResult.FeedInsertionError.PARSE_ERROR);
-
+                    Log.d(TAG, "addFeeds: " + e.getMessage());
+                    insertionResult.setInsertionError(FeedInsertionResult.FeedInsertionError.UNKNOWN_ERROR);
+                } finally {
                     insertionResult.setParsingResult(parsingResult);
                     insertionResults.add(insertionResult);
                 }
@@ -152,67 +128,38 @@ public class LocalFeedRepository extends ARepository<Void> {
         });
     }
 
-    private void insertNewItems(AFeed feed, RSSQuery.RSSType type) throws ParseException {
-        Feed dbFeed;
-        List<Item> items;
+    @SuppressWarnings("SimplifyStreamApiCallChains")
+    private void insertNewItems(Feed feed, List<Item> items) {
+        database.feedDao().updateHeaders(feed.getEtag(), feed.getLastModified(), feed.getId());
 
-        switch (type) {
-            case RSS_2:
-                dbFeed = database.feedDao().getFeedByUrl(((RSSFeed) feed).getChannel().getFeedUrl(), account.getId());
-                items = ItemMatcher.itemsFromRSS(((RSSFeed) feed).getChannel().getItems(), dbFeed);
-                break;
-            case RSS_ATOM:
-                dbFeed = database.feedDao().getFeedByUrl(((ATOMFeed) feed).getUrl(), account.getId());
-                items = ItemMatcher.itemsFromATOM(((ATOMFeed) feed).getEntries(), dbFeed);
-                break;
-            case RSS_JSON:
-                dbFeed = database.feedDao().getFeedByUrl(((JSONFeed) feed).getFeedUrl(), account.getId());
-                items = ItemMatcher.itemsFromJSON(((JSONFeed) feed).getItems(), dbFeed);
-                break;
-            default:
-                throw new IllegalArgumentException("Unknown RSS type");
-        }
-
-        database.feedDao().updateHeaders(dbFeed.getEtag(), dbFeed.getLastModified(), dbFeed.getId());
         Collections.sort(items, Item::compareTo);
 
-        int maxItems = Integer.parseInt(SharedPreferencesManager.readString(context, SharedPreferencesManager.SharedPrefKey.ITEMS_TO_PARSE_MAX_NB));
-        if (maxItems > 0 && items.size() > maxItems)
+        int maxItems = Integer.parseInt(SharedPreferencesManager.readString(
+                SharedPreferencesManager.SharedPrefKey.ITEMS_TO_PARSE_MAX_NB));
+        if (maxItems > 0 && items.size() > maxItems) {
             items = items.subList(items.size() - maxItems, items.size());
-
-        insertItems(items, dbFeed);
-    }
-
-    private Feed insertFeed(AFeed feed, RSSQuery.RSSType type, ParsingResult parsingResult) {
-        Feed dbFeed;
-        switch (type) {
-            case RSS_2:
-                dbFeed = FeedMatcher.feedFromRSS((RSSFeed) feed);
-                break;
-            case RSS_ATOM:
-                dbFeed = FeedMatcher.feedFromATOM((ATOMFeed) feed);
-                break;
-            case RSS_JSON:
-                dbFeed = FeedMatcher.feedFromJSON((JSONFeed) feed);
-                break;
-            default:
-                throw new IllegalArgumentException("Unknown RSS type");
         }
 
-        dbFeed.setFolderId(parsingResult.getFolderId());
+        items.stream().forEach(item -> item.setFeedId(feed.getId()));
+        insertItems(items, feed);
+    }
 
-        if (database.feedDao().feedExists(dbFeed.getUrl(), account.getId()))
+    private Feed insertFeed(Feed feed, ParsingResult parsingResult) {
+        feed.setFolderId(parsingResult.getFolderId());
+
+        if (database.feedDao().feedExists(feed.getUrl(), account.getId())) {
             return null; // feed already inserted
+        }
 
-        setFeedColors(dbFeed);
-        dbFeed.setAccountId(account.getId());
+        setFeedColors(feed);
+        feed.setAccountId(account.getId());
 
         // we need empty headers to query the feed just after, without any 304 result
-        dbFeed.setEtag(null);
-        dbFeed.setLastModified(null);
+        feed.setEtag(null);
+        feed.setLastModified(null);
 
-        dbFeed.setId((int) (database.feedDao().compatInsert(dbFeed)));
-        return dbFeed;
+        feed.setId((int) (database.feedDao().compatInsert(feed)));
+        return feed;
     }
 
     private void insertItems(Collection<Item> items, Feed feed) {
@@ -222,29 +169,13 @@ public class LocalFeedRepository extends ARepository<Void> {
             if (!database.itemDao().itemExists(dbItem.getGuid(), feed.getAccountId())) {
                 if (dbItem.getDescription() != null) {
                     dbItem.setCleanDescription(Jsoup.parse(dbItem.getDescription()).text());
-
-                    if (dbItem.getImageLink() == null) {
-                        String imageUrl = HtmlParser.getDescImageLink(dbItem.getDescription(), feed.getSiteUrl());
-
-                        if (imageUrl != null)
-                            dbItem.setImageLink(imageUrl);
-                    }
                 }
 
-                // we check a second time because imageLink could have been set earlier with media:content tag value
-                if (dbItem.getImageLink() != null) {
-                    if (dbItem.getContent() != null) {
-                        // removing cover image in content if found in description
-                        dbItem.setContent(HtmlParser.deleteCoverImage(dbItem.getContent()));
-
-                    } else if (dbItem.getDescription() != null)
-                        dbItem.setDescription(HtmlParser.deleteCoverImage(dbItem.getDescription()));
-                }
-
-                if (dbItem.getContent() != null)
-                    dbItem.setReadTime(Utils.readTimeFromString(Jsoup.parse(dbItem.getContent()).text()));
-                else if (dbItem.getDescription() != null)
+                if (dbItem.getContent() != null) {
+                    dbItem.setReadTime(Utils.readTimeFromString(dbItem.getContent()));
+                } else if (dbItem.getDescription() != null) {
                     dbItem.setReadTime(Utils.readTimeFromString(dbItem.getCleanDescription()));
+                }
 
                 itemsToInsert.add(dbItem);
             }
@@ -253,13 +184,4 @@ public class LocalFeedRepository extends ARepository<Void> {
         syncResult.getItems().addAll(itemsToInsert);
         database.itemDao().insert(itemsToInsert);
     }
-
-    private FeedInsertionResult.FeedInsertionError getErrorFromException(Exception e) {
-        if (e instanceof UnknownFormatException)
-            return FeedInsertionResult.FeedInsertionError.FORMAT_ERROR;
-        else if (e instanceof NetworkErrorException)
-            return FeedInsertionResult.FeedInsertionError.NETWORK_ERROR;
-        else
-            return FeedInsertionResult.FeedInsertionError.UNKNOWN_ERROR;
-    }
 }
diff --git a/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java b/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java
index 018d6a7d..ea6f6e57 100644
--- a/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java
+++ b/app/src/main/java/com/readrops/app/repositories/NextNewsRepository.java
@@ -2,106 +2,149 @@ package com.readrops.app.repositories;
 
 import android.content.Context;
 import android.database.sqlite.SQLiteConstraintException;
+import android.util.Log;
 import android.util.TimingLogger;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
-import com.readrops.api.services.Credentials;
 import com.readrops.api.services.SyncResult;
 import com.readrops.api.services.SyncType;
-import com.readrops.api.services.nextcloudnews.NextNewsAPI;
+import com.readrops.api.services.nextcloudnews.NextNewsDataSource;
 import com.readrops.api.services.nextcloudnews.NextNewsSyncData;
-import com.readrops.api.services.nextcloudnews.json.NextNewsUser;
-import com.readrops.api.utils.UnknownFormatException;
-import com.readrops.app.utils.FeedInsertionResult;
-import com.readrops.app.utils.ParsingResult;
+import com.readrops.api.services.nextcloudnews.adapters.NextNewsUserAdapter;
+import com.readrops.api.utils.exceptions.UnknownFormatException;
+import com.readrops.app.addfeed.FeedInsertionResult;
+import com.readrops.app.addfeed.ParsingResult;
 import com.readrops.app.utils.Utils;
+import com.readrops.db.Database;
 import com.readrops.db.entities.Feed;
 import com.readrops.db.entities.Folder;
 import com.readrops.db.entities.Item;
 import com.readrops.db.entities.account.Account;
+import com.readrops.db.pojo.ItemReadStarState;
 
 import org.joda.time.LocalDateTime;
+import org.koin.java.KoinJavaComponent;
 
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.stream.Collectors;
 
 import io.reactivex.Completable;
 import io.reactivex.Observable;
 import io.reactivex.Single;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
 
-public class NextNewsRepository extends ARepository<NextNewsAPI> {
+public class NextNewsRepository extends ARepository {
 
     private static final String TAG = NextNewsRepository.class.getSimpleName();
 
-    public NextNewsRepository(@NonNull Context context, @Nullable Account account) {
-        super(context, account);
+    private final NextNewsDataSource dataSource;
+
+    public NextNewsRepository(NextNewsDataSource dataSource, Database database, @NonNull Context context, @Nullable Account account) {
+        super(database, context, account);
+
+        this.dataSource = dataSource;
     }
 
     @Override
-    protected NextNewsAPI createAPI() {
-        if (account != null)
-            return new NextNewsAPI(Credentials.toCredentials(account));
+    public Completable login(Account account, boolean insert) {
+        setCredentials(account);
+        return Single.<String>create(emitter -> {
+            OkHttpClient httpClient = KoinJavaComponent.get(OkHttpClient.class);
 
-        return null;
-    }
+            Request request = new Request.Builder()
+                    .url(account.getUrl() + "/ocs/v1.php/cloud/users/" + account.getLogin())
+                    .addHeader("OCS-APIRequest", "true")
+                    .build();
 
-    @Override
-    public Single<Boolean> login(Account account, boolean insert) {
-        return Single.<NextNewsUser>create(emitter -> {
-            if (api == null)
-                api = new NextNewsAPI(Credentials.toCredentials(account));
-            else
-                api.setCredentials(Credentials.toCredentials(account));
+            Response response = httpClient.newCall(request).execute();
 
-            NextNewsUser user = api.login();
+            if (response.isSuccessful()) {
+                String displayName = new NextNewsUserAdapter().fromXml(response.body().byteStream());
+                response.body().close();
 
-            if (user != null) {
-                emitter.onSuccess(user);
+                emitter.onSuccess(displayName);
             } else {
-                emitter.onError(new Exception("Login failed. Please check your credentials and your Nextcloud News setup."));
+                // TODO better error handling
+                emitter.onError(new Exception("Login exception : " + response.code() + " error"));
             }
-        }).flatMap(user -> {
-            account.setDisplayedName(user.getDisplayName());
+        }).flatMapCompletable(displayName -> {
+            account.setDisplayedName(displayName);
             account.setCurrentAccount(true);
 
             if (insert) {
                 return database.accountDao().insert(account)
-                        .flatMap(id -> {
+                        .flatMapCompletable(id -> {
                             account.setId(id.intValue());
-                            return Single.just(true);
+                            return Completable.complete();
                         });
             }
 
-            return Single.just(true);
+            return Completable.complete();
         });
     }
 
     @Override
     public Observable<Feed> sync(List<Feed> feeds) {
+        setCredentials(account);
         return Observable.create(emitter -> {
             try {
                 long lastModified = LocalDateTime.now().toDateTime().getMillis();
                 SyncType syncType;
 
-                if (account.getLastModified() != 0)
+                if (account.getLastModified() != 0) {
                     syncType = SyncType.CLASSIC_SYNC;
-                else
+                } else {
                     syncType = SyncType.INITIAL_SYNC;
+                }
 
                 NextNewsSyncData syncData = new NextNewsSyncData();
 
                 if (syncType == SyncType.CLASSIC_SYNC) {
                     syncData.setLastModified(account.getLastModified() / 1000L);
-                    syncData.setReadItems(database.itemDao().getReadChanges(account.getId()));
-                    syncData.setUnreadItems(database.itemDao().getUnreadChanges(account.getId()));
+
+                    List<ItemReadStarState> itemStateChanges = database
+                            .itemStateChangesDao()
+                            .getNextcloudNewsStateChanges(account.getId());
+
+                    syncData.setReadItems(itemStateChanges.stream()
+                            .filter(it -> it.getReadChange() && it.getRead())
+                            .map(ItemReadStarState::getRemoteId)
+                            .collect(Collectors.toList()));
+
+                    syncData.setUnreadItems(itemStateChanges.stream()
+                            .filter(it -> it.getReadChange() && !it.getRead())
+                            .map(ItemReadStarState::getRemoteId)
+                            .collect(Collectors.toList()));
+
+                    List<String> starredItemsIds = itemStateChanges.stream()
+                            .filter(it -> it.getStarChange() && it.getStarred())
+                            .map(ItemReadStarState::getRemoteId)
+                            .collect(Collectors.toList());
+
+                    if (!starredItemsIds.isEmpty()) {
+                        syncData.setStarredItems(database.itemDao().getStarChanges(starredItemsIds, account.getId()));
+                    }
+
+                    List<String> unstarredItemsIds = itemStateChanges.stream()
+                            .filter(it -> it.getStarChange() && !it.getStarred())
+                            .map(ItemReadStarState::getRemoteId)
+                            .collect(Collectors.toList());
+
+                    if (!unstarredItemsIds.isEmpty()) {
+                        syncData.setUnstarredItems(database.itemDao().getStarChanges(unstarredItemsIds, account.getId()));
+                    }
+
                 }
 
                 TimingLogger timings = new TimingLogger(TAG, "nextcloud news " + syncType.name().toLowerCase());
-                SyncResult result = api.sync(syncType, syncData);
+                SyncResult result = dataSource.sync(syncType, syncData);
                 timings.addSplit("server queries");
 
                 if (!result.isError()) {
@@ -113,20 +156,25 @@ public class NextNewsRepository extends ARepository<NextNewsAPI> {
                     insertFeeds(result.getFeeds(), false);
                     timings.addSplit("insert feeds");
 
-                    insertItems(result.getItems(), syncType == SyncType.INITIAL_SYNC);
+                    boolean initialSync = syncType == SyncType.INITIAL_SYNC;
+                    insertItems(result.getItems(), initialSync);
                     timings.addSplit("insert items");
+
+                    insertItems(result.getStarredItems(), initialSync);
                     timings.dumpToLog();
 
                     account.setLastModified(lastModified);
                     database.accountDao().updateLastModified(account.getId(), lastModified);
-                    database.itemDao().resetReadChanges(account.getId());
+
+                    database.itemStateChangesDao().resetStateChanges(account.getId());
 
                     emitter.onComplete();
-                } else
+                } else {
                     emitter.onError(new Throwable());
+                }
 
             } catch (Exception e) {
-                e.printStackTrace();
+                Log.d(TAG, "sync: " + e.getMessage());
                 emitter.onError(e);
             }
         });
@@ -134,6 +182,7 @@ public class NextNewsRepository extends ARepository<NextNewsAPI> {
 
     @Override
     public Single<List<FeedInsertionResult>> addFeeds(List<ParsingResult> results) {
+        setCredentials(account);
         return Single.create(emitter -> {
             List<FeedInsertionResult> feedInsertionResults = new ArrayList<>();
 
@@ -141,11 +190,11 @@ public class NextNewsRepository extends ARepository<NextNewsAPI> {
                 FeedInsertionResult insertionResult = new FeedInsertionResult();
 
                 try {
-                    List<Feed> nextNewsFeeds = api.createFeed(result.getUrl(), 0);
+                    List<Feed> nextNewsFeeds = dataSource.createFeed(result.getUrl(), 0);
 
                     if (nextNewsFeeds != null) {
                         List<Feed> newFeeds = insertFeeds(nextNewsFeeds, true);
-                        // there is always only one object in the list, see nextcloud news api doc
+                        // there is always only one object in the list, see nextcloud news dataSource doc
                         insertionResult.setFeed(newFeeds.get(0));
                     } else
                         insertionResult.setInsertionError(FeedInsertionResult.FeedInsertionError.UNKNOWN_ERROR);
@@ -171,6 +220,7 @@ public class NextNewsRepository extends ARepository<NextNewsAPI> {
 
     @Override
     public Completable updateFeed(Feed feed) {
+        setCredentials(account);
         return Completable.create(emitter -> {
             Folder folder = feed.getFolderId() == null ? null : database.folderDao().select(feed.getFolderId());
 
@@ -180,7 +230,7 @@ public class NextNewsRepository extends ARepository<NextNewsAPI> {
                 feed.setRemoteFolderId(String.valueOf(0)); // 0 for no folder
 
             try {
-                if (api.renameFeed(feed) && api.changeFeedFolder(feed)) {
+                if (dataSource.renameFeed(feed) && dataSource.changeFeedFolder(feed)) {
                     emitter.onComplete();
                 } else
                     emitter.onError(new Exception("Unknown error when updating feed"));
@@ -192,9 +242,10 @@ public class NextNewsRepository extends ARepository<NextNewsAPI> {
 
     @Override
     public Completable deleteFeed(Feed feed) {
+        setCredentials(account);
         return Completable.create(emitter -> {
             try {
-                if (api.deleteFeed(Integer.parseInt(feed.getRemoteId()))) {
+                if (dataSource.deleteFeed(Integer.parseInt(feed.getRemoteId()))) {
                     emitter.onComplete();
                 } else
                     emitter.onError(new Exception("Unknown error"));
@@ -208,9 +259,10 @@ public class NextNewsRepository extends ARepository<NextNewsAPI> {
 
     @Override
     public Single<Long> addFolder(Folder folder) {
+        setCredentials(account);
         return Single.<Folder>create(emitter -> {
             try {
-                List<Folder> folders = api.createFolder(folder);
+                List<Folder> folders = dataSource.createFolder(folder);
 
                 if (folders != null) {
                     Folder nextNewsFolder = folders.get(0); // always only one item returned by the server, see doc
@@ -227,9 +279,10 @@ public class NextNewsRepository extends ARepository<NextNewsAPI> {
 
     @Override
     public Completable updateFolder(Folder folder) {
+        setCredentials(account);
         return Completable.create(emitter -> {
             try {
-                if (api.renameFolder(folder)) {
+                if (dataSource.renameFolder(folder)) {
                     emitter.onComplete();
                 } else
                     emitter.onError(new Exception("Unknown error"));
@@ -244,9 +297,10 @@ public class NextNewsRepository extends ARepository<NextNewsAPI> {
 
     @Override
     public Completable deleteFolder(Folder folder) {
+        setCredentials(account);
         return Completable.create(emitter -> {
             try {
-                if (api.deleteFolder(folder)) {
+                if (dataSource.deleteFolder(folder)) {
                     emitter.onComplete();
                 } else
                     emitter.onError(new Exception("Unknown error"));
@@ -296,7 +350,7 @@ public class NextNewsRepository extends ARepository<NextNewsAPI> {
 
             //if the item already exists, update only its read state
             if (!initialSync && feedId > 0 && database.itemDao().remoteItemExists(String.valueOf(item.getRemoteId()), feedId)) {
-                database.itemDao().setReadState(item.getRemoteId(), item.isRead());
+                database.itemDao().setReadAndStarState(item.getRemoteId(), item.isRead(), item.isStarred());
                 continue;
             }
 
diff --git a/app/src/main/java/com/readrops/app/fragments/settings/AccountSettingsFragment.java b/app/src/main/java/com/readrops/app/settings/AccountSettingsFragment.java
similarity index 73%
rename from app/src/main/java/com/readrops/app/fragments/settings/AccountSettingsFragment.java
rename to app/src/main/java/com/readrops/app/settings/AccountSettingsFragment.java
index a2221f56..ecf63de3 100644
--- a/app/src/main/java/com/readrops/app/fragments/settings/AccountSettingsFragment.java
+++ b/app/src/main/java/com/readrops/app/settings/AccountSettingsFragment.java
@@ -1,4 +1,4 @@
-package com.readrops.app.fragments.settings;
+package com.readrops.app.settings;
 
 
 import android.Manifest;
@@ -7,44 +7,48 @@ import android.app.PendingIntent;
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.net.Uri;
+import android.os.Build;
 import android.os.Bundle;
-import android.os.Environment;
 import android.provider.Settings;
-import android.util.Log;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.core.app.NotificationCompat;
 import androidx.core.app.NotificationManagerCompat;
 import androidx.fragment.app.Fragment;
-import androidx.lifecycle.ViewModelProvider;
 import androidx.preference.Preference;
 import androidx.preference.PreferenceFragmentCompat;
 
 import com.afollestad.materialdialogs.MaterialDialog;
+import com.readrops.api.opml.OPMLHelper;
 import com.readrops.api.opml.OPMLParser;
 import com.readrops.app.R;
 import com.readrops.app.ReadropsApp;
-import com.readrops.app.activities.AddAccountActivity;
-import com.readrops.app.activities.ManageFeedsFoldersActivity;
-import com.readrops.app.activities.NotificationPermissionActivity;
+import com.readrops.app.account.AccountViewModel;
+import com.readrops.app.account.AddAccountActivity;
+import com.readrops.app.feedsfolders.ManageFeedsFoldersActivity;
+import com.readrops.app.notifications.NotificationPermissionActivity;
+import com.readrops.app.utils.FileUtils;
 import com.readrops.app.utils.PermissionManager;
 import com.readrops.app.utils.SharedPreferencesManager;
 import com.readrops.app.utils.Utils;
-import com.readrops.app.viewmodels.AccountViewModel;
+import com.readrops.db.entities.Feed;
+import com.readrops.db.entities.Folder;
 import com.readrops.db.entities.account.Account;
 import com.readrops.db.entities.account.AccountType;
 
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.OutputStream;
+import org.koin.androidx.viewmodel.compat.ViewModelCompat;
+
+import java.util.List;
+import java.util.Map;
 
 import io.reactivex.android.schedulers.AndroidSchedulers;
 import io.reactivex.observers.DisposableCompletableObserver;
 import io.reactivex.schedulers.Schedulers;
+import kotlin.Unit;
 
 import static android.app.Activity.RESULT_OK;
+import static com.readrops.api.opml.OPMLHelper.OPEN_OPML_FILE_REQUEST;
 import static com.readrops.app.utils.ReadropsKeys.ACCOUNT;
 import static com.readrops.app.utils.ReadropsKeys.ACCOUNT_ID;
 import static com.readrops.app.utils.ReadropsKeys.EDIT_ACCOUNT;
@@ -56,7 +60,6 @@ public class AccountSettingsFragment extends PreferenceFragmentCompat {
 
     private static final String TAG = AccountSettingsFragment.class.getSimpleName();
 
-    public static final int OPEN_OPML_FILE_REQUEST = 1;
     private static final int WRITE_EXTERNAL_STORAGE_REQUEST = 1;
 
     private Account account;
@@ -76,6 +79,7 @@ public class AccountSettingsFragment extends PreferenceFragmentCompat {
         return fragment;
     }
 
+    @SuppressWarnings("ConstantConditions")
     @Override
     public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
         addPreferencesFromResource(R.xml.acount_preferences);
@@ -120,16 +124,7 @@ public class AccountSettingsFragment extends PreferenceFragmentCompat {
         opmlPref.setOnPreferenceClickListener(preference -> {
             new MaterialDialog.Builder(getActivity())
                     .items(R.array.opml_import_export)
-                    .itemsCallback(((dialog, itemView, position, text) -> {
-                        if (position == 0) {
-                            openOPMLFile();
-                        } else {
-                            if (PermissionManager.isPermissionGranted(getContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE))
-                                exportAsOPMLFile();
-                            else
-                                requestExternalStoragePermission();
-                        }
-                    }))
+                    .itemsCallback(((dialog, itemView, position, text) -> openOPMLMode(position)))
                     .show();
             return true;
         });
@@ -147,7 +142,7 @@ public class AccountSettingsFragment extends PreferenceFragmentCompat {
     public void onCreate(@Nullable Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
 
-        viewModel = new ViewModelProvider(this).get(AccountViewModel.class);
+        viewModel = ViewModelCompat.getViewModel(this, AccountViewModel.class);
         viewModel.setAccount(account);
     }
 
@@ -157,8 +152,8 @@ public class AccountSettingsFragment extends PreferenceFragmentCompat {
                 .positiveText(R.string.validate)
                 .negativeText(R.string.cancel)
                 .onPositive(((dialog, which) -> {
-                    SharedPreferencesManager.remove(getContext(), account.getLoginKey());
-                    SharedPreferencesManager.remove(getContext(), account.getPasswordKey());
+                    SharedPreferencesManager.remove(account.getLoginKey());
+                    SharedPreferencesManager.remove(account.getPasswordKey());
 
                     viewModel.delete(account)
                             .subscribeOn(Schedulers.io())
@@ -178,16 +173,24 @@ public class AccountSettingsFragment extends PreferenceFragmentCompat {
                 .show();
     }
 
-    // region opml import
-
-    private void openOPMLFile() {
-        Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
-        intent.addCategory(Intent.CATEGORY_OPENABLE);
-        intent.setType("application/*");
-
-        startActivityForResult(intent, OPEN_OPML_FILE_REQUEST);
+    private void openOPMLMode(int position) {
+        if (position == 0) {
+            OPMLHelper.openFileIntent(this);
+        } else {
+            if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
+                if (PermissionManager.isPermissionGranted(getContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
+                    exportAsOPMLFile();
+                } else {
+                    requestExternalStoragePermission();
+                }
+            } else {
+                exportAsOPMLFile();
+            }
+        }
     }
 
+    // region opml import
+
     @Override
     public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
         if (requestCode == OPEN_OPML_FILE_REQUEST && resultCode == RESULT_OK && data != null) {
@@ -207,7 +210,7 @@ public class AccountSettingsFragment extends PreferenceFragmentCompat {
     }
 
     private void parseOPMLFile(Uri uri, MaterialDialog dialog) {
-        viewModel.parseOPMLFile(uri)
+        viewModel.parseOPMLFile(uri, getContext())
                 .subscribeOn(Schedulers.io())
                 .observeOn(AndroidSchedulers.mainThread())
                 .subscribe(new DisposableCompletableObserver() {
@@ -238,51 +241,35 @@ public class AccountSettingsFragment extends PreferenceFragmentCompat {
     //region opml export
 
     private void exportAsOPMLFile() {
+        String fileName = "subscriptions.opml";
+
         try {
-            String filePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath();
-            File file = new File(filePath, "subscriptions.opml");
+            String path = FileUtils.writeDownloadFile(getContext(), fileName, "text/x-opml", outputStream -> {
+                Map<Folder, List<Feed>> folderListMap = viewModel.getFoldersWithFeeds()
+                        .subscribeOn(Schedulers.io())
+                        .blockingGet();
 
-            final OutputStream outputStream = new FileOutputStream(file);
 
-            viewModel.getFoldersWithFeeds()
-                    .flatMapCompletable(folderListMap -> OPMLParser.write(folderListMap, outputStream))
-                    .subscribeOn(Schedulers.io())
-                    .observeOn(AndroidSchedulers.mainThread())
-                    .doAfterTerminate(() -> {
-                        try {
-                            outputStream.flush();
-                            outputStream.close();
+                OPMLParser.write(folderListMap, outputStream)
+                        .blockingAwait();
 
-                        } catch (IOException e) {
-                            Log.e(TAG, e.getMessage());
-                            Utils.showSnackbar(getView(), e.getMessage());
-                        }
-                    })
-                    .subscribe(new DisposableCompletableObserver() {
-                        @Override
-                        public void onComplete() {
-                            displayNotification(file);
-                        }
+                return Unit.INSTANCE;
+            });
 
-                        @Override
-                        public void onError(Throwable e) {
-                            Utils.showSnackbar(getView(), e.getMessage());
-                        }
-                    });
+            displayNotification(fileName, path);
         } catch (Exception e) {
-            Log.e(TAG, e.getMessage());
-            Utils.showSnackbar(getView(), e.getMessage());
+            displayErrorMessage();
         }
 
     }
 
-    private void displayNotification(File file) {
+    private void displayNotification(String name, String absolutePath) {
         Intent intent = new Intent(Intent.ACTION_VIEW);
-        intent.setDataAndType(Uri.parse(file.getAbsolutePath()), "text/plain");
+        intent.setDataAndType(Uri.parse(absolutePath), "text/plain");
 
         Notification notification = new NotificationCompat.Builder(getContext(), ReadropsApp.OPML_EXPORT_CHANNEL_ID)
                 .setContentTitle(getString(R.string.opml_export))
-                .setContentText(file.getName())
+                .setContentText(name)
                 .setSmallIcon(R.drawable.ic_notif)
                 .setContentIntent(PendingIntent.getActivity(getContext(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT))
                 .setAutoCancel(true)
diff --git a/app/src/main/java/com/readrops/app/activities/SettingsActivity.java b/app/src/main/java/com/readrops/app/settings/SettingsActivity.java
similarity index 91%
rename from app/src/main/java/com/readrops/app/activities/SettingsActivity.java
rename to app/src/main/java/com/readrops/app/settings/SettingsActivity.java
index 0a259894..06b0a639 100644
--- a/app/src/main/java/com/readrops/app/activities/SettingsActivity.java
+++ b/app/src/main/java/com/readrops/app/settings/SettingsActivity.java
@@ -1,4 +1,4 @@
-package com.readrops.app.activities;
+package com.readrops.app.settings;
 
 import android.os.Bundle;
 import android.view.MenuItem;
@@ -8,8 +8,6 @@ import androidx.fragment.app.Fragment;
 
 import com.readrops.app.R;
 import com.readrops.db.entities.account.Account;
-import com.readrops.app.fragments.settings.AccountSettingsFragment;
-import com.readrops.app.fragments.settings.SettingsFragment;
 
 import static com.readrops.app.utils.ReadropsKeys.ACCOUNT;
 import static com.readrops.app.utils.ReadropsKeys.SETTINGS;
diff --git a/app/src/main/java/com/readrops/app/fragments/settings/SettingsFragment.java b/app/src/main/java/com/readrops/app/settings/SettingsFragment.java
similarity index 95%
rename from app/src/main/java/com/readrops/app/fragments/settings/SettingsFragment.java
rename to app/src/main/java/com/readrops/app/settings/SettingsFragment.java
index ce84fd77..c0a02dcd 100644
--- a/app/src/main/java/com/readrops/app/fragments/settings/SettingsFragment.java
+++ b/app/src/main/java/com/readrops/app/settings/SettingsFragment.java
@@ -1,4 +1,4 @@
-package com.readrops.app.fragments.settings;
+package com.readrops.app.settings;
 
 import android.content.Intent;
 import android.os.Bundle;
@@ -15,10 +15,12 @@ import androidx.work.PeriodicWorkRequest;
 import androidx.work.WorkManager;
 
 import com.readrops.app.R;
-import com.readrops.app.utils.SyncWorker;
+import com.readrops.app.notifications.sync.SyncWorker;
 import com.readrops.app.utils.feedscolors.FeedsColorsIntentService;
 import com.readrops.db.Database;
 
+import org.koin.java.KoinJavaComponent;
+
 import java.util.ArrayList;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -38,7 +40,7 @@ public class SettingsFragment extends PreferenceFragmentCompat {
 
         AtomicBoolean serviceStarted = new AtomicBoolean(false);
         feedsColorsPreference.setOnPreferenceClickListener(preference -> {
-            Database database = Database.getInstance(getContext());
+            Database database = KoinJavaComponent.get(Database.class);
 
             database.feedDao().getAllFeeds().observe(getActivity(), feeds -> {
                 if (!serviceStarted.get()) {
diff --git a/app/src/main/java/com/readrops/app/utils/DateUtils.java b/app/src/main/java/com/readrops/app/utils/DateUtils.java
deleted file mode 100644
index f7ea1e36..00000000
--- a/app/src/main/java/com/readrops/app/utils/DateUtils.java
+++ /dev/null
@@ -1,60 +0,0 @@
-package com.readrops.app.utils;
-
-import org.joda.time.LocalDateTime;
-import org.joda.time.format.DateTimeFormat;
-import org.joda.time.format.DateTimeFormatter;
-import org.joda.time.format.DateTimeFormatterBuilder;
-
-import java.util.Locale;
-
-public final class DateUtils {
-
-    /**
-     * Base of common RSS 2 date formats.
-     * Examples :
-     * Fri, 04 Jan 2019 22:21:46 GMT
-     * Fri, 04 Jan 2019 22:21:46 +0000
-     */
-    private static final String RSS_2_BASE_PATTERN = "EEE, dd MMM yyyy HH:mm:ss";
-
-    private static final String GMT_PATTERN = "ZZZ";
-
-    private static final String OFFSET_PATTERN = "Z";
-
-    private static final String ISO_PATTERN = ".SSSZZ";
-
-    private static final String EDT_PATTERN = "zzz";
-
-    /**
-     * Date pattern for format : 2019-01-04T22:21:46+00:00
-     */
-    private static final String ATOM_JSON_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss";
-
-    public static LocalDateTime stringToLocalDateTime(String value) {
-        DateTimeFormatter formatter = new DateTimeFormatterBuilder()
-                .appendOptional(DateTimeFormat.forPattern(RSS_2_BASE_PATTERN + " ").getParser()) // with timezone
-                .appendOptional(DateTimeFormat.forPattern(RSS_2_BASE_PATTERN).getParser()) // no timezone, important order here
-                .appendOptional(DateTimeFormat.forPattern(ATOM_JSON_DATE_FORMAT).getParser())
-                .appendOptional(DateTimeFormat.forPattern(GMT_PATTERN).getParser())
-                .appendOptional(DateTimeFormat.forPattern(OFFSET_PATTERN).getParser())
-                .appendOptional(DateTimeFormat.forPattern(ISO_PATTERN).getParser())
-                .appendOptional(DateTimeFormat.forPattern(EDT_PATTERN).getParser())
-                .toFormatter()
-                .withLocale(Locale.ENGLISH)
-                .withOffsetParsed();
-
-        return formatter.parseLocalDateTime(value);
-    }
-
-    public static String formattedDateByLocal(LocalDateTime dateTime) {
-        return DateTimeFormat.mediumDate()
-                .withLocale(Locale.getDefault())
-                .print(dateTime);
-    }
-
-    public static String formattedDateTimeByLocal(LocalDateTime dateTime) {
-        return DateTimeFormat.forPattern("dd MMM yyyy · HH:mm")
-                .withLocale(Locale.getDefault())
-                .print(dateTime);
-    }
-}
diff --git a/app/src/main/java/com/readrops/app/utils/FileUtils.kt b/app/src/main/java/com/readrops/app/utils/FileUtils.kt
new file mode 100644
index 00000000..e0aba2ff
--- /dev/null
+++ b/app/src/main/java/com/readrops/app/utils/FileUtils.kt
@@ -0,0 +1,69 @@
+package com.readrops.app.utils
+
+import android.content.ContentValues
+import android.content.Context
+import android.os.Build
+import android.os.Environment
+import android.provider.MediaStore
+import androidx.annotation.RequiresApi
+import java.io.File
+import java.io.FileOutputStream
+import java.io.OutputStream
+
+object FileUtils {
+
+    @JvmStatic
+    fun writeDownloadFile(context: Context, fileName: String, mimeType: String, listener: (OutputStream) -> Unit): String {
+        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
+            writeFileApi29(context, fileName, mimeType, listener)
+        else
+            writeFileApi28(fileName, listener)
+    }
+
+    @RequiresApi(Build.VERSION_CODES.Q)
+    private fun writeFileApi29(context: Context, fileName: String, mimeType: String, listener: (OutputStream) -> Unit): String {
+        val resolver = context.contentResolver
+        val downloadsUri = MediaStore.Downloads
+                .getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
+
+        val fileDetails = ContentValues().apply {
+            put(MediaStore.Downloads.DISPLAY_NAME, fileName)
+            put(MediaStore.Downloads.IS_PENDING, 1)
+            put(MediaStore.Downloads.MIME_TYPE, mimeType)
+        }
+
+        val contentUri = resolver.insert(downloadsUri, fileDetails)
+
+        resolver.openOutputStream(contentUri!!)!!.use { stream ->
+            try {
+                listener(stream)
+            } catch (e: Exception) {
+                throw e
+            } finally {
+                stream.flush()
+                stream.close()
+            }
+
+            fileDetails.put(MediaStore.Downloads.IS_PENDING, 0)
+            resolver.update(contentUri, fileDetails, null, null)
+        }
+
+        fileDetails.put(MediaStore.Downloads.IS_PENDING, 0)
+        resolver.update(contentUri, fileDetails, null, null)
+
+        return contentUri.path!!
+    }
+
+    private fun writeFileApi28(fileName: String, listener: (OutputStream) -> Unit): String {
+        val filePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath
+        val file = File(filePath, fileName)
+
+        val outputStream = FileOutputStream(file)
+        listener(outputStream)
+
+        outputStream.flush()
+        outputStream.close()
+
+        return file.absolutePath
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/readrops/app/utils/GlideModule.java b/app/src/main/java/com/readrops/app/utils/GlideModule.java
deleted file mode 100644
index 7d765a40..00000000
--- a/app/src/main/java/com/readrops/app/utils/GlideModule.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package com.readrops.app.utils;
-
-import androidx.annotation.NonNull;
-
-import com.bumptech.glide.module.AppGlideModule;
-
-@com.bumptech.glide.annotation.GlideModule
-public class GlideModule extends AppGlideModule {
-
-}
diff --git a/app/src/main/java/com/readrops/app/utils/HtmlParser.java b/app/src/main/java/com/readrops/app/utils/HtmlParser.java
index 85c7965d..e7ef597a 100644
--- a/app/src/main/java/com/readrops/app/utils/HtmlParser.java
+++ b/app/src/main/java/com/readrops/app/utils/HtmlParser.java
@@ -5,20 +5,22 @@ import android.util.Log;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
-import com.readrops.api.utils.HttpManager;
-import com.readrops.api.utils.LibUtils;
+import com.readrops.api.localfeed.LocalRSSHelper;
+import com.readrops.api.utils.ApiUtils;
+import com.readrops.api.utils.AuthInterceptor;
+import com.readrops.app.addfeed.ParsingResult;
 
 import org.jsoup.Jsoup;
 import org.jsoup.nodes.Document;
 import org.jsoup.nodes.Element;
 import org.jsoup.select.Elements;
+import org.koin.java.KoinJavaComponent;
 
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
-import java.util.regex.Pattern;
 
+import okhttp3.OkHttpClient;
 import okhttp3.Request;
 import okhttp3.Response;
 
@@ -26,8 +28,6 @@ public final class HtmlParser {
 
     private static final String TAG = HtmlParser.class.getSimpleName();
 
-    public static final String COVER_IMAGE_REGEX = "^(<p>|(<div.*>))?<img.*>";
-
     /**
      * Parse the html page to get all rss urls
      *
@@ -46,7 +46,7 @@ public final class HtmlParser {
             for (Element element : elements) {
                 String type = element.attributes().get("type");
 
-                if (isTypeRssFeed(type)) {
+                if (LocalRSSHelper.isRSSType(type)) {
                     String feedUrl = element.absUrl("href");
                     String label = element.attributes().get("title");
 
@@ -60,35 +60,6 @@ public final class HtmlParser {
         }
     }
 
-    private static boolean isTypeRssFeed(String type) {
-        return type.equals(LibUtils.RSS_DEFAULT_CONTENT_TYPE) ||
-                type.equals(LibUtils.ATOM_CONTENT_TYPE) ||
-                type.equals(LibUtils.JSON_CONTENT_TYPE) ||
-                type.equals(LibUtils.RSS_TEXT_CONTENT_TYPE) ||
-                type.equals(LibUtils.RSS_APPLICATION_CONTENT_TYPE);
-    }
-
-    /**
-     * get the feed item image based on open graph metadata.
-     * Warning, This method is slow.
-     *
-     * @param url url to request
-     * @return the item image
-     */
-    public static String getOGImageLink(String url) throws IOException {
-        String imageUrl = null;
-
-        String head = getHTMLHeadFromUrl(url);
-
-        Document document = Jsoup.parse(head);
-        Element element = document.select("meta[property=og:image]").first();
-
-        if (element != null)
-            imageUrl = element.attributes().get("content");
-
-        return imageUrl;
-    }
-
     @Nullable
     public static String getFaviconLink(@NonNull String url) {
         String favUrl = null;
@@ -115,10 +86,11 @@ public final class HtmlParser {
         long start = System.currentTimeMillis();
 
         try {
-            Response response = HttpManager.getInstance().getOkHttpClient()
+            Response response = KoinJavaComponent.get(OkHttpClient.class)
                     .newCall(new Request.Builder().url(url).build()).execute();
+            KoinJavaComponent.get(AuthInterceptor.class).setCredentials(null);
 
-            if (response.header("Content-Type").contains(LibUtils.HTML_CONTENT_TYPE)) {
+            if (response.header("Content-Type").contains(ApiUtils.HTML_CONTENT_TYPE)) {
                 String body = response.body().string();
                 String head = body.substring(body.indexOf("<head"), body.indexOf("</head>"));
 
@@ -130,32 +102,9 @@ public final class HtmlParser {
                 return null;
             }
         } catch (Exception e) {
+            Log.d(TAG, e.getMessage());
             return null;
         }
 
     }
-
-    public static String getDescImageLink(String description, String url) {
-        Document document = Jsoup.parse(description, url);
-        Elements elements = document.select("img");
-
-        if (!elements.isEmpty())
-            return elements.first().absUrl("src");
-        else
-            return null;
-    }
-
-    public static String deleteCoverImage(String content) {
-        Document document = Jsoup.parse(content);
-
-        if (Pattern.compile(COVER_IMAGE_REGEX).matcher(document.body().html()).find()) {
-            Elements elements = document.select("img");
-
-            if (!elements.isEmpty())
-                elements.first().remove();
-
-            return document.toString();
-        } else
-            return content;
-    }
 }
diff --git a/app/src/main/java/com/readrops/app/utils/PermissionManager.kt b/app/src/main/java/com/readrops/app/utils/PermissionManager.kt
index 55242ef9..6d72c145 100644
--- a/app/src/main/java/com/readrops/app/utils/PermissionManager.kt
+++ b/app/src/main/java/com/readrops/app/utils/PermissionManager.kt
@@ -7,20 +7,18 @@ import androidx.core.app.ActivityCompat
 import androidx.core.content.ContextCompat
 import androidx.fragment.app.Fragment
 
-class PermissionManager {
+object PermissionManager {
 
-    companion object {
-        @JvmStatic
-        fun isPermissionGranted(context: Context, permission: String): Boolean =
-                ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
+    @JvmStatic
+    fun isPermissionGranted(context: Context, permission: String): Boolean =
+            ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED
 
-        @JvmStatic
-        fun requestPermissions(activity: Activity, requestCode: Int, vararg permissions: String) =
-                ActivityCompat.requestPermissions(activity, permissions, requestCode)
+    @JvmStatic
+    fun requestPermissions(activity: Activity, requestCode: Int, vararg permissions: String) =
+            ActivityCompat.requestPermissions(activity, permissions, requestCode)
 
-        @JvmStatic
-        fun requestPermissions(fragment: Fragment, requestCode: Int, vararg permissions: String) =
-                fragment.requestPermissions(permissions, requestCode)
-    }
+    @JvmStatic
+    fun requestPermissions(fragment: Fragment, requestCode: Int, vararg permissions: String) =
+            fragment.requestPermissions(permissions, requestCode)
 
 }
\ No newline at end of file
diff --git a/app/src/main/java/com/readrops/app/utils/ReadropsGlideModule.kt b/app/src/main/java/com/readrops/app/utils/ReadropsGlideModule.kt
new file mode 100644
index 00000000..aebc4491
--- /dev/null
+++ b/app/src/main/java/com/readrops/app/utils/ReadropsGlideModule.kt
@@ -0,0 +1,23 @@
+package com.readrops.app.utils
+
+import android.content.Context
+import com.bumptech.glide.Glide
+import com.bumptech.glide.Registry
+import com.bumptech.glide.annotation.GlideModule
+import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
+import com.bumptech.glide.load.model.GlideUrl
+import com.bumptech.glide.module.AppGlideModule
+import okhttp3.OkHttpClient
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.get
+import java.io.InputStream
+
+@GlideModule
+class ReadropsGlideModule : AppGlideModule(), KoinComponent {
+
+    override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
+        val factory = OkHttpUrlLoader.Factory(get<OkHttpClient>())
+
+        glide.registry.replace(GlideUrl::class.java, InputStream::class.java, factory)
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/readrops/app/utils/ReadropsKeys.kt b/app/src/main/java/com/readrops/app/utils/ReadropsKeys.kt
index 57247d0a..01833caf 100644
--- a/app/src/main/java/com/readrops/app/utils/ReadropsKeys.kt
+++ b/app/src/main/java/com/readrops/app/utils/ReadropsKeys.kt
@@ -20,4 +20,6 @@ object ReadropsKeys {
     const val ACTION_BAR_COLOR = "ACTION_BAR_COLOR_KEY"
 
     const val FEEDS = "FEEDS"
+
+    const val STARRED_ITEM = "STARRED_ITEM"
 }
\ No newline at end of file
diff --git a/app/src/main/java/com/readrops/app/utils/SharedPreferencesManager.java b/app/src/main/java/com/readrops/app/utils/SharedPreferencesManager.java
index 0da4ee57..9d42e45b 100644
--- a/app/src/main/java/com/readrops/app/utils/SharedPreferencesManager.java
+++ b/app/src/main/java/com/readrops/app/utils/SharedPreferencesManager.java
@@ -1,19 +1,15 @@
 package com.readrops.app.utils;
 
-import android.content.Context;
 import android.content.SharedPreferences;
-import android.preference.PreferenceManager;
 
 import androidx.annotation.NonNull;
 
+import org.koin.java.KoinJavaComponent;
+
 public final class SharedPreferencesManager {
 
-    private static SharedPreferences getSharedPreferences(Context context) {
-        return PreferenceManager.getDefaultSharedPreferences(context);
-    }
-
-    public static void writeValue(Context context, String key, Object value) {
-        SharedPreferences sharedPref = getSharedPreferences(context);
+    public static void writeValue(String key, Object value) {
+        SharedPreferences sharedPref = KoinJavaComponent.get(SharedPreferences.class);
         SharedPreferences.Editor editor = sharedPref.edit();
 
         if (value instanceof Boolean)
@@ -24,32 +20,32 @@ public final class SharedPreferencesManager {
         editor.apply();
     }
 
-    public static void writeValue(Context context, SharedPrefKey sharedPrefKey, Object value) {
-        writeValue(context, sharedPrefKey.key, value);
+    public static void writeValue(SharedPrefKey sharedPrefKey, Object value) {
+        writeValue(sharedPrefKey.key, value);
     }
 
-    public static int readInt(Context context, SharedPrefKey sharedPrefKey) {
-        SharedPreferences sharedPreferences = getSharedPreferences(context);
+    public static int readInt(SharedPrefKey sharedPrefKey) {
+        SharedPreferences sharedPreferences = KoinJavaComponent.get(SharedPreferences.class);
         return sharedPreferences.getInt(sharedPrefKey.key, sharedPrefKey.getIntDefaultValue());
     }
 
-    public static boolean readBoolean(Context context, SharedPrefKey sharedPrefKey) {
-        SharedPreferences sharedPreferences = getSharedPreferences(context);
+    public static boolean readBoolean(SharedPrefKey sharedPrefKey) {
+        SharedPreferences sharedPreferences = KoinJavaComponent.get(SharedPreferences.class);
         return sharedPreferences.getBoolean(sharedPrefKey.key, sharedPrefKey.getBooleanDefaultValue());
     }
 
-    public static String readString(Context context, String key) {
-        SharedPreferences sharedPreferences = getSharedPreferences(context);
+    public static String readString(String key) {
+        SharedPreferences sharedPreferences = KoinJavaComponent.get(SharedPreferences.class);
         return sharedPreferences.getString(key, null);
     }
 
-    public static String readString(Context context, SharedPrefKey sharedPrefKey) {
-        SharedPreferences sharedPreferences = getSharedPreferences(context);
+    public static String readString(SharedPrefKey sharedPrefKey) {
+        SharedPreferences sharedPreferences = KoinJavaComponent.get(SharedPreferences.class);
         return sharedPreferences.getString(sharedPrefKey.key, sharedPrefKey.getStringDefaultValue());
     }
 
-    public static void remove(Context context, String key) {
-        SharedPreferences sharedPreferences = getSharedPreferences(context);
+    public static void remove(String key) {
+        SharedPreferences sharedPreferences = KoinJavaComponent.get(SharedPreferences.class);
         SharedPreferences.Editor editor = sharedPreferences.edit();
 
         editor.remove(key);
diff --git a/app/src/main/java/com/readrops/app/utils/Utils.java b/app/src/main/java/com/readrops/app/utils/Utils.java
index ea220f4e..1c3ecb28 100644
--- a/app/src/main/java/com/readrops/app/utils/Utils.java
+++ b/app/src/main/java/com/readrops/app/utils/Utils.java
@@ -15,9 +15,9 @@ import androidx.annotation.ColorInt;
 import androidx.annotation.NonNull;
 
 import com.google.android.material.snackbar.Snackbar;
-import com.readrops.api.utils.HttpManager;
+import com.readrops.api.utils.AuthInterceptor;
 
-import org.jsoup.Jsoup;
+import org.koin.java.KoinJavaComponent;
 
 import java.io.InputStream;
 import java.util.Locale;
@@ -36,10 +36,10 @@ public final class Utils {
 
     public static Bitmap getImageFromUrl(String url) {
         try {
-            OkHttpClient okHttpClient = HttpManager.getInstance().getOkHttpClient();
             Request request = new Request.Builder().url(url).build();
+            KoinJavaComponent.get(AuthInterceptor.class).setCredentials(null);
 
-            Response response = okHttpClient.newCall(request).execute();
+            Response response = KoinJavaComponent.get(OkHttpClient.class).newCall(request).execute();
 
             if (response.isSuccessful()) {
                 InputStream inputStream = response.body().byteStream();
@@ -97,16 +97,6 @@ public final class Utils {
         snackbar.show();
     }
 
-    /**
-     * Remove html tags and trim the text
-     *
-     * @param text string to clean
-     * @return cleaned text
-     */
-    public static String cleanText(String text) {
-        return Jsoup.parse(text).text().trim();
-    }
-
     public static Bitmap getBitmapFromDrawable(Drawable drawable) {
         Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
                 drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
diff --git a/app/src/main/java/com/readrops/app/utils/customviews/CustomExpandableBadgeDrawerItem.java b/app/src/main/java/com/readrops/app/utils/customviews/CustomExpandableBadgeDrawerItem.java
new file mode 100644
index 00000000..31277365
--- /dev/null
+++ b/app/src/main/java/com/readrops/app/utils/customviews/CustomExpandableBadgeDrawerItem.java
@@ -0,0 +1,174 @@
+package com.readrops.app.utils.customviews;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.LayoutRes;
+import androidx.annotation.StringRes;
+
+import com.mikepenz.fastadapter.IClickable;
+import com.mikepenz.fastadapter.IItem;
+import com.mikepenz.fastadapter.listeners.OnClickListener;
+import com.mikepenz.iconics.IconicsDrawable;
+import com.mikepenz.materialdrawer.Drawer;
+import com.mikepenz.materialdrawer.holder.BadgeStyle;
+import com.mikepenz.materialdrawer.holder.ColorHolder;
+import com.mikepenz.materialdrawer.holder.StringHolder;
+import com.mikepenz.materialdrawer.icons.MaterialDrawerFont;
+import com.mikepenz.materialdrawer.model.BaseDescribeableDrawerItem;
+import com.mikepenz.materialdrawer.model.BaseViewHolder;
+import com.mikepenz.materialdrawer.model.interfaces.ColorfulBadgeable;
+import com.readrops.app.R;
+
+import java.util.List;
+
+/**
+ * This a simple modification of original ExpandableBadgeDrawerItem from MaterialDrawer lib to get two click events from an expandable drawer item
+ */
+public class CustomExpandableBadgeDrawerItem extends BaseDescribeableDrawerItem<CustomExpandableBadgeDrawerItem,
+        CustomExpandableBadgeDrawerItem.ViewHolder>
+        implements ColorfulBadgeable<CustomExpandableBadgeDrawerItem>, IClickable {
+
+    protected ColorHolder arrowColor;
+
+    protected int arrowRotationAngleStart = 0;
+
+    protected int arrowRotationAngleEnd = 180;
+
+    protected StringHolder mBadge;
+    protected BadgeStyle mBadgeStyle = new BadgeStyle();
+
+    @Override
+    public int getType() {
+        return R.id.material_drawer_item_expandable_badge;
+    }
+
+    @Override
+    @LayoutRes
+    public int getLayoutRes() {
+        return R.layout.custom_expandable_drawer_item;
+    }
+
+    @Override
+    public void bindView(CustomExpandableBadgeDrawerItem.ViewHolder viewHolder, List payloads) {
+        super.bindView(viewHolder, payloads);
+
+        Context ctx = viewHolder.itemView.getContext();
+        //bind the basic view parts
+        bindViewHelper(viewHolder);
+
+        //set the text for the badge or hide
+        boolean badgeVisible = StringHolder.applyToOrHide(mBadge, viewHolder.badge);
+        //style the badge if it is visible
+        if (true) {
+            mBadgeStyle.style(viewHolder.badge, getTextColorStateList(getColor(ctx), getSelectedTextColor(ctx)));
+            viewHolder.badgeContainer.setVisibility(View.VISIBLE);
+        } else {
+            viewHolder.badgeContainer.setVisibility(View.GONE);
+        }
+
+        //define the typeface for our textViews
+        if (getTypeface() != null) {
+            viewHolder.badge.setTypeface(getTypeface());
+        }
+
+        //make sure all animations are stopped
+        if (viewHolder.arrow.getDrawable() instanceof IconicsDrawable) {
+            ((IconicsDrawable) viewHolder.arrow.getDrawable()).color(this.arrowColor != null ? this.arrowColor.color(ctx) : getIconColor(ctx));
+        }
+        viewHolder.arrow.clearAnimation();
+        if (!isExpanded()) {
+            viewHolder.arrow.setRotation(this.arrowRotationAngleStart);
+        } else {
+            viewHolder.arrow.setRotation(this.arrowRotationAngleEnd);
+        }
+
+        //call the onPostBindView method to trigger post bind view actions (like the listener to modify the item if required)
+        onPostBindView(this, viewHolder.itemView);
+    }
+
+    @Override
+    public CustomExpandableBadgeDrawerItem withOnDrawerItemClickListener(Drawer.OnDrawerItemClickListener onDrawerItemClickListener) {
+        mOnDrawerItemClickListener = null;
+        return this;
+    }
+
+    @Override
+    public Drawer.OnDrawerItemClickListener getOnDrawerItemClickListener() {
+        return null;
+    }
+
+    @Override
+    public CustomExpandableBadgeDrawerItem withBadge(StringHolder badge) {
+        this.mBadge = badge;
+        return this;
+    }
+
+    @Override
+    public CustomExpandableBadgeDrawerItem withBadge(String badge) {
+        this.mBadge = new StringHolder(badge);
+        return this;
+    }
+
+    @Override
+    public CustomExpandableBadgeDrawerItem withBadge(@StringRes int badgeRes) {
+        this.mBadge = new StringHolder(badgeRes);
+        return this;
+    }
+
+    @Override
+    public CustomExpandableBadgeDrawerItem withBadgeStyle(BadgeStyle badgeStyle) {
+        this.mBadgeStyle = badgeStyle;
+        return this;
+    }
+
+    public StringHolder getBadge() {
+        return mBadge;
+    }
+
+    public BadgeStyle getBadgeStyle() {
+        return mBadgeStyle;
+    }
+
+    @Override
+    public ViewHolder getViewHolder(View v) {
+        return new ViewHolder(v);
+    }
+
+    @Override
+    public IItem withOnItemPreClickListener(OnClickListener onItemPreClickListener) {
+        return null;
+    }
+
+    @Override
+    public OnClickListener getOnPreItemClickListener() {
+        return null;
+    }
+
+    @Override
+    public IItem withOnItemClickListener(OnClickListener onItemClickListener) {
+        return null;
+    }
+
+    @Override
+    public OnClickListener getOnItemClickListener() {
+        return null;
+    }
+
+    public static class ViewHolder extends BaseViewHolder {
+        public ImageView arrow;
+        public View badgeContainer;
+        public TextView badge;
+
+        public ViewHolder(View view) {
+            super(view);
+            badgeContainer = view.findViewById(R.id.material_drawer_badge_container);
+            badge = view.findViewById(R.id.material_drawer_badge);
+            arrow = view.findViewById(R.id.material_drawer_arrow);
+            arrow.setImageDrawable(new IconicsDrawable(view.getContext(), MaterialDrawerFont.Icon.mdf_expand_more).sizeDp(16).paddingDp(2).color(Color.BLACK));
+        }
+    }
+}
diff --git a/app/src/main/java/com/readrops/app/utils/EmptyListView.kt b/app/src/main/java/com/readrops/app/utils/customviews/EmptyListView.kt
similarity index 94%
rename from app/src/main/java/com/readrops/app/utils/EmptyListView.kt
rename to app/src/main/java/com/readrops/app/utils/customviews/EmptyListView.kt
index dadd5c20..c353c825 100644
--- a/app/src/main/java/com/readrops/app/utils/EmptyListView.kt
+++ b/app/src/main/java/com/readrops/app/utils/customviews/EmptyListView.kt
@@ -1,4 +1,4 @@
-package com.readrops.app.utils
+package com.readrops.app.utils.customviews
 
 import android.content.Context
 import android.util.AttributeSet
diff --git a/app/src/main/java/com/readrops/app/utils/ReadropsItemTouchCallback.kt b/app/src/main/java/com/readrops/app/utils/customviews/ReadropsItemTouchCallback.kt
similarity index 99%
rename from app/src/main/java/com/readrops/app/utils/ReadropsItemTouchCallback.kt
rename to app/src/main/java/com/readrops/app/utils/customviews/ReadropsItemTouchCallback.kt
index 07357416..8c61fb14 100644
--- a/app/src/main/java/com/readrops/app/utils/ReadropsItemTouchCallback.kt
+++ b/app/src/main/java/com/readrops/app/utils/customviews/ReadropsItemTouchCallback.kt
@@ -1,4 +1,4 @@
-package com.readrops.app.utils
+package com.readrops.app.utils.customviews
 
 import android.content.Context
 import android.graphics.Canvas
diff --git a/app/src/main/java/com/readrops/app/utils/ReadropsWebView.java b/app/src/main/java/com/readrops/app/utils/customviews/ReadropsWebView.java
similarity index 97%
rename from app/src/main/java/com/readrops/app/utils/ReadropsWebView.java
rename to app/src/main/java/com/readrops/app/utils/customviews/ReadropsWebView.java
index 554503d8..60d35341 100644
--- a/app/src/main/java/com/readrops/app/utils/ReadropsWebView.java
+++ b/app/src/main/java/com/readrops/app/utils/customviews/ReadropsWebView.java
@@ -1,4 +1,4 @@
-package com.readrops.app.utils;
+package com.readrops.app.utils.customviews;
 
 import android.annotation.SuppressLint;
 import android.content.Context;
@@ -12,6 +12,7 @@ import androidx.annotation.ColorInt;
 import androidx.annotation.Nullable;
 
 import com.readrops.app.R;
+import com.readrops.app.utils.Utils;
 import com.readrops.db.pojo.ItemWithFeed;
 
 import org.jsoup.Jsoup;
diff --git a/app/src/main/java/com/readrops/app/utils/feedscolors/FeedsColorsIntentService.kt b/app/src/main/java/com/readrops/app/utils/feedscolors/FeedsColorsIntentService.kt
index 880eb4c9..464b0603 100644
--- a/app/src/main/java/com/readrops/app/utils/feedscolors/FeedsColorsIntentService.kt
+++ b/app/src/main/java/com/readrops/app/utils/feedscolors/FeedsColorsIntentService.kt
@@ -6,15 +6,17 @@ import androidx.core.app.NotificationCompat
 import androidx.core.app.NotificationManagerCompat
 import com.readrops.app.R
 import com.readrops.app.ReadropsApp
+import com.readrops.app.utils.ReadropsKeys.FEEDS
 import com.readrops.db.Database
 import com.readrops.db.entities.Feed
-import com.readrops.app.utils.ReadropsKeys.FEEDS
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.get
 
-class FeedsColorsIntentService : IntentService("FeedsColorsIntentService") {
+class FeedsColorsIntentService : IntentService("FeedsColorsIntentService"), KoinComponent {
 
     override fun onHandleIntent(intent: Intent?) {
         val feeds: List<Feed> = intent!!.getParcelableArrayListExtra(FEEDS)!!
-        val database = Database.getInstance(this)
+        val database = get<Database>()
 
         val notificationBuilder = NotificationCompat.Builder(this, ReadropsApp.FEEDS_COLORS_CHANNEL_ID)
                 .setContentTitle(getString(R.string.get_feeds_colors))
diff --git a/app/src/main/java/com/readrops/app/utils/matchers/FeedMatcher.java b/app/src/main/java/com/readrops/app/utils/matchers/FeedMatcher.java
deleted file mode 100644
index bb07ae91..00000000
--- a/app/src/main/java/com/readrops/app/utils/matchers/FeedMatcher.java
+++ /dev/null
@@ -1,65 +0,0 @@
-package com.readrops.app.utils.matchers;
-
-import com.readrops.db.entities.Feed;
-import com.readrops.api.localfeed.atom.ATOMFeed;
-import com.readrops.api.localfeed.json.JSONFeed;
-import com.readrops.api.localfeed.rss.RSSChannel;
-import com.readrops.api.localfeed.rss.RSSFeed;
-
-import org.jsoup.Jsoup;
-
-public final class FeedMatcher {
-    
-    public static Feed feedFromRSS(RSSFeed rssFeed) {
-        Feed feed = new Feed();
-        RSSChannel channel = rssFeed.getChannel();
-
-        feed.setName(Jsoup.parse(channel.getTitle()).text());
-        feed.setUrl(channel.getFeedUrl());
-        feed.setSiteUrl(channel.getUrl());
-        feed.setDescription(channel.getDescription());
-        feed.setLastUpdated(channel.getLastUpdated());
-
-        feed.setEtag(rssFeed.getEtag());
-        feed.setLastModified(rssFeed.getLastModified());
-
-        feed.setFolderId(null);
-
-        return feed;
-    }
-
-    public static Feed feedFromATOM(ATOMFeed atomFeed) {
-        Feed feed = new Feed();
-
-        feed.setName(atomFeed.getTitle());
-        feed.setDescription(atomFeed.getSubtitle());
-        feed.setUrl(atomFeed.getUrl());
-        feed.setSiteUrl(atomFeed.getWebsiteUrl());
-        feed.setDescription(atomFeed.getSubtitle());
-        feed.setLastUpdated(atomFeed.getUpdated());
-
-        feed.setEtag(atomFeed.getEtag());
-        feed.setLastModified(atomFeed.getLastModified());
-
-        feed.setFolderId(null);
-
-        return feed;
-    }
-
-    public static Feed feedFromJSON(JSONFeed jsonFeed) {
-        Feed feed = new Feed();
-
-        feed.setName(jsonFeed.getTitle());
-        feed.setUrl(jsonFeed.getFeedUrl());
-        feed.setSiteUrl(jsonFeed.getHomePageUrl());
-        feed.setDescription(jsonFeed.getDescription());
-
-        feed.setEtag(jsonFeed.getEtag());
-        feed.setLastModified(jsonFeed.getLastModified());
-        feed.setIconUrl(jsonFeed.getFaviconUrl());
-
-        feed.setFolderId(null);
-
-        return feed;
-    }
-}
diff --git a/app/src/main/java/com/readrops/app/utils/matchers/ItemMatcher.java b/app/src/main/java/com/readrops/app/utils/matchers/ItemMatcher.java
deleted file mode 100644
index 096134c6..00000000
--- a/app/src/main/java/com/readrops/app/utils/matchers/ItemMatcher.java
+++ /dev/null
@@ -1,122 +0,0 @@
-package com.readrops.app.utils.matchers;
-
-import com.readrops.app.utils.DateUtils;
-import com.readrops.app.utils.Utils;
-import com.readrops.db.entities.Feed;
-import com.readrops.db.entities.Item;
-import com.readrops.api.localfeed.atom.ATOMEntry;
-import com.readrops.api.localfeed.json.JSONItem;
-import com.readrops.api.localfeed.rss.RSSEnclosure;
-import com.readrops.api.localfeed.rss.RSSItem;
-import com.readrops.api.localfeed.rss.RSSMediaContent;
-import com.readrops.api.utils.ParseException;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public final class ItemMatcher {
-
-    public static List<Item> itemsFromRSS(List<RSSItem> items, Feed feed) throws ParseException {
-        List<Item> dbItems = new ArrayList<>();
-
-        for (RSSItem item : items) {
-            Item newItem = new Item();
-
-            newItem.setAuthor(item.getAuthor());
-            newItem.setContent(item.getContent()); // Jsoup.clean(item.getContent(), Whitelist.relaxed())
-            newItem.setDescription(item.getDescription());
-            newItem.setGuid(item.getGuid() != null ? item.getGuid() : item.getLink());
-            newItem.setTitle(Utils.cleanText(item.getTitle()));
-
-            try {
-                newItem.setPubDate(DateUtils.stringToLocalDateTime(item.getDate()));
-            } catch (Exception e) {
-                throw new ParseException();
-            }
-
-            newItem.setLink(item.getLink());
-            newItem.setFeedId(feed.getId());
-
-            if (item.getMediaContents() != null && !item.getMediaContents().isEmpty()) {
-                for (RSSMediaContent mediaContent : item.getMediaContents()) {
-                    if (mediaContent.getMedium() != null && Utils.isTypeImage(mediaContent.getMedium())) {
-                        newItem.setImageLink(mediaContent.getUrl());
-                        break;
-                    }
-                }
-            } else {
-                if (item.getEnclosures() != null) {
-                    for (RSSEnclosure enclosure : item.getEnclosures()) {
-                        if (enclosure.getType() != null && Utils.isTypeImage(enclosure.getType())
-                                && enclosure.getUrl() != null) {
-                            newItem.setImageLink(enclosure.getUrl());
-                            break;
-                        }
-                    }
-
-                }
-            }
-
-            dbItems.add(newItem);
-        }
-
-        return dbItems;
-    }
-
-    public static List<Item> itemsFromATOM(List<ATOMEntry> items, Feed feed) throws ParseException {
-        List<Item> dbItems = new ArrayList<>();
-
-        for (ATOMEntry item : items) {
-            Item dbItem = new Item();
-
-            dbItem.setContent(item.getContent()); // Jsoup.clean(item.getContent(), Whitelist.relaxed())
-            dbItem.setDescription(item.getSummary());
-            dbItem.setGuid(item.getId());
-            dbItem.setTitle(Utils.cleanText(item.getTitle()));
-
-            try {
-                dbItem.setPubDate(DateUtils.stringToLocalDateTime(item.getUpdated()));
-            } catch (Exception e) {
-                throw new ParseException();
-            }
-
-            dbItem.setLink(item.getUrl());
-
-            dbItem.setFeedId(feed.getId());
-
-            dbItems.add(dbItem);
-        }
-
-        return dbItems;
-    }
-
-    public static List<Item> itemsFromJSON(List<JSONItem> items, Feed feed) throws ParseException {
-        List<Item> dbItems = new ArrayList<>();
-
-        for (JSONItem item : items) {
-            Item dbItem = new Item();
-
-            if (item.getAuthor() != null)
-                dbItem.setAuthor(item.getAuthor().getName());
-
-            dbItem.setContent(item.getContent()); // Jsoup.clean(item.getContent(), Whitelist.relaxed())
-            dbItem.setDescription(item.getSummary());
-            dbItem.setGuid(item.getId());
-            dbItem.setTitle(Utils.cleanText(item.getTitle()));
-
-            try {
-                dbItem.setPubDate(DateUtils.stringToLocalDateTime(item.getPubDate()));
-            } catch (Exception e) {
-                throw new ParseException();
-            }
-
-            dbItem.setLink(item.getUrl());
-
-            dbItem.setFeedId(feed.getId());
-
-            dbItems.add(dbItem);
-        }
-
-        return dbItems;
-    }
-}
diff --git a/app/src/main/java/com/readrops/app/viewmodels/ItemViewModel.java b/app/src/main/java/com/readrops/app/viewmodels/ItemViewModel.java
deleted file mode 100644
index a6243e99..00000000
--- a/app/src/main/java/com/readrops/app/viewmodels/ItemViewModel.java
+++ /dev/null
@@ -1,50 +0,0 @@
-package com.readrops.app.viewmodels;
-
-import android.app.Application;
-import android.graphics.Bitmap;
-import android.net.Uri;
-
-import androidx.annotation.NonNull;
-import androidx.core.content.FileProvider;
-import androidx.lifecycle.AndroidViewModel;
-import androidx.lifecycle.LiveData;
-
-import com.readrops.db.Database;
-import com.readrops.db.dao.ItemDao;
-import com.readrops.db.pojo.ItemWithFeed;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.OutputStream;
-
-public class ItemViewModel extends AndroidViewModel {
-
-    private ItemDao itemDao;
-
-    public ItemViewModel(@NonNull Application application) {
-        super(application);
-        itemDao = Database.getInstance(application).itemDao();
-    }
-
-    public LiveData<ItemWithFeed> getItemById(int id) {
-        return itemDao.getItemById(id);
-    }
-
-
-    public Uri saveImageInCache(Bitmap bitmap) throws IOException {
-        File imagesFolder = new File(getApplication().getCacheDir().getAbsolutePath(), "images");
-
-        if (!imagesFolder.exists())
-            imagesFolder.mkdirs();
-
-        File image = new File(imagesFolder, "shared_image.png");
-        OutputStream stream = new FileOutputStream(image);
-        bitmap.compress(Bitmap.CompressFormat.PNG, 90, stream);
-
-        stream.flush();
-        stream.close();
-
-        return FileProvider.getUriForFile(getApplication(), getApplication().getPackageName(), image);
-    }
-}
diff --git a/app/src/main/res/drawable/ic_empty_star.xml b/app/src/main/res/drawable/ic_empty_star.xml
new file mode 100644
index 00000000..26a48774
--- /dev/null
+++ b/app/src/main/res/drawable/ic_empty_star.xml
@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#727272"
+    android:viewportHeight="24" android:viewportWidth="24"
+    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="@android:color/white" android:pathData="M22,9.24l-7.19,-0.62L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21 12,17.27 18.18,21l-1.63,-7.03L22,9.24zM12,15.4l-3.76,2.27 1,-4.28 -3.32,-2.88 4.38,-0.38L12,6.1l1.71,4.04 4.38,0.38 -3.32,2.88 1,4.28L12,15.4z"/>
+</vector>
diff --git a/app/src/main/res/drawable/ic_read_later.xml b/app/src/main/res/drawable/ic_read_later.xml
index 3d68d402..c1c86f62 100644
--- a/app/src/main/res/drawable/ic_read_later.xml
+++ b/app/src/main/res/drawable/ic_read_later.xml
@@ -1,5 +1,5 @@
 <vector android:height="24dp" android:tint="#727272"
-    android:viewportHeight="24.0" android:viewportWidth="24.0"
+    android:viewportHeight="24" android:viewportWidth="24"
     android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
-    <path android:fillColor="#FF000000" android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z"/>
+    <path android:fillColor="@android:color/white" android:pathData="M15,1L9,1v2h6L15,1zM11,14h2L13,8h-2v6zM19.03,7.39l1.42,-1.42c-0.43,-0.51 -0.9,-0.99 -1.41,-1.41l-1.42,1.42C16.07,4.74 14.12,4 12,4c-4.97,0 -9,4.03 -9,9s4.02,9 9,9 9,-4.03 9,-9c0,-2.12 -0.74,-4.07 -1.97,-5.61zM12,20c-3.87,0 -7,-3.13 -7,-7s3.13,-7 7,-7 7,3.13 7,7 -3.13,7 -7,7z"/>
 </vector>
diff --git a/app/src/main/res/drawable/ic_star.xml b/app/src/main/res/drawable/ic_star.xml
new file mode 100644
index 00000000..c172fe62
--- /dev/null
+++ b/app/src/main/res/drawable/ic_star.xml
@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#727272"
+    android:viewportHeight="24" android:viewportWidth="24"
+    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="@android:color/white" android:pathData="M12,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21z"/>
+</vector>
diff --git a/app/src/main/res/layout/activity_account_type_list.xml b/app/src/main/res/layout/activity_account_type_list.xml
index 32d7fb03..8ce3e3a1 100644
--- a/app/src/main/res/layout/activity_account_type_list.xml
+++ b/app/src/main/res/layout/activity_account_type_list.xml
@@ -5,7 +5,7 @@
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    tools:context=".activities.AccountTypeListActivity">
+    tools:context=".account.AccountTypeListActivity">
 
     <androidx.constraintlayout.widget.ConstraintLayout
         android:id="@+id/account_type_list_root"
diff --git a/app/src/main/res/layout/activity_add_account.xml b/app/src/main/res/layout/activity_add_account.xml
index 7bc822e3..07cb8f83 100644
--- a/app/src/main/res/layout/activity_add_account.xml
+++ b/app/src/main/res/layout/activity_add_account.xml
@@ -5,7 +5,7 @@
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    tools:context=".activities.AddAccountActivity">
+    tools:context=".account.AddAccountActivity">
 
     <androidx.constraintlayout.widget.ConstraintLayout
         android:id="@+id/add_account_root"
diff --git a/app/src/main/res/layout/activity_add_feed.xml b/app/src/main/res/layout/activity_add_feed.xml
index 123511a4..86dc1823 100644
--- a/app/src/main/res/layout/activity_add_feed.xml
+++ b/app/src/main/res/layout/activity_add_feed.xml
@@ -6,7 +6,7 @@
     android:id="@+id/add_feed_root"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    tools:context=".activities.AddFeedActivity">
+    tools:context=".addfeed.AddFeedActivity">
 
     <androidx.constraintlayout.widget.ConstraintLayout
         android:layout_width="match_parent"
@@ -36,6 +36,7 @@
                 android:id="@+id/add_feed_input_layout"
                 android:layout_width="0dp"
                 android:layout_height="wrap_content"
+                app:endIconMode="clear_text"
                 app:layout_constraintEnd_toStartOf="@id/add_feed_load"
                 app:layout_constraintStart_toStartOf="parent"
                 app:layout_constraintTop_toTopOf="parent">
@@ -44,7 +45,6 @@
                     android:id="@+id/add_feed_text_input"
                     android:layout_width="match_parent"
                     android:layout_height="wrap_content"
-                    android:drawableEnd="@drawable/ic_cancel_grey"
                     android:hint="@string/feed_url"
                     android:inputType="text" />
 
diff --git a/app/src/main/res/layout/activity_item.xml b/app/src/main/res/layout/activity_item.xml
index d143394b..c7249ef3 100644
--- a/app/src/main/res/layout/activity_item.xml
+++ b/app/src/main/res/layout/activity_item.xml
@@ -6,7 +6,7 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:fitsSystemWindows="true"
-    tools:context=".activities.ItemActivity">
+    tools:context=".item.ItemActivity">
 
     <com.google.android.material.appbar.AppBarLayout
         android:id="@+id/app_bar_layout"
@@ -54,7 +54,8 @@
         android:focusable="true"
         android:src="@drawable/ic_open_in_browser_white"
         app:layout_anchor="@id/app_bar_layout"
-        app:layout_anchorGravity="bottom|right|end" />
+        app:layout_anchorGravity="bottom|end" />
+
 
     <androidx.core.widget.NestedScrollView
         android:layout_width="match_parent"
@@ -152,7 +153,7 @@
 
             </RelativeLayout>
 
-            <com.readrops.app.utils.ReadropsWebView
+            <com.readrops.app.utils.customviews.ReadropsWebView
                 android:id="@+id/item_webview"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
@@ -165,4 +166,14 @@
 
     </androidx.core.widget.NestedScrollView>
 
+    <com.google.android.material.floatingactionbutton.FloatingActionButton
+        android:id="@+id/item_star_fab"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="bottom|end"
+        android:layout_margin="16dp"
+        android:src="@drawable/ic_empty_star"
+        android:tint="@android:color/white"
+        app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior" />
+
 </androidx.coordinatorlayout.widget.CoordinatorLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 83f2fcd1..fb141ac6 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -5,7 +5,7 @@
     android:id="@+id/main_root"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    tools:context=".activities.MainActivity">
+    tools:context=".itemslist.MainActivity">
 
     <LinearLayout
         android:layout_width="match_parent"
@@ -85,7 +85,7 @@
         android:layout_height="match_parent"
         android:animateLayoutChanges="true">
 
-        <com.readrops.app.utils.EmptyListView
+        <com.readrops.app.utils.customviews.EmptyListView
             android:id="@+id/empty_list_layout"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
diff --git a/app/src/main/res/layout/activity_manage_feeds_folders.xml b/app/src/main/res/layout/activity_manage_feeds_folders.xml
index bafba0a0..2e510b02 100644
--- a/app/src/main/res/layout/activity_manage_feeds_folders.xml
+++ b/app/src/main/res/layout/activity_manage_feeds_folders.xml
@@ -4,7 +4,7 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
-    tools:context=".activities.ManageFeedsFoldersActivity"
+    tools:context=".feedsfolders.ManageFeedsFoldersActivity"
     android:id="@+id/manage_feeds_folders_root"
     android:layout_width="match_parent"
     android:layout_height="match_parent">
diff --git a/app/src/main/res/layout/activity_notification_permission.xml b/app/src/main/res/layout/activity_notification_permission.xml
index b8a52082..fa78aa28 100644
--- a/app/src/main/res/layout/activity_notification_permission.xml
+++ b/app/src/main/res/layout/activity_notification_permission.xml
@@ -5,7 +5,7 @@
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    tools:context="com.readrops.app.activities.NotificationPermissionActivity">
+    tools:context="com.readrops.app.notifications.NotificationPermissionActivity">
 
     <androidx.constraintlayout.widget.ConstraintLayout
         android:layout_width="match_parent"
diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml
index 1e65e830..94e921a9 100644
--- a/app/src/main/res/layout/activity_settings.xml
+++ b/app/src/main/res/layout/activity_settings.xml
@@ -4,7 +4,7 @@
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    tools:context=".activities.SettingsActivity">
+    tools:context=".settings.SettingsActivity">
 
     <FrameLayout
         android:id="@+id/settings_activity_fragment"
diff --git a/app/src/main/res/layout/activity_splash.xml b/app/src/main/res/layout/activity_splash.xml
index c9c03a64..5825a84a 100644
--- a/app/src/main/res/layout/activity_splash.xml
+++ b/app/src/main/res/layout/activity_splash.xml
@@ -4,6 +4,6 @@
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    tools:context=".activities.SplashActivity">
+    tools:context=".SplashActivity">
 
 </androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_web_view.xml b/app/src/main/res/layout/activity_web_view.xml
index 0c79eee6..2854e005 100644
--- a/app/src/main/res/layout/activity_web_view.xml
+++ b/app/src/main/res/layout/activity_web_view.xml
@@ -2,7 +2,7 @@
 <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
-    tools:context="com.readrops.app.activities.WebViewActivity"
+    tools:context="com.readrops.app.item.WebViewActivity"
     android:layout_width="match_parent"
     android:layout_height="match_parent">
 
diff --git a/app/src/main/res/layout/custom_expandable_drawer_item.xml b/app/src/main/res/layout/custom_expandable_drawer_item.xml
new file mode 100644
index 00000000..1139334e
--- /dev/null
+++ b/app/src/main/res/layout/custom_expandable_drawer_item.xml
@@ -0,0 +1,109 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/expandable_item_container"
+    android:layout_width="match_parent"
+    android:layout_height="@dimen/material_drawer_item_primary"
+    android:clickable="true"
+    android:orientation="horizontal"
+    android:paddingStart="@dimen/material_drawer_vertical_padding"
+    android:paddingLeft="@dimen/material_drawer_vertical_padding"
+    android:paddingEnd="@dimen/material_drawer_vertical_padding"
+    android:paddingRight="@dimen/material_drawer_vertical_padding">
+
+    <androidx.appcompat.widget.AppCompatImageView
+        android:id="@+id/material_drawer_icon"
+        android:layout_width="@dimen/material_drawer_item_primary_icon"
+        android:layout_height="@dimen/material_drawer_item_primary"
+        android:layout_gravity="center_vertical"
+        android:paddingStart="0dp"
+        android:paddingLeft="0dp"
+        android:paddingTop="@dimen/material_drawer_item_primary_icon_padding"
+        android:paddingEnd="@dimen/material_drawer_item_primary_icon_padding_right"
+        android:paddingRight="@dimen/material_drawer_item_primary_icon_padding_right"
+        android:paddingBottom="@dimen/material_drawer_item_primary_icon_padding"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/material_drawer_name"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:fontFamily="sans-serif-medium"
+        android:gravity="center_vertical|start"
+        android:lines="1"
+        android:singleLine="true"
+        android:textDirection="anyRtl"
+        android:textSize="@dimen/material_drawer_item_primary_text"
+        app:layout_constraintBottom_toTopOf="@id/material_drawer_description"
+        app:layout_constraintEnd_toStartOf="@+id/material_drawer_badge_container"
+        app:layout_constraintStart_toEndOf="@id/material_drawer_icon"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintVertical_chainStyle="packed"
+        tools:text="Some drawer text" />
+
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/material_drawer_description"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:fontFamily="sans-serif"
+        android:gravity="center_vertical|start"
+        android:lines="1"
+        android:singleLine="true"
+        android:textDirection="anyRtl"
+        android:textSize="@dimen/material_drawer_item_primary_description"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toStartOf="@+id/material_drawer_badge_container"
+        app:layout_constraintStart_toEndOf="@id/material_drawer_icon"
+        app:layout_constraintTop_toBottomOf="@id/material_drawer_name"
+        tools:text="Some drawer text" />
+
+    <LinearLayout
+        android:id="@+id/material_drawer_badge_container"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:gravity="center"
+        android:paddingStart="@dimen/material_drawer_padding"
+        android:paddingLeft="@dimen/material_drawer_padding"
+        android:paddingEnd="0dp"
+        android:paddingRight="0dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toStartOf="@id/material_drawer_arrow_container"
+        app:layout_constraintTop_toTopOf="parent">
+
+        <androidx.appcompat.widget.AppCompatTextView
+            android:id="@+id/material_drawer_badge"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:fontFamily="sans-serif"
+            android:gravity="center"
+            android:lines="1"
+            android:minWidth="20dp"
+            android:paddingLeft="1dp"
+            android:paddingRight="1dp"
+            android:singleLine="true"
+            android:textSize="@dimen/material_drawer_item_primary_text"
+            tools:text="99" />
+    </LinearLayout>
+
+    <LinearLayout
+        android:id="@+id/material_drawer_arrow_container"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:gravity="center"
+        android:paddingStart="@dimen/material_drawer_padding"
+        android:paddingLeft="@dimen/material_drawer_padding"
+        android:paddingEnd="0dp"
+        android:paddingRight="0dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toTopOf="parent">
+
+        <androidx.appcompat.widget.AppCompatImageView
+            android:id="@+id/material_drawer_arrow"
+            android:layout_width="16dp"
+            android:layout_height="16dp" />
+    </LinearLayout>
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_feeds.xml b/app/src/main/res/layout/fragment_feeds.xml
index 487b0244..3358c261 100644
--- a/app/src/main/res/layout/fragment_feeds.xml
+++ b/app/src/main/res/layout/fragment_feeds.xml
@@ -6,7 +6,7 @@
     android:id="@+id/feeds_root"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    tools:context=".fragments.FeedsFragment">
+    tools:context=".feedsfolders.feeds.FeedsFragment">
 
     <androidx.recyclerview.widget.RecyclerView
         android:id="@+id/feeds_recyclerview"
@@ -15,7 +15,7 @@
         tools:itemCount="5"
         tools:listitem="@layout/feed_layout" />
 
-    <com.readrops.app.utils.EmptyListView
+    <com.readrops.app.utils.customviews.EmptyListView
         android:id="@+id/feeds_empty_list"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
diff --git a/app/src/main/res/layout/fragment_folders.xml b/app/src/main/res/layout/fragment_folders.xml
index be958bd3..f92d9078 100644
--- a/app/src/main/res/layout/fragment_folders.xml
+++ b/app/src/main/res/layout/fragment_folders.xml
@@ -6,7 +6,7 @@
     android:id="@+id/folders_root"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    tools:context=".fragments.FoldersFragment">
+    tools:context=".feedsfolders.folders.FoldersFragment">
 
     <androidx.recyclerview.widget.RecyclerView
         android:id="@+id/folders_list"
@@ -15,7 +15,7 @@
         tools:itemCount="5"
         tools:listitem="@layout/folder_layout" />
 
-    <com.readrops.app.utils.EmptyListView
+    <com.readrops.app.utils.customviews.EmptyListView
         android:id="@+id/folders_empty_list"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index fe4f52b0..c370ce6a 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -132,5 +132,7 @@
     <string name="show_caption">Afficher la légende</string>
     <string name="password_helper">Votre mot de passe d\'API (Configuration > Profil)</string>
     <string name="synchronize">Synchroniser</string>
+    <string name="navigator_view">Vue navigateur</string>
+    <string name="favorites">Favoris</string>
 
 </resources>
\ No newline at end of file
diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml
new file mode 100644
index 00000000..4e9a3b09
--- /dev/null
+++ b/app/src/main/res/values-in/strings.xml
@@ -0,0 +1,143 @@
+<resources>
+    <string name="app_name" translatable="false">Readrops</string>
+
+    <string name="to_read">To read</string>
+    <string name="non_read_articles">Non read articles</string>
+    <string name="open_nav_drawer">Buka menu</string>
+    <string name="close_nav_drawer">Tutup menu</string>
+    <string name="add_feed_item">Tambah feed</string>
+    <string name="add_folder_item">Tambah folder</string>
+    <string name="settings">Pengaturan</string>
+    <string name="about">Tentang</string>
+    <string name="add_feed_title">Tambah feed</string>
+    <string name="feed_url">Url feed</string>
+    <string name="validate">Validasi</string>
+    <string name="empty_field">Ruas tidak boleh kosong</string>
+    <string name="wrong_url">URL tidak valid</string>
+    <string name="add_feed_no_result">Url feed tidak ditemukan</string>
+    <string name="add_feed_connexion_error">Galat koneksi</string>
+    <string name="add_feed_unknownhost_error">Hos tidak diketahui</string>
+    <string name="by_author">oleh %1$s</string>
+    <string name="read_time">%1$s mnt</string>
+    <string name="read_time_lower_than_1">Kurang dari 1 menit</string>
+    <string name="read_time_one_minute">1 mnt</string>
+    <string name="interpoint" translatable="false">·</string>
+    <string name="share_article">Bagikan Artikel</string>
+    <string name="open_url">Buka url</string>
+    <string name="add_folder">Tambah folder</string>
+    <string name="feed_folder">Folder feed</string>
+    <string name="feed_name">Nama feed</string>
+    <string name="edit_feed">Sunting feed</string>
+    <string name="folder">Folder</string>
+    <string name="no_folder">Tidak ada folder</string>
+    <string name="cancel">Batal</string>
+    <string name="delete_feed">Hapus feed ?</string>
+    <string name="load">Muat</string>
+    <string name="updating_feed">Memperbarui feed : %1$s</string>
+    <string name="results">Hasil</string>
+    <string name="feed_insertion_successfull">Feed %1$s berhasil ditambahkan</string>
+    <string name="feed_insertion_network_failed">Jaringan terputus saat mengakses feed %1$s</string>
+    <string name="feed_insertion_parse_failed">Terjadi kesalahan saat mengurai feed %1$s</string>
+    <string name="feed_insertion_wrong_format">Kesalahan format untuk feed %1$s</string>
+    <string name="feed_insertion_unknown_error">Kesalahan tidak diketahui untuk feed %1$s</string>
+    <string name="articles">Artikel</string>
+    <string name="read_later">Baca nanti</string>
+    <string name="show_read_articles">Tampilkan artikel dibaca</string>
+    <string name="filter">Filter</string>
+    <string name="unread">Tandai belum dibaca</string>
+    <string name="read">Tandai dibaca</string>
+    <string name="select_all">Pilih semua</string>
+    <string name="account_url">Url akun</string>
+    <string name="account_name">Nama akun</string>
+    <string name="login">Masuk</string>
+    <string name="password">Sandi</string>
+    <string name="password_helper">Ini adalah sandi API FreshRSS Anda (Konfigurasi > Profil)</string>
+    <string name="account_settings">Pengaturan akun</string>
+    <string name="add_account">Tambah akun</string>
+    <string name="no_feed">Tidak ada feed</string>
+    <string name="choose_account">Pilih akun</string>
+    <string name="feeds_and_folders">Feed dan folder</string>
+    <string name="account">Akun</string>
+    <string name="manage_feeds_folders">Kelola feed dan folder</string>
+    <string name="folders">Folder</string>
+    <string name="feeds">Feed</string>
+    <string name="edit_folder">Sunting folder</string>
+    <string name="delete_folder">Hapus folder ?</string>
+    <string name="delete_account">Hapus akun</string>
+    <string name="delete_account_question">Hapus akun ?</string>
+    <string name="feed_deleted">Feed %1$s telah dihapus</string>
+    <string name="feed_doesnt_exist">Feed %1$s tidak ada pada server</string>
+    <string name="error_occured">Telah terjadi keslahan</string>
+    <string name="folder_already_exists">Folder sudah ada</string>
+    <string name="folder_bad_format">Format tidak valid untuk folder baru</string>
+    <string name="folder_doesnt_exist">Folder tidak ada pada server</string>
+
+    <string name="credentials">Kredensial</string>
+    <string name="filter_newest">Terbaru > terlama</string>
+    <string name="filter_oldest">Terlama > terbaru</string>
+    <string name="login_failed">Gagal masuk. Silakan periksa kredensial Anda</string>
+    <string name="new_account">Akun baru</string>
+    <string name="app_licence">Aplikasi dirilis dengan lisensi GPLv3</string>
+    <string name="app_url" translatable="false">https://github.com/readrops/Readrops</string>
+    <string name="number_items_to_parse">Jumlah maksimum item per feed</string>
+    <string name="unlimited">Tidak terbatas</string>
+    <string name="local">Lokal</string>
+    <string name="feeds_number">%1$s feed</string>
+    <string name="feed_number">%1$s feed</string>
+    <string name="delete">Hapus</string>
+    <string name="app_description" translatable="false"><![CDATA[%1$s <br/><br/> %2$s]]></string>
+    <string name="no_item">Tidak ada item</string>
+    <string name="no_feed_found">Feed tidak ditemukan</string>
+    <string name="feed_insertion_error">Galat feed %1$s</string>
+    <string name="get_feeds_colors">Ambil warna feed</string>
+    <string name="feeds_colors">Warna Feed</string>
+    <string name="global">Global</string>
+    <string name="reload_feeds_colors">Muat ulang warna feed</string>
+    <string name="open_items_in">Buka item di</string>
+    <string name="webview">Webview</string>
+    <string name="external_navigator">Peramban eksternal</string>
+    <string name="actualize">Aktualisasikan</string>
+    <string name="share_url">Bagikan url</string>
+    <string name="opml_import_export">Impor/Ekspor OPML</string>
+    <string name="opml_processing">Memproses berkas OPML</string>
+    <string name="operation_takes_time">Proses ini bisa memakan waktu lama karena feed harus diproses satu persatu.</string>
+    <string name="processing_file_failed">Telah terjadi kesalahan saat memproses berkas</string>
+    <string name="opml_import">Impor OPML</string>
+    <string name="opml_export">Ekspor OPML</string>
+    <string name="subscriptions" translatable="false">Subscriptions</string>
+    <string name="external_storage_opml_export">Ekspor berkas OPML membutuhkan izin akses penyimpanan eksternal</string>
+    <string name="try_again">Coba lagi</string>
+    <string name="permissions">Perizinan</string>
+    <string name="or">Atau</string>
+    <string name="image_options">Opsi Gambar</string>
+    <string name="download_image">Unduh gambar</string>
+    <string name="share_image">Bagikan gambar</string>
+    <string name="theme">Tema</string>
+    <string name="light">Terang</string>
+    <string name="dark">Gelap</string>
+    <string name="opml_export_description">Ekspor feed dan folder</string>
+    <string name="new_feed">Feed baru</string>
+    <string name="download_image_permission">Untuk mengunduh gambar, dibutuhkan izin akses penyimpanan</string>
+    <string name="auto_synchro">Sinkronisasi otomatis</string>
+    <string name="manual">Manual</string>
+    <string name="min_30">30 mnt</string>
+    <string name="hour_1">1 jam</string>
+    <string name="hour_2">2 jam</string>
+    <string name="hour_3">3 jam</string>
+    <string name="hour_6">6 jam</string>
+    <string name="hour_12">12 jam</string>
+    <string name="every_day">Sehari sekali</string>
+    <string name="account_synchro">Sinkronisasi akun</string>
+    <string name="new_items">%1$s artikel baru</string>
+    <string name="enable_notifications">Aktifkan notifikasi</string>
+    <string name="notifications">Notifikasi</string>
+    <string name="enable_all_feeds">Aktifkan semua notifikasi feed</string>
+    <string name="auto_synchro_disabled">Sinkronisasi otomatis dinonaktifkan</string>
+    <string name="enable_auto_synchro_text">Agar bisa ditampilkan, notifikasi membutuhkan sinkronisasi otomatis untuk diaktifkan.\nApakah Anda ingin membuka pengaturan ?</string>
+    <string name="open">Buka</string>
+    <string name="back">Kembali</string>
+    <string name="show_caption">Tampilkan takarir</string>
+    <string name="synchronize">Sinkronkan</string>
+    <string name="navigator_view">Navigator view</string>
+    <string name="favorites">Favorit</string>
+</resources>
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
index 824cd35c..a603ebf8 100644
--- a/app/src/main/res/values/arrays.xml
+++ b/app/src/main/res/values/arrays.xml
@@ -33,11 +33,13 @@
     <string-array name="open_items_in">
         <item>@string/external_navigator</item>
         <item>@string/webview</item>
+        <item>@string/navigator_view</item>
     </string-array>
 
     <string-array name="open_item_in_values">
         <item>0</item>
         <item>1</item>
+        <item>2</item>
     </string-array>
 
     <string-array name="themes">
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 077eaec1..eac89635 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -74,7 +74,7 @@
 
     <string name="credentials">Credentials</string>
     <string name="filter_newest">Newest to oldest</string>
-    <string name="filter_oldest">Oldest to newsest</string>
+    <string name="filter_oldest">Oldest to newest</string>
     <string name="login_failed">Login failed. Please check your credentials</string>
     <string name="new_account">New account</string>
     <string name="app_licence">App released under the GPLv3 licence</string>
@@ -138,4 +138,6 @@
     <string name="back">Back</string>
     <string name="show_caption">Show caption</string>
     <string name="synchronize">Synchronize</string>
+    <string name="navigator_view">Navigator view</string>
+    <string name="favorites">Favorites</string>
 </resources>
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml
index 9a14e305..625fc43a 100644
--- a/app/src/main/res/xml/preferences.xml
+++ b/app/src/main/res/xml/preferences.xml
@@ -16,7 +16,7 @@
             android:title="@string/reload_feeds_colors" />
 
         <ListPreference
-            android:defaultValue="0"
+            android:defaultValue="2"
             android:entries="@array/open_items_in"
             android:entryValues="@array/open_item_in_values"
             android:key="open_items_in"
diff --git a/app/src/test/java/com/readrops/app/HtmlParserTest.java b/app/src/test/java/com/readrops/app/HtmlParserTest.java
deleted file mode 100644
index bf8f6e47..00000000
--- a/app/src/test/java/com/readrops/app/HtmlParserTest.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package com.readrops.app;
-
-import com.readrops.app.utils.HtmlParser;
-import com.readrops.app.utils.ParsingResult;
-
-import org.junit.Assert;
-import org.junit.Test;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import static junit.framework.TestCase.assertEquals;
-
-public class HtmlParserTest {
-
-    @Test
-    public void getFeedLinkTest() throws Exception {
-        String url = "https://github.com/readrops/Readrops";
-
-        ParsingResult parsingResult = new ParsingResult("https://github.com/readrops/Readrops/commits/develop.atom", "Recent Commits to Readrops:develop");
-        List<ParsingResult> parsingResultList = new ArrayList<>();
-        parsingResultList.add(parsingResult);
-
-        List<ParsingResult> parsingResultList1 = HtmlParser.getFeedLink(url);
-
-        Assert.assertEquals(parsingResultList, parsingResultList1);
-    }
-
-    @Test
-    public void getFaviconLinkTest() {
-        String url = "https://github.com/readrops/Readrops";
-
-        assertEquals("https://github.com/fluidicon.png", HtmlParser.getFaviconLink(url));
-    }
-}
diff --git a/app/src/test/java/com/readrops/app/HtmlParserTest.kt b/app/src/test/java/com/readrops/app/HtmlParserTest.kt
new file mode 100644
index 00000000..32c766ea
--- /dev/null
+++ b/app/src/test/java/com/readrops/app/HtmlParserTest.kt
@@ -0,0 +1,44 @@
+package com.readrops.app
+
+import com.readrops.api.utils.AuthInterceptor
+import com.readrops.app.utils.HtmlParser
+import com.readrops.app.addfeed.ParsingResult
+import junit.framework.TestCase
+import okhttp3.OkHttpClient
+import org.junit.Assert
+import org.junit.Rule
+import org.junit.Test
+import org.koin.dsl.module
+import org.koin.test.KoinTestRule
+
+class HtmlParserTest {
+
+    @get:Rule
+    val koinTestRule = KoinTestRule.create {
+        modules(module {
+            single { OkHttpClient.Builder().addInterceptor(get<AuthInterceptor>()).build() }
+            single { AuthInterceptor() }
+        })
+    }
+
+/*
+    @Test
+    fun getFeedLinkTest() {
+        val url = "https://github.com/readrops/Readrops"
+        val parsingResult = ParsingResult("https://github.com/readrops/Readrops/commits/develop.atom",
+                "Recent Commits to Readrops:develop")
+
+        val parsingResultList = mutableListOf(parsingResult)
+
+        val parsingResultList1 = HtmlParser.getFeedLink(url)
+        Assert.assertEquals(parsingResultList, parsingResultList1)
+    }
+*/
+
+    @Test
+    fun getFaviconLinkTest() {
+        val url = "https://github.com/readrops/Readrops"
+
+        TestCase.assertEquals("https://github.com/fluidicon.png", HtmlParser.getFaviconLink(url))
+    }
+}
\ No newline at end of file
diff --git a/app/src/test/java/com/readrops/app/UtilsTest.java b/app/src/test/java/com/readrops/app/UtilsTest.java
index b15e86a8..f9c4eb22 100644
--- a/app/src/test/java/com/readrops/app/UtilsTest.java
+++ b/app/src/test/java/com/readrops/app/UtilsTest.java
@@ -7,17 +7,9 @@ import com.readrops.app.utils.Utils;
 import org.junit.Test;
 
 import static junit.framework.Assert.assertTrue;
-import static junit.framework.TestCase.assertEquals;
 
 public class UtilsTest {
 
-    @Test
-    public void cleanTextTest() {
-        String text = "    <p>This is a text<br/>to</p> clean    ";
-
-        assertEquals("This is a text to clean", Utils.cleanText(text));
-    }
-
     @Test
     public void colorTooBrightTest() {
         assertTrue(Utils.isColorTooBright(-986896));
diff --git a/build.gradle b/build.gradle
index 00210d22..70d196b9 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,14 +1,14 @@
 // Top-level build file where you can add configuration options common to all sub-projects/modules.
 
 buildscript {
-    ext.kotlin_version = '1.3.72'
+    ext.kotlin_version = '1.4.32'
 
     repositories {
         google()
-        jcenter()
+        mavenCentral()
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:4.0.1'
+        classpath 'com.android.tools.build:gradle:4.2.2'
         classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
     }
 }
@@ -16,8 +16,8 @@ buildscript {
 allprojects {
     repositories {
         google()
-        jcenter()
         mavenCentral()
+        maven { url 'https://jitpack.io' }
     }
     afterEvaluate {
         tasks.withType(JavaCompile.class) {
@@ -27,10 +27,10 @@ allprojects {
 }
 
 ext {
-    compileSdkVersion = 29
+    compileSdkVersion = 30
     minSdkVersion = 21
-    targetSdkVersion = 29
-    buildToolsVersion = "29.0.3"
+    targetSdkVersion = 30
+    buildToolsVersion = "30.0.3"
 }
 
 task clean(type: Delete) {
diff --git a/db/build.gradle b/db/build.gradle
index 1cb655c4..f6d28aac 100644
--- a/db/build.gradle
+++ b/db/build.gradle
@@ -1,6 +1,5 @@
 apply plugin: 'com.android.library'
 apply plugin: 'kotlin-android'
-apply plugin: 'kotlin-android-extensions'
 apply plugin: 'kotlin-kapt'
 
 android {
@@ -48,25 +47,40 @@ android {
         sourceCompatibility JavaVersion.VERSION_1_8
         targetCompatibility JavaVersion.VERSION_1_8
     }
+    kotlinOptions {
+        jvmTarget = '1.8'
+    }
 }
 
 dependencies {
     implementation fileTree(dir: 'libs', include: ['*.jar'])
 
-    api 'androidx.appcompat:appcompat:1.2.0-rc02'
+    api "androidx.core:core-ktx:1.6.0"
+    api 'androidx.appcompat:appcompat:1.3.0'
+    api "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
 
-    testImplementation 'junit:junit:4.12'
-    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
-    androidTestImplementation 'androidx.test:runner:1.2.0'
-    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
+    testImplementation 'junit:junit:4.13'
+    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
+    androidTestImplementation 'androidx.test:runner:1.4.0'
+    androidTestImplementation 'androidx.test:rules:1.4.0'
+    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
 
-    api 'androidx.room:room-runtime:2.2.5'
-    kapt 'androidx.room:room-compiler:2.2.5'
-    implementation 'androidx.room:room-rxjava2:2.2.5'
-    androidTestImplementation "androidx.room:room-testing:2.2.5"
+    def room_version = "2.3.0"
+    api "androidx.room:room-runtime:$room_version"
+    kapt "androidx.room:room-compiler:$room_version"
+    implementation "androidx.room:room-rxjava2:$room_version"
+    androidTestImplementation "androidx.room:room-testing:$room_version"
+
+    implementation 'com.github.MatrixDev.Roomigrant:RoomigrantLib:0.3.4'
+    kapt 'com.github.MatrixDev.Roomigrant:RoomigrantCompiler:0.3.4'
 
     api 'androidx.paging:paging-runtime:2.1.2'
     api 'androidx.paging:paging-common:2.1.2'
 
-    api 'joda-time:joda-time:2.10.5'
+    api 'joda-time:joda-time:2.10.10'
+
+    def koin_version = "2.2.3"
+    api "io.insert-koin:koin-android:$koin_version"
+    api "io.insert-koin:koin-androidx-scope:$koin_version"
+    api "io.insert-koin:koin-androidx-viewmodel:$koin_version"
 }
diff --git a/db/schemas/com.readrops.db.Database/3.json b/db/schemas/com.readrops.db.Database/3.json
new file mode 100644
index 00000000..464e5f8b
--- /dev/null
+++ b/db/schemas/com.readrops.db.Database/3.json
@@ -0,0 +1,537 @@
+{
+  "formatVersion": 1,
+  "database": {
+    "version": 3,
+    "identityHash": "3c36644243041ad4676b32310d961ca9",
+    "entities": [
+      {
+        "tableName": "Feed",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `description` TEXT, `url` TEXT, `siteUrl` TEXT, `lastUpdated` TEXT, `text_color` INTEGER NOT NULL, `background_color` INTEGER NOT NULL, `icon_url` TEXT, `etag` TEXT, `last_modified` TEXT, `folder_id` INTEGER, `remoteId` TEXT, `account_id` INTEGER NOT NULL, `notification_enabled` INTEGER NOT NULL DEFAULT 1, FOREIGN KEY(`folder_id`) REFERENCES `Folder`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`account_id`) REFERENCES `Account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "name",
+            "columnName": "name",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "description",
+            "columnName": "description",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "url",
+            "columnName": "url",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "siteUrl",
+            "columnName": "siteUrl",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "lastUpdated",
+            "columnName": "lastUpdated",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "textColor",
+            "columnName": "text_color",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "backgroundColor",
+            "columnName": "background_color",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "iconUrl",
+            "columnName": "icon_url",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "etag",
+            "columnName": "etag",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "lastModified",
+            "columnName": "last_modified",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "folderId",
+            "columnName": "folder_id",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "remoteId",
+            "columnName": "remoteId",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "accountId",
+            "columnName": "account_id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "notificationEnabled",
+            "columnName": "notification_enabled",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "1"
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": true
+        },
+        "indices": [
+          {
+            "name": "index_Feed_folder_id",
+            "unique": false,
+            "columnNames": [
+              "folder_id"
+            ],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_Feed_folder_id` ON `${TABLE_NAME}` (`folder_id`)"
+          },
+          {
+            "name": "index_Feed_account_id",
+            "unique": false,
+            "columnNames": [
+              "account_id"
+            ],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_Feed_account_id` ON `${TABLE_NAME}` (`account_id`)"
+          }
+        ],
+        "foreignKeys": [
+          {
+            "table": "Folder",
+            "onDelete": "SET NULL",
+            "onUpdate": "NO ACTION",
+            "columns": [
+              "folder_id"
+            ],
+            "referencedColumns": [
+              "id"
+            ]
+          },
+          {
+            "table": "Account",
+            "onDelete": "CASCADE",
+            "onUpdate": "NO ACTION",
+            "columns": [
+              "account_id"
+            ],
+            "referencedColumns": [
+              "id"
+            ]
+          }
+        ]
+      },
+      {
+        "tableName": "Item",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT, `description` TEXT, `clean_description` TEXT, `link` TEXT, `image_link` TEXT, `author` TEXT, `pub_date` INTEGER, `content` TEXT, `feed_id` INTEGER NOT NULL, `guid` TEXT, `read_time` REAL NOT NULL, `read` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `read_it_later` INTEGER NOT NULL, `remoteId` TEXT, FOREIGN KEY(`feed_id`) REFERENCES `Feed`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "title",
+            "columnName": "title",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "description",
+            "columnName": "description",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "cleanDescription",
+            "columnName": "clean_description",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "link",
+            "columnName": "link",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "imageLink",
+            "columnName": "image_link",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "author",
+            "columnName": "author",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "pubDate",
+            "columnName": "pub_date",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "content",
+            "columnName": "content",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "feedId",
+            "columnName": "feed_id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "guid",
+            "columnName": "guid",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "readTime",
+            "columnName": "read_time",
+            "affinity": "REAL",
+            "notNull": true
+          },
+          {
+            "fieldPath": "read",
+            "columnName": "read",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "starred",
+            "columnName": "starred",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "readItLater",
+            "columnName": "read_it_later",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "remoteId",
+            "columnName": "remoteId",
+            "affinity": "TEXT",
+            "notNull": false
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": true
+        },
+        "indices": [
+          {
+            "name": "index_Item_feed_id",
+            "unique": false,
+            "columnNames": [
+              "feed_id"
+            ],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_Item_feed_id` ON `${TABLE_NAME}` (`feed_id`)"
+          },
+          {
+            "name": "index_Item_guid",
+            "unique": false,
+            "columnNames": [
+              "guid"
+            ],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_Item_guid` ON `${TABLE_NAME}` (`guid`)"
+          }
+        ],
+        "foreignKeys": [
+          {
+            "table": "Feed",
+            "onDelete": "CASCADE",
+            "onUpdate": "NO ACTION",
+            "columns": [
+              "feed_id"
+            ],
+            "referencedColumns": [
+              "id"
+            ]
+          }
+        ]
+      },
+      {
+        "tableName": "Folder",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `remoteId` TEXT, `account_id` INTEGER NOT NULL, FOREIGN KEY(`account_id`) REFERENCES `Account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "name",
+            "columnName": "name",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "remoteId",
+            "columnName": "remoteId",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "accountId",
+            "columnName": "account_id",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": true
+        },
+        "indices": [
+          {
+            "name": "index_Folder_account_id",
+            "unique": false,
+            "columnNames": [
+              "account_id"
+            ],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_Folder_account_id` ON `${TABLE_NAME}` (`account_id`)"
+          }
+        ],
+        "foreignKeys": [
+          {
+            "table": "Account",
+            "onDelete": "CASCADE",
+            "onUpdate": "NO ACTION",
+            "columns": [
+              "account_id"
+            ],
+            "referencedColumns": [
+              "id"
+            ]
+          }
+        ]
+      },
+      {
+        "tableName": "Account",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT, `account_name` TEXT, `displayed_name` TEXT, `account_type` INTEGER, `last_modified` INTEGER NOT NULL, `current_account` INTEGER NOT NULL, `token` TEXT, `writeToken` TEXT, `notifications_enabled` INTEGER NOT NULL)",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "url",
+            "columnName": "url",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "accountName",
+            "columnName": "account_name",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "displayedName",
+            "columnName": "displayed_name",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "accountType",
+            "columnName": "account_type",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "lastModified",
+            "columnName": "last_modified",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "currentAccount",
+            "columnName": "current_account",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "token",
+            "columnName": "token",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "writeToken",
+            "columnName": "writeToken",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "notificationsEnabled",
+            "columnName": "notifications_enabled",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": true
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "ItemStateChange",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `read_change` INTEGER NOT NULL, `star_change` INTEGER NOT NULL, `account_id` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`account_id`) REFERENCES `Account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "readChange",
+            "columnName": "read_change",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "starChange",
+            "columnName": "star_change",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "accountId",
+            "columnName": "account_id",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [],
+        "foreignKeys": [
+          {
+            "table": "Account",
+            "onDelete": "CASCADE",
+            "onUpdate": "NO ACTION",
+            "columns": [
+              "account_id"
+            ],
+            "referencedColumns": [
+              "id"
+            ]
+          }
+        ]
+      },
+      {
+        "tableName": "ItemState",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `read` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `remote_id` TEXT NOT NULL, `account_id` INTEGER NOT NULL, FOREIGN KEY(`account_id`) REFERENCES `Account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "read",
+            "columnName": "read",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "starred",
+            "columnName": "starred",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "remoteId",
+            "columnName": "remote_id",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "accountId",
+            "columnName": "account_id",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": true
+        },
+        "indices": [],
+        "foreignKeys": [
+          {
+            "table": "Account",
+            "onDelete": "CASCADE",
+            "onUpdate": "NO ACTION",
+            "columns": [
+              "account_id"
+            ],
+            "referencedColumns": [
+              "id"
+            ]
+          }
+        ]
+      }
+    ],
+    "views": [],
+    "setupQueries": [
+      "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3c36644243041ad4676b32310d961ca9')"
+    ]
+  }
+}
\ No newline at end of file
diff --git a/db/src/androidTest/java/com/readrops/db/DatabaseMigrationsTest.kt b/db/src/androidTest/java/com/readrops/db/DatabaseMigrationsTest.kt
deleted file mode 100644
index 038d02fc..00000000
--- a/db/src/androidTest/java/com/readrops/db/DatabaseMigrationsTest.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-package com.readrops.db
-
-import androidx.room.testing.MigrationTestHelper
-import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.platform.app.InstrumentationRegistry
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import java.io.IOException
-
-@RunWith(AndroidJUnit4::class)
-class DatabaseMigrationsTest {
-
-    private val testDb = "migration-test"
-
-    @get:Rule
-    val helper = MigrationTestHelper(
-            InstrumentationRegistry.getInstrumentation(),
-            "com.readrops.db.Database/",
-            FrameworkSQLiteOpenHelperFactory()
-    )
-
-    @Test
-    @Throws(IOException::class)
-    fun migrate1To2() {
-        helper.createDatabase(testDb, 1).close()
-        helper.runMigrationsAndValidate(testDb, 2, true, Database.MIGRATION_1_2).close()
-    }
-}
\ No newline at end of file
diff --git a/db/src/main/java/com/readrops/db/Database.java b/db/src/main/java/com/readrops/db/Database.java
deleted file mode 100644
index f9cd69f8..00000000
--- a/db/src/main/java/com/readrops/db/Database.java
+++ /dev/null
@@ -1,53 +0,0 @@
-package com.readrops.db;
-
-import android.content.Context;
-
-import androidx.annotation.NonNull;
-import androidx.room.Room;
-import androidx.room.RoomDatabase;
-import androidx.room.TypeConverters;
-import androidx.room.migration.Migration;
-import androidx.sqlite.db.SupportSQLiteDatabase;
-
-import com.readrops.db.dao.AccountDao;
-import com.readrops.db.dao.FeedDao;
-import com.readrops.db.dao.FolderDao;
-import com.readrops.db.dao.ItemDao;
-import com.readrops.db.entities.Feed;
-import com.readrops.db.entities.Folder;
-import com.readrops.db.entities.Item;
-import com.readrops.db.entities.account.Account;
-
-
-@androidx.room.Database(entities = {Feed.class, Item.class, Folder.class, Account.class}, version = 2)
-@TypeConverters({Converters.class})
-public abstract class Database extends RoomDatabase {
-
-    public abstract FeedDao feedDao();
-
-    public abstract ItemDao itemDao();
-
-    public abstract FolderDao folderDao();
-
-    public abstract AccountDao accountDao();
-
-    private static Database database;
-
-    public static Database getInstance(Context context) {
-        if (database == null)
-            database = Room.databaseBuilder(context, Database.class, "readrops-db")
-                    .addMigrations(MIGRATION_1_2)
-                    .build();
-
-        return database;
-    }
-
-    public static final Migration MIGRATION_1_2 = new Migration(1, 2) {
-        @Override
-        public void migrate(@NonNull SupportSQLiteDatabase database) {
-            database.execSQL("Alter Table Account Add Column notifications_enabled INTEGER Not Null Default 0");
-
-            database.execSQL("Alter Table Feed Add Column notification_enabled INTEGER Not Null Default 1");
-        }
-    };
-}
\ No newline at end of file
diff --git a/db/src/main/java/com/readrops/db/Database.kt b/db/src/main/java/com/readrops/db/Database.kt
new file mode 100644
index 00000000..16ab3856
--- /dev/null
+++ b/db/src/main/java/com/readrops/db/Database.kt
@@ -0,0 +1,27 @@
+package com.readrops.db
+
+import androidx.room.Database
+import androidx.room.RoomDatabase
+import androidx.room.TypeConverters
+import com.readrops.db.dao.*
+import com.readrops.db.entities.*
+import com.readrops.db.entities.account.Account
+import dev.matrix.roomigrant.GenerateRoomMigrations
+
+@Database(entities = [Feed::class, Item::class, Folder::class, Account::class,
+    ItemStateChange::class, ItemState::class], version = 3)
+@TypeConverters(Converters::class)
+@GenerateRoomMigrations
+abstract class Database : RoomDatabase() {
+    abstract fun feedDao(): FeedDao
+
+    abstract fun itemDao(): ItemDao
+
+    abstract fun folderDao(): FolderDao
+
+    abstract fun accountDao(): AccountDao
+
+    abstract fun itemStateDao(): ItemStateDao
+
+    abstract fun itemStateChangesDao(): ItemStateChangeDao
+}
\ No newline at end of file
diff --git a/db/src/main/java/com/readrops/db/DbModule.kt b/db/src/main/java/com/readrops/db/DbModule.kt
new file mode 100644
index 00000000..b8a55249
--- /dev/null
+++ b/db/src/main/java/com/readrops/db/DbModule.kt
@@ -0,0 +1,13 @@
+package com.readrops.db
+
+import androidx.room.Room
+import org.koin.dsl.module
+
+val dbModule = module {
+
+    single(createdAtStart = true) {
+        Room.databaseBuilder(get(), Database::class.java, "readrops-db")
+                .addMigrations(*Database_Migrations.build())
+                .build()
+    }
+}
\ No newline at end of file
diff --git a/db/src/main/java/com/readrops/db/ItemsListQueryBuilder.java b/db/src/main/java/com/readrops/db/ItemsListQueryBuilder.java
deleted file mode 100644
index 60a2ca9a..00000000
--- a/db/src/main/java/com/readrops/db/ItemsListQueryBuilder.java
+++ /dev/null
@@ -1,114 +0,0 @@
-package com.readrops.db;
-
-import androidx.sqlite.db.SupportSQLiteQuery;
-import androidx.sqlite.db.SupportSQLiteQueryBuilder;
-
-import com.readrops.db.filters.FilterType;
-import com.readrops.db.filters.ListSortType;
-
-public class ItemsListQueryBuilder {
-
-    private String[] columns = {"Item.id", "title", "clean_description", "image_link", "pub_date", "read",
-            "read_changed", "read_it_later", "Feed.name", "text_color", "background_color", "icon_url", "read_time", "Item.remoteId",
-            "Feed.id as feedId", "Feed.account_id", "Folder.id as folder_id", "Folder.name as folder_name"};
-
-    private String SELECT_ALL_JOIN = "Item INNER JOIN Feed on Item.feed_id = Feed.id " +
-            "LEFT JOIN Folder on Feed.folder_id = Folder.id";
-
-    private String ORDER_BY_ASC = "Item.id DESC";
-
-    private String ORDER_BY_DESC = "pub_date ASC";
-
-    private SupportSQLiteQueryBuilder queryBuilder;
-
-    private boolean showReadItems;
-    private int filterFeedId;
-    private int accountId;
-
-    private FilterType filterType;
-    private ListSortType sortType;
-
-    public ItemsListQueryBuilder() {
-        queryBuilder = SupportSQLiteQueryBuilder.builder(SELECT_ALL_JOIN);
-    }
-
-    private String buildWhereClause() {
-        StringBuilder stringBuilder = new StringBuilder(80);
-
-        stringBuilder.append("Feed.account_id = ").append(accountId).append(" And ");
-
-        if (!showReadItems)
-            stringBuilder.append("read = 0 And ");
-
-        switch (filterType) {
-            case FEED_FILTER:
-                stringBuilder.append("feed_id = ").append(filterFeedId).append(" And read_it_later = 0");
-                break;
-            case READ_IT_LATER_FILTER:
-                stringBuilder.append("read_it_later = 1");
-                break;
-            case NO_FILTER:
-                stringBuilder.append("read_it_later = 0");
-                break;
-            default:
-                stringBuilder.append("read_it_later = 0");
-                break;
-        }
-
-        return stringBuilder.toString();
-    }
-
-
-    public SupportSQLiteQuery getQuery() {
-        queryBuilder.columns(columns);
-
-        queryBuilder.selection(buildWhereClause(), new String[0]);
-
-        if (sortType == ListSortType.NEWEST_TO_OLDEST)
-            queryBuilder.orderBy(ORDER_BY_ASC);
-        else
-            queryBuilder.orderBy(ORDER_BY_DESC);
-
-        return queryBuilder.create();
-    }
-
-    public boolean showReadItems() {
-        return showReadItems;
-    }
-
-    public void setShowReadItems(boolean showReadItems) {
-        this.showReadItems = showReadItems;
-    }
-
-    public int getFilterFeedId() {
-        return filterFeedId;
-    }
-
-    public void setFilterFeedId(int filterFeedId) {
-        this.filterFeedId = filterFeedId;
-    }
-
-    public FilterType getFilterType() {
-        return filterType;
-    }
-
-    public void setFilterType(FilterType filterType) {
-        this.filterType = filterType;
-    }
-
-    public ListSortType getSortType() {
-        return sortType;
-    }
-
-    public void setSortType(ListSortType sortType) {
-        this.sortType = sortType;
-    }
-
-    public int getAccountId() {
-        return accountId;
-    }
-
-    public void setAccountId(int accountId) {
-        this.accountId = accountId;
-    }
-}
diff --git a/db/src/main/java/com/readrops/db/dao/FeedDao.java b/db/src/main/java/com/readrops/db/dao/FeedDao.java
index 9ff6a5b7..af171860 100644
--- a/db/src/main/java/com/readrops/db/dao/FeedDao.java
+++ b/db/src/main/java/com/readrops/db/dao/FeedDao.java
@@ -78,8 +78,8 @@ public abstract class FeedDao implements BaseDao<Feed> {
     @Query("Select remoteId From Feed Where account_id = :accountId")
     public abstract List<String> getFeedRemoteIdsOfAccount(int accountId);
 
-    @Query("Delete from Feed Where remoteId in (:ids)")
-    abstract void deleteByIds(List<String> ids);
+    @Query("Delete from Feed Where remoteId in (:ids) And account_id = :accountId")
+    abstract void deleteByIds(List<String> ids, int accountId);
 
     @Query("Select id From Folder Where remoteId = :remoteId And account_id = :accountId")
     abstract int getRemoteFolderLocalId(String remoteId, int accountId);
@@ -123,8 +123,9 @@ public abstract class FeedDao implements BaseDao<Feed> {
             }
         }
 
-        if (!accountFeedIds.isEmpty())
-            deleteByIds(accountFeedIds);
+        if (!accountFeedIds.isEmpty()) {
+            deleteByIds(accountFeedIds, account.getId());
+        }
 
         return insert(feedsToInsert);
     }
diff --git a/db/src/main/java/com/readrops/db/dao/FolderDao.java b/db/src/main/java/com/readrops/db/dao/FolderDao.java
index b527c4df..75f1b507 100644
--- a/db/src/main/java/com/readrops/db/dao/FolderDao.java
+++ b/db/src/main/java/com/readrops/db/dao/FolderDao.java
@@ -36,8 +36,8 @@ public abstract class FolderDao implements BaseDao<Folder> {
     @Query("Select remoteId From Folder Where account_id = :accountId")
     public abstract List<String> getFolderRemoteIdsOfAccount(int accountId);
 
-    @Query("Delete From Folder Where remoteId in (:ids)")
-    abstract void deleteByIds(List<String> ids);
+    @Query("Delete From Folder Where remoteId in (:ids) And account_id = :accountId")
+    abstract void deleteByIds(List<String> ids, int accountId);
 
     @Query("Select * From Folder Where name = :name And account_id = :accountId")
     public abstract Folder getFolderByName(String name, int accountId);
@@ -47,7 +47,7 @@ public abstract class FolderDao implements BaseDao<Folder> {
      *
      * @param folders folders to insert or update
      * @param account owner of the feeds
-     * @return the list of the inserted feeds ids
+     * @return the list of the inserted folders ids
      */
     @Transaction
     public List<Long> foldersUpsert(List<Folder> folders, Account account) {
@@ -64,8 +64,9 @@ public abstract class FolderDao implements BaseDao<Folder> {
             }
         }
 
-        if (!accountFolderIds.isEmpty())
-            deleteByIds(accountFolderIds);
+        if (!accountFolderIds.isEmpty()) {
+            deleteByIds(accountFolderIds, account.getId());
+        }
 
         return insert(foldersToInsert);
     }
diff --git a/db/src/main/java/com/readrops/db/dao/ItemDao.java b/db/src/main/java/com/readrops/db/dao/ItemDao.java
index b0eb1f45..30f368c0 100644
--- a/db/src/main/java/com/readrops/db/dao/ItemDao.java
+++ b/db/src/main/java/com/readrops/db/dao/ItemDao.java
@@ -6,13 +6,14 @@ import androidx.paging.DataSource;
 import androidx.room.Dao;
 import androidx.room.Query;
 import androidx.room.RawQuery;
-import androidx.room.RoomWarnings;
 import androidx.sqlite.db.SupportSQLiteQuery;
 
 import com.readrops.db.entities.Feed;
 import com.readrops.db.entities.Folder;
 import com.readrops.db.entities.Item;
+import com.readrops.db.entities.ItemState;
 import com.readrops.db.pojo.ItemWithFeed;
+import com.readrops.db.pojo.StarItem;
 
 import java.util.List;
 
@@ -21,7 +22,7 @@ import io.reactivex.Completable;
 @Dao
 public interface ItemDao extends BaseDao<Item> {
 
-    @RawQuery(observedEntities = {Item.class, Folder.class, Feed.class})
+    @RawQuery(observedEntities = {Item.class, Folder.class, Feed.class, ItemState.class})
     DataSource.Factory<Integer, ItemWithFeed> selectAll(SupportSQLiteQuery query);
 
     @Query("Select * From Item Where id = :itemId")
@@ -36,43 +37,30 @@ public interface ItemDao extends BaseDao<Item> {
     @Query("Select * From Item Where remoteId = :remoteId And feed_id = :feedId")
     Item selectByRemoteId(String remoteId, int feedId);
 
-    /**
-     * Set an item read or unread
-     *
-     * @param itemId      id of the item to update
-     * @param read   1 for read, 0 for unread
-     * @param readChanged
-     */
-    @Query("Update Item Set read_changed = :readChanged, read = :read Where id = :itemId")
-    Completable setReadState(int itemId, boolean read, boolean readChanged);
+    @Query("Update Item Set read = :read Where id = :itemId")
+    Completable setReadState(int itemId, boolean read);
 
-    @Query("Update Item set read_changed = 1, read = :readState Where feed_id In (Select id From Feed Where account_id = :accountId)")
+    @Query("Update Item set starred = :starred Where id = :itemId")
+    Completable setStarState(int itemId, boolean starred);
+
+    @Query("Update Item set read = :readState Where feed_id In (Select id From Feed Where account_id = :accountId)")
     Completable setAllItemsReadState(int readState, int accountId);
 
-    @Query("Update Item set read_changed = 1, read = :readState Where feed_id = :feedId")
+    @Query("Update Item set read = :readState Where feed_id = :feedId")
     Completable setAllFeedItemsReadState(int feedId, int readState);
 
-    @Query("Update Item set read_it_later = 1 Where id = :itemId")
-    Completable setReadItLater(int itemId);
+    @Query("Update Item set read_it_later = :readLater Where id = :itemId")
+    Completable setReadItLater(boolean readLater, int itemId);
 
     @Query("Select count(*) From Item Where feed_id = :feedId And read = 0")
     int getUnreadCount(int feedId);
+    
+    @RawQuery(observedEntities = {Item.class, ItemState.class})
+    LiveData<ItemWithFeed> getItemById(SupportSQLiteQuery query);
 
-    @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
-    @Query("Select title, Item.description, content, link, pub_date, image_link, author, read, text_color, " +
-            "background_color, read_time, Feed.name, Feed.id as feedId, siteUrl, Folder.id as folder_id, " +
-            "Folder.name as folder_name from Item Inner Join Feed On Item.feed_id = Feed.id Left Join Folder on Folder.id = Feed.folder_id Where Item.id = :id")
-    LiveData<ItemWithFeed> getItemById(int id);
+    @Query("Select Item.guid, Feed.remoteId as feedRemoteId From Item Inner Join Feed On Item.feed_id = Feed.id Where Item.remoteId In (:remoteIds) And account_id = :accountId")
+    List<StarItem> getStarChanges(List<String> remoteIds, int accountId);
 
-    @Query("Select Item.remoteId From Item Inner Join Feed On Item.feed_id = Feed.id Where read_changed = 1 And read = 1 And account_id = :accountId")
-    List<String> getReadChanges(int accountId);
-
-    @Query("Select Item.remoteId From Item Inner Join Feed On Item.feed_id = Feed.id Where read_changed = 1 And read = 0 And account_id = :accountId")
-    List<String> getUnreadChanges(int accountId);
-
-    @Query("Update Item set read_changed = 0 Where feed_id in (Select id From Feed Where account_id = :accountId)")
-    void resetReadChanges(int accountId);
-
-    @Query("Update Item set read = :read Where remoteId = :remoteId")
-    void setReadState(String remoteId, boolean read);
+    @Query("Update Item set read = :read, starred = :starred Where remoteId = :remoteId")
+    void setReadAndStarState(String remoteId, boolean read, boolean starred);
 }
diff --git a/db/src/main/java/com/readrops/db/dao/ItemStateChangeDao.kt b/db/src/main/java/com/readrops/db/dao/ItemStateChangeDao.kt
new file mode 100644
index 00000000..0ae93b72
--- /dev/null
+++ b/db/src/main/java/com/readrops/db/dao/ItemStateChangeDao.kt
@@ -0,0 +1,119 @@
+package com.readrops.db.dao
+
+import androidx.room.Dao
+import androidx.room.Delete
+import androidx.room.Insert
+import androidx.room.Query
+import com.readrops.db.entities.Item
+import com.readrops.db.entities.ItemStateChange
+import com.readrops.db.pojo.ItemReadStarState
+import io.reactivex.Completable
+
+@Dao
+interface ItemStateChangeDao : BaseDao<ItemStateChange> {
+
+    @Insert
+    fun insertItemStateChange(itemStateChange: ItemStateChange)
+
+    @Delete
+    fun deleteItemStateChange(itemStateChange: ItemStateChange)
+
+    @Query("Delete From ItemStateChange Where account_id = :accountId")
+    fun resetStateChanges(accountId: Int)
+
+    @Query("Select case When ItemState.remote_id is NULL Or ItemState.read = 1 Then 1 else 0 End read,  " +
+            "case When ItemState.remote_id is NULL Or ItemState.starred = 1 Then 1 else 0 End starred," +
+            "ItemStateChange.read_change, ItemStateChange.star_change, Item.remoteId " +
+            "From ItemStateChange Inner Join Item On ItemStateChange.id = Item.id " +
+            "Left Join ItemState On ItemState.remote_id = Item.remoteId Where ItemStateChange.account_id = :accountId")
+    fun getItemStateChanges(accountId: Int): List<ItemReadStarState>
+
+    @Query("Select Item.read, Item.starred," +
+            "ItemStateChange.read_change, ItemStateChange.star_change, Item.remoteId " +
+            "From ItemStateChange Inner Join Item On ItemStateChange.id = Item.id " +
+            "Where ItemStateChange.account_id = :accountId")
+    fun getNextcloudNewsStateChanges(accountId: Int): List<ItemReadStarState>
+
+    @Query("Select Case When :itemId In (Select id From ItemStateChange Where read_change = 1) Then 1 Else 0 End")
+    fun readStateChangeExists(itemId: Int): Boolean
+
+    @Query("Select Case When :itemId In (Select id From ItemStateChange Where star_change = 1) Then 1 Else 0 End")
+    fun starStateChangeExists(itemId: Int): Boolean
+
+    fun upsertItemReadStateChange(item: Item, accountId: Int, useSeparateState: Boolean) = Completable.create {
+        if (itemStateChangeExists(item.id, accountId)) {
+            val oldItemReadState = if (useSeparateState)
+                getItemReadState(item.remoteId, accountId)
+            else
+                getStandardItemReadState(item.remoteId, accountId)
+
+            val readChange = item.isRead != oldItemReadState
+
+            if (readChange) {
+                val oldItemStateChange = selectItemStateChange(item.id)
+                val newReadChange = !oldItemStateChange.readChange
+
+                if (!newReadChange && !oldItemStateChange.starChange) {
+                    deleteItemStateChange(oldItemStateChange)
+                } else {
+                    updateItemReadStateChange(newReadChange, oldItemStateChange.id)
+                }
+            }
+        } else {
+            insertItemStateChange(ItemStateChange(id = item.id, readChange = true, accountId = accountId))
+        }
+
+        it.onComplete()
+    }
+
+    fun upsertItemStarStateChange(item: Item, accountId: Int, useSeparateState: Boolean) = Completable.create {
+        if (itemStateChangeExists(item.id, accountId)) {
+            val oldItemStarState = if (useSeparateState)
+                getItemStarState(item.remoteId, accountId)
+            else
+                getStandardItemStarState(item.remoteId, accountId)
+
+            val starChange = item.isStarred != oldItemStarState
+
+            if (starChange) {
+                val oldItemStateChange = selectItemStateChange(item.id)
+                val newStarChange = !oldItemStateChange.starChange
+
+                if (!newStarChange && !oldItemStateChange.readChange) {
+                    deleteItemStateChange(oldItemStateChange)
+                } else {
+                    updateItemStarStateChange(newStarChange, oldItemStateChange.id)
+                }
+            }
+        } else {
+            insertItemStateChange(ItemStateChange(id = item.id, starChange = true, accountId = accountId))
+        }
+
+        it.onComplete()
+    }
+
+    @Query("Select * From ItemStateChange Where id = :id")
+    fun selectItemStateChange(id: Int): ItemStateChange
+
+    @Query("Select case When Exists (Select id, account_id From ItemStateChange Where id = :id And account_id = :accountId) Then 1 else 0 End")
+    fun itemStateChangeExists(id: Int, accountId: Int): Boolean
+
+    @Query("Select read From ItemState Where remote_id = :remoteId And account_id = :accountId")
+    fun getItemReadState(remoteId: String, accountId: Int): Boolean
+
+    @Query("Select read From Item Inner Join Feed On Item.feed_id = Feed.id Where Item.remoteId = :remoteId And account_id = :accountId")
+    fun getStandardItemReadState(remoteId: String, accountId: Int): Boolean
+
+    @Query("Select starred From ItemState Where remote_id = :remoteId And account_id = :accountId")
+    fun getItemStarState(remoteId: String, accountId: Int): Boolean
+
+    @Query("Select starred From Item Inner Join Feed On Item.feed_id = Feed.id Where Item.remoteId = :remoteId And account_id = :accountId")
+    fun getStandardItemStarState(remoteId: String, accountId: Int): Boolean
+
+    @Query("Update ItemStateChange set read_change = :readChange Where id = :id")
+    fun updateItemReadStateChange(readChange: Boolean, id: Int)
+
+    @Query("Update ItemStateChange set star_change = :starChange Where id = :id")
+    fun updateItemStarStateChange(starChange: Boolean, id: Int)
+
+}
\ No newline at end of file
diff --git a/db/src/main/java/com/readrops/db/dao/ItemStateDao.kt b/db/src/main/java/com/readrops/db/dao/ItemStateDao.kt
new file mode 100644
index 00000000..770da7c8
--- /dev/null
+++ b/db/src/main/java/com/readrops/db/dao/ItemStateDao.kt
@@ -0,0 +1,52 @@
+package com.readrops.db.dao
+
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.Query
+import com.readrops.db.entities.ItemState
+import io.reactivex.Completable
+
+@Dao
+interface ItemStateDao : BaseDao<ItemState> {
+
+    @Query("Delete From ItemState Where account_id = :accountId")
+    fun deleteItemsStates(accountId: Int)
+
+    @Query("Delete From ItemState Where remote_id = :remoteId And account_id = :accountId")
+    fun deleteItemState(remoteId: String, accountId: Int)
+
+    @Insert
+    fun insertItemStates(items: List<ItemState>)
+
+    @Insert
+    fun insertItemState(itemState: ItemState)
+
+    @Query("Update ItemState set read = :read Where remote_id = :remoteId And account_id = :accountId")
+    fun updateItemReadState(read: Boolean, remoteId: String, accountId: Int)
+
+    @Query("Update ItemState set starred = :star Where remote_id = :remoteId And account_id = :accountId")
+    fun updateItemStarState(star: Boolean, remoteId: String, accountId: Int)
+
+    @Query("Select case When Exists (Select remote_id, account_id From ItemState Where remote_id = :remoteId And account_id = :accountId) Then 1 else 0 End")
+    fun itemStateExists(remoteId: String, accountId: Int): Boolean
+
+    fun upsertItemReadState(itemState: ItemState) = Completable.create {
+        if (itemStateExists(itemState.remoteId, itemState.accountId)) {
+            updateItemReadState(itemState.read, itemState.remoteId, itemState.accountId)
+        } else {
+            insertItemState(itemState)
+        }
+
+        it.onComplete()
+    }
+
+    fun upsertItemStarState(itemState: ItemState) = Completable.create {
+        if (itemStateExists(itemState.remoteId, itemState.accountId)) {
+            updateItemStarState(itemState.starred, itemState.remoteId, itemState.accountId)
+        } else {
+            insertItemState(itemState)
+        }
+
+        it.onComplete()
+    }
+}
\ No newline at end of file
diff --git a/db/src/main/java/com/readrops/db/entities/Folder.java b/db/src/main/java/com/readrops/db/entities/Folder.java
index 0889b6f7..f03c081e 100644
--- a/db/src/main/java/com/readrops/db/entities/Folder.java
+++ b/db/src/main/java/com/readrops/db/entities/Folder.java
@@ -11,6 +11,8 @@ import androidx.room.PrimaryKey;
 
 import com.readrops.db.entities.account.Account;
 
+import java.util.Objects;
+
 @Entity(foreignKeys = @ForeignKey(entity = Account.class, parentColumns = "id",
         childColumns = "account_id", onDelete = ForeignKey.CASCADE))
 public class Folder implements Parcelable, Comparable<Folder> {
@@ -94,6 +96,26 @@ public class Folder implements Parcelable, Comparable<Folder> {
         dest.writeString(name);
     }
 
+    @Override
+    public boolean equals(Object o) {
+        if (o == null) {
+            return false;
+        } else if (!(o instanceof Folder)) {
+            return false;
+        } else {
+            Folder folder = (Folder) o;
+
+            return id == folder.id && Objects.equals(name, folder.name) &&
+                    Objects.equals(remoteId, folder.remoteId) &&
+                    accountId == folder.accountId;
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(id, name, remoteId, accountId);
+    }
+
     @Override
     public int compareTo(Folder o) {
         return this.getName().compareTo(o.getName());
diff --git a/db/src/main/java/com/readrops/db/entities/Item.java b/db/src/main/java/com/readrops/db/entities/Item.java
index a56f01a7..b722574d 100644
--- a/db/src/main/java/com/readrops/db/entities/Item.java
+++ b/db/src/main/java/com/readrops/db/entities/Item.java
@@ -47,8 +47,7 @@ public class Item implements Comparable<Item> {
 
     private boolean read;
 
-    @ColumnInfo(name = "read_changed")
-    private boolean readChanged;
+    private boolean starred;
 
     @ColumnInfo(name = "read_it_later")
     private boolean readItLater;
@@ -173,12 +172,12 @@ public class Item implements Comparable<Item> {
         this.read = read;
     }
 
-    public boolean isReadChanged() {
-        return readChanged;
+    public boolean isStarred() {
+        return starred;
     }
 
-    public void setReadChanged(boolean readChanged) {
-        this.readChanged = readChanged;
+    public void setStarred(boolean starred) {
+        this.starred = starred;
     }
 
     public boolean isReadItLater() {
diff --git a/db/src/main/java/com/readrops/db/entities/ItemState.kt b/db/src/main/java/com/readrops/db/entities/ItemState.kt
new file mode 100644
index 00000000..aebca1de
--- /dev/null
+++ b/db/src/main/java/com/readrops/db/entities/ItemState.kt
@@ -0,0 +1,26 @@
+package com.readrops.db.entities
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.ForeignKey
+import androidx.room.PrimaryKey
+import com.readrops.db.entities.account.Account
+
+@Entity(foreignKeys = [ForeignKey(entity = Account::class, parentColumns = ["id"],
+        childColumns = ["account_id"], onDelete = ForeignKey.CASCADE)])
+data class ItemStateChange(
+        @PrimaryKey val id: Int = 0,
+        @ColumnInfo(name = "read_change") val readChange: Boolean = false,
+        @ColumnInfo(name = "star_change") val starChange: Boolean = false,
+        @ColumnInfo(name = "account_id") val accountId: Int,
+)
+
+@Entity(foreignKeys = [ForeignKey(entity = Account::class, parentColumns = ["id"],
+        childColumns = ["account_id"], onDelete = ForeignKey.CASCADE)])
+data class ItemState(
+        @PrimaryKey(autoGenerate = true) val id: Int = 0,
+        val read: Boolean = false,
+        val starred: Boolean = false,
+        @ColumnInfo(name = "remote_id") val remoteId: String,
+        @ColumnInfo(name = "account_id") val accountId: Int,
+)
\ No newline at end of file
diff --git a/db/src/main/java/com/readrops/db/entities/account/Account.java b/db/src/main/java/com/readrops/db/entities/account/Account.java
index 950acd84..1e37ecc8 100644
--- a/db/src/main/java/com/readrops/db/entities/account/Account.java
+++ b/db/src/main/java/com/readrops/db/entities/account/Account.java
@@ -185,6 +185,10 @@ public class Account implements Parcelable {
         this.notificationsEnabled = notificationsEnabled;
     }
 
+    public AccountConfig getConfig() {
+        return accountType.getAccountConfig();
+    }
+
     @Override
     public int describeContents() {
         return 0;
diff --git a/db/src/main/java/com/readrops/db/entities/account/AccountConfig.java b/db/src/main/java/com/readrops/db/entities/account/AccountConfig.java
index b05c8bf7..7474c10c 100644
--- a/db/src/main/java/com/readrops/db/entities/account/AccountConfig.java
+++ b/db/src/main/java/com/readrops/db/entities/account/AccountConfig.java
@@ -6,25 +6,33 @@ public class AccountConfig {
             .setFeedUrlEditable(true)
             .setFolderCreation(true)
             .setNoFolderCase(false)
+            .setUseSeparateState(false)
             .build();
 
-    public static final AccountConfig NEXTNEWS = new AccountConfigBuilder()
+    public static final AccountConfig NEXTCLOUD_NEWS = new AccountConfigBuilder()
             .setFeedUrlEditable(false)
             .setFolderCreation(true)
             .setNoFolderCase(false)
+            .setUseSeparateState(false)
             .build();
 
     public static final AccountConfig FRESHRSS = new AccountConfigBuilder()
             .setFeedUrlEditable(false)
             .setFolderCreation(false)
             .setNoFolderCase(true)
+            .setUseSeparateState(true)
             .build();
 
-    private boolean feedUrlEditable;
+    private final boolean feedUrlEditable;
 
-    private boolean folderCreation;
+    private final boolean folderCreation;
 
-    private boolean noFolderCase;
+    private final boolean noFolderCase;
+
+    /*
+    Let knows if it uses ItemState table to synchronize state
+     */
+    private final boolean useSeparateState;
 
     public boolean isFeedUrlEditable() {
         return feedUrlEditable;
@@ -38,16 +46,22 @@ public class AccountConfig {
         return noFolderCase;
     }
 
+    public boolean useSeparateState() {
+        return useSeparateState;
+    }
+
     public AccountConfig(AccountConfigBuilder builder) {
         this.feedUrlEditable = builder.feedUrlEditable;
         this.folderCreation = builder.folderCreation;
         this.noFolderCase = builder.noFolderCase;
+        this.useSeparateState = builder.useSeparateState;
     }
 
     public static class AccountConfigBuilder {
         private boolean feedUrlEditable;
         private boolean folderCreation;
         private boolean noFolderCase;
+        private boolean useSeparateState;
 
         public AccountConfigBuilder setFeedUrlEditable(boolean feedUrlEditable) {
             this.feedUrlEditable = feedUrlEditable;
@@ -64,6 +78,11 @@ public class AccountConfig {
             return this;
         }
 
+        public AccountConfigBuilder setUseSeparateState(boolean useSeparateState) {
+            this.useSeparateState = useSeparateState;
+            return this;
+        }
+
         public AccountConfig build() {
             return new AccountConfig(this);
         }
diff --git a/db/src/main/java/com/readrops/db/entities/account/AccountType.java b/db/src/main/java/com/readrops/db/entities/account/AccountType.java
index c724893e..65ec00cd 100644
--- a/db/src/main/java/com/readrops/db/entities/account/AccountType.java
+++ b/db/src/main/java/com/readrops/db/entities/account/AccountType.java
@@ -11,7 +11,7 @@ import com.readrops.db.R;
 
 public enum AccountType implements Parcelable {
     LOCAL(R.mipmap.ic_launcher, R.string.local_account, AccountConfig.LOCAL),
-    NEXTCLOUD_NEWS(R.drawable.ic_nextcloud_news, R.string.nextcloud_news, AccountConfig.NEXTNEWS),
+    NEXTCLOUD_NEWS(R.drawable.ic_nextcloud_news, R.string.nextcloud_news, AccountConfig.NEXTCLOUD_NEWS),
     FEEDLY(R.drawable.ic_feedly, R.string.feedly, null),
     FRESHRSS(R.drawable.ic_freshrss, R.string.freshrss, AccountConfig.FRESHRSS);
 
diff --git a/db/src/main/java/com/readrops/db/filters/FilterType.java b/db/src/main/java/com/readrops/db/filters/FilterType.java
index 7ed9eb9f..e971c0af 100644
--- a/db/src/main/java/com/readrops/db/filters/FilterType.java
+++ b/db/src/main/java/com/readrops/db/filters/FilterType.java
@@ -2,6 +2,8 @@ package com.readrops.db.filters;
 
 public enum FilterType {
     FEED_FILTER,
+    FOLDER_FILER,
     READ_IT_LATER_FILTER,
+    STARS_FILTER,
     NO_FILTER
 }
diff --git a/db/src/main/java/com/readrops/db/pojo/ItemReadStarState.kt b/db/src/main/java/com/readrops/db/pojo/ItemReadStarState.kt
new file mode 100644
index 00000000..4ada491f
--- /dev/null
+++ b/db/src/main/java/com/readrops/db/pojo/ItemReadStarState.kt
@@ -0,0 +1,9 @@
+package com.readrops.db.pojo
+
+import androidx.room.ColumnInfo
+
+data class ItemReadStarState(val remoteId: String,
+                             val read: Boolean,
+                             val starred: Boolean,
+                             @ColumnInfo(name = "read_change") val readChange: Boolean,
+                             @ColumnInfo(name = "star_change") val starChange: Boolean)
\ No newline at end of file
diff --git a/db/src/main/java/com/readrops/db/pojo/StarItem.kt b/db/src/main/java/com/readrops/db/pojo/StarItem.kt
new file mode 100644
index 00000000..a58ba616
--- /dev/null
+++ b/db/src/main/java/com/readrops/db/pojo/StarItem.kt
@@ -0,0 +1,7 @@
+package com.readrops.db.pojo
+
+import androidx.room.ColumnInfo
+
+
+data class StarItem(@ColumnInfo val feedRemoteId: String,
+        @ColumnInfo(name = "guid") val guidHash: String)
\ No newline at end of file
diff --git a/db/src/main/java/com/readrops/db/queries/ItemSelectionQueryBuilder.kt b/db/src/main/java/com/readrops/db/queries/ItemSelectionQueryBuilder.kt
new file mode 100644
index 00000000..1da869d1
--- /dev/null
+++ b/db/src/main/java/com/readrops/db/queries/ItemSelectionQueryBuilder.kt
@@ -0,0 +1,34 @@
+package com.readrops.db.queries
+
+import androidx.sqlite.db.SupportSQLiteQuery
+import androidx.sqlite.db.SupportSQLiteQueryBuilder
+
+object ItemSelectionQueryBuilder {
+
+    private val COLUMNS = arrayOf("Item.id", "Item.remoteId", "title", "Item.description", "content",
+            "link", "pub_date", "image_link", "author", "Item.read", "text_color",
+            "background_color", "read_time", "Feed.name", "Feed.id as feedId", "siteUrl",
+            "Folder.id as folder_id", "Folder.name as folder_name")
+
+    private val SEPARATE_STATE_COLUMNS = arrayOf("case When ItemState.starred = 1 Then 1 else 0 End starred")
+
+    private const val JOIN = "Item Inner Join Feed On Item.feed_id = Feed.id Left Join Folder on Folder.id = Feed.folder_id"
+
+    private const val SEPARATE_STATE_JOIN = " Left Join ItemState On ItemState.remote_id = Item.remoteId"
+
+    /**
+     * @param separateState Indicates if item state must be retrieved from ItemState table
+     */
+    @JvmStatic
+    fun buildQuery(itemId: Int, separateState: Boolean): SupportSQLiteQuery {
+        val tables = if (separateState) JOIN + SEPARATE_STATE_JOIN else JOIN
+        val columns = if (separateState) COLUMNS.plus(SEPARATE_STATE_COLUMNS) else COLUMNS.plus("starred")
+
+        return SupportSQLiteQueryBuilder.builder(tables).run {
+            columns(columns)
+            selection("Item.id = $itemId", null)
+
+            create()
+        }
+    }
+}
\ No newline at end of file
diff --git a/db/src/main/java/com/readrops/db/queries/ItemsQueryBuilder.kt b/db/src/main/java/com/readrops/db/queries/ItemsQueryBuilder.kt
new file mode 100644
index 00000000..c5f5bc13
--- /dev/null
+++ b/db/src/main/java/com/readrops/db/queries/ItemsQueryBuilder.kt
@@ -0,0 +1,91 @@
+package com.readrops.db.queries
+
+import androidx.sqlite.db.SupportSQLiteQuery
+import androidx.sqlite.db.SupportSQLiteQueryBuilder
+import com.readrops.db.filters.FilterType
+import com.readrops.db.filters.ListSortType
+
+object ItemsQueryBuilder {
+
+    private val COLUMNS = arrayOf("Item.id", "Item.remoteId", "title", "clean_description", "image_link", "pub_date",
+            "read_it_later", "Feed.name", "text_color", "background_color", "icon_url", "read_time",
+            "Feed.id as feedId", "Feed.account_id", "Folder.id as folder_id", "Folder.name as folder_name")
+
+    private val SEPARATE_STATE_COLUMNS = arrayOf("case When ItemState.remote_id is NULL Or ItemState.read = 1 Then 1 else 0 End read",
+            "case When ItemState.remote_id is NULL or ItemState.starred = 1 Then 1 else 0 End starred")
+
+    private val OTHER_COLUMNS = arrayOf("read", "starred")
+
+    private val SELECT_ALL_JOIN = """Item INNER JOIN Feed on Item.feed_id = Feed.id
+            LEFT JOIN Folder on Feed.folder_id = Folder.id """.trimIndent()
+
+    private const val SEPARATE_STATE_JOIN = "LEFT JOIN ItemState On Item.remoteId = ItemState.remote_id"
+
+    private const val ORDER_BY_ASC = "pub_date DESC"
+
+    private const val ORDER_BY_DESC = "pub_date ASC"
+
+    @JvmStatic
+    fun buildItemsQuery(queryFilters: QueryFilters, separateState: Boolean): SupportSQLiteQuery =
+            buildQuery(queryFilters, separateState)
+
+    @JvmStatic
+    fun buildItemsQuery(queryFilters: QueryFilters): SupportSQLiteQuery =
+            buildQuery(queryFilters, false)
+
+    private fun buildQuery(queryFilters: QueryFilters, separateState: Boolean): SupportSQLiteQuery = with(queryFilters) {
+        if (accountId == 0)
+            throw IllegalArgumentException("AccountId must be greater than 0")
+
+        if (filterType == FilterType.FEED_FILTER && filterFeedId == 0)
+            throw IllegalArgumentException("FeedId must be greater than 0 if current filter is FEED_FILTER")
+
+        val columns = if (separateState) COLUMNS.plus(SEPARATE_STATE_COLUMNS) else COLUMNS.plus(OTHER_COLUMNS)
+        val selectAllJoin = if (separateState) SELECT_ALL_JOIN + SEPARATE_STATE_JOIN else SELECT_ALL_JOIN
+
+        SupportSQLiteQueryBuilder.builder(selectAllJoin).run {
+            columns(columns)
+            selection(buildWhereClause(this@with, separateState), null)
+            orderBy(if (sortType == ListSortType.NEWEST_TO_OLDEST) ORDER_BY_ASC else ORDER_BY_DESC)
+
+            create()
+        }
+    }
+
+    private fun buildWhereClause(queryFilters: QueryFilters, separateState: Boolean): String = StringBuilder(500).run {
+        append("Feed.account_id = ${queryFilters.accountId} And ")
+
+        if (!queryFilters.showReadItems) {
+            if (separateState)
+                append("ItemState.read = 0 And ")
+            else
+                append("Item.read = 0 And ")
+        }
+
+        when (queryFilters.filterType) {
+            FilterType.FEED_FILTER -> append("feed_id = ${queryFilters.filterFeedId} And read_it_later = 0")
+            FilterType.FOLDER_FILER -> append("folder_id = ${queryFilters.filterFolderId} And read_it_later = 0")
+            FilterType.READ_IT_LATER_FILTER -> append("read_it_later = 1")
+            FilterType.STARS_FILTER -> {
+                if (separateState) {
+                    append("ItemState.starred = 1 And read_it_later = 0")
+                } else {
+                    append("starred = 1 And read_it_later = 0")
+                }
+            }
+            else -> append("read_it_later = 0")
+        }
+
+        toString()
+    }
+
+}
+
+class QueryFilters(
+        var showReadItems: Boolean = true,
+        var filterFeedId: Int = 0,
+        var filterFolderId: Int = 0,
+        var accountId: Int = 0,
+        var filterType: FilterType = FilterType.NO_FILTER,
+        var sortType: ListSortType = ListSortType.NEWEST_TO_OLDEST,
+)
\ No newline at end of file
diff --git a/db/src/test/java/com/readrops/db/ExampleUnitTest.java b/db/src/test/java/com/readrops/db/ExampleUnitTest.java
deleted file mode 100644
index f8cec603..00000000
--- a/db/src/test/java/com/readrops/db/ExampleUnitTest.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.readrops.db;
-
-import org.junit.Test;
-
-import static org.junit.Assert.*;
-
-/**
- * Example local unit test, which will execute on the development machine (host).
- *
- * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
- */
-public class ExampleUnitTest {
-    @Test
-    public void addition_isCorrect() {
-        assertEquals(4, 2 + 2);
-    }
-}
\ No newline at end of file
diff --git a/db/src/test/java/com/readrops/db/ItemsQueryBuilderTest.kt b/db/src/test/java/com/readrops/db/ItemsQueryBuilderTest.kt
new file mode 100644
index 00000000..ae2baa01
--- /dev/null
+++ b/db/src/test/java/com/readrops/db/ItemsQueryBuilderTest.kt
@@ -0,0 +1,75 @@
+package com.readrops.db
+
+import com.readrops.db.filters.FilterType
+import com.readrops.db.filters.ListSortType
+import com.readrops.db.queries.ItemsQueryBuilder
+import com.readrops.db.queries.QueryFilters
+import junit.framework.TestCase.assertFalse
+import junit.framework.TestCase.assertTrue
+import org.junit.Test
+
+class ItemsQueryBuilderTest {
+
+    @Test
+    fun noFilterDefaultSortCaseTest() {
+        val queryFilters = QueryFilters(accountId = 1)
+
+        val query = ItemsQueryBuilder.buildItemsQuery(queryFilters).sql
+
+        assertTrue(query.contains("Feed.account_id = 1"))
+        assertTrue(query.contains("read_it_later = 0"))
+        assertTrue(query.contains("pub_date DESC"))
+
+        assertFalse(query.contains("read = 0 And"))
+    }
+
+    @Test
+    fun feedFilterCaseTest() {
+        val queryFilters = QueryFilters(accountId = 1, filterType = FilterType.FEED_FILTER,
+                filterFeedId = 15)
+
+        val query = ItemsQueryBuilder.buildItemsQuery(queryFilters).sql
+
+        assertTrue(query.contains("feed_id = 15 And read_it_later = 0"))
+    }
+
+    @Test
+    fun readLaterFilterCaseTest() {
+        val queryFilters = QueryFilters(accountId = 1, filterType = FilterType.READ_IT_LATER_FILTER)
+
+        val query = ItemsQueryBuilder.buildItemsQuery(queryFilters).sql
+        assertTrue(query.contains("read_it_later = 1"))
+    }
+
+    @Test
+    fun starsFilterCaseTest() {
+        val queryFilters = QueryFilters(accountId = 1, filterType = FilterType.STARS_FILTER)
+
+        val query = ItemsQueryBuilder.buildItemsQuery(queryFilters).sql
+        assertTrue(query.contains("starred = 1 And read_it_later = 0"))
+    }
+
+    @Test
+    fun oldestSortCaseTest() {
+        val queryFilters = QueryFilters(accountId = 1, sortType = ListSortType.OLDEST_TO_NEWEST,
+                showReadItems = false)
+
+        val query = ItemsQueryBuilder.buildItemsQuery(queryFilters).sql
+
+        assertTrue(query.contains("read = 0 And "))
+        assertTrue(query.contains("pub_date ASC"))
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun accountIdExceptionTest() {
+        val queryFilters = QueryFilters()
+
+        ItemsQueryBuilder.buildItemsQuery(queryFilters)
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun filterFeedIdExceptionTest() {
+        val queryFilters = QueryFilters(accountId = 1, filterType = FilterType.FEED_FILTER)
+        ItemsQueryBuilder.buildItemsQuery(queryFilters)
+    }
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index 09457c85..70ede94b 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -11,9 +11,8 @@ android.useAndroidX=true
 org.gradle.jvmargs=-Xmx1536m
 android.databinding.incremental=true
 kapt.incremental.apt=true
-# When configured, Gradle will run in incubating parallel mode.
-# This option should only be used with decoupled projects. More details, visit
-# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
-# org.gradle.parallel=true
+org.gradle.parallel=true
+kotlin.parallel.tasks.in.project=true
+android.defaults.buildfeatures.buildconfig=false
 
 
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index ebe84a9e..0d90a675 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Fri Jul 10 17:43:55 CEST 2020
+#Fri Oct 23 19:12:43 CEST 2020
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip