A

Android Integration

Native Android app integration with Kotlin/Java

Native AndroidKotlin/JavaModern Architecture
Quick Start

1. Add Dependencies (build.gradle)

gradle
Click to copy
// app/build.gradle
dependencies {
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
    implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0'
    implementation 'com.github.bumptech.glide:glide:4.16.0'
    implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0'
    implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.7.0'
    implementation 'androidx.activity:activity-ktx:1.8.2'
    implementation 'androidx.fragment:fragment-ktx:1.6.2'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
    
    // For ViewBinding
    viewBinding {
        enabled = true
    }
}

// Add internet permission in AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

2. API Service Interface (Kotlin)

kotlin
Click to copy
// CalligraphyApiService.kt
import retrofit2.Response
import retrofit2.http.*

interface CalligraphyApiService {
    
    @POST("v2/generate")
    suspend fun generateCalligraphy(
        @Header("x-api-key") apiKey: String,
        @Body request: GenerateRequest
    ): Response<CalligraphyResponse>
    
    @POST("v2/download-svg")
    suspend fun downloadSvg(
        @Header("x-api-key") apiKey: String,
        @Body request: SvgDownloadRequest
    ): Response<SvgDownloadResponse>
    
    @POST("v2/download-image")
    suspend fun downloadImage(
        @Header("x-api-key") apiKey: String,
        @Body request: ImageDownloadRequest
    ): Response<ImageDownloadResponse>
}

// Data classes for API requests and responses
data class GenerateRequest(
    val text: String,
    val language: String = "hindi",
    val fontStyle: String = "calligraphy",
    val count: Int = 1,
    val imageFormat: String = "png"
)

data class SvgDownloadRequest(
    val resultText: String,
    val fontName: String
)

data class ImageDownloadRequest(
    val resultText: String,
    val fontName: String,
    val imageWidth: Int = 800,
    val imageHeight: Int = 300,
    val imageQuality: Int = 90,
    val imageFormat: String = "png",
    val backgroundColor: String = "#ffffff",
    val textColor: String = "#000000"
)

data class CalligraphyResponse(
    val success: Boolean,
    val data: CalligraphyData?
)

data class CalligraphyData(
    val results: List<CalligraphyResult>,
    val metadata: CalligraphyMetadata,
    val usage: CalligraphyUsage
)

data class CalligraphyResult(
    val id: String,
    val resultText: String,
    val fontFamily: String,
    val cdnUrl: String?,
    val appliedVariants: List<AppliedVariant>
)

data class AppliedVariant(
    val type: String,
    val position: Int,
    val character: String
)

data class CalligraphyMetadata(
    val totalVariations: Int,
    val averageVariantsApplied: Double,
    val fontFamily: String
)

data class CalligraphyUsage(
    val creditsUsed: Int,
    val remainingCredits: Int
)

data class SvgDownloadResponse(
    val success: Boolean,
    val data: SvgData?
)

data class SvgData(
    val resultText: String,
    val fontFamily: String,
    val svgBase64: String,
    val svgString: String,
    val dimensions: Dimensions
)

data class ImageDownloadResponse(
    val success: Boolean,
    val data: ImageData?
)

data class ImageData(
    val resultText: String,
    val fontName: String,
    val format: String,
    val imageBase64: String,
    val dimensions: Dimensions,
    val quality: Int,
    val backgroundColor: String,
    val textColor: String
)

data class Dimensions(
    val width: Int,
    val height: Int
)

3. API Client Setup

kotlin
Click to copy
// ApiClient.kt
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit

object ApiClient {
    private const val BASE_URL = "https://api.calligraphymaker.com/api/"
    const val API_KEY = "YOUR_API_KEY_HERE" // Store securely in production
    
    private val loggingInterceptor = HttpLoggingInterceptor().apply {
        level = if (BuildConfig.DEBUG) {
            HttpLoggingInterceptor.Level.BODY
        } else {
            HttpLoggingInterceptor.Level.NONE
        }
    }
    
    private val okHttpClient = OkHttpClient.Builder()
        .addInterceptor(loggingInterceptor)
        .connectTimeout(30, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS)
        .writeTimeout(30, TimeUnit.SECONDS)
        .build()
    
    private val retrofit = Retrofit.Builder()
        .baseUrl(BASE_URL)
        .client(okHttpClient)
        .addConverterFactory(GsonConverterFactory.create())
        .build()
    
    val calligraphyService: CalligraphyApiService = retrofit.create(CalligraphyApiService::class.java)
}

// Repository class for API calls
class CalligraphyRepository {
    private val apiService = ApiClient.calligraphyService
    
    suspend fun generateCalligraphy(
        text: String,
        language: String = "hindi",
        fontStyle: String = "calligraphy",
        count: Int = 1
    ): Result<CalligraphyData> {
        return try {
            val request = GenerateRequest(
                text = text,
                language = language,
                fontStyle = fontStyle,
                count = count
            )
            
            val response = apiService.generateCalligraphy(ApiClient.API_KEY, request)
            
            if (response.isSuccessful && response.body()?.success == true) {
                response.body()?.data?.let { data ->
                    Result.success(data)
                } ?: Result.failure(Exception("No data received"))
            } else {
                Result.failure(Exception("API Error: ${response.code()} - ${response.message()}"))
            }
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
    
    suspend fun downloadSvg(
        resultText: String,
        fontName: String
    ): Result<SvgData> {
        return try {
            val request = SvgDownloadRequest(resultText, fontName)
            val response = apiService.downloadSvg(ApiClient.API_KEY, request)
            
            if (response.isSuccessful && response.body()?.success == true) {
                response.body()?.data?.let { data ->
                    Result.success(data)
                } ?: Result.failure(Exception("No data received"))
            } else {
                Result.failure(Exception("API Error: ${response.code()} - ${response.message()}"))
            }
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
}

4. ViewModel Implementation

kotlin
Click to copy
// CalligraphyViewModel.kt
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch

class CalligraphyViewModel(
    private val repository: CalligraphyRepository
) : ViewModel() {
    
    private val _uiState = MutableStateFlow(CalligraphyUiState())
    val uiState: StateFlow<CalligraphyUiState> = _uiState.asStateFlow()
    
    fun generateCalligraphy(
        text: String,
        language: String = "hindi",
        fontStyle: String = "calligraphy",
        count: Int = 3
    ) {
        if (text.isBlank()) {
            _uiState.value = _uiState.value.copy(
                error = "Please enter some text"
            )
            return
        }
        
        _uiState.value = _uiState.value.copy(
            isLoading = true,
            error = null
        )
        
        viewModelScope.launch {
            repository.generateCalligraphy(text, language, fontStyle, count)
                .onSuccess { data ->
                    _uiState.value = _uiState.value.copy(
                        isLoading = false,
                        results = data.results,
                        usage = data.usage,
                        error = null
                    )
                }
                .onFailure { exception ->
                    _uiState.value = _uiState.value.copy(
                        isLoading = false,
                        error = exception.message ?: "Unknown error occurred"
                    )
                }
        }
    }
    
    fun downloadSvg(resultText: String, fontName: String) {
        _uiState.value = _uiState.value.copy(isLoading = true)
        
        viewModelScope.launch {
            repository.downloadSvg(resultText, fontName)
                .onSuccess { svgData ->
                    _uiState.value = _uiState.value.copy(
                        isLoading = false,
                        downloadedSvg = svgData
                    )
                }
                .onFailure { exception ->
                    _uiState.value = _uiState.value.copy(
                        isLoading = false,
                        error = exception.message ?: "Download failed"
                    )
                }
        }
    }
    
    fun clearError() {
        _uiState.value = _uiState.value.copy(error = null)
    }
    
    fun clearResults() {
        _uiState.value = _uiState.value.copy(
            results = emptyList(),
            downloadedSvg = null
        )
    }
}

data class CalligraphyUiState(
    val isLoading: Boolean = false,
    val results: List<CalligraphyResult> = emptyList(),
    val usage: CalligraphyUsage? = null,
    val downloadedSvg: SvgData? = null,
    val error: String? = null
)

class CalligraphyViewModelFactory(
    private val repository: CalligraphyRepository
) : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(CalligraphyViewModel::class.java)) {
            @Suppress("UNCHECKED_CAST")
            return CalligraphyViewModel(repository) as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")
    }
}
UI Implementation

MainActivity.kt

kotlin
Click to copy
// MainActivity.kt
import android.os.Bundle
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.widget.doOnTextChanged
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import com.bumptech.glide.Glide
import kotlinx.coroutines.launch

class MainActivity : AppCompatActivity() {
    
    private lateinit var binding: ActivityMainBinding
    private lateinit var adapter: CalligraphyResultsAdapter
    
    private val viewModel: CalligraphyViewModel by viewModels {
        CalligraphyViewModelFactory(CalligraphyRepository())
    }
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        
        setupUI()
        observeViewModel()
    }
    
    private fun setupUI() {
        // Setup RecyclerView
        adapter = CalligraphyResultsAdapter(
            onItemClick = { result ->
                // Handle item click
                showResultDetails(result)
            },
            onDownloadClick = { result ->
                // Handle download
                downloadImage(result.cdnUrl)
            }
        )
        
        binding.recyclerViewResults.layoutManager = GridLayoutManager(this, 2)
        binding.recyclerViewResults.adapter = adapter
        
        // Setup input
        binding.editTextInput.setText("नमस्ते दुनिया")
        
        // Setup language spinner
        val languages = arrayOf("Hindi", "Marathi", "Gujarati", "English")
        val languageAdapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, languages)
        languageAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
        binding.spinnerLanguage.adapter = languageAdapter
        
        // Setup font style spinner
        val fontStyles = arrayOf("Calligraphy", "Decorative", "Publication")
        val styleAdapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, fontStyles)
        styleAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
        binding.spinnerFontStyle.adapter = styleAdapter
        
        // Setup generate button
        binding.buttonGenerate.setOnClickListener {
            val text = binding.editTextInput.text.toString()
            val language = getSelectedLanguage()
            val fontStyle = getSelectedFontStyle()
            val count = binding.seekBarCount.progress + 1
            
            viewModel.generateCalligraphy(text, language, fontStyle, count)
        }
        
        // Setup count seeker
        binding.seekBarCount.max = 5
        binding.seekBarCount.progress = 2
        binding.textViewCount.text = "Count: 3"
        
        binding.seekBarCount.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
            override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
                binding.textViewCount.text = "Count: ${progress + 1}"
            }
            override fun onStartTrackingTouch(seekBar: SeekBar?) {}
            override fun onStopTrackingTouch(seekBar: SeekBar?) {}
        })
        
        // Setup clear button
        binding.buttonClear.setOnClickListener {
            viewModel.clearResults()
            binding.editTextInput.text?.clear()
        }
    }
    
    private fun observeViewModel() {
        lifecycleScope.launch {
            viewModel.uiState.collect { state ->
                // Update loading state
                binding.progressBar.visibility = if (state.isLoading) View.VISIBLE else View.GONE
                binding.buttonGenerate.isEnabled = !state.isLoading
                
                // Update results
                adapter.submitList(state.results)
                
                // Show results count
                if (state.results.isNotEmpty()) {
                    binding.textViewResultsCount.text = "Generated ${state.results.size} results"
                    binding.textViewResultsCount.visibility = View.VISIBLE
                } else {
                    binding.textViewResultsCount.visibility = View.GONE
                }
                
                // Update usage info
                state.usage?.let { usage ->
                    binding.textViewCredits.text = "Credits remaining: ${usage.remainingCredits}"
                    binding.textViewCredits.visibility = View.VISIBLE
                }
                
                // Handle errors
                state.error?.let { error ->
                    Toast.makeText(this@MainActivity, error, Toast.LENGTH_LONG).show()
                    viewModel.clearError()
                }
                
                // Handle downloaded SVG
                state.downloadedSvg?.let { svgData ->
                    // Process SVG data
                    showSvgDownloadDialog(svgData)
                }
            }
        }
    }
    
    private fun getSelectedLanguage(): String {
        return when (binding.spinnerLanguage.selectedItemPosition) {
            0 -> "hindi"
            1 -> "marathi"
            2 -> "gujarati"
            3 -> "english"
            else -> "hindi"
        }
    }
    
    private fun getSelectedFontStyle(): String {
        return when (binding.spinnerFontStyle.selectedItemPosition) {
            0 -> "calligraphy"
            1 -> "decorative"
            2 -> "publication"
            else -> "calligraphy"
        }
    }
    
    private fun showResultDetails(result: CalligraphyResult) {
        AlertDialog.Builder(this)
            .setTitle("Calligraphy Result")
            .setMessage("Text: ${result.resultText}\nFont: ${result.fontFamily}\nVariants: ${result.appliedVariants.size}")
            .setPositiveButton("Download SVG") { _, _ ->
                viewModel.downloadSvg(result.resultText, result.fontFamily)
            }
            .setNegativeButton("Close", null)
            .show()
    }
    
    private fun downloadImage(imageUrl: String?) {
        imageUrl?.let { url ->
            // Implement image download using DownloadManager
            val downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
            val request = DownloadManager.Request(Uri.parse(url))
                .setTitle("Calligraphy Image")
                .setDescription("Downloading calligraphy image...")
                .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
                .setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "calligraphy_${System.currentTimeMillis()}.png")
            
            downloadManager.enqueue(request)
            Toast.makeText(this, "Download started", Toast.LENGTH_SHORT).show()
        }
    }
    
    private fun showSvgDownloadDialog(svgData: SvgData) {
        // Show SVG download success dialog
        AlertDialog.Builder(this)
            .setTitle("SVG Downloaded")
            .setMessage("SVG file generated successfully!\nDimensions: ${svgData.dimensions.width}x${svgData.dimensions.height}")
            .setPositiveButton("Save to Device") { _, _ ->
                saveSvgToDevice(svgData.svgString)
            }
            .setNegativeButton("Close", null)
            .show()
    }
    
    private fun saveSvgToDevice(svgContent: String) {
        // Implement SVG file saving
        try {
            val fileName = "calligraphy_${System.currentTimeMillis()}.svg"
            val file = File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), fileName)
            file.writeText(svgContent)
            Toast.makeText(this, "SVG saved to Downloads", Toast.LENGTH_SHORT).show()
        } catch (e: Exception) {
            Toast.makeText(this, "Failed to save SVG", Toast.LENGTH_SHORT).show()
        }
    }
}

RecyclerView Adapter

kotlin
Click to copy
// CalligraphyResultsAdapter.kt
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide

class CalligraphyResultsAdapter(
    private val onItemClick: (CalligraphyResult) -> Unit,
    private val onDownloadClick: (CalligraphyResult) -> Unit
) : ListAdapter<CalligraphyResult, CalligraphyResultsAdapter.ViewHolder>(DiffCallback()) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val binding = ItemCalligraphyResultBinding.inflate(
            LayoutInflater.from(parent.context),
            parent,
            false
        )
        return ViewHolder(binding)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.bind(getItem(position))
    }

    inner class ViewHolder(
        private val binding: ItemCalligraphyResultBinding
    ) : RecyclerView.ViewHolder(binding.root) {

        fun bind(result: CalligraphyResult) {
            // Load image using Glide
            if (result.cdnUrl != null) {
                Glide.with(binding.root.context)
                    .load(result.cdnUrl)
                    .placeholder(R.drawable.ic_placeholder)
                    .error(R.drawable.ic_error)
                    .into(binding.imageViewResult)
            }

            // Set text
            binding.textViewResultText.text = result.resultText
            binding.textViewFontFamily.text = result.fontFamily
            binding.textViewVariants.text = "${result.appliedVariants.size} variants"

            // Set click listeners
            binding.root.setOnClickListener {
                onItemClick(result)
            }

            binding.buttonDownload.setOnClickListener {
                onDownloadClick(result)
            }

            // Show/hide download button based on image availability
            binding.buttonDownload.visibility = if (result.cdnUrl != null) {
                View.VISIBLE
            } else {
                View.GONE
            }
        }
    }

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

        override fun areContentsTheSame(oldItem: CalligraphyResult, newItem: CalligraphyResult): Boolean {
            return oldItem == newItem
        }
    }
}
Layout Files

activity_main.xml

xml
Click to copy
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="16dp"
    tools:context=".MainActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <!-- Header -->
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Calligraphy Generator"
            android:textSize="24sp"
            android:textStyle="bold"
            android:gravity="center"
            android:layout_marginBottom="24dp" />

        <!-- Input Section -->
        <com.google.android.material.card.MaterialCardView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="16dp"
            app:cardCornerRadius="8dp"
            app:cardElevation="4dp">

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical"
                android:padding="16dp">

                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="Enter Text"
                    android:textStyle="bold"
                    android:layout_marginBottom="8dp" />

                <EditText
                    android:id="@+id/editTextInput"
                    android:layout_width="match_parent"
                    android:layout_height="100dp"
                    android:gravity="top"
                    android:hint="Enter text to convert to calligraphy"
                    android:inputType="textMultiLine"
                    android:layout_marginBottom="16dp" />

                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:orientation="horizontal"
                    android:layout_marginBottom="16dp">

                    <LinearLayout
                        android:layout_width="0dp"
                        android:layout_height="wrap_content"
                        android:layout_weight="1"
                        android:orientation="vertical"
                        android:layout_marginEnd="8dp">

                        <TextView
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:text="Language"
                            android:textStyle="bold" />

                        <Spinner
                            android:id="@+id/spinnerLanguage"
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content" />

                    </LinearLayout>

                    <LinearLayout
                        android:layout_width="0dp"
                        android:layout_height="wrap_content"
                        android:layout_weight="1"
                        android:orientation="vertical"
                        android:layout_marginStart="8dp">

                        <TextView
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:text="Font Style"
                            android:textStyle="bold" />

                        <Spinner
                            android:id="@+id/spinnerFontStyle"
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content" />

                    </LinearLayout>

                </LinearLayout>

                <TextView
                    android:id="@+id/textViewCount"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="Count: 3"
                    android:textStyle="bold"
                    android:layout_marginBottom="8dp" />

                <SeekBar
                    android:id="@+id/seekBarCount"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginBottom="16dp" />

                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:orientation="horizontal">

                    <Button
                        android:id="@+id/buttonGenerate"
                        android:layout_width="0dp"
                        android:layout_height="wrap_content"
                        android:layout_weight="1"
                        android:text="Generate Calligraphy"
                        android:layout_marginEnd="8dp" />

                    <Button
                        android:id="@+id/buttonClear"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="Clear"
                        style="@style/Widget.Material3.Button.OutlinedButton" />

                </LinearLayout>

            </LinearLayout>

        </com.google.android.material.card.MaterialCardView>

        <!-- Loading -->
        <ProgressBar
            android:id="@+id/progressBar"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:visibility="gone" />

        <!-- Results Info -->
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:layout_marginBottom="8dp">

            <TextView
                android:id="@+id/textViewResultsCount"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:textStyle="bold"
                android:visibility="gone" />

            <TextView
                android:id="@+id/textViewCredits"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textSize="12sp"
                android:visibility="gone" />

        </LinearLayout>

        <!-- Results Grid -->
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recyclerViewResults"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:nestedScrollingEnabled="false" />

    </LinearLayout>

</ScrollView>

item_calligraphy_result.xml

xml
Click to copy
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="4dp"
    app:cardCornerRadius="8dp"
    app:cardElevation="2dp">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="8dp">

        <ImageView
            android:id="@+id/imageViewResult"
            android:layout_width="match_parent"
            android:layout_height="120dp"
            android:scaleType="centerInside"
            android:background="@color/light_gray" />

        <TextView
            android:id="@+id/textViewResultText"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:maxLines="2"
            android:ellipsize="end"
            android:textStyle="bold"
            android:textSize="12sp" />

        <TextView
            android:id="@+id/textViewFontFamily"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="4dp"
            android:textSize="10sp"
            android:textColor="@color/gray" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:orientation="horizontal">

            <TextView
                android:id="@+id/textViewVariants"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:textSize="10sp"
                android:textColor="@color/gray" />

            <Button
                android:id="@+id/buttonDownload"
                android:layout_width="wrap_content"
                android:layout_height="32dp"
                android:text="Download"
                android:textSize="10sp"
                style="@style/Widget.Material3.Button.OutlinedButton" />

        </LinearLayout>

    </LinearLayout>

</com.google.android.material.card.MaterialCardView>
What You'll Build
Native Android App
Full-featured Kotlin/Java app
MVVM Architecture
Clean architecture with ViewModel
Image Loading
Glide integration for image caching
Download Management
Built-in download functionality
Requirements
Android API 21+ (Android 5.0)
Kotlin 1.8+ or Java 8+
Android Studio
API Key from Calligraphy API