DEV Community

Cover image for Building an Android App with Kotlin, Retrofit, and Coroutines: A Practical Guide2
Daniel Ioni
Daniel Ioni

Posted on

Building an Android App with Kotlin, Retrofit, and Coroutines: A Practical Guide2

Building an Android App with Kotlin, Retrofit, and Coroutines: A Practical Guide

Introduction

In this article, I'll share my experience building a production-ready Android app for MyZubster, an open-source skill exchange platform. I'll cover the key architectural decisions and code patterns I used.

The app handles:

  • 🔐 JWT authentication with token management
  • 📅 Booking system with calendar
  • 💳 Monero payments with QR Code
  • 📋 Complete work history with pagination
  • 🔔 Real-time session management

Tech Stack Overview

Component Technology
Language Kotlin
Networking Retrofit + OkHttp
Async Coroutines + Flow
UI Material Design + ViewBinding
Storage SharedPreferences
QR Code ZXing
Architecture MVVM + Repository Pattern

1. Authentication & Token Management

I implemented JWT-based authentication with automatic token injection using SharedPreferences for storage.

TokenManager.kt


kotlin
class TokenManager(context: Context) {
    private val prefs = context.getSharedPreferences("MyZubsterPrefs", Context.MODE_PRIVATE)

    fun saveAuthData(token: String, userId: String, email: String, name: String) {
        prefs.edit().apply {
            putString("jwt_token", token)
            putString("user_id", userId)
            putString("user_email", email)
            putString("user_name", name)
            apply()
        }
    }

    fun getToken(): String? = prefs.getString("jwt_token", null)
    fun getUserId(): String? = prefs.getString("user_id", null)
    fun isLoggedIn(): Boolean = !getToken().isNullOrEmpty()

    fun clear() {
        prefs.edit().clear().apply()
    }
}
LoginActivity.kt
kotlin

class LoginActivity : AppCompatActivity() {
    private lateinit var tokenManager: TokenManager

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login)

        tokenManager = TokenManager(this)

        // Check if already logged in
        if (tokenManager.isLoggedIn()) {
            startActivity(Intent(this, MainActivity::class.java))
            finish()
        }
    }

    private fun performLogin(email: String, password: String) {
        // Simulate login (replace with actual API call)
        val token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
        tokenManager.saveAuthData(
            token = token,
            userId = "65f1a2b3c4d5e6f7g8h9i0j1",
            email = email,
            name = "Test User"
        )
        startActivity(Intent(this, MainActivity::class.java))
        finish()
    }
}

2. Networking with Retrofit

I used Retrofit with an AuthInterceptor to automatically add the JWT token to every request.
ApiService.kt
kotlin

interface ApiService {
    @POST("api/v1/auth/login")
    suspend fun login(
        @Body request: LoginRequest
    ): Response<AuthResponse>

    @GET("api/v1/bookings/history/{userId}")
    suspend fun getBookingHistory(
        @Path("userId") userId: String,
        @Query("page") page: Int = 1,
        @Query("limit") limit: Int = 10
    ): Response<BookingHistoryResponse>
}

AuthInterceptor.kt
kotlin

class AuthInterceptor(private val tokenManager: TokenManager) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        val token = tokenManager.getToken()

        val newRequest = if (!token.isNullOrEmpty()) {
            request.newBuilder()
                .header("Authorization", "Bearer $token")
                .build()
        } else {
            request
        }

        return chain.proceed(newRequest)
    }
}

RetrofitClient.kt
kotlin

object RetrofitClient {
    private const val BASE_URL = "http://10.0.2.2:5000/"

    private val okHttpClient = OkHttpClient.Builder()
        .addInterceptor(AuthInterceptor(tokenManager))
        .connectTimeout(30, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS)
        .build()

    private val retrofit = Retrofit.Builder()
        .baseUrl(BASE_URL)
        .client(okHttpClient)
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    val apiService: ApiService by lazy {
        retrofit.create(ApiService::class.java)
    }
}

3. Booking History with Pagination

I implemented a RecyclerView with pagination using the Repository pattern and StateFlow.
BookingHistoryRepository.kt
kotlin

class BookingHistoryRepository {
    suspend fun getBookingHistory(
        userId: String,
        page: Int = 1,
        limit: Int = 10
    ): Response<BookingHistoryResponse> {
        return RetrofitClient.apiService.getBookingHistory(userId, page, limit)
    }
}

BookingHistoryViewModel.kt
kotlin

class BookingHistoryViewModel : ViewModel() {
    private val repository = BookingHistoryRepository()

    private val _bookings = MutableStateFlow<List<BookingHistory>>(emptyList())
    val bookings: StateFlow<List<BookingHistory>> = _bookings.asStateFlow()

    private val _isLoading = MutableStateFlow(false)
    val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()

    private val _error = MutableStateFlow<String?>(null)
    val error: StateFlow<String?> = _error.asStateFlow()

    private var currentPage = 1
    private var hasMoreData = true

    fun loadBookings(userId: String, page: Int = 1) {
        if (_isLoading.value) return

        viewModelScope.launch {
            _isLoading.value = true
            _error.value = null

            try {
                val response = repository.getBookingHistory(userId, page, 10)
                if (response.isSuccessful && response.body()?.success == true) {
                    val data = response.body()?.data ?: emptyList()
                    if (page == 1) {
                        _bookings.value = data
                    } else {
                        _bookings.value = _bookings.value + data
                    }
                    hasMoreData = data.isNotEmpty()
                    currentPage = page
                } else {
                    _error.value = response.body()?.error ?: "Loading failed"
                }
            } catch (e: Exception) {
                _error.value = e.message
            } finally {
                _isLoading.value = false
            }
        }
    }

    fun loadNextPage() {
        if (hasMoreData && !_isLoading.value) {
            loadBookings(currentPage + 1)
        }
    }
}

4. QR Code Generation

For Monero payments, I integrated QR code generation using the ZXing library.
PaymentActivity.kt
kotlin

class PaymentActivity : AppCompatActivity() {
    private lateinit var ivQRCode: ImageView
    private lateinit var tvAddress: TextView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_payment)

        ivQRCode = findViewById(R.id.ivQRCode)
        tvAddress = findViewById(R.id.tvPaymentAddress)

        val address = "4A6d7f8g9h0i1j2k3l4m5n6o7p8q9r0s1t2u3v4w5x6y7z8"
        tvAddress.text = address
        ivQRCode.setImageBitmap(generateQRCode(address))
    }

    private fun generateQRCode(address: String): Bitmap {
        val writer = QRCodeWriter()
        val bitMatrix = writer.encode(address, BarcodeFormat.QR_CODE, 200, 200)
        val bitmap = Bitmap.createBitmap(200, 200, Bitmap.Config.RGB_565)

        for (x in 0 until 200) {
            for (y in 0 until 200) {
                bitmap.setPixel(x, y, if (bitMatrix[x, y]) Color.BLACK else Color.WHITE)
            }
        }
        return bitmap
    }
}

5. Session Management with Countdown

I implemented a session timer that shows remaining time in the UI.
SessionManager.kt
kotlin

class SessionManager(private val context: Context) {
    private val tokenManager = TokenManager(context)
    private val SESSION_DURATION = 3600L // 1 hour

    fun getSessionRemainingTime(): Long {
        // Calculate remaining time
        val loginTime = tokenManager.getLoginTimestamp()
        val elapsed = System.currentTimeMillis() - loginTime
        return (SESSION_DURATION * 1000 - elapsed) / 1000
    }

    fun getSessionCountdown(callback: (String) -> Unit) {
        Thread {
            while (true) {
                val remaining = getSessionRemainingTime()
                if (remaining <= 0) break

                val formatted = formatTime(remaining)
                Handler(Looper.getMainLooper()).post {
                    callback(formatted)
                }
                Thread.sleep(1000)
            }
        }.start()
    }

    private fun formatTime(seconds: Long): String {
        val hours = seconds / 3600
        val minutes = (seconds % 3600) / 60
        val secs = seconds % 60
        return "${hours}h ${minutes}m ${secs}s"
    }
}

6. Logout with Confirmation Dialog
kotlin

class MainActivity : AppCompatActivity() {
    private lateinit var tokenManager: TokenManager

    private fun logout() {
        AlertDialog.Builder(this)
            .setTitle("Logout")
            .setMessage("Are you sure you want to logout?")
            .setPositiveButton("Yes") { _, _ ->
                tokenManager.clear()
                startActivity(Intent(this, LoginActivity::class.java))
                finish()
            }
            .setNegativeButton("Cancel", null)
            .show()
    }
}

Key Takeaways

    Use Repository Pattern - Separates data sources from UI logic

    Implement AuthInterceptor - Automates token injection

    Use StateFlow - For reactive UI updates

    Handle Pagination - For efficient data loading

    Session Management - Improves user experience

Project Structure
text

app/src/main/java/com/myzubster/
├── activities/
│   ├── MainActivity.kt
│   └── BookingHistoryActivity.kt
├── adapters/
│   └── BookingHistoryAdapter.kt
├── models/
│   ├── BookingHistory.kt
│   └── AuthResponse.kt
├── network/
│   ├── ApiService.kt
│   ├── RetrofitClient.kt
│   └── AuthInterceptor.kt
├── repositories/
│   └── BookingHistoryRepository.kt
├── ui/auth/
│   ├── LoginActivity.kt
│   └── RegisterActivity.kt
├── utils/
│   ├── TokenManager.kt
│   └── SessionManager.kt
└── viewmodels/
    └── BookingHistoryViewModel.kt

Resources

The full project is open-source and available on GitHub:

🔗 DanielIoni-creator/MyZubsterAPP
💡 Want to Contribute?

    ⭐ Star the repository

    🐛 Report issues

    🔧 Submit pull requests

    💬 Share feedback

Follow me on GitHub: DanielIoni-creator

Tags: android kotlin retrofit coroutines opensource mvvm materialdesign
text
Enter fullscreen mode Exit fullscreen mode

Top comments (0)