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 17 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:$accompanistCoilVersion"

// 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
)
}
15 changes: 15 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,12 @@ import javax.inject.Singleton
*/
interface NewsRepository {

/**
* Gets the particular article from database for
* the given [articleId]
*/
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 +52,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 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 @@ -4,6 +4,7 @@ import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Transaction
import com.akshay.newsapp.news.storage.NewsDatabaseMigration.V2.NewsArticle
import com.akshay.newsapp.news.storage.entity.NewsArticleDb
import kotlinx.coroutines.flow.Flow

Expand All @@ -28,6 +29,12 @@ interface NewsArticlesDao {
insertArticles(articles)
}

/**
* Get news article for the specified [articleId]
*/
@Query("SELECT * FROM ${NewsArticle.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,130 @@
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
) {
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
) {
Text(
text = message,
style = MaterialTheme.typography.body1
)
}
}

@Preview(showBackground = true)
@Composable
fun errorViewPreview() {
errorView(message = "Something went wrong!")
}
Loading