DEV Community

Papon Ahasan
Papon Ahasan

Posted on • Edited on

Mastering Android Kotlin: Junior vs Mid vs Senior — Practical Code Examples

1. Loading Data in a Fragment

Scenario: Load and display user data from a ViewModel.

  lifecycleScope.launchWhenStarted {
      viewModel.userData
          .filterNotNull()
          .collect { user ->
              textView.text = user.name
          }
  }
Enter fullscreen mode Exit fullscreen mode

2. Making a Network Request with Retrofit

Scenario: Fetch user data from an API using Retrofit

  lifecycleScope.launch {
      runCatching { api.getUser() }
          .onSuccess { user ->
              textView.text = user.name
          }
          .onFailure { error ->
              // Handle error
          }
  }
Enter fullscreen mode Exit fullscreen mode
  lifecycleScope.launch {
      runCatching { api.getData() }
          .onSuccess { response ->
              if (response.isSuccessful) {
                  textView.text = response.body()?.data
              } else {
                  showError("Error: ${response.code()}")
              }
          }
          .onFailure {
              showError("Unexpected Error")
          }
  }
Enter fullscreen mode Exit fullscreen mode

3. Handling Button Clicks

Scenario: Respond to a button click event.

  button.setOnClickListener {
      context?.let {
          Toast.makeText(it, "Clicked", Toast.LENGTH_SHORT).show()
      }
  }
Enter fullscreen mode Exit fullscreen mode

4. Navigating Between Fragments

Scenario: Navigate from one fragment to another using Navigation Component.

  val action = FirstFragmentDirections.actionFirstFragmentToSecondFragment()
  findNavController().navigate(action)
Enter fullscreen mode Exit fullscreen mode

5. Displaying a List with RecyclerView

Scenario: Display a list of items using RecyclerView.

  val adapter = MyAdapter().apply {
      submitList(items)
  }
  recyclerView.adapter = adapter
Enter fullscreen mode Exit fullscreen mode
  class MyAdapter : ListAdapter<Item, ItemViewHolder>(ItemDiffCallback()) {
      // ...
  }

  class ItemDiffCallback : DiffUtil.ItemCallback<Item>() {
      override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
          return oldItem.id == newItem.id
      }

      override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
          return oldItem == newItem
      }
  }
Enter fullscreen mode Exit fullscreen mode
  class MyAdapter @Inject constructor() : PagingDataAdapter<Item, MyViewHolder>(DiffCallback()) {
      // Integrates with Paging 3 library
  }
Enter fullscreen mode Exit fullscreen mode

6. Implementing a Search Feature with Debounce

Scenario: Implement a search functionality that waits for the user to stop typing before making a network request.

  // Mid:
  private var searchJob: Job? = null

  searchEditText.addTextChangedListener {
      val query = it.toString()
      searchJob?.cancel()
      searchJob = lifecycleScope.launch {
          delay(300)
          performSearch(query)
      }
  }
Enter fullscreen mode Exit fullscreen mode
// Senior:
  val queryFlow = callbackFlow {
      val watcher = searchEditText.addTextChangedListener {
          trySend(it.toString())
      }
      awaitClose { searchEditText.removeTextChangedListener(watcher) }
  }

  lifecycleScope.launch {
      queryFlow
          .debounce(300)
          .distinctUntilChanged()
          .collect { query ->
              performSearch(query)
          }
  }
Enter fullscreen mode Exit fullscreen mode
  private val _uiState = MutableStateFlow<SearchUiState>(SearchUiState.Idle)
    val uiState: StateFlow<SearchUiState> = _uiState
private val searchDebounce = MutableStateFlow("")
    private fun observeSearch() {
        viewModelScope.launch {
            searchDebounce
                .debounce(300)
                .filter { it.isNotBlank() }
                .distinctUntilChanged()
                .flatMapLatest { query ->
                    flow {
                        emit(SearchUiState.Loading)
                        val results = runCatching { repository.searchDrugs(query) }
                            .getOrElse { throw it }
                        emit(SearchUiState.Success(results))
                    }.catch { e ->
                        emit(SearchUiState.Error(e.message ?: "Unexpected error"))
                    }.flowOn(ioDispatcher)
                }
                .collect { _uiState.value = it }
        }
    }
Enter fullscreen mode Exit fullscreen mode

07. Managing State with ViewModel and LiveData

Scenario: Manage UI state using ViewModel and LiveData.

  // Junior:
  var count = 0

  fun increment() {
      count++
      textView.text = count.toString()
  }
Enter fullscreen mode Exit fullscreen mode
  // Mid:
  class MyViewModel : ViewModel() {
      val count = MutableLiveData<Int>()

      fun increment() {
          val current = count.value ?: 0
          count.value = current + 1
      }
  }
Enter fullscreen mode Exit fullscreen mode
// Senior:
  class MyViewModel : ViewModel() {
      private val _count = MutableStateFlow(0)
      val count: StateFlow<Int> = _count.asStateFlow()

      fun increment() {
          _count.value += 1
      }
  }
Enter fullscreen mode Exit fullscreen mode

08. Requesting Permissions

Scenario: Request location permission at runtime.

class PermissionManager(private val activity: Activity) {
    fun hasPermission(permission: String): Boolean {
        return ContextCompat.checkSelfPermission(activity, permission) == PackageManager.PERMISSION_GRANTED
    }

    fun requestPermissions(vararg permissions: String, requestCode: Int) {
        ActivityCompat.requestPermissions(activity, permissions, requestCode)
    }
}

// Usage
if (!permissionManager.hasPermission(Manifest.permission.ACCESS_FINE_LOCATION)) {
    permissionManager.requestPermissions(Manifest.permission.ACCESS_FINE_LOCATION, LOCATION_REQUEST_CODE)
}
Enter fullscreen mode Exit fullscreen mode

09. Data Storage (SharedPreferences vs DataStore)

Scenario: Store user preference like theme or token.

Switches to DataStore and uses Flow for reactivity

class UserPreferences(context: Context) {
    private val dataStore = context.createDataStore(name = "user_prefs")

    val tokenFlow: Flow<String?> = dataStore.data
        .map { it["token"] }

    suspend fun saveToken(token: String) {
        dataStore.edit {
            it["token"] = token
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

10. Image Loading (with Glide/Coil)

// Mid-level
fun ImageView.loadImage(url: String) {
    Glide.with(this.context)
        .load(url)
        .placeholder(R.drawable.placeholder)
        .error(R.drawable.error)
        .into(this)
}
Enter fullscreen mode Exit fullscreen mode

Reusable component with fallback, caching, and testing
consideration.

// Senior
object ImageLoader {
    fun load(context: Context, imageView: ImageView, url: String) {
        Glide.with(context)
            .load(url)
            .placeholder(R.drawable.placeholder)
            .error(R.drawable.error)
            .diskCacheStrategy(DiskCacheStrategy.ALL)
            .into(imageView)
    }
}

// Usage
ImageLoader.load(context, imageView, url)
Enter fullscreen mode Exit fullscreen mode

DRY-Compliant Approach:

fun Date.toFormattedString(): String {
    val formatter = SimpleDateFormat("dd MMM yyyy", Locale.getDefault())
    return formatter.format(this)
}
Enter fullscreen mode Exit fullscreen mode

DRY-Compliant Approach:

if (text != null && text.isNotEmpty()) {
    // do something
}

fun String?.isNotNullOrEmpty(): Boolean = this != null && this.isNotEmpty()
Enter fullscreen mode Exit fullscreen mode

11. Managing UI Events (e.g., Button Click Debounce)

// Mid-level
button.setOnClickListener(object : View.OnClickListener {
    private var lastClickTime = 0L
    override fun onClick(v: View?) {
        if (System.currentTimeMillis() - lastClickTime < 500) return
        lastClickTime = System.currentTimeMillis()
        // handle click
    }
})
Enter fullscreen mode Exit fullscreen mode
// Senior: Debounce extension function to apply to any View.
fun View.setDebouncedClickListener(delay: Long = 500L, action: () -> Unit) {
    var lastClickTime = 0L
    setOnClickListener {
        val currentTime = System.currentTimeMillis()
        if (currentTime - lastClickTime > delay) {
            lastClickTime = currentTime
            action()
        }
    }
}

// Usage
button.setDebouncedClickListener {
    // handle click
}
Enter fullscreen mode Exit fullscreen mode

12. Handling Null Safety

  val name: String = getName() ?: return
  println(name.length)
Enter fullscreen mode Exit fullscreen mode

13. Using Extension Functions

  fun User.getFullName(): String {
      return "$firstName $lastName"
  }
  // Senior:
  val User.fullName: String
      get() = "$firstName $lastName"
Enter fullscreen mode Exit fullscreen mode

14. Implementing Singleton Pattern

  @Singleton
  class DatabaseHelper @Inject constructor() {
      // Initialization code
  }
Enter fullscreen mode Exit fullscreen mode

15. Using Coroutines for Asynchronous Tasks

Scenario: Fetching data asynchronously.

  // Mid-level:
  lifecycleScope.launch {
      val data = withContext(Dispatchers.IO) { fetchData() }
      updateUI(data)
  }
  // Senior:
  viewModelScope.launch {
      try {
          val data = repository.getData()
          _state.value = UiState.Success(data)
      } catch (e: Exception) {
          _state.value = UiState.Error(e)
      }
  }

Enter fullscreen mode Exit fullscreen mode

16. Using Sealed Classes for UI State

// Mid-level:
enum class UiState { LOADING, SUCCESS, ERROR }

// Senior:
sealed class UiState {
   object Loading : UiState()
   data class Success(val data: Data) : UiState()
   data class Error(val exception: Throwable) : UiState()
}
Enter fullscreen mode Exit fullscreen mode

17. Handling Configuration Changes

Scenario: Preserving data on rotation.

  // Senior:
  // Combine ViewModel with SavedStateHandle
Enter fullscreen mode Exit fullscreen mode

17.

🎓 𝐉𝐮𝐧𝐢𝐨𝐫:

val call = api.getUser()
call.enqueue(object : Callback<User> {
 override fun onResponse(...) { ... }
 override fun onFailure(...) { ... }
})
Enter fullscreen mode Exit fullscreen mode

🧑‍💻 𝐌𝐢𝐝:

lifecycleScope.launch {
 try {
 val user = api.getUser()
 // Update UI
 } catch (e: Exception) {
 // Handle Error
 }
}
Enter fullscreen mode Exit fullscreen mode

🧙 𝐒𝐞𝐧𝐢𝐨𝐫:

val flow = flow { emit(api.getUser()) }
 .flowOn(Dispatchers.IO)
 .catch { handleError(it) }
 .onEach { updateUI(it) }

lifecycleScope.launchWhenStarted { flow.collect() }
Enter fullscreen mode Exit fullscreen mode

18.

🎓 𝐉𝐮𝐧𝐢𝐨𝐫:

    try {
       val result = repository.searchDrugs(query) // Might run on Main Thread ❌
            searchResults.value = result
       } catch (e: Exception) {
                errorMessage.value = e.message
       } finally {
            isLoading.value = false
    }
Enter fullscreen mode Exit fullscreen mode

🧑‍💻 𝐌𝐢𝐝:

   runCatching {
      repository.searchDrugs(query)
         }.onSuccess { results ->
              _uiState.value = SearchUiState.Success(results)
         }.onFailure { e ->
              _uiState.value = SearchUiState.Error(e.message ?: "Unknown error")
         }
Enter fullscreen mode Exit fullscreen mode

🧙 𝐒𝐞𝐧𝐢𝐨𝐫:

       flow {
            emit(SearchUiState.Loading)
                  val results = runCatching { repository.searchDrugs(query) }
                  .getOrElse { throw it }
                        emit(SearchUiState.Success(results))
                   }.catch { e ->
                        emit(SearchUiState.Error(e.message ?: "Unexpected error"))
             }.flowOn(ioDispatcher)
Enter fullscreen mode Exit fullscreen mode

https://halilozel1903.medium.com/mastering-android-kotlin-junior-vs-mid-vs-senior-5-practical-code-examples-073472d4c41a

Top comments (0)