Android 개발을 처음 시작하는 분들을 위한 상세한 가이드입니다.
이 프로젝트는 Android 멀티 모듈 애플리케이션입니다. 스마트폰과 자동차(Android Automotive) 모두에서 작동하는 음악 플레이어 앱의 기본 구조를 담고 있습니다.
- 언어: Kotlin (코틀린) 2.0.21
- 최소 SDK: Android 9.0 (API 28)
- 대상 SDK: Android 36
- 빌드 도구: Gradle 8.13
- UI 프레임워크: Material Design 3
- mobile: 스마트폰용 앱
- automotive: 자동차용 앱
- shared: 두 앱이 공유하는 라이브러리
TsetApplication/
├── mobile/ # 📱 스마트폰 앱 모듈
├── automotive/ # 🚗 자동차 앱 모듈
├── shared/ # 📦 공유 라이브러리
├── gradle/ # ⚙️ Gradle 빌드 설정
├── build.gradle.kts # 🔧 루트 빌드 설정
├── settings.gradle.kts # 🔧 프로젝트 설정
└── README.md # 📖 이 문서
mobile/
├── build.gradle.kts # 모듈 빌드 설정
├── src/
│ ├── main/ # 메인 소스 코드
│ │ ├── java/com/example/tsetapplication/ # Kotlin 코드
│ │ │ └── MainActivity.kt # 메인 화면 액티비티
│ │ ├── res/ # 리소스 파일들
│ │ │ ├── layout/ # 화면 레이아웃
│ │ │ │ └── activity_main.xml # 메인 화면 레이아웃
│ │ │ ├── drawable/ # 이미지, 도형 등
│ │ │ ├── mipmap-*/ # 앱 아이콘 (화질별)
│ │ │ ├── values/ # 값 리소스
│ │ │ │ ├── colors.xml # 색상 정의
│ │ │ │ ├── strings.xml # 문자열 정의
│ │ │ │ └── themes.xml # 테마 정의
│ │ │ └── xml/ # 기타 XML 설정
│ │ └── AndroidManifest.xml # 앱 설정 파일
│ ├── test/ # 단위 테스트
│ └── androidTest/ # UI 테스트
└── proguard-rules.pro # 코드 난독화 규칙
- 앱의 실제 로직이 작성되는 곳
- 패키지명 형식:
com.example.tsetapplication - 모든
.kt파일(코틀린 파일)이 여기에 위치
- layout/: XML로 작성된 화면 레이아웃
- drawable/: 이미지, 벡터 그래픽, 도형 등
- mipmap-*/: 앱 아이콘 (hdpi, xhdpi 등 화질별 분류)
- values/: 색상, 문자열, 스타일 등의 값 정의
- xml/: 백업 규칙, 기타 설정 파일
- 앱의 기본 정보를 선언하는 필수 파일
- 앱 이름, 아이콘, 권한, 액티비티 등을 정의
shared/
├── build.gradle # 라이브러리 빌드 설정
├── src/
│ └── main/
│ ├── java/com/example/tsetapplication/shared/
│ │ └── MyMusicService.kt # 음악 서비스
│ ├── res/
│ │ └── xml/
│ │ └── automotive_app_desc.xml # Automotive 설정
│ └── AndroidManifest.xml # 서비스 선언
- 전체 프로젝트의 공통 플러그인 정의
- 모든 모듈에 적용되는 기본 설정
- 프로젝트에 포함될 모듈 선언
- 저장소(Repository) 설정
- Gradle 빌드 성능 설정
- JVM 메모리 옵션 등
- 의존성 라이브러리 버전 관리
- 중앙 집중식 버전 관리 파일
코틀린(Kotlin)은 JetBrains가 개발한 현대적인 프로그래밍 언어로, Google이 Android 공식 언어로 채택했습니다.
- ✅ 간결함: Java보다 적은 코드로 같은 기능 구현
- ✅ 안전성: Null 안전성으로 런타임 오류 감소
- ✅ 상호운용성: Java 코드와 100% 호환
- ✅ 현대적: 함수형 프로그래밍, 코루틴 등 지원
// val: 불변 변수 (값 변경 불가, Java의 final)
val name = "Android"
val count: Int = 10
// var: 가변 변수 (값 변경 가능)
var score = 0
score = 100 // OK// 기본 형태
fun greet(name: String): String {
return "Hello, $name!"
}
// 단일 표현식 함수
fun add(a: Int, b: Int) = a + b
// 반환값 없는 함수
fun printMessage(msg: String): Unit { // Unit은 생략 가능
println(msg)
}// 기본 클래스
class Person {
var name: String = ""
var age: Int = 0
}
// 생성자가 있는 클래스
class Student(val name: String, var grade: Int) {
fun study() {
println("$name is studying")
}
}
// 클래스 사용
val student = Student("John", 10)
student.study() // "John is studying"// 부모 클래스 (open 키워드 필요)
open class Animal {
open fun sound() {
println("Some sound")
}
}
// 자식 클래스
class Dog : Animal() {
override fun sound() {
println("Bark!")
}
}// Nullable 타입 (? 붙임)
var name: String? = null
name = "Kotlin"
// Non-null 타입 (? 없음)
var age: Int = 10
// age = null // 컴파일 오류!
// 안전한 호출
val length = name?.length // name이 null이면 length도 null
// Elvis 연산자
val len = name?.length ?: 0 // name이 null이면 0 반환// 기본 형태
val sum = { a: Int, b: Int -> a + b }
println(sum(3, 5)) // 8
// 리스너에서 자주 사용
button.setOnClickListener { view ->
// 버튼 클릭 시 실행
println("Button clicked")
}
// 파라미터가 하나일 때 it 사용
items.forEach { item ->
println(item)
}
// 더 간단하게
items.forEach {
println(it) // it은 현재 item
}// apply: 객체 설정에 유용
val person = Person().apply {
name = "John"
age = 25
}
// let: null 체크와 함께 사용
name?.let {
println("Name is $it")
}
// with: 객체의 여러 메서드 호출
with(person) {
println(name)
println(age)
}Android 앱은 4가지 주요 구성 요소로 이루어집니다:
- 사용자 인터페이스 화면을 담당
- 하나의 Activity = 하나의 화면
- 예: 로그인 화면, 메인 화면, 설정 화면 등
- 백그라운드 작업을 담당
- 화면 없이 실행
- 예: 음악 재생, 파일 다운로드 등
- 시스템 이벤트를 수신
- 예: 배터리 부족 알림, 네트워크 변경 등
- 앱 간 데이터 공유
- 예: 연락처, 갤러리 데이터 접근
사용자가 앱 아이콘 터치
↓
Android 시스템이 앱 프로세스 시작
↓
Application 클래스 생성 (앱 전체 초기화)
↓
AndroidManifest.xml 읽기
↓
MAIN/LAUNCHER 액티비티 찾기
↓
MainActivity 생성 및 onCreate() 호출
↓
레이아웃 파일(activity_main.xml) 로드
↓
화면에 UI 표시
↓
사용자 입력 대기
Activity는 생명주기를 가지며, 각 단계마다 호출되는 콜백 메서드가 있습니다:
class MainActivity : AppCompatActivity() {
// 1. 액티비티가 생성될 때 (최초 1회)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 초기화 작업: UI 설정, 변수 초기화 등
}
// 2. 화면이 보이기 시작할 때
override fun onStart() {
super.onStart()
// 리소스 준비
}
// 3. 사용자와 상호작용 가능한 상태
override fun onResume() {
super.onResume()
// 애니메이션 시작, 센서 등록 등
}
// 4. 일시 정지 (다른 앱이 foreground로 옴)
override fun onPause() {
super.onPause()
// 애니메이션 중지, 데이터 저장 등
}
// 5. 화면이 보이지 않을 때
override fun onStop() {
super.onStop()
// 리소스 해제
}
// 6. 액티비티가 완전히 종료될 때
override fun onDestroy() {
super.onDestroy()
// 최종 정리 작업
}
}onCreate() → onStart() → onResume() [실행 중] → onPause() → onStop() → onDestroy()
↑ ↓
└─────────────── onRestart() ←──────────────────────┘
모든 Android 앱은 AndroidManifest.xml 파일을 가져야 하며, 다음 정보를 포함합니다:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.tsetapplication">
<!-- 권한 선언 -->
<uses-permission android:name="android.permission.INTERNET" />
<application
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/Theme.TsetApplication">
<!-- 액티비티 선언 -->
<activity
android:name=".MainActivity"
android:exported="true">
<!-- 앱 런처에 표시할 메인 액티비티 지정 -->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- 서비스 선언 -->
<service android:name=".MyMusicService" />
</application>
</manifest>Android UI는 XML 레이아웃과 Kotlin 코드로 구성됩니다:
화면 = Layout (XML) + Activity (Kotlin)
-
View: 화면에 표시되는 모든 UI 요소의 기본 클래스
- 예: TextView, Button, ImageView 등
-
ViewGroup: View들을 담는 컨테이너
- 예: LinearLayout, ConstraintLayout, FrameLayout 등
ViewGroup (컨테이너)
├── View (텍스트)
├── View (버튼)
└── ViewGroup (하위 컨테이너)
├── View (이미지)
└── View (입력창)
<?xml version="1.0" encoding="utf-8"?>
<!-- ConstraintLayout: 제약 조건으로 View 위치를 지정하는 레이아웃 -->
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/main"
android:layout_width="match_parent" <!-- 부모 너비만큼 -->
android:layout_height="match_parent"> <!-- 부모 높이만큼 -->
<!-- TextView: 텍스트를 표시하는 View -->
<TextView
android:layout_width="wrap_content" <!-- 내용만큼 -->
android:layout_height="wrap_content" <!-- 내용만큼 -->
android:text="Hello World!"
<!-- 제약 조건: 화면 중앙에 배치 -->
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>| 속성 | 설명 | 가능한 값 |
|---|---|---|
android:id |
View의 고유 식별자 | @+id/view_name |
layout_width |
너비 | match_parent, wrap_content, 100dp |
layout_height |
높이 | match_parent, wrap_content, 100dp |
android:text |
텍스트 내용 | "문자열" 또는 @string/name |
android:textSize |
글자 크기 | 16sp |
android:textColor |
글자 색상 | #FF0000 또는 @color/red |
- 유연한 레이아웃
- 성능 최적화
- 복잡한 UI도 깊이 없이 구현 가능
<androidx.constraintlayout.widget.ConstraintLayout>
<Button
android:id="@+id/button1"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<Button
android:id="@+id/button2"
app:layout_constraintTop_toBottomOf="@id/button1"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>- 세로 또는 가로로 순서대로 배치
<LinearLayout
android:orientation="vertical"> <!-- 세로 배치 -->
<TextView android:text="첫 번째" />
<TextView android:text="두 번째" />
<TextView android:text="세 번째" />
</LinearLayout>- View를 겹쳐서 배치
<FrameLayout>
<ImageView /> <!-- 배경 이미지 -->
<TextView /> <!-- 이미지 위에 텍스트 -->
</FrameLayout>class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// XML의 View를 Kotlin 코드에서 가져오기
val textView = findViewById<TextView>(R.id.textView)
textView.text = "텍스트 변경"
val button = findViewById<Button>(R.id.button)
button.setOnClickListener {
// 버튼 클릭 시 실행
textView.text = "버튼이 클릭되었습니다"
}
}
}// build.gradle.kts에 설정 필요
android {
buildFeatures {
viewBinding = true
}
}
// Activity에서 사용
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// View에 쉽게 접근
binding.textView.text = "텍스트 변경"
binding.button.setOnClickListener {
binding.textView.text = "버튼 클릭됨"
}
}
}// 방법 1: setOnClickListener
button.setOnClickListener {
Toast.makeText(this, "버튼 클릭!", Toast.LENGTH_SHORT).show()
}
// 방법 2: 람다 표현식으로 View 접근
button.setOnClickListener { view ->
view.isEnabled = false // 버튼 비활성화
}val editText = findViewById<EditText>(R.id.editText)
// 텍스트 변경 감지
editText.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
println("입력된 텍스트: ${s.toString()}")
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
})// 짧은 메시지 (2초)
Toast.makeText(this, "저장되었습니다", Toast.LENGTH_SHORT).show()
// 긴 메시지 (3.5초)
Toast.makeText(this, "오류가 발생했습니다", Toast.LENGTH_LONG).show()Snackbar.make(view, "삭제되었습니다", Snackbar.LENGTH_LONG)
.setAction("취소") {
// 취소 버튼 클릭 시 실행
println("삭제 취소")
}
.show()// 간단한 이동
val intent = Intent(this, SecondActivity::class.java)
startActivity(intent)
// 데이터 전달
val intent = Intent(this, SecondActivity::class.java)
intent.putExtra("name", "John")
intent.putExtra("age", 25)
startActivity(intent)
// SecondActivity에서 데이터 받기
class SecondActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val name = intent.getStringExtra("name")
val age = intent.getIntExtra("age", 0)
println("Name: $name, Age: $age")
}
}RecyclerView는 긴 목록을 효율적으로 표시하는 뷰입니다.
// 1. 데이터 클래스
data class Item(val title: String, val description: String)
// 2. ViewHolder
class ItemViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val titleText: TextView = view.findViewById(R.id.titleText)
val descText: TextView = view.findViewById(R.id.descText)
}
// 3. Adapter
class ItemAdapter(private val items: List<Item>) :
RecyclerView.Adapter<ItemViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_layout, parent, false)
return ItemViewHolder(view)
}
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
val item = items[position]
holder.titleText.text = item.title
holder.descText.text = item.description
}
override fun getItemCount() = items.size
}
// 4. Activity에서 사용
val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = ItemAdapter(itemList)파일 위치: mobile/src/main/java/com/example/tsetapplication/MainActivity.kt
package com.example.tsetapplication
// 필요한 클래스들을 import (가져오기)
import android.os.Bundle // 데이터 전달용
import androidx.activity.enableEdgeToEdge // 전체 화면 모드
import androidx.appcompat.app.AppCompatActivity // 기본 Activity 클래스
import androidx.core.view.ViewCompat // View 호환성 도구
import androidx.core.view.WindowInsetsCompat // 시스템 바 정보
// MainActivity 클래스 선언
// : AppCompatActivity() = AppCompatActivity를 상속받음
class MainActivity : AppCompatActivity() {
// override = 부모 클래스의 메서드를 재정의
// onCreate = Activity가 생성될 때 호출되는 메서드
override fun onCreate(savedInstanceState: Bundle?) {
// 부모 클래스의 onCreate 먼저 호출 (필수!)
super.onCreate(savedInstanceState)
// Edge-to-Edge 모드 활성화
// 상태바, 내비게이션바 뒤까지 컨텐츠 표시
enableEdgeToEdge()
// XML 레이아웃 파일을 화면에 표시
// R.layout.activity_main = res/layout/activity_main.xml
setContentView(R.layout.activity_main)
// 시스템 바(상태바, 내비게이션바) 영역 처리
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
// 시스템 바의 크기 정보 가져오기
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
// View에 패딩 설정 (시스템 바에 가려지지 않도록)
v.setPadding(
systemBars.left, // 왼쪽 패딩
systemBars.top, // 위쪽 패딩
systemBars.right, // 오른쪽 패딩
systemBars.bottom // 아래쪽 패딩
)
// insets를 반환 (다른 리스너에서도 사용 가능하도록)
insets
}
}
}- 사용자가 앱 아이콘 터치
- Android 시스템이 MainActivity 인스턴스 생성
onCreate()메서드 호출enableEdgeToEdge()- 전체 화면 모드 활성화setContentView()- activity_main.xml 레이아웃 로드setOnApplyWindowInsetsListener()- 시스템 바 영역 패딩 설정- 화면 표시 완료
AppCompatActivity
- 모든 Activity의 기본 클래스
- Android 이전 버전과의 호환성 제공
- ActionBar, 테마 등의 기능 포함
Bundle
- 데이터를 저장하고 전달하는 컨테이너
- Activity 재생성 시 상태 복원에 사용
savedInstanceState로 이전 상태 받음
findViewById()
- XML에 정의된 View를 Kotlin 코드에서 찾는 메서드
- ID로 View를 검색
- 반환 타입: View (형변환 필요)
파일 위치: mobile/src/main/res/layout/activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- XML 선언: 파일 버전과 인코딩 지정 -->
<!-- ConstraintLayout: 제약 조건 기반 레이아웃 -->
<androidx.constraintlayout.widget.ConstraintLayout
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"
<!-- View의 고유 ID (MainActivity에서 findViewById로 찾을 때 사용) -->
android:id="@+id/main"
<!-- 레이아웃 크기 -->
android:layout_width="match_parent" <!-- 부모 너비만큼 (화면 전체) -->
android:layout_height="match_parent" <!-- 부모 높이만큼 (화면 전체) -->
<!-- tools 네임스페이스: 개발 시에만 사용, 실제 앱에는 영향 없음 -->
tools:context=".MainActivity">
<!-- TextView: 텍스트를 표시하는 View -->
<TextView
<!-- wrap_content: 내용물 크기만큼 -->
android:layout_width="wrap_content"
android:layout_height="wrap_content"
<!-- 표시할 텍스트 -->
android:text="Hello World!"
<!-- ConstraintLayout 제약 조건들 -->
<!-- Bottom을 parent의 Bottom에 연결 -->
app:layout_constraintBottom_toBottomOf="parent"
<!-- End(오른쪽)를 parent의 End에 연결 -->
app:layout_constraintEnd_toEndOf="parent"
<!-- Start(왼쪽)를 parent의 Start에 연결 -->
app:layout_constraintStart_toStartOf="parent"
<!-- Top을 parent의 Top에 연결 -->
app:layout_constraintTop_toTopOf="parent" />
<!-- 네 방향 모두 parent 중앙에 연결 → 화면 정중앙에 배치 -->
</androidx.constraintlayout.widget.ConstraintLayout>화면 (parent)
┌─────────────────────────────┐
│ │
│ │ ← Top
│ TextView │ ← 모든 방향이 parent와 연결되어
│ "Hello World!" │ 화면 정중앙에 위치
│ │ ← Bottom
│ │
└─────────────────────────────┘
Start → ← End
파일 위치: shared/src/main/java/com/example/tsetapplication/shared/MyMusicService.kt
package com.example.tsetapplication.shared
// 필요한 클래스 import
import android.os.Bundle
import android.support.v4.media.MediaBrowserCompat.MediaItem
import androidx.media.MediaBrowserServiceCompat
import android.support.v4.media.session.MediaSessionCompat
import java.util.ArrayList
/**
* MyMusicService: 음악 재생 서비스
*
* MediaBrowserServiceCompat를 상속받아 구현
* - Android Auto, Automotive에서 음악 탐색 가능
* - 백그라운드에서 음악 재생
* - 다른 앱(예: Android Auto)에서 제어 가능
*/
class MyMusicService : MediaBrowserServiceCompat() {
// MediaSession: 미디어 재생 상태 관리
// lateinit: 나중에 초기화할 변수 (onCreate에서 초기화)
private lateinit var session: MediaSessionCompat
// callback: 사용자 동작(재생, 일시정지 등)을 처리하는 객체
// object: 익명 클래스 인스턴스 생성
private val callback = object : MediaSessionCompat.Callback() {
// 재생 버튼을 눌렀을 때
override fun onPlay() {
// TODO: 실제 음악 재생 로직 구현
}
// 특정 곡으로 이동
override fun onSkipToQueueItem(queueId: Long) {}
// 특정 위치로 이동 (예: 30초로 이동)
override fun onSeekTo(position: Long) {}
// 미디어 ID로 재생
override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) {}
// 일시정지
override fun onPause() {}
// 정지
override fun onStop() {}
// 다음 곡
override fun onSkipToNext() {}
// 이전 곡
override fun onSkipToPrevious() {}
// 사용자 정의 동작
override fun onCustomAction(action: String?, extras: Bundle?) {}
// 검색어로 재생 (예: "비틀즈 노래 틀어줘")
override fun onPlayFromSearch(query: String?, extras: Bundle?) {}
}
// 서비스가 생성될 때 호출 (최초 1회)
override fun onCreate() {
super.onCreate()
// MediaSession 생성
session = MediaSessionCompat(this, "MyMusicService")
// Session Token 설정 (다른 앱이 이 서비스를 찾을 수 있게 함)
sessionToken = session.sessionToken
// Callback 등록 (사용자 동작 처리)
session.setCallback(callback)
// 플래그 설정
session.setFlags(
// 미디어 버튼 처리 (헤드셋 버튼 등)
MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or
// 전송 제어 처리 (재생, 일시정지 등)
MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS
)
}
// 서비스가 종료될 때 호출
override fun onDestroy() {
// MediaSession 리소스 해제
session.release()
}
/**
* onGetRoot: 클라이언트가 서비스에 연결할 때 호출
*
* @param clientPackageName 연결 요청한 앱의 패키지명
* @param clientUid 클라이언트 앱의 UID
* @param rootHints 클라이언트가 제공한 힌트
* @return BrowserRoot 객체 (연결 허용 시) 또는 null (거부 시)
*/
override fun onGetRoot(
clientPackageName: String,
clientUid: Int,
rootHints: Bundle?
): MediaBrowserServiceCompat.BrowserRoot? {
// "root"라는 ID로 루트 반환 (모든 클라이언트 허용)
// 실제 앱에서는 clientPackageName을 검사하여 보안 강화 필요
return MediaBrowserServiceCompat.BrowserRoot("root", null)
}
/**
* onLoadChildren: 미디어 항목 목록을 요청할 때 호출
*
* @param parentId 부모 미디어 항목의 ID
* @param result 결과를 전달할 객체
*/
override fun onLoadChildren(
parentId: String,
result: Result<MutableList<MediaItem>>
) {
// 빈 리스트 반환 (실제로는 음악 목록을 반환해야 함)
// TODO: 데이터베이스나 파일에서 음악 목록 가져오기
result.sendResult(ArrayList())
}
}┌──────────────────────────┐
│ Android Auto / 자동차 │
│ (클라이언트) │
└────────┬─────────────────┘
│ 연결 요청
↓
┌──────────────────────────┐
│ MyMusicService │
│ (서비스) │
├──────────────────────────┤
│ onGetRoot() ← 연결 허용? │
│ onLoadChildren() ← 목록 │
├──────────────────────────┤
│ MediaSession │
│ - callback │
│ - onPlay() │
│ - onPause() │
│ - onSkipToNext() │
└──────────────────────────┘
↓
실제 음악 재생
R 클래스는 Android가 자동으로 생성하는 클래스로, 모든 리소스에 접근할 수 있는 ID를 제공합니다.
// R 클래스 구조 (자동 생성됨, 직접 수정 불가)
class R {
class layout {
const val activity_main = 0x7f030001
}
class id {
const val main = 0x7f070001
const val textView = 0x7f070002
}
class string {
const val app_name = 0x7f0a0001
}
class drawable {
const val ic_launcher = 0x7f020001
}
}
// 사용 예
setContentView(R.layout.activity_main) // 레이아웃 파일
val view = findViewById(R.id.textView) // View ID
val appName = getString(R.string.app_name) // 문자열 리소스-
프로젝트 열기
- Android Studio 실행
Open an Existing Project선택TsetApplication폴더 선택
-
Gradle 동기화
- 프로젝트가 열리면 자동으로 Gradle 동기화 시작
- 또는
File → Sync Project with Gradle Files
-
에뮬레이터 또는 실제 기기 연결
- 에뮬레이터:
Tools → Device Manager → Create Device - 실제 기기: USB 연결 + 개발자 옵션 활성화
- 에뮬레이터:
-
앱 실행
- 상단 툴바에서 실행 버튼 (▶) 클릭
- 또는
Shift + F10(Windows/Linux),Ctrl + R(Mac)
# Unix/Mac
./gradlew build
# Windows
gradlew.bat build# 앱 빌드 (APK 생성)
./gradlew assembleDebug # 디버그 APK
./gradlew assembleRelease # 릴리스 APK
# 앱 설치 및 실행
./gradlew installDebug # 디버그 APK 설치
./gradlew installRelease # 릴리스 APK 설치
# 테스트 실행
./gradlew test # 단위 테스트
./gradlew connectedAndroidTest # 기기 테스트
# 빌드 정리
./gradlew clean # 빌드 결과물 삭제
# 의존성 확인
./gradlew dependencies # 의존성 트리 출력빌드가 완료되면 APK 파일이 다음 위치에 생성됩니다:
mobile/build/outputs/apk/debug/mobile-debug.apk
Android Studio 하단의 Logcat 탭에서 앱 로그를 실시간으로 확인할 수 있습니다.
// 코드에서 로그 출력
import android.util.Log
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 로그 출력
Log.d("MainActivity", "onCreate 호출됨") // Debug
Log.i("MainActivity", "정보 로그") // Info
Log.w("MainActivity", "경고 로그") // Warning
Log.e("MainActivity", "에러 로그") // Error
}
}Logcat 필터:
# 앱의 로그만 보기
package:com.example.tsetapplication
# 특정 태그만 보기
tag:MainActivity
# 에러만 보기
level:ERROR
- Kotlin 기본 문법 학습
- Activity와 Lifecycle 이해
- Layout과 View 학습
- RecyclerView로 리스트 구현
- ViewModel과 LiveData (MVVM 패턴)
- Room 데이터베이스
- Retrofit으로 네트워크 통신
- Coroutine으로 비동기 처리
File → Invalidate Caches / Restart → Invalidate and Restart
./gradlew clean
./gradlew buildlocal.properties 파일 확인:
sdk.dir=/Users/사용자명/Library/Android/sdk- Hardware Acceleration 활성화 (Intel HAXM, AMD Hypervisor)
- RAM 할당량 증가 (AVD Manager에서 설정)
- x86 이미지 사용 (ARM보다 빠름)
이 가이드가 Android 개발을 시작하는 데 도움이 되기를 바랍니다. 궁금한 점이 있으면 공식 문서를 참고하거나, 커뮤니티에 질문해보세요!
행복한 코딩 되세요! 🚀