본문 바로가기
■Development■/《Android》

[Android] Android 개발의 기본, Activity 완벽 이해하기 (초보자 가이드) 1편

by 은스타 2020. 4. 8.
반응형

안드로이드 개발의 기본, Activity 완벽 이해하기 (초보자 가이드)

 

안녕하세여. 안드로이드 개발을 시작하면 반드시 알아야 하는 핵심 개념 중 하나가 바로 'Activity'입니다. 사용자와 직접 상호작용하는 화면을 담당하는 Activity는 안드로이드 앱 개발의 근간이 되는 요소입니다. 이 글에서는 초보 개발자도 쉽게 이해할 수 있도록 Activity의 기본 개념부터 활용 방법까지 상세히 알아보겠습니다.


목차

 

#1. Activity란 무엇인가?

Activity는 안드로이드 앱에서 사용자와 상호작용하기 위한 단일 화면을 의미합니다. 앱은 보통 여러 개의 Activity로 구성되며, 각 Activity는 서로 다른 기능과 화면을 담당합니다. 예를 들어, 메시지 앱에서는 대화 목록을 보여주는 Activity, 대화 내용을 보여주는 Activity, 새 메시지를 작성하는 Activity 등이 있을 수 있습니다.

안드로이드 공식 문서에서는 Activity를 다음과 같이 정의합니다:

"Activity는 앱과 사용자가 상호작용할 수 있는 진입점이며, 앱이 UI를 그리는 창을 제공합니다."

 

Activity는 android.app.Activity 클래스를 상속받아 구현하며, 최근에는 주로 androidx.appcompat.app.AppCompatActivity를 사용하여 하위 버전 호환성을 유지합니다.

 

#2. Activity의 생명주기(Lifecycle)

Activity의 가장 중요한 특징 중 하나는 '생명주기(Lifecycle)'입니다. Activity는 생성되고, 사용자에게 보여지고, 다시 숨겨지며, 종료되기까지 여러 상태 변화를 겪게 됩니다. 이러한 상태 변화를 제대로 관리하는 것이 안드로이드 앱 개발의 핵심입니다.

Activity 생명주기 메서드는 다음과 같습니다:

onCreate()

Activity가 처음 생성될 때 호출됩니다. 여기서 UI를 초기화하고, 뷰를 생성하며, 필요한 데이터를 준비합니다.

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    // UI 초기화, 뷰 바인딩, 데이터 준비 등
    val button = findViewById<Button>(R.id.button_start)
    button.setOnClickListener {
        // 버튼 클릭 처리
    }
}

주요 작업:

  • 레이아웃 설정 (setContentView())
  • 뷰 찾기 및 초기화 (findViewById())
  • 데이터 바인딩 설정
  • 클릭 리스너 등록
  • ViewModel 초기화

onStart()

Activity가 사용자에게 보여지기 직전에 호출됩니다. 화면에 표시되기 위한 준비를 합니다.

override fun onStart() {
    super.onStart()
    // 화면에 표시되기 직전 준비 작업
}

주요 작업:

  • UI 업데이트 준비
  • 애니메이션 시작 준비
  • 센서 리스너 등록

onResume()

Activity가 사용자와 상호작용할 수 있는 상태가 되었을 때 호출됩니다. 이 상태에서 사용자 입력을 받을 수 있습니다.

override fun onResume() {
    super.onResume()
    // 사용자와 상호작용 준비 완료
    // 실시간 업데이트, 애니메이션 시작 등
}

주요 작업:

  • 실시간 데이터 업데이트 시작
  • 카메라, 위치 등 리소스 집약적 기능 시작
  • 애니메이션 재생 시작

onPause()

Activity가 부분적으로 보이지 않게 될 때 호출됩니다. 다른 Activity가 전면에 올라오거나, 대화상자가 표시될 때 발생합니다.

override fun onPause() {
    super.onPause()
    // 일시 중지 상태 처리
    // 리소스 집약적 작업 일시 중지
}

주요 작업:

  • 애니메이션, 비디오 일시 중지
  • 지속적인 위치 업데이트 중지
  • 중요한 데이터 저장
  • 커밋되지 않은 변경사항 저장

onStop()

Activity가 더 이상 사용자에게 보이지 않을 때 호출됩니다. 다른 Activity가 완전히 화면을 가리거나, 앱이 백그라ун드로 이동했을 때 발생합니다.

override fun onStop() {
    super.onStop()
    // 화면에서 완전히 가려짐
    // 무거운 리소스 해제
}

주요 작업:

  • 불필요한 리소스 해제
  • 데이터베이스 연결 해제
  • 광고 중지
  • 브로드캐스트 리시버 해제

onRestart()

Activity가 중지되었다가 다시 시작될 때 호출됩니다. onStop() 이후 다시 화면에 보이기 전에 호출됩니다.

override fun onRestart() {
    super.onRestart()
    // 중지 상태에서 다시 시작될 때 추가 초기화
}

주요 작업:

  • 일시 중지된 작업 재개 준비
  • 변경된 환경 설정 반영

onDestroy()

Activity가 완전히 종료되기 전에 호출됩니다. 시스템이 메모리 확보를 위해 Activity를 제거하거나, finish()가 호출되었을 때 발생합니다.

override fun onDestroy() {
    super.onDestroy()
    // 최종 정리 작업
    // 모든 리소스 해제
}

주요 작업:

  • 모든 리소스 해제
  • 스레드 종료
  • 데이터베이스 연결 종료
  • 메모리 누수 방지를 위한 정리

 

#3. Activity 생명주기 시각화

Activity의 생명주기를 그림으로 표현하면 다음과 같습니다:

주요 생명주기 이벤트 흐름:

  1. Activity 시작: onCreate()onStart()onResume()
  2. Activity 일시 중지: onPause()
  3. Activity 재개: onResume()
  4. Activity 중지: onPause()onStop()
  5. Activity 재시작: onRestart()onStart()onResume()
  6. Activity 종료: onPause()onStop()onDestroy()

 

#4. Activity 구현하기

기본 Activity 만들기

안드로이드 스튜디오에서는 새 Activity를 쉽게 생성할 수 있습니다. 수동으로 만들고 싶다면 다음과 같이 작성합니다:

// Kotlin으로 작성한 기본 Activity
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 여기서 초기화 작업 수행
    }
}
// Java로 작성한 기본 Activity
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 여기서 초기화 작업 수행
    }
}

레이아웃 연결하기

Activity에는 해당 화면의 UI를 정의하는 레이아웃 파일이 필요합니다. 레이아웃 파일은 XML로 작성되며, setContentView() 메서드를 통해 Activity와 연결됩니다.

<!-- res/layout/activity_main.xml -->
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    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="match_parent">

    <TextView
        android:id="@+id/text_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello, Activity!"
        android:textSize="24sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/button_start"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Start Next Activity"
        android:layout_marginTop="16dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/text_view" />

</androidx.constraintlayout.widget.ConstraintLayout>

매니페스트 등록하기

모든 Activity는 안드로이드 시스템에 등록되어야 합니다. 이 작업은 AndroidManifest.xml 파일에서 이루어집니다:

<!-- AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.myapp">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">

        <!-- 메인 Activity 선언 -->
        <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>

        <!-- 다른 Activity 선언 -->
        <activity
            android:name=".SecondActivity"
            android:exported="false" />

    </application>
</manifest>

위 매니페스트에서:

  • android:exported="true": 다른 앱에서 이 Activity를 시작할 수 있음을 의미
  • intent-filterMAIN 액션과 LAUNCHER 카테고리: 앱 런처에서 이 Activity를 시작 화면으로 표시

 

#5. Activity 간 전환하기

Intent로 새 Activity 시작하기

안드로이드에서는 Intent를 사용하여 Activity 간 전환을 수행합니다:

// Kotlin에서 새 Activity 시작하기
val button = findViewById<Button>(R.id.button_start)
button.setOnClickListener {
    val intent = Intent(this, SecondActivity::class.java)
    startActivity(intent)
}
// Java에서 새 Activity 시작하기
Button button = findViewById(R.id.button_start);
button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Intent intent = new Intent(MainActivity.this, SecondActivity.class);
        startActivity(intent);
    }
});

데이터 전달하기

Intent에 추가 데이터를 담아 다른 Activity로 전달할 수 있습니다:

// Kotlin으로 데이터 전달하기
val intent = Intent(this, SecondActivity::class.java).apply {
    putExtra("user_name", "John Doe")
    putExtra("user_age", 25)
    putExtra("is_premium", true)
}
startActivity(intent)

수신하는 Activity에서는 다음과 같이 데이터를 받습니다:

// 전달받은 데이터 읽기
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_second)

    val userName = intent.getStringExtra("user_name") ?: "Unknown"
    val userAge = intent.getIntExtra("user_age", 0)
    val isPremium = intent.getBooleanExtra("is_premium", false)

    // 데이터 사용하기
    val textView = findViewById<TextView>(R.id.text_view)
    textView.text = "Name: $userName, Age: $userAge, Premium: $isPremium"
}

결과 받아오기

다른 Activity를 시작하고 결과를 받아올 수도 있습니다. 최신 안드로이드에서는 ActivityResultLauncher를 사용합니다:

// 최신 방식 (ActivityResultLauncher 사용)
private val startForResult = registerForActivityResult(
    ActivityResultContracts.StartActivityForResult()
) { result ->
    if (result.resultCode == Activity.RESULT_OK) {
        val data = result.data
        val returnedText = data?.getStringExtra("result_text") ?: ""
        Toast.makeText(this, "Returned: $returnedText", Toast.LENGTH_SHORT).show()
    }
}

// 버튼 클릭 시 결과를 기대하며 Activity 시작
binding.buttonStartForResult.setOnClickListener {
    val intent = Intent(this, InputActivity::class.java)
    startForResult.launch(intent)
}

결과를 반환하는 Activity:

// 결과 전달하기
binding.buttonSubmit.setOnClickListener {
    val resultText = binding.editTextInput.text.toString()
    val resultIntent = Intent().apply {
        putExtra("result_text", resultText)
    }
    setResult(Activity.RESULT_OK, resultIntent)
    finish() // Activity 종료
}

 

#6. Activity의 다양한 실행 모드

Activity의 실행 방식을 제어하는 '실행 모드(Launch Mode)'에 대해 알아보겠습니다:

standard

기본 실행 모드로, Activity 시작 요청이 있을 때마다 새 인스턴스를 생성합니다.

<activity
    android:name=".StandardActivity"
    android:launchMode="standard" />

특징:

  • 동일한 Activity가 여러 번 중첩될 수 있음
  • 같은 Activity의 여러 인스턴스가 백 스택에 쌓임

singleTop

현재 화면의 최상단에 동일한 Activity가 있을 경우, 새 인스턴스를 생성하지 않고 기존 인스턴스의 onNewIntent() 메서드를 호출합니다.

<activity
    android:name=".SingleTopActivity"
    android:launchMode="singleTop" />

특징:

  • 중복 인스턴스 생성 방지
  • 알림에서 Activity를 시작할 때 유용
  • 액티비티가 백 스택 최상단에 있을 때만 재사용

singleTask

해당 Activity의 인스턴스가 이미 존재하면, 해당 인스턴스를 포함하는 태스크를 전면으로 가져오고 그 위에 있던 Activity들을 모두 제거합니다.

<activity
    android:name=".SingleTaskActivity"
    android:launchMode="singleTask" />

특징:

  • 하나의 태스크에 인스턴스 하나만 존재
  • 홈 화면 등 주요 진입점 Activity에 적합
  • 새 Intent는 onNewIntent()로 전달됨

singleInstance

독자적인 태스크에서 단 하나의 인스턴스만 존재하며, 다른 Activity와 함께 실행되지 않습니다.

<activity
    android:name=".SingleInstanceActivity"
    android:launchMode="singleInstance" />

특징:

  • 전용 태스크에서 단독으로 실행
  • 다른 앱과 Activity를 공유할 때 유용
  • 전화 앱 호출 화면 등에 사용

 

#7. Activity에서 권한 처리하기

안드로이드 6.0(API 23) 이상부터는 위험 권한을 실행 시간에 요청해야 합니다:

// 권한 확인 및 요청
private fun checkAndRequestPermission() {
    if (ContextCompat.checkSelfPermission(
            this,
            Manifest.permission.CAMERA
        ) != PackageManager.PERMISSION_GRANTED
    ) {
        // 권한이 없는 경우 요청
        ActivityCompat.requestPermissions(
            this,
            arrayOf(Manifest.permission.CAMERA),
            CAMERA_PERMISSION_REQUEST_CODE
        )
    } else {
        // 이미 권한이 있는 경우 카메라 시작
        startCamera()
    }
}

// 권한 요청 결과 처리
override fun onRequestPermissionsResult(
    requestCode: Int,
    permissions: Array<out String>,
    grantResults: IntArray
) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    when (requestCode) {
        CAMERA_PERMISSION_REQUEST_CODE -> {
            if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                // 권한 획득 성공
                startCamera()
            } else {
                // 권한 거부됨
                Toast.makeText(this, "카메라 권한이 필요합니다", Toast.LENGTH_SHORT).show()
            }
        }
    }
}

companion object {
    private const val CAMERA_PERMISSION_REQUEST_CODE = 100
}

 

#8. Fragment와 Activity의 관계

Fragment는 Activity 내에서 동작하는 UI 모듈로, 재사용성과 화면 분할을 지원합니다:

// Activity에 Fragment 추가하기
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    if (savedInstanceState == null) {
        supportFragmentManager.beginTransaction()
            .add(R.id.fragment_container, HomeFragment())
            .commit()
    }
}

Activity와 Fragment 간 통신:

// Fragment에서 Activity 참조하기
override fun onAttach(context: Context) {
    super.onAttach(context)
    if (context is OnFragmentInteractionListener) {
        listener = context
    } else {
        throw RuntimeException("$context must implement OnFragmentInteractionListener")
    }
}

// 인터페이스를 통한 통신
interface OnFragmentInteractionListener {
    fun onFragmentInteraction(data: String)
}

 

#9. Activity 상태 저장 및 복원

화면 회전 등으로 Activity가 재생성될 때 상태를 유지하기 위한 방법:

// 상태 저장하기
override fun onSaveInstanceState(outState: Bundle) {
    super.onSaveInstanceState(outState)

    // 데이터 저장
    outState.putString("user_input", binding.editTextInput.text.toString())
    outState.putInt("counter", counter)
}

// 상태 복원하기
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    // 저장된 데이터 복원
    if (savedInstanceState != null) {
        val userInput = savedInstanceState.getString("user_input", "")
        counter = savedInstanceState.getInt("counter", 0)

        binding.editTextInput.setText(userInput)
        binding.textViewCounter.text = "Counter: $counter"
    }
}

 

#10. 최신 안드로이드에서의 Activity

ViewModel과 Activity

ViewModel을 사용하여 UI 데이터를 관리하고 화면 회전 시에도 데이터를 유지할 수 있습니다:

// ViewModel 정의
class MainViewModel : ViewModel() {
    private val _counter = MutableLiveData<Int>(0)
    val counter: LiveData<Int> = _counter

    fun incrementCounter() {
        _counter.value = (_counter.value ?: 0) + 1
    }
}

// Activity에서 ViewModel 사용
class MainActivity : AppCompatActivity() {
    private lateinit var viewModel: MainViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // ViewModel 초기화
        viewModel = ViewModelProvider(this).get(MainViewModel::class.java)

        // LiveData 관찰
        viewModel.counter.observe(this, { count ->
            binding.textViewCounter.text = "Counter: $count"
        })

        // 버튼 클릭 시 카운터 증가
        binding.buttonIncrement.setOnClickListener {
            viewModel.incrementCounter()
        }
    }
}

LiveData와 Activity

LiveData를 사용하여 데이터 변경을 관찰하고 UI를 자동으로 업데이트할 수 있습니다:

// LiveData 예제
class WeatherViewModel : ViewModel() {
    private val _temperature = MutableLiveData<Float>()
    val temperature: LiveData<Float> = _temperature

    fun fetchWeatherData() {
        // 날씨 데이터를 가져와서 LiveData 업데이트
        viewModelScope.launch {
            try {
                val weatherData = weatherRepository.getWeatherData()
                _temperature.value = weatherData.temperature
            } catch (e: Exception) {
                // 오류 처리
            }
        }
    }
}

// Activity에서 LiveData 관찰
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_weather)

    val weatherViewModel = ViewModelProvider(this).get(WeatherViewModel::class.java)

    weatherViewModel.temperature.observe(this, { temperature ->
        binding.textViewTemperature.text = "현재 온도: ${temperature}°C"
    })

    // 데이터 로드
    weatherViewModel.fetchWeatherData()
}

Navigation Component와 Activity

Navigation Component를 사용하여 Activity와 Fragment 간 탐색을 관리할 수 있습니다:

// MainActivity에서 NavHost 설정
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    val navHostFragment = supportFragmentManager
        .findFragmentById(R.id.nav_host_fragment) as NavHostFragment
    val navController = navHostFragment.navController

    // 하단 네비게이션과 연결
    val bottomNav = findViewById<BottomNavigationView>(R.id.bottom_nav)
    bottomNav.setupWithNavController(navController)
}

 

긴 글 읽어 주셔서 감사힙니다.

2편에서 이어서 연재하도록 하겠습니다.

끝. 

반응형