/** * Room Database & Retrofit Networking — Practical Exercises * Mobile Application Development | SUZA | Semester II 2025/2026 * * Dependencies: * implementation("androidx.room:room-runtime:2.6.1") * implementation("androidx.room:room-ktx:2.6.1") * ksp("androidx.room:room-compiler:2.6.1") * implementation("com.squareup.retrofit2:retrofit:2.9.0") * implementation("com.squareup.retrofit2:converter-kotlinx-serialization:2.9.0") * implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0") * implementation("io.coil-kt:coil-compose:2.5.0") */ import android.content.Context import androidx.room.* import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import retrofit2.Retrofit import retrofit2.http.GET import retrofit2.http.Path // ============ ROOM ============ // 1. Entity @Entity(tableName = "tasks") data class Task( @PrimaryKey(autoGenerate = true) val id: Int = 0, val title: String, val done: Boolean = false, val priority: Int = 1 // 1=Low, 2=Med, 3=High ) // 2. DAO @Dao interface TaskDao { @Insert suspend fun insert(task: Task): Long @Update suspend fun update(task: Task) @Delete suspend fun delete(task: Task) @Query("SELECT * FROM tasks ORDER BY done ASC, priority DESC, id DESC") fun observeAll(): Flow> @Query("SELECT * FROM tasks WHERE id = :id") suspend fun getById(id: Int): Task? @Query("SELECT COUNT(*) FROM tasks WHERE done = 0") fun pendingCount(): Flow } // 3. Database @Database(entities = [Task::class], version = 1, exportSchema = false) abstract class TaskDatabase : RoomDatabase() { abstract fun dao(): TaskDao companion object { @Volatile private var INSTANCE: TaskDatabase? = null fun get(ctx: Context): TaskDatabase = INSTANCE ?: synchronized(this) { Room.databaseBuilder(ctx, TaskDatabase::class.java, "tasks.db").build() .also { INSTANCE = it } } } } // 4. Repository class TaskRepository(private val dao: TaskDao) { val tasks = dao.observeAll() val pending = dao.pendingCount() suspend fun add(t: Task) = dao.insert(t) suspend fun toggle(t: Task) = dao.update(t.copy(done = !t.done)) suspend fun remove(t: Task) = dao.delete(t) } // 5. ViewModel class TaskViewModel(private val repo: TaskRepository) : ViewModel() { val tasks: StateFlow> = repo.tasks .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList()) fun add(title: String, priority: Int) = viewModelScope.launch { repo.add(Task(title = title, priority = priority)) } fun toggle(task: Task) = viewModelScope.launch { repo.toggle(task) } fun remove(task: Task) = viewModelScope.launch { repo.remove(task) } } // ============ RETROFIT ============ // 6. DTO models — GitHub API example @Serializable data class GitHubUser( @SerialName("login") val login: String, @SerialName("name") val name: String? = null, @SerialName("avatar_url") val avatarUrl: String, @SerialName("public_repos") val publicRepos: Int, @SerialName("followers") val followers: Int, @SerialName("bio") val bio: String? = null ) @Serializable data class GitHubRepo( val id: Long, val name: String, @SerialName("full_name") val fullName: String, @SerialName("stargazers_count") val stars: Int, val language: String? = null, val description: String? = null ) // 7. API interface interface GitHubApi { @GET("users/{username}") suspend fun getUser(@Path("username") username: String): GitHubUser @GET("users/{username}/repos") suspend fun getRepos(@Path("username") username: String): List } // 8. Retrofit client object GitHubClient { private val json = Json { ignoreUnknownKeys = true } // Uncomment when wired into an Android module with full dependencies: // val api: GitHubApi = Retrofit.Builder() // .baseUrl("https://api.github.com/") // .addConverterFactory( // json.asConverterFactory("application/json".toMediaType()) // ) // .build() // .create(GitHubApi::class.java) } // 9. UI state + ViewModel sealed interface GitHubUiState { object Idle : GitHubUiState object Loading : GitHubUiState data class Success(val user: GitHubUser, val repos: List) : GitHubUiState data class Error(val message: String) : GitHubUiState } class GitHubViewModel(private val api: GitHubApi) : ViewModel() { var state by mutableStateOf(GitHubUiState.Idle) private set fun load(username: String) = viewModelScope.launch { state = GitHubUiState.Loading state = try { val user = api.getUser(username) val repos = api.getRepos(username).sortedByDescending { it.stars } GitHubUiState.Success(user, repos.take(20)) } catch (t: Throwable) { GitHubUiState.Error(t.message ?: "Unknown error") } } } // 10. EXERCISE — complete the NoteRepository below // TODO: Given NoteDao with observeAll(): Flow> and insert/update/delete, // write a NoteRepository that: // a) exposes a computed Flow> sorted by updatedAt descending // b) provides addNote(title, body) that sets updatedAt = System.currentTimeMillis() // c) provides pin(note) and unpin(note) — hint: add a "pinned" Boolean // field to Note (and update the DAO sort) so pinned notes float to the top // d) search(query) returning Flow> (use .map + .filter)