1. Layout Inflation (레이아웃 인플레이션)
앱이 실행될 때 XML 레이아웃의 내용이 메모리에 객체화되고 객체화된 XML 레이아웃을 소스 파일에서 사용한다.
XML 레이아웃의 내용이 메모리에 객체화 되는 과정을 Inflation(인플레이션)이라고 한다.
* setContentView() : 레이아웃을 메모리에 객체화 하는 메소드
- 인자로 R.layout.activity_main 과 같이 받아서, 해당 레이아웃 XML 파일을 메모리에 객체화한다.
- 화면 전체(메인 화면)에 나타낼 뷰를 지정할 수 있다.
- 화면의 일부분을 차지하는 부분 레이아웃을 객체화할 수는 없다.
- 부분 레이아웃을 메모리에 객체화 하려면 인플레이터를 사용해야 한다.
2. LayoutInflater (부분 화면) 클래스 사용하기
안드로이드는 부분 화면을 객체화 하여 화면에 띄우기 위해 시스템 서비스로 LayoutInflater 라는 클래스를 제공한다.
getSystemService() 메서드를 사용하여 LayoutInflater 객체를 참조할 수 있다.
cf) 시스템 서비스는 단말이 시작되면서 항상 실행되는 서비스이다.
혹은 LayoutInflater.from() 를 이용하여 LayoutInflater 객체를 참조할 수도 있다.
cf) 프로젝트에서 액티비티 추가하기
[app 우클릭] -> [New] -> [Activity] -> [Empty Activity] |
Activity Name을 MenuActivity 라고 입력하고 Finish 클릭 |
MenuActivity로 실습을 할것이므로 Manifest 파일에 첫 실행 액티비티를 위처럼 수정한다. |
public class MenuActivity extends AppCompatActivity {
LinearLayout container;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_menu);
container = findViewById(R.id.container);
Button button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener(){
/**
* 버튼을 클릭했을 때 부분화면을 전체 레이아웃의 리니어 레이아웃에 추가한다.
* @param v
*/
@Override
public void onClick(View v){
LayoutInflater inflater = (LayoutInflater) getSystemService(
Context.LAYOUT_INFLATER_SERVICE);
// 부분 레이아웃(sub1.xml)을 메모리에 객체화
// container에 부분 레이아웃을 붙인다.
inflater.inflate(R.layout.sub1, container, true);
// 그 후 container에서 부분 레이아웃의 요소를 참조할 수 있게 된다.
CheckBox checkBox = container.findViewById(R.id.checkBox);
checkBox.setText("로딩되었어요.");
}
});
}
}
sub1.xml |
activity_menu.xml |
추가하기 버튼을 클릭하면 부분화면이 아래에 추가된다. |
3. 여러 화면 만들고 화면간 전환하기
프로젝트에 MenuActivity를 새로 추가하면 Manifest 파일에 자동으로 추가된다. label 속성은 화면의 제목, theme 는 대화상자 형태로 액티비티를 설정한다. |
activity_menu.xml 단순하게 버튼 한개만 추가한 형태이다. 이 화면은 대화상자 형태로 표시 될 것이다. |
activity_main.xml 메인 액티비티 역시 버튼 하나로 구성되었다. |
메인 액티비티에서 버튼을 클릭하면, 메뉴 액티비티를 띄울 것이고
메뉴 액티비티에는 돌아가기 버튼이 있어 클릭 시 메인 액티비티로 돌아가게 된다.
MainActivity를 작성하기 앞서 부모 클래스들에게서 상속받은 메서드들 중 하나인 onActivityResult()를 Override 하여 구현한다. 새로 띄운 액티비티로부터 응답을 처리 하기 위한 메소드 이다. |
* MainActivity.java
public class MainActivity extends AppCompatActivity {
// 새 액티비티를 띄울 때 보낼 코드
public static final int REQUEST_CODE_MENU = 101;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
/**
* 버튼을 클릭했을 때 새로운 액티비티(MenuActivity)를 띄운다.
* @param v
*/
@Override
public void onClick(View v) {
// 액티비티를 띄우기 위해 인텐트 객체를 사용한다.
// 또한 액티비티간에 데이터를 전달할 수 있다.
// 이벤트 처리 메서드 안에서 this 변수로 MainActivity 객체를 참조할 수 없으므로
// getApplicationContext() 를 사용하였다.
// Intent 생성자 인자로 MenuActivity.class를 넣음으로써 이와 연결된다.
Intent intent = new Intent(getApplicationContext(), MenuActivity.class);
// startActivityForResult 는 새 액티비티로부터 응답을 받을 수 있다.
startActivityForResult(intent, REQUEST_CODE_MENU);
}
});
}
/**
* 새로 띄었던 액티비티가 응답을 보내오면 그 응답을 처리한다.
* @param requestCode : 액티비티를 띄울 때 전달했던 요청 코드
* @param resultCode : 새 액티비티로부터 전달된 응답코드 (새 액티비티에서 처리한 결과가 정상인지 여부 판단)
* @param data : 새 액티비티로부터 받은 인텐트
*/
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if(requestCode == REQUEST_CODE_MENU) {
Toast.makeText(getApplicationContext(),
"onActivityResult 호출됨. 요청 코드 : " + requestCode +
", 결과 코드 : " + resultCode, Toast.LENGTH_LONG ).show();
if(resultCode == RESULT_OK){
//인텐트로부터 데이터 꺼내오기
String name = data.getStringExtra("name");
Toast.makeText(getApplicationContext(), "응답으로 전달된 name : " + name, Toast.LENGTH_LONG).show();
}
}
}
}
* 새로 띄울 액티비티(MenuActivity.java)
public class MenuActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_menu);
Button button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v){
// 인텐트 객체 생성
Intent intent = new Intent();
// name 값을 부가 데이터로 넣기
intent.putExtra("name", "mike~");
// 새로 띄운 액티비티에서 이전 액티비티로 인텐트를 전달하기
setResult(RESULT_OK, intent);
// 현재 액티비티 화면에서 없애기
finish();
}
});
}
}
버튼을 클릭하면 다이얼로그 방식의 MenuActivity가 보여진다. |
메인 액티비티로 돌아가게 되면 응답을 처리하는 메서드가 호출되며 요청, 결과 코드를 확인할 수 있다. Toast가 두개여서 나중것이 가려졌는데 Toast 한개를 주석처리하면 확인 할 수 있다. |
4. Intent (인텐트) 사용하기
인텐트는 앱 구성 요소 간에 작업 수행을 위한 정보를 전달하는 역할을 한다.
* 인텐트의 기본 구성 요소 : 액션(Action) , 데이터(Data)
액션은 수행할 기능이며 데이터는 수행될 대상의 데이터를 의미한다.
액션과 데이터를 이용해 인텐트 객체를 생성하고 필요한 액티비티를 띄어줄 수 있다.
* 인텐트 종류 : 명시적 인텐트, 암시적 인텐트
명시적 인텐트는 클래스 객체나 컴포넌트 이름을 지정하여 호출할 대상을 확실히 아는 경우를 말한다.
암시적 인텐트는 액션과 데이터를 지정하였으나 호출할 대상이 달라질 수 있는 경우를 말한다.
- 암시적 인텐트의 여러가지 속성
- 범주(Category) : 액션이 실행되는데 필요한 추가적 정보
- 타입(Type) : 인텐트에 들어가는 데이터의 MIME 타입을 명시적으로 지정
- 컴포넌트(Component) : 인텐트에 사용될 컴포넌트 클래스 명을 명시
- 부가 데이터(Extra Data) : 추가적인 정보가 들어있는 번들 데이터를 전달
아래 코드는 인텐트에 액션과 데이터를 넣어 다른 앱의 액티비티를 띄우는 코드이다.
단순히 입력상자와 버튼을 만들었다. 입력상자에는 전화번호 포맷 형식의 데이터를 입력한다. |
public class MainActivity extends AppCompatActivity {
EditText editText;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
editText = findViewById(R.id.editText);
Button button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//입력 상자에 입력된 전화번호 가져오기
String data = editText.getText().toString();
//전화 걸기 화면을 보여줄 인텐트 객체 생성
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(data));
//액티비티 띄우기 (액티비티 메모리에 올리기)
startActivity(intent);
}
});
}
}
전화걸기 버튼을 클릭했을 때, 자동으로 전화번호 앱이 실행되는 것을 확인할 수 있다. |
아래 두번째 코드는 컴포넌트 이름을 이용해 새로운 액티비티를 띄우는 경우이다.
위 화면 레이아웃에서 id가 button2인 버튼을 하나더 추가하고, 새로운 액티비티(MenuActivity)를 하나 생성한다.
Button button2 = findViewById(R.id.button2);
button2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent();
//컴포넌트 이름을 지정할 수 있는 객체 생성
//ComponentName 생성자의 인자로 순서대로 패키지명과, 클래스 명이 된다.
ComponentName name = new ComponentName("org.techtown.samplecallintent",
"org.techtown.samplecallintent.MenuActivity");
// 인텐트 객체에 컴포넌트를 지정한다.
intent.setComponent(name);
startActivityForResult(intent,101);
}
});
컴포넌트이름을 지정할 때 클래스 명은 앞에 패키지 경로까지 전부다 적어주어야 한다.
인텐트를 이용해 다른 화면을 띄울 수가 있는데 직접 만든 화면이나, 다른 사람이 만든 앱의 화면을 띄울 수도 있다.
5. 플래그와 부가 데이터 사용하기
startActivity 또는 startActivityForResult 메서드를 여러 번 호출 하게 될 경우 메모리에 여러 개가 중복되어 만들어질 것이다.
따라서 중복된 액티비티는 띄우지 않게 하기 위해 플래그를 사용한다.
액티비티가 만들어질 때마다 액티비티 스택이라는 곳에 차곡차곡 쌓이게 되는데, startActivity 또는 startActivityForResult를 호출하게 되면 이전의 액티비티는 액티비티 스택에 저장되고 새로 만들어진 액티비티가 화면에 보이게 된다.
즉 가장 상위에 쌓인 것부터 화면에 보여지면서 back 시스템 버튼을 눌렀을 때 그 아래의 액티비티가 보여지게 된다.
동일한 액티비티들을 여러 번 실행한다면 동일한 액티비티가 스택에 여러 개 들어가게 되는 문제를 해결해주는 것이 플래그이다.
* 대표적인 플래그들
- FLAG_ACTIVITY_SINGLE_TOP : 이미 생성된 액티비티가 있다면 그 액티비티를 그대로 사용하라는 플래그. 기존에 사용하는 액티비티에서 인텐트 객체만 전달받으려면 onNewIntent()를 오버라이드하면 된다.
- FLAG_ACTIVITY_NO_HISTORY : 처음 이후에 실행된 액티비티는 액티비티 스택에 쌓이지 않는다.
- FLAG_ACTIVITY_CLEAR_TOP : 이것이 설정되어 있는 액티비티 위에 있는 다른 액티비티를 모두 종료하게 된다. 홈 화면과 같이 항상 우선하는 액티비티를 만들때 유용함.
* 인텐트에 객체를 직렬화하여 데이터 보내기
객체를 직렬화 하기 위해서는 데이터를 담고 있는 클래스는 Parcelable 인터페이스를 구현해야한다.
자바의 Serializable 직렬화와 동일한 개념이다.
* Parcelable 직렬화 필수 구현
- CREATOR 상수 정의
- describeContents() 구현
- writeToParcel(Parcel dest, int flags) 구현
import android.os.Parcel;
import android.os.Parcelable;
/*
안드로이드에서의 직렬화는 Parcelable 인터페이스를 구현함으로써 사용할 수 있다.
*/
public class SimpleData implements Parcelable {
int number;
String message;
public SimpleData(int num, String msg){
number = num;
message = msg;
}
/**
* SimpleData의 생성자의 인자로 Parcel 객체를 받아들이는데
* Parcel 객체에서 데이터를 읽어들이는 역할을 한다.
* @param src
*/
public SimpleData(Parcel src){
number = src.readInt();
message = src.readString();
}
/**
* CREATOR 상수를 정의해야한다.
* Parcel 객체를 읽어들이고, 객체를 생성하는 역할
* 반드시 static final 로 선언해야 한다.
*/
public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
public SimpleData createFromParcel(Parcel in){
return new SimpleData(in);
}
public SimpleData[] newArray(int size){
return new SimpleData[size];
}
};
/**
* 필수 구현
* @return
*/
public int describeContents(){
return 0;
}
/**
* 필수 구현
* SimpleData 객체에 있는 데이터를 Parcel 객체로 만드는 역할을 한다.
* @param dest
* @param flags
*/
public void writeToParcel(Parcel dest, int flags){
dest.writeInt(number);
dest.writeString(message);
}
}
public class MainActivity extends AppCompatActivity {
public static final int REQUEST_CODE_MENU = 101;
public static final String KEY_SIMPLE_DATA = "data";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(getApplicationContext(), MenuActivity.class);
// 객체를 생성
SimpleData data = new SimpleData(100, "Hello Android!!");
// 인텐트에 객체를 보낸다.
intent.putExtra(KEY_SIMPLE_DATA, data);
startActivityForResult(intent, REQUEST_CODE_MENU);
}
});
}
}
public class MenuActivity extends AppCompatActivity {
TextView textView;
public static final String KEY_SIMPLE_DATA = "data";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_menu);
textView = findViewById(R.id.textView);
Button button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent();
intent.putExtra("name", "mike");
setResult(RESULT_OK, intent);
finish();
}
});
/**
* 메인 액티비티가 전달한 인텐트를 참조하기 위해 onCreate()안에서 getIntent()를 호출
*/
Intent intent = getIntent();
processIntent(intent);
}
public void processIntent(Intent intent){
if(intent != null){
//Bundle 객체를 반환한다.
Bundle bundle = intent.getExtras();
//Bundle 객체에서 키를 이용하여
//getParcelable메서드를 호출하면 SimpleData 객체을 얻어낼 수 있다.
SimpleData data = bundle.getParcelable(KEY_SIMPLE_DATA);
textView.setText("전달 받은 데이터\nNumber: "+data.number +
"\nMessage:"+data.message);
}
}
}
6. 태스크(Task) 관리 이해하기
태스크는 앱이 어떻게 동작할지 결정하는데 사용된다.
태스크를 이용하면 프로세스처럼 독립적인 실행 단위와 상관없이 어떤 화면들이 같이 동작해야 하는지 흐름을 관리할 수 있다.
프로세스끼리는 정보를 공유할 수가 없다.
따라서 하나의 프로세스에서 다른 프로세스의 화면을 띄우려면 시스템의 도움이 필요하다.
시스템에서 이런 액티비티의 각종 정보를 저장해두기 위해 태스크라는 것을 만든다.
시스템에서는 액티비티들의 정보를 저장해두기 위해 태스트라는 것을 만들고 알아서 관리하지만 직접 제어할 수도 있다.
AndroidManifest.xml 에 액티비티를 등록할 때 태스크도 함께 설정할 수 있다.
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//나 자신을 띄운다.
Intent intent = new Intent(getApplicationContext(), MainActivity.class);
startActivity(intent);
}
});
}
}
위 코드는 버튼을 클릭할 때마다 자기자신의 액티비티를 스택에 넣는다. 이것은 AndroidManifest.xml 파일에서 activity 태그의 속성에서 android:launchMode="standard" 을 넣어주는 것과 같다.
만약 android:launchMode="singleTop" 으로 설정한다면 태스크의 가장 위쪽에 있는 액티비티는 더 이상 새로이 만들지 않게된다.
이는 5번의 플래그의 FLAG_ACTIVITY_SINGLE_TOP 와 같은 효과이다.
따라서 중복되어 액티비티가 생성되지 않으므로 (onCreate메서드가 실행되지 않음)
인텐트는 onNewIntent()메서드로 전달받으면 된다.
singleTask : 액티비티가 실행되는 시점에 새로운 태스크를 만들게 된다.
singleInstance: 액티비티가 실행되는 시점에 새로운 태스크를 만들면서, 그 이후에 실행되는 액티비티들은 이 태스크를 공유하지 않는다.
7. 액티비티의 수명주기와 SharedPreferences 이해하기
onCreate -> onStart -> onResume -> onPause ->onStop -> onDestroy
앱이 갑자기 중지되어 앱 데이터의 저장과 복원이 필요할 때가 있다.
간단한 데이터를 저장하거나 복원시에는 SharedPreferences 를 사용할 수 있다.
아래 코드는 앱을 실행했을 때 입력상자에 사람 이름을 입력한 상태에서, 앱을 종료한 후 다시 실행했을 때 사람 이름이 그대로 보이도록 만드는 내용이다.
앱이 갑자기 중지 될때 항상 호출되는 메서드는 onPause이다.
앱이 시작될 때 항상 호출되는 메서드는 onResume이다.
즉 중지 시에 데이터를 저장하고 시작할 때 데이터를 복원하면 된다.
public class MainActivity extends AppCompatActivity {
EditText nameInput;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(getApplicationContext(), MenuActivity.class);
startActivity(intent);
}
});
Toast.makeText(this, "onCreate 호출됨.",Toast.LENGTH_LONG).show();
nameInput = findViewById(R.id.nameInput);
}
@Override
protected void onStart() {
super.onStart();
println("onStart 호출됨.");
}
@Override
protected void onStop() {
super.onStop();
println("onStop 호출됨.");
}
@Override
protected void onDestroy() {
super.onDestroy();
println("onDestroy 호출됨.");
}
public void println(String data){
Toast.makeText(this, data, Toast.LENGTH_LONG).show();
//Logcat에 로그 출력하기
Log.d("Main", data);
}
@Override
protected void onPause() {
super.onPause();
Toast.makeText(this, "onPause 호출됨", Toast.LENGTH_LONG).show();
//현재 입력상자에 입력된 데이터를 저장한다.
saveState();
}
@Override
protected void onResume() {
super.onResume();
Toast.makeText(this, "onPause 호출됨", Toast.LENGTH_LONG).show();
//설정 정보에 저장된 데이터를 복원한다.
restoreState();
}
protected void restoreState(){
SharedPreferences pref = getSharedPreferences("pref", Activity.MODE_PRIVATE);
if( (pref != null) && (pref.contains("name"))){
String name = pref.getString("name","");
nameInput.setText(name);
}
}
protected void saveState(){
SharedPreferences pref = getSharedPreferences("pref", Activity.MODE_PRIVATE);
SharedPreferences.Editor editor = pref.edit();
editor.putString("name", nameInput.getText().toString());
editor.commit(); //커밋을 해야 데이터가 저장이 된다.
}
protected void clearState(){
SharedPreferences pref = getSharedPreferences("pref", Activity.MODE_PRIVATE);
SharedPreferences.Editor editor = pref.edit();
editor.clear();
editor.commit();
}
}