Free tutorials & notes in Hindi & English · Clean code examples 🧰 Free PDF & Image Tools →
🏆 Complete App & Publish  ·  Lesson 41

Project: Build All Screens

Step 6: Add / Edit note screen

9:41▮▮ 85%←Note⋮Title (0/60)DetailsSubject: General ▾LowMedium ✓HighAdd to favourites✓Remind meSAVE NOTE9:41▮▮ 85%←Note⋮🖼️attached photo🖼 Add photoRemove⏰ 24 Nov 2026, 09:00 AMSAVE NOTE
Top and bottom of the editor (scrolls in a NestedScrollView)
XML – layout/fragment_add_edit_note.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- NestedScrollView: the form scrolls when the keyboard is open -->
<androidx.core.widget.NestedScrollView 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="match_parent">

    <LinearLayout
        android:id="@+id/form"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="16dp">

        <com.google.android.material.textfield.TextInputLayout
            android:id="@+id/tilTitle"
            style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="@string/hint_title"
            app:counterEnabled="true"
            app:counterMaxLength="60"
            app:startIconDrawable="@drawable/ic_note">

            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/etTitle"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:inputType="textCapSentences"
                android:maxLength="60" />
        </com.google.android.material.textfield.TextInputLayout>

        <com.google.android.material.textfield.TextInputLayout
            style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="12dp"
            android:hint="@string/hint_content">

            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/etContent"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:gravity="top"
                android:inputType="textMultiLine|textCapSentences"
                android:minLines="4" />
        </com.google.android.material.textfield.TextInputLayout>

        <!-- Exposed dropdown menu (Material version of a Spinner) -->
        <com.google.android.material.textfield.TextInputLayout
            style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="12dp"
            android:hint="@string/hint_subject">

            <com.google.android.material.textfield.MaterialAutoCompleteTextView
                android:id="@+id/actvSubject"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:inputType="textCapWords" />
        </com.google.android.material.textfield.TextInputLayout>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            android:text="@string/priority"
            android:textAppearance="?attr/textAppearanceTitleSmall" />

        <com.google.android.material.chip.ChipGroup
            android:id="@+id/chipGroupPriority"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:selectionRequired="true"
            app:singleSelection="true">

            <com.google.android.material.chip.Chip
                android:id="@+id/chipLow"
                style="@style/Widget.Material3.Chip.Filter"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@string/priority_low" />

            <com.google.android.material.chip.Chip
                android:id="@+id/chipMedium"
                style="@style/Widget.Material3.Chip.Filter"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@string/priority_medium" />

            <com.google.android.material.chip.Chip
                android:id="@+id/chipHigh"
                style="@style/Widget.Material3.Chip.Filter"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@string/priority_high" />
        </com.google.android.material.chip.ChipGroup>

        <com.google.android.material.checkbox.MaterialCheckBox
            android:id="@+id/cbFavorite"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:text="@string/add_to_favourites" />

        <com.google.android.material.materialswitch.MaterialSwitch
            android:id="@+id/switchReminder"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:text="@string/reminder" />

        <com.google.android.material.button.MaterialButton
            android:id="@+id/btnPickDate"
            style="@style/Widget.Material3.Button.TonalButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/pick_date"
            android:visibility="gone"
            app:icon="@drawable/ic_alarm" />

        <ImageView
            android:id="@+id/ivPhoto"
            android:layout_width="match_parent"
            android:layout_height="200dp"
            android:layout_marginTop="16dp"
            android:contentDescription="@string/photo"
            android:scaleType="centerCrop"
            android:visibility="gone" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:orientation="horizontal">

            <com.google.android.material.button.MaterialButton
                android:id="@+id/btnAddPhoto"
                style="@style/Widget.Material3.Button.OutlinedButton"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@string/add_photo"
                app:icon="@drawable/ic_image" />

            <com.google.android.material.button.MaterialButton
                android:id="@+id/btnRemovePhoto"
                style="@style/Widget.Material3.Button.TextButton"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="8dp"
                android:text="@string/remove_photo"
                android:visibility="gone" />
        </LinearLayout>

        <com.google.android.material.button.MaterialButton
            android:id="@+id/btnSave"
            android:layout_width="match_parent"
            android:layout_height="56dp"
            android:layout_marginTop="24dp"
            android:text="@string/save_note" />
    </LinearLayout>
</androidx.core.widget.NestedScrollView>
Kotlin – ui/editor/AddEditNoteFragment.kt
package com.codingeasily.studentdiary.ui.editor

import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.Toast
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.view.MenuProvider
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.bumptech.glide.Glide
import com.codingeasily.studentdiary.R
import com.codingeasily.studentdiary.data.Note
import com.codingeasily.studentdiary.databinding.FragmentAddEditNoteBinding
import com.codingeasily.studentdiary.ui.NoteViewModel
import com.codingeasily.studentdiary.util.DateUtils
import com.codingeasily.studentdiary.util.ImageStore
import com.google.android.material.datepicker.MaterialDatePicker
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.timepicker.MaterialTimePicker
import com.google.android.material.timepicker.TimeFormat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.util.Calendar
import java.util.TimeZone

/** Add a new note (noteId = -1) or edit an existing one. */
class AddEditNoteFragment : Fragment() {

    private var _binding: FragmentAddEditNoteBinding? = null
    private val binding get() = _binding!!
    private val viewModel: NoteViewModel by activityViewModels()

    private var existing: Note? = null
    private var dueAt: Long? = null
    private var imagePath: String? = null

    // Android Photo Picker – no storage permission needed
    private val pickImage = registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri: Uri? ->
        if (uri != null) copyImage(uri)
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        _binding = FragmentAddEditNoteBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // Subject dropdown (exposed dropdown menu = AutoCompleteTextView)
        val subjects = resources.getStringArray(R.array.subjects)
        binding.actvSubject.setAdapter(ArrayAdapter(requireContext(), android.R.layout.simple_list_item_1, subjects))

        binding.switchReminder.setOnCheckedChangeListener { _, checked ->
            binding.btnPickDate.isVisible = checked
            if (checked && dueAt == null) pickDateTime()
        }
        binding.btnPickDate.setOnClickListener { pickDateTime() }
        binding.btnAddPhoto.setOnClickListener {
            pickImage.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
        }
        binding.btnRemovePhoto.setOnClickListener { imagePath = null; showImage() }
        binding.btnSave.setOnClickListener { save() }

        val noteId = arguments?.getLong(ARG_NOTE_ID, -1L) ?: -1L
        if (noteId != -1L) {
            viewLifecycleOwner.lifecycleScope.launch {
                viewModel.getNote(noteId)?.let { fill(it) }
            }
            addDeleteMenu()
        } else {
            binding.actvSubject.setText(subjects.first(), false)
            binding.chipMedium.isChecked = true
        }
    }

    private fun fill(note: Note) {
        existing = note
        binding.etTitle.setText(note.title)
        binding.etContent.setText(note.content)
        binding.actvSubject.setText(note.subject, false)
        when (note.priority) {
            Note.PRIORITY_HIGH -> binding.chipHigh.isChecked = true
            Note.PRIORITY_LOW -> binding.chipLow.isChecked = true
            else -> binding.chipMedium.isChecked = true
        }
        binding.cbFavorite.isChecked = note.isFavorite
        dueAt = note.dueAt
        binding.switchReminder.isChecked = note.dueAt != null
        showDue()
        imagePath = note.imagePath
        showImage()
    }

    /** MaterialDatePicker → MaterialTimePicker → dueAt */
    private fun pickDateTime() {
        val datePicker = MaterialDatePicker.Builder.datePicker()
            .setTitleText(R.string.pick_date)
            .setSelection(MaterialDatePicker.todayInUtcMilliseconds())
            .build()
        datePicker.addOnPositiveButtonClickListener { utcMillis ->
            // The date picker returns midnight UTC – convert to a local calendar date
            val utc = Calendar.getInstance(TimeZone.getTimeZone("UTC")).apply { timeInMillis = utcMillis }
            val timePicker = MaterialTimePicker.Builder()
                .setTimeFormat(TimeFormat.CLOCK_12H)
                .setHour(9)
                .setMinute(0)
                .setTitleText(R.string.pick_time)
                .build()
            timePicker.addOnPositiveButtonClickListener {
                val local = Calendar.getInstance().apply {
                    set(utc.get(Calendar.YEAR), utc.get(Calendar.MONTH), utc.get(Calendar.DAY_OF_MONTH),
                        timePicker.hour, timePicker.minute, 0)
                    set(Calendar.MILLISECOND, 0)
                }
                dueAt = local.timeInMillis
                showDue()
            }
            timePicker.show(childFragmentManager, "time")
        }
        datePicker.addOnNegativeButtonClickListener { if (dueAt == null) binding.switchReminder.isChecked = false }
        datePicker.show(childFragmentManager, "date")
    }

    private fun showDue() {
        val due = dueAt
        binding.btnPickDate.isVisible = binding.switchReminder.isChecked
        binding.btnPickDate.text = if (due != null) DateUtils.format(due) else getString(R.string.pick_date)
    }

    private fun copyImage(uri: Uri) {
        viewLifecycleOwner.lifecycleScope.launch {
            val path = withContext(Dispatchers.IO) { ImageStore.copyToAppStorage(requireContext(), uri) }
            if (path == null) {
                Toast.makeText(requireContext(), R.string.image_error, Toast.LENGTH_SHORT).show()
            } else {
                imagePath = path
                showImage()
            }
        }
    }

    private fun showImage() {
        val path = imagePath
        binding.ivPhoto.isVisible = path != null
        binding.btnRemovePhoto.isVisible = path != null
        if (path != null) Glide.with(this).load(File(path)).centerCrop().into(binding.ivPhoto)
    }

    private fun save() {
        val title = binding.etTitle.text?.toString()?.trim().orEmpty()
        if (title.isEmpty()) {
            binding.tilTitle.error = getString(R.string.error_title)
            return
        }
        binding.tilTitle.error = null

        val reminderOn = binding.switchReminder.isChecked
        val due = dueAt
        if (reminderOn && (due == null || due < System.currentTimeMillis())) {
            Snackbar.make(binding.root, R.string.error_due, Snackbar.LENGTH_LONG).show()
            return
        }

        val priority = when (binding.chipGroupPriority.checkedChipId) {
            R.id.chipHigh -> Note.PRIORITY_HIGH
            R.id.chipLow -> Note.PRIORITY_LOW
            else -> Note.PRIORITY_MEDIUM
        }
        val base = existing ?: Note(title = title)
        val note = base.copy(
            title = title,
            content = binding.etContent.text?.toString()?.trim().orEmpty(),
            subject = binding.actvSubject.text.toString().ifBlank { "General" },
            priority = priority,
            dueAt = if (reminderOn) due else null,
            isFavorite = binding.cbFavorite.isChecked,
            imagePath = imagePath
        )
        binding.btnSave.isEnabled = false
        viewModel.save(note) {
            if (!isAdded) return@save            // user already left the screen
            Toast.makeText(requireContext(), R.string.saved, Toast.LENGTH_SHORT).show()
            findNavController().navigateUp()
        }
    }

    private fun addDeleteMenu() {
        requireActivity().addMenuProvider(object : MenuProvider {
            override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
                menuInflater.inflate(R.menu.menu_editor, menu)
            }
            override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
                if (menuItem.itemId != R.id.action_delete) return false
                val note = existing ?: return true
                MaterialAlertDialogBuilder(requireContext())
                    .setTitle(R.string.delete_note)
                    .setMessage(getString(R.string.delete_confirm, note.title))
                    .setNegativeButton(R.string.cancel, null)
                    .setPositiveButton(R.string.delete) { _, _ ->
                        viewModel.delete(note)
                        ImageStore.delete(note.imagePath)
                        findNavController().navigateUp()
                    }
                    .show()
                return true
            }
        }, viewLifecycleOwner, Lifecycle.State.RESUMED)
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }

    companion object {
        const val ARG_NOTE_ID = "noteId"
    }
}
Kotlin – util/ImageStore.kt
package com.codingeasily.studentdiary.util

import android.content.Context
import android.net.Uri
import java.io.File
import java.util.UUID

/**
 * The photo picker gives a temporary content:// Uri.
 * We copy the picture into the app's private folder so it stays available.
 */
object ImageStore {
    fun copyToAppStorage(context: Context, uri: Uri): String? = try {
        val dir = File(context.filesDir, "images").apply { mkdirs() }
        val file = File(dir, "${UUID.randomUUID()}.jpg")
        context.contentResolver.openInputStream(uri)?.use { input ->
            file.outputStream().use { output -> input.copyTo(output) }
        }
        file.absolutePath
    } catch (e: Exception) {
        null
    }

    fun delete(path: String?) {
        if (path != null) File(path).delete()
    }
}
Savevalidate title + timecopy() Notepriority, subject…viewModel.saveRoom insert/updateReminderSchedulerschedule/cancelnavigateUp()back to list
What happens on "Save note"

Step 7: Reminders

Kotlin – reminder/ReminderScheduler.kt
package com.codingeasily.studentdiary.reminder

import android.content.Context
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.workDataOf
import java.util.concurrent.TimeUnit

object ReminderScheduler {

    private fun workName(noteId: Long) = "reminder_$noteId"

    /** Schedules (or re-schedules) one reminder for a note. */
    fun schedule(context: Context, noteId: Long, atMillis: Long) {
        val delay = atMillis - System.currentTimeMillis()
        if (delay <= 0) return
        val request = OneTimeWorkRequestBuilder<ReminderWorker>()
            .setInitialDelay(delay, TimeUnit.MILLISECONDS)
            .setInputData(workDataOf(ReminderWorker.KEY_NOTE_ID to noteId))
            .build()
        WorkManager.getInstance(context)
            .enqueueUniqueWork(workName(noteId), ExistingWorkPolicy.REPLACE, request)
    }

    fun cancel(context: Context, noteId: Long) {
        WorkManager.getInstance(context).cancelUniqueWork(workName(noteId))
    }
}
Kotlin – reminder/ReminderWorker.kt
package com.codingeasily.studentdiary.reminder

import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.codingeasily.studentdiary.R
import com.codingeasily.studentdiary.data.AppDatabase
import com.codingeasily.studentdiary.data.SettingsStore

/** WorkManager runs this at the reminder time, even if the app was closed. */
class ReminderWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {

    override suspend fun doWork(): Result {
        val noteId = inputData.getLong(KEY_NOTE_ID, -1L)
        if (noteId == -1L || !SettingsStore(applicationContext).remindersEnabled) return Result.success()

        val note = AppDatabase.get(applicationContext).noteDao().getById(noteId)
            ?: return Result.success()          // note was deleted
        if (note.isDone) return Result.success()

        NotificationHelper.show(
            applicationContext,
            note.id,
            applicationContext.getString(R.string.reminder_title, note.title),
            note.content.ifBlank { note.subject }
        )
        return Result.success()
    }

    companion object {
        const val KEY_NOTE_ID = "note_id"
    }
}

NotificationHelper.kt is explained line by line in the Notifications lesson.

Step 8: Subjects grid

XML – layout/item_subject.xml
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView 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:layout_margin="6dp"
    app:cardCornerRadius="16dp">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:orientation="vertical"
        android:padding="20dp">

        <TextView
            android:id="@+id/tvLetter"
            android:layout_width="56dp"
            android:layout_height="56dp"
            android:background="@drawable/bg_circle_brand"
            android:gravity="center"
            android:textColor="@android:color/white"
            android:textSize="24sp"
            android:textStyle="bold" />

        <TextView
            android:id="@+id/tvSubject"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:textAppearance="?attr/textAppearanceTitleMedium" />

        <TextView
            android:id="@+id/tvCount"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textAppearance="?attr/textAppearanceBodySmall" />
    </LinearLayout>
</com.google.android.material.card.MaterialCardView>
Kotlin – ui/subjects/SubjectAdapter.kt
package com.codingeasily.studentdiary.ui.subjects

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.codingeasily.studentdiary.R
import com.codingeasily.studentdiary.data.SubjectCount
import com.codingeasily.studentdiary.databinding.ItemSubjectBinding

class SubjectAdapter(private val onClick: (SubjectCount) -> Unit) :
    ListAdapter<SubjectCount, SubjectAdapter.VH>(DIFF) {

    class VH(val binding: ItemSubjectBinding) : RecyclerView.ViewHolder(binding.root)

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

    override fun onBindViewHolder(holder: VH, position: Int) {
        val item = getItem(position)
        with(holder.binding) {
            tvLetter.text = item.subject.take(1).uppercase()
            tvSubject.text = item.subject
            tvCount.text = root.resources.getQuantityString(R.plurals.notes_count, item.total, item.total)
            root.setOnClickListener { onClick(item) }
        }
    }

    companion object {
        private val DIFF = object : DiffUtil.ItemCallback<SubjectCount>() {
            override fun areItemsTheSame(a: SubjectCount, b: SubjectCount) = a.subject == b.subject
            override fun areContentsTheSame(a: SubjectCount, b: SubjectCount) = a == b
        }
    }
}
Kotlin – ui/subjects/SubjectsFragment.kt
package com.codingeasily.studentdiary.ui.subjects

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.GridLayoutManager
import com.codingeasily.studentdiary.R
import com.codingeasily.studentdiary.databinding.FragmentSubjectsBinding
import com.codingeasily.studentdiary.ui.NoteFilter
import com.codingeasily.studentdiary.ui.NoteViewModel
import com.codingeasily.studentdiary.ui.notes.NoteListFragment

/** Grid of subjects with note counts (GROUP BY query). */
class SubjectsFragment : Fragment() {

    private var _binding: FragmentSubjectsBinding? = null
    private val binding get() = _binding!!
    private val viewModel: NoteViewModel by activityViewModels()

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        _binding = FragmentSubjectsBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val adapter = SubjectAdapter { item ->
            findNavController().navigate(
                R.id.action_subjects_to_subjectNotes,
                bundleOf(
                    NoteListFragment.ARG_FILTER to NoteFilter.SUBJECT.name,
                    NoteListFragment.ARG_SUBJECT to item.subject
                )
            )
        }
        binding.recyclerView.layoutManager = GridLayoutManager(requireContext(), 2)
        binding.recyclerView.adapter = adapter

        viewModel.subjectCounts.observe(viewLifecycleOwner) { list ->
            adapter.submitList(list)
            binding.emptyView.isVisible = list.isEmpty()
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

Step 9: Progress + tip of the day

XML – layout/fragment_stats.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView 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="match_parent">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="16dp">

        <!-- three number boxes in a row -->
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:baselineAligned="false"
            android:orientation="horizontal">

            <com.google.android.material.card.MaterialCardView
                style="@style/StatCard"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1">

                <LinearLayout style="@style/StatBox">
                    <TextView android:id="@+id/tvTotal" style="@style/StatNumber" />
                    <TextView style="@style/StatLabel" android:text="@string/stat_total" />
                </LinearLayout>
            </com.google.android.material.card.MaterialCardView>

            <com.google.android.material.card.MaterialCardView
                style="@style/StatCard"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_marginHorizontal="8dp"
                android:layout_weight="1">

                <LinearLayout style="@style/StatBox">
                    <TextView android:id="@+id/tvPending" style="@style/StatNumber" />
                    <TextView style="@style/StatLabel" android:text="@string/stat_pending" />
                </LinearLayout>
            </com.google.android.material.card.MaterialCardView>

            <com.google.android.material.card.MaterialCardView
                style="@style/StatCard"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1">

                <LinearLayout style="@style/StatBox">
                    <TextView android:id="@+id/tvDone" style="@style/StatNumber" />
                    <TextView style="@style/StatLabel" android:text="@string/stat_done" />
                </LinearLayout>
            </com.google.android.material.card.MaterialCardView>
        </LinearLayout>

        <TextView
            android:id="@+id/tvPercent"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="20dp"
            android:textAppearance="?attr/textAppearanceTitleMedium" />

        <com.google.android.material.progressindicator.LinearProgressIndicator
            android:id="@+id/progressDone"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:max="100"
            app:trackCornerRadius="6dp"
            app:trackThickness="12dp" />

        <!-- Tip of the day from https://codingeasily.in/api/android-tips.json -->
        <com.google.android.material.card.MaterialCardView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="24dp"
            app:cardCornerRadius="16dp">

            <FrameLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:minHeight="120dp">

                <com.google.android.material.progressindicator.CircularProgressIndicator
                    android:id="@+id/tipProgress"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center"
                    android:indeterminate="true" />

                <LinearLayout
                    android:id="@+id/tipContent"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:orientation="vertical"
                    android:visibility="gone">

                    <ImageView
                        android:id="@+id/ivTip"
                        android:layout_width="match_parent"
                        android:layout_height="160dp"
                        android:contentDescription="@string/tip_of_day"
                        android:scaleType="centerCrop" />

                    <LinearLayout
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:orientation="vertical"
                        android:padding="16dp">

                        <TextView
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:text="@string/tip_of_day"
                            android:textAppearance="?attr/textAppearanceLabelLarge"
                            android:textColor="@color/brand" />

                        <TextView
                            android:id="@+id/tvTipTitle"
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:layout_marginTop="4dp"
                            android:textAppearance="?attr/textAppearanceTitleLarge" />

                        <TextView
                            android:id="@+id/tvTipText"
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:layout_marginTop="4dp"
                            android:textAppearance="?attr/textAppearanceBodyMedium" />

                        <com.google.android.material.button.MaterialButton
                            android:id="@+id/btnReadMore"
                            style="@style/Widget.Material3.Button.TextButton"
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:text="@string/read_more" />
                    </LinearLayout>
                </LinearLayout>

                <LinearLayout
                    android:id="@+id/tipError"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center"
                    android:gravity="center"
                    android:orientation="vertical"
                    android:padding="16dp"
                    android:visibility="gone">

                    <TextView
                        android:id="@+id/tvTipError"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:gravity="center" />

                    <com.google.android.material.button.MaterialButton
                        android:id="@+id/btnRetry"
                        style="@style/Widget.Material3.Button.TonalButton"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="@string/retry" />
                </LinearLayout>
            </FrameLayout>
        </com.google.android.material.card.MaterialCardView>
    </LinearLayout>
</androidx.core.widget.NestedScrollView>
Kotlin – ui/stats/StatsFragment.kt
package com.codingeasily.studentdiary.ui.stats

import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import com.bumptech.glide.Glide
import com.codingeasily.studentdiary.R
import com.codingeasily.studentdiary.databinding.FragmentStatsBinding
import com.codingeasily.studentdiary.ui.NoteViewModel
import com.codingeasily.studentdiary.ui.TipState

/** Progress numbers from Room + "Tip of the day" from the internet (Retrofit + Glide). */
class StatsFragment : Fragment() {

    private var _binding: FragmentStatsBinding? = null
    private val binding get() = _binding!!
    private val viewModel: NoteViewModel by activityViewModels()

    private var total = 0
    private var done = 0

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        _binding = FragmentStatsBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        viewModel.countAll.observe(viewLifecycleOwner) { total = it; showProgress() }
        viewModel.countDone.observe(viewLifecycleOwner) { done = it; showProgress() }

        viewModel.tip.observe(viewLifecycleOwner) { state ->
            binding.tipProgress.isVisible = state is TipState.Loading
            binding.tipContent.isVisible = state is TipState.Success
            binding.tipError.isVisible = state is TipState.Error
            when (state) {
                is TipState.Success -> {
                    binding.tvTipTitle.text = state.tip.title
                    binding.tvTipText.text = state.tip.text
                    Glide.with(this).load(state.tip.image)
                        .placeholder(R.drawable.ic_image)
                        .error(R.drawable.ic_image)
                        .into(binding.ivTip)
                    binding.btnReadMore.setOnClickListener {
                        // Implicit intent: let the phone choose a browser
                        startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(state.tip.link)))
                    }
                }
                is TipState.Error -> binding.tvTipError.text = getString(R.string.tip_error, state.message)
                TipState.Loading -> Unit
            }
        }
        binding.btnRetry.setOnClickListener { viewModel.loadTip(force = true) }
        viewModel.loadTip()
    }

    private fun showProgress() {
        val pending = total - done
        val percent = if (total == 0) 0 else done * 100 / total
        binding.tvTotal.text = total.toString()
        binding.tvDone.text = done.toString()
        binding.tvPending.text = pending.toString()
        binding.progressDone.setProgressCompat(percent, true)
        binding.tvPercent.text = getString(R.string.percent_done, percent)
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

The API behind the tip (server-api/ folder)

By default the app reads the static file api/android-tips.json. The download also contains a full PHP + MySQL API in server-api/ – tips.php, database.sql and a password-protected admin.php to add or hide tips without updating the app. It returns the same JSON, so only TIPS_BASE_URL and TIPS_PATH in network/TipsApi.kt change.

PHP – server-api/tips.php
<?php
/* GET tips.php          -> {"tips":[ {id,title,text,image,link}, ... ]}
 * GET tips.php?id=3     -> {"tips":[ {...one tip...} ]}
 * GET tips.php?random=1 -> {"tips":[ {...one random tip...} ]}
 * Same JSON shape as android-tips.json, so the app's Tip / TipsResponse classes work unchanged. */
require_once __DIR__ . '/db.php';

if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
    sd_json(array('error' => 'Only GET is allowed'), 405);
}

try {
    $cols = 'id, title, text, image, link';
    if (isset($_GET['id'])) {
        $id = (int) $_GET['id'];
        $st = sd_db()->prepare("SELECT $cols FROM tips WHERE id = ? AND is_active = 1");
        $st->execute(array($id));
    } elseif (!empty($_GET['random'])) {
        $st = sd_db()->query("SELECT $cols FROM tips WHERE is_active = 1 ORDER BY RAND() LIMIT 1");
    } else {
        $st = sd_db()->query("SELECT $cols FROM tips WHERE is_active = 1 ORDER BY id");
    }
    $rows = $st->fetchAll();
    foreach ($rows as $i => $r) {
        $rows[$i]['id'] = (int) $r['id'];          // keep id numeric for Gson
    }
    if (isset($_GET['id']) && !$rows) {
        sd_json(array('error' => 'Tip not found', 'tips' => array()), 404);
    }
    sd_json(array('tips' => $rows));
} catch (Exception $e) {
    sd_json(array('error' => 'Database error – check config.php and import database.sql', 'tips' => array()), 500);
}

Step 10: Settings and Help

XML – layout/fragment_settings.xml
<?xml version="1.0" encoding="utf-8"?>
<ScrollView 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="match_parent">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="16dp">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/profile"
            android:textAppearance="?attr/textAppearanceTitleMedium" />

        <com.google.android.material.textfield.TextInputLayout
            android:id="@+id/tilName"
            style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:hint="@string/your_name"
            app:startIconDrawable="@drawable/ic_person">

            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/etName"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:inputType="textPersonName|textCapWords" />
        </com.google.android.material.textfield.TextInputLayout>

        <com.google.android.material.button.MaterialButton
            android:id="@+id/btnSaveName"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:text="@string/save" />

        <com.google.android.material.divider.MaterialDivider
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginVertical="16dp" />

        <com.google.android.material.materialswitch.MaterialSwitch
            android:id="@+id/switchDark"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:minHeight="56dp"
            android:text="@string/dark_mode" />

        <com.google.android.material.materialswitch.MaterialSwitch
            android:id="@+id/switchReminders"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:minHeight="56dp"
            android:text="@string/reminder_notifications" />

        <com.google.android.material.divider.MaterialDivider
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginVertical="16dp" />

        <com.google.android.material.button.MaterialButton
            android:id="@+id/btnClearDone"
            style="@style/Widget.Material3.Button.OutlinedButton"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@string/clear_done"
            app:icon="@drawable/ic_delete" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="24dp"
            android:gravity="center"
            android:text="@string/version_text"
            android:textAppearance="?attr/textAppearanceBodySmall" />
    </LinearLayout>
</ScrollView>
Kotlin – ui/settings/SettingsFragment.kt
package com.codingeasily.studentdiary.ui.settings

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import com.codingeasily.studentdiary.R
import com.codingeasily.studentdiary.data.SettingsStore
import com.codingeasily.studentdiary.databinding.FragmentSettingsBinding
import com.codingeasily.studentdiary.ui.NoteViewModel
import com.google.android.material.dialog.MaterialAlertDialogBuilder

/** Name, dark mode and reminder switch saved in SharedPreferences. */
class SettingsFragment : Fragment() {

    private var _binding: FragmentSettingsBinding? = null
    private val binding get() = _binding!!
    private val viewModel: NoteViewModel by activityViewModels()
    private lateinit var settings: SettingsStore

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        _binding = FragmentSettingsBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        settings = SettingsStore(requireContext())

        binding.etName.setText(settings.userName)
        binding.switchDark.isChecked = settings.darkMode
        binding.switchReminders.isChecked = settings.remindersEnabled

        binding.btnSaveName.setOnClickListener {
            val name = binding.etName.text?.toString()?.trim().orEmpty()
            if (name.isEmpty()) {
                binding.tilName.error = getString(R.string.error_name)
            } else {
                binding.tilName.error = null
                settings.userName = name
                Toast.makeText(requireContext(), R.string.saved, Toast.LENGTH_SHORT).show()
            }
        }

        binding.switchDark.setOnCheckedChangeListener { _, checked ->
            settings.darkMode = checked
            settings.applyTheme()           // the activity is recreated with the new theme
        }
        binding.switchReminders.setOnCheckedChangeListener { _, checked ->
            settings.remindersEnabled = checked
        }

        binding.btnClearDone.setOnClickListener {
            MaterialAlertDialogBuilder(requireContext())
                .setTitle(R.string.clear_done)
                .setMessage(R.string.clear_done_confirm)
                .setNegativeButton(R.string.cancel, null)
                .setPositiveButton(R.string.delete) { _, _ ->
                    viewModel.deleteDone { count ->
                        if (isAdded) Toast.makeText(
                            requireContext(),
                            resources.getQuantityString(R.plurals.deleted_count, count, count),
                            Toast.LENGTH_SHORT
                        ).show()
                    }
                }
                .show()
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

The Help screen (HelpFragment.kt + fragment_help.xml) is in the WebView lesson.

Step 11: Strings, themes and icons

XML – values/strings.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">Student Diary</string>
    <string name="tagline">Notes · Homework · Reminders</string>
    <string name="hello_user">Hello, %1$s 👋</string>

    <!-- menus -->
    <string name="menu_notes">Notes</string>
    <string name="menu_subjects">Subjects</string>
    <string name="menu_stats">Progress</string>
    <string name="menu_favourites">Favourites</string>
    <string name="menu_settings">Settings</string>
    <string name="menu_help">Help &amp; Tutorial</string>
    <string name="menu_more">More</string>
    <string name="share_app">Share app</string>
    <string name="share_text">I organise my homework with Student Diary – built in the free Android course at https://codingeasily.in/tutorial/android/</string>
    <string name="search">Search</string>
    <string name="search_hint">Search notes…</string>

    <!-- tabs -->
    <string name="tab_all">All</string>
    <string name="tab_pending">Pending</string>
    <string name="tab_done">Done</string>

    <!-- list -->
    <string name="add_note">Add note</string>
    <string name="empty_notes">No notes here yet.\nTap + to add one.</string>
    <string name="empty_subjects">Add a note to see your subjects.</string>
    <string name="note_deleted">\"%1$s\" deleted</string>
    <string name="undo">Undo</string>
    <string name="due_at">Due %1$s</string>
    <string name="mark_done">Mark as done</string>
    <string name="favourite">Favourite</string>
    <string name="photo">Photo</string>
    <plurals name="notes_count">
        <item quantity="one">%d note</item>
        <item quantity="other">%d notes</item>
    </plurals>

    <!-- editor -->
    <string name="note">Note</string>
    <string name="hint_title">Title</string>
    <string name="hint_content">Details</string>
    <string name="hint_subject">Subject</string>
    <string name="priority">Priority</string>
    <string name="priority_low">Low</string>
    <string name="priority_medium">Medium</string>
    <string name="priority_high">High</string>
    <string name="add_to_favourites">Add to favourites</string>
    <string name="reminder">Remind me</string>
    <string name="pick_date">Pick date &amp; time</string>
    <string name="pick_time">Pick time</string>
    <string name="add_photo">Add photo</string>
    <string name="remove_photo">Remove</string>
    <string name="save_note">Save note</string>
    <string name="saved">Saved</string>
    <string name="error_title">Please enter a title</string>
    <string name="error_due">Choose a reminder time in the future</string>
    <string name="image_error">Could not read that image</string>
    <string name="delete">Delete</string>
    <string name="cancel">Cancel</string>
    <string name="delete_note">Delete note?</string>
    <string name="delete_confirm">\"%1$s\" will be deleted permanently.</string>

    <!-- stats -->
    <string name="stat_total">Total</string>
    <string name="stat_pending">Pending</string>
    <string name="stat_done">Done</string>
    <string name="percent_done">%1$d%% completed</string>
    <string name="tip_of_day">Tip of the day</string>
    <string name="read_more">Read more</string>
    <string name="retry">Retry</string>
    <string name="tip_error">Could not load the tip (%1$s). Check your internet.</string>

    <!-- settings -->
    <string name="profile">Profile</string>
    <string name="your_name">Your name</string>
    <string name="save">Save</string>
    <string name="error_name">Name cannot be empty</string>
    <string name="dark_mode">Dark mode</string>
    <string name="reminder_notifications">Reminder notifications</string>
    <string name="clear_done">Delete all done notes</string>
    <string name="clear_done_confirm">All notes marked as done will be deleted.</string>
    <plurals name="deleted_count">
        <item quantity="one">%d note deleted</item>
        <item quantity="other">%d notes deleted</item>
    </plurals>
    <string name="version_text">Student Diary 1.0 · made in the CodingEasily Android course</string>

    <!-- notifications -->
    <string name="channel_reminders">Reminders</string>
    <string name="channel_reminders_desc">Homework and exam reminders</string>
    <string name="reminder_title">⏰ %1$s</string>
    <string name="notifications_off">Reminders will not appear until you allow notifications in Settings.</string>
</resources>
XML – values/arrays.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string-array name="subjects">
        <item>General</item>
        <item>Maths</item>
        <item>Physics</item>
        <item>Chemistry</item>
        <item>Biology</item>
        <item>English</item>
        <item>Hindi</item>
        <item>Computer</item>
        <item>History</item>
    </string-array>
</resources>
XML – menu/menu_notes.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <item
        android:id="@+id/action_search"
        android:icon="@drawable/ic_search"
        android:title="@string/search"
        app:actionViewClass="androidx.appcompat.widget.SearchView"
        app:showAsAction="ifRoom|collapseActionView" />
</menu>
XML – menu/menu_editor.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <item
        android:id="@+id/action_delete"
        android:icon="@drawable/ic_delete"
        android:title="@string/delete"
        app:showAsAction="ifRoom" />
</menu>
XML – drawable/ic_launcher_foreground.xml (app icon)
<?xml version="1.0" encoding="utf-8"?>
<!-- 108x108 adaptive icon foreground: a white notebook with a green check -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="108dp"
    android:height="108dp"
    android:viewportWidth="108"
    android:viewportHeight="108">
    <path android:fillColor="#FFFFFF" android:pathData="M38,30h32a4,4 0,0 1,4 4v40a4,4 0,0 1,-4 4h-32a4,4 0,0 1,-4 -4v-40a4,4 0,0 1,4 -4z" />
    <path android:fillColor="#BBF7D0" android:pathData="M34,34h6v40h-6z" />
    <path android:fillColor="#16A34A" android:pathData="M47,55l5,5 11,-12 3,3 -14,15 -8,-8z" />
</vector>

Themes (values/themes.xml, values-night/themes.xml) and colours are in the Material Theme lesson; all 20 vector icons are in the download ZIP.

Step 12: Test checklist

  1. 1Add 3 notes with different subjects and priorities → they appear in All and Pending
  2. 2Tick one as done → it moves to the Done tab and the Progress % changes
  3. 3Swipe a note → Snackbar → Undo brings it back
  4. 4Search in the toolbar filters every tab
  5. 5Set a reminder 2 minutes ahead, close the app → notification arrives and opens the app
  6. 6Attach a photo, restart the phone → photo still visible
  7. 7Turn on Dark mode → the whole app switches theme
  8. 8Rotate on every screen → nothing is lost
  9. 9Turn off internet → Progress shows the tip error with Retry
Run all nine before publishing

Make it your own

  • Add a timetable screen (Room table + weekly RecyclerView).
  • Backup notes to your own PHP API with Retrofit (you will build such an API in the e-commerce course).
  • Home-screen widget showing today's pending notes.
  • Translate to Hindi with values-hi/strings.xml.
✅ Next

Your app is complete. In the next lesson you will create a signed release bundle and publish it on Google Play.

Frequently Asked Questions

My reminder notification did not appear — why?
Check that notifications are allowed for the app, the reminder time is in the future, and "Reminder notifications" is on in Settings. Battery savers on some phones delay WorkManager by a few minutes.
How do I translate the app to Hindi?
Copy res/values/strings.xml to res/values-hi/strings.xml and translate the values. Phones set to Hindi pick it up automatically.
← Android Tutorial (Kotlin)
🔗

Share this topic with a friend

यह topic किसी दोस्त को भेजें

Found it useful? Send it to a classmate learning the same thing.

अच्छा लगा? जो दोस्त यही सीख रहा है, उसे भेज दीजिए।

Next topicSigned APK/AAB & Play Store Your progress: 41 / 43 (95%) · Lesson 42 / 43 Continue →

How was this lesson?

Your feedback helps us make the content better.

Write a review or suggestion (optional)

After approval, your name and city may appear publicly with your review. Please do not write your email or phone number.

▶ CodingEasily on YouTube

Learn with video lessons in easy Hindi

Featured video: RDBMS Part 3 – Class XI Commerce IT

▶ Subscribe – it’s free