Free tutorials & notes in Hindi & English · Clean code examples 🧰 मुफ़्त PDF और फ़ोटो टूल्स →
🏆 पूरी App और Publish  ·  Lesson 41

Project: सभी 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⋮🖼️जुड़ी हुई photo🖼 Add photoRemove⏰ 24 Nov 2026, 09:00 AMSAVE NOTE
Editor का ऊपर और नीचे का हिस्सा (NestedScrollView में scroll)
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()
    }
}
Savetitle + time validateNote copy()priority, subject…viewModel.saveRoom insert/updateReminderSchedulerschedule/cancelnavigateUp()list पर वापस
"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 Notifications lesson में line by line समझाया गया है।

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

Tip के पीछे की API (server-api/ folder)

Default में app static file api/android-tips.json पढ़ती है। Download में server-api/ folder में पूरी PHP + MySQL API भी है – tips.php, database.sql और password वाला admin.php, जिससे app update किए बिना tips जोड़ या छुपा सकते हैं। यह वही JSON लौटाती है, इसलिए network/TipsApi.kt में सिर्फ़ TIPS_BASE_URL और TIPS_PATH बदलने होते हैं।

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 और 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
    }
}

Help screen (HelpFragment.kt + fragment_help.xml) WebView lesson में है।

Step 11: Strings, themes और 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) और colours Material Theme lesson में हैं; सारे 20 vector icons download ZIP में हैं।

Step 12: Test checklist

  1. 1अलग subjects और priorities वाले 3 notes जोड़ें → All और Pending में दिखें
  2. 2एक को done tick करें → Done tab में जाए और Progress % बदले
  3. 3Note swipe करें → Snackbar → Undo से वापस
  4. 4Toolbar search हर tab filter करे
  5. 52 मिनट आगे का reminder लगाकर app बंद करें → notification आए और app खोले
  6. 6Photo जोड़कर phone restart करें → photo अब भी दिखे
  7. 7Dark mode on करें → पूरी app की theme बदले
  8. 8हर screen पर rotate करें → कुछ न खोए
  9. 9Internet बंद करें → Progress Retry के साथ tip error दिखाए
Publish से पहले नौ के नौ चलाएँ

इसे अपना बनाइए

  • Timetable screen जोड़ें (Room table + weekly RecyclerView)।
  • Retrofit से notes अपनी PHP API पर backup करें (ऐसी API आप e-commerce course में बनाएँगे)।
  • आज के pending notes दिखाने वाला home-screen widget।
  • values-hi/strings.xml से Hindi में translate करें।
✅ Next

आपकी app पूरी हो गई। अगले lesson में signed release bundle बनाकर Google Play पर publish करेंगे।

अक्सर पूछे जाने वाले प्रश्न (FAQ)

मेरा reminder notification क्यों नहीं आया?
Check करें कि app के लिए notifications allowed हैं, reminder time future में है, और Settings में "Reminder notifications" on है। कुछ phones के battery savers WorkManager को कुछ मिनट देर कर देते हैं।
App को Hindi में कैसे translate करें?
res/values/strings.xml को res/values-hi/strings.xml में copy करके values translate करें। Hindi पर set phones इसे अपने आप ले लेते हैं।
← Android Tutorial (Kotlin)
🔗

Share this topic with a friend

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

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

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

अगला topicSigned APK/AAB और Play Store आपकी प्रगति: 41 / 43 (95%) · Lesson 42 / 43 आगे बढ़ें →

यह lesson कैसा लगा?

आपकी राय से हम content और बेहतर बनाते हैं।

अपना review और सुझाव लिखें (वैकल्पिक)

Review approve होने के बाद आपका नाम और शहर इसके साथ public दिख सकते हैं। Email या phone न लिखें।

▶ CodingEasily on YouTube

Video से सीखें – आसान Hindi में

चुनी हुई video: RDBMS भाग 3 – Class XI Commerce IT

▶ Channel subscribe करें