We will be starting with a new series where I’ll create the news app, the project is quite big so it will take around six videos to complete it, okay?
Also, we will be covering five major advanced topics such as MVVM, Retrofit, Navigation Components, Coroutines, and Room Databases in one single project. So, I highly recommend doing a revision of each topic before starting with the project.
Topic Videos:
Alright, let’s have a look at the demo.
This is how it looks!
I have kept the project very minimal and useful.
We have a bottom navigation with three categories – headlines, favorites, and search.
As you can see in the headlines the latest news is shown here. All, of them, are fetched from news API in real-time.
You, can even click on it and see it opens up the article in a web view, great right?
You can scroll through it and you can save it in your favorites section by clicking on this floating acting button.
Look, it’s saved here in favorites – not, just that but you can also remove it from favorites by swiping it and you can undo it as well, right?
Then, go to the search section.
Here, you can literally search anything it will come up with the results – see, there it is!
All, of these, might look very easy to you and it is indeed very easy…it’s, just that we have to write a lot of code for them.
Now, let’s go into the technical side of it.
To, display all these articles are using news API which I’ll show you later. So, with the help of that, it fetches the real-time data or news and displays it on the recycler view.
Then, with the help of web view – we, can open the article in the app itself without going to any browser.
Then, by clicking on add to fav – that, particular article gets saved in the room database so what you see here is all saved in the room database hence later whenever you open the app again – they, will be still here.
Lastly, search uses API only, and other topics like MVVM will help us to make the project organized and segregated in model view and ViewModel not, just that but also the navigation component will help us to navigate from one screen to another screen effortlessly.
Note: This is my practice project which works the same way as the original one but please copy only the required code because copying the entire code of gradle or manifest will lead to errors related to the project package name.
colors.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <color name="black">#FF000000</color> <color name="white">#FFFFFFFF</color> <color name="purple">#593c8f</color> <color name="yellow">#FFC000</color> </resources>
themes.xml
<resources xmlns:tools="http://schemas.android.com/tools"> <!-- Base application theme. --> <style name="Base.Theme.NewsProjectPractice" parent="Theme.Material3.DayNight.NoActionBar"> <!-- Customize your light theme here. --> <item name="colorPrimary">@color/purple</item> <item name="colorPrimaryVariant">@color/purple</item> <item name="android:statusBarColor">@color/purple</item> </style> <style name="Theme.NewsProjectPractice" parent="Base.Theme.NewsProjectPractice" /> <style name="App.Custom.Indicator" parent="Widget.Material3.BottomNavigationView.ActiveIndicator"> <item name="android:color">@color/yellow</item> </style> </resources>
Drawables:
search_border.xml
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <stroke android:color="@color/purple" android:width="2dp"/> <corners android:radius="14dp"/> </shape>
build gradle (Project)
// Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { repositories { google() } dependencies { val navVersion = "2.7.5" classpath("androidx.navigation:navigation-safe-args-gradle-plugin:$navVersion") } } plugins { id("com.android.application") version "8.1.1" apply false id("org.jetbrains.kotlin.android") version "1.9.0" apply false id("com.google.devtools.ksp") version "1.9.0-1.0.13" apply false }
build gradle (Module)
plugins { id("com.android.application") id("org.jetbrains.kotlin.android") id("com.google.devtools.ksp") id("androidx.navigation.safeargs.kotlin") } android { namespace = "com.example.newsprojectpractice" compileSdk = 34 defaultConfig { applicationId = "com.example.newsprojectpractice" minSdk = 28 targetSdk = 33 versionCode = 1 versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } buildTypes { release { isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } } compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = "1.8" } buildFeatures{ viewBinding = true } } dependencies { implementation("androidx.core:core-ktx:1.12.0") implementation("androidx.appcompat:appcompat:1.6.1") implementation("com.google.android.material:material:1.10.0") implementation("androidx.constraintlayout:constraintlayout:2.1.4") testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") // Architectural Components implementation ("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2") // Room implementation ("androidx.room:room-runtime:2.6.0") ksp ("androidx.room:room-compiler:2.6.0") // Kotlin Extensions and Coroutines support for Room implementation ("androidx.room:room-ktx:2.6.0") // Coroutines implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1") implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1") // Coroutine Lifecycle Scopes implementation ("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2") implementation ("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2") // Retrofit implementation ("com.squareup.retrofit2:retrofit:2.9.0") implementation ("com.squareup.retrofit2:converter-gson:2.9.0") implementation ("com.squareup.okhttp3:logging-interceptor:4.5.0") // Navigation Components implementation ("androidx.navigation:navigation-fragment-ktx:2.7.5") implementation ("androidx.navigation:navigation-ui-ktx:2.7.5") // Glide implementation ("com.github.bumptech.glide:glide:4.12.0") ksp ("com.github.bumptech.glide:compiler:4.12.0") }
bottom_navigation_menu.xml
<?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@+id/headlinesFragment" android:title="Headlines" android:icon="@drawable/baseline_headlines_24" /> <item android:id="@+id/favouritesFragment" android:title="Favourites" android:icon="@drawable/baseline_favorite_24" /> <item android:id="@+id/searchFragment" android:title="Search" android:icon="@drawable/baseline_search_24" /> </menu>
Packages and Fragments
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.INTERNET"/> <application android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.NewsProjectPractice" tools:targetApi="31"> <activity android:name=".ui.NewsActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <meta-data android:name="preloaded_fonts" android:resource="@array/preloaded_fonts" /> </application> </manifest>
fragment_article.xml
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent"> <WebView android:id="@+id/webView" android:layout_width="match_parent" android:layout_height="match_parent" /> <com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/fab" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/baseline_favorite_24" app:fabSize="normal" android:layout_marginBottom="32dp" android:layout_marginEnd="32dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
fragment_favorites.xml
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Favourites." android:textSize="30sp" android:fontFamily="@font/poppins" android:textColor="@color/purple" android:textStyle="bold" android:layout_marginStart="8dp" android:layout_marginTop="12dp"/> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recyclerFavourites" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginTop="60dp" android:layout_marginStart="8dp"/> </FrameLayout>
item_error.xml
<?xml version="1.0" encoding="utf-8"?> <androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" app:cardCornerRadius="10dp"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <TextView android:id="@+id/errorText" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_margin="16dp" android:layout_weight="2" android:textSize="16sp" android:gravity="center_horizontal" android:text="No Internet" android:textAlignment="center" /> <Button android:id="@+id/retryButton" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_margin="16dp" android:layout_weight="1" android:text="Retry" /> </LinearLayout> </androidx.cardview.widget.CardView>
fragment_headlines.xml
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Headlines." android:textSize="30sp" android:fontFamily="@font/poppins" android:textColor="@color/purple" android:textStyle="bold" android:layout_marginStart="8dp" android:layout_marginTop="12dp" app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent"/> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recyclerHeadlines" android:layout_width="match_parent" android:layout_height="match_parent" android:clipToPadding="false" android:paddingBottom="50dp" android:layout_marginTop="70dp" android:layout_marginStart="8dp" android:layout_marginEnd="8dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <include android:id="@+id/itemHeadlinesError" layout="@layout/item_error" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="16dp" android:visibility="invisible" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <ProgressBar android:id="@+id/paginationProgressBar" style="?attr/progressBarStyle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:visibility="invisible" android:background="@android:color/transparent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toStartOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
fragment_search.xml
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Search." android:textSize="30sp" android:fontFamily="@font/poppins" android:textColor="@color/purple" android:textStyle="bold" android:layout_marginStart="8dp" android:layout_marginTop="12dp" app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent"/> <EditText android:id="@+id/searchEdit" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="Search news..." android:layout_marginTop="70dp" android:layout_marginEnd="8dp" android:layout_marginStart="8dp" android:padding="8dp" android:textSize="14sp" android:fontFamily="@font/poppins" android:drawableTint="@color/purple" android:background="@drawable/search_border" android:drawableEnd="@drawable/baseline_search_24" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recyclerSearch" android:layout_width="match_parent" android:layout_height="0dp" android:paddingBottom="50dp" android:clipToPadding="false" android:layout_marginStart="8dp" android:layout_marginEnd="8dp" android:layout_marginTop="12dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintTop_toBottomOf="@+id/searchEdit" tools:layout_editor_absoluteX="0dp" /> <include android:id="@+id/itemSearchError" layout="@layout/item_error" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="16dp" android:visibility="invisible" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <ProgressBar android:id="@+id/paginationProgressBar" style="?attr/progressBarStyle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:visibility="invisible" android:background="@android:color/transparent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toStartOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
item_news.xml
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="8dp"> <ImageView android:id="@+id/articleImage" android:layout_width="160dp" android:layout_height="90dp" android:scaleType="centerCrop" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/articleSource" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="4dp" android:text="SOURCE" android:fontFamily="@font/poppins" android:textColor="@color/purple" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/articleImage" /> <TextView android:id="@+id/articleTitle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:ellipsize="end" android:maxLines="2" android:text="TITLE" android:textColor="@color/black" android:textSize="15sp" android:textStyle="bold" app:layout_constraintRight_toRightOf="parent" app:layout_constraintLeft_toRightOf="@+id/articleImage" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/articleDescription" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:layout_marginTop="4dp" android:ellipsize="end" android:maxLines="2" android:text="DESCRIPTION" android:textColor="@color/black" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/articleImage" app:layout_constraintTop_toBottomOf="@+id/articleTitle" /> <TextView android:id="@+id/articleDateTime" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="12dp" android:text="DATE TIME" android:textSize="11sp" android:layout_marginStart="8dp" android:textColor="@color/black" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@+id/articleDescription" /> </androidx.constraintlayout.widget.ConstraintLayout>
activity_news.xml
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".ui.NewsActivity"> <FrameLayout android:id="@+id/flFragment" android:layout_width="match_parent" android:layout_height="0dp" app:layout_constraintBottom_toTopOf="@+id/bottomNavigationView" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> <androidx.fragment.app.FragmentContainerView android:id="@+id/newsNavHostFragment" android:name="androidx.navigation.fragment.NavHostFragment" android:layout_width="match_parent" android:layout_height="match_parent" app:defaultNavHost="true" app:navGraph="@navigation/news_nav_graph" /> </FrameLayout> <com.google.android.material.bottomnavigation.BottomNavigationView android:id="@+id/bottomNavigationView" android:layout_width="match_parent" android:layout_height="80dp" app:menu="@menu/bottom_navigation_menu" app:itemTextColor="@color/white" app:itemIconTint="@color/white" android:background="@color/purple" app:itemActiveIndicatorStyle="@style/App.Custom.Indicator" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent"/> </androidx.constraintlayout.widget.ConstraintLayout>
news_nav_graph.xml
<?xml version="1.0" encoding="utf-8"?> <navigation xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/news_nav_graph" app:startDestination="@id/headlinesFragment"> <fragment android:id="@+id/headlinesFragment" android:name="com.example.newsprojectpractice.ui.fragments.HeadlinesFragment" android:label="HeadlinesFragment"> <action android:id="@+id/action_headlinesFragment_to_articleFragment" app:destination="@id/articleFragment"/> </fragment> <fragment android:id="@+id/favouritesFragment" android:name="com.example.newsprojectpractice.ui.fragments.FavouritesFragment" android:label="FavouritesFragment"> <action android:id="@+id/action_favouritesFragment_to_articleFragment" app:destination="@id/articleFragment"/> </fragment> <fragment android:id="@+id/searchFragment" android:name="com.example.newsprojectpractice.ui.fragments.SearchFragment" android:label="SearchFragment"> <action android:id="@+id/action_searchFragment_to_articleFragment" app:destination="@id/articleFragment"/> </fragment> <fragment android:id="@+id/articleFragment" android:name="com.example.newsprojectpractice.ui.fragments.ArticleFragment" android:label="ArticleFragment" > <argument android:name="article" app:argType="com.example.newsprojectpractice.models.Article" /> </fragment> </navigation>
Alright, now the only last thing that is left is the API key.
Where, do we get this API key from?
So, every API has its own key which is private and you are not supposed to share it with anyone, and with the help of that key, you can access the data.
As we want news data there are varieties of news APIs available on the internet – out, of which we will be using News API.
News API: https://newsapi.org/
It’s, free, easy to integrate, and widely used.
Just, to show you – we, also have another famous news API called g news – if, you want you can use it too but as a beginner follow me.
gNews:
So, the best part about news API is that it provides a lot of customization like country, language, publishers, or timing – you will know when we will implement retrofit.
Now, let’s focus on the API key.
As a – new user clicks on the get API key.
Fill, in all the details like name, email, and password and submit it…then, at the end, it will provide us with an api key…and, that’s it.
All, our prerequisites are done!
Now, in the upcoming videos – we can write the code without any interference…and, that’s where the real logic begins…so, stay tuned for that, okay!
You can create a splash screen if you want to – click here for the code
Please subscribe to my YouTube channel:
Android Knowledge – Click here