HomeAndroidEasy RSS Feed Reader - Jetpack Compose

Easy RSS Feed Reader – Jetpack Compose


That is the very first Jetpack Compose Android app that I constructed. It’s a easy app that reads my weblog’s rss.xml and shops them in a neighborhood database. You possibly can bookmark articles, mark articles as learn, share articles and search articles by title. It reveals the total article content material inside the app.

Android_News_Overview.gif

Excessive-level Structure

That is the high-level structure design which is predicated on MVVM / advisable Android app structure.

Simple_RSS_Feed_Reader_Jetpack_Compose.drawio.png

As you could already know, the UI occasion flows downward and information flows upward by way of callback or Move. The dependency course can also be a method from UI layer to Knowledge layer.

The next desk summarizes the duty of all of the elements in UI, area and information layers.

UI Layer Accountability
MainActivity Constructs MainViewModel and all its dependencies comparable to ArticlesRepositoryImpl, ArticlesDatabase and WebService
MainScreen Setup high bar and backside bar navigation, construct navigation graph, setup snack bar UI show
HomeScreen Acts as begin vacation spot display which lists all of the articles from rss.xml. Supplies the flexibility to bookmark, share, mark as unread on every article, add search articles characteristic at high bar
UnreadScreen Lists all unread articles right here
BookmarkScreen Lists all bookmarked articles right here
SearchScreen Reveals the article search outcomes
MainViewModel Supplies UI states (information wanted by all of the composable capabilities), acquire flows from ArticlesRepository, refresh the articles in ArticlesRepository
Area Layer Accountability
ArticlesRepository Acts as interface between UI layer and information layer. Supplies area information mannequin (articles data) to the UI layer by way of Move
Knowledge Layer Accountability
ArticlesRepositoryImpl Implements the ArticlesRepository interface, fetches articles from WebService and write into the ArticlesDatabase, map and rework native information to area information
ArticlesDatabase Implements native RoomDatabase which acts as single supply of fact
WebServce Fetches XML string utilizing ktor shopper, parses the XML feed and converts the XML to distant information (which is remodeled to native information for native database writing)

Necessary be aware: This app is beneath heavy improvement. So, the knowledge supplied on this article could also be outdated. For instance, I’ve

  • Modified the app to make use of a number of view fashions as a substitute of a single view mannequin

  • Added usecase lessons within the area layer and transfer all repository-related lessons (together with the ArticlesRepository interface) from the area to the info layer.

  • Applied Proto DataStore to retailer consumer preferences as a substitute of utilizing the identical ArticlesDatabase room database.

Implementation Particulars

I simply spotlight the high-level implementations which are price mentioning. The supply code proven right here might not be full. For particulars, please discuss with the supply code immediately.

High and Backside App Bars

The highest and backside app bars are applied utilizing Scaffold composable perform.

@Composable
enjoyable MainScreen(viewModel: MainViewModel, useSystemUIController: Boolean) {
    
    val scaffoldState = rememberScaffoldState()
    val navHostController = rememberNavController()

    Scaffold(
        scaffoldState = scaffoldState,
        topBar = { TopBar(navHostController, viewModel) },
        bottomBar = { BottomBarNav(navHostController) }
    ) {
        NavGraph(viewModel, navHostController)
    }
    
}

Navigation Graph

The navigation graph implementation is similar to what I did on this article:

The display navigation again stack seems to be like this.

Simple_RSS_Feed_Reader_Navigation_Backstack.drawio.png

HomeScreen is the beginning vacation spot which navigates to totally different screens. As a result of the underside navigation can navigate from and to any display, calling popUpTo(NavRoute.Dwelling.path) us to make sure the again stack is all the time 2-level depth.

@Composable
non-public enjoyable BottomNavigationItem() {
    
    val chosen = currentNavRoutePath == targetNavRoutePath
    rowScope.BottomNavigationItem(
        
        onClick = {
            if(!chosen) {
                navHostController.navigate(targetNavRoutePath) {
                    popUpTo(NavRoute.Dwelling.path) {
                        inclusive = (targetNavRoutePath == NavRoute.Dwelling.path)
                    }
                }
            }
        },
        
    )
}

For backside navigation implementation, you may discuss with this text:

Picture Loading

For picture loading, I used the rememberImagePainter() composable perform from the coil picture loading library.

@Composable
non-public enjoyable ArticleImage(article: Article) {
    Picture(
        painter = rememberImagePainter(
            information = article.picture,
            builder = {
                placeholder(R.drawable.loading_animation)
            }
        ),
        contentScale = ContentScale.Crop,
        contentDescription = "",
        modifier = Modifier
            .measurement(150.dp, 150.dp)
            .clip(MaterialTheme.shapes.medium)
    )
}

coil is the one picture loading libary that helps Jetpack Compose so far as I do know

There may be this landscapist library that wraps round different image-loading libraries for Jetpack Compose, however I do not know if there are any benefits of utilizing it.

XML Fetching and Parsing

To fetch the XML remotely, I take advantage of Ktor Shopper library, which is the multiplatform asynchronous HTTP shopper. The implementation is tremendous easy right here.

class WebService {

    droop enjoyable getXMlString(url: String): String {
        val shopper = HttpClient()
        val response: HttpResponse = shopper.request(url)
        shopper.shut()
        return response.physique()
    }
}

The difficulty with utilizing Ktor Shopper might be its efficiency. Based mostly on the little expertise I did within the following article, it runs 2x slower!

Nonetheless, it’s not a direct comparability, as this utilization is fairly easy. It does not use Kotlin Serialization which probably is the primary problem right here. Nicely, that is one thing for me to experiment sooner or later.

[Updated – Jan 15, 2023]: Ktor Shopper throws the next exception on API 21

java.util.concurrent.ExecutionException: java.lang.NoClassDefFoundError: io.ktor.util.collections.ConcurrentMap$$ExternalSyntheticLambda0

To workaround this problem, I take advantage of the OkHttpClient.

interface WebService {
    droop enjoyable getXMlString(url: String): String
}

class OkHttpWebService : WebService {

    override droop enjoyable getXMlString(url: String): String {
        val shopper = OkHttpClient()
        val request: Request = Request.Builder()
            .url(url)
            .construct()
        var response = shopper.newCall(request).execute()

        return response.physique?.string() ?: ""
    }
}

Please be aware that I’ve extracted out the WebService as an interface as I wish to preserve each Ktor Shopper and OkHttp Shopper implementations.

To parse the XML, I used the XmlPullParser library. FeedPaser.parse() is the high-level implementation. It converts the XML string to Record<ArticleFeed>.

class FeedParser {

    non-public val pullParserFactory = XmlPullParserFactory.newInstance()
    non-public val parser = pullParserFactory.newPullParser()

    enjoyable parse(xml: String): Record<ArticleFeed> {

        parser.setInput(xml.byteInputStream(), null)

        val articlesFeed = mutableListOf<ArticleFeed>()
        var feedTitle = ""

        whereas (parser.eventType != XmlPullParser.END_DOCUMENT) {

            if (parser.eventType  == XmlPullParser.START_TAG && parser.title == "title") {
                feedTitle = readText(parser)

            } else if (parser.eventType  == XmlPullParser.START_TAG && parser.title == "merchandise") {
                val feedItem = readFeedItem(parser)
                val articleFeed = ArticleFeed(
                    feedItem = feedItem,
                    feedTitle = feedTitle)
                articlesFeed.add(articleFeed)
            }
            parser.subsequent()
        }

        return articlesFeed
    }
    
}

Native SQLite Database

I used the Room database library from Android Jetpack to construct the SQLite native database. The utilization is fairly commonplace, so I am not going to speak about it. As a substitute, I share with you what I did a bit in another way within the following.

As a substitute of exhausting coding the desk title, I declare a singleton under.

object DatabaseConstants {
    const val ARTICLE_TABLE_NAME = "article"
}

Then, I take advantage of it in ArticleEntity

@Entity(tableName = DatabaseConstants.ARTICLE_TABLE_NAME)
information class ArticleEntity(
    @PrimaryKey(autoGenerate = true)
    val id: Int,
    val title: String,
    val hyperlink: String,
    val creator: String,
    val pubDate: Lengthy,
    val picture: String,
    val bookmarked: Boolean,
    val learn: Boolean,

    val feedTitle: String,
)

and in addition in ArticlesDao interface.

@Dao
interface ArticlesDao {
    @Question("SELECT * FROM ${DatabaseConstants.ARTICLE_TABLE_NAME} ORDER by pubDate DESC")
    enjoyable selectAllArticles(): Move<Record<ArticleEntity>>

    
}

One other drawback I confronted is deleting all of the articles doesn’t reset the auto-increment of the first key. To repair this, I have to bypass Room and run SQL question immediately utilizing runSqlQuery() to delete the sqlite_sequence.

@Database(
    model = 1,
    entities = [ArticleEntity::class],
    exportSchema = false)
summary class ArticlesDatabase : RoomDatabase() {
    protected summary val dao: ArticlesDao
    
    enjoyable deleteAllArticles() {
        dao.deleteAllArticles()
        
        runSqlQuery("DELETE FROM sqlite_sequence WHERE title="${DatabaseConstants.ARTICLE_TABLE_NAME}"")
    }
    
}

Article Display

By proper, I ought to be capable of construct the article display from the feed’s information, however I took the shortcut to implement an in-app net browser utilizing WebView. I simply have to wrap it contained in the AndroidView composable perform.

@Composable
non-public enjoyable ArticleWebView(url: String) {

    if (url.isEmpty()) {
        return
    }

    Column {

        AndroidView(manufacturing unit = {
            WebView(it).apply {
                webViewClient = WebViewClient()
                loadUrl(url)
            }
        })
    }
}

It is extremely easy, is not it? The disadvantage is it does not assist offline view. I did attempt to work round by loading the HTML as a substitute of URL, however no luck.

Swipe Refresh

To refresh the articles, I take advantage of the Swipe Refresh library from Accompanist to name MainViewModel.refresh() whenever you swipe down the display.

@Composable
enjoyable ArticlesScreen() {
    
    SwipeRefresh(
        state = rememberSwipeRefreshState(viewModel.isRefreshing),
        onRefresh = { viewModel.refresh() }
    ) {
        
    }
}

[Updated – Jan 2, 2023]: Swipe refresh library from Accompanish is deprecated and changed by Modifier.pullRefresh() in androidx.compose.materials library.

After the migration, the code seems to be like this.

@OptIn(ExperimentalMaterialApi::class)
@Composable
enjoyable ArticlesScreen(
    onReadClick: (Article) -> Unit,
) {
    
    val pullRefreshState = rememberPullRefreshState(
        viewModel.isRefreshing, 
        viewModel.Refresh)

    Field(Modifier.pullRefresh(pullRefreshState)) {
        

        PullRefreshIndicator(
            viewModel.isRefreshing, 
            pullRefreshState, 
            Modifier.align(Alignment.TopCenter))
    }
}

Knowledge Mapper

Article is the area information utilized by the UI layer. ArticleEntity is the native database information and ArticleFeed is the distant information within the information layer. The next Kotlin’s extension capabilities are used to implement this information mapping / transformation:

  • ArticleFeed.asArticleEntity()

  • ArticleEnitty.asArticle()

  • Article.asArticleEntity()

Simple_RSS_Feed_Reader_Data_Mapper.drawio.png

To retailer ArticleFeed into the ArticlesDatabase(single supply of fact), ArticleFeed is required to be transformed or mapped to ArticleEntity first.

To show the Article from ArticlesDatabse, ArticleEntity is required to be transformed or mapped to Article first.

To replace the ArticlesDatabase (e.g. bookmark the article), Article is required to be transformed or mapped to the ArticleEntity first.

That is asArticle() extension perform for instance (which additionally contains the Record<ArticleEntity> -> Record<Article> transformation):

enjoyable Record<ArticleEntity>.asArticles() : Record<Article> {
    return map { articleEntity ->
        articleEntity.asArticle()
    }
}

enjoyable ArticleEntity.asArticle(): Article {
    return Article(
        id = id,
        title = title,
        hyperlink = hyperlink,
        creator = creator,
        pubDate = pubDate,
        picture = picture,
        bookmarked = bookmarked,
        learn = learn,

        feedTitle = feedTitle,
    )
}

Splash Display

[Updated – Jan 29, 2023]: Added this splash display implementation into this app.

WorkManager and Notification

[Updated – Feb 11, 2023]: Applied a background job utilizing WorkManager to synch the most recent articles and submit a notification when new articles arrived.

These are the high-level steps to schedule the work request that may be finished in onCreate() in your Utility()

  1. Set the work constraints (required web connection)

  2. Create the periodic work request (that runs each 24 hours)

  3. Enqueue a periodic work request utilizing WorkManager

class AndroidNewsApplication: Utility() {

    override enjoyable onCreate() {
        tremendous.onCreate()
        
        val constraints = Constraints.Builder()
            .setRequiredNetworkType(NetworkType.CONNECTED)
            .construct()

        
        val syncWorkRequest = PeriodicWorkRequestBuilder<SyncWorker>(
            24, 
            TimeUnit.HOURS
        )
            .setConstraints(constraints)
            .construct()

        
        val workManager = WorkManager.getInstance(this)
        workManager.enqueueUniquePeriodicWork(
            "SyncWorker",
            ExistingPeriodicWorkPolicy.REPLACE,
            syncWorkRequest)
    }
}

For a extra detailed instance, you may discuss with the next article.

Folder Construction

The high-level folder construction seems to be like this, which is organized by layer.

Simple_RSS_Feed_Reader_Jetpack_Compose_01.png

Since it is a easy app, organizing by layer is smart to me. For extra particulars about organizing Android package deal folder construction, discuss with this text.

Unit and Instrumented Checks

I didn’t write plenty of testing right here. The unit check merely checks all articles in MainViewModel should not null.

@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidJUnit4::class)
class HomeViewModelTest {

    non-public lateinit var viewModel: AllArticlesViewModel

    @get:Rule
    var instantExecutorRule = InstantTaskExecutorRule()

    @Earlier than
    enjoyable setupViewModel() {
        val repository = FakeArticlesRepositoryImpl()
        viewModel = AllArticlesViewModel(repository)
    }

    @Check
    enjoyable allArticles_areNotNull() = runTest {

        Assert.assertNotEquals(null, viewModel.articles.first())

        delay(1000)
        Assert.assertNotEquals(null, viewModel.articles)
    }
}

FakeArticlesRepositoryImpl implementation could be discovered right here.

For the instrumented check, I simply checked the package deal title and the underside navigation names.

@RunWith(AndroidJUnit4::class)
class AppContextTest {
    @Check
    enjoyable useAppContext() {
        val appContext = InstrumentationRegistry.getInstrumentation().targetContext
        assertEquals("vtsen.hashnode.dev.androidnews", appContext.packageName)
    }
}
class ComposeTest {

    @get:Rule
    val composeTestRule = createAndroidComposeRule<MainActivity>()

    @Check
    enjoyable bottomNavigationNames_areValid() {
        var textual content = composeTestRule.exercise.getString(R.string.house)
        composeTestRule.onNodeWithText(textual content).assertExists()

        textual content = composeTestRule.exercise.getString(R.string.unread_articles)
        composeTestRule.onNodeWithText(textual content).assertExists()

        textual content = composeTestRule.exercise.getString(R.string.bookmarks)
        composeTestRule.onNodeWithText(textual content).assertExists()
    }
}

Future Work

One mistake I made is naming conversion of a composable perform, that I did not begin with a noun. That is quoted from Compose API tips

@Composable annotation utilizing PascalCase, and the title MUST be that of a noun, not a verb or verb phrase, nor a nouned preposition, adjective or adverb. Nouns MAY be prefixed by descriptive adjectives.

For instance, BuildNavGraph() must be renamed to NavGraph(). It’s a element / widget, not an motion. It should not begin with a verb BuildXxx.

I additionally tried to transform the MainViewModel to make use of hilt dependency inject. I documented the steps I did on this article:

Since that is my first Jetpack Compose app, I am certain there may be room for enchancment. All of the potential enhancements that may be finished for this app is documented within the GitHub’s points right here.

Perhaps you may obtain and set up the app and let me know any feedbacks?

google-play-badge.png

Supply Code

GitHub Repository:

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments