مقاله

توسعه اپلیکیشن اندروید با Jetpack Compose

راهنمای کامل برای ایجاد UI های مدرن و تعاملی با Jetpack Compose Jetpack Compose آینده توسعه UI اندروید است. با استفاده از declarative programming و Kotlin، می‌توانید اپلیکیشن‌های زیبا و کارآمد بسازید. علی سلمانیان آماده کمک به شما در پروژه‌های Compose است.

📅 20 December 2023 ⏱️ 12 دقیقه 👤 علی سلمانیان
توسعه اپلیکیشن اندروید با Jetpack Compose - راهنمای کامل برای ایجاد UI های مدرن و تعاملی با Jetpack Compose
Jetpack Compose آینده توسعه UI اندروید است. با استفاده از declarative programming و Kotlin، می‌توانید اپلیکیشن‌های زیبا و کارآمد بسازید. علی سلمانیان آماده کمک به شما در پروژه‌های Compose است.

مقدمه

Jetpack Compose انقلابی در توسعه UI اندروید است. علی سلمانیان در این مقاله، راهنمای کاملی از Compose ارائه می‌دهد که به شما کمک می‌کند تا اپلیکیشن‌های مدرن و زیبا بسازید. Compose با استفاده از Kotlin، توسعه UI را ساده‌تر و قدرتمندتر کرده است.

Jetpack Compose چیست؟

Jetpack Compose یک toolkit مدرن برای ساخت UI های اندروید است که:

  • از declarative programming استفاده می‌کند
  • کد کمتر و خوانایی بیشتری دارد
  • State management ساده‌تری ارائه می‌دهد
  • با Material Design 3 سازگار است
  • Performance بهتری دارد

🚀 مزایای اصلی Compose:

  • Declarative UI: UI را بر اساس state توصیف می‌کنید
  • Reusable Components: Component های قابل استفاده مجدد
  • Powerful State Management: مدیریت state پیشرفته
  • Material Design 3: پشتیبانی کامل از Material Design
  • Animation Support: انیمیشن‌های قدرتمند

راه‌اندازی پروژه Compose

1. تنظیمات اولیه

برای شروع با Compose، ابتدا باید پروژه خود را پیکربندی کنید:

// build.gradle (Module: app)
android {
    compileSdk 34
    
    defaultConfig {
        applicationId "com.example.mycomposeapp"
        minSdk 21
        targetSdk 34
        versionCode 1
        versionName "1.0"
    }
    
    buildFeatures {
        compose true
    }
    
    composeOptions {
        kotlinCompilerExtensionVersion '1.5.4'
    }
}

dependencies {
    implementation 'androidx.core:core-ktx:1.12.0'
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
    implementation 'androidx.activity:activity-compose:1.8.2'
    
    // Compose BOM
    implementation platform('androidx.compose:compose-bom:2023.10.01')
    implementation 'androidx.compose.ui:ui'
    implementation 'androidx.compose.ui:ui-graphics'
    implementation 'androidx.compose.ui:ui-tooling-preview'
    implementation 'androidx.compose.material3:material3'
    
    // Navigation
    implementation 'androidx.navigation:navigation-compose:2.7.5'
    
    // ViewModel
    implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0'
    
    // Testing
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.5'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
    androidTestImplementation platform('androidx.compose:compose-bom:2023.10.01')
    androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
    debugImplementation 'androidx.compose.ui:ui-tooling'
    debugImplementation 'androidx.compose.ui:ui-test-manifest'
}

2. MainActivity با Compose

// MainActivity.kt
package com.example.mycomposeapp

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import com.example.mycomposeapp.ui.theme.MyComposeAppTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyComposeAppTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    MyApp()
                }
            }
        }
    }
}

@Composable
fun MyApp() {
    // Your app content here
    Text(
        text = "سلام به Compose!",
        style = MaterialTheme.typography.headlineLarge
    )
}

مفاهیم اصلی Compose

1. Composable Functions

Composable functions قلب Compose هستند. در اینجا مثال‌های کاربردی می‌بینید:

// Basic Composable
@Composable
fun Greeting(name: String) {
    Text(
        text = "سلام $name!",
        style = MaterialTheme.typography.headlineMedium,
        color = MaterialTheme.colorScheme.primary
    )
}

// Composable with State
@Composable
fun Counter() {
    var count by remember { mutableIntStateOf(0) }
    
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(
            text = "شمارش: $count",
            style = MaterialTheme.typography.headlineLarge
        )
        
        Spacer(modifier = Modifier.height(16.dp))
        
        Row {
            Button(
                onClick = { count-- },
                modifier = Modifier.padding(8.dp)
            ) {
                Text("کاهش")
            }
            
            Button(
                onClick = { count++ },
                modifier = Modifier.padding(8.dp)
            ) {
                Text("افزایش")
            }
        }
    }
}

// Complex Composable with Parameters
@Composable
fun UserCard(
    user: User,
    onEditClick: (User) -> Unit,
    onDeleteClick: (User) -> Unit,
    modifier: Modifier = Modifier
) {
    Card(
        modifier = modifier
            .fillMaxWidth()
            .padding(8.dp),
        elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
    ) {
        Column(
            modifier = Modifier.padding(16.dp)
        ) {
            Row(
                modifier = Modifier.fillMaxWidth(),
                horizontalArrangement = Arrangement.SpaceBetween,
                verticalAlignment = Alignment.CenterVertically
            ) {
                Column {
                    Text(
                        text = user.name,
                        style = MaterialTheme.typography.headlineSmall
                    )
                    Text(
                        text = user.email,
                        style = MaterialTheme.typography.bodyMedium,
                        color = MaterialTheme.colorScheme.onSurfaceVariant
                    )
                }
                
                Row {
                    IconButton(onClick = { onEditClick(user) }) {
                        Icon(
                            imageVector = Icons.Default.Edit,
                            contentDescription = "ویرایش"
                        )
                    }
                    IconButton(onClick = { onDeleteClick(user) }) {
                        Icon(
                            imageVector = Icons.Default.Delete,
                            contentDescription = "حذف"
                        )
                    }
                }
            }
        }
    }
}

2. State Management پیشرفته

مدیریت state در Compose نیاز به درک عمیق دارد:

// State Hoisting
@Composable
fun TodoScreen() {
    var todoText by remember { mutableStateOf("") }
    var todos by remember { mutableStateOf(listOf()) }
    
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
    ) {
        // Input field
        OutlinedTextField(
            value = todoText,
            onValueChange = { todoText = it },
            label = { Text("کار جدید") },
            modifier = Modifier.fillMaxWidth()
        )
        
        Spacer(modifier = Modifier.height(16.dp))
        
        // Add button
        Button(
            onClick = {
                if (todoText.isNotBlank()) {
                    todos = todos + Todo(
                        id = System.currentTimeMillis(),
                        text = todoText,
                        isCompleted = false
                    )
                    todoText = ""
                }
            },
            modifier = Modifier.fillMaxWidth()
        ) {
            Text("اضافه کردن")
        }
        
        Spacer(modifier = Modifier.height(16.dp))
        
        // Todo list
        LazyColumn {
            items(todos) { todo ->
                TodoItem(
                    todo = todo,
                    onToggleComplete = { updatedTodo ->
                        todos = todos.map { 
                            if (it.id == updatedTodo.id) updatedTodo else it 
                        }
                    },
                    onDelete = { todoToDelete ->
                        todos = todos.filter { it.id != todoToDelete.id }
                    }
                )
            }
        }
    }
}

@Composable
fun TodoItem(
    todo: Todo,
    onToggleComplete: (Todo) -> Unit,
    onDelete: (Todo) -> Unit
) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(vertical = 4.dp)
    ) {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(16.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            Checkbox(
                checked = todo.isCompleted,
                onCheckedChange = { 
                    onToggleComplete(todo.copy(isCompleted = it))
                }
            )
            
            Spacer(modifier = Modifier.width(8.dp))
            
            Text(
                text = todo.text,
                style = MaterialTheme.typography.bodyLarge,
                modifier = Modifier.weight(1f),
                textDecoration = if (todo.isCompleted) 
                    TextDecoration.LineThrough else null
            )
            
            IconButton(onClick = { onDelete(todo) }) {
                Icon(
                    imageVector = Icons.Default.Delete,
                    contentDescription = "حذف"
                )
            }
        }
    }
}

3. Navigation در Compose

Navigation Component برای Compose:

// Navigation Setup
@Composable
fun MyApp() {
    val navController = rememberNavController()
    
    NavHost(
        navController = navController,
        startDestination = "home"
    ) {
        composable("home") {
            HomeScreen(
                onNavigateToProfile = { 
                    navController.navigate("profile/$it") 
                }
            )
        }
        
        composable(
            "profile/{userId}",
            arguments = listOf(navArgument("userId") { type = NavType.StringType })
        ) { backStackEntry ->
            val userId = backStackEntry.arguments?.getString("userId") ?: ""
            ProfileScreen(
                userId = userId,
                onNavigateBack = { navController.popBackStack() }
            )
        }
        
        composable("settings") {
            SettingsScreen(
                onNavigateBack = { navController.popBackStack() }
            )
        }
    }
}

// Home Screen
@Composable
fun HomeScreen(
    onNavigateToProfile: (String) -> Unit
) {
    val users = remember { 
        listOf(
            User("1", "علی سلمانیان", "ali@example.com"),
            User("2", "مریم احمدی", "maryam@example.com"),
            User("3", "حسن رضایی", "hasan@example.com")
        )
    }
    
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
    ) {
        Text(
            text = "لیست کاربران",
            style = MaterialTheme.typography.headlineLarge,
            modifier = Modifier.padding(bottom = 16.dp)
        )
        
        LazyColumn {
            items(users) { user ->
                UserCard(
                    user = user,
                    onClick = { onNavigateToProfile(user.id) }
                )
            }
        }
    }
}

// Profile Screen
@Composable
fun ProfileScreen(
    userId: String,
    onNavigateBack: () -> Unit
) {
    var user by remember { mutableStateOf(null) }
    var isLoading by remember { mutableStateOf(true) }
    
    LaunchedEffect(userId) {
        // Simulate API call
        delay(1000)
        user = User(userId, "علی سلمانیان", "ali@example.com")
        isLoading = false
    }
    
    if (isLoading) {
        Box(
            modifier = Modifier.fillMaxSize(),
            contentAlignment = Alignment.Center
        ) {
            CircularProgressIndicator()
        }
    } else {
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(16.dp)
        ) {
            TopAppBar(
                title = { Text("پروفایل") },
                navigationIcon = {
                    IconButton(onClick = onNavigateBack) {
                        Icon(
                            imageVector = Icons.Default.ArrowBack,
                            contentDescription = "بازگشت"
                        )
                    }
                }
            )
            
            user?.let { userData ->
                Card(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(16.dp)
                ) {
                    Column(
                        modifier = Modifier.padding(16.dp)
                    ) {
                        Text(
                            text = userData.name,
                            style = MaterialTheme.typography.headlineMedium
                        )
                        Text(
                            text = userData.email,
                            style = MaterialTheme.typography.bodyLarge,
                            color = MaterialTheme.colorScheme.onSurfaceVariant
                        )
                    }
                }
            }
        }
    }
}

Material Design 3 در Compose

1. Theme و Color System

// Theme.kt
@Composable
fun MyComposeAppTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    val colorScheme = when {
        darkTheme -> DarkColorScheme
        else -> LightColorScheme
    }
    
    MaterialTheme(
        colorScheme = colorScheme,
        typography = Typography,
        content = content
    )
}

private val LightColorScheme = lightColorScheme(
    primary = Color(0xFF6750A4),
    onPrimary = Color(0xFFFFFFFF),
    primaryContainer = Color(0xFFEADDFF),
    onPrimaryContainer = Color(0xFF21005D),
    secondary = Color(0xFF625B71),
    onSecondary = Color(0xFFFFFFFF),
    secondaryContainer = Color(0xFFE8DEF8),
    onSecondaryContainer = Color(0xFF1D192B),
    tertiary = Color(0xFF7D5260),
    onTertiary = Color(0xFFFFFFFF),
    tertiaryContainer = Color(0xFFFFD8E4),
    onTertiaryContainer = Color(0xFF31111D),
    error = Color(0xFFBA1A1A),
    onError = Color(0xFFFFFFFF),
    errorContainer = Color(0xFFFFDAD6),
    onErrorContainer = Color(0xFF410002),
    background = Color(0xFFFFFBFE),
    onBackground = Color(0xFF1C1B1F),
    surface = Color(0xFFFFFBFE),
    onSurface = Color(0xFF1C1B1F),
    surfaceVariant = Color(0xFFE7E0EC),
    onSurfaceVariant = Color(0xFF49454F),
    outline = Color(0xFF79747E)
)

private val DarkColorScheme = darkColorScheme(
    primary = Color(0xFFD0BCFF),
    onPrimary = Color(0xFF381E72),
    primaryContainer = Color(0xFF4F378B),
    onPrimaryContainer = Color(0xFFEADDFF),
    secondary = Color(0xFFCCC2DC),
    onSecondary = Color(0xFF332D41),
    secondaryContainer = Color(0xFF4A4458),
    onSecondaryContainer = Color(0xFFE8DEF8),
    tertiary = Color(0xFFEFB8C8),
    onTertiary = Color(0xFF492532),
    tertiaryContainer = Color(0xFF633B48),
    onTertiaryContainer = Color(0xFFFFD8E4),
    error = Color(0xFFFFB4AB),
    onError = Color(0xFF690005),
    errorContainer = Color(0xFF93000A),
    onErrorContainer = Color(0xFFFFDAD6),
    background = Color(0xFF1C1B1F),
    onBackground = Color(0xFFE6E1E5),
    surface = Color(0xFF1C1B1F),
    onSurface = Color(0xFFE6E1E5),
    surfaceVariant = Color(0xFF49454F),
    onSurfaceVariant = Color(0xFFCAC4D0),
    outline = Color(0xFF938F99)
)

2. Typography System

// Typography.kt
val Typography = Typography(
    displayLarge = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Normal,
        fontSize = 57.sp,
        lineHeight = 64.sp,
        letterSpacing = (-0.25).sp
    ),
    displayMedium = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Normal,
        fontSize = 45.sp,
        lineHeight = 52.sp,
        letterSpacing = 0.sp
    ),
    displaySmall = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Normal,
        fontSize = 36.sp,
        lineHeight = 44.sp,
        letterSpacing = 0.sp
    ),
    headlineLarge = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Normal,
        fontSize = 32.sp,
        lineHeight = 40.sp,
        letterSpacing = 0.sp
    ),
    headlineMedium = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Normal,
        fontSize = 28.sp,
        lineHeight = 36.sp,
        letterSpacing = 0.sp
    ),
    headlineSmall = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Normal,
        fontSize = 24.sp,
        lineHeight = 32.sp,
        letterSpacing = 0.sp
    ),
    titleLarge = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Medium,
        fontSize = 22.sp,
        lineHeight = 28.sp,
        letterSpacing = 0.sp
    ),
    titleMedium = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Medium,
        fontSize = 16.sp,
        lineHeight = 24.sp,
        letterSpacing = 0.15.sp
    ),
    titleSmall = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Medium,
        fontSize = 14.sp,
        lineHeight = 20.sp,
        letterSpacing = 0.1.sp
    ),
    bodyLarge = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Normal,
        fontSize = 16.sp,
        lineHeight = 24.sp,
        letterSpacing = 0.15.sp
    ),
    bodyMedium = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Normal,
        fontSize = 14.sp,
        lineHeight = 20.sp,
        letterSpacing = 0.25.sp
    ),
    bodySmall = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Normal,
        fontSize = 12.sp,
        lineHeight = 16.sp,
        letterSpacing = 0.4.sp
    ),
    labelLarge = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Medium,
        fontSize = 14.sp,
        lineHeight = 20.sp,
        letterSpacing = 0.1.sp
    ),
    labelMedium = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Medium,
        fontSize = 12.sp,
        lineHeight = 16.sp,
        letterSpacing = 0.5.sp
    ),
    labelSmall = TextStyle(
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Medium,
        fontSize = 11.sp,
        lineHeight = 16.sp,
        letterSpacing = 0.5.sp
    )
)

انیمیشن‌ها در Compose

1. Basic Animations

// Simple Animation
@Composable
fun AnimatedCounter() {
    var count by remember { mutableIntStateOf(0) }
    val animatedCount by animateIntAsState(
        targetValue = count,
        animationSpec = tween(300)
    )
    
    Column(
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(
            text = "شمارش: $animatedCount",
            style = MaterialTheme.typography.headlineLarge
        )
        
        Button(onClick = { count++ }) {
            Text("افزایش")
        }
    }
}

// Visibility Animation
@Composable
fun AnimatedVisibilityExample() {
    var visible by remember { mutableStateOf(false) }
    
    Column {
        Button(onClick = { visible = !visible }) {
            Text(if (visible) "مخفی کردن" else "نمایش")
        }
        
        AnimatedVisibility(
            visible = visible,
            enter = slideInVertically() + fadeIn(),
            exit = slideOutVertically() + fadeOut()
        ) {
            Card(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(16.dp)
            ) {
                Text(
                    text = "این متن با انیمیشن نمایش داده می‌شود",
                    modifier = Modifier.padding(16.dp)
                )
            }
        }
    }
}

// Complex Animation
@Composable
fun AnimatedCard() {
    var expanded by remember { mutableStateOf(false) }
    val rotation by animateFloatAsState(
        targetValue = if (expanded) 180f else 0f,
        animationSpec = tween(500)
    )
    
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp)
            .clickable { expanded = !expanded }
    ) {
        Column(
            modifier = Modifier.padding(16.dp)
        ) {
            Row(
                modifier = Modifier.fillMaxWidth(),
                horizontalArrangement = Arrangement.SpaceBetween,
                verticalAlignment = Alignment.CenterVertically
            ) {
                Text(
                    text = "کارت قابل انبساط",
                    style = MaterialTheme.typography.titleMedium
                )
                
                Icon(
                    imageVector = Icons.Default.ExpandMore,
                    contentDescription = null,
                    modifier = Modifier.rotate(rotation)
                )
            }
            
            AnimatedVisibility(
                visible = expanded,
                enter = expandVertically() + fadeIn(),
                exit = shrinkVertically() + fadeOut()
            ) {
                Column {
                    Spacer(modifier = Modifier.height(16.dp))
                    Text(
                        text = "این محتوای اضافی است که با انیمیشن نمایش داده می‌شود.",
                        style = MaterialTheme.typography.bodyMedium
                    )
                }
            }
        }
    }
}

بهترین روش‌های توسعه

1. Performance Optimization

// LazyColumn for Large Lists
@Composable
fun ProductList(products: List) {
    LazyColumn(
        modifier = Modifier.fillMaxSize(),
        contentPadding = PaddingValues(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        items(
            items = products,
            key = { product -> product.id }
        ) { product ->
            ProductCard(product = product)
        }
    }
}

// Memoization with remember
@Composable
fun ExpensiveComputation(data: List) {
    val processedData = remember(data) {
        data.map { item ->
            // Expensive computation
            processData(item)
        }
    }
    
    LazyColumn {
        items(processedData) { item ->
            DataItem(item = item)
        }
    }
}

// State Hoisting
@Composable
fun SearchableList(
    items: List,
    onItemClick: (Item) -> Unit
) {
    var searchQuery by remember { mutableStateOf("") }
    var filteredItems by remember(searchQuery, items) {
        derivedStateOf {
            items.filter { item ->
                item.name.contains(searchQuery, ignoreCase = true)
            }
        }
    }
    
    Column {
        SearchBar(
            query = searchQuery,
            onQueryChange = { searchQuery = it }
        )
        
        LazyColumn {
            items(filteredItems) { item ->
                ItemCard(
                    item = item,
                    onClick = { onItemClick(item) }
                )
            }
        }
    }
}

2. Testing در Compose

// Unit Test
@Test
fun `Counter should increment when button clicked`() {
    composeTestRule.setContent {
        Counter()
    }
    
    composeTestRule
        .onNodeWithText("شمارش: 0")
        .assertIsDisplayed()
    
    composeTestRule
        .onNodeWithText("افزایش")
        .performClick()
    
    composeTestRule
        .onNodeWithText("شمارش: 1")
        .assertIsDisplayed()
}

// UI Test
@Test
fun `UserCard should display user information`() {
    val user = User("1", "علی سلمانیان", "ali@example.com")
    
    composeTestRule.setContent {
        UserCard(
            user = user,
            onEditClick = {},
            onDeleteClick = {}
        )
    }
    
    composeTestRule
        .onNodeWithText("علی سلمانیان")
        .assertIsDisplayed()
    
    composeTestRule
        .onNodeWithText("ali@example.com")
        .assertIsDisplayed()
}

// Integration Test
@Test
fun `Navigation should work correctly`() {
    composeTestRule.setContent {
        MyApp()
    }
    
    // Navigate to profile
    composeTestRule
        .onNodeWithText("علی سلمانیان")
        .performClick()
    
    composeTestRule
        .onNodeWithText("پروفایل")
        .assertIsDisplayed()
}

💡 نکات مهم Compose:

  • همیشه از remember برای state استفاده کنید
  • State را به بالاترین سطح ممکن منتقل کنید
  • از LazyColumn برای لیست‌های بزرگ استفاده کنید
  • Component ها را کوچک و قابل استفاده مجدد نگه دارید
  • از Material Design 3 استفاده کنید

مقالات مرتبط

برای یادگیری بیشتر، مقالات زیر را مطالعه کنید:

 

درباره نویسنده

علی سلمانیان - برنامه نویس وب و اندروید با تخصص در Jetpack Compose. متخصص Kotlin، Material Design و Android Development.

📧 alisalmanian1395@gmail.com | 📱 +98 938 822 2808

تکنولوژی‌های استفاده شده:
Android Jetpack Compose Kotlin
آماده شروع پروژه خود هستید؟

با من تماس بگیرید تا درباره پروژه شما صحبت کنیم

نظرات (0)

برای ثبت نظر باید وارد حساب کاربری خود شوید.

ورود / ثبت نام

هنوز نظری ثبت نشده است. اولین نفری باشید که نظر می‌دهد!