🏆 पूरी App और Publish · Lesson 40
Project: Student Diary App (Plan और Setup)
जो app आप बनाएँगे
Student Diary एक पूरी homework और notes app है। इसमें इस course की लगभग हर चीज़ एक असली project में use होती है।
Features → lessons
| Feature | किस lesson से |
|---|---|
| Drawer + toolbar + bottom navigation + FAB | Navigation Drawer, Bottom Navigation, Menus |
| All / Pending / Done tabs | TabLayout + ViewPager2 |
| Swipe-to-delete + Undo, search वाली list | RecyclerView (advanced), Snackbar, Toolbar |
| Dropdown, chips, switch, pickers, photo वाला form | Inputs, Pickers, Camera & Gallery |
| Offline storage | Room |
| Flow + LiveData के साथ MVVM | MVVM, Coroutines |
| App बंद होने पर भी reminders | WorkManager, Notifications, Permissions |
| Internet से tip of the day | Retrofit, Glide |
| Dark mode, name, settings | Material Theme, SharedPreferences |
| Online help | WebView |
Architecture
Project structure
📁 StudentDiary/ ├─ settings.gradle.kts · build.gradle.kts · gradle/libs.versions.toml └─ 📁 app/src/main/ ├─ AndroidManifest.xml ├─ 📁 java/com/codingeasily/studentdiary/ │ ├─ StudentDiaryApp.kt theme + notification channel │ ├─ MainActivity.kt drawer + bottom nav + FAB │ ├─ 📁 data/ Note.kt NoteDao.kt AppDatabase.kt NoteRepository.kt SettingsStore.kt │ ├─ 📁 network/ Tip.kt TipsApi.kt │ ├─ 📁 reminder/ ReminderWorker.kt ReminderScheduler.kt NotificationHelper.kt │ ├─ 📁 ui/ NoteViewModel.kt │ │ ├─ 📁 notes/ NotesFragment.kt NoteListFragment.kt NoteAdapter.kt │ │ ├─ 📁 editor/ AddEditNoteFragment.kt │ │ ├─ 📁 subjects/ SubjectsFragment.kt SubjectAdapter.kt │ │ ├─ 📁 stats/ settings/ help/ │ └─ 📁 util/ DateUtils.kt ImageStore.kt └─ 📁 res/ layout (12) · menu (4) · navigation · values · values-night · drawable (20) · mipmap
Step 1: Gradle setup
नया project बनाएँ (Empty Views Activity, package com.codingeasily.studentdiary, minimum SDK 26), फिर version catalog और app build file बदलें:
TOML – gradle/libs.versions.toml
[versions]
agp = "8.7.3"
kotlin = "2.0.21"
ksp = "2.0.21-1.0.28"
coreKtx = "1.15.0"
appcompat = "1.7.0"
material = "1.12.0"
constraintlayout = "2.2.0"
recyclerview = "1.4.0"
viewpager2 = "1.1.0"
activityKtx = "1.9.3"
fragmentKtx = "1.8.5"
lifecycle = "2.8.7"
navigation = "2.8.5"
room = "2.6.1"
work = "2.10.0"
coroutines = "1.9.0"
retrofit = "2.11.0"
glide = "4.16.0"
junit = "4.13.2"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
androidx-recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "recyclerview" }
androidx-viewpager2 = { group = "androidx.viewpager2", name = "viewpager2", version.ref = "viewpager2" }
androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activityKtx" }
androidx-fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "fragmentKtx" }
androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycle" }
androidx-lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycle" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" }
androidx-navigation-fragment-ktx = { group = "androidx.navigation", name = "navigation-fragment-ktx", version.ref = "navigation" }
androidx-navigation-ui-ktx = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "navigation" }
androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
androidx-work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "work" }
kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" }
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
retrofit-converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" }
glide = { group = "com.github.bumptech.glide", name = "glide", version.ref = "glide" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }Kotlin DSL – app/build.gradle.kts
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.ksp) // generates Room code
}
android {
namespace = "com.codingeasily.studentdiary"
compileSdk = 35
defaultConfig {
applicationId = "com.codingeasily.studentdiary"
minSdk = 26
targetSdk = 35
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
viewBinding = true // generates ActivityMainBinding, FragmentNotesBinding, ...
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.recyclerview)
implementation(libs.androidx.viewpager2)
implementation(libs.androidx.activity.ktx)
implementation(libs.androidx.fragment.ktx)
implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.androidx.lifecycle.livedata.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.navigation.fragment.ktx)
implementation(libs.androidx.navigation.ui.ktx)
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.room.ktx)
ksp(libs.androidx.room.compiler)
implementation(libs.androidx.work.runtime.ktx)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.retrofit)
implementation(libs.retrofit.converter.gson)
implementation(libs.glide)
testImplementation(libs.junit)
}Step 2: Manifest और Application
XML – AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Needed for the Help screen (WebView) and the "Tip of the day" (Retrofit) -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- Android 13+ asks the user before showing notifications -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:name=".StudentDiaryApp"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.StudentDiary">
<activity
android:name=".MainActivity"
android:exported="true"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>Kotlin – StudentDiaryApp.kt
package com.codingeasily.studentdiary
import android.app.Application
import com.codingeasily.studentdiary.data.SettingsStore
import com.codingeasily.studentdiary.reminder.NotificationHelper
/**
* Runs once when the app process starts (declared in AndroidManifest.xml).
* We apply the saved light/dark theme and create the notification channel here.
*/
class StudentDiaryApp : Application() {
override fun onCreate() {
super.onCreate()
SettingsStore(this).applyTheme()
NotificationHelper.createChannel(this)
}
}Step 3: Data layer
Kotlin – data/Note.kt
package com.codingeasily.studentdiary.data
import androidx.room.Entity
import androidx.room.PrimaryKey
/** One row of the "notes" table. */
@Entity(tableName = "notes")
data class Note(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val title: String,
val content: String = "",
val subject: String = "General",
val priority: Int = PRIORITY_MEDIUM, // 0 = low, 1 = medium, 2 = high
val dueAt: Long? = null, // reminder time in millis, null = no reminder
val isDone: Boolean = false,
val isFavorite: Boolean = false,
val imagePath: String? = null, // photo copied into app storage
val createdAt: Long = System.currentTimeMillis()
) {
companion object {
const val PRIORITY_LOW = 0
const val PRIORITY_MEDIUM = 1
const val PRIORITY_HIGH = 2
}
}
/** Result of a GROUP BY query: how many notes each subject has. */
data class SubjectCount(val subject: String, val total: Int)Kotlin – data/NoteDao.kt
package com.codingeasily.studentdiary.data
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update
import kotlinx.coroutines.flow.Flow
/** All SQL for the notes table lives here. Room generates the implementation. */
@Dao
interface NoteDao {
// Flow = the UI is updated automatically whenever the table changes
@Query("SELECT * FROM notes ORDER BY isDone ASC, priority DESC, createdAt DESC")
fun getAll(): Flow<List<Note>>
@Query("SELECT * FROM notes WHERE isDone = :done ORDER BY priority DESC, createdAt DESC")
fun getByDone(done: Boolean): Flow<List<Note>>
@Query("SELECT * FROM notes WHERE isFavorite = 1 ORDER BY createdAt DESC")
fun getFavorites(): Flow<List<Note>>
@Query("SELECT * FROM notes WHERE subject = :subject ORDER BY createdAt DESC")
fun getBySubject(subject: String): Flow<List<Note>>
@Query("SELECT * FROM notes WHERE title LIKE '%' || :q || '%' OR content LIKE '%' || :q || '%' ORDER BY createdAt DESC")
fun search(q: String): Flow<List<Note>>
@Query("SELECT subject, COUNT(*) AS total FROM notes GROUP BY subject ORDER BY total DESC")
fun subjectCounts(): Flow<List<SubjectCount>>
@Query("SELECT COUNT(*) FROM notes")
fun countAll(): Flow<Int>
@Query("SELECT COUNT(*) FROM notes WHERE isDone = 1")
fun countDone(): Flow<Int>
@Query("SELECT * FROM notes WHERE id = :id")
suspend fun getById(id: Long): Note?
@Insert
suspend fun insert(note: Note): Long
@Update
suspend fun update(note: Note)
@Delete
suspend fun delete(note: Note)
@Query("DELETE FROM notes WHERE isDone = 1")
suspend fun deleteDone(): Int
}Kotlin – data/AppDatabase.kt
package com.codingeasily.studentdiary.data
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
@Database(entities = [Note::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract fun noteDao(): NoteDao
companion object {
@Volatile private var INSTANCE: AppDatabase? = null
/** Singleton: the whole app shares one database connection. */
fun get(context: Context): AppDatabase =
INSTANCE ?: synchronized(this) {
INSTANCE ?: Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"student_diary.db"
).build().also { INSTANCE = it }
}
}
}Kotlin – data/NoteRepository.kt
package com.codingeasily.studentdiary.data
import kotlinx.coroutines.flow.Flow
/**
* The single source of truth for notes. ViewModels talk to the repository,
* never directly to the DAO. Later you could add a network source here.
*/
class NoteRepository(private val dao: NoteDao) {
val allNotes: Flow<List<Note>> = dao.getAll()
val pendingNotes: Flow<List<Note>> = dao.getByDone(false)
val doneNotes: Flow<List<Note>> = dao.getByDone(true)
val favorites: Flow<List<Note>> = dao.getFavorites()
val subjectCounts: Flow<List<SubjectCount>> = dao.subjectCounts()
val countAll: Flow<Int> = dao.countAll()
val countDone: Flow<Int> = dao.countDone()
fun bySubject(subject: String) = dao.getBySubject(subject)
fun search(q: String) = dao.search(q)
suspend fun get(id: Long) = dao.getById(id)
suspend fun save(note: Note): Long =
if (note.id == 0L) dao.insert(note) else { dao.update(note); note.id }
suspend fun delete(note: Note) = dao.delete(note)
suspend fun deleteDone() = dao.deleteDone()
}Kotlin – util/DateUtils.kt
package com.codingeasily.studentdiary.util
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
object DateUtils {
private val full = SimpleDateFormat("dd MMM yyyy, hh:mm a", Locale.getDefault())
private val short = SimpleDateFormat("dd MMM", Locale.getDefault())
fun format(millis: Long): String = full.format(Date(millis))
fun formatShort(millis: Long): String = short.format(Date(millis))
}Step 4: Shared ViewModel
Kotlin – ui/NoteViewModel.kt
package com.codingeasily.studentdiary.ui
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import com.codingeasily.studentdiary.data.AppDatabase
import com.codingeasily.studentdiary.data.Note
import com.codingeasily.studentdiary.data.NoteRepository
import com.codingeasily.studentdiary.data.SubjectCount
import com.codingeasily.studentdiary.network.Tip
import com.codingeasily.studentdiary.network.TipsApi
import com.codingeasily.studentdiary.reminder.ReminderScheduler
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
/** Which notes a list screen shows. */
enum class NoteFilter { ALL, PENDING, DONE, FAVORITES, SUBJECT }
/** State of the "Tip of the day" card. */
sealed class TipState {
data object Loading : TipState()
data class Success(val tip: Tip) : TipState()
data class Error(val message: String) : TipState()
}
/**
* Shared by all screens (activityViewModels). Survives screen rotation.
* The UI observes LiveData; all database work runs in viewModelScope.
*/
class NoteViewModel(application: Application) : AndroidViewModel(application) {
private val repo = NoteRepository(AppDatabase.get(application).noteDao())
// ---------- lists ----------
private val searchQuery = MutableStateFlow("")
fun setSearchQuery(q: String) { searchQuery.value = q.trim() }
@OptIn(ExperimentalCoroutinesApi::class)
fun notes(filter: NoteFilter, subject: String? = null): LiveData<List<Note>> {
val source: Flow<List<Note>> = when (filter) {
NoteFilter.ALL -> repo.allNotes
NoteFilter.PENDING -> repo.pendingNotes
NoteFilter.DONE -> repo.doneNotes
NoteFilter.FAVORITES -> repo.favorites
NoteFilter.SUBJECT -> repo.bySubject(subject ?: "")
}
return searchQuery.flatMapLatest { q ->
if (q.isEmpty()) source
else source.map { list ->
list.filter { it.title.contains(q, ignoreCase = true) || it.content.contains(q, ignoreCase = true) }
}
}.asLiveData()
}
val subjectCounts: LiveData<List<SubjectCount>> = repo.subjectCounts.asLiveData()
val countAll: LiveData<Int> = repo.countAll.asLiveData()
val countDone: LiveData<Int> = repo.countDone.asLiveData()
// ---------- single note ----------
suspend fun getNote(id: Long): Note? = repo.get(id)
fun save(note: Note, onSaved: (Long) -> Unit) {
viewModelScope.launch {
val id = repo.save(note)
val due = note.dueAt
if (due != null && !note.isDone) ReminderScheduler.schedule(getApplication(), id, due)
else ReminderScheduler.cancel(getApplication(), id)
onSaved(id)
}
}
fun toggleDone(note: Note) = viewModelScope.launch {
repo.save(note.copy(isDone = !note.isDone))
if (!note.isDone) ReminderScheduler.cancel(getApplication(), note.id)
}
fun toggleFavorite(note: Note) = viewModelScope.launch {
repo.save(note.copy(isFavorite = !note.isFavorite))
}
fun delete(note: Note) = viewModelScope.launch {
repo.delete(note)
ReminderScheduler.cancel(getApplication(), note.id)
}
/** Used by the "Undo" button of the Snackbar. */
fun restore(note: Note) = viewModelScope.launch {
repo.save(note.copy(id = 0))
}
fun deleteDone(onDone: (Int) -> Unit) = viewModelScope.launch {
onDone(repo.deleteDone())
}
// ---------- tip of the day (Retrofit) ----------
private val _tip = MutableLiveData<TipState>()
val tip: LiveData<TipState> = _tip
fun loadTip(force: Boolean = false) {
if (!force && _tip.value is TipState.Success) return
_tip.value = TipState.Loading
viewModelScope.launch {
_tip.value = try {
val tips = TipsApi.service.getTips().tips
if (tips.isEmpty()) TipState.Error("No tips yet")
else TipState.Success(tips[(System.currentTimeMillis() / 86_400_000L % tips.size).toInt()])
} catch (e: Exception) {
TipState.Error(e.message ?: "Network error")
}
}
}
}Step 5: Notes list (tabs + RecyclerView)
XML – layout/fragment_note_list.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingTop="8dp"
android:paddingBottom="88dp"
tools:listitem="@layout/item_note" />
<LinearLayout
android:id="@+id/emptyView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone">
<ImageView
android:layout_width="96dp"
android:layout_height="96dp"
android:alpha="0.4"
android:contentDescription="@null"
android:src="@drawable/ic_note" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/empty_notes"
android:textAppearance="?attr/textAppearanceBodyLarge" />
</LinearLayout>
</FrameLayout>XML – layout/item_note.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_marginHorizontal="12dp"
android:layout_marginVertical="6dp"
app:cardCornerRadius="14dp"
app:cardElevation="2dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="76dp">
<!-- coloured strip shows priority -->
<View
android:id="@+id/priorityStrip"
android:layout_width="6dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/cbDone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:contentDescription="@string/mark_done"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/priorityStrip"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="?attr/textAppearanceTitleMedium"
app:layout_constraintEnd_toStartOf="@id/ivThumb"
app:layout_constraintStart_toEndOf="@id/cbDone"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvContent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="2"
android:textAppearance="?attr/textAppearanceBodyMedium"
app:layout_constraintEnd_toEndOf="@id/tvTitle"
app:layout_constraintStart_toStartOf="@id/tvTitle"
app:layout_constraintTop_toBottomOf="@id/tvTitle" />
<TextView
android:id="@+id/tvSubject"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:layout_marginBottom="12dp"
android:background="@drawable/bg_chip"
android:paddingHorizontal="10dp"
android:paddingVertical="2dp"
android:textAppearance="?attr/textAppearanceLabelMedium"
android:textColor="@color/brand"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@id/tvTitle"
app:layout_constraintTop_toBottomOf="@id/tvContent" />
<TextView
android:id="@+id/tvDue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:drawablePadding="4dp"
android:gravity="center_vertical"
android:textAppearance="?attr/textAppearanceLabelMedium"
app:drawableStartCompat="@drawable/ic_alarm"
app:layout_constraintBaseline_toBaselineOf="@id/tvSubject"
app:layout_constraintStart_toEndOf="@id/tvSubject" />
<ImageView
android:id="@+id/ivThumb"
android:layout_width="52dp"
android:layout_height="52dp"
android:layout_marginEnd="4dp"
android:contentDescription="@string/photo"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/btnFavorite"
app:layout_constraintTop_toTopOf="parent" />
<ImageButton
android:id="@+id/btnFavorite"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/favourite"
android:src="@drawable/ic_star_outline"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="@color/star" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>Kotlin – ui/notes/NoteListFragment.kt
package com.codingeasily.studentdiary.ui.notes
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.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.codingeasily.studentdiary.R
import com.codingeasily.studentdiary.databinding.FragmentNoteListBinding
import com.codingeasily.studentdiary.ui.NoteFilter
import com.codingeasily.studentdiary.ui.NoteViewModel
import com.google.android.material.snackbar.Snackbar
/**
* A reusable list screen. It is used:
* - inside the 3 tabs of NotesFragment (ALL / PENDING / DONE)
* - as the Favourites screen (drawer)
* - as the notes of one subject (from SubjectsFragment)
*/
class NoteListFragment : Fragment() {
private var _binding: FragmentNoteListBinding? = null
private val binding get() = _binding!! // valid only between onCreateView and onDestroyView
private val viewModel: NoteViewModel by activityViewModels()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentNoteListBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val filter = NoteFilter.valueOf(requireArguments().getString(ARG_FILTER, NoteFilter.ALL.name))
val subject = arguments?.getString(ARG_SUBJECT)
val adapter = NoteAdapter(
onClick = { note ->
findNavController().navigate(R.id.action_global_editor, bundleOf("noteId" to note.id))
},
onToggleDone = { viewModel.toggleDone(it) },
onToggleFavorite = { viewModel.toggleFavorite(it) }
)
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
binding.recyclerView.adapter = adapter
viewModel.notes(filter, subject).observe(viewLifecycleOwner) { list ->
adapter.submitList(list)
binding.emptyView.isVisible = list.isEmpty()
}
// Swipe left or right to delete, with UNDO
val swipe = object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) {
override fun onMove(rv: RecyclerView, vh: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder) = false
override fun onSwiped(vh: RecyclerView.ViewHolder, direction: Int) {
val position = vh.bindingAdapterPosition
if (position == RecyclerView.NO_POSITION) return
val note = adapter.currentList[position]
viewModel.delete(note)
Snackbar.make(binding.root, getString(R.string.note_deleted, note.title), Snackbar.LENGTH_LONG)
.setAction(R.string.undo) { viewModel.restore(note) }
.show()
}
}
ItemTouchHelper(swipe).attachToRecyclerView(binding.recyclerView)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null // avoid memory leaks: the view is gone, the fragment may live on
}
companion object {
const val ARG_FILTER = "filter"
const val ARG_SUBJECT = "subject"
fun newInstance(filter: NoteFilter) = NoteListFragment().apply {
arguments = bundleOf(ARG_FILTER to filter.name)
}
}
}Adapter (NoteAdapter.kt) और tabs host (NotesFragment.kt) पूरे RecyclerView advanced और TabLayout lessons में हैं, और main screen Navigation Drawer lesson में।
अगला part
- 1Part 2: Pickers, photo और reminders वाली Add/Edit note screen
- 2Subjects grid, Retrofit tip वाली Progress screen, Settings, Help
- 3Resources: strings, themes, icons — फिर सब test
- 4Release lesson: Google Play पर sign करके publish
✅ Checkpoint
इस part के बाद app चलाकर तीन खाली tabs और drawer देख सकते हैं। List में देखने के लिए StudentDiaryApp.onCreate में coroutine से अस्थायी रूप से एक note insert करें।
अक्सर पूछे जाने वाले प्रश्न (FAQ)
क्या पूरा Student Diary source code download कर सकते हैं?
हाँ। इस page का download card use करें। Folder को Android Studio में खोलें, Gradle sync होने दें, और Run दबाएँ।
क्या publish से पहले project compile हुआ था?
Kotlin sources syntax-check किए गए और हर view id, resource और class reference automated checker से verify किया गया। हमारा build server Android libraries download नहीं कर सकता, इसलिए पहला Gradle sync अपने Android Studio में चलाएँ; Studio नया AGP या library version suggest करे तो accept कर सकते हैं।
