Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:17:22 +08:00
commit 2c8fd6d9a0
22 changed files with 8353 additions and 0 deletions

View File

@@ -0,0 +1,180 @@
---
name: android-code-review
description: Comprehensive Android Kotlin code review for Payoo Android app. Checks Kotlin best practices, coroutines, Clean Architecture, MVVM patterns, lifecycle management, dependency injection, memory management, security, and performance. Use when reviewing Kotlin files, pull requests, or ViewModels, Activities, Fragments, UseCases, and Repositories.
allowed-tools: Read, Grep, Glob
---
# Android Code Review
Expert Android code reviewer for Payoo Android application, specializing in Kotlin, Coroutines, Jetpack components, and Clean Architecture patterns.
## When to Activate
- "review android code", "check android file", "review android PR"
- Mentions Kotlin/Java files: Activity, Fragment, ViewModel, UseCase, Repository
- "code quality", "best practices", "check android standards"
- Coroutines, Clean Architecture, MVVM patterns, Jetpack components
## Review Process
### Step 1: Identify Scope
Determine what to review:
- Specific files (e.g., "PaymentViewModel.kt")
- Directories (e.g., "payment module")
- Git changes (recent commits, PR diff)
- Entire module or feature
### Step 2: Read and Analyze
Use Read tool to examine files, checking against 7 core categories.
### Step 3: Apply Standards
#### 1. Naming Conventions ✅
- **Types**: PascalCase, descriptive (e.g., `PaymentViewModel`)
- **Variables**: camelCase (e.g., `paymentAmount`, `isLoading`)
- **Constants**: UPPER_SNAKE_CASE (e.g., `MAX_RETRY_COUNT`)
- **Booleans**: Prefix with `is`, `has`, `should`, `can`
- **No abbreviations** except URL, ID, API, HTTP, UI
- **Views**: Include type suffix (e.g., `amountEditText`, `submitButton`)
#### 2. Kotlin Best Practices 🎯
- **Null Safety**: Use `?` and `!!` appropriately, prefer safe calls
- **Data Classes**: Use for DTOs and models
- **Sealed Classes**: For state management and result types
- **Extension Functions**: For reusable utilities
- **Scope Functions**: Use `let`, `apply`, `run`, `also`, `with` correctly
- **Immutability**: Prefer `val` over `var`
#### 3. Coroutines Patterns 🔄
- **Scope**: Use `viewModelScope`, `lifecycleScope` appropriately
- **Dispatchers**: `Dispatchers.IO` for network/DB, `Dispatchers.Main` for UI
- **Cancellation**: All coroutines properly cancelled
- **Error Handling**: Use `try-catch` or `runCatching` in coroutines
- **Flows**: Use `StateFlow`, `SharedFlow` for reactive data
- **No blocking**: Never use `runBlocking` in production code
#### 4. Clean Architecture 🏗️
- **Flow**: ViewModel → UseCase → Repository → DataSource (API/DB)
- **ViewModels**: Extend `ViewModel`, expose UI state via `StateFlow`
- **UseCases**: Contain business logic, single responsibility
- **Repositories**: Abstract data sources
- **DI**: Dependencies injected (Dagger/Hilt/Koin)
- **Layers**: Strict separation (Presentation/Domain/Data)
#### 5. Lifecycle Management 🔁
- **ViewModels**: Don't hold Activity/Fragment references
- **Observers**: Use `viewLifecycleOwner` in Fragments
- **Coroutines**: Launch in lifecycle-aware scopes
- **Resources**: Clean up in `onDestroy` or `onCleared`
- **Configuration Changes**: Handle properly
#### 6. Security 🔒
- **Sensitive Data**: Use EncryptedSharedPreferences
- **API Keys**: Never hardcode, use BuildConfig
- **Network**: HTTPS only, certificate pinning if needed
- **Logs**: No sensitive data in production logs
- **Input Validation**: Sanitize user inputs
- **ProGuard/R8**: Proper obfuscation rules
#### 7. Performance ⚡
- **Background Work**: Network and DB on IO dispatcher
- **Memory Leaks**: No Activity/Context leaks
- **RecyclerView**: Use DiffUtil, ViewBinding
- **Images**: Use Coil/Glide with proper caching
- **Database**: Room queries optimized with indexes
- **Lazy Loading**: Load data on demand
### Step 4: Generate Report
Provide structured output with:
- **Summary**: Issue counts by severity (🔴 Critical, 🟠 High, 🟡 Medium, 🟢 Low)
- **Issues by category**: Organized findings
- **Code examples**: Current vs. fixed code
- **Explanations**: Why it matters
- **Recommendations**: Prioritized actions
## Severity Levels
🔴 **Critical** - Fix immediately
- Memory leaks (Activity/Context references)
- Coroutines not cancelled → Resource leak
- Sensitive data in plain SharedPreferences
- UI updates on background thread → Crash risk
- Hardcoded API keys or secrets
🟠 **High Priority** - Fix soon
- Missing error handling in coroutines
- Wrong Dispatcher usage
- ViewModel calling repository directly (skip UseCase)
- Business logic in ViewModel/Activity
- No ProGuard rules for critical code
🟡 **Medium Priority** - Should improve
- Not using lifecycle-aware components
- Poor naming conventions
- Not using data classes for models
- Missing null safety checks
- Inefficient RecyclerView usage
🟢 **Low Priority** - Nice to have
- Inconsistent code style
- Could use more extension functions
- Documentation improvements
## Output Format
```markdown
# Android Code Review Report
## Summary
- 🔴 Critical: X | 🟠 High: X | 🟡 Medium: X | 🟢 Low: X
- By category: Naming: X, Kotlin: X, Coroutines: X, Architecture: X, etc.
## Critical Issues
### 🔴 [Category] - [Issue Title]
**File**: `path/to/file.kt:line`
**Current**:
```kotlin
// problematic code
```
**Fix**:
```kotlin
// corrected code
```
**Why**: [Explanation of impact]
---
## Recommendations
1. Fix all critical issues immediately
2. Address high priority before next release
3. Plan medium priority for next sprint
## Positive Observations
✅ [Acknowledge well-written code]
```
## Quick Reference
**Standards**: See `standards.md` in this skill directory for detailed Android coding standards including:
- Kotlin best practices and idioms
- Coroutines patterns and anti-patterns
- Clean Architecture implementation
- Dependency injection patterns
- Security guidelines
- Performance optimization techniques
**Examples**: See `examples.md` for extensive code examples, common issues, and recommended patterns.
## Tips
- **Be thorough**: Check all 7 categories
- **Be specific**: Reference exact line numbers
- **Be constructive**: Explain why, not just what
- **Be practical**: Prioritize by severity
- **Be encouraging**: Acknowledge good code
- **Context matters**: Consider app-specific requirements

View File

@@ -0,0 +1,845 @@
# Android Code Review Examples
Real-world examples of common issues and recommended patterns for Android Kotlin development.
## Example 1: Memory Leak - Activity Reference in ViewModel
### ❌ BAD: Holding Activity Reference
```kotlin
// PaymentViewModel.kt
class PaymentViewModel(
private val activity: PaymentActivity // Memory leak!
) : ViewModel() {
fun onPaymentComplete() {
activity.showSuccessDialog() // Crash if Activity destroyed!
activity.finish()
}
}
// PaymentActivity.kt
class PaymentActivity : AppCompatActivity() {
private val viewModel by viewModels<PaymentViewModel> {
PaymentViewModelFactory(this) // Passing Activity reference!
}
}
```
**Issues:**
- 🔴 **Critical**: Memory leak - ViewModel outlives Activity
- 🔴 **Critical**: Crash risk if Activity destroyed but ViewModel still active
- 🟠 **High**: Tight coupling between ViewModel and View
### ✅ GOOD: Event-Based Navigation
```kotlin
// PaymentViewModel.kt
class PaymentViewModel : ViewModel() {
private val _events = MutableSharedFlow<PaymentEvent>()
val events: SharedFlow<PaymentEvent> = _events.asSharedFlow()
fun onPaymentComplete() {
viewModelScope.launch {
_events.emit(PaymentEvent.ShowSuccessDialog)
_events.emit(PaymentEvent.Finish)
}
}
}
sealed class PaymentEvent {
object ShowSuccessDialog : PaymentEvent()
object Finish : PaymentEvent()
data class NavigateTo(val destination: String) : PaymentEvent()
}
// PaymentActivity.kt
@AndroidEntryPoint
class PaymentActivity : AppCompatActivity() {
@Inject
lateinit var viewModel: PaymentViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
viewModel.events.collect { event ->
when (event) {
PaymentEvent.ShowSuccessDialog -> showSuccessDialog()
PaymentEvent.Finish -> finish()
is PaymentEvent.NavigateTo -> navigate(event.destination)
}
}
}
}
}
```
**Benefits:**
- ✅ No memory leaks - ViewModel doesn't hold Activity reference
- ✅ Lifecycle-safe - Events only processed when Activity is active
- ✅ Testable - ViewModel logic can be tested independently
---
## Example 2: Coroutine Scope and Cancellation
### ❌ BAD: GlobalScope and No Cancellation
```kotlin
class PaymentViewModel : ViewModel() {
fun loadPayments() {
GlobalScope.launch { // Never cancelled!
val payments = repository.getPayments()
_payments.value = payments // Can crash if ViewModel cleared
}
}
fun processPayment(request: PaymentRequest) {
GlobalScope.launch(Dispatchers.Main) {
val result = repository.processPayment(request) // Blocks UI thread!
_result.value = result
}
}
}
```
**Issues:**
- 🔴 **Critical**: GlobalScope coroutines never cancelled - resource leak
- 🔴 **Critical**: Network/DB on Main thread - ANR risk
- 🟠 **High**: Crash risk when updating UI after ViewModel cleared
### ✅ GOOD: ViewModelScope and Proper Dispatchers
```kotlin
class PaymentViewModel(
private val repository: PaymentRepository
) : ViewModel() {
private val _uiState = MutableStateFlow<UiState<List<Payment>>>(UiState.Loading)
val uiState: StateFlow<UiState<List<Payment>>> = _uiState.asStateFlow()
fun loadPayments() {
viewModelScope.launch {
_uiState.value = UiState.Loading
try {
// Repository handles IO dispatcher internally
val payments = repository.getPayments()
_uiState.value = UiState.Success(payments)
} catch (e: IOException) {
_uiState.value = UiState.Error("Network error: ${e.message}")
} catch (e: Exception) {
_uiState.value = UiState.Error("Unexpected error: ${e.message}")
}
}
}
fun processPayment(request: PaymentRequest) {
viewModelScope.launch {
_uiState.value = UiState.Loading
runCatching {
repository.processPayment(request)
}.onSuccess { result ->
_uiState.value = UiState.Success(result)
}.onFailure { error ->
_uiState.value = UiState.Error(error.message ?: "Unknown error")
}
}
}
}
// Repository with proper dispatcher
class PaymentRepositoryImpl(
private val api: PaymentApi,
private val dao: PaymentDao,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : PaymentRepository {
override suspend fun getPayments(): List<Payment> = withContext(ioDispatcher) {
api.fetchPayments()
}
override suspend fun processPayment(request: PaymentRequest): PaymentResult =
withContext(ioDispatcher) {
api.processPayment(request)
}
}
```
**Benefits:**
- ✅ Automatic cancellation when ViewModel cleared
- ✅ Proper thread management - IO work on background
- ✅ Comprehensive error handling
- ✅ Single source of truth for UI state
---
## Example 3: Clean Architecture Violation
### ❌ BAD: ViewModel with Business Logic and Direct API Calls
```kotlin
class PaymentViewModel(
private val api: PaymentApi // Wrong layer!
) : ViewModel() {
private val _result = MutableLiveData<PaymentResult>()
val result: LiveData<PaymentResult> = _result
fun processPayment(amount: String, merchantId: String) {
viewModelScope.launch {
// Business logic in ViewModel - WRONG!
val amountBigDecimal = try {
BigDecimal(amount)
} catch (e: NumberFormatException) {
_result.value = PaymentResult.Error("Invalid amount")
return@launch
}
if (amountBigDecimal < BigDecimal("1000")) {
_result.value = PaymentResult.Error("Minimum amount is 1000 VND")
return@launch
}
val fee = amountBigDecimal * BigDecimal("0.02") // Business logic!
val total = amountBigDecimal + fee
// Calling API directly - WRONG!
try {
val response = api.processPayment(
PaymentRequest(
amount = total,
merchantId = merchantId,
timestamp = System.currentTimeMillis()
)
)
_result.value = PaymentResult.Success(response)
} catch (e: Exception) {
_result.value = PaymentResult.Error(e.message ?: "Error")
}
}
}
}
```
**Issues:**
- 🟠 **High**: Business logic in ViewModel (validation, fee calculation)
- 🟠 **High**: ViewModel depends on Data layer (API) directly
- 🟡 **Medium**: No UseCase - business logic not reusable
- 🟡 **Medium**: Mixing concerns - validation, calculation, networking
### ✅ GOOD: Proper Clean Architecture Layers
```kotlin
// Domain Layer - UseCase
class ProcessPaymentUseCase(
private val repository: PaymentRepository,
private val validator: PaymentValidator,
private val feeCalculator: FeeCalculator
) {
suspend operator fun invoke(
amount: String,
merchantId: String
): Result<PaymentResult> = runCatching {
// Business logic in UseCase
val amountBigDecimal = validator.parseAndValidateAmount(amount)
validator.validateMerchant(merchantId)
val fee = feeCalculator.calculateFee(amountBigDecimal)
val total = amountBigDecimal + fee
val request = PaymentRequest(
amount = total,
merchantId = merchantId,
timestamp = System.currentTimeMillis()
)
repository.processPayment(request)
}
}
// Domain Layer - Validators and Calculators
class PaymentValidator {
fun parseAndValidateAmount(amount: String): BigDecimal {
val amountBigDecimal = amount.toBigDecimalOrNull()
?: throw IllegalArgumentException("Invalid amount format")
if (amountBigDecimal < MIN_AMOUNT) {
throw IllegalArgumentException("Minimum amount is $MIN_AMOUNT VND")
}
return amountBigDecimal
}
fun validateMerchant(merchantId: String) {
if (merchantId.isBlank()) {
throw IllegalArgumentException("Invalid merchant ID")
}
}
companion object {
private val MIN_AMOUNT = BigDecimal("1000")
}
}
class FeeCalculator {
fun calculateFee(amount: BigDecimal): BigDecimal {
return amount * FEE_RATE
}
companion object {
private val FEE_RATE = BigDecimal("0.02")
}
}
// Presentation Layer - ViewModel
class PaymentViewModel(
private val processPaymentUseCase: ProcessPaymentUseCase
) : ViewModel() {
private val _uiState = MutableStateFlow<PaymentUiState>(PaymentUiState.Initial)
val uiState: StateFlow<PaymentUiState> = _uiState.asStateFlow()
fun processPayment(amount: String, merchantId: String) {
viewModelScope.launch {
_uiState.value = PaymentUiState.Loading
processPaymentUseCase(amount, merchantId)
.onSuccess { result ->
_uiState.value = PaymentUiState.Success(result)
}
.onFailure { error ->
_uiState.value = PaymentUiState.Error(error.message ?: "Unknown error")
}
}
}
}
// Data Layer - Repository
class PaymentRepositoryImpl(
private val remoteDataSource: PaymentRemoteDataSource,
private val localDataSource: PaymentLocalDataSource,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : PaymentRepository {
override suspend fun processPayment(
request: PaymentRequest
): PaymentResult = withContext(ioDispatcher) {
val result = remoteDataSource.processPayment(request)
localDataSource.savePayment(result.toEntity())
result
}
}
```
**Benefits:**
- ✅ Clear separation of concerns
- ✅ Business logic in Domain layer (UseCase)
- ✅ ViewModel only handles presentation logic
- ✅ Reusable business logic
- ✅ Testable - each layer can be tested independently
---
## Example 4: Insecure Storage
### ❌ BAD: Plain SharedPreferences for Sensitive Data
```kotlin
class AuthManager(private val context: Context) {
private val prefs = context.getSharedPreferences("auth", Context.MODE_PRIVATE)
fun saveAuthToken(token: String) {
prefs.edit {
putString("auth_token", token) // Plain text!
}
}
fun saveUserPin(pin: String) {
prefs.edit {
putString("user_pin", pin) // Plain text!
}
}
fun getAuthToken(): String? {
return prefs.getString("auth_token", null)
}
}
```
**Issues:**
- 🔴 **Critical**: Auth token stored in plain text
- 🔴 **Critical**: User PIN stored in plain text
- 🔴 **Critical**: Security vulnerability - data can be read by rooted devices
### ✅ GOOD: EncryptedSharedPreferences
```kotlin
class SecureAuthManager(private val context: Context) {
private val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
private val encryptedPrefs = EncryptedSharedPreferences.create(
context,
"secure_auth",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
fun saveAuthToken(token: String) {
encryptedPrefs.edit {
putString(KEY_AUTH_TOKEN, token)
}
}
fun saveUserPin(pin: String) {
encryptedPrefs.edit {
putString(KEY_USER_PIN, pin)
}
}
fun getAuthToken(): String? {
return encryptedPrefs.getString(KEY_AUTH_TOKEN, null)
}
fun clearAuthData() {
encryptedPrefs.edit {
remove(KEY_AUTH_TOKEN)
remove(KEY_USER_PIN)
}
}
companion object {
private const val KEY_AUTH_TOKEN = "auth_token"
private const val KEY_USER_PIN = "user_pin"
}
}
```
**Benefits:**
- ✅ Encrypted storage using AES256
- ✅ Protected against unauthorized access
- ✅ Secure even on rooted devices
- ✅ Clear API for data management
---
## Example 5: Inefficient RecyclerView
### ❌ BAD: notifyDataSetChanged and No ViewBinding
```kotlin
class PaymentAdapter : RecyclerView.Adapter<PaymentAdapter.ViewHolder>() {
private var payments: List<Payment> = emptyList()
fun updatePayments(newPayments: List<Payment>) {
payments = newPayments
notifyDataSetChanged() // Inefficient!
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_payment, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(payments[position])
}
override fun getItemCount() = payments.size
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
// Finding views repeatedly - inefficient!
fun bind(payment: Payment) {
itemView.findViewById<TextView>(R.id.amountTextView).text =
payment.amount.toString()
itemView.findViewById<TextView>(R.id.merchantTextView).text =
payment.merchantName
itemView.findViewById<TextView>(R.id.dateTextView).text =
payment.formattedDate
}
}
}
```
**Issues:**
- 🟡 **Medium**: notifyDataSetChanged() redraws entire list
- 🟡 **Medium**: findViewById called repeatedly in bind()
- 🟡 **Medium**: No ViewBinding - prone to errors
### ✅ GOOD: ListAdapter with DiffUtil and ViewBinding
```kotlin
class PaymentAdapter(
private val onItemClick: (Payment) -> Unit
) : ListAdapter<Payment, PaymentAdapter.ViewHolder>(PaymentDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = ItemPaymentBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return ViewHolder(binding, onItemClick)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(getItem(position))
}
class ViewHolder(
private val binding: ItemPaymentBinding,
private val onItemClick: (Payment) -> Unit
) : RecyclerView.ViewHolder(binding.root) {
fun bind(payment: Payment) {
binding.apply {
amountTextView.text = payment.amount.formatAsCurrency()
merchantTextView.text = payment.merchantName
dateTextView.text = payment.formattedDate
statusBadge.text = payment.status
// Set status color
statusBadge.setBackgroundResource(
when (payment.status) {
"SUCCESS" -> R.drawable.bg_status_success
"PENDING" -> R.drawable.bg_status_pending
"FAILED" -> R.drawable.bg_status_failed
else -> R.drawable.bg_status_unknown
}
)
root.setOnClickListener {
onItemClick(payment)
}
}
}
}
}
class PaymentDiffCallback : DiffUtil.ItemCallback<Payment>() {
override fun areItemsTheSame(oldItem: Payment, newItem: Payment): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Payment, newItem: Payment): Boolean {
return oldItem == newItem
}
}
// Usage in Fragment
class PaymentListFragment : Fragment() {
private val adapter = PaymentAdapter { payment ->
navigateToPaymentDetail(payment.id)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.recyclerView.adapter = adapter
viewLifecycleOwner.lifecycleScope.launch {
viewModel.payments.collect { payments ->
adapter.submitList(payments) // DiffUtil automatically calculates changes
}
}
}
}
```
**Benefits:**
- ✅ Efficient updates - only changed items redrawn
- ✅ ViewBinding - type-safe, no findViewById
- ✅ Automatic animation with DiffUtil
- ✅ Click handling with lambda
- ✅ Extension function for formatting
---
## Example 6: Fragment Lifecycle Issues
### ❌ BAD: Wrong Lifecycle Owner
```kotlin
class PaymentFragment : Fragment() {
private var _binding: FragmentPaymentBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentPaymentBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Using fragment's lifecycle - WRONG!
lifecycleScope.launch {
viewModel.uiState.collect { state ->
updateUi(state) // Can crash after onDestroyView!
}
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
```
**Issues:**
- 🔴 **Critical**: Using fragment lifecycle instead of view lifecycle
- 🔴 **Critical**: Crash risk - accessing binding after onDestroyView
- 🟠 **High**: Memory leak between onDestroyView and onDestroy
### ✅ GOOD: Proper Lifecycle Management
```kotlin
class PaymentFragment : Fragment() {
private var _binding: FragmentPaymentBinding? = null
private val binding get() = _binding!!
private val viewModel: PaymentViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentPaymentBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupViews()
observeViewModel()
}
private fun setupViews() {
binding.submitButton.setOnClickListener {
val amount = binding.amountEditText.text.toString()
viewModel.processPayment(amount)
}
}
private fun observeViewModel() {
// Use viewLifecycleOwner for UI updates
viewLifecycleOwner.lifecycleScope.launch {
viewModel.uiState.collect { state ->
updateUi(state)
}
}
// Separate collection for one-time events
viewLifecycleOwner.lifecycleScope.launch {
viewModel.events.collect { event ->
handleEvent(event)
}
}
}
private fun updateUi(state: PaymentUiState) {
when (state) {
is PaymentUiState.Loading -> {
binding.progressBar.show()
binding.submitButton.isEnabled = false
}
is PaymentUiState.Success -> {
binding.progressBar.hide()
binding.submitButton.isEnabled = true
showSuccessMessage(state.result)
}
is PaymentUiState.Error -> {
binding.progressBar.hide()
binding.submitButton.isEnabled = true
showError(state.message)
}
is PaymentUiState.Initial -> {
binding.progressBar.hide()
binding.submitButton.isEnabled = true
}
}
}
private fun handleEvent(event: PaymentEvent) {
when (event) {
is PaymentEvent.NavigateToReceipt -> {
findNavController().navigate(
PaymentFragmentDirections.actionToReceipt(event.transactionId)
)
}
is PaymentEvent.ShowError -> {
Snackbar.make(binding.root, event.message, Snackbar.LENGTH_LONG).show()
}
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
```
**Benefits:**
- ✅ Correct lifecycle owner - viewLifecycleOwner
- ✅ No crash risk - collections cancelled when view destroyed
- ✅ Proper binding cleanup
- ✅ Separation of state and events
- ✅ Clean code organization
---
## Example 7: Hardcoded Configuration
### ❌ BAD: Hardcoded API Keys and URLs
```kotlin
class ApiClient {
private val retrofit = Retrofit.Builder()
.baseUrl("https://api.payoo.vn/v1/") // Hardcoded!
.addConverterFactory(GsonConverterFactory.create())
.build()
private val apiKey = "sk_live_abc123xyz789" // Hardcoded secret!
fun getPaymentApi(): PaymentApi {
return retrofit.create(PaymentApi::class.java)
}
}
class AuthInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request().newBuilder()
.addHeader("API-Key", "sk_live_abc123xyz789") // Hardcoded!
.build()
return chain.proceed(request)
}
}
```
**Issues:**
- 🔴 **Critical**: API key exposed in source code
- 🟠 **High**: Can't switch between dev/staging/prod
- 🟠 **High**: Security risk if code is decompiled
### ✅ GOOD: BuildConfig and Gradle Properties
```kotlin
// gradle.properties (add to .gitignore)
API_KEY_DEBUG=sk_test_debug_key
API_KEY_RELEASE=sk_live_production_key
API_BASE_URL_DEBUG=https://api-dev.payoo.vn/v1/
API_BASE_URL_RELEASE=https://api.payoo.vn/v1/
// app/build.gradle.kts
android {
defaultConfig {
buildConfigField("String", "API_KEY", "\"${project.findProperty("API_KEY_DEBUG")}\"")
buildConfigField("String", "API_BASE_URL", "\"${project.findProperty("API_BASE_URL_DEBUG")}\"")
}
buildTypes {
getByName("debug") {
buildConfigField("String", "API_KEY", "\"${project.findProperty("API_KEY_DEBUG")}\"")
buildConfigField("String", "API_BASE_URL", "\"${project.findProperty("API_BASE_URL_DEBUG")}\"")
}
getByName("release") {
buildConfigField("String", "API_KEY", "\"${project.findProperty("API_KEY_RELEASE")}\"")
buildConfigField("String", "API_BASE_URL", "\"${project.findProperty("API_BASE_URL_RELEASE")}\"")
}
}
}
// NetworkModule.kt
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideAuthInterceptor(): AuthInterceptor {
return AuthInterceptor(BuildConfig.API_KEY)
}
@Provides
@Singleton
fun provideOkHttpClient(authInterceptor: AuthInterceptor): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(authInterceptor)
.addInterceptor(HttpLoggingInterceptor().apply {
level = if (BuildConfig.DEBUG) {
HttpLoggingInterceptor.Level.BODY
} else {
HttpLoggingInterceptor.Level.NONE
}
})
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
}
@Provides
@Singleton
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl(BuildConfig.API_BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
}
class AuthInterceptor(private val apiKey: String) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request().newBuilder()
.addHeader("API-Key", apiKey)
.addHeader("Content-Type", "application/json")
.build()
return chain.proceed(request)
}
}
```
**Benefits:**
- ✅ API keys not in source code
- ✅ Different configs for debug/release
- ✅ Secure - keys in gradle.properties (gitignored)
- ✅ Easy to manage different environments
- ✅ Dependency injection for testability
---
## Summary of Common Issues
### Critical (Fix Immediately)
1. Memory leaks (Activity/Context in ViewModel)
2. Coroutines not cancelled (GlobalScope)
3. Plain text storage for sensitive data
4. Hardcoded API keys/secrets
5. UI updates on background thread
### High Priority (Fix Soon)
6. Business logic in ViewModel
7. Direct API calls from ViewModel
8. Wrong lifecycle owner in Fragments
9. No error handling in coroutines
10. Wrong Dispatcher usage
### Medium Priority (Should Improve)
11. notifyDataSetChanged() instead of DiffUtil
12. findViewById instead of ViewBinding
13. No null safety checks
14. Poor naming conventions
15. Not using data classes
These examples provide concrete guidance for identifying and fixing common Android code issues during reviews.

View File

@@ -0,0 +1,953 @@
# Android Code Review Standards
Comprehensive standards for Android Kotlin development in the Payoo Android application.
## 1. Naming Conventions
### Types and Classes
```kotlin
// ✅ GOOD: PascalCase, descriptive
class PaymentViewModel
class TransactionRepository
interface UserDataSource
sealed class PaymentResult
// ❌ BAD: Abbreviations, unclear
class PmtVM
class TxnRepo
```
### Variables and Properties
```kotlin
// ✅ GOOD: camelCase, descriptive
val paymentAmount: BigDecimal
var isLoading: Boolean
private val transactionList: List<Transaction>
// ❌ BAD: Abbreviations, unclear
val amt: BigDecimal
var loading: Boolean
val txns: List<Transaction>
```
### Constants
```kotlin
// ✅ GOOD: UPPER_SNAKE_CASE
const val MAX_RETRY_COUNT = 3
const val API_BASE_URL = "https://api.payoo.vn"
private const val CACHE_DURATION_MS = 5000L
// ❌ BAD: Wrong case
const val maxRetryCount = 3
const val apiBaseUrl = "https://api.payoo.vn"
```
### Boolean Variables
```kotlin
// ✅ GOOD: Prefix with is, has, should, can
val isPaymentSuccessful: Boolean
val hasInternetConnection: Boolean
val shouldRetry: Boolean
val canProceed: Boolean
// ❌ BAD: No prefix
val paymentSuccessful: Boolean
val internetConnection: Boolean
```
### View IDs and Binding
```kotlin
// ✅ GOOD: Include type suffix
binding.amountEditText
binding.submitButton
binding.paymentRecyclerView
binding.errorTextView
// ❌ BAD: No type suffix
binding.amount
binding.submit
binding.payments
```
## 2. Kotlin Best Practices
### Null Safety
```kotlin
// ✅ GOOD: Safe calls and Elvis operator
val length = text?.length ?: 0
user?.name?.let { name ->
displayName(name)
}
// ❌ BAD: Force unwrap without checking
val length = text!!.length
displayName(user!!.name!!)
```
### Data Classes
```kotlin
// ✅ GOOD: Data classes for DTOs/Models
data class User(
val id: String,
val name: String,
val email: String
)
data class PaymentRequest(
val amount: BigDecimal,
val merchantId: String,
val currency: String = "VND"
)
// ❌ BAD: Regular class for simple data
class User {
var id: String = ""
var name: String = ""
var email: String = ""
}
```
### Sealed Classes for State
```kotlin
// ✅ GOOD: Sealed classes for state management
sealed class UiState<out T> {
object Loading : UiState<Nothing>()
data class Success<T>(val data: T) : UiState<T>()
data class Error(val message: String) : UiState<Nothing>()
}
sealed class PaymentResult {
data class Success(val transactionId: String) : PaymentResult()
data class Failure(val error: PaymentError) : PaymentResult()
object Cancelled : PaymentResult()
}
// ❌ BAD: Using enums or nullable types
enum class State { LOADING, SUCCESS, ERROR }
```
### Immutability
```kotlin
// ✅ GOOD: Prefer val over var
val userId = getUserId()
val configuration = Config(apiKey, baseUrl)
// ❌ BAD: Unnecessary var
var userId = getUserId() // Never changed
var configuration = Config(apiKey, baseUrl) // Never changed
```
### Extension Functions
```kotlin
// ✅ GOOD: Extension functions for reusable utilities
fun String.isValidEmail(): Boolean {
return android.util.Patterns.EMAIL_ADDRESS.matcher(this).matches()
}
fun BigDecimal.formatAsCurrency(): String {
return NumberFormat.getCurrencyInstance(Locale("vi", "VN")).format(this)
}
fun View.show() {
visibility = View.VISIBLE
}
fun View.hide() {
visibility = View.GONE
}
// Usage
if (email.isValidEmail()) {
binding.submitButton.show()
}
```
### Scope Functions
```kotlin
// ✅ GOOD: Appropriate scope function usage
// apply: Configure object
val user = User().apply {
name = "John"
email = "john@example.com"
}
// let: Null-safe operations
user?.let { u ->
saveUser(u)
}
// also: Side effects
val numbers = mutableListOf(1, 2, 3).also {
println("List created with ${it.size} elements")
}
// run: Execute block and return result
val result = repository.run {
fetchData()
processData()
}
// with: Multiple operations on object
with(binding) {
titleTextView.text = title
descriptionTextView.text = description
imageView.load(imageUrl)
}
```
## 3. Coroutines Patterns
### Proper Scope Usage
```kotlin
// ✅ GOOD: Use appropriate scope
class PaymentViewModel : ViewModel() {
fun loadPayments() {
viewModelScope.launch {
// Automatically cancelled when ViewModel cleared
val payments = repository.getPayments()
_uiState.value = UiState.Success(payments)
}
}
}
class PaymentFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
// Cancelled when view is destroyed
viewModel.uiState.collect { state ->
updateUi(state)
}
}
}
}
// ❌ BAD: GlobalScope or runBlocking
GlobalScope.launch { // Never cancelled
repository.getPayments()
}
runBlocking { // Blocks thread
repository.getPayments()
}
```
### Dispatcher Usage
```kotlin
// ✅ GOOD: Appropriate dispatcher for task
class PaymentRepository(
private val api: PaymentApi,
private val database: PaymentDao,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) {
suspend fun getPayments(): List<Payment> = withContext(ioDispatcher) {
val remotePayments = api.fetchPayments()
database.insertAll(remotePayments)
database.getAllPayments()
}
}
// ❌ BAD: Wrong dispatcher
class PaymentRepository {
suspend fun getPayments(): List<Payment> = withContext(Dispatchers.Main) {
// Network/DB work on Main thread!
api.fetchPayments()
}
}
```
### Error Handling
```kotlin
// ✅ GOOD: Proper error handling
viewModelScope.launch {
_uiState.value = UiState.Loading
try {
val result = repository.processPayment(request)
_uiState.value = UiState.Success(result)
} catch (e: IOException) {
_uiState.value = UiState.Error("Network error: ${e.message}")
} catch (e: Exception) {
_uiState.value = UiState.Error("Unexpected error: ${e.message}")
}
}
// Or using runCatching
viewModelScope.launch {
_uiState.value = UiState.Loading
runCatching {
repository.processPayment(request)
}.onSuccess { result ->
_uiState.value = UiState.Success(result)
}.onFailure { error ->
_uiState.value = UiState.Error(error.message ?: "Unknown error")
}
}
// ❌ BAD: No error handling
viewModelScope.launch {
val result = repository.processPayment(request) // Can crash
_uiState.value = UiState.Success(result)
}
```
### Flow Usage
```kotlin
// ✅ GOOD: StateFlow for UI state
class PaymentViewModel : ViewModel() {
private val _uiState = MutableStateFlow<UiState<Payment>>(UiState.Loading)
val uiState: StateFlow<UiState<Payment>> = _uiState.asStateFlow()
private val _events = MutableSharedFlow<PaymentEvent>()
val events: SharedFlow<PaymentEvent> = _events.asSharedFlow()
fun processPayment(request: PaymentRequest) {
viewModelScope.launch {
repository.processPayment(request)
.catch { error ->
_uiState.value = UiState.Error(error.message)
}
.collect { result ->
_uiState.value = UiState.Success(result)
}
}
}
}
// ❌ BAD: LiveData with nullable types
class PaymentViewModel : ViewModel() {
val payment: MutableLiveData<Payment?> = MutableLiveData(null)
val error: MutableLiveData<String?> = MutableLiveData(null)
}
```
## 4. Clean Architecture
### Layer Structure
```
app/
├── data/
│ ├── datasource/ # API, Database, Cache
│ ├── repository/ # Implementation
│ └── model/ # DTOs, Entities
├── domain/
│ ├── model/ # Domain models
│ ├── repository/ # Repository interfaces
│ └── usecase/ # Business logic
└── presentation/
├── ui/ # Activities, Fragments
├── viewmodel/ # ViewModels
└── adapter/ # RecyclerView adapters
```
### ViewModel (Presentation Layer)
```kotlin
// ✅ GOOD: ViewModel uses UseCase, exposes UI state
class PaymentViewModel(
private val processPaymentUseCase: ProcessPaymentUseCase,
private val getPaymentHistoryUseCase: GetPaymentHistoryUseCase
) : ViewModel() {
private val _uiState = MutableStateFlow<PaymentUiState>(PaymentUiState.Initial)
val uiState: StateFlow<PaymentUiState> = _uiState.asStateFlow()
fun processPayment(amount: BigDecimal, merchantId: String) {
viewModelScope.launch {
_uiState.value = PaymentUiState.Loading
processPaymentUseCase(amount, merchantId)
.onSuccess { result ->
_uiState.value = PaymentUiState.Success(result)
}
.onFailure { error ->
_uiState.value = PaymentUiState.Error(error.message)
}
}
}
}
// ❌ BAD: ViewModel calls repository directly, has business logic
class PaymentViewModel(
private val repository: PaymentRepository
) : ViewModel() {
fun processPayment(amount: BigDecimal, merchantId: String) {
viewModelScope.launch {
// Business logic in ViewModel - WRONG!
if (amount < BigDecimal.ZERO) {
_error.value = "Invalid amount"
return@launch
}
val fee = amount * BigDecimal("0.02") // Business logic!
val total = amount + fee
// Calling repository directly - WRONG!
repository.processPayment(total, merchantId)
}
}
}
```
### UseCase (Domain Layer)
```kotlin
// ✅ GOOD: UseCase contains business logic
class ProcessPaymentUseCase(
private val paymentRepository: PaymentRepository,
private val feeCalculator: FeeCalculator,
private val validator: PaymentValidator
) {
suspend operator fun invoke(
amount: BigDecimal,
merchantId: String
): Result<PaymentResult> = runCatching {
// Business logic here
validator.validateAmount(amount)
validator.validateMerchant(merchantId)
val fee = feeCalculator.calculateFee(amount)
val totalAmount = amount + fee
val request = PaymentRequest(
amount = totalAmount,
merchantId = merchantId,
timestamp = System.currentTimeMillis()
)
paymentRepository.processPayment(request)
}
}
// ❌ BAD: UseCase just forwards to repository
class ProcessPaymentUseCase(
private val repository: PaymentRepository
) {
suspend operator fun invoke(request: PaymentRequest) =
repository.processPayment(request) // No value added!
}
```
### Repository (Data Layer)
```kotlin
// ✅ GOOD: Repository abstracts data sources
interface PaymentRepository {
suspend fun processPayment(request: PaymentRequest): PaymentResult
suspend fun getPaymentHistory(): List<Payment>
}
class PaymentRepositoryImpl(
private val remoteDataSource: PaymentRemoteDataSource,
private val localDataSource: PaymentLocalDataSource,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : PaymentRepository {
override suspend fun processPayment(
request: PaymentRequest
): PaymentResult = withContext(ioDispatcher) {
val result = remoteDataSource.processPayment(request)
localDataSource.savePayment(result.toEntity())
result
}
override suspend fun getPaymentHistory(): List<Payment> = withContext(ioDispatcher) {
try {
val remote = remoteDataSource.getPayments()
localDataSource.saveAll(remote.map { it.toEntity() })
remote
} catch (e: IOException) {
// Fallback to cache
localDataSource.getPayments().map { it.toDomain() }
}
}
}
// ❌ BAD: Repository has business logic
class PaymentRepositoryImpl(
private val api: PaymentApi
) : PaymentRepository {
override suspend fun processPayment(request: PaymentRequest): PaymentResult {
// Business logic in repository - WRONG!
if (request.amount < MIN_AMOUNT) {
throw IllegalArgumentException("Amount too low")
}
val fee = request.amount * FEE_RATE // Business logic!
val modifiedRequest = request.copy(amount = request.amount + fee)
return api.processPayment(modifiedRequest)
}
}
```
## 5. Lifecycle Management
### Activity/Fragment References
```kotlin
// ✅ GOOD: No Activity/Fragment references in ViewModel
class PaymentViewModel(
private val processPaymentUseCase: ProcessPaymentUseCase
) : ViewModel() {
private val _navigationEvent = MutableSharedFlow<NavigationEvent>()
val navigationEvent: SharedFlow<NavigationEvent> = _navigationEvent.asSharedFlow()
fun onPaymentSuccess() {
viewModelScope.launch {
_navigationEvent.emit(NavigationEvent.ToReceipt)
}
}
}
// In Fragment
class PaymentFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
viewModel.navigationEvent.collect { event ->
when (event) {
NavigationEvent.ToReceipt -> navigateToReceipt()
}
}
}
}
}
// ❌ BAD: Holding Activity/Fragment reference
class PaymentViewModel(
private val fragment: PaymentFragment // Memory leak!
) : ViewModel() {
fun onPaymentSuccess() {
fragment.navigateToReceipt() // Crash if Fragment destroyed!
}
}
```
### Observer Lifecycle
```kotlin
// ✅ GOOD: Use viewLifecycleOwner in Fragments
class PaymentFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Use viewLifecycleOwner, not this (fragment lifecycle)
viewLifecycleOwner.lifecycleScope.launch {
viewModel.uiState.collect { state ->
updateUi(state)
}
}
}
}
// ❌ BAD: Using fragment's lifecycle
class PaymentFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// This leaks between onDestroyView and onDestroy!
lifecycleScope.launch {
viewModel.uiState.collect { state ->
updateUi(state) // Can crash if view is destroyed
}
}
}
}
```
### Cleanup
```kotlin
// ✅ GOOD: Proper cleanup
class PaymentViewModel : ViewModel() {
private val job = SupervisorJob()
private val scope = CoroutineScope(Dispatchers.IO + job)
override fun onCleared() {
super.onCleared()
job.cancel()
}
}
// ❌ BAD: No cleanup
class PaymentViewModel : ViewModel() {
private val scope = CoroutineScope(Dispatchers.IO)
// No cleanup - scope keeps running!
}
```
## 6. Security Best Practices
### Secure Storage
```kotlin
// ✅ GOOD: Encrypted storage for sensitive data
class SecurePreferences(context: Context) {
private val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
private val encryptedPrefs = EncryptedSharedPreferences.create(
context,
"secure_prefs",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
fun saveToken(token: String) {
encryptedPrefs.edit {
putString(KEY_AUTH_TOKEN, token)
}
}
}
// ❌ BAD: Plain SharedPreferences for sensitive data
class Preferences(context: Context) {
private val prefs = context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
fun saveToken(token: String) {
prefs.edit {
putString("auth_token", token) // Plain text!
}
}
}
```
### API Keys
```kotlin
// ✅ GOOD: API keys in gradle.properties (not checked in) and BuildConfig
// gradle.properties (add to .gitignore)
API_KEY=your_secret_key_here
API_BASE_URL=https://api.payoo.vn
// build.gradle.kts
android {
defaultConfig {
buildConfigField("String", "API_KEY", "\"${project.findProperty("API_KEY")}\"")
buildConfigField("String", "API_BASE_URL", "\"${project.findProperty("API_BASE_URL")}\"")
}
}
// Usage
class ApiClient {
private val apiKey = BuildConfig.API_KEY
private val baseUrl = BuildConfig.API_BASE_URL
}
// ❌ BAD: Hardcoded API keys
class ApiClient {
private val apiKey = "sk_live_1234567890abcdef" // Exposed in code!
private val baseUrl = "https://api.payoo.vn"
}
```
### Logging
```kotlin
// ✅ GOOD: No sensitive data in logs, conditional logging
class PaymentLogger {
fun logPayment(request: PaymentRequest) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "Processing payment for merchant: ${request.merchantId}")
Log.d(TAG, "Amount: ${request.amount.setScale(2)}")
// NO card numbers, tokens, or PII
}
}
}
// ❌ BAD: Logging sensitive data
class PaymentLogger {
fun logPayment(request: PaymentRequest) {
Log.d(TAG, "Payment: $request") // Might contain sensitive data!
Log.d(TAG, "Token: ${request.authToken}") // Exposed in logs!
Log.d(TAG, "Card: ${request.cardNumber}") // PCI violation!
}
}
```
## 7. Performance Optimization
### Background Work
```kotlin
// ✅ GOOD: Network/DB on background thread
class PaymentRepository(
private val api: PaymentApi,
private val dao: PaymentDao,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) {
suspend fun syncPayments() = withContext(ioDispatcher) {
val remotePayments = api.fetchPayments()
dao.deleteAll()
dao.insertAll(remotePayments.map { it.toEntity() })
}
}
// ❌ BAD: Blocking Main thread
class PaymentRepository(
private val api: PaymentApi,
private val dao: PaymentDao
) {
fun syncPayments() {
// Blocking Main thread!
val remotePayments = api.fetchPayments().execute()
dao.deleteAll()
dao.insertAll(remotePayments.map { it.toEntity() })
}
}
```
### RecyclerView Optimization
```kotlin
// ✅ GOOD: DiffUtil for efficient updates
class PaymentAdapter : ListAdapter<Payment, PaymentViewHolder>(PaymentDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PaymentViewHolder {
val binding = ItemPaymentBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return PaymentViewHolder(binding)
}
override fun onBindViewHolder(holder: PaymentViewHolder, position: Int) {
holder.bind(getItem(position))
}
}
class PaymentDiffCallback : DiffUtil.ItemCallback<Payment>() {
override fun areItemsTheSame(oldItem: Payment, newItem: Payment) =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: Payment, newItem: Payment) =
oldItem == newItem
}
// ❌ BAD: notifyDataSetChanged for all updates
class PaymentAdapter : RecyclerView.Adapter<PaymentViewHolder>() {
private var payments: List<Payment> = emptyList()
fun updatePayments(newPayments: List<Payment>) {
payments = newPayments
notifyDataSetChanged() // Inefficient!
}
}
```
### Image Loading
```kotlin
// ✅ GOOD: Coil/Glide with proper caching
class ImageLoader(private val context: Context) {
fun loadImage(imageView: ImageView, url: String) {
imageView.load(url) {
crossfade(true)
placeholder(R.drawable.placeholder)
error(R.drawable.error)
transformations(CircleCropTransformation())
memoryCachePolicy(CachePolicy.ENABLED)
diskCachePolicy(CachePolicy.ENABLED)
}
}
}
// ❌ BAD: Loading images without caching
class ImageLoader {
suspend fun loadImage(imageView: ImageView, url: String) {
val bitmap = withContext(Dispatchers.IO) {
URL(url).openStream().use { stream ->
BitmapFactory.decodeStream(stream) // No caching, inefficient!
}
}
imageView.setImageBitmap(bitmap)
}
}
```
### Database Optimization
```kotlin
// ✅ GOOD: Indexed queries, efficient types
@Entity(
tableName = "payments",
indices = [
Index(value = ["merchantId"]),
Index(value = ["timestamp"])
]
)
data class PaymentEntity(
@PrimaryKey val id: String,
val merchantId: String,
val amount: Long, // Store cents, not BigDecimal
val timestamp: Long,
val status: String
)
@Dao
interface PaymentDao {
@Query("SELECT * FROM payments WHERE merchantId = :merchantId AND timestamp > :after ORDER BY timestamp DESC")
fun getPaymentsByMerchant(merchantId: String, after: Long): Flow<List<PaymentEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(payments: List<PaymentEntity>)
}
// ❌ BAD: No indexes, inefficient queries
@Entity(tableName = "payments")
data class PaymentEntity(
@PrimaryKey val id: String,
val merchantId: String,
val amount: BigDecimal, // Not supported by Room!
val timestamp: Long,
val status: String
)
@Dao
interface PaymentDao {
@Query("SELECT * FROM payments")
fun getAllPayments(): List<PaymentEntity> // Load all, then filter!
}
```
## 8. Dependency Injection
### Hilt/Dagger Setup
```kotlin
// ✅ GOOD: Proper DI with Hilt
@HiltAndroidApp
class PayooApplication : Application()
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(AuthInterceptor())
.connectTimeout(30, TimeUnit.SECONDS)
.build()
}
@Provides
@Singleton
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl(BuildConfig.API_BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
@Provides
@Singleton
fun providePaymentApi(retrofit: Retrofit): PaymentApi {
return retrofit.create(PaymentApi::class.java)
}
}
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
@Singleton
abstract fun bindPaymentRepository(
impl: PaymentRepositoryImpl
): PaymentRepository
}
@AndroidEntryPoint
class PaymentActivity : AppCompatActivity() {
@Inject
lateinit var viewModelFactory: PaymentViewModelFactory
}
// ❌ BAD: Manual instantiation, singletons
object ApiClient {
val instance: PaymentApi by lazy {
Retrofit.Builder()
.baseUrl("https://api.payoo.vn")
.build()
.create(PaymentApi::class.java)
}
}
class PaymentViewModel {
private val repository = PaymentRepository(ApiClient.instance) // Tight coupling!
}
```
---
## Summary Checklist
Use this checklist during code reviews:
### Naming ✅
- [ ] Classes/Interfaces in PascalCase
- [ ] Variables/Functions in camelCase
- [ ] Constants in UPPER_SNAKE_CASE
- [ ] Boolean variables prefixed (is, has, should, can)
- [ ] No unclear abbreviations
### Kotlin ✅
- [ ] Null safety properly handled
- [ ] Data classes for DTOs/models
- [ ] Sealed classes for states
- [ ] Prefer val over var
- [ ] Extension functions where appropriate
### Coroutines ✅
- [ ] Proper scope usage (viewModelScope, lifecycleScope)
- [ ] Correct dispatchers (IO for network/DB, Main for UI)
- [ ] Error handling in all coroutines
- [ ] Flows for reactive data
- [ ] No runBlocking in production
### Architecture ✅
- [ ] Clear layer separation (Presentation/Domain/Data)
- [ ] ViewModels use UseCases
- [ ] UseCases contain business logic
- [ ] Repositories abstract data sources
- [ ] Dependency injection configured
### Lifecycle ✅
- [ ] No Activity/Fragment references in ViewModel
- [ ] viewLifecycleOwner in Fragments
- [ ] Proper cleanup in onCleared
- [ ] Configuration changes handled
### Security ✅
- [ ] EncryptedSharedPreferences for sensitive data
- [ ] No hardcoded API keys/secrets
- [ ] No sensitive data in logs
- [ ] HTTPS only
- [ ] Input validation
### Performance ✅
- [ ] Network/DB on background threads
- [ ] No memory leaks
- [ ] DiffUtil in RecyclerView
- [ ] Image caching (Coil/Glide)
- [ ] Database queries optimized
This comprehensive standards document provides detailed guidance for Android code reviews in the Payoo Android application.