Basic SQLite database
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
Toldi Balázs Ádám 2021-10-23 14:01:47 +02:00
parent 652e2f2f32
commit af94e16f4e
12 changed files with 317 additions and 78 deletions

View file

@ -1,6 +1,7 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
}
android {
@ -47,6 +48,15 @@ android {
}
dependencies {
def room_version = "2.3.0"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
kapt "androidx.room:room-compiler:$room_version"
// optional - RxJava2 support for Room
implementation "androidx.room:room-rxjava2:$room_version"
def lifecycle_version = "2.4.0-rc01"
implementation 'androidx.core:core-ktx:1.6.0'

View file

@ -5,6 +5,7 @@ import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures
@ -16,6 +17,7 @@ import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -30,14 +32,13 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.ViewModelProvider
import coil.compose.rememberImagePainter
import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import eu.toldi.balazs.anotherfeedreader.entities.Article
import eu.toldi.balazs.anotherfeedreader.entities.Feed
import eu.toldi.balazs.anotherfeedreader.entities.FeedData
import eu.toldi.balazs.anotherfeedreader.entities.FeedGroup
import eu.toldi.balazs.anotherfeedreader.sqlite.Repo
import eu.toldi.balazs.anotherfeedreader.ui.theme.AnotherFeedReaderTheme
import eu.toldi.balazs.anotherfeedreader.viewmodel.FeedViewModel
import kotlinx.coroutines.Dispatchers.IO
@ -46,40 +47,31 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import java.net.URL
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
class MainActivity : ComponentActivity() {
val feeds = FeedData().apply {
addFeed(FeedGroup("magyar").apply {
addFeed(Feed(URL("https://hvg.hu/rss")))
addFeed(Feed(URL("https://telex.hu/rss")))
addFeed(Feed(URL("https://24.hu/feed")))
addFeed(Feed(URL("https://444.hu/feed")))
})
addFeed(Feed(URL("https://www.theguardian.com/world/rss")))
}
lateinit var viewModel: FeedViewModel
private val viewModel: FeedViewModel by viewModels()
var syncJob: Job? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AnotherFeedReaderTheme {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
viewModel = ViewModelProvider(this).get(FeedViewModel::class.java).apply {
feedData = feeds
}
viewModel.updateFeed()
viewModel.changeFeedList()
viewModel.updateFeeds()
var isAddFeedDialogOpen by remember {
mutableStateOf(false)
}
val scope = rememberCoroutineScope()
val drawerState = rememberDrawerState(DrawerValue.Closed)
val articleList = viewModel.articles.value
val articleList = viewModel.articles.observeAsState(emptyList())
Scaffold(
floatingActionButton = {
FloatingActionButton(
@ -102,7 +94,7 @@ class MainActivity : ComponentActivity() {
SwipeRefresh(
state = rememberSwipeRefreshState(isRefreshing),
onRefresh = {
viewModel.updateFeed()
viewModel.updateFeeds()
},
) {
LazyColumn(
@ -111,8 +103,8 @@ class MainActivity : ComponentActivity() {
.fillMaxWidth()
) {
items(articleList.size) { index ->
val article = articleList[index]
items(articleList.value.size) { index ->
val article = articleList.value[index]
showArticle(article = article)
}
}
@ -137,7 +129,7 @@ class MainActivity : ComponentActivity() {
Text(text = "Add feed")
},
text = {
Column() {
Column {
OutlinedTextField(
value = text,
onValueChange = {
@ -157,7 +149,8 @@ class MainActivity : ComponentActivity() {
.wrapContentSize(Alignment.TopStart)
) {
Text(
text = if (selectedIndex == 0) "None" else feeds.feedList.filterIsInstance<FeedGroup>()[selectedIndex - 1].name.toString(),
text = if (selectedIndex == 0) "None" else viewModel.feedsState.value.filterIsInstance<FeedGroup>()[selectedIndex - 1].name.toString(),
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = { expanded = true })
@ -173,13 +166,13 @@ class MainActivity : ComponentActivity() {
}) {
Text(text = "None")
}
feeds.feedList.filterIsInstance<FeedGroup>()
viewModel.feedsState.value.filterIsInstance<FeedGroup>()
.forEachIndexed { index, s ->
DropdownMenuItem(onClick = {
selectedIndex = index + 1
expanded = false
}) {
feeds.feedList[index].name?.let {
viewModel.feedsState.value[index].name?.let {
Text(text = it)
}
}
@ -205,10 +198,10 @@ class MainActivity : ComponentActivity() {
val feed = Feed(URL(text))
syncJob = GlobalScope.launch(IO) { feed.updateFeed() }
if (selectedIndex == 0)
feeds.addFeed(feed)
viewModel.addFeed(feed)
else {
var counter = 0
feeds.feedList.forEachIndexed { index, feedGroup ->
viewModel.feedsState.value.forEachIndexed { index, feedGroup ->
if (feedGroup is FeedGroup) {
counter++
if (counter == selectedIndex) {
@ -217,7 +210,6 @@ class MainActivity : ComponentActivity() {
}
}
}
viewModel.feedData = feeds
viewModel.changeFeedList()
} catch (e: Exception) {
@ -303,7 +295,7 @@ class MainActivity : ComponentActivity() {
WebView.webURL = article.url.toString()
WebView.barTitle =
article.feed.name.toString()
article.feed?.name.toString() ?: ""
startActivity(
Intent(
baseContext,
@ -322,7 +314,7 @@ class MainActivity : ComponentActivity() {
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {
val painter = if (article.feed.faviconURL != null) {
val painter = if (article.feed?.faviconURL != null) {
rememberImagePainter(article.feed.faviconURL)
} else {
painterResource(id = R.drawable.ic_launcher_background)
@ -345,7 +337,7 @@ class MainActivity : ComponentActivity() {
modifier = Modifier.padding(horizontal = 2.dp)
)
}
article.feed.name?.let {
article.feed?.name?.let {
Text(
text = (fun(): String {
return if (it.length > 25)
@ -382,7 +374,7 @@ class MainActivity : ComponentActivity() {
}
if (article.title != null) {
Text(
text = article.title.trim(),
text = article.title!!.trim(),
fontSize = 20.sp,
maxLines = 2,
overflow = TextOverflow.Ellipsis
@ -390,7 +382,7 @@ class MainActivity : ComponentActivity() {
} else Text("")
if (article.description != null) {
Text(
text = article.description,
text = article.description!!,
fontSize = 17.sp,
maxLines = 2,
overflow = TextOverflow.Ellipsis
@ -411,9 +403,9 @@ class MainActivity : ComponentActivity() {
.fillMaxWidth()
.padding(vertical = 5.dp, horizontal = 4.dp)
.clickable {
feeds.limit = if (feeds.limit == feed) {
/*feeds.limit = if (feeds.limit == feed) {
null
} else feed
} else feed*/
viewModel.changeFeed()
},
elevation = 10.dp,
@ -456,9 +448,9 @@ class MainActivity : ComponentActivity() {
.fillMaxWidth()
.padding(start = 25.dp, top = 5.dp, end = 4.dp, bottom = 5.dp)
.clickable {
feeds.limit = if (feeds.limit == it) {
/*feeds.limit = if (feeds.limit == it) {
null
} else it
} else it*/
viewModel.changeFeed()
},
elevation = 10.dp,
@ -493,7 +485,7 @@ class MainActivity : ComponentActivity() {
.fillMaxWidth()
.padding(vertical = 5.dp, horizontal = 4.dp)
.clickable {
feeds.limit = null
//feeds.limit = null
viewModel.changeFeed()
},
elevation = 10.dp,
@ -522,11 +514,11 @@ class MainActivity : ComponentActivity() {
AnotherFeedReaderTheme {
Column() {
MenuBar({})
val article = Article(
val article = Article(null,
title = "Teszt Article",
description = "Ez az cikk azért létezik, hogy kipróbájam a cikkek megjelenítését.",
author = "Toldi Balázs",
pubDate = LocalDateTime.now(),
pubDate_stamp = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC),
feed = Feed("Test 1")
)
Column(
@ -534,11 +526,12 @@ class MainActivity : ComponentActivity() {
) {
showArticle(article)
showArticle(
Article(
Article(null,
title = "Teszt Article #2",
description = "Ez egy másik megjelenítést tesztelő cikk. Ebben egy nagyon hosszú szöveget akarok ábrázolni, aminek nem szabad egyben kiférnie. Legfeljebb 2 sort jeleníthet meg",
author = "Toldi Balázs",
pubDate = LocalDateTime.now().plusDays(-1),
pubDate_stamp = LocalDateTime.now().plusDays(-1).toEpochSecond(
ZoneOffset.UTC),
feed = Feed("Test 1")
)
)

View file

@ -1,5 +1,9 @@
package eu.toldi.balazs.anotherfeedreader.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@ -9,40 +13,53 @@ import org.w3c.dom.Element
import org.w3c.dom.Node
import org.w3c.dom.NodeList
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
class Article(
@Entity(tableName = "article")
data class Article(
@PrimaryKey(autoGenerate= true) var articleId: Long?,
/**
* A Cikk címe
*/
val title: String? = null,
@ColumnInfo(name = "title")
var title: String? = null,
/**
* A cíkk elérési útvonala
*/
val url: String? = null,
@ColumnInfo(name = "url")
var url: String? = null,
/**
* A cikkhez tartozó leírás
*/
val description: String? = null,
@ColumnInfo(name = "description")
var description: String? = null,
/**
* Cikkhez tartozó kép elérési útvonala
*/
val imageURL: String? = null,
@ColumnInfo(name = "imageURL")
var imageURL: String? = null,
/**
* A cikk szerzője
*/
val author: String? = null,
val pubDate: LocalDateTime? = null,
val feed: Feed
) {
@ColumnInfo(name = "author")
var author: String? = null,
/**
* A cikk kiadásának dátuma
*/
@ColumnInfo(name = "pubDate")
var pubDate_stamp : Long?,
@Ignore
val feed: Feed?
) {
@Ignore
val pubDate : LocalDateTime?
init {
pubDate = pubDate_stamp?.let { LocalDateTime.ofEpochSecond(it,0, ZoneOffset.UTC) }
}
override fun toString(): String {
return "Article{" +
"title='" + title + '\'' +
@ -53,7 +70,12 @@ class Article(
", pubDate=" + pubDate +
'}'
}
constructor() : this(null,"","","","","",null,null){
}
@ColumnInfo(name = "")
var feedId: Long = 0
companion object {
/**
* Cikk beolvasása egy megadott Node objektumból. Egy hírcsatorna beolvasásakor könnyedén alkalmazható egy "item" Node-ra
@ -93,7 +115,7 @@ class Article(
}
}
if (imageURL == null && url != null) {
runBlocking (IO) {
runBlocking(IO) {
val con = Jsoup.connect(url)
val doc = con.userAgent("Mozilla").get()
@ -108,7 +130,8 @@ class Article(
}
}
}
return Article(title, url, description, imageURL, author, pubDate, feed)
return Article(null,title, url, description, imageURL, author, pubDate?.toEpochSecond(
ZoneOffset.UTC), feed)
}
}

View file

@ -1,7 +1,14 @@
package eu.toldi.balazs.anotherfeedreader.entities
import android.util.Log
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
import eu.toldi.balazs.anotherfeedreader.MainActivity
import eu.toldi.balazs.anotherfeedreader.sqlite.Repo
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers.IO
import net.mm2d.touchicon.TouchIconExtractor
import org.w3c.dom.Document
import org.w3c.dom.Node
@ -10,23 +17,29 @@ import java.net.URL
import javax.xml.parsers.DocumentBuilder
import javax.xml.parsers.DocumentBuilderFactory
@Entity
open class Feed
(
@PrimaryKey(autoGenerate = true)
var feedId: Long? = null,
/**
* A hírcsatorna elérési útvonala
*/
val link: URL
@ColumnInfo(name = "feedURL")
var feedURL: String
) {
@ColumnInfo(name = "faviconURL")
var faviconURL: String? = null
/**
* A hírcsatorna neve
*/
@ColumnInfo(name = "title")
var name: String? = null
@Ignore
val link = URL(feedURL)
constructor(name: String) : this(URL("http://example.com")) {
constructor(name: String) : this(null,"http://example.com") {
this.name = name
}
@ -70,7 +83,6 @@ open class Feed
if (doc.getElementsByTagName("title").length > 0)
name = doc.getElementsByTagName("title").item(0).getTextContent()
val items: NodeList = doc.getElementsByTagName("item")
for (i in 0 until items.length) {
val article: Node = items.item(i)
val a = withContext(Dispatchers.IO) {Article.createFromNode(article, this@Feed)}

View file

@ -0,0 +1,58 @@
package eu.toldi.balazs.anotherfeedreader.repository
import androidx.lifecycle.LiveData
import eu.toldi.balazs.anotherfeedreader.entities.Article
import eu.toldi.balazs.anotherfeedreader.entities.Feed
import eu.toldi.balazs.anotherfeedreader.entities.FeedData
import eu.toldi.balazs.anotherfeedreader.entities.FeedGroup
import eu.toldi.balazs.anotherfeedreader.sqlite.ArticleDAO
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.withContext
import java.net.URL
class FeedRepository(private val articleDAO: ArticleDAO) {
val feedData = FeedData().apply {
addFeed(FeedGroup("magyar").apply {
addFeed(Feed(feedURL = "https://hvg.hu/rss"))
addFeed(Feed(feedURL = "https://telex.hu/rss"))
addFeed(Feed(feedURL = "https://24.hu/feed"))
addFeed(Feed(feedURL = "https://444.hu/feed"))
})
addFeed(Feed(feedURL = "https://www.theguardian.com/world/rss"))
}
fun getAllArticles(): LiveData<List<Article>> {
return articleDAO.getAll()
}
suspend fun instert(article: Article){
articleDAO.insertAll(article)
}
suspend fun delete(article: Article){
articleDAO.delete(article)
}
fun getFeedList() : List<Feed> = feedData.feedList
suspend fun updateFeeds() {
var done = false
feedData.limit?.let {
it.updateFeed()
done = true
}
if (!done)
feedData.updateFeed()
feedData.articleList.forEach {
withContext(IO){
instert(it)
}
}
}
fun addFeed(feed: Feed) {
feedData.addFeed(feed)
}
}

View file

@ -0,0 +1,28 @@
package eu.toldi.balazs.anotherfeedreader.sqlite
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import eu.toldi.balazs.anotherfeedreader.entities.Article
@Database(entities = [Article::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun articleDao(): ArticleDAO
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getInstance(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
INSTANCE ?: Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"feed_database"
).fallbackToDestructiveMigration().build().also { INSTANCE = it }
}
}
}
}

View file

@ -0,0 +1,26 @@
package eu.toldi.balazs.anotherfeedreader.sqlite
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import eu.toldi.balazs.anotherfeedreader.entities.Article
@Dao
interface ArticleDAO {
@Query("SELECT * FROM article")
fun getAll(): LiveData<List<Article>>
@Query("SELECT * FROM article WHERE articleId IN (:articleIds)")
fun loadAllByIds(articleIds: IntArray): List<Article>
@Query("SELECT * FROM article WHERE url=:url")
fun findByName(url: String): Article?
@Insert
fun insertAll(vararg articles: Article)
@Delete
fun delete(article: Article)
}

View file

@ -0,0 +1,26 @@
package eu.toldi.balazs.anotherfeedreader.sqlite
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import eu.toldi.balazs.anotherfeedreader.entities.Feed
@Dao
interface FeedDAO {
@Query("SELECT * FROM feed")
fun getAll(): LiveData<List<Feed>>
@Query("SELECT * FROM feed WHERE feedId IN (:feedIds)")
fun loadAllByIds(feedIds: IntArray): List<Feed>
@Query("SELECT * FROM article WHERE url=:url")
fun findByName(url: String): Feed?
@Insert
fun insertAll(vararg feeds: Feed)
@Delete
fun delete(feed: Feed)
}

View file

@ -0,0 +1,15 @@
package eu.toldi.balazs.anotherfeedreader.sqlite
import androidx.room.Embedded
import androidx.room.Relation
import eu.toldi.balazs.anotherfeedreader.entities.Article
import eu.toldi.balazs.anotherfeedreader.entities.Feed
data class FeedsAndArticles(
@Embedded val feed: Feed,
@Relation(
parentColumn = "feedId",
entityColumn = "feedId"
)
val articles: List<Article>
)

View file

@ -0,0 +1,25 @@
package eu.toldi.balazs.anotherfeedreader.sqlite
import androidx.lifecycle.LiveData
import androidx.room.*
import eu.toldi.balazs.anotherfeedreader.entities.Article
@Dao
interface FeedsAndArticlesDAO {
@Transaction
@Query("SELECT * FROM feed")
fun getAll(): LiveData<List<FeedsAndArticles>>
@Query("SELECT * FROM article WHERE articleId IN (:articleIds)")
fun loadAllByIds(articleIds: IntArray): List<Article>
@Query("SELECT * FROM article WHERE url=:url")
fun findArticleByURL(url: String): Article?
@Insert
fun insertArticles(vararg articles: Article)
@Delete
fun deleteArticle(article: Article)
}

View file

@ -0,0 +1,16 @@
package eu.toldi.balazs.anotherfeedreader.sqlite
import android.content.Context
import androidx.room.Room
object Repo {
lateinit var db: AppDatabase
fun initDb(context: Context) {
db = Room.databaseBuilder(
context,
AppDatabase::class.java, "article"
).build()
}
}

View file

@ -1,40 +1,42 @@
package eu.toldi.balazs.anotherfeedreader.viewmodel
import android.app.Application
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import eu.toldi.balazs.anotherfeedreader.entities.Article
import eu.toldi.balazs.anotherfeedreader.entities.Feed
import eu.toldi.balazs.anotherfeedreader.entities.FeedData
import eu.toldi.balazs.anotherfeedreader.entities.FeedGroup
import eu.toldi.balazs.anotherfeedreader.repository.FeedRepository
import eu.toldi.balazs.anotherfeedreader.sqlite.AppDatabase
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import java.net.URL
class FeedViewModel : ViewModel() {
class FeedViewModel(application: Application) : AndroidViewModel(application) {
var feedData = FeedData()
val articles: MutableState<List<Article>> = mutableStateOf(listOf())
private val feedRepo: FeedRepository
val articles: LiveData<List<Article>>
val feedsState = mutableStateOf(emptyList<Feed>())
val isRefereshing: MutableState<Boolean> = mutableStateOf(false)
init {
val articleDAO = AppDatabase.getInstance(application).articleDao()
feedRepo = FeedRepository(articleDAO = articleDAO)
articles = feedRepo.getAllArticles()
viewModelScope.launch {
articles.value = feedData.articleList
feedsState.value = feedData.feedList
feedsState.value = feedRepo.getFeedList()
}
}
fun updateFeed(): Job {
fun updateFeeds(): Job {
return viewModelScope.launch {
isRefereshing.value = true
var done = false
feedData.limit?.let {
it.updateFeed()
done = true
}
if (!done)
feedData.updateFeed()
articles.value = feedData.articleList
feedRepo.updateFeeds()
isRefereshing.value = false
}
}
@ -42,14 +44,19 @@ class FeedViewModel : ViewModel() {
fun changeFeed() {
viewModelScope.launch {
isRefereshing.value = true
articles.value = feedData.articleList
///articles.value = feedRepo.getFeedList()
isRefereshing.value = false
}
}
fun changeFeedList() {
viewModelScope.launch {
feedsState.value = feedData.feedList
feedsState.value = feedRepo.getFeedList()
}
}
fun addFeed(feed: Feed){
feedRepo.addFeed(feed)
feedsState.value = feedRepo.getFeedList()
}
}