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 @@ -