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
Top comments (0)