【Android】How to handle UI interactions with event processing

 

🧑🏻‍💻 Summary

  • UI Only
    • Good for very small, UI-contained logic.
    • No state preservation; everything resets on recomposition.
    • Simple one-time actions still work fine.
    • Weak testability and scalability.
    • Poor separation of concerns without ViewModel.
  • StateFlow
    • Designed specifically for holding and exposing UI state.
    • Always provides the latest value after recomposition.
    • Not suitable for one-time events because old values persist.
    • Ideal for continuously changing UI states.
    • Assumes at least one active collector.
    • Ensures consistent UI across configuration changes.
  • SharedFlow
    • Optimal for one-time UI events (Toast, Snackbar, Navigation).
    • replay = 0 + extraBufferCapacity = 1 helps prevent event loss.
    • Supports multiple collectors safely.
    • Not intended for state storage.
    • Standard modern pattern for UI event handling.
    • Robust against lifecycle situations where collectors appear/disappear.
  • Channel
    • Best suited for FIFO, strictly ordered event delivery.
    • Should only have one collector (shared consumption is unsafe).
    • Ensures one-by-one processing with guaranteed order.
    • Less compatible with UI lifecycle due to collector timing issues.
    • Typically replaced by SharedFlow in UI-layer logic.
    • More appropriate for internal sequential pipelines rather than UI events.

👉 Why Kotlin Channels Are the Natural Solution for Preventing "Double Execution" of Events


How dp/sp/px Conversion Works in Android

  • This code defines extension functions to convert between dp, sp, and px.
  • It relies on density (for dp) and scaledDensity (for sp) extracted from Android’s DisplayMetrics.
  • The goal is to keep UI elements visually consistent across devices with different screen densities.

 

🧑🏻‍💻 Why These Conversions Matter (Foundation)

Screen Density Model


+---------------------------------------------+
| density       → converts dp ↔ px            |
| scaledDensity → converts sp ↔ px (font size)|
+---------------------------------------------+

  • dp: density-independent pixels
  • sp: scale-independent pixels (respects user font size setting)
  • px: raw physical pixels

density and scaledDensity come from:


resources.displayMetrics

This ensures the UI scales correctly across devices.

 

🧑🏻‍💻 Key Conversion Logic

1. dp → px

Formula: px = dp × density


dpToPx(dp) = dp.value * density

2. dp → sp

( dp → px → sp )
Formula: sp = dp × density ÷ scaledDensity


dpToSp(dp) = (dp.value * density / scale).sp

3. px → dp

Formula: dp = px ÷ density


toDp(px) = (px / density).dp

4. px → sp

Formula: sp = px ÷ scaledDensity


toSp(px) = (px / scale).sp

5. sp → dp

Formula: dp = sp × scaledDensity ÷ density


spToDp(sp) = (sp.value * scale / density).dp

6. sp → px

Formula: px = sp × scaledDensity


spToPx(sp) = sp.value * scale

7. Type-Specific Extensions

The code also adds natural calling styles:


Dp.toPx(context)
Float.toDp(context)
TextUnit.toPx(context)
TextUnit.toDp(context)

These simply delegate to the Context converters and make the API flexible.

 

🧑🏻‍💻 Visualization — Full Conversion Map


+------------------+           +------------------+
|       Dp         | <------>  |        px        |
|   (dp.value)     |           |     (Float)      |
+------------------+           +------------------+
          |                               ^
          | dpToSp / spToDp               |
          v                               |
+------------------+           +------------------+
|       Sp         | <------>  |    scaled px     |
|  (TextUnit.sp)   |           |  (scaledDensity) |
+------------------+           +------------------+

 

🧑🏻‍💻 Notes & Caveats

  • scaledDensity changes when users adjust system font size.
  • Jetpack Compose usually hides px conversions, but you still need px for:
    • Custom drawing
    • Canvas operations
    • Bitmap sizing
  • Expert consultation recommended for deeply understanding DPI internals in OEM-modified environments.

 

🧑🏻‍💻 References

👉 dp / px / sp 完全相互変換
👉 DisplayMetrics  |  API reference  |  Android Developers
👉 各種のピクセル密度をサポートする  |  Compatibility  |  Android Developers


Patterning Dagger/Hilt Cases Where a Module Is or Is Not Required

 

🧑🏻‍💻 When a Module Is Not Required: Concrete Classes

In Hilt/Dagger, concrete classes with an @Inject constructor can be injected automatically.


class ApiClient @Inject constructor()

class UserRepository @Inject constructor(
    private val api: ApiClient
)

Point:
If the class can be instantiated directly, a Module is not required.

 

🧑🏻‍💻 When a Module Is Required (1): Interfaces

When injecting an interface, Hilt cannot determine which implementation to use.

You need to specify it explicitly using @Binds inside a Module.


interface Logger { 
    fun log(msg: String) 
}

class ConsoleLogger @Inject constructor() : Logger

@Module
@InstallIn(SingletonComponent::class)
interface LoggerModule {
    @Binds
    fun bindLogger(impl: ConsoleLogger): Logger
}

Point:
Interfaces always require a Module.

 

🧑🏻‍💻 When a Module Is Required (2): External Libraries

External libraries typically do not have @Inject constructor.

You must provide the creation logic inside a Module.


@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
    @Provides
    fun provideClient(): OkHttpClient = OkHttpClient.Builder().build()
}

Point:
You make external classes injectable by defining how to create them in a Module.

 

🧑🏻‍💻 Summary

  • Your own concrete classes → auto-injectable
  • Interfaces & external libraries → Module required
  • Multiple implementations or singleton handling → use @Qualifier, @Named, @Singleton

👉 Dagger/HiltでModuleが必要か一目でわかるようにパターン化する