Build Android App với Jetpack Compose
Cập nhật 2026
- Kotlin 2.1 stable với Compose multiplatform improvements
- Jetpack Compose 1.8 default cho Android dev mới
- Android Studio Narwhal 2026.1+ với Gemini in IDE
- Android 15+ target SDK requirement cho new apps trên Play Store
- VN context: Android chiếm ~85% VN market — mass market app PHẢI có Android
Tại sao Android native + Jetpack Compose?
- Android 85% VN — mass market reach
- Jetpack Compose: modern declarative UI (giống SwiftUI)
- Kotlin: concise, type-safe, official Google
- Performance native tốt nhất
- Free dev tool, dev được trên mọi OS (Windows, Mac, Linux)
Prerequisites
- Android Studio (free, ~3GB)
- JDK 17+ (Android Studio install kèm)
- Android device hoặc emulator để test
- Google Play Developer account ($25 one-time, chỉ cần khi publish)
Chương 1: setup
1.1 Cài Android Studio
- Download https://developer.android.com/studio
- Install (~3GB)
- Mở → Setup Wizard
- Install SDK platform (Android 15)
- Install AVD (Android Virtual Device)
1.2 Tạo project
- New Project → "Empty Activity" (Compose)
- Name:
MyFirstApp - Package:
com.yourname.myfirstapp - Language: Kotlin
- Minimum SDK: API 24 (Android 7.0) — cover 99% device
- Build configuration: Kotlin DSL
Project structure:
app/
├── src/main/
│ ├── AndroidManifest.xml
│ ├── kotlin/com/yourname/myfirstapp/
│ │ ├── MainActivity.kt
│ │ └── ui/theme/
│ └── res/
└── build.gradle.kts1.3 Run trên Emulator
- Tools → Device Manager → Create Device → Pixel 8 Pro
- Click Run
- App build và chạy trong emulator
Chương 2: Compose fundamentals
2.1 MainActivity
kotlin
package com.yourname.myfirstapp
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyAppTheme {
Surface(modifier = Modifier.fillMaxSize()) {
GreetingScreen()
}
}
}
}
}
@Composable
fun GreetingScreen() {
Column(
modifier = Modifier.fillMaxSize().padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Xin chào Việt Nam!",
style = MaterialTheme.typography.headlineLarge
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Welcome to Jetpack Compose",
style = MaterialTheme.typography.bodyLarge
)
}
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
MyAppTheme { GreetingScreen() }
}2.2 State management
kotlin
@Composable
fun CounterScreen() {
var count by remember { mutableStateOf(0) }
Column(
modifier = Modifier.fillMaxSize().padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Đếm: $count",
style = MaterialTheme.typography.headlineMedium
)
Spacer(modifier = Modifier.height(16.dp))
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
Button(onClick = { count-- }) { Text("−") }
Button(onClick = { count++ }) { Text("+") }
}
}
}2.3 List + Navigation
kotlin
data class Contact(val name: String, val phone: String)
@Composable
fun ContactsScreen(onContactClick: (Contact) -> Unit) {
val contacts = listOf(
Contact("Nguyễn Văn A", "0901234567"),
Contact("Trần Thị B", "0912345678"),
Contact("Lê Minh C", "0923456789")
)
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(contacts) { contact ->
ContactItem(contact = contact, onClick = { onContactClick(contact) })
Divider()
}
}
}
@Composable
fun ContactItem(contact: Contact, onClick: () -> Unit) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onClick() }
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.Person,
contentDescription = null,
modifier = Modifier.size(40.dp)
)
Spacer(modifier = Modifier.width(16.dp))
Column {
Text(contact.name, style = MaterialTheme.typography.titleMedium)
Text(contact.phone, style = MaterialTheme.typography.bodyMedium)
}
}
}2.4 Navigation
gradle
// build.gradle.kts (Module: app)
dependencies {
implementation("androidx.navigation:navigation-compose:2.8.0")
}kotlin
@Composable
fun AppNavigation() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "contacts") {
composable("contacts") {
ContactsScreen(onContactClick = { contact ->
navController.navigate("detail/${contact.name}")
})
}
composable("detail/{name}") { entry ->
val name = entry.arguments?.getString("name") ?: ""
ContactDetailScreen(name = name)
}
}
}Chương 3: networking với Retrofit + Coroutines
3.1 Dependency
gradle
dependencies {
implementation("com.squareup.retrofit2:retrofit:2.11.0")
implementation("com.squareup.retrofit2:converter-moshi:2.11.0")
implementation("com.squareup.moshi:moshi-kotlin:1.15.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.0")
}3.2 API service
kotlin
import retrofit2.http.GET
data class Post(val id: Int, val title: String, val body: String)
interface ApiService {
@GET("posts")
suspend fun getPosts(): List<Post>
}
object RetrofitClient {
val api: ApiService = Retrofit.Builder()
.baseUrl("https://jsonplaceholder.typicode.com/")
.addConverterFactory(MoshiConverterFactory.create())
.build()
.create(ApiService::class.java)
}3.3 ViewModel + UI
kotlin
class PostsViewModel : ViewModel() {
var posts by mutableStateOf<List<Post>>(emptyList())
private set
var isLoading by mutableStateOf(false)
private set
var error by mutableStateOf<String?>(null)
private set
fun fetchPosts() {
viewModelScope.launch {
isLoading = true
try {
posts = RetrofitClient.api.getPosts()
} catch (e: Exception) {
error = e.message
} finally {
isLoading = false
}
}
}
}
@Composable
fun PostsScreen(viewModel: PostsViewModel = viewModel()) {
LaunchedEffect(Unit) { viewModel.fetchPosts() }
Box(modifier = Modifier.fillMaxSize()) {
when {
viewModel.isLoading -> CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
viewModel.error != null -> Text("Lỗi: ${viewModel.error}", color = Color.Red)
else -> LazyColumn {
items(viewModel.posts) { post ->
PostItem(post)
}
}
}
}
}Chương 4: Gemini Nano on-device
Android 14+ có Gemini Nano built-in:
gradle
dependencies {
implementation("com.google.ai.edge.aicore:aicore:0.0.1-exp01")
}kotlin
import com.google.ai.edge.aicore.GenerativeModel
class AISummarizer(private val context: Context) {
private val model = GenerativeModel(
generationConfig {
temperature = 0.7f
maxOutputTokens = 500
}
)
suspend fun summarize(text: String): String {
val response = model.generateContent(
"Tóm tắt text sau bằng tiếng Việt, 3 bullet point:\n\n$text"
)
return response.text ?: "Không có kết quả"
}
}Free, on-device, không cần internet.
Chương 5: features Android native
5.1 Camera
kotlin
@Composable
fun CameraScreen() {
val context = LocalContext.current
var capturedImage by remember { mutableStateOf<Bitmap?>(null) }
val cameraLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.TakePicturePreview()
) { bitmap -> capturedImage = bitmap }
Column {
Button(onClick = { cameraLauncher.launch(null) }) {
Text("Chụp ảnh")
}
capturedImage?.let { bitmap ->
Image(bitmap = bitmap.asImageBitmap(), contentDescription = null)
}
}
}5.2 Location
gradle
dependencies {
implementation("com.google.android.gms:play-services-location:21.3.0")
}kotlin
import com.google.android.gms.location.LocationServices
suspend fun getCurrentLocation(context: Context): Location? {
val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context)
return suspendCoroutine { continuation ->
fusedLocationClient.lastLocation
.addOnSuccessListener { location -> continuation.resume(location) }
.addOnFailureListener { continuation.resume(null) }
}
}Add permission trong AndroidManifest.xml:
xml
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>5.3 Local storage với DataStore
gradle
dependencies {
implementation("androidx.datastore:datastore-preferences:1.1.0")
}kotlin
val Context.dataStore by preferencesDataStore(name = "settings")
class SettingsRepository(private val context: Context) {
private val THEME_KEY = stringPreferencesKey("theme")
val themeFlow: Flow<String> = context.dataStore.data
.map { preferences -> preferences[THEME_KEY] ?: "light" }
suspend fun setTheme(theme: String) {
context.dataStore.edit { preferences ->
preferences[THEME_KEY] = theme
}
}
}5.4 Database với Room
gradle
dependencies {
implementation("androidx.room:room-runtime:2.6.1")
implementation("androidx.room:room-ktx:2.6.1")
ksp("androidx.room:room-compiler:2.6.1")
}kotlin
@Entity
data class Note(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val title: String,
val content: String,
val createdAt: Long = System.currentTimeMillis()
)
@Dao
interface NoteDao {
@Query("SELECT * FROM Note ORDER BY createdAt DESC")
fun getAll(): Flow<List<Note>>
@Insert
suspend fun insert(note: Note)
}
@Database(entities = [Note::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun noteDao(): NoteDao
}Chương 6: publish Play Store
6.1 Google Play Developer account
- https://play.google.com/console
- $25 one-time fee
- Verify identity
6.2 Build signed APK / AAB
- Build → Generate Signed Bundle/APK
- Choose Android App Bundle (AAB) — required by Play Store
- Create keystore (lưu kỹ! Mất là không update được app)
- Sign release variant
6.3 Upload Play Console
- Create app
- Fill app details:
- Title, short description, full description (cả VN + EN)
- Category (Productivity, Games...)
- Content rating questionnaire
- Target audience
- Upload screenshots, icon (512x512), feature graphic (1024x500)
- Upload AAB
- Submit for review
Review thường 1-7 ngày.
6.4 Internal testing trước
Trước public release:
- Play Console → Testing → Internal testing
- Add 100 tester via email
- Tester opt-in via link
- Test feedback → fix → re-upload
Chương 7: best practice
Performance
- Compose performance:
remember,derivedStateOf,keyđúng cách - Lazy lists cho data lớn
- Image caching: Coil library
- Profile với Android Studio Profiler
UX
- Material Design 3 guidelines
- Dark theme support
- Edge-to-edge display (Android 15+)
- Adaptive layout cho tablet, foldable
- Vietnamese localization
Architecture
- MVVM với ViewModel + StateFlow
- Use Cases / Repository pattern
- Dependency injection (Hilt)
- Module hoá với Gradle modules
- Test: JUnit + Compose UI test
Security
- Encrypted SharedPreferences cho secret
- Network security config (HTTPS only)
- Certificate pinning cho banking
- Biometric auth
- ProGuard / R8 cho release build
Câu hỏi thường gặp
Q1: Compose vs XML View?
Compose 2026 đã mature. Default cho project mới. XML View vẫn dùng được khi:
- Maintain legacy
- Cần feature chưa có ở Compose
- Lib bên thứ 3 chỉ XML
Q2: Java vs Kotlin?
Kotlin official từ 2017. Tất cả new project nên Kotlin. Java vẫn work nhưng deprecated cho new feature.
Q3: React Native vs Native Android cho VN?
| Tiêu chí | Native Android | React Native |
|---|---|---|
| Performance | Tốt nhất | Tốt cho most case |
| Dev time | Lâu hơn | Nhanh |
| Audience | Android only | iOS + Android share code |
| Tooling | Mature (Android Studio) | Maturing |
| Best for | Performance-critical, native feature | MVP, cross-platform |
Q4: Cost dev Android?
- Hardware: $0 (work trên Windows/Mac/Linux thường có)
- Android Studio: free
- Play Developer: $25 one-time
- Play Store fee: 15-30% revenue
- Tổng MVP dev solo: $25 + thời gian
Q5: Học bao lâu?
- Basic Compose UI: 2-3 tuần
- Production-ready app: 2-4 tháng
- Master: 6-12 tháng
Tài liệu tham khảo
Phụ lục: Android dev 2026 cho VN
A. Trend 2026
- Compose Multiplatform (Q2/2026): share code Android + iOS + Desktop + Web
- Gemini in Android Studio (Narwhal): AI built-in IDE
- AndroidX Bluetooth new API
- App size budget: Play Store limit 200MB bundle, 4GB total
- Privacy Sandbox: SDK Runtime, FLEDGE for advertising
B. VN-specific tips
- Localization: tiếng Việt từ đầu, dùng
res/values-vi/strings.xml - Payment: Google Play Billing ($) hoặc external (Momo, ZaloPay) cho subscription. Cẩn thận Google policy
- Push: Firebase Cloud Messaging (FCM) standard
- Maps: Google Maps SDK, alternative MapBox
- Phone format VN: dùng
libphonenumberlibrary - VietQR: tích hợp
viet-qr-androidlibrary - Vietnam ID OCR: Mindee API hoặc custom ML model
C. Stack đề xuất 2026
UI: Jetpack Compose + Material 3
Architecture: MVVM + StateFlow + Use Cases
DI: Hilt
Network: Retrofit + OkHttp + Moshi
Database: Room + DataStore
Auth: Firebase Auth + Google Sign-In + Sign in with Zalo
Image: Coil 2.x
Async: Coroutines + Flow
Test: JUnit5 + Mockk + Compose UI Test
Analytics: Firebase Analytics + Posthog
Crash: Firebase Crashlytics hoặc Sentry
CI/CD: GitHub Actions + FastlaneD. Common pitfalls
- Recomposition vô độ: dùng
remember,derivedStateOftránh re-render thừa - Memory leak: ViewModel hold Context → leak
- Background task: Android 15+ restrict tighter, dùng WorkManager
- Permission: Android 13+ có notification permission, 14+ có photo permission granular
- Target SDK update mandatory: mỗi năm phải bump target SDK
- Sign in with Google: deprecate cách cũ, dùng Credential Manager (Android 14+)
E. Build cho thị trường lower-end VN
VN có nhiều device tier-2 (low RAM, slow CPU):
- Test trên emulator API 28-30 (mid-range device)
- Optimize cho 2GB RAM device
- App size <50MB lý tưởng
- Use Lite Mode (Firebase) cho slow network
- Offline-first design