70일차(1)/Android App(13) : Custom Adapter 생성, Serializable 인터페이스 구현
- Custom Adapter 만들어서 view연결하기
- Class에 Serializable 인터페이스 구현하기
- 새 모듈 생성
MainActivity
package com.example.step06customadapter;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import androidx.appcompat.app.AppCompatActivity;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity implements AdapterView.OnItemClickListener {
List<CountryDto> countries;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//아답타에 연결할 모델 객체 생성
countries=new ArrayList<>();
//셈플데이터
countries.add(new CountryDto(R.drawable.austria,
"오스트리아", "어쩌구.. 저쩌구.."));
countries.add(new CountryDto(R.drawable.belgium,
"벨기에", "어쩌구.. 저쩌구.."));
countries.add(new CountryDto(R.drawable.brazil,
"브라질", "어쩌구.. 저쩌구.."));
countries.add(new CountryDto(R.drawable.france,
"프랑스", "어쩌구.. 저쩌구.."));
countries.add(new CountryDto(R.drawable.germany,
"독일", "어쩌구.. 저쩌구.."));
countries.add(new CountryDto(R.drawable.greece,
"그리스", "어쩌구.. 저쩌구.."));
countries.add(new CountryDto(R.drawable.israel,
"이스라엘", "어쩌구.. 저쩌구.."));
countries.add(new CountryDto(R.drawable.italy,
"이탈리아", "어쩌구.. 저쩌구.."));
countries.add(new CountryDto(R.drawable.japan,
"일본", "그지 같은 나라~"));
countries.add(new CountryDto(R.drawable.korea,
"대한민국", "어쩌구.. 저쩌구.."));
countries.add(new CountryDto(R.drawable.poland,
"폴란드", "어쩌구.. 저쩌구.."));
countries.add(new CountryDto(R.drawable.spain,
"스페인", "어쩌구.. 저쩌구.."));
countries.add(new CountryDto(R.drawable.usa,
"미국", "어쩌구.. 저쩌구.."));
//ListView에 연결할 아답타
CountryAdapter adapter=new CountryAdapter(this, R.layout.listview_cell, countries);
//listView의 참조값 얻어오기
ListView listView=findViewById(R.id.listView);
//아답타를 ListView에 연결하기
listView.setAdapter(adapter);
//ListView의 셀을 클릭했을 때 동작할 리스너 등록
listView.setOnItemClickListener(this);
}
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
//DetailActivity로 이동
Intent intent=new Intent(this, DetailActivity.class);
/*
Intent 객체에 어떤 정보를 담아서 다른 액티비티에 전달할 수 있다.
여기서는 어떤 객체를 담아야 할까?
클릭한 cell의 index에 해당하는 CountryDto 객체를 담아야 한다.
*/
CountryDto dto=countries.get(i);
/*
intent에 담을 데이터를 Serializable type으로 만들어 놓고 담는다.
.putExtra( key, Serializable type value )
*/
intent.putExtra("dto", dto);
startActivity(intent);
}
}
- ListView의 셀에 국가 이미지를 각각의 국가명과 함께 넣고,
클릭하면 해당 국가의 설명이 있는 페이지로 이동하는 app을 만들기!
- 이 경우 아답타를 커스텀으로 만들어야 한다.
- 문자열을 출력할 때는 이미 만들어진 arrayAdapter를 import해서 사용하면 되는데
이미지를 출력할 준비가 된 아답타는 없어서 우리가 직접 만들어야 한다.
- 패키지 우클릭- java 클래스 - CountryDto 클래스생성
CountryDto
package com.example.step06customadapter;
import java.io.Serializable;
public class CountryDto implements Serializable {
//필드
private int resId; //출력할 이미지 리소스 아이디 R.id.austria 등등의 값
private String name; //나라의 이름
private String content; //나라에 대한 자세한 설명
//디폴트 생성자
public CountryDto(){}
public CountryDto(int resId, String name, String content) {
this.resId = resId;
this.name = name;
this.content = content;
}
public int getResId() {
return resId;
}
public void setResId(int resId) {
this.resId = resId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
- 화면에서 우클릭하거나 / alt+insert 를 누르면 Generate가 있다.
- 필드를 모두 선택해서 생성자 만들기
- 똑같이 setter / getter 만들어주기
레이아웃 폴더 우클릭- Layout Resource file
listview_cell 생성
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="120dp">
<ImageView
android:id="@+id/imageView"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_marginStart="50dp"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/austria" />
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="50dp"
android:text="TextView"
android:textSize="30sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/imageView"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
- constraint layout의 크기 조절하기. 가로는 폭 전체 사용, 세로는 100dp 로 고정한 것이다.
- constraint layout은 UI를 배치할때 제약조건을 추가함으로서 레이아웃을 결정한다.
- 셀에 imageView 요소를 추가하고 위아래로 여백 넣어서 고정해주기!
- textview 도 간격을 주고 위아래 0 으로 조정하면 자동으로 가운데 정렬된다. 해당 요소에 걸리는 제약조건을 추가한 것.
- 글자크기는 30sp로! (다른 UI들은 대부분 dp 단위를 사용. 텍스트만 sp 단위 사용)
- 이렇게 셀 역할을 하는 UI를 하나 만들어보았다.
- 이렇게 국가별로 셀을 만들고, 각각의 셀을 클릭하면 국가 activity로 이동하도록 할 예정!
- Button, ImageView, xxxLayout 등등..
모든 UI는 부모중에 항상 View 클래스가 있다.
- 우리가 만든 constraintLayout은 이 객체가 만약 생성되면(new) View가 된다.
- 만들어진 constraint Layout 을 ListView가 사용하게 할 것이다.
- 이 셀 하나하나는 모두 View이다! 이 안에 imageView, textView를 하나씩 넣을 것이다.
- constraint Layout 로 셀을 하나하나 만들면 각각의 셀을 구성할 수 있는 view를 만들 수 있다.
- 이 셀 안에 view를 공급하는 주체는 Adapter이다.
- ListView 입장에서 Adapter는 셀을 공급하는 공급자가 된다.
- 모델은 Adapter에 데이터를 공급한다. List<CountryDto>를 adapter에 연결한다!
- 아답타에 뷰를 만들어내는 레이아웃은 listview_cell.xml이다. 이 레이아웃 안에는 Constraint Layout이 있다.
이 Layout 안에는 imageView, textView가 포함되어 있는 것이다.
- ListView는 저 아답타와 리스트를 가지고 셀을 만들어서 공급한다.
- Adapter를 우리가 custom으로 만든다. 하지만 이 아답타는 특별한 일을 하기 때문에 맘대로 만들 수는 없다.
- 인터페이스를 구현하거나, 클래스를 상속해서 어떤 특정한 형식으로 만들어야 한다.
이제 만든 셀을 사용해서 View를 공급하는 adapter를 만들것이다.
CountryAdapter java 클래스 생성
package com.example.step06customadapter;
import android.content.Context;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.List;
/*
ListView에 연결할 아답타 클래스 정의하기
- BaseAdapter 추상 클래스를 상속받아서 만든다.
*/
public class CountryAdapter extends BaseAdapter {
//필드
Context context;
int layoutRes;
List<CountryDto> list;
//생성자
public CountryAdapter(Context context, int layoutRes, List<CountryDto> list){
//생성자의 인자로 전달된 값을 필드에 저장한다.
this.context=context;
this.layoutRes=layoutRes;
this.list=list;
}
/*
아래 4개의 메소드는 ListView가 필요시 호출하는 메소드이다.
따라서 적절한 값을 리턴하도록 우리가 프로그래밍해야한다.
*/
@Override
public int getCount() {
//모델의 개수
return list.size();
}
@Override
public Object getItem(int i) {
// i번째 인덱스에 해당하는 모델을 리턴해야 한다.
return list.get(i);
}
@Override
public long getItemId(int i) {
// i번째 인덱스에 해당하는 모델의 id(primary key 값) 리턴하기(없다면 그냥 인덱스를 아이디 값으로 사용)
return i;
}
//인자로 전달된 position에 해당하는 cell view를 만들어서 리턴하거나
//이미 만들어진 cell view의 내용만 만들어서 리턴해준다.
@Override
public View getView(int i, View view, ViewGroup viewGroup) {
Log.e("CountryAdapter", "getView() 호출됨 i:"+i);
//1. res/layout/listview_cell.xml 문서를 전개해서 View 객체를 만든다.
if(view == null){
Log.e("CountryAdapter", "view가 null이어서 cell view 를 새로 만듭니다.");
//레이아웃 전개자(레이아웃 xml 문서를 이용해서 View를 만드는 객체) 객체의 참조값 얻어오기
LayoutInflater inflater=LayoutInflater.from(context);
//listView_cell.xml 문서를 전개해서 새 xml문서를 만든다.
view=inflater.inflate(layoutRes, viewGroup, false);
}
//2. i 에 해당하는 CountryDto 객체의 참조값을 얻어온다.
CountryDto dto=list.get(i);
//3. 만든 view 객체 안에 있는 imageView, textView의 참조값을 얻어온다.
ImageView imageView=view.findViewById(R.id.imageView);
TextView textView=view.findViewById(R.id.textView);
//4. imageView, textView 에 정보를 출력한다.
imageView.setImageResource(dto.getResId());
textView.setText(dto.getName());
// i번째 인덱스에 해당하는 View를 리턴해준다.
return view;
}
}
- BaseAdapter 라는 추상클래스를 상속받는다.
- 클래스 뒤에서 alt+enter 하면 해야하는 행동이 나온다.(메소드 받기)
- 4개의 메소드 전체를 override 해주고, 기본 생성자도 하나 만들어준다.
- 이전에 adapter를 사용했던 사례를 보면, 인자 3개를 받는다.
- 1번째 인자: context type
- 2번째 인자: layout Resource
- 3번째 인자: model(data)를 연결
- 이 ArrayAdapter도 이미 만들어져 있는 것을 사용했지만, 따지고 보면 BaseAdapter를 상속받아서 만들어진 것이다.
- 인자를 똑같이 넣어준다. 이 자리에는 listview_cell 이 들어간다.(정수값으로 관리되고 있는 레이아웃!)
- 이것도 필드에서 만든 것이다.
- 필드를 만들고, 생성자에서 들어온 값을 필드에 저장한다.
- 아래 4개의 메소드는 ListView가 필요시 호출하는 메소드이다.
- 따라서 적절한 값을 리턴하도록 우리가 프로그래밍해야 한다.
(현재 리턴값은 null, 0으로 들어가 있다)
1) getCount : 모델의 갯수 리턴 (셀에 출력할 데이터의 개수)
- LIst<CountryDto> 의 개수이므로 list.size();
2) getItem : i번째 인덱스에 해당하는 모델을 리턴해야 한다.
- list.get(i) . 해당 메소드는 object를 리턴하게 되어 있으므로 countryDto가 들어갈 수 있다.
3) getItemId : i번째 인덱스에 해당하는 모델의 id(primary key 값) 리턴하기(없다면 그냥 인덱스를 아이디 값으로 사용)
- 지금은 id가 존재하지 않는다.
- 있다면 위와 같이 List.get(i).getId() 형태로 가져왔을 것이지만,
지금은 DB에서 가져오는 값이 아니므로 그냥 인덱스( i ) 만 가져온다.
4) getView : i번째 인덱스에 해당하는 View를 리턴해준다.
listview_cell.xml 을 가져와서 view를 만들어서 그 view에 data를 출력해서 이곳에 넣어 리턴해주어야 한다.
- 그런데 listView에 셀이 여러개이면 전부 만들어야 할까?
- View를 위아래로 스크롤하려면 View를 많이 만들어야 한다. 100개정도라고 가정한다면?
- 하지만 이걸 100개씩 만드는 대신, 최소한의 갯수만 만들고
만들어진 view를 재활용해서 데이터만 다르게 출력하면 되지않을까?
- getView(int, view, ViewGroup) 에서 인자 view는 전달될 수도 있고 아닐 수도 있다.
- view가 만들어져 있다면 전달될 것이고, 아니면 직접 만들어야 한다.
→ 이 view 값이 null일 경우와 아닌 경우를 분기해야 한다.
- 레이아웃 전개자가 필요하므로 LayoutInflater 객체를 얻어낸다.
- LayoutInflater : 레이아웃 xml 문서를 사용해서 View를 만드는 객체이다.
- inflate() 메소드 4개 중 이 메소드를 활용해서 전개할 것이다.
- listview_cell 의 참조값인 정수값을 전달해주면 이 전개자 객체가 알아서 view를 만들어서 리턴해준다.
- view는 null일 수도 아닐 수도 있다. null이면 새로 만들고 아니면 이전 것을 재활용한다!
- view 안에 imageView와 textView가 들어있다.
- 참조값은 findViewById() 로 찾아온다.
- 이것은 액티비티에서 사용하던 메소드와는 좀 다르다. View 자체가 가지고있는 메소드를 사용하는 것이다.
- 여기서 참조값을 알아내는 이유는 dto의 내용을 view에 전달하기위한 것!
- 각각의 imageView, textView에서 dto에 들어있는 값을 받아서 view를 최종적으로 리턴하면 된다.
- 이제 액티비티의 레이아웃을 구성할 예정!
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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<ListView
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="1dp"
android:layout_marginTop="1dp"
android:layout_marginEnd="1dp"
android:layout_marginBottom="1dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:id="@+id/listView"/>
</androidx.constraintlayout.widget.ConstraintLayout>
- 레거시-listView 에서 추가해주고 위치 대략 조정하기
- MainActivity 에도 샘플데이터(국가별 정보)를 넣어준다.
- 리스트뷰에 연결한 아답타 추가
- setAdapter() : 우리가 만든 아답타를 가져와주는 메소드
- BaseAdapter 를 상속받았기 때문에 이곳에 넣어서 사용할 수가 있다.
- run 해보면 국가 이미지가 이렇게 출력된다.
- 폰의 크기에 따라 다르지만 대략 10~11개정도의 view 가 필요하다.
- 그 다음(아래)부터는 cellview를 새로 만들 필요가 없다.
- 만들어진 뷰에 다른 내용만 출력하면 된다.
- 이 내용이 countryAdapter에 들어있다.
- null일 때만 새로 만들고, null이 아니면 위에 있는 cell을 재활용한다.
- if문을 건너뛸 수도 있는데, 건너뛴다는 의미는 이미 만들어진 view에 참조값이 전달된다는 뜻이다.
- 빨강: null인 경우, 새로 view를 만들어서 리턴
- 파랑: null이 아닌 경우, 이미 만들어진 view에 내용만 바꾸어서 내용을 출력함
- view를 최소한의 갯수만 만들어서 재활용하면 된다.
- 해당 cell이 생성되는지 재활용되는지 확인하기 위해 Log 로 출력해보면 이렇다.
- 추가로 로딩하면 아래쪽 view들도 나온다. 이후에 위아래로 왔다갔다 하면 새로 만들지는 않고 재활용만 한다!
- 리스트에 액티비티를 연결해서 목록을 클릭하면 상세내용을 보여줄 수 있도록 하기
new-Activity-Empty Activity
DetailActivity
package com.example.step06customadapter;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
public class DetailActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_detail);
// Detail 액티비티에 전달된 intent 객체의 참조값 얻어오기(MainActivity에서 생성한)
Intent intent=getIntent();
// intent 객체에 dto라는 키값으로 담긴 데이터를 얻어와서 원래 type으로 casting 한다.
CountryDto dto=(CountryDto)intent.getSerializableExtra("dto");
// activity_detail.xml을 전개했을 때 생성되는 UI의 참조값 얻어오기
ImageView imageView=findViewById(R.id.imageView);
TextView textView=findViewById(R.id.textView);
Button confirmBtn=findViewById(R.id.confirmBtn);
//imageView, TextView에 필요한 정보 출력하기
imageView.setImageResource(dto.getResId());
textView.setText(dto.getContent());
//버튼에 리스너를 익명 클래스를 이용해서 등록하기
confirmBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//액티비티 종료하기
//DetailActivity.this.finish();
finish();
}
});
}
}
activity_detail.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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".DetailActivity">
<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="50dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/austria" />
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="50dp"
android:text="TextView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imageView" />
<Button
android:id="@+id/confirmBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="100dp"
android:text="확인"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
- imageView를 하나 추가해주고, 좌우 가운데정렬, 위에서 50 떨어지도록 배치한 것
- infer 를 눌러서 배치하고 일부 수정하는것도 편하다.
- 만들어진 제약조건이 필요 없을 경우, x 아이콘을 누르면 삭제된다.
- 현재 레이아웃으로는 가로로 눕혔을때는 이렇게 나오는데, 이따가 조정해줄 것!
- 메인액티비티에 내용 추가! 클래스에 OnItemClickListener 를 구현한다.
listView.setOnItemClickListener(this);
- implement 하면 this 로 리스너 등록 가능
- 아래 override한 메소드에서 intent 객체를 생성해서 연결한다.
- 이제 클릭하면 액티비티(화면) 이동이 일어난다. 하지만 현재는 전체 모두 오스트리아 페이지로만 이동한다.
- A Activity → B Activity 로 이동할 때 어떤 정보를 전달해야 하는 경우가 있다.
- 지금도 해당 국가를 클릭시 그 국가에 대한 내용(CountryDto)를 전달해야 한다.
- Intent 객체에 어떤 정보를 담아서 다른 액티비티에 전달할 수 있다.
- 여기서는 어떤 객체를 담아야 할까?
- 클릭한 cell의 index 에 해당하는 CountryDto 객체를 담아야 한다.
- intent 객체에 점찍어보면 put 으로 시작하는 메소드들이 있다.
- 클릭한 셀의 인덱스 번호가 dto에 전달되면 그 i (인덱스번호) 에 해당하는 값을 가져와준다.
- dto라는 키값으로 연결하고 싶은데 이렇게 할 수 없다. CountryDto 타입을 받아주는 putExtra 메소드는 없기 때문에...
→ 그러면 CountryDto를 이 중 하나의 데이터 타입으로 바꾸면 된다.
- Serializable 은 인터페이스이다. 구현할 추상 메소드가 존재하지 않는 빈 인터페이스이다.
- 타입을 바꿀 수 있다면 이 메소드에 담아서 사용할 수 있다.
- 이렇게 Dto 에 가서 바로 implement Serializable 로 구현해주면 된다.
- Serializable은 이처럼 비어 있는 인터페이스이다. 객체를 직렬화한다고도 한다.
.putExtra( key, Serializable type value )
- 만든 클래스를 intent에 담고 싶으면 이렇게 serializable 을 사용하면 된다.
- detailActivity 수정
- intent가 액티비티 사이의 매개체 역할을 하는 것이다.
- intent에 putExtra() 로 Dto를 담아두었으니, getIntent를 사용해서 가져오면 된다.
CountryDto dto=(CountryDto)intent.getSerializableExtra("dto");
- get으로 가져올 수 있다.
- 이 가져온 값을 CountryDto에 다시 담아주면서 casting하면 된다.
- 이제 각각의 UI의 참조값을 얻어오고, 버튼에 리스너를 등록하면 된다.
- 이제 list에서 특정 국가를 클릭하면 특정 국가의 정보 화면(activity) 이 나온다.
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<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/Theme.MyAndroid">
<activity
android:name=".DetailActivity"
android:exported="false"
android:screenOrientation="portrait">
<meta-data
android:name="android.app.lib_name"
android:value="" />
</activity>
<activity
android:name=".MainActivity"
android:exported="true"
android:screenOrientation="fullSensor">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.lib_name"
android:value="" />
</activity>
</application>
</manifest>
android:screenOrientation="portrait"
- 이 속성값이 추가되면, 디바이스를 세로 방향으로밖에 쓰지 못한다!
- "portrait" 로 속성값을 작성해주면 세로형으로 고정된다.
- 위와 같이 작성하면 메인은 가로/세로 모두로 사용할 수 있고,
Detail화면은 세로로밖에 사용할 수 없다.
- 이처럼 화면을 가로로 돌려도 세로로만 고정되어 사용된다.