xavier collantes

Building a Kotlin App for Faxion AI

By Xavier Collantes


Kotlin Android Development
As the Head of Engineering at Faxion AI, my responsibility was to provide technological answers to the vision of an AI styling platform. Research yielded our target market was iOS focused but we wanted to provide alternatives for React Web and Android.
This article walks through the architectural decisions and implementation details of the Faxion Android app, showcasing how I built a production-ready mobile application using modern Android development practices.
The app provides feature parity with the React and iOS applications:
  • AI-Powered Fashion Recommendations: Personalized outfit suggestions based on user preferences
  • Wardrobe Management: Upload and organize clothing items with advanced categorization
  • Outfit Creation and Sharing: Create outfits from wardrobe items and share with the community
  • User Profiles: Detailed profiles with body shape, style preferences, and measurements
Learning Resource

The Android app was delayed release to the Play Store to prioritize building the ML and Image Generation workflows.

Architecture Overview

Our Faxion Android app follows Clean Architecture principles with clear separation of concerns across three main layers:
Clean Architecture Layers
txt
1app/src/main/kotlin/com/faxion/android/
2├── core/                    # Core utilities and constants
3├── data/                    # Data layer (models, API, repositories)
4├── domain/                  # Business logic layer (use cases, repositories)
5├── presentation/            # UI layer (screens, ViewModels)
6└── di/                      # Dependency injection modules
7
snippet hosted withby Xavier

Key Architectural Decisions

  • MVVM + Clean Architecture: Provides clear separation between UI, business logic, and data
  • Firebase Authentication and Firestore: Database and authentication

Dependencies and Configuration

Our build.gradle.kts uses modern Android tooling with version catalogs:

app/build.gradle.kts

kotlin
1plugins {
2    alias(libs.plugins.android.application)
3    alias(libs.plugins.kotlin.android)
4    alias(libs.plugins.kotlin.compose)
5    alias(libs.plugins.kotlin.serialization)
6    alias(libs.plugins.kotlin.parcelize)
7    alias(libs.plugins.hilt)
8    alias(libs.plugins.ksp)
9    alias(libs.plugins.google.services)
10}
11
12android {
13    namespace = "com.faxion.android"
14    compileSdk = 35
15
16    defaultConfig {
17        applicationId = "com.faxion.android"
18        minSdk = 26  // Modern baseline for better API support
19        targetSdk = 35
20        versionCode = 1
21        versionName = "1.0"
22    }
23
24    buildFeatures {
25        compose = true
26        buildConfig = true
27    }
28
29    compileOptions {
30        sourceCompatibility = JavaVersion.VERSION_11
31        targetCompatibility = JavaVersion.VERSION_11
32    }
33}
34
35dependencies {
36    // Modern Compose stack with Material3
37    implementation(platform(libs.androidx.compose.bom))
38    implementation(libs.androidx.material3)
39    implementation(libs.androidx.compose.animation)
40
41    // Architecture components
42    implementation(libs.hilt.android)
43    implementation(libs.androidx.lifecycle.viewmodel.compose)
44    implementation(libs.androidx.navigation.compose)
45
46    // Networking with Kotlin Serialization (faster than Gson)
47    implementation(libs.retrofit)
48    implementation(libs.retrofit.kotlinx.serialization)
49    implementation(libs.kotlinx.serialization.json)
50
51    // Firebase stack for real-time features
52    implementation(platform(libs.firebase.bom))
53    implementation(libs.firebase.auth)
54    implementation(libs.firebase.firestore)
55    implementation(libs.firebase.storage)
56
57    // Additional modern libraries
58    implementation(libs.coil) // Async image loading
59    implementation(libs.androidx.camera.core) // Camera integration
60    implementation(libs.revenuecat) // Subscription management
61}
62
app/build.gradle.kts hosted withby Xavier

Why These Technology Choices?

Kotlin Serialization over Gson

We chose Kotlin Serialization for JSON parsing because:
  • Performance: No reflection at runtime
  • Null: Better integration with Kotlin's null safety
  • Smaller APK size: No additional runtime dependencies

Kotlin Serialization is a faster JSON parsing than Gson because Gson has to recognize the fields at runtime, while Kotlin Serialization does it at compile-time. This is called 'reflection'.

Kotlin Serialization handles nulls more predictably than Gson which reduced the risk of null pointer exceptions which can crash the app.

Firebase Integration

Firebase Platform
Firebase provides several advantages for our use case:
  • Real-time sync: Immediate updates across devices
  • Offline support: Built-in local caching
  • Authentication: Simplified OAuth and email/password flows
  • Scalability: Handles growth without infrastructure management

Data Models

Our data models are designed to match the web application's schema while optimizing for mobile use:

User Profile Model

The Profile model captures comprehensive user information for personalized recommendations:

data/model/Profile.kt

kotlin
1@Serializable
2data class Profile(
3    val userId: String? = null,
4    val firstName: String? = null,
5    val lastName: String? = null,
6    val birthDate: String? = null, // ISO date string
7    val gender: Gender? = null,
8    val unit: Unit = Unit.METRIC,
9    val faceImagePath: String? = null,
10    val fullBodyImagePath: String? = null,
11    val heightCm: Double? = null,
12    val weightKg: Double? = null,
13    val bodyShape: BodyShape? = null,
14    val stylePreferences: StylePreferences? = null,
15    val subscription: String? = null,
16    val termsAccepted: Boolean = false
17)
18
19@Serializable
20data class StylePreferences(
21    val preferredColors: List<String> = emptyList(),
22    val favoriteStyles: List<String> = emptyList(),
23    val dislikedStyles: List<String> = emptyList(),
24    val occasionPreferences: List<String> = emptyList()
25)
26
27@Serializable
28enum class BodyShape {
29    HOURGLASS, RECTANGLE, TRIANGLE, INVERTED_TRIANGLE, ROUND, OVAL
30}
31
data/model/Profile.kt hosted withby Xavier

Design Decisions for Data Models

  • Kotlin Serialization: Uses @Serializable annotation for better performance
  • Nullable: Most fields are nullable for graceful API evolution
  • ISO Date Strings: Consistent date handling across platforms
  • Enums for Type Safety: Reduces errors and improves code clarity
  • Nested Objects: Rich data structures that mirror the web app's complexity

API Integration

Our API service provides a clean interface to the Faxion backend, organized by feature domains:

data/remote/api/FaxionApi.kt

kotlin
1interface FaxionApi {
2
3    // Authentication endpoints
4    @POST(Constants.Api.Endpoints.INIT_USER)
5    suspend fun initUser(): Response<User>
6
7    // Profile management
8    @GET(Constants.Api.Endpoints.PROFILE)
9    suspend fun getProfile(): Response<Profile>
10
11    @PUT(Constants.Api.Endpoints.PROFILE)
12    suspend fun updateProfile(@Body profile: Profile): Response<Profile>
13
14    // Outfit CRUD operations
15    @GET(Constants.Api.Endpoints.OUTFITS)
16    suspend fun getOutfits(): Response<List<Outfit>>
17
18    @POST(Constants.Api.Endpoints.SINGLE_OUTFIT)
19    suspend fun createOutfit(@Body outfit: Outfit): Response<Outfit>
20
21    @PUT("${Constants.Api.Endpoints.SINGLE_OUTFIT}/{outfitId}")
22    suspend fun updateOutfit(
23        @Path("outfitId") outfitId: String,
24        @Body outfit: Outfit
25    ): Response<Outfit>
26
27    // Social interactions
28    @POST("${Constants.Api.Endpoints.SET_OUTFIT_LIKE}/{outfitId}")
29    suspend fun likeOutfit(@Path("outfitId") outfitId: String): Response<Unit>
30
31    @POST("${Constants.Api.Endpoints.SET_OUTFIT_FAVORITE}/{outfitId}")
32    suspend fun favoriteOutfit(@Path("outfitId") outfitId: String): Response<Unit>
33
34    @POST("${Constants.Api.Endpoints.SET_SHARED_OUTFIT}/{outfitId}")
35    suspend fun shareOutfit(@Path("outfitId") outfitId: String): Response<Unit>
36
37    // AI recommendations
38    @POST(Constants.Api.Endpoints.RECOMMENDATIONS)
39    suspend fun getRecommendations(@Body request: Map<String, Any>): Response<List<Outfit>>
40}
41
data/remote/api/FaxionApi.kt hosted withby Xavier

API Endpoint Organization

We organize endpoints using a constants file for maintainability:

core/Constants.kt

kotlin
1object Constants {
2    object Api {
3        const val BASE_URL = "https://api.faxion.ai/"
4
5        object Endpoints {
6            // Outfit management
7            const val OUTFITS = "v1/outfits/outfits"
8            const val SINGLE_OUTFIT = "v1/outfits/outfit"
9            const val SET_SHARED_OUTFIT = "v1/outfits/share"
10            const val SET_OUTFIT_LIKE = "v1/outfits/liked"
11            const val SET_OUTFIT_FAVORITE = "v1/outfits/favorite"
12
13            // Profile management
14            const val PROFILE = "v1/user/profile"
15            const val INIT_USER = "v1/firebase/init-user"
16
17            // AI recommendations
18            const val RECOMMENDATIONS = "v1/gen/generate"
19
20            // Subscription management
21            const val AVAILABLE_PLANS = "v1/subscriptions/available"
22            const val PURCHASE_LINKS = "v1/subscriptions/rc-purchase-links"
23        }
24    }
25}
26
core/Constants.kt hosted withby Xavier

Network Configuration

Our network module uses modern best practices:

di/NetworkModule.kt

kotlin
1@Module
2@InstallIn(SingletonComponent::class)
3object NetworkModule {
4
5    @Provides
6    @Singleton
7    fun provideJson(): Json = Json {
8        ignoreUnknownKeys = true      // Graceful API evolution
9        coerceInputValues = true      // Handle type mismatches
10        isLenient = true              // Flexible parsing
11    }
12
13    @Provides
14    @Singleton
15    fun provideOkHttpClient(): OkHttpClient {
16        return OkHttpClient.Builder()
17            .connectTimeout(30, TimeUnit.SECONDS)
18            .readTimeout(30, TimeUnit.SECONDS)
19            .writeTimeout(30, TimeUnit.SECONDS)
20            .apply {
21                if (BuildConfig.DEBUG) {
22                    addInterceptor(
23                        HttpLoggingInterceptor().apply {
24                            level = HttpLoggingInterceptor.Level.BODY
25                        }
26                    )
27                }
28            }
29            .build()
30    }
31
32    @Provides
33    @Singleton
34    fun provideRetrofit(okHttpClient: OkHttpClient, json: Json): Retrofit {
35        return Retrofit.Builder()
36            .baseUrl(Constants.Api.BASE_URL)
37            .client(okHttpClient)
38            .addConverterFactory(
39                json.asConverterFactory("application/json".toMediaType())
40            )
41            .build()
42    }
43}
44
di/NetworkModule.kt hosted withby Xavier

MVVM with Jetpack Compose

Modern Android Development

ViewModels with State Management

Our ViewModels use modern state management patterns with Compose integration:

presentation/screens/auth/AuthViewModel.kt

kotlin
1@HiltViewModel
2class AuthViewModel @Inject constructor(
3    private val authRepository: AuthRepository,
4    private val signInWithEmailUseCase: SignInWithEmailUseCase,
5    private val signUpWithEmailUseCase: SignUpWithEmailUseCase,
6    private val resetPasswordUseCase: ResetPasswordUseCase
7) : ViewModel() {
8
9    private val _uiState = MutableStateFlow(AuthUiState())
10    val uiState = _uiState.asStateFlow()
11
12    // Real-time auth state from Firebase
13    val currentUser = authRepository.getCurrentUser()
14        .stateIn(
15            scope = viewModelScope,
16            started = SharingStarted.WhileSubscribed(5000),
17            initialValue = null
18        )
19
20    fun signInWithEmail(email: String, password: String) {
21        viewModelScope.launch {
22            _uiState.value = _uiState.value.copy(isLoading = true, error = null)
23
24            when (val result = signInWithEmailUseCase(email, password)) {
25                is Resource.Success -> {
26                    _uiState.value = _uiState.value.copy(
27                        isLoading = false,
28                        isSignedIn = true,
29                        error = null
30                    )
31                }
32                is Resource.Error -> {
33                    _uiState.value = _uiState.value.copy(
34                        isLoading = false,
35                        error = result.message
36                    )
37                }
38                is Resource.Loading -> {
39                    _uiState.value = _uiState.value.copy(isLoading = true)
40                }
41            }
42        }
43    }
44
45    // Additional auth methods...
46    fun togglePasswordVisibility() {
47        _uiState.value = _uiState.value.copy(
48            isPasswordVisible = !_uiState.value.isPasswordVisible
49        )
50    }
51
52    fun clearError() {
53        _uiState.value = _uiState.value.copy(error = null)
54    }
55}
56
57data class AuthUiState(
58    val isLoading: Boolean = false,
59    val isSignedIn: Boolean = false,
60    val isSignInMode: Boolean = true,
61    val email: String = "",
62    val password: String = "",
63    val isPasswordVisible: Boolean = false,
64    val error: String? = null,
65    val resetPasswordEmailSent: Boolean = false
66)
67
presentation/screens/auth/AuthViewModel.kt hosted withby Xavier

Compose UI with Material3

Material 3 is an implementation of Google's Material Design. Other implementations I have used include MaterialUI for React.

presentation/screens/home/HomeScreen.kt

kotlin
1@OptIn(ExperimentalMaterial3Api::class)
2@Composable
3fun HomeScreen(
4    onNavigateToUpload: () -> Unit,
5    onNavigateToWardrobe: () -> Unit,
6    onNavigateToProfile: () -> Unit,
7    onNavigateToSettings: () -> Unit,
8    viewModel: HomeViewModel = hiltViewModel()
9) {
10    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
11    val currentUser by viewModel.currentUser.collectAsStateWithLifecycle()
12
13    Column(modifier = Modifier.fillMaxSize()) {
14        TopAppBar(
15            title = {
16                Text(
17                    text = currentUser?.displayName?.let { "Welcome, $it" }
18                        ?: currentUser?.email?.substringBefore("@")?.let { "Welcome, $it" }
19                        ?: "Faxion"
20                )
21            },
22            actions = {
23                IconButton(onClick = viewModel::refreshData) {
24                    Icon(Icons.Default.Refresh, contentDescription = "Refresh")
25                }
26                IconButton(onClick = onNavigateToProfile) {
27                    Icon(Icons.Default.Person, contentDescription = "Profile")
28                }
29                IconButton(onClick = onNavigateToSettings) {
30                    Icon(Icons.Default.Settings, contentDescription = "Settings")
31                }
32            }
33        )
34
35        LazyColumn(
36            modifier = Modifier
37                .fillMaxSize()
38                .padding(16.dp),
39            verticalArrangement = Arrangement.spacedBy(16.dp)
40        ) {
41            // Profile completion card
42            item {
43                ProfileCompletionCard(
44                    userProfile = uiState.userProfile,
45                    isLoading = uiState.isLoadingProfile,
46                    onNavigateToProfile = onNavigateToProfile
47                )
48            }
49
50            // Quick actions
51            item {
52                QuickActionsSection(
53                    onNavigateToUpload = onNavigateToUpload,
54                    onNavigateToWardrobe = onNavigateToWardrobe
55                )
56            }
57
58            // User outfits with loading states
59            if (uiState.userOutfits.isEmpty() && !uiState.isLoadingOutfits) {
60                item {
61                    EmptyOutfitsCard(onNavigateToUpload = onNavigateToUpload)
62                }
63            } else {
64                items(uiState.userOutfits.take(6)) { outfit ->
65                    OutfitCard(outfit = outfit)
66                }
67            }
68
69            // Error handling with retry
70            uiState.error?.let { error ->
71                item {
72                    ErrorCard(
73                        error = error,
74                        onRetry = {
75                            viewModel.clearError()
76                            viewModel.refreshData()
77                        }
78                    )
79                }
80            }
81        }
82    }
83}
84
presentation/screens/home/HomeScreen.kt hosted withby Xavier

presentation/navigation/FaxionNavGraph.kt

kotlin
1@Composable
2fun FaxionNavGraph(
3    modifier: Modifier = Modifier,
4    navController: NavHostController = rememberNavController()
5) {
6    val authViewModel: AuthViewModel = hiltViewModel()
7    val currentUser by authViewModel.currentUser.collectAsStateWithLifecycle()
8
9    // Dynamic start destination based on auth state
10    val startDestination = if (currentUser != null) {
11        Screen.Home.route
12    } else {
13        Screen.Auth.route
14    }
15
16    NavHost(
17        navController = navController,
18        startDestination = startDestination,
19        modifier = modifier
20    ) {
21        composable(Screen.Auth.route) {
22            AuthScreen(
23                onNavigateToHome = {
24                    navController.navigate(Screen.Home.route) {
25                        popUpTo(Screen.Auth.route) { inclusive = true }
26                    }
27                }
28            )
29        }
30
31        composable(Screen.Home.route) {
32            // Protected route with auth guard
33            LaunchedEffect(currentUser) {
34                if (currentUser == null) {
35                    navController.navigate(Screen.Auth.route) {
36                        popUpTo(Screen.Home.route) { inclusive = true }
37                    }
38                }
39            }
40
41            if (currentUser != null) {
42                HomeScreen(
43                    onNavigateToUpload = { navController.navigate(Screen.Upload.route) },
44                    onNavigateToWardrobe = { navController.navigate(Screen.Wardrobe.route) },
45                    onNavigateToProfile = { navController.navigate(Screen.Profile.route) },
46                    onNavigateToSettings = { navController.navigate(Screen.Settings.route) }
47                )
48            }
49        }
50
51        // Additional protected routes...
52    }
53}
54
presentation/navigation/FaxionNavGraph.kt hosted withby Xavier

Testing Strategy

Testing was not the highest priority since we are a startup, but we did our best to test the critical paths manually.

Our testing approach covers all architectural layers:
kotlin
1// Example unit test for Use Case
2@Test
3fun `signInWithEmail should return error for invalid email format`() = runTest {
4    // Given
5    val invalidEmail = "invalid-email"
6    val password = "validPassword123"
7
8    // When
9    val result = signInWithEmailUseCase(invalidEmail, password)
10
11    // Then
12    assertTrue(result is Resource.Error)
13    assertEquals("Invalid email format", result.message)
14}
15
snippet hosted withby Xavier

CI/CD Pipeline

  • Automated Testing: Unit and instrumentation tests on every PR
  • Code Quality: KtLint for formatting
  • Play Store Deployment: Automated release to internal testing

Challenges Overcome

Gradle builds have to sync and take up local computer resources. Android Studio's performance always take time to build the project.
Version dependencies change over time for example I had an error with coil where I had to update the where coil was pulled. Coil 3 moved from io.coil-kt to io.coil-kt.coil3, and its artifacts were renamed. That is why Gradle could not find io.coil-kt:coil-compose:3.3.0.

Future Improvements

  • Offline-First Architecture: Implement proper offline support with Room
  • Push Notifications: Add Firebase Cloud Messaging for real-time updates
  • Advanced Caching: Implement intelligent cache invalidation
  • A/B Testing: Add Firebase Remote Config for feature flagging

Related Articles

Related by topics:

thingsIBuilt
frontend
firebase
Faxion AI: Building an AI Fashion Platform

Architecting and leading the development of a groundbreaking AI fashion platform that reduced cloud costs by 80% and supports thousands of daily users.

By Xavier Collantes7/1/2025
thingsIBuilt
frontend
infrastructure
+4

HomeFeedback