Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New details screen using Jetpack compose #50

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
6850b2b
Initial dependency setup for Jetpack compose
rohithThammaiah Oct 18, 2020
fba2d09
Enabled `ViewBinding`
rohithThammaiah Oct 25, 2020
15f83d5
Migrated `NewsActivity` screen to ViewBinding
rohithThammaiah Oct 25, 2020
c60b094
Migrated `NewsHolder` item to ViewBinding
rohithThammaiah Oct 25, 2020
9c79fab
Merge branch 'migrate/viewBinding' into feature/detailsScreen
rohithThammaiah Oct 25, 2020
acbe215
Added function to fetch particular article from database
rohithThammaiah Oct 25, 2020
6d7de5d
Navigating to new `NewsDetailsActivity` on click of a news item
rohithThammaiah Oct 25, 2020
05e7a08
Using Compose's `ActionBar` instead of Activity's
rohithThammaiah Oct 25, 2020
cbd141e
Added views for loading and error states in Details screen
rohithThammaiah Oct 26, 2020
713cf81
Added previews for loading & error view
rohithThammaiah Oct 26, 2020
8f7bbad
Matched style of loadingIndicator with `progressBarStyleLarge`
rohithThammaiah Oct 26, 2020
fa0bf31
Created a new constant `accompanistCoilVersion` for accompanist depen…
rohithThammaiah Oct 29, 2020
b63f574
Fixed typo in doc for `getNewsArticle`
rohithThammaiah Oct 29, 2020
7d46a28
Fixed typo in comment
rohithThammaiah Oct 29, 2020
8686255
Fixed comment typo & using constant value for table name in sql query
rohithThammaiah Oct 29, 2020
2df855c
Code clean-up in `NewsDetailsActivity`
rohithThammaiah Oct 29, 2020
350e1d2
Updated doc of `AppTheme.NoActionBar`
rohithThammaiah Nov 2, 2020
5fbc54e
Merge pull request #1 from AkshayChordiya/master
rohithThammaiah Nov 3, 2020
f15d8b8
Merge branch 'master' into feature/detailsScreen
rohithThammaiah Nov 3, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,20 @@ android {
exclude "**.kotlin_builtins"
exclude "**.kotlin_module"
}

buildFeatures {
viewBinding true
compose true
}

kotlinOptions {
jvmTarget = '1.8'
}

composeOptions {
kotlinCompilerVersion kotlinVersion
kotlinCompilerExtensionVersion composeVersion
}
}

kapt {
Expand Down Expand Up @@ -102,13 +116,22 @@ dependencies {

// Coil
implementation "io.coil-kt:coil:$coilVersion"
implementation "dev.chrisbanes.accompanist:accompanist-coil:0.3.1"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's best to keep the version as a constant in project/build.gradle


// Hilt + Dagger
implementation "com.google.dagger:hilt-android:$hiltAndroidVersion"
kapt "com.google.dagger:hilt-android-compiler:$hiltAndroidVersion"
implementation "androidx.hilt:hilt-lifecycle-viewmodel:$hiltViewModelVersion"
kapt "androidx.hilt:hilt-compiler:$hiltViewModelVersion"

// Jetpack Compose
implementation "androidx.compose.runtime:runtime:$composeVersion"
implementation "androidx.compose.ui:ui:$composeVersion"
implementation "androidx.ui:ui-tooling:$composeVersion"
implementation "androidx.compose.foundation:foundation:$composeVersion"
implementation "androidx.compose.material:material:$composeVersion"
implementation "androidx.compose.runtime:runtime-livedata:$composeVersion"

// KTX
implementation "androidx.core:core-ktx:$coreKtxVersion"
implementation "androidx.fragment:fragment-ktx:$fragmentKtxVersion"
Expand All @@ -125,3 +148,15 @@ dependencies {
// Debug
implementation "com.jakewharton.timber:timber:$timberVersion"
}

tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
kotlinOptions {
freeCompilerArgs += '-Xopt-in=kotlin.RequiresOptIn'
// Enable experimental coroutines APIs, including Flow
freeCompilerArgs += '-Xopt-in=kotlin.Experimental'
freeCompilerArgs += '-Xallow-jvm-ir-dependencies'

// Set JVM target to 1.8
jvmTarget = "1.8"
}
}
3 changes: 3 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".news.ui.activity.NewsDetailsActivity"
android:theme="@style/AppTheme.NoActionBar"/>
</application>

</manifest>
7 changes: 7 additions & 0 deletions app/src/main/java/com/akshay/newsapp/core/ui/compose/Color.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.akshay.newsapp.core.ui.compose

import androidx.compose.ui.graphics.Color

val colorPrimary = Color(0xFF121258)
val colorPrimaryDark = Color(0xFF0F0F4A)
val colorAccent = Color(0xFFFFC039)
35 changes: 35 additions & 0 deletions app/src/main/java/com/akshay/newsapp/core/ui/compose/Theme.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.akshay.newsapp.core.ui.compose

import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material.MaterialTheme
import androidx.compose.material.MaterialTheme.shapes
import androidx.compose.material.darkColors
import androidx.compose.material.lightColors
import androidx.compose.runtime.Composable

private val DarkColorPalette = darkColors(
primary = colorPrimary,
primaryVariant = colorPrimaryDark,
secondary = colorAccent
)

private val LightColorPalette = lightColors(
primary = colorPrimary,
primaryVariant = colorPrimaryDark,
secondary = colorAccent
)

@Composable
fun NewsTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable() () -> Unit) {
val colors = if (darkTheme) {
DarkColorPalette
} else {
LightColorPalette
}

MaterialTheme(
colors = colors,
shapes = shapes,
content = content
)
}
16 changes: 16 additions & 0 deletions app/src/main/java/com/akshay/newsapp/news/domain/NewsRepository.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ import javax.inject.Singleton
*/
interface NewsRepository {

/**
* Gets the particular article from database for
* the give articleId
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor typo

Suggested change
* the give articleId
* the given [articleId]

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can skip the @param code comment

* @param articleId id from NewsArticleDb
*/
fun getNewsArticle(articleId: Int): Flow<ViewState<NewsArticleDb>>

/**
* Gets tne cached news article from database and tries to get
* fresh news articles from web and save into database
Expand All @@ -46,6 +53,15 @@ class DefaultNewsRepository @Inject constructor(
private val newsService: NewsService
) : NewsRepository, NewsMapper {

override fun getNewsArticle(articleId: Int): Flow<ViewState<NewsArticleDb>> = flow {
// 1. Start with loading
emit(ViewState.loading())

// 2. Fetch and update state particular article from database
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// 2. Fetch and update state particular article from database
// 2. Fetch the news article

val article = newsDao.getNewsArticle(articleId = articleId)
emitAll(article.map { ViewState.success(it) })
}.flowOn(Dispatchers.IO)

override fun getNewsArticles(): Flow<ViewState<List<NewsArticleDb>>> = flow {
// 1. Start with loading
emit(ViewState.loading())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ interface NewsArticlesDao {
insertArticles(articles)
}

/**
* Get News article for given articleId
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Get News article for given articleId
* Get news article for the specified [articleId]

*/
@Query("SELECT * FROM news_article WHERE id = :articleId")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor suggestion to use constant

Suggested change
@Query("SELECT * FROM news_article WHERE id = :articleId")
@Query("SELECT * FROM ${NewsArticles.tableName} WHERE id = :articleId")

fun getNewsArticle(articleId: Int): Flow<NewsArticleDb>

/**
* Get all the articles from table
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,44 +1,54 @@
package com.akshay.newsapp.news.ui.activity

import android.content.Intent
import android.os.Bundle
import androidx.activity.viewModels
import androidx.recyclerview.widget.LinearLayoutManager
import com.akshay.newsapp.R
import com.akshay.newsapp.core.ui.ViewState
import com.akshay.newsapp.core.ui.base.BaseActivity
import com.akshay.newsapp.core.utils.observeNotNull
import com.akshay.newsapp.core.utils.toast
import com.akshay.newsapp.databinding.ActivityMainBinding
import com.akshay.newsapp.news.ui.adapter.NewsArticlesAdapter
import com.akshay.newsapp.news.ui.model.NewsAdapterEvent
import com.akshay.newsapp.news.ui.viewmodel.NewsArticleViewModel
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.empty_layout.*
import kotlinx.android.synthetic.main.progress_layout.*


class NewsActivity : BaseActivity() {

private val newsArticleViewModel: NewsArticleViewModel by viewModels()

private lateinit var binding: ActivityMainBinding

/**
* Starting point of the activity
*/
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)

// Setting up RecyclerView and adapter
newsList.setEmptyView(empty_view)
newsList.setProgressView(progress_view)

val adapter = NewsArticlesAdapter { toast("Clicked on item") }
newsList.adapter = adapter
newsList.layoutManager = LinearLayoutManager(this)
binding.newsList.setEmptyView(binding.emptyLayout.emptyView)
binding.newsList.setProgressView(binding.progressLayout.progressView)

val adapter = NewsArticlesAdapter {
when (it) {
is NewsAdapterEvent.ClickEvent -> {
val intent = Intent(this, NewsDetailsActivity::class.java)
intent.putExtra(NEWS_ARG_ARTICLE_ID, it.newsArticle.id)
startActivity(intent)
}
}
}
binding.newsList.adapter = adapter
binding.newsList.layoutManager = LinearLayoutManager(this)

// Update the UI on state change
newsArticleViewModel.getNewsArticles().observeNotNull(this) { state ->
when (state) {
is ViewState.Success -> adapter.submitList(state.data)
is ViewState.Loading -> newsList.showLoading()
is ViewState.Loading -> binding.newsList.showLoading()
is ViewState.Error -> toast("Something went wrong ¯\\_(ツ)_/¯ => ${state.message}")
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package com.akshay.newsapp.news.ui.activity

import android.os.Bundle
import androidx.activity.viewModels
import androidx.compose.foundation.Icon
import androidx.compose.foundation.ScrollableColumn
import androidx.compose.foundation.Text
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.setContent
import androidx.compose.ui.unit.dp
import androidx.ui.tooling.preview.Preview
import com.akshay.newsapp.R
import com.akshay.newsapp.core.ui.ViewState
import com.akshay.newsapp.core.ui.base.BaseActivity
import com.akshay.newsapp.core.ui.compose.NewsTheme
import com.akshay.newsapp.news.storage.entity.NewsArticleDb
import com.akshay.newsapp.news.ui.viewmodel.NewsArticleViewModel
import dev.chrisbanes.accompanist.coil.CoilImage

const val NEWS_ARG_ARTICLE_ID = "articleId"

class NewsDetailsActivity : BaseActivity() {

private val articleId: Int by lazy {
intent.getIntExtra(NEWS_ARG_ARTICLE_ID, -1)
}

private val newsArticleViewModel: NewsArticleViewModel by viewModels()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
NewsTheme {
Scaffold(topBar = {
TopAppBar(
title = {},
backgroundColor = MaterialTheme.colors.primary,
navigationIcon = { IconButton(onClick = { finish() }) {
Icon(Icons.Filled.ArrowBack) }
}
)
}, bodyContent = {
newsDetailsScreen(newsArticleViewModel = newsArticleViewModel, articleId)
})
}
}
}
}

@Composable
fun newsDetailsScreen(newsArticleViewModel: NewsArticleViewModel, newsId: Int) {
val viewState by newsArticleViewModel.getNewsArticle(articleId = newsId).observeAsState(ViewState.loading())
when (viewState) {
is ViewState.Loading -> {
loadingIndicator()
}
is ViewState.Error -> {
errorView((viewState as ViewState.Error<NewsArticleDb>).message)
}
is ViewState.Success -> {
ScrollableColumn(modifier = Modifier.padding(horizontal = 16.dp)) {
with((viewState as ViewState.Success<NewsArticleDb>).data) {
Spacer(modifier = Modifier.preferredHeight(8.dp))
CoilImage(data = urlToImage ?: R.drawable.tools_placeholder,
modifier = Modifier
.heightIn(min = 180.dp)
.fillMaxWidth()
.clip(shape = MaterialTheme.shapes.medium)
)
Spacer(Modifier.preferredHeight(16.dp))
Text(text = title ?: "", style = MaterialTheme.typography.h6)
Spacer(Modifier.preferredHeight(8.dp))
Text(text = content ?: "", style = MaterialTheme.typography.body1)
}
}
}
}
}

@Preview(showBackground = true)
@Composable
fun loadingIndicator() {
Column(modifier = Modifier.fillMaxWidth().fillMaxHeight(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor suggestion to put all parameters on new line for cleaner look

Suggested change
Column(modifier = Modifier.fillMaxWidth().fillMaxHeight(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally) {
Column(
modifier = Modifier.fillMaxWidth().fillMaxHeight(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {

CircularProgressIndicator(
color = MaterialTheme.colors.secondary,
strokeWidth = 6.dp,
modifier = Modifier.preferredSize(64.dp)
)
}
}

@Composable
fun errorView(message: String) {
Column(modifier = Modifier.fillMaxWidth().fillMaxHeight(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same suggestion as above to put all parameters on new line

Text(text = message, style = MaterialTheme.typography.body1)
}
}

@Preview(showBackground = true)
@Composable
fun errorViewPreview() {
errorView(message = "Something went wrong!")
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import coil.api.load
import coil.load
import com.akshay.newsapp.R
import com.akshay.newsapp.core.utils.inflate
import com.akshay.newsapp.databinding.RowNewsArticleBinding
import com.akshay.newsapp.news.storage.entity.NewsArticleDb
import com.akshay.newsapp.news.ui.model.NewsAdapterEvent
import kotlinx.android.synthetic.main.row_news_article.view.*

/**
* The News adapter to show the news in a list.
Expand All @@ -34,20 +34,22 @@ class NewsArticlesAdapter(
*/
class NewsHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

private val binding = RowNewsArticleBinding.bind(itemView)

/**
* Binds the UI with the data and handles clicks
*/
fun bind(newsArticle: NewsArticleDb, listener: (NewsAdapterEvent) -> Unit) = with(itemView) {
newsTitle.text = newsArticle.title
newsAuthor.text = newsArticle.author
binding.newsTitle.text = newsArticle.title
binding.newsAuthor.text = newsArticle.author
//TODO: need to format date
//tvListItemDateTime.text = getFormattedDate(newsArticle.publishedAt)
newsPublishedAt.text = newsArticle.publishedAt
newsImage.load(newsArticle.urlToImage) {
binding.newsPublishedAt.text = newsArticle.publishedAt
binding.newsImage.load(newsArticle.urlToImage) {
placeholder(R.drawable.tools_placeholder)
error(R.drawable.tools_placeholder)
}
setOnClickListener { listener(NewsAdapterEvent.ClickEvent) }
setOnClickListener { listener(NewsAdapterEvent.ClickEvent(newsArticle = newsArticle)) }
}
}

Expand Down
Loading