마스터Q&A 안드로이드는 안드로이드 개발자들의 질문과 답변을 위한 지식 커뮤니티 사이트입니다. 안드로이드펍에서 운영하고 있습니다. [사용법, 운영진]

리사이클러뷰 어디가 잘못됐는지 봐주세요...(+코드조언..)

0 추천

이게 제가 구현하고자 하는것이구요..

 

이게 제가 지금 테스트로 실험해보고있는 것입니다..최종적으로는 저는 이것을 다이얼로그에 띄워서 선택할것인데.. 문제는 처음 등, 어깨 선택했을때는 선택도 잘되고 바뀌는것도 잘되는데

등- 어꺠 -등으로 다시 선택하면 아이템이 나오질 않더군요..

사진을 보시면 처음 두개는 잘변환되는데 이후에는 안나오는게 제가 클릭하고 있는데도 안나오는것입니다

디버깅을 해보니 자꾸 들어가는 데이터 사이즈가 0이라고 뜨던데 원인을 모르겠습니다 ㅠㅠ

어디가 문제인가요?ㅜㅜ

 

MainActivity.java

public class MainActivity extends AppCompatActivity {

    TextView back;
    TextView sholuder;
    TextView leg;
    RecyclerView recyclerView;

    ArrayList<String> items;
    ArrayList<String> items2;
    WorkoutAdapter adapter;
    String[] workout_list;
    String[] workout_list2;

    boolean selection = false;

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

        back = findViewById(R.id.back);
        sholuder = findViewById(R.id.shouler);
        leg = findViewById(R.id.leg);
        recyclerView = findViewById(R.id.recyclerview);

        back.setTag("back");
        sholuder.setTag("sholuder");
        leg.setTag("leg");


        LinearLayoutManager layoutManager = new LinearLayoutManager(getApplicationContext(), RecyclerView.VERTICAL, false);
        recyclerView.setLayoutManager(layoutManager);
        
        adapter = new WorkoutAdapter();
        items = new ArrayList<>(); // 등
        items2 = new ArrayList<>(); // 어깨
        workout_list = getResources().getStringArray(R.array.dialog_list);
        workout_list2 = getResources().getStringArray(R.array.dialog_list2);

        recyclerView.setAdapter(adapter);

        for(int i=0; i<workout_list.length; i++) { // 등 데이터 삽임
            items.add(workout_list[i]);
        }

        for(int i=0; i<workout_list2.length; i++) { // 어깨 데이터 삽임
            items2.add(workout_list2[i]);
        }


        back.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (!selection) {
                    adapter.removeItems();
                    adapter.addItems(items);
                    adapter.notifyDataSetChanged();
                }
            }
        });

        sholuder.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(!selection) {
                    adapter.removeItems();
                    adapter.addItems(items2);
                    adapter.notifyDataSetChanged();
                }
            }
        });
    }
}

 

Adapter.java

public class WorkoutAdapter extends RecyclerView.Adapter<WorkoutAdapter.ViewHolder> {
    ArrayList<String> items = new ArrayList<>();

    public void addItems(ArrayList<String> items ){
        this.items = items;
    }

    public void removeItems() {
        items.clear();
    }

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        LayoutInflater inflater = LayoutInflater.from(parent.getContext());
        View itemView = inflater.inflate(R.layout.workout_item, parent, false);

        return new ViewHolder(itemView);
    }

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        holder.setItem(items.get(position));
    }

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

    public class ViewHolder extends RecyclerView.ViewHolder {
        TextView workout;

        public ViewHolder(@NonNull View itemView) {
            super(itemView);
            workout = itemView.findViewById(R.id.workout);
        }

        public void setItem(String item) {
            workout.setText(item);
        }
    }
}

 

*임의로 실험한다고 변수명같은것은 매우 대충 짰으니 양해좀 부탁드립니다..

 

일단 구성이 strings에 아이템에 뿌려줄 문자열 배열을 정의해놓은 상태입니다.

그걸 메인에서 ArrayList<String> 타입으로 넣어준 상태입니다.

 

그 다음에 이 넣은 ArrayList<String>을 어댑터에 넣어줍니다 (adpater.addItems(items))

removeItems가 어댑터 내에 정의 되어있는 실질적으로 보여주는 items를 clear()하는데

이것은 앞전에 클릭한 다른부위의 운동항목들을 지워줍니다.

이유는 클리어 해주지않고 계속 add할경우 이 items에 클릭할때마다 계속 데이터가 쌓여서 이전 다른 부위

의 운동항목까지 같이 보일까봐 해줬습니다.

clear() 해준후 다시 add해서 아이템을 보여주는데 처음 등-어깨로 가면 아이템이 잘바뀌는데,..

 

다시 등을 누르면 아이템이 없습니다..디버깅 해보니 다시 눌렀을때의

                    adapter.addItems(items);

부분에 이 items..그러니까 메인에 정의되어있는 문자열이 들어있는 ArrayList<String>이겠죠.

이것이 size가 0으로나오네요 즉 아무것도 들어있지 않습니다 ㅠ

왜 그런건가요?ㅜ

 

 

+) 추가적으로.. 이렇게 코드를 짰는데 어더한가요..? 제 생각에는 더 좋은 방법이 있을거같은데..

for문을 본문 코드에서는 테스트용으로 2개만 썼지만.. 실제로는 운동부위가 5~6개가 돼서

for문이 더 늘어나는데.. 그렇게되면 for문만 5개로 돌아가는 양은 적지만 코드 양으로보나

뭘로보나 지저분하기도하고 별로 같습니다..

탭이나 뷰페이저는 사용하고싶지는 않은데 다른 좋은 방법이 있을까요?

codeslave (3,940 포인트) 님이 2021년 1월 5일 질문

3개의 답변

0 추천
코드가 이상은 없어 보이구요. selection이라는 변수가 좀 의심스러워 보입니다. 제가 보기에 버튼 선택상태를 체크하는 용도라면 그 변수를 없어도 돼 보입니다. 왜냐하면 이미 아이템과 버튼 클릭이벤트를 버튼별로 처리했으니까요. 한번에 한개의 버튼만 눌려지길 원한다면  ChipGroup이나 RadioButtonGroup을 사용하세요. RadioButton도 selector를 위의 화면처럼 백그라운드를 줄 수 있습니다. 반드시 한개만 눌려져야 한다면 버튼보다는 Chip이나 RadioButton이 맞아 보입니다.
spark (227,470 포인트) 님이 2021년 1월 5일 답변
spark님이 2021년 1월 5일 수정
0 추천

데이터를 가져오는 부분은 우선 별도의 클래스로 옮기세요. 인터페이스를 쓰시면 더 좋구요. 이유는 뷰는 데이터를 어떻게 가져오는지 알아야 할 필요가 없고 이건 뷰의 책임이 아니기 때문이죠. 책임이 다르다면 분리하는게 좋은 코드입니다.

대충 다음과 같은 형태로 짜보았습니다. 테스트가 안된 코드이니 copy & paste하시지 마시고 관심사를 분리한다는 개념을 이해하세요. 그리고 생성자에 Android 플랫폼에 종속적인 Resource를 전달하는 것은 좋지않습니다만, 님의 경우는 코드가 아주 작기 때문에 그렇게 까지 문제로 보이진 않습니다. 이것도 가능하다면 인터페이스 감싸서 넘기면 더 나을 수도 있겠죠.

이렇게 하시면 유닛테스트가 가능한 코드가 됩니다.

public enum BodyType {
    BACK(R.array.dialog_list1), 
    SHOULDER(R.array.dialog_list2),, 
    LEGS(R.array.dialog_list3),;

    @ArrayRes
    private int resourceId:

    public BodyType(int resourceId) {
       this.resourceId = resourceId;
    }
 
    public int getResourceId() {
           return resourceId;
    } 
}

public interface WorkoutDataSource {
     public List<String> getWorkoutListByPart(BodyType type);
}

public class WorkoutLocalDataSource implements WorkoutDataSource {

    private final Resources resources;
    public WorkoutLocalDataSource(Resources resources) {
         this.resources = resources;
    }
    
    @Override
    public List<String> getWorkoutListByPart(BodyType type) {
         return Arrays.asList(resources.getStringArray(type.getResourceId()));
    }
}


//Activity
private WorkoutDataSource workoutDataSource = new WorkoutLocalDataSource(getResources());


back.setOnClickListener(v -> { 
     updateWorkoutItems(workoutDataSource.getWorkoutListByPart(BodyType.BACK));
});


private void updateWorkoutItems(List<String> workoutItems) {
   adapter.setItems(workoutItems);
   recyclerView.post(runnable -> { 
        adapter.notifyDataSetChanged();
   });
}

public class WorkoutAdapter ... {
  
  ...
   public void setItems(List<String> items) {
          this.items = items;
   }
}
spark (227,470 포인트) 님이 2021년 1월 5일 답변
spark님이 2021년 1월 5일 수정
감사합니다 선생님. 테스트용에서 실험해보니 잘 작동하네요..
이번에 많은것을 배웠습니다.. 무식하게 for문으로 하나하나 가져오거나 하는 방법이 아니라 저런식으로 구조를 설계하고 나누고 데이터를 가져오고 세팅도하는것을..
코드측면에서 질문좀 드리겠습니다..문법이나 등등..

1.List를 사용을 하셨는데 ArrayList 보다 좋나요? 아니면 Array.asList를 사용해야만하고 Array.asList의 리턴타입이 List<T>라서 사용하신건가요?
ArrayList같은것이 List 컬렉션(상속관계?)라서 구조는 비슷한거같지만 저는 별이유 없이 ArrayList를 사용했던터라 그쪽이 익숙해서 그랬습니다. List<>가 가지는 특별한 장점같은것이 있을까요?

2. WorkoutDataSource를 인터페이스로 설정해주셨는데 보시다시피 구조가 추상함수 하나밖에 가지지 않습니다. 저같은 초보입장에서는 이정도면 그냥
WorkoutLocalDataSource에 하면되지않을까 라는 의문이 생기지 않을 수없습니다.
선생님께서 지난번에 세세하게 나누라고 하셨지만 굳이 특별한 기능을 가지는 것도아닌것같고 또 WorkoutLocalDataSource에다가 해도 무리가 없을것같은데 굳이 인터페이스까지 나누는 이유는 무엇인지 궁금합니다.
추후 다른 기능(타입)을 추가할때를 위함이신 다형성(Ploymorphism)때문이실까요?

3. 제가 아직 스레드를 잘 다루지 못하고 다룬적이 없는데....액티비티의 updateWorkoutItems()메소드에서 리사이클러뷰를 스레드로 따로 동작시켜주는 이유가 궁금합니다. 버튼 여러개만 왔다갔다하는데 따로 스레드로 작업?을 설정해주시는 이유가 무엇인가요? 스레드가 무엇인지는 알고있으나 여기서는 왜 설정해주셨는지 저의 수준에서는 아직 이해를 잘 못하겠습니다.
하나의 기능에는 무조건 스레드로 동작시켜주는게 나중에 멀티태스크?작업? 처리에 유용해서 그런것인가요?

저도 무슨 말을 하는지 모르겟지만..주저리주저리 써봤습니다 감사합니다
0 추천
1.List를 사용을 하셨는데 ArrayList 보다 좋나요? 아니면 Array.asList를 사용해야만하고 Array.asList의 리턴타입이 List<T>라서 사용하신건가요?
ArrayList같은것이 List 컬렉션(상속관계?)라서 구조는 비슷한거같지만 저는 별이유 없이 ArrayList를 사용했던터라 그쪽이 익숙해서 그랬습니다. List<>가 가지는 특별한 장점같은것이 있을까요?
 

좋다기 보다는 더 좋은 습관입니다. 가능하다면 private, final과 readonly(immutable) 형태의 타입을 쓰는 것이 더 좋은 습관입니다. 그래야, 원하지 않는 상태의 변경이나, 상태관리에 의한 리스크를 줄일 수 있으니까요. 상태를 변경하기 보다는 복사를 해서 쓰는 것이 더 좋은 습관입니다. 참고로, 이건 저만의 의견이 아니라 이미 소프트웨어 엔지니어링에서 많은 사람들이 동의하는 내용입니다.

이런 식으로 동작하는 예 중의 하나가 페이스북의 ReactJs의 Virtual DOM입니다. 보통은 뷰를 관리할 때 뷰트리를 한버만 생성하고 변경된 노드만 업데이트 하는데, 이 방식은 View의 변경사항이 있을 때 View Tree를 다시 그리는 방법으로 발상의 전환을 했습니다. 모든 경우에 그렇지는 않지만, View Tree가 복잡할수록 퍼포먼스가 좋아집니다.
 

2. WorkoutDataSource를 인터페이스로 설정해주셨는데 보시다시피 구조가 추상함수 하나밖에 가지지 않습니다. 저같은 초보입장에서는 이정도면 그냥
WorkoutLocalDataSource에 하면되지않을까 라는 의문이 생기지 않을 수없습니다.
선생님께서 지난번에 세세하게 나누라고 하셨지만 굳이 특별한 기능을 가지는 것도아닌것같고 또 WorkoutLocalDataSource에다가 해도 무리가 없을것같은데 굳이 인터페이스까지 나누는 이유는 무엇인지 궁금합니다.
추후 다른 기능(타입)을 추가할때를 위함이신 다형성(Ploymorphism)때문이실까요?
 

여기서의 핵심은 계층(layer)의 분리입니다. 소프트웨어 엔지니어링의 중요한 부분 중의 하나가 어떻게 서로 다른 계층은 잘 분리해내는가에 있습니다. 계층이란게 서로 경계가 달라지는 걸 의미하는데, 즉, 액티비티는 View  Layer이고, DataSource는 Data Layer입니다. 서로 근본적으로 하는 일과 관심사가 다릅니다. View는 화면에 필요한 것을 그리기 위한 것이고 Data 는 데이터를 처리하기 위한 것이죠. 보통는 View, Domain, Data Layer이렇게 세개의 레이어를 사용합니다. 좀 더 나은 Layering을 위해 Model-View-Controller, Model-View-Presenter, Model-View-ViewModel 같은 패턴을 적용하는 겁니다.
레이어 간에 서로 의존성이 없는 상태가 가장 좋기 때문에 상대방 레이어의 구현에 대해 몰라야 합니다. 그래서 인터페이스가 필요합니다. 특히 이런 구조는 팀으로 일할 때 정말 중요합니다. 서로 간에 코드에 대한 의존이 적게 하려면, 자연스럽게 이런 구조로 가게 됩니다. 의존이 많은 수록, 한곳에서 뭔가를 변경하면, 다른 쪽에서도 같이 변경이 많이 일어나게 됩니다. 같은 계층이 아닌 경우는 가능하다면 인터페이스를 꼭 사용하는 것이 좋은 습관입니다.

요즘의 아키텍쳐는 각 레이어마다 데이터를 나타내기 위해 다른 데이터 클래스를 사용합니다. 즉, API request/reposne(Data layer)를 Domain layer에서는 domain entity로 맵핑해서 사용하고, domain entity는 View  layer에서는 View에 적합한 데이터 클래스로 바꾸어서 사용합니다. 마찬가지로 레이어간에 의존성을 줄이기 위해서입니다.

3. 제가 아직 스레드를 잘 다루지 못하고 다룬적이 없는데....액티비티의 updateWorkoutItems()메소드에서 리사이클러뷰를 스레드로 따로 동작시켜주는 이유가 궁금합니다. 버튼 여러개만 왔다갔다하는데 따로 스레드로 작업?을 설정해주시는 이유가 무엇인가요? 스레드가 무엇인지는 알고있으나 여기서는 왜 설정해주셨는지 저의 수준에서는 아직 이해를 잘 못하겠습니다.
하나의 기능에는 무조건 스레드로 동작시켜주는게 나중에 멀티태스크?작업? 처리에 유용해서 그런것인가요?

리사이클러뷰가 많이 복잡한 뷰입니다. 지금은 아이템이 단순해서 에러가 안 날수 있지만,  아이템이 조금 복잡해지면 notify* 메소드를 호출할 때 시간차에 호출할 때 뷰를 업데이트 할 수 없는 상태에서 업데이트를 시도하려고 했다는 에러가 날 때가 있습니다. 저는 이런 에러를 여러변 경험했구요. 그래서 에러에 안전하게 업데이트하는 방식은 view.post에서 쓰레드 안에서 notify* 메소드를 호출하는 것입니다.
안드로이드의 메인(UI) thread는 16ms(초당 60 프레임) 마다 화면을 갱신하기 때문에, 정말 화면을 그리는 일 이외에는 다른 작업은 백그라운드 쓰레드에서 처리해주는 것이 좋습니다.
이게 왜 RxJava나 Coroutines같은 라이브리러를 사용하는지에 대한 핵심적인 이유입니다. Facebook의 Litho같은 라이브러리는 뷰의 첫번째 아이템을 제외하고는 뷰의 위치와 크기 등의 계산을 백그라운드 쓰레드에서 해줍니다. 이게 올해 나올 Jetpack Compose가 해줄 수 있는 부분 중의 하나이구요. IOS SwiftUI도 그럴 거라고 생각합니다.

따라서 데이터를 가져오거나 정렬 등을 계산하거나 하는 일 등은 별도의 레이어에서, 그리고 당연히 백그라운드 쓰레드에서 처리해 주어야 합니다. 그렇기 때문에 안드로이드 개발자라면 당연히 비동기 처리를 할 수 있는 방법을 알야야 합니다.
spark (227,470 포인트) 님이 2021년 1월 6일 답변
spark님이 2021년 1월 6일 수정
감사합니다. 꼼꼼히 읽었지만 아직 어려운 내용이 많네요.. 제 수준에서는 몇개빼곤 이해하기 어렵네요ㅎㅎ..그래도 계속공부하다가 다시 이걸 봤을때는 다 이해하길 기대하겠습니다 감사합니다
디자인패턴과 SOLID Principle에 대해서 공부해 보세요.
...