The repository pattern helps organize data access in your app. It acts like a middleman between your app and data sources, making your code cleaner and easier to manage.
Repository pattern in depth in Android Kotlin
interface UserRepository {
suspend fun getUser(userId: String): User
suspend fun saveUser(user: User)
}
class UserRepositoryImpl(
private val localDataSource: UserLocalDataSource,
private val remoteDataSource: UserRemoteDataSource
) : UserRepository {
override suspend fun getUser(userId: String): User {
val localUser = localDataSource.getUser(userId)
return localUser ?: remoteDataSource.getUser(userId).also {
localDataSource.saveUser(it)
}
}
override suspend fun saveUser(user: User) {
localDataSource.saveUser(user)
remoteDataSource.saveUser(user)
}
}The UserRepository interface defines what data actions are possible.
The UserRepositoryImpl class handles where data comes from and where it is saved.
interface ProductRepository {
suspend fun getProduct(productId: String): Product
}class ProductRepositoryImpl( private val localDataSource: ProductLocalDataSource, private val remoteDataSource: ProductRemoteDataSource ) : ProductRepository { override suspend fun getProduct(productId: String): Product { return localDataSource.getProduct(productId) ?: remoteDataSource.getProduct(productId) } }
class FakeUserRepository : UserRepository { private val fakeData = mutableMapOf<String, User>() override suspend fun getUser(userId: String): User = fakeData[userId] ?: User(userId, "Fake User") override suspend fun saveUser(user: User) { fakeData[user.id] = user } }
This example shows a repository that first checks local storage for a user. If not found, it fetches from remote and saves locally. It also saves users to both places.
data class User(val id: String, val name: String) interface UserLocalDataSource { suspend fun getUser(userId: String): User? suspend fun saveUser(user: User) } interface UserRemoteDataSource { suspend fun getUser(userId: String): User suspend fun saveUser(user: User) } interface UserRepository { suspend fun getUser(userId: String): User suspend fun saveUser(user: User) } class UserRepositoryImpl( private val localDataSource: UserLocalDataSource, private val remoteDataSource: UserRemoteDataSource ) : UserRepository { override suspend fun getUser(userId: String): User { val localUser = localDataSource.getUser(userId) return localUser ?: remoteDataSource.getUser(userId).also { localDataSource.saveUser(it) } } override suspend fun saveUser(user: User) { localDataSource.saveUser(user) remoteDataSource.saveUser(user) } } // Fake implementations for demo class FakeLocalDataSource : UserLocalDataSource { private val storage = mutableMapOf<String, User>() override suspend fun getUser(userId: String): User? = storage[userId] override suspend fun saveUser(user: User) { storage[user.id] = user } } class FakeRemoteDataSource : UserRemoteDataSource { private val storage = mutableMapOf<String, User>("1" to User("1", "Alice")) override suspend fun getUser(userId: String): User = storage[userId] ?: User(userId, "Unknown") override suspend fun saveUser(user: User) { storage[user.id] = user } } suspend fun main() { val local = FakeLocalDataSource() val remote = FakeRemoteDataSource() val repo = UserRepositoryImpl(local, remote) val user = repo.getUser("1") println("User name: ${user.name}") repo.saveUser(User("2", "Bob")) val user2 = repo.getUser("2") println("User2 name: ${user2.name}") }
Repository pattern helps keep your app's data logic clean and testable.
Use interfaces so you can swap real and fake data sources easily.
Remember to handle errors and loading states when fetching data.
The repository pattern separates data access from UI code.
It can combine multiple data sources like local and remote.
It makes testing easier by allowing fake data sources.
Practice
Repository pattern in Android Kotlin development?Solution
Step 1: Understand the role of Repository
The Repository pattern abstracts data sources and provides a clean API for data access.Step 2: Identify separation of concerns
This pattern separates data handling from UI code, improving maintainability and testability.Final Answer:
To separate data access logic from UI components -> Option AQuick Check:
Repository pattern = separation of data and UI [OK]
- Confusing Repository with UI or navigation components
- Thinking Repository manages UI animations
- Assuming Repository handles app styling
Solution
Step 1: Identify interface syntax in Kotlin
Interfaces use theinterfacekeyword and declare functions without bodies.Step 2: Check each option
interface UserRepository { fun getUser(id: Int): User } correctly declares an interface with a function signature. class UserRepository { fun getUser(id: Int): User } is a class, not interface. fun UserRepository() { return User } is invalid syntax. object UserRepository { val user = User() } is an object, not an interface.Final Answer:
interface UserRepository { fun getUser(id: Int): User } -> Option DQuick Check:
Kotlin interface syntax = interface UserRepository { fun getUser(id: Int): User } [OK]
- Using class instead of interface for Repository declaration
- Writing invalid function syntax inside interface
- Confusing object declaration with interface
class UserRepository {
fun getUserName(id: Int): String {
return if (id == 1) "Alice" else "Unknown"
}
}
fun main() {
val repo = UserRepository()
println(repo.getUserName(1))
println(repo.getUserName(2))
}Solution
Step 1: Analyze getUserName function
If the id is 1, it returns "Alice", otherwise "Unknown".Step 2: Trace main function calls
Calling getUserName(1) returns "Alice". Calling getUserName(2) returns "Unknown".Final Answer:
Alice\nUnknown -> Option CQuick Check:
id=1 returns Alice, else Unknown [OK]
- Mixing up return values for different ids
- Assuming numeric output instead of strings
- Confusing order of print statements
class UserRepository {
fun getUser(id: Int): User {
return User()
}
}
class User(val name: String)
fun main() {
val repo = UserRepository()
val user = repo.getUser(1)
println(user.name)
}Solution
Step 1: Check User class constructor
User class requires anameparameter in its constructor.Step 2: Check UserRepository getUser return
getUser returnsUser()without passingname, causing a compile error.Final Answer:
User() requires a name parameter but none provided -> Option BQuick Check:
Missing constructor argument causes error [OK]
- Ignoring required constructor parameters
- Thinking class must be interface for Repository
- Assuming println can't print properties
Solution
Step 1: Understand Repository role with multiple data sources
The Repository should abstract and combine data from local and remote sources.Step 2: Evaluate options for separation and abstraction
Create a Repository that combines local and remote data sources and exposes a unified API correctly combines data sources and exposes a clean API. The other options break separation or ignore data sources.Final Answer:
Create a Repository that combines local and remote data sources and exposes a unified API -> Option AQuick Check:
Repository unifies data sources for UI [OK]
- Accessing data sources directly from UI
- Ignoring one data source completely
- Mixing data calls in UI instead of Repository
Ready to prove it?
Pass the test and this pattern moves unknown
