Build Your Own Habit Tracker - NullClass

Special Sale the courses from 497 rs for limited time.

Build Your Own Habit Tracker

Habit Tracker:

  • Initial Setup ->

Download android studio from here – https://developer.android.com/studio?gclid=Cj0KCQiAuP-OBhDqARIsAD4XHpcTZ73G0aCMVa5Q89oZcqU862FIxHFF94khfszkJ590UHERqzdLfMEaAgvuEALw_wcB&gclsrc=aw.ds

 

Extract the zip file and go to android-studio-> bin . Right click on studio 64 and go to -> send to -> Desktop.

 

This creates an android studio shortcut on your desktop. Now click on android studio icon and select “create new project”. In the menu that appears, select empty activity and click on next.

 

Next, give your project a name “Video downloader”, select Kotlin as the language and and set min sdk to 23.

 

Now, add the following dependencies to the build.gradle(Module) file ->

// Dagger Hilt
implementation “com.google.dagger:hilt-android:2.38.1”
kapt “com.google.dagger:hilt-compiler:2.38.1”
implementation “androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha03”

// Coroutines
implementation ‘org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0’
implementation ‘org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0’
implementation “org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.3.9”

// Room
implementation “androidx.room:room-runtime:2.4.0”
kapt “androidx.room:room-compiler:2.4.0”
implementation “androidx.room:room-ktx:2.4.0”

//Navigation Component
implementation “androidx.navigation:navigation-fragment-ktx:$nav_version”
implementation “androidx.navigation:navigation-ui-ktx:$nav_version”

//Horizontal date picker
implementation ‘com.github.jhonnyx2012:horizontal-picker:1.0.6’

//Circular progress indicator
implementation ‘com.mikhaellopez:circularprogressbar:3.1.0’

 

Dagger Hilt -> A library that allows us to inject dependencies (objects) that a class needs to function at runtime automatically. This keeps the code cleaner.

Co-routines -> The kotlin co-rounites allow us to do long running operations like database operations or network operations on a background thread. This keeps the UI from freezing when the app does heavy operations like accessing a database. We will see more about co-routines later in the tutorial.

Room -> A library for using SQLite database for android. We will use SQLite database to store all the tasks.

Navigation Component -> We will use this library to move from one screen (fragment) to another and pass arguments between different screens.

Horizontal date picker -> A library for using a horizontally oriented date picker.

Circular progress Indicator -> We will use this library to show a circular progress indicator to indicate the percentage of tasks completed by the user.

 

  • Writing SQLite code ->

We first create model classes that will tell SQLit how to create and store data in the database.

Go to root package, right click and select new -> Package. Name the new Package “model”

 

 

 

In the package, create a new class called Task and add the following code->

enum class TimeOfDay {
MORNING,
AFTERNOON,
EVENING,
ALL
}

@Parcelize
@Entity(tableName = “tasks”)
data class Task(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
val heading: String,
val desc: String,
val time: Long,
val isPast: Boolean = false,
val isDone: Boolean = false
) : Parcelable {
val timeFormatted: String
get() = DateFormat.getDateTimeInstance().format(time)
}

 

The @Entity annotation tells the room library that this is a model class for SQLite. The tablename is the name of the table in which all the objects will be stored. We have several fields -> The task Heading, description , time.  The “isPast” variable tells if a task is pending or not. The “isDone” tells if the task is completed by user or not. The timeFormatted attribute takes the time of creation of a task which is in milliseconds and converts it into a human readable date and time format.

We also have an id for each task. We annotate it with “@PrimaryKey” annotation. It tells Room that this field will be the key used to distinguish two tasks and it will be unique for all tasks. We set the “autogenerate” attribute as true since now room will itself generate a unique id for all tasks.

We also add a @Parcelize annotation to the class. This allows the class to be sent between different fragments in android.

We also create an enum class “TimeOfDay”. It will store the various kinds of day types there can be for a task -> morning, afternoon and evening.

 

Next, we create the DAO class (Database access Object).

Go to root package, and create a package called room. Inside this package, create a new interface called “TaskDao”.

 

Add the following code to the new interface ->

@Dao
interface TaskDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTask(task: Task)

@Query(“SELECT * FROM tasks WHERE isPast = :isPast ORDER BY time DESC”)
fun getTasks(isPast: Boolean): Flow<List<Task>>

@Query(“SELECT COUNT(*) FROM tasks WHERE isPast == :isPast AND isDone == :isDone “)
fun getTaskCount(isPast: Boolean, isDone: Boolean): Flow<Int>

@Query(“SELECT * FROM tasks WHERE heading =:name”)
suspend fun getTaskByHeading(name: String): Task

@Update
suspend fun update(task: Task)

@Delete
suspend fun deleteTask(task: Task)
}

 

The @Dao annotation tells room library that this interface is a database access object.

We create a function to insert a new task and annotate it with @Insert.

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTask(task: Task)

 

The onConflictStatergy Replace parameter tells the room library to replace an old task with the new one incase two tasks have same id.

The suspend modifier makes sure that the function can be operated on a background thread.

Next we have a function to get all tasks for which the “isPast” parameter is false(The task is not past deadline yet).

We write the following function ->

@Query(“SELECT * FROM tasks WHERE isPast = :isPast ORDER BY time DESC”)
fun getTasks(isPast: Boolean): Flow<List<Task>>

This gives us a Flow of list of tasks for which the isPast parameter is false. The ORDER BY parameter sorts all the tasks in descending order by the time of creation.

A Kotlin Flow is a asynchronous stream of data. So we take the list of tasks from the SQLite database on a background thread.

 

@Query(“SELECT COUNT(*) FROM tasks WHERE isPast == :isPast AND isDone == :isDone “)
fun getTaskCount(isPast: Boolean, isDone: Boolean): Flow<Int>

This function takes the isPast parameter (true or false) and isDone (true or false) and returns a Flow of Int, which is the count of tasks for which the isPast and isDone is equal to what we pass as a parameter.

 

@Query(“SELECT * FROM tasks WHERE heading =:name”)
suspend fun getTaskByHeading(name: String): Task

This functions returns the task whose name is same as the one we pass as the function parameter.

Next we have the update function ->

@Update
suspend fun update(task: Task)

This will be used to update any task in the database.

And lastly, we have the function to delete a task ->
@Delete
suspend fun deleteTask(task: Task)

 

Now we create the room database class. Go to room package and create a new class “TaskDatabase”.  Add the following code in the class –>

@Database(entities = [Task::class], version = 1)
abstract class TaskDatabase : RoomDatabase() {
abstract fun getTaskDao(): TaskDao
}

 

The “@Database” annotation tells room library that this is a database class. We pass the entities(classes whose objects will be stored in the database) and the version number of this database to the @Database annotation.

We make this class abstract with the “abstract” keyword. Inside the class we have a single abstract function “getTaskDao()” which returns a TaskDao object. We make the TaskDatabase class and the function inside it abstract since Room library will later generate the necessary SQLite code on its own at runtime.

 

  • Dependency Injection

We will now setup dependency injection -> a mechanism to provide dependencies(objects) throughout the app without creating the same objects again and again. This makes the code cleaner and more concise and efficient.

 

Create a new package in the root directory and call it “di”

Create a new object and call it “RoomModule”

@Module
@InstallIn(SingletonComponent::class)
object RoomModule {
@Provides
@Singleton
fun providesTasksDatabase(app: Application): TaskDatabase =
Room.databaseBuilder(app, TaskDatabase::class.java, “tasks_database”)
.fallbackToDestructiveMigration()
.build()

@Provides
@Singleton
fun providesTasksDao(taskDatabase: TaskDatabase): TaskDao = taskDatabase.getTaskDao()

@Provides
@Singleton
fun providesAlarmManager(app: Application): AlarmManager =
app.applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager

@ApplicationScope
@Provides
@Singleton
fun provideApplicationScope() = CoroutineScope(SupervisorJob())
}

@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class ApplicationScope

 

The “@Module” tells dagger – hilt that this object provides dependencies. The @Installin(SingletonComponent::class) makes the dependencies described in this module live as long as the application keeps running. This allows us to use these dependencies anywhere in our app.

 

The “@Provides” annotation tells dagger-hilt that this function provides a dependency (object). The @Singleton annotation makes the object to be generated only once and then later, the same object is injected where ever it is required. This makes the code more efficient, since now, we won’t be creating new objects again and again.

 

We first have a function to make the TaskDatabase Object->

@Singleton
fun providesTasksDatabase(app: Application): TaskDatabase =
Room.databaseBuilder(app, TaskDatabase::class.java, “tasks_database”)
.fallbackToDestructiveMigration()
.build()

 

This function takes the Application as a parameter (which is automatically provided by dagger).

We call the Room.databaseBuilder function and pass the application instance, the TaskDatabase class and the database name -> “tasks_database”. The fallbackToDestructiveMigration() tells room that incase the app is uninstalled, we delete all app data.

Next we create a function to provide a TaskDao instance ->

@Provides
@Singleton
fun providesTasksDao(taskDatabase: TaskDatabase): TaskDao = taskDatabase.getTaskDao()

The taskDatabase object is automatically injected by Dagger Hilt from the previous function we created. We call the getTaskDao method in the taskDatabase to get the taskDao object.

 

Now, we create a function to provide the alarm manager ->

@Provides
@Singleton
fun providesAlarmManager(app: Application): AlarmManager =
app.applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager

 

The application instance is injected automatically into the function. We use the app instance and get the application context to the System service -> The alarm manager.

 

Next, we need to create our own annotation using the following code ->

@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class ApplicationScope

 

This creates a new Qualifier (annotation) named “ApplicationScope” which gets retained during Runtime of the app.

 

Using this new Annotation, we create another function to get a co-routine scope ->

@ApplicationScope
@Provides
@Singleton
fun provideApplicationScope() = CoroutineScope(SupervisorJob())

 

This co-rountine scope will allow us to launch co-rountines – suspendable pieces of computations that can be run on a background thread without freezing the main thread.

 

  • Creating the viewModel ->

Create a new package in the root directory called “viewModels”. Create a new class called TaskViewModel and add the following code ->

@HiltViewModel
class TaskViewModel @Inject constructor(
private val app: Application,
private val savedStateHandle: SavedStateHandle,
private val taskDao: TaskDao,
private val alarmManager: AlarmManager
) : AndroidViewModel(app) {

var dayOfMonth = MutableStateFlow(0)
var month = MutableStateFlow(1)
var timeOfDay = MutableStateFlow(TimeOfDay.ALL)
@ExperimentalCoroutinesApi
val tasksFlow = combine(
dayOfMonth, month, timeOfDay
) { dayOfMonth, month, timeOfDay ->
Triple(dayOfMonth, month, timeOfDay)
}.flatMapLatest { (dayOfMonth, month, timeOfDay) ->
taskDao.getTasks(false).map {
val taskList = it.filter { task ->
task.getDayOfMonth() == dayOfMonth && task.getMonth() == month
&& (timeOfDay == TimeOfDay.ALL || timeOfDay == task.getTimeOfDay())
}
taskList
}
}
val notificationsFlow = taskDao.getTasks(true)
val completedCountFlow = taskDao.getTaskCount(isPast = true, isDone = true)
val inCompleteCountFlow = taskDao.getTaskCount(isPast = true, isDone = false)

@ExperimentalCoroutinesApi
val totalTaskCountFlow =
combine(completedCountFlow, inCompleteCountFlow) { completedCount, inCompleteCount ->
Pair(completedCount, inCompleteCount)
}.flatMapLatest { (completedCount, inCompletedCount) ->
getTotalCount(completedCount, inCompletedCount)
}

@ExperimentalCoroutinesApi
val progress =
combine(completedCountFlow, inCompleteCountFlow) { completedCount, inCompleteCount ->
Pair(completedCount, inCompleteCount)
}.flatMapLatest { (completedCount, inCompletedCount) ->
getProgress(completedCount, inCompletedCount)
}

private fun getProgress(completedCount: Int, inCompletedCount: Int) = flow {
if (inCompletedCount + completedCount == 0) {
emit(0F)
} else {
emit((completedCount * 100F) / (inCompletedCount + completedCount))
}
}

private fun getTotalCount(completedCount: Int, inCompletedCount: Int) = flow {
emit(completedCount + inCompletedCount)
}

fun onTaskCheckedChanged(task: Task, isDone: Boolean) = viewModelScope.launch {
taskDao.update(task.copy(isDone = isDone))
}

fun getTaskTime(): Long {
val calendar = Calendar.getInstance()
calendar.set(year, monthOfYear, day, hourOfDay, minute, 0)
return calendar.timeInMillis
}

fun onAddTaskClick() {
viewModelScope.launch {
val newTask = Task(heading = heading, desc = desc, time = time!!)
taskDao.insertTask(newTask)
val intent = Intent(app.applicationContext, AlarmBroadCastReceiver::class.java)
intent.putExtra(“task”, newTask.heading)
val pendingIntent = PendingIntent.getBroadcast(
app.applicationContext,
0,
intent,
PendingIntent.FLAG_CANCEL_CURRENT
)
alarmManager.setExact(AlarmManager.RTC_WAKEUP, time!!, pendingIntent)
tasksChannel.send(TasksEvent.TaskSuccess(“Task Added”))
}
}

fun onDeleteTask(task: Task) {
viewModelScope.launch {
taskDao.deleteTask(task)
tasksChannel.send(TasksEvent.TaskDeleted(“Task Deleted”, task))
}
}

fun onTaskReAdd(task: Task) {
viewModelScope.launch {
taskDao.insertTask(task)
tasksChannel.send(TasksEvent.TaskSuccess(“Task Re-Added”))
}
}
}

 

Lets examine code step by step.

  • First, we have a class which extends from the AndroidViewModel class.
  • We annotate it with the @HiltViewModel annotation which tells Dagger hilt that this class is a viewModel. We then put the @Inject annotation to the constructor of the Task ViewModel class. Dagger Hilt will then automatically inject dependencies into this class at runtime -> The TaskDao, the application Instance, the SavedStateInstance and the alarm Manager.
  • The application Instance and the saved State instance is automatically injected into the viewModel. The taskDao and the alarmManager is provided using the RoomModule we created earlier.

 

Now we store the current selected day and month in the following variables->

var dayOfMonth = MutableStateFlow(0)
var month = MutableStateFlow(1)

dayOfmonth and month are mutable state flows with intial values 0 and 1 respectively. Making them mutable state flows allows us to -> 1) Change their values (they are mutable) and 2) Allows us to observe their values asynchronously using the collectLatest method (which we will see later).

 

Similarly, we have another mutable state flow to store the current selected time of day (it can be morning, afternoon, evening or All). Intially, we set the time of day to be “ALL”

var timeOfDay = MutableStateFlow(TimeOfDay.ALL)

 

Next, we query our database using the taskDao object injected by dagger hilt into the viewModel. We use the “combine” operator on the three mutable state flows ->

dayOfMonth, month and timeOfDay. The operator combines the three mutable state flows into a triple of values. Now, if any one of these 3 values change, we get a new triple, which we use to query tasks from our sqLite database.

 

@ExperimentalCoroutinesApi
val tasksFlow = combine(
dayOfMonth, month, timeOfDay
) { dayOfMonth, month, timeOfDay ->
Triple(dayOfMonth, month, timeOfDay)
}.flatMapLatest { (dayOfMonth, month, timeOfDay) ->
taskDao.getTasks(false).map {
val taskList = it.filter { task ->
task.getDayOfMonth() == dayOfMonth && task.getMonth() == month
&& (timeOfDay == TimeOfDay.ALL || timeOfDay == task.getTimeOfDay())
}
taskList
}
}

 

  • We use the flatmapLatest operator to query the database. This operator gets triggered everytime one of the 3 values of the triple changes, and we use these values to query and filter out tasks. We use the filter operator and filter out only those tasks that have the given dayOfMonth, month and timeOfDay values.

 

Next, we create three more flows->

val notificationsFlow = taskDao.getTasks(true)
val completedCountFlow = taskDao.getTaskCount(isPast = true, isDone = true)
val inCompleteCountFlow = taskDao.getTaskCount(isPast = true, isDone = false)

 

We have the notifications flow, which has the list of all the tasks for which the isPast parameter is true (the task is past deadline).

Next, we have the completedtedCountFlow and the inCompletedCountFlow, which are the counts of the tasks which are past deadline and for which the isDone parameter is true and false respectively.

 

Next we have the completedTaskFlow , where we again use the combine operator to combine the completedCountFlow and the inCompletedCountFlow into a Pair of values. We then use the flatmapLatest operator to call the getTotalCount function, which adds them up and returns the total count.

 

Now, we have the progress Flow, we again use the combine operator and the flatMapLatest operator on the completedCountFlow and the inCompletedFlow to get the progress from the getProgress Method.

    @ExperimentalCoroutinesApi
val progress =
combine(completedCountFlow, inCompleteCountFlow) { completedCount, inCompleteCount ->
Pair(completedCount, inCompleteCount)
}.flatMapLatest { (completedCount, inCompletedCount) ->
getProgress(completedCount, inCompletedCount)
}

  • The get Progress method returns (completedCount*100F/( completedCount + inCompletedCount) value.

Now, we create the function to add a task ->

fun onAddTaskClick() {
viewModelScope.launch {
val newTask = Task(heading = heading, desc = desc, time = time!!)
taskDao.insertTask(newTask)
val intent = Intent(app.applicationContext, AlarmBroadCastReceiver::class.java)
intent.putExtra(“task”, newTask.heading)
val pendingIntent = PendingIntent.getBroadcast(
app.applicationContext,
0,
intent,
PendingIntent.FLAG_CANCEL_CURRENT
)
alarmManager.setExact(AlarmManager.RTC_WAKEUP, time!!, pendingIntent)
}
}

  • We write this code asynchronously in the viewModel scope. All the tasks in this function are done on a background thread. We create a new Task object and insert it into the database using the taskDao.
  • Now we create a new Intent for our alarm Manager. We put the heading of the task into the intent and then create a pendingIntent object which intiates our alarm Manager. Next we call the alarm Manager object and set the Alarm. The RTC_WAKEUP parameter allows the notification to be shown even when the phone is locked.

Similarly, we create the deleteTask and ReAddTask functions ->

fun onDeleteTask(task: Task) {
viewModelScope.launch {
taskDao.deleteTask(task)
tasksChannel.send(TasksEvent.TaskDeleted(“Task Deleted”, task))
}
}

fun onTaskReAdd(task: Task) {
viewModelScope.launch {
taskDao.insertTask(task)
tasksChannel.send(TasksEvent.TaskSuccess(“Task Re-Added”))
}
}

 

Both of these functions use the viewModelScope co-routine to execute database operations on a background thread.

 

 

  • Creating the Task Repository broadcast Receiver – >

Create a new package named repository in the root package and create a class “TaskRepository”. Add the following code to the class->

@Singleton
class TaskRepository @Inject constructor(
private val taskDao: TaskDao,
@ApplicationScope private val applicationScope: CoroutineScope
) {
fun update(taskName: String) {
applicationScope.launch {
val task = taskDao.getTaskByHeading(taskName)
taskDao.update(task.copy(isPast = true)

 

  • We annotate the class with @Singleton so dagger hilt will only create one instance of this class and use it in the app.
  • Dagger Hilt then automatically injects the taskDao and the applicationScope that we had created earlier in the AppModule.
  • Next we create a single function “update”. We get the taskName as a parameter and then get the task with this heading using the taskDao. Next we update the task and set the isPast parameter of this task to true (this task is past deadline) .

 

Next, create a new class and name it AlarmBroadCastReceiver ->

const val CHANNEL_ID = “habit_tracker”

@AndroidEntryPoint
class AlarmBroadCastReceiver : BroadcastReceiver() {

@Inject
lateinit var taskRepository: TaskRepository

override fun onReceive(context: Context?, p1: Intent?) {
if(context==null)
return
p1?.let { intent ->
val intent1 = Intent(context, MainActivity::class.java)
val taskName = intent.getStringExtra(“task”)!!
taskRepository.update(taskName)
val pendingIntent = PendingIntent.getActivity(context, 0, intent1, 0)
val notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val mBuilder = NotificationCompat.Builder(context, CHANNEL_ID)
mBuilder.setAutoCancel(true)
mBuilder.setSmallIcon(R.drawable.alarm_icon)
mBuilder.setContentText(taskName)
mBuilder.setContentIntent(pendingIntent)
mBuilder.priority = NotificationCompat.PRIORITY_HIGH
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
“channel name”,
NotificationManager.IMPORTANCE_HIGH
)
channel.enableVibration(true)
notificationManager.createNotificationChannel(channel)
mBuilder.setChannelId(CHANNEL_ID)
}

val notification = mBuilder.build()
notificationManager.notify(1, notification)
}
}
}

 

 

  • This class inherits from the BroadCast Receiver class. We use the @Inject annotation to tell dagger hilt to inject the taskRepository object into this class automatically.
  • This class gets called everytime the alarm Manager triggers.
  • We override the onreceive function and get the task name from the intent. We then call the update function in the task repository and pass the task name as a parameter. This updates the task and makes the isPast parameter to be true (it is past deadline).

 

 

val pendingIntent = PendingIntent.getActivity(context, 0, intent1, 0)
val notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val mBuilder = NotificationCompat.Builder(context, CHANNEL_ID)
mBuilder.setAutoCancel(true)
mBuilder.setSmallIcon(R.drawable.alarm_icon)
mBuilder.setContentText(taskName)
mBuilder.setContentIntent(pendingIntent)
mBuilder.priority = NotificationCompat.PRIORITY_HIGH

 

Next we create a pending intent and create the notification manager object using the context.getSystemService function.

 

We create a notification builder and set some properties to it -> the icon, the text to be displayed, the pending Intent and the priority.

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
“channel name”,
NotificationManager.IMPORTANCE_HIGH
)
channel.enableVibration(true)
notificationManager.createNotificationChannel(channel)
mBuilder.setChannelId(CHANNEL_ID)
}

val notification = mBuilder.build()
notificationManager.notify(1, notification)

 

Next we check if the build version of the android mobile the app is running on is greater than version O or not. If yes, we need to create a notification channel. (We can’t display notifications without using the notifications channels in version greater than Android O )

Then we use the notification manager and show the final notification.

 

  • Creating the UI ->

Now we will create the ui.

Go to resource -> layout and create 2 files -> fragment_home, fragment_notifications.

 

Add the following code to the fragment_home file->

<?xml version=”1.0″ encoding=”utf-8″?>
<RelativeLayout 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”
android:background=”@color/black”
tools:context=”.ui.HomeFragment”>

<TextView
android:id=”@+id/tvToday”
android:layout_width=”match_parent”
android:layout_height=”wrap_content”
android:layout_marginStart=”20dp”
android:layout_marginTop=”20dp”
android:text=”Today”
android:textColor=”@color/white”
android:textSize=”25sp”
android:textStyle=”bold” />

<TextView
android:id=”@+id/tvDate”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_below=”@id/tvToday”
android:layout_marginStart=”20dp”
android:textColor=”@color/white”
android:textSize=”17sp”
tools:text=”Jan 11″ />

<LinearLayout
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:layout_below=”@id/tvDate”
android:layout_marginTop=”20dp”
android:background=”@drawable/background_layout”
android:orientation=”vertical”>

<com.github.jhonnyx2012.horizontalpicker.HorizontalPicker
android:id=”@+id/datePicker”
android:layout_width=”match_parent”
android:layout_height=”wrap_content”
android:layout_marginTop=”30dp” />

<HorizontalScrollView
android:layout_width=”match_parent”
android:layout_height=”wrap_content”
android:layout_marginTop=”10dp”>

<com.google.android.material.chip.ChipGroup
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_marginStart=”10dp”
android:layout_marginEnd=”10dp”>

<com.google.android.material.chip.Chip
android:id=”@+id/all_chip”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_marginStart=”10dp”
android:layout_marginEnd=”10dp”
android:backgroundTint=”#0e05b0″
android:text=”All”
android:textColor=”@color/white”
android:textSize=”18sp”
android:textStyle=”bold”
app:textEndPadding=”20dp”
app:textStartPadding=”20dp” />

<com.google.android.material.chip.Chip
android:id=”@+id/morning_chip”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_marginEnd=”10dp”
android:backgroundTint=”#0e05b0″
android:text=”Morning”
android:textColor=”@color/white”
android:textSize=”18sp”
android:textStyle=”bold”
app:textEndPadding=”20dp”
app:textStartPadding=”20dp” />

<com.google.android.material.chip.Chip
android:id=”@+id/afternoon_chip”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_marginEnd=”10dp”
android:backgroundTint=”#0e05b0″
android:text=”Afternoon”
android:textColor=”@color/white”
android:textSize=”18sp”
android:textStyle=”bold”
app:textEndPadding=”20dp”
app:textStartPadding=”20dp” />

<com.google.android.material.chip.Chip
android:id=”@+id/evening_chip”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_marginEnd=”20dp”
android:backgroundTint=”#0e05b0″
android:text=”Evening”
android:textColor=”@color/white”
android:textSize=”18sp”
android:textStyle=”bold”
app:textEndPadding=”20dp”
app:textStartPadding=”20dp” />
</com.google.android.material.chip.ChipGroup>
</HorizontalScrollView>

<RelativeLayout
android:layout_width=”match_parent”
android:layout_height=”match_parent”>

<androidx.recyclerview.widget.RecyclerView
android:id=”@+id/tasks_recyclerview”
android:layout_width=”match_parent”
android:layout_height=”wrap_content”
android:layout_marginTop=”20dp”
android:orientation=”vertical”
app:layoutManager=”androidx.recyclerview.widget.LinearLayoutManager”
tools:listitem=”@layout/task_display_layout” />

<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id=”@+id/add_task_button”
android:layout_width=”60dp”
android:layout_height=”60dp”
android:layout_alignParentEnd=”true”
android:layout_alignParentBottom=”true”
android:layout_marginEnd=”30dp”
android:layout_marginBottom=”30dp”
android:src=”@drawable/add_icon” />
</RelativeLayout>
</LinearLayout>
</RelativeLayout>

 

 

  • This is a simple layout. We have a textivew to display the date and time. We have a horizontal date time picker to allow user to select a particular date.
  • Next we create a horizontal chip group which will have 4 chips -> All, morning, afternoon and evening. The user can click on one of these chips to select a particular time of day. And lastly, we have a recyclerview to display the tasks.

 

Next add the following code to the fragment_notifications file ->

 

 

<?xml version=”1.0″ encoding=”utf-8″?>
<RelativeLayout 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”
android:background=”@color/black”
tools:context=”.ui.NotificationsFragment”>

<TextView
android:id=”@+id/tvToday”
android:layout_width=”match_parent”
android:layout_height=”wrap_content”
android:layout_marginStart=”20dp”
android:layout_marginTop=”20dp”
android:text=”Notifications”
android:textColor=”@color/white”
android:textSize=”25sp”
android:textStyle=”bold” />

<TextView
android:id=”@+id/tvDate”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_below=”@id/tvToday”
android:layout_marginStart=”20dp”
android:textColor=”@color/white”
android:textSize=”17sp”
tools:text=”Jan 11″ />

<LinearLayout
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:layout_below=”@id/tvDate”
android:layout_marginTop=”20dp”
android:background=”@drawable/background_layout”
android:orientation=”vertical”>

<androidx.recyclerview.widget.RecyclerView
android:id=”@+id/completed_tasks_recyclerview”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:layout_marginTop=”20dp”
android:orientation=”vertical”
app:layoutManager=”androidx.recyclerview.widget.LinearLayoutManager”
tools:listitem=”@layout/task_display_layout” />
</LinearLayout>
</RelativeLayout>

 

We again have a textview to display the current date and time. We also have a recyclerview to display tasks that are past deadline.

 

Next, we create the task Adapter to display the tasks in the recyclerview. Create a new package in the root directory and name it “adapter”. In the new package, create a new class called TaskAdapter and add the following code ->

 

class TaskAdapter(private val listener: OnItemClickListener) :
ListAdapter<Task, TaskAdapter.TaskViewHolder>(COMPARATOR) {

companion object {
val COMPARATOR = object : DiffUtil.ItemCallback<Task>() {
override fun areItemsTheSame(oldItem: Task, newItem: Task): Boolean =
oldItem.id == newItem.id

override fun areContentsTheSame(oldItem: Task, newItem: Task): Boolean =
oldItem == newItem
}
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TaskViewHolder =
TaskViewHolder(
TaskDisplayLayoutBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)

override fun onBindViewHolder(holder: TaskViewHolder, position: Int) {
val item = getItem(position)
if (item != null) {
holder.bind(item)
}
}

interface OnItemClickListener {
fun onItemClick(task: Task)
fun onCheckBoxClick(task: Task, isDone: Boolean)
}

inner class TaskViewHolder(private val binding: TaskDisplayLayoutBinding) :
RecyclerView.ViewHolder(binding.root) {

init {
binding.apply {
root.setOnClickListener {
val position = adapterPosition
if (position != RecyclerView.NO_POSITION) {
val task = getItem(position)
if (task != null) {
listener.onItemClick(task)
}
}
}
checkbox.setOnClickListener {
val position = adapterPosition
if (position != RecyclerView.NO_POSITION) {
val task = getItem(position)
if (task != null) {
listener.onCheckBoxClick(task, checkbox.isChecked)
}
}
}
}
}

fun bind(task: Task) {
binding.apply {
taskNameTextView.text = task.heading
timeTextView.text = task.timeFormatted
checkbox.isChecked = task.isDone
}
}
}

 

 

  • We have two methods -> The onCreateViewHolder, where create the object of the TaskViewHolder class, the onBindViewHolder, where we get the current task to be displayed.
  • We also have an onItemClickListener interface having one method named “onItemClick” which takes a task object and one method named “onCheckBoxClicked”. These function will get called everytime the user clicks on a task and on the checkbox respectively. We will write the implementation of this function by overriding this interface in the Fragment classes later.
  • Next, we create the viewHolder class, which has a single TaskDisplayLayoutBinding object in its constructor.

root.setOnClickListener {
val position = adapterPosition
if (position != RecyclerView.NO_POSITION) {
val task = getItem(position)
if (task != null) {
listener.onItemClick(task)
}
}
}

We get the current adapter position. If it is a valid adapter position, we get the current task. If it is not null, we use the listener object and pass the task to the onItemClick function.

Simlarly, we add the clicklistener to the checkbox.
checkbox.setOnClickListener {
val position = adapterPosition
if (position != RecyclerView.NO_POSITION) {
val task = getItem(position)
if (task != null) {
listener.onCheckBoxClick(task, checkbox.isChecked)
}
}
}
}

 

 

 

We create an init block, where we add an onClickListener to the task layout ->

checkbox.setOnClickListener {
val position = adapterPosition
if (position != RecyclerView.NO_POSITION) {
val task = getItem(position)
if (task != null) {
listener.onCheckBoxClick(task, checkbox.isChecked)
}
}
}
}

 

Lastly, we create the bind function which displays the task in the task Layout ->

fun bind(task: Task) {
binding.apply {
taskNameTextView.text = task.heading

timeTextView.text = task.timeFormatted
checkbox.isChecked = task.isDone
}
}

 

  • We have the task Heading and time formatted into textviews. The checkbox is checked depending on whether the task is done or not.
  • Now, in the root package, create a new package and name it ui. Create a new class and call it HomeFragment. Add the following code to the HomeFragment->

 

@AndroidEntryPoint
class HomeFragment : Fragment(R.layout.fragment_home), TaskAdapter.OnItemClickListener,
DatePickerListener {
private var _binding: FragmentHomeBinding? = null
private val
binding get() = _binding!!
private val viewModel by activityViewModels<TaskViewModel>()

@ExperimentalCoroutinesApi
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding = FragmentHomeBinding.bind(view)
val taskAdapter = TaskAdapter(this)
binding.datePicker.setListener(this).init()
binding.apply {
tvDate.text = DateFormat.getDateTimeInstance().format(System.currentTimeMillis())
allChip.setOnClickListener { viewModel.timeOfDay.value = TimeOfDay.ALL }
morningChip.setOnClickListener { viewModel.timeOfDay.value = TimeOfDay.MORNING }
afternoonChip.setOnClickListener { viewModel.timeOfDay.value = TimeOfDay.AFTERNOON }
eveningChip.setOnClickListener { viewModel.timeOfDay.value = TimeOfDay.EVENING }
tasksRecyclerview.adapter = taskAdapter
addTaskButton.setOnClickListener {
findNavController().navigate(
HomeFragmentDirections.actionHomeFragmentToAddTaskFragment2()
)
}
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
viewModel.tasksFlow.collectLatest { tasksList ->
taskAdapter.submitList(tasksList)
}
}

}
}

override fun onCheckBoxClick(task: Task, isDone: Boolean) {
viewModel.onTaskCheckedChanged(task, isDone)
}

override fun onDateSelected(dateSelected: DateTime?) {
dateSelected?.let { dateTime ->
viewModel.dayOfMonth.value = dateTime.dayOfMonth
viewModel.month.value = dateTime.monthOfYear – 1
}
}

}

 

We annotate the class the @AndroidEntryPoint which allows dagger to inject the TaskViewModel into the class. We then create the ViewBinding instance. We will use it to access the various views in the layout

private var _binding: FragmentHomeBinding? = null
private val
binding get() = _binding!!

We also get our viewModel injected into the class by dagger hilt.

private val viewModel by activityViewModels<TaskViewModel>()

 

Next, we create the task Adapter and initialize the date picker ->

val taskAdapter = TaskAdapter(this)
binding.datePicker.setListener(this).init()

Now, we add the adapter for the recyclerview.

tasksRecyclerview.adapter = taskAdapter

 

  • We then set the current date and time in the date and time textview using the date format class. The System.currentTimeInMillis returns the time in milliseconds, which the format function converts to date and time.

We all set onClick listeners on the “All” , “Morning” , “Afternoon” and “evening” chips and update the value of timeOfDay mutable state flow according to what user has clicked.

tvDate.text = DateFormat.getDateTimeInstance().format(System.currentTimeMillis())
allChip.setOnClickListener { viewModel.timeOfDay.value = TimeOfDay.ALL }
morningChip.setOnClickListener { viewModel.timeOfDay.value = TimeOfDay.MORNING }
afternoonChip.setOnClickListener { viewModel.timeOfDay.value = TimeOfDay.AFTERNOON }
eveningChip.setOnClickListener { viewModel.timeOfDay.value = TimeOfDay.EVENING }

 

Next we set an onClick listener on the add task button ->

addTaskButton.setOnClickListener {
findNavController().navigate(
HomeFragmentDirections.actionHomeFragmentToAddTaskFragment2()
)
}

  • When the user clicks on the addTaskButton, we use the navigation component library to navigate to the AddTaskFragment using the findNavController().navigate() function and pass the argument of HomeFragmentDirections to it which is generated automatically by the navigation component library
  • Now, we launch a co-rountine in the viewlifecyle scope and collect the tasksFlow using the collectLatest operator. This automatically updates the tasks List anytime something changes in the task database. Doing this inside a co-rountine makes this process run on a background thread. In the end , we call the submit List method on the taskAdapter.

viewLifecycleOwner.lifecycleScope.launchWhenStarted {
viewModel.tasksFlow.collectLatest { tasksList ->
taskAdapter.submitList(tasksList)
}
}

 

Now we override two functions ->

override fun onCheckBoxClick(task: Task, isDone: Boolean) {
viewModel.onTaskCheckedChanged(task, isDone)
}

override fun onDateSelected(dateSelected: DateTime?) {
dateSelected?.let { dateTime ->
viewModel.dayOfMonth.value = dateTime.dayOfMonth
viewModel.month.value = dateTime.monthOfYear – 1
}
}

  • The onCheckBox method is called everytime the user clicks on a checkbox for a particular task. We pass the task and isDone Boolean value and then the viewModel method is called to update the task (which we created earlier)
  • We then have the onDateSelected method. Everytime the date is selected, this method is called. The “let” operator only executes if the selected date is not null. If it is not null, we set the values of dayOfMonth and month in the viewModel.
  • Now, in the ui package, create a new class NotificationsFragment and add the following code to the class ->

 

@AndroidEntryPoint
class NotificationsFragment : Fragment(R.layout.fragment_notifications){
private var _binding: FragmentNotificationsBinding? = null
private val
binding get() = _binding!!
private val viewModel by activityViewModels<TaskViewModel>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding = FragmentNotificationsBinding.bind(view)
val taskAdapter = TaskAdapter(this)
binding.apply {
completedTasksRecyclerview.adapter = taskAdapter
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
viewModel.notificationsFlow.collectLatest { notifications->
noTasksTextView.isVisible = notifications.isNullOrEmpty()
taskAdapter.submitList(notifications)
}
}
}
}

override fun onCheckBoxClick(task: Task, isDone: Boolean) {
viewModel.onTaskCheckedChanged(task, isDone)
}
}

 

The code is quite similar to the home fragment. We have the @AndroidEntryPoint annotation which tells when daggerHilt to inject dependencies into this class. We initiate the FragmentNoficationsBinding and then assign its value in the onViewCreated method.

_binding = FragmentNotificationsBinding.bind(view)

Next, we create the task adapter and assign it to the recyclerview ->

val taskAdapter = TaskAdapter(this)

completedTasksRecyclerview.adapter = taskAdapter

Next, we again launch a co-rountine in the viewlifecycle scope , similar to the way we did in the home fragment, and collect the notifications flow using the collectLatest method. Next, we use the submit list method to put the list in the recyclerview.

viewLifecycleOwner.lifecycleScope.launchWhenStarted {
viewModel.notificationsFlow.collectLatest { notifications->
noTasksTextView.isVisible = notifications.isNullOrEmpty()
taskAdapter.submitList(notifications)
}
}

 

  • And lastly, we override the onCheckbox click function similar to the way we did in the home fragment. Inside this, we call the onTaskCheckedChanged method of the viewModel to update the task.

 

To know about Industrial Based Learning and more Information like this make sure to register yourself at NullClass now !

 

 

 

 

 

 

 

 

 

 

 

January 27, 2022

0 responses on "Build Your Own Habit Tracker"

Leave a Message