2023.03.09 - 안드로이드 앱개발자 과정

CHA·2023년 3월 10일
0

Android



ButterKnife 라이브러리

2020년까지 안드로이드의 거의 모든 앱에는 ButterKnife 라이브러리가 사용되었습니다. 원래 우리가 xml 의 뷰를 사용하기 위해서는 findViewById() 를 이용하여 뷰를 찾고, 뷰를 제어해왔습니다만, 코드가 길어지고 성능이 안좋다는 평을 받아왔습니다. 그래서 등장한 라이브러리가 ButterKnife 라이브러리 입니다. 지금은 ViewBinding 기술이 나와서 Deprecated 되었지만, 어떻게 사용하는 라이브러리인지 간단하게만 보고 넘어갑시다.

일단 Dependency 부터 추가해줍시다.

implementation 'com.jakewharton:butterknife:10.2.3'
annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.3'

그리고 자바 코드를 봅시다. 어노테이션을 이용하여 뷰와 연결하고 사용할 수 있습니다.

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv) 
    TextView tv;

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

        tv.setText("aaaa");
    }

    @OnClick(R.id.btn)
    void clickBtn(){
        tv.setText("버튼을 누르면 글씨가 바뀌어요");
    }
}

버튼을 하나 달고, 버튼을 누르면 텍스트뷰의 글씨가 변환되는 테스트입니다. 보기만 해도 상당히 코드가 간결해진걸 느낄 수 있습니다. 다만, 버터나이프의 경우 편의성의 측면에서는 월등했지만, 성능적인 이슈는 해결하지 못했습니다. 그래서 안드로이드 에서는 View BindingData Binding 기술을 등장시켰습니다.


View Binding

기존의 findViewById 를 사용하여 xml 뷰를 참조하면 코드가 굉장히 지저분해집니다. 한 페이지안에서 뷰가 몇개 없을때는 모르겠지만, 예를 들어 뷰 개수가 100개라면 100줄의 코드를 작성해주어야 합니다. 그래서 안드로이드에서는 findViewById 를 대체할 수 있는 기술인 View Binding 을 만들어주었습니다. 코드의 가독성은 물론이고, 성능적인 측면에서도 월등한 모습을 보여줍니다.

뷰 바인딩 기술은 MainActivity 에서 setContentView 를 이용하여 activity_main.xml의 뷰를 참조하는것 대신, 하나의 객체를 만듭니다. 그 객체의 이름은 ActivityMainBinding 이며, 이 객체에게 xml 뷰의 모든 뷰들의 참조변수를 담아둡니다. 그리고 MainActivity 에서는 이 객체의 참조변수를 만들어두고 참조변수를 이용하여 xml 뷰들을 제어할 수 있습니다.

또한 ViewBinding 은 안드로이드의 아키텍처 구성요소 중 하나이기 때문에 build.gradle 에 기능 사용여부를 설정해주어야 합니다.

buildFeatures {
    viewBinding true
}

그러면 View Binding 을 어떻게 사용하는지 Activity, Fragment, RecyclerView 를 기준으로 알아봅시다.


Activity 에서의 View Binding

먼저 액티비티에서 뷰 바인딩을 사용해봅시다.

public class MainActivity extends AppCompatActivity {
    ActivityMainBinding binding;

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

        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        binding.tv.setText("Nice to meet you");

        binding.btn.setOnClickListener(view -> {
            binding.tv.setText("what?");
            Intent intent = new Intent(this, SecondActivity.class);
            startActivity(intent);
        });

        binding.btn.setOnLongClickListener(view -> {
            Toast.makeText(this, "long~ click", Toast.LENGTH_SHORT).show();
            return true;
        });

        binding.btn2.setOnClickListener(view -> {
            String result = binding.et.getText().toString();
            binding.tvResult.setText(result);
        });
    }
}
  • binding = ActivityMainBinding.inflate(getLayoutInflater());
    getLayoutInflater() 메소드를 사용하여 LayoutInflater 를 가져와 인플레이트를 수행합니다. 즉, ActivityBinding 객체에 activity_main.xml 에 있는 뷰 객체들을 넣어주는 코드 입니다.

  • setContentView(binding.getRoot());
    가져온 뷰 객체들 중, 최상위 레이아웃을 가져와 setContentView() 메서드의 파라미터로 전달합니다. 이 과정을 통해 메인 화면에 뷰들을 표시할 준비를 마칩니다.

나머지 과정들은 바인딩된 뷰들을 제어하는 코드들 입니다. 그리 어렵지 않으니 읽어보고 넘어갑시다.


Fragment 에서의 View Binding

테스트 목적으로 Fragment 는 정적으로 MainActivity 에 붙여주었습니다. 또한 뷰바인딩의 기술적인 부분을 보려는 목적이기에 xml 파일은 따로 보지 않겠습니다.

public class MyFragment extends Fragment {

    FragmentMyBinding binding;

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        binding = FragmentMyBinding.inflate(inflater,container,false);
        return binding.getRoot();
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        binding.btn.setOnClickListener(v->binding.tv.setText("Good Day"));
    }
}

프래그먼트 역시 액티비티에서의 뷰바인딩 과정과 비슷하게 이뤄집니다. onCreateView() 에서는 fragment_my.xml 의 뷰들을 인플레이션 과정을 거쳐 FragmentBinding 객체에게 인플레이트 합니다. 그리고 리턴값으로 최상위 뷰 객체를 리턴합니다. 여기까지가 프래그먼트에서 뷰바인딩을 활용하여 뷰를 생성한 과정입니다.

onViewCreated() 에서는 바인딩된 뷰들을 제어하는 코드를 작성합니다.


RecyclerView 에서의 View Binding

리사이클러뷰에서 역시 뷰 바인딩을 활용할 수 있습니다. 리사이클러뷰에 데이터를 뿌려줄 때 사용하는 뷰홀더에서 findViewById() 를 활용하여 뷰 객체를 참조하므로, 이것을 뷰바인딩으로 대체할 수 있습니다.

대량의 데이터와 시안 준비하기

public class ItemVO {
    String title; 
    int imgResId; 

    public ItemVO() {
    }

    public ItemVO(String title, int imgResId) {
        this.title = title;
        this.imgResId = imgResId;
    }
}

시안은 카드뷰 하나에 이미지 뷰 하나, 텍스트뷰 하나를 담았습니다.

리사이클러뷰에서 ViewBinding 을 사용하는 두가지 방법

  • 첫번째 방법

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.VH> {
    RecyclerItemBinding binding;
    Context context;
    ArrayList<ItemVO> items;

    public MyAdapter(Context context, ArrayList<ItemVO> items) {
        this.context = context;
        this.items = items;
    }
    @NonNull
    @Override
    public VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        return new VH(LayoutInflater.from(context).inflate(R.layout.recycler_item,parent,false));
    }

    @Override
    public void onBindViewHolder(@NonNull VH holder, int position) {
        ItemVO itemVO = items.get(position);
        holder.binding.iv.setImageResource(itemVO.imgResId);
    }

    @Override
    public int getItemCount() {
        return items.size();
    }

    class VH extends RecyclerView.ViewHolder{

        RecyclerItemBinding binding;

        public VH(@NonNull View itemView) {
            super(itemView);
            binding = RecyclerItemBinding.bind(itemView);
        }
    }
}

나머지 코드들은 기존의 어댑터 생성 코드와 크게 다르지 않으므로 넘어가고, 뷰홀더 클래스쪽만 살펴봅시다.

뷰홀더 클래스의 역할 중 하나는, 리사이클러뷰의 아이템뷰, 즉 여기서는 카드뷰를 가지고 있는 recycler_item.xml 파일의 뷰의 참조입니다. 원래는 findViewById() 를 활용하여 뷰 객체를 참조했지만, 뷰바인딩을 활용하면, RecyclerItemBinding 참조변수를 하나 만들고 binding = RecyclerItemBinding.bind(itemView); 을 이용하여 리사이클러뷰의 아이템 뷰들의 객체를 참조할 수 있게됩니다.

단, inflate() 가 아닌 bind() 를 사용한 이유는 이미 onCreateView() 에서 뷰홀더 객체가 생성될 때 이미 recycler_item.xml 의 뷰들은 객체로 생성이 되어있기 때문입니다. inflate() 를 사용한다면 이미 객체로 만들어진 뷰들을 다시 새로운 객체로 만드는것이기 때문에 메모리에 악영향을 미칠 수 있어 bind() 를 사용하여 이미 만들어진 뷰객체와 바인딩 객체를 연결해주어야 합니다.

  • 두번째 방법

public class MyAdapter2 extends RecyclerView.Adapter<MyAdapter2.VH> {

    Context context;
    ArrayList<ItemVO> items;
    RecyclerItemBinding binding;

    public MyAdapter2(Context context, ArrayList<ItemVO> items) {
        this.context = context;
        this.items = items;
    }

    @NonNull
    @Override
    public VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        binding = RecyclerItemBinding.inflate(LayoutInflater.from(context),parent,false);
        return new VH(binding);
    }

    @Override
    public void onBindViewHolder(@NonNull VH holder, int position) {
        binding.iv.setImageResource(items.get(position).imgResId);
    }

    @Override
    public int getItemCount() {
        return 0;
    }

    class VH extends RecyclerView.ViewHolder{

        public VH(@NonNull RecyclerItemBinding binding) {
            super(binding.getRoot());
        }
    }
}

두번째 방법 역시 첫번째 방법과 내부적인 원리는 동일합니다. 다만, 뷰홀더 객체가 리사이클러뷰의 아이템뷰 객체들을 파라미터로 전달받는 방법에 차이가 있습니다. 애초에 onCreateViewHolder() 에서는 리턴값으로 뷰홀더 객체를 전달합니다. 여기서 원래는 생성된 아이템뷰 객체를 전달했으나, 두번째 방법에서는 아이템뷰 객체가 아닌 바인딩 객체를 전달합니다. 그래서 뷰홀더의 생성자에서도 바인딩 객체로 파라미터를 받았으며, 뷰홀더의 부모생성자의 파라미터에도 바인딩된 뷰 객체 중 최상위 객체를 전달해주었습니다.


Data Binding

데이터 바인딩은 프로그래밍에서 데이터와 UI 요소를 서로 연결하는 작업입니다. 이를 통해 데이터의 변경이 UI 요소에 반영되고, UI 요소의 변경이 데이터에 반영될 수 있습니다. 일반적으로 데이터 바인딩은 다음과 같은 원리를 따릅니다.

데이터 모델: 프로그램에서 사용되는 데이터를 모델로 정의합니다. 이 모델은 일반적으로 객체, 배열, 맵 등으로 구성됩니다.

바인딩 표현식: UI 요소와 데이터 모델을 연결하는 표현식을 정의합니다. 이 표현식은 데이터 모델의 특정 속성과 UI 요소의 특정 속성을 연결합니다.

바인딩 업데이트: 데이터 모델이 변경될 때마다 UI 요소도 자동으로 업데이트됩니다. 이를 위해 바인딩 업데이트 시스템이 필요합니다. 이 시스템은 데이터 모델의 변경을 감지하고, 이에 따라 UI 요소를 업데이트합니다.

이러한 데이터 바인딩 원리를 이용하면, 데이터와 UI 요소를 효율적으로 연결할 수 있습니다. 이를 통해 프로그래밍 작업의 생산성을 높이고, 코드의 가독성을 높일 수 있습니다.


DataBinding 기능 허용하기

Data Binding 역시 안드로이드의 아키텍쳐 구성요소이기 때문에 데이터 바인딩을 사용하기 위한 설정부터 해주어야 합니다.

buildFeatures {
    dataBinding true
}

데이터 모델 클래스 정의하기

데이터 바인딩을 사용하기 위해서는 데이터 모델 클래스부터 정의해주어야 합니다.

public class User {
    private String name;
    private int age;
    private boolean fav;
    
    public User(String name, int age, boolean fav) {
        this.name = name;
        this.age = age;
        this.fav = fav;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
    
    public int getFav() {
        return fav;
    }

    public void setFav(boolean fav) {
        this.fav = fav;
    }
    
    
    public void changeName(View view){
        this.name = "robin"; 
    }
    
    
    public void increageAge(View view){
        this.age++;
    }
}

activity_main.xml 정의하기

Data Binding 을 사용하기 위해서 xml 파일을 정의해봅시다. 기존에 우리가 화면을 구성하기 위해 xml 파일을 작성할 때, 최상위 뷰로 리니어 레이아웃 또는 상대 레이아웃 등을 사용해왔습니다. 데이터 바인딩을 사용할 때에는 이런 최상위 뷰 대신 <layout> 태그로 시작해주어야 합니다.

그리고 <layout> 태그문 안쪽에는 데이터를 담을 <data> 와 화면에 표시할 뷰들을 정의한 뷰그룹이 있어야 합니다.

<layout 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">

    <data>
        <variable
            name="user"
            type="com.example.ex79_databinding.User" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:padding="16dp"
        tools:context=".MainActivity">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textColor="@color/black"
            android:padding="8dp"
            android:text="@{user.name}"/> 

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:padding="8dp"
            android:textColor="@color/black"
            android:text="@{String.valueOf(user.age)}"/>

        <CheckBox
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="종아요"
            android:checked="@{user.fav}"
            />

        <Button
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="change Text"
            android:onClick="@{user::changeName}"/>

        <Button
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="나이 증가"
            android:onClick="@{user::increaseAge}"/>
    </LinearLayout>


</layout>

User 클래스는 앱에서 사용하는 데이터를 담아두는 클래스라고 보면 됩니다. 그래서 get 과 set 을 이용하여 데이터들을 관리할 수 있습니다. 또한 이렇게 생성된 데이터들을 activity_main.xml 에서 @{user.name} 등의 형태로 사용할 수 있습니다. 어떠한 데이터가 어떠한 뷰의 속성에 들어가야할지를 xml 에서 정의해주어야 합니다. 그러면 데이터가 변할 때, 뷰의 업데이트 또한 자동으로 이루어집니다.

다만, 위 코드를 보면 버튼을 눌렀을 때 이름과 나이의 데이터가 바뀌고 자동으로 뷰가 업데이트 되어야 합니다. 그런데 직접 실행시켜보면 데이터가 변한다고 하더라도, 뷰의 업데이트는 이루어지지 않음을 알 수 있습니다. 그 이유는 데이터 바인딩에서 일반 자료형은 값이 변경되더라도 자동으로 갱신되지 않습니다.

이는 데이터 바인딩의 동작원리와 관계가 있습니다. 데이터 바인딩의 경우 Observable 객체와 Observer 객체를 이용하여 뷰와 데이터를 연결합니다. Observable 객체는 데이터의 변경을 감지하며, Observer 객체는 변경된 데이터를 뷰에 전달하는 역할을 합니다. 여기서, 우리가 사용했던 기본형 변수나 일반 객체 등은 Observable 객체가 아니기 때문에 데이터 바인딩이 데이터의 변경을 감지하지 못합니다. 그렇기 때문에 업데이트가 되지 않는것 입니다.

그래서 일반 자료형을 사용하고자 할 때에는 일반 자료형을 감싸는 객체를 사용하기를 권장합니다. 그 객체는 Observable 객체 입니다. 이 객체를 사용하면 일반 자료형을 사용한다고 하더라도, 데이터가 변경되면 자동으로 뷰의 업데이트가 이루어 집니다. 그래서 위의 코드를 Observable 객체를 이용하여 다시 작성하면 다음과 같습니다.

public class User {
    private ObservableField<String> name = new ObservableField<>();
    private ObservableInt age;
    private ObservableBoolean fav;

    public User(String name, int age, boolean fav) {
        this.name.set(name);
        this.age.set(age);
        this.fav.set(fav);
    }

    public String getName() {
        return name.get();
    }

    public void setName(String name) {
        this.name.set(name);
    }

    public int getAge() {
        return age.get();
    }

    public void setAge(int age) {
        this.age.set(age);
    }

    public boolean getFav() {
        return fav.get();
    }

    public void setFav(boolean fav) {
        this.fav.set(fav);
    }

    public void changeName(View view) {
        this.name.set("robin");
    }

    public void increaseAge(View view) {
        this.age.set(this.age.get()+1);
    }
}

Activity Lifecycle

액티비티에는 생명주기가 존재합니다. 그리고 각 생명주기에 따라 실행되는 콜백 메서드들이 존재합니다. 생명주기를 Lifecycle, 그 메소드들을 LifecycleMethod 라고 합니다. 생명주기 메소드들의 주요 6가지에 대해 한번 알아봅시다. 아래 그림은 개발자 사이트에서 가져온 그림이며, 생명주기 사이클에 대한 그림입니다.


Activity 가 화면에 보여질 때

Activity 가 화면에 보여질 때 실행되는 메소드는 3가지가 있습니다. onCreate(), onStart(), onResume() 입니다.

  • onCreate() : 액티비티가 처음 메모리에 만들어 질 때 자동으로 실행되는 메소드 입니다. 이 메소드가 실행되는 동안에는 어떠한 UI 도 그려지지 않은 상태입니다.

  • onStart() : 액티비티의 뷰들이 보이기 시작할 때 자동으로 실행되는 메소드 입니다. 이 메소드가 실행중에는 사용자가 터치를 해도 반응이 없습니다. 즉, Interaction 이 불가한 상태입니다.

  • onResume() : 액티비티가 완전히 보일 때 자동으로 실행되는 메소드 이며, 터치도 가능한 상태입니다.

public class MainActivity extends AppCompatActivity {

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

        findViewById(R.id.btn).setOnClickListener(view -> {
            Intent intent = new Intent(this, SecondActivity.class);
            startActivity(intent);
        });
        Log.i("TAG","onCreate");
    }

    @Override
    protected void onStart() {
        super.onStart();
        Log.i("TAG","onStart");
    }

    @Override
    protected void onResume() {
        super.onResume();
        Log.i("TAG","onResume");
    }
}

위 3개의 메소드가 실행된 후에는 액티비티가 실행 중인 상태가 되며, Activity 가 Running 중 이라는 표현을 사용하기도 합니다.


Activity 가 화면에 사라질 때

Activity 가 화면에서 사라질 때 실행되는 메소드는 3가지가 있습니다. onPause(), onStop(), onDestroy() 입니다.

  • onPause() : 어떤 이유로든 액티비티가 화면에서 안보이기 시작할 때 자동으로 실행되는 메소드 입니다. UI 는 아직 화면에 남아있지만 터치는 불가한 상태가 됩니다. 그리고 보통 스레드를 이곳에서 pause 합니다.

  • onStop() : 액티비티가 완전히 안보일 때 자동실행되는 메소드 입니다.

액티비티가 다른 액티비티에 의해 가려진 상태라면 위의 두 메소드만 실행됩니다.

  • onDestroy() : 디바이스의 뒤로가기 버튼 혹은 finish() 메소드로 액티비티를 종료했을 때 자동으로 실행되는 메소드 입니다. 즉, 액티비티가 메모리에서 소멸되었을 때 실행됩니다. 단, 안드로이드 12버전(API 31) 부터 디바이스의 뒤로가기 버튼을 눌러도 액티비티가 메모리에서 소멸되지 않습니다. 그 이유는 빠르게 앱을 다시 실행하기 위해서 입니다. 만일, 뒤로가기 버튼을 눌렀을 때 액티비티를 종료시키고 싶다면, onBackPressed() 메서드를 오버라이드 하여 finish() 메소드를 호출시키거나, 앱의 최신 목록에서 스와이프 하여 종료해주어야 합니다.
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onPause() {
        super.onPause();
        Log.i("TAG","onPause");
    }

    @Override
    protected void onStop() {
        super.onStop();
        Log.i("TAG","onStop");
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        Log.i("TAG","onDestroy");
    }
}
profile
Developer

0개의 댓글