diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 00000000..61a9130c
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index 440480e5..ed9c379e 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -1,9 +1,11 @@
+
diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
new file mode 100644
index 00000000..a5f05cd8
--- /dev/null
+++ b/.idea/jarRepositories.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 7bfef59d..d5d35ec4 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 00000000..94a25f7f
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 00000000..034e8480
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,21 @@
+# Security Policy
+
+## Supported Versions
+
+Use this section to tell people about which versions of your project are
+currently being supported with security updates.
+
+| Version | Supported |
+| ------- | ------------------ |
+| 5.1.x | :white_check_mark: |
+| 5.0.x | :x: |
+| 4.0.x | :white_check_mark: |
+| < 4.0 | :x: |
+
+## Reporting a Vulnerability
+
+Use this section to tell people how to report a vulnerability.
+
+Tell them where to go, how often they can expect to get an update on a
+reported vulnerability, what to expect if the vulnerability is accepted or
+declined, etc.
diff --git a/app/build.gradle b/app/build.gradle
index ed2ecfe3..0e6d2a60 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -5,13 +5,13 @@ apply plugin: 'kotlin-kapt'
apply plugin: "androidx.navigation.safeargs.kotlin"
android {
- compileSdkVersion 29
+ compileSdkVersion 30
buildToolsVersion "29.0.3"
defaultConfig {
applicationId "com.androiddevs.mvvmnewsapp"
minSdkVersion 21
- targetSdkVersion 29
+ targetSdkVersion 30
versionCode 1
versionName "1.0"
@@ -35,26 +35,26 @@ android {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
- implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
- implementation 'androidx.appcompat:appcompat:1.1.0'
- implementation 'androidx.core:core-ktx:1.2.0'
- implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
- testImplementation 'junit:junit:4.12'
- androidTestImplementation 'androidx.test.ext:junit:1.1.1'
- androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
+ implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.71'
+ implementation 'androidx.appcompat:appcompat:1.2.0'
+ implementation 'androidx.core:core-ktx:1.3.2'
+ implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
+ testImplementation 'junit:junit:4.13.1'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.2'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
// Architectural Components
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
// Room
- implementation "androidx.room:room-runtime:2.2.5"
- kapt "androidx.room:room-compiler:2.2.5"
+ implementation "androidx.room:room-runtime:2.2.6"
+ kapt "androidx.room:room-compiler:2.2.6"
// Kotlin Extensions and Coroutines support for Room
- implementation "androidx.room:room-ktx:2.2.5"
+ implementation "androidx.room:room-ktx:2.2.6"
// Coroutines
- implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.5'
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.5'
// Coroutine Lifecycle Scopes
@@ -67,8 +67,8 @@ dependencies {
implementation "com.squareup.okhttp3:logging-interceptor:4.5.0"
// Navigation Components
- implementation "androidx.navigation:navigation-fragment-ktx:2.2.1"
- implementation "androidx.navigation:navigation-ui-ktx:2.2.1"
+ implementation "androidx.navigation:navigation-fragment-ktx:2.3.2"
+ implementation "androidx.navigation:navigation-ui-ktx:2.3.2"
// Glide
implementation 'com.github.bumptech.glide:glide:4.11.0'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 0a2ed1dd..2cef5ac7 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -2,6 +2,8 @@
+
+
-
+
diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png
new file mode 100644
index 00000000..032b1bb1
Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ
diff --git a/app/src/main/java/adapters/NewsAdapter.kt b/app/src/main/java/adapters/NewsAdapter.kt
new file mode 100644
index 00000000..deaf583d
--- /dev/null
+++ b/app/src/main/java/adapters/NewsAdapter.kt
@@ -0,0 +1,65 @@
+package adapters
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.recyclerview.widget.AsyncListDiffer
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.RecyclerView
+import com.androiddevs.mvvmnewsapp.R
+import com.bumptech.glide.Glide
+import kotlinx.android.synthetic.main.item_article_preview.view.*
+import models.Article
+
+class NewsAdapter: RecyclerView.Adapter() {
+
+ inner class ArticleViewHolder(itemView: View): RecyclerView.ViewHolder(itemView)
+
+ private val differCallback = object : DiffUtil.ItemCallback(){
+
+ override fun areItemsTheSame(oldItem: Article, newItem: Article): Boolean {
+ return oldItem.url == newItem.url
+ }
+
+ override fun areContentsTheSame(oldItem: Article, newItem: Article): Boolean {
+ return oldItem == newItem
+ }
+ }
+
+ val differ = AsyncListDiffer(this, differCallback)
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ArticleViewHolder {
+ return ArticleViewHolder(
+ LayoutInflater.from(parent.context).inflate(
+ R.layout.item_article_preview,
+ parent,
+ false
+ )
+ )
+ }
+
+ override fun onBindViewHolder(holder: ArticleViewHolder, position: Int) {
+ val article = differ.currentList[position]
+ holder.itemView.apply {
+ Glide.with(this).load(article.urlToImage).into(ivArticleImage)
+ tvSource.text = article.source.name
+ tvTitle.text = article.title
+ tvDescription.text = article.description
+ tvPublishedAt.text = article.publishedAt
+ setOnClickListener{
+ onItemClickListener?.let{ it(article) }
+ }
+ }
+ }
+
+ override fun getItemCount(): Int {
+ return differ.currentList.size
+ }
+
+ private var onItemClickListener:((Article) -> Unit)? = null
+
+ fun setOnItemClickListener(listener: (Article) -> Unit){
+ onItemClickListener = listener
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/api/NewsAPI.kt b/app/src/main/java/api/NewsAPI.kt
new file mode 100644
index 00000000..fd25aeb5
--- /dev/null
+++ b/app/src/main/java/api/NewsAPI.kt
@@ -0,0 +1,20 @@
+package api
+
+import models.NewsResponse
+import retrofit2.Response
+import retrofit2.http.GET
+import retrofit2.http.Query
+import util.Constants.Companion.API_KEY
+
+interface NewsAPI {
+
+ @GET("v2/top-headlines")
+ suspend fun getBreakingNews(
+ @Query("country")
+ countryCode : String = "id",
+ @Query("page")
+ pageNumber:Int = 1,
+ @Query("apiKey")
+ apiKey: String = API_KEY
+ ) : Response
+}
\ No newline at end of file
diff --git a/app/src/main/java/api/RetrofitInstance.kt b/app/src/main/java/api/RetrofitInstance.kt
new file mode 100644
index 00000000..e4a2a122
--- /dev/null
+++ b/app/src/main/java/api/RetrofitInstance.kt
@@ -0,0 +1,22 @@
+package api
+
+import okhttp3.OkHttpClient
+import okhttp3.logging.HttpLoggingInterceptor
+import retrofit2.Retrofit
+import retrofit2.converter.gson.GsonConverterFactory
+import util.Constants.Companion.BASE_URL
+
+class RetrofitInstance {
+ companion object{
+ private val retrofit by lazy {
+ val logging = HttpLoggingInterceptor()
+ logging.setLevel(HttpLoggingInterceptor.Level.BODY)
+ val client = OkHttpClient.Builder().addInterceptor(logging).build()
+ Retrofit.Builder().baseUrl(BASE_URL).addConverterFactory(GsonConverterFactory.create()).client(client).build()
+ }
+
+ val api by lazy {
+ retrofit.create(NewsAPI::class.java)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/androiddevs/mvvmnewsapp/MainActivity.kt b/app/src/main/java/com/androiddevs/mvvmnewsapp/MainActivity.kt
deleted file mode 100644
index 3752ae31..00000000
--- a/app/src/main/java/com/androiddevs/mvvmnewsapp/MainActivity.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.androiddevs.mvvmnewsapp
-
-import androidx.appcompat.app.AppCompatActivity
-import android.os.Bundle
-
-class MainActivity : AppCompatActivity() {
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_main)
- }
-}
diff --git a/app/src/main/java/com/androiddevs/mvvmnewsapp/ui/NewsAcitivity.kt b/app/src/main/java/com/androiddevs/mvvmnewsapp/ui/NewsAcitivity.kt
new file mode 100644
index 00000000..0432b94a
--- /dev/null
+++ b/app/src/main/java/com/androiddevs/mvvmnewsapp/ui/NewsAcitivity.kt
@@ -0,0 +1,27 @@
+package com.androiddevs.mvvmnewsapp.ui
+
+import androidx.appcompat.app.AppCompatActivity
+import android.os.Bundle
+import androidx.lifecycle.ViewModelProvider
+import androidx.navigation.fragment.findNavController
+import androidx.navigation.ui.setupWithNavController
+import com.androiddevs.mvvmnewsapp.R
+import db.ArticleDatabase
+import kotlinx.android.synthetic.main.activity_news.*
+import repository.NewsRepository
+
+class NewsActivity : AppCompatActivity() {
+
+ lateinit var viewModel: NewsViewModel
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_news)
+
+ val newsRepository = NewsRepository(ArticleDatabase(this))
+ val viewModelProviderFactory = NewsViewModelProviderFactory(newsRepository)
+ viewModel = ViewModelProvider(this, viewModelProviderFactory).get(NewsViewModel::class.java)
+
+ bottomNavigationView.setupWithNavController(newsNavHostFragment.findNavController())
+ }
+}
diff --git a/app/src/main/java/com/androiddevs/mvvmnewsapp/ui/NewsViewModel.kt b/app/src/main/java/com/androiddevs/mvvmnewsapp/ui/NewsViewModel.kt
new file mode 100644
index 00000000..35862b8a
--- /dev/null
+++ b/app/src/main/java/com/androiddevs/mvvmnewsapp/ui/NewsViewModel.kt
@@ -0,0 +1,52 @@
+package com.androiddevs.mvvmnewsapp.ui
+
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.launch
+import models.Article
+import models.NewsResponse
+import repository.NewsRepository
+import retrofit2.Response
+import util.Resource
+
+class NewsViewModel(
+
+ private val newsRepository: NewsRepository
+
+) : ViewModel() {
+
+ val breakingNews: MutableLiveData> = MutableLiveData()
+ private var breakingNewsPage = 1
+
+ init {
+ getBreakingNews("id")
+ }
+
+ private fun getBreakingNews(countryCode: String) = viewModelScope.launch {
+ breakingNews.postValue(Resource.Loading())
+ val response = newsRepository.getBreakingNews(countryCode, breakingNewsPage)
+ breakingNews.postValue(handleBreakingNewsResponse(response))
+ }
+
+ private fun handleBreakingNewsResponse(response: Response) : Resource {
+ if(response.isSuccessful){
+ response.body()?.let { resultResponse ->
+ return Resource.Success(resultResponse)
+ }
+ }
+
+ return Resource.Error(response.message())
+ }
+
+ fun saveArticle(article: Article) = viewModelScope.launch {
+ newsRepository.upsert(article)
+ }
+
+ fun getSavedNews() = newsRepository.getSavedNews()
+
+ fun deleteArticle(article: Article) = viewModelScope.launch {
+ newsRepository.deleteArticle(article)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/androiddevs/mvvmnewsapp/ui/NewsViewModelProviderFactory.kt b/app/src/main/java/com/androiddevs/mvvmnewsapp/ui/NewsViewModelProviderFactory.kt
new file mode 100644
index 00000000..2d3607d9
--- /dev/null
+++ b/app/src/main/java/com/androiddevs/mvvmnewsapp/ui/NewsViewModelProviderFactory.kt
@@ -0,0 +1,14 @@
+package com.androiddevs.mvvmnewsapp.ui
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import repository.NewsRepository
+
+class NewsViewModelProviderFactory(
+ private val newsRepository : NewsRepository
+) : ViewModelProvider.Factory {
+
+ override fun create(modelClass: Class): T {
+ return NewsViewModel(newsRepository) as T
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/androiddevs/mvvmnewsapp/ui/fragments/ArticleFragment.kt b/app/src/main/java/com/androiddevs/mvvmnewsapp/ui/fragments/ArticleFragment.kt
new file mode 100644
index 00000000..d2208aca
--- /dev/null
+++ b/app/src/main/java/com/androiddevs/mvvmnewsapp/ui/fragments/ArticleFragment.kt
@@ -0,0 +1,33 @@
+package com.androiddevs.mvvmnewsapp.ui.fragments
+
+import android.os.Bundle
+import android.view.View
+import android.webkit.WebViewClient
+import androidx.fragment.app.Fragment
+import androidx.navigation.fragment.navArgs
+import com.androiddevs.mvvmnewsapp.R
+import com.androiddevs.mvvmnewsapp.ui.NewsActivity
+import com.androiddevs.mvvmnewsapp.ui.NewsViewModel
+import com.google.android.material.snackbar.Snackbar
+import kotlinx.android.synthetic.main.fragment_article.*
+
+class ArticleFragment: Fragment(R.layout.fragment_article) {
+
+ private lateinit var viewModel: NewsViewModel
+ private val args: ArticleFragmentArgs by navArgs()
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ viewModel = (activity as NewsActivity).viewModel
+ val article = args.article
+ webView.apply {
+ webViewClient = WebViewClient()
+ loadUrl(article.url)
+ }
+
+ fab.setOnClickListener{
+ viewModel.saveArticle(article)
+ Snackbar.make(view, "Artikel berhasil disimpan", Snackbar.LENGTH_SHORT).show()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/androiddevs/mvvmnewsapp/ui/fragments/BreakingNewsFragment.kt b/app/src/main/java/com/androiddevs/mvvmnewsapp/ui/fragments/BreakingNewsFragment.kt
new file mode 100644
index 00000000..c9e2865b
--- /dev/null
+++ b/app/src/main/java/com/androiddevs/mvvmnewsapp/ui/fragments/BreakingNewsFragment.kt
@@ -0,0 +1,80 @@
+package com.androiddevs.mvvmnewsapp.ui.fragments
+
+import adapters.NewsAdapter
+import android.os.Bundle
+import android.util.Log
+import android.view.View
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.Observer
+import androidx.navigation.fragment.findNavController
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.androiddevs.mvvmnewsapp.R
+import com.androiddevs.mvvmnewsapp.ui.NewsActivity
+import com.androiddevs.mvvmnewsapp.ui.NewsViewModel
+import kotlinx.android.synthetic.main.fragment_breaking_news.*
+import util.Resource
+
+class BreakingNewsFragment: Fragment(R.layout.fragment_breaking_news) {
+
+ private lateinit var viewModel : NewsViewModel
+ private lateinit var newsAdapter: NewsAdapter
+
+ private val TAG = "BreakingNewsFragment"
+
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+ viewModel = (activity as NewsActivity).viewModel
+
+ setupRecyclerView()
+
+ newsAdapter.setOnItemClickListener {
+ val bundle = Bundle().apply {
+ putSerializable("article", it)
+ }
+
+ findNavController().navigate(
+ R.id.action_breakingNewsFragment_to_articleFragment,
+ bundle
+ )
+ }
+
+ viewModel.breakingNews.observe(viewLifecycleOwner, Observer {
+ response -> when(response){
+ is Resource.Success -> {
+ hideProgressBar()
+ response.data?.let{
+ newsResponse -> newsAdapter.differ.submitList(newsResponse.articles)
+ }
+ }
+
+ is Resource.Error -> {
+ hideProgressBar()
+ response.message?.let{
+ message -> Log.e(TAG, "An error occured: $message")
+ }
+ }
+
+ is Resource.Loading -> {
+ showProgressBar()
+ }
+ }
+ })
+
+ }
+
+ private fun hideProgressBar() {
+ paginationProgressBar.visibility = View.INVISIBLE
+ }
+
+ private fun showProgressBar(){
+ paginationProgressBar.visibility = View.VISIBLE
+ }
+
+ private fun setupRecyclerView(){
+ newsAdapter = NewsAdapter()
+ rvBreakingNews.apply {
+ adapter = newsAdapter
+ layoutManager = LinearLayoutManager(activity)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/androiddevs/mvvmnewsapp/ui/fragments/SavedNewsFragment.kt b/app/src/main/java/com/androiddevs/mvvmnewsapp/ui/fragments/SavedNewsFragment.kt
new file mode 100644
index 00000000..3982187d
--- /dev/null
+++ b/app/src/main/java/com/androiddevs/mvvmnewsapp/ui/fragments/SavedNewsFragment.kt
@@ -0,0 +1,82 @@
+package com.androiddevs.mvvmnewsapp.ui.fragments
+
+import adapters.NewsAdapter
+import android.os.Bundle
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.Observer
+import androidx.navigation.fragment.findNavController
+import androidx.recyclerview.widget.ItemTouchHelper
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.androiddevs.mvvmnewsapp.R
+import com.androiddevs.mvvmnewsapp.ui.NewsActivity
+import com.androiddevs.mvvmnewsapp.ui.NewsViewModel
+import com.google.android.material.snackbar.Snackbar
+import kotlinx.android.synthetic.main.fragment_saved_news.*
+
+class SavedNewsFragment: Fragment(R.layout.fragment_saved_news) {
+
+ lateinit var viewModel: NewsViewModel
+ lateinit var newsAdapter: NewsAdapter
+
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+
+ viewModel = (activity as NewsActivity).viewModel
+ setupRecyclerView()
+
+ newsAdapter.setOnItemClickListener {
+ val bundle = Bundle().apply {
+ putSerializable("article", it)
+ }
+
+ findNavController().navigate(
+ R.id.action_savedNewsFragment_to_articleFragment,
+ bundle
+ )
+ }
+
+ val itemTouchHelperCallback = object : ItemTouchHelper.SimpleCallback(
+ ItemTouchHelper.UP or ItemTouchHelper.DOWN,
+ ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
+ ) {
+ override fun onMove(
+ recyclerView: RecyclerView,
+ viewHolder: RecyclerView.ViewHolder,
+ target: RecyclerView.ViewHolder
+ ): Boolean {
+ return true
+ }
+
+ override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
+ val position = viewHolder.adapterPosition
+ val article = newsAdapter.differ.currentList[position]
+ viewModel.deleteArticle(article)
+ view?.let {
+ Snackbar.make(it, "Artikel berhasil dihapus", Snackbar.LENGTH_LONG).apply {
+ setAction("Undo"){
+ viewModel.saveArticle(article)
+ }
+ show()
+ }
+ }
+ }
+ }
+
+ ItemTouchHelper(itemTouchHelperCallback).apply {
+ attachToRecyclerView(rvSavedNews)
+ }
+
+ viewModel.getSavedNews().observe(viewLifecycleOwner, Observer {
+ articles -> newsAdapter.differ.submitList(articles)
+ })
+ }
+
+ private fun setupRecyclerView(){
+ newsAdapter = NewsAdapter()
+ rvSavedNews.apply {
+ adapter = newsAdapter
+ layoutManager = LinearLayoutManager(activity)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/db/ArticleDao.kt b/app/src/main/java/db/ArticleDao.kt
new file mode 100644
index 00000000..a179f8ac
--- /dev/null
+++ b/app/src/main/java/db/ArticleDao.kt
@@ -0,0 +1,17 @@
+package db
+
+import androidx.lifecycle.LiveData
+import androidx.room.*
+import models.Article
+
+@Dao
+interface ArticleDao {
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun upsert(articleDao: Article): Long
+
+ @Query("SELECT * FROM articles")
+ fun getAllArticles() : LiveData>
+
+ @Delete
+ suspend fun deleteArticle(article: Article)
+}
\ No newline at end of file
diff --git a/app/src/main/java/db/ArticleDatabase.kt b/app/src/main/java/db/ArticleDatabase.kt
new file mode 100644
index 00000000..928de6a9
--- /dev/null
+++ b/app/src/main/java/db/ArticleDatabase.kt
@@ -0,0 +1,34 @@
+package db
+
+import android.content.Context
+import androidx.room.*
+import models.Article
+
+@Database(
+ entities = [Article::class],
+ version = 1
+)
+
+@TypeConverters(Converters::class)
+
+abstract class ArticleDatabase : RoomDatabase() {
+
+ abstract fun getArticleDao(): ArticleDao
+
+ companion object{
+ @Volatile
+ private var instance: ArticleDatabase? = null
+ private val LOCK = Any()
+
+ operator fun invoke(context: Context) = instance ?: synchronized(LOCK) {
+ instance ?: createDatabase(context).also { instance = it }
+ }
+
+ private fun createDatabase(context: Context) =
+ Room.databaseBuilder(
+ context.applicationContext,
+ ArticleDatabase::class.java,
+ "article_db.db"
+ ).build()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/db/Converters.kt b/app/src/main/java/db/Converters.kt
new file mode 100644
index 00000000..8eebdf9c
--- /dev/null
+++ b/app/src/main/java/db/Converters.kt
@@ -0,0 +1,17 @@
+package db
+
+import androidx.room.TypeConverter
+import models.Source
+
+class Converters {
+
+ @TypeConverter
+ fun fromSource(source: Source): String{
+ return source.name
+ }
+
+ @TypeConverter
+ fun toSource(name: String): Source {
+ return Source(name, name)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/models/Article.kt b/app/src/main/java/models/Article.kt
new file mode 100644
index 00000000..a7f4ab5d
--- /dev/null
+++ b/app/src/main/java/models/Article.kt
@@ -0,0 +1,22 @@
+package models
+
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import java.io.Serializable
+
+@Entity(
+ tableName = "articles"
+)
+
+data class Article(
+ @PrimaryKey(autoGenerate = true)
+ val id: Int? = null,
+ val author: String,
+ val content: String,
+ val description: String,
+ val publishedAt: String,
+ val source: Source,
+ val title: String,
+ val url: String,
+ val urlToImage: String
+) : Serializable
\ No newline at end of file
diff --git a/app/src/main/java/models/NewsResponse.kt b/app/src/main/java/models/NewsResponse.kt
new file mode 100644
index 00000000..3ef71797
--- /dev/null
+++ b/app/src/main/java/models/NewsResponse.kt
@@ -0,0 +1,7 @@
+package models
+
+data class NewsResponse(
+ val articles: List,
+ val status: String,
+ val totalResults: Int
+)
\ No newline at end of file
diff --git a/app/src/main/java/models/Source.kt b/app/src/main/java/models/Source.kt
new file mode 100644
index 00000000..1b0a3ad9
--- /dev/null
+++ b/app/src/main/java/models/Source.kt
@@ -0,0 +1,6 @@
+package models
+
+data class Source(
+ val id: Any,
+ val name: String
+)
\ No newline at end of file
diff --git a/app/src/main/java/repository/NewsRepository.kt b/app/src/main/java/repository/NewsRepository.kt
new file mode 100644
index 00000000..e460b19b
--- /dev/null
+++ b/app/src/main/java/repository/NewsRepository.kt
@@ -0,0 +1,18 @@
+package repository
+
+import api.RetrofitInstance
+import db.ArticleDatabase
+import models.Article
+
+class NewsRepository(
+ val db: ArticleDatabase
+) {
+ suspend fun getBreakingNews(countryCode: String, pageNumber: Int) =
+ RetrofitInstance.api.getBreakingNews(countryCode, pageNumber)
+
+ suspend fun upsert(article: Article) = db.getArticleDao().upsert(article)
+
+ fun getSavedNews() = db.getArticleDao().getAllArticles()
+
+ suspend fun deleteArticle(article: Article) = db.getArticleDao().deleteArticle(article)
+}
\ No newline at end of file
diff --git a/app/src/main/java/util/Constants.kt b/app/src/main/java/util/Constants.kt
new file mode 100644
index 00000000..f24c653a
--- /dev/null
+++ b/app/src/main/java/util/Constants.kt
@@ -0,0 +1,9 @@
+package util
+
+class Constants {
+
+ companion object{
+ const val API_KEY = "b76c2f36b6fb4e76a083fc8d8d9aea99"
+ const val BASE_URL = "https://newsapi.org/"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/util/Resource.kt b/app/src/main/java/util/Resource.kt
new file mode 100644
index 00000000..6e76af16
--- /dev/null
+++ b/app/src/main/java/util/Resource.kt
@@ -0,0 +1,10 @@
+package util
+
+sealed class Resource(
+ val data:T ?= null,
+ val message: String? = null
+) {
+ class Success(data: T):Resource(data)
+ class Error(message: String, data: T? = null) : Resource(data, message)
+ class Loading : Resource()
+}
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_news.xml
similarity index 76%
rename from app/src/main/res/layout/activity_main.xml
rename to app/src/main/res/layout/activity_news.xml
index ef503e9b..471488cc 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_news.xml
@@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
- tools:context=".MainActivity">
+ tools:context=".ui.NewsActivity">
-
+
@@ -24,6 +30,7 @@
android:id="@+id/bottomNavigationView"
android:layout_width="match_parent"
android:layout_height="56dp"
+ app:menu="@menu/bottom_navigation_menu"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
diff --git a/app/src/main/res/layout/fragment_article.xml b/app/src/main/res/layout/fragment_article.xml
index 6b37df8d..569f9694 100644
--- a/app/src/main/res/layout/fragment_article.xml
+++ b/app/src/main/res/layout/fragment_article.xml
@@ -1,7 +1,6 @@
diff --git a/app/src/main/res/layout/fragment_breaking_news.xml b/app/src/main/res/layout/fragment_breaking_news.xml
index 9b294093..1c98c7b6 100644
--- a/app/src/main/res/layout/fragment_breaking_news.xml
+++ b/app/src/main/res/layout/fragment_breaking_news.xml
@@ -1,32 +1,44 @@
+ app:layout_constraintTop_toBottomOf="@+id/tvFavouriteNewsFragmentTitle" />
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_saved_news.xml b/app/src/main/res/layout/fragment_saved_news.xml
index c5b88192..9a6bddcf 100644
--- a/app/src/main/res/layout/fragment_saved_news.xml
+++ b/app/src/main/res/layout/fragment_saved_news.xml
@@ -1,12 +1,30 @@
-
+
+
+ android:layout_height="0dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/tvFavouriteNewsFragmentTitle" />
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_search_news.xml b/app/src/main/res/layout/fragment_search_news.xml
deleted file mode 100644
index 4b55fdd2..00000000
--- a/app/src/main/res/layout/fragment_search_news.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_article_preview.xml b/app/src/main/res/layout/item_article_preview.xml
index b8b33382..0765bf76 100644
--- a/app/src/main/res/layout/item_article_preview.xml
+++ b/app/src/main/res/layout/item_article_preview.xml
@@ -1,10 +1,9 @@
+ android:padding="16dp">
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
index eca70cfe..036d09bc 100644
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -1,5 +1,5 @@
-
-
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
index eca70cfe..036d09bc 100644
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -1,5 +1,5 @@
-
-
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
index a571e600..f0f6fd93 100644
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
new file mode 100644
index 00000000..3af4f84f
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
index 61da551c..079e82fb 100644
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
index c41dd285..2eeb0517 100644
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
new file mode 100644
index 00000000..687c7182
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
index db5080a7..198cd490 100644
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
index 6dba46da..669bc5d8 100644
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 00000000..6c630aa8
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
index da31a871..70b3527e 100644
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
index 15ac6817..af33c6f5 100644
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 00000000..78568f5a
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
index b216f2d3..9f36709e 100644
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
index f25a4197..ca93d3f4 100644
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 00000000..efe10288
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
index e96783cc..fcecee49 100644
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/navigation/news_nav_graph.xml b/app/src/main/res/navigation/news_nav_graph.xml
new file mode 100644
index 00000000..85df6ad6
--- /dev/null
+++ b/app/src/main/res/navigation/news_nav_graph.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 030098fe..c5405728 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -1,6 +1,6 @@
- #6200EE
- #3700B3
+ #0087EE
+ #0027B3
#03DAC5
diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 00000000..c5d5899f
--- /dev/null
+++ b/app/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+ #FFFFFF
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index b610a268..101ad856 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,3 +1,3 @@
- MVVMNewsApp
+ Halo Indonesia
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 5885930d..0eb88fe3 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -1,7 +1,7 @@
-