본문 바로가기
공부기록/안드로이드

안드로이드 RecyclerView를 이용해 이미지 게시판 만들기(인스타그램 따라해보기)

by 책읽는 개발자 ami 2021. 1. 25.
728x90
반응형

인스타그램이랑 비슷한가요?ㅋㅋㅋ 인스타그램이라고 써놓고도 민망하지만 여튼 제 목표였으니까요,,ㅎ

동적으로 받아온 데이터를 그리드 형식으로 배치하기 위해서 RecyclerView를 이용했습니다.

하지만 여전히 RecyclerView에 대해 이해가 부족한 점 양해부탁..

현재 위 사진은 프래그먼트 안에 프래그먼트로 구성되어 있습니다. 관련 포스트는 아래에 ~

2021/01/25 - [공부기록/안드로이드] - 안드로이드 프래그먼트 안에 프래그먼트 - 인스타그램처럼 만들기

 

안드로이드 프래그먼트 안에 프래그먼트 - 인스타그램처럼 만들기

지금 프로젝트를 하고 있는 앱은 크게 home, 레시피, 재료나눔, 마이페이지 탭으로 구성되어 있다. (왼쪽) 인스타그램은 어떻게 구성되어 있는지는 모르겠지만, (오른쪽) 인스타그램 사진처럼 구

amikim5263.tistory.com


Fragment_4_Mypost.java : 프래그먼트 안에 프래그먼트 중 하나(다른 프래그먼트들도 구조는 똑같기 때문에 하나만 작성하겠습니다.)

public class Fragment_4_Mypost extends Fragment {

	private RecyclerView recyclerView;
    private SwipeRefreshLayout refreshLayout;
    private List<WriteItem> postItems = new Vector<>();
    private PostRecyclerViewAdapter adapter;
 	
    @Override
    public void onResume() {
    	super.onResume();
        // 데이터 받아오는 코드도 여기 작성하면 된다. 
        new PostRecipeAsyncTask().execute(server+"Mypage/post.do",loginEmail);
        // 나는 위와 같이 AsyncTask를 상속받은 메소드를 구현하여 서버에서 데이터를 얻어온다.
        
        GridLayoutManager gridLayoutManager = new GridLayoutManager(view.getContext(),3);
        recyclerView.setLayoutManager(gridLayoutManager);
        //커스텀 어댑터 생성
        adapter = new PostRecyclerViewAdapter(postItems,getContext());
    }
    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    	view = inflater.inflate(R.layout.mypage_post_layout,null,false);
        recyclerView = view.findViewById(R.id.postRecyclerView);
        //위로 당겨서 새로고침
        refreshLayout = view.findViewById(R.id.swipePostlayout);
        refreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
            @Override
            public void onRefresh() {
            	//위로 당기면 똑같은 프래그먼트를 detach 후 attach를 하여 새로고침 기능을 구현한다.
                getFragmentManager().beginTransaction()
                        .detach(Fragment_4_Mypost.this).attach(Fragment_4_Mypost.this).commit();
            }
        });
        return view;   
    }
}
private class PostRecipeAsyncTask extends AsyncTask<String,Void,String> {
        @Override
        protected String doInBackground(String... strings) {
            StringBuffer buf = new StringBuffer();
            try {
                URL url = new URL(String.format("%s?email=%s", strings[0], strings[1]));
                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                conn.setRequestMethod("POST");
                int responseCode = conn.getResponseCode();
                if (responseCode == HttpURLConnection.HTTP_OK) {
                    BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"));
                    String line;
                    while ((line = br.readLine()) != null) {
                        buf.append(line);
                    }
                    br.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            return buf.toString();
        }

        @Override
        protected void onPostExecute(String result) {
            postItems.clear();
            try {
                JSONArray jsonArray = new JSONArray(result);
                for(int i=0; i<jsonArray.length();i++){
                    JSONObject jsonObject = jsonArray.getJSONObject(i);
                    String no = jsonObject.getString("mr_no");
                    String thumb = jsonObject.getString("mr_attachfile");
                    String attach = server +"upload/" + thumb;
                    String writer = jsonObject.getString("email");
                    WriteItem item = new WriteItem();
                    item.setMr_no(no);
                    item.setMr_attachfile(attach);
                    item.setEmail(writer);
                    postItems.add(item);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            //데이터를 다 받은뒤 어댑터 적용
            recyclerView.setAdapter(adapter);
            adapter.notifyDataSetChanged();
        }
 }

- onResume을 오버라이딩한 이유

  1. 처음 마이페이지 클릭한 경우
  2. 마이페이지와 같은 레벨의 프래그먼트(예를 들면 홈, 레시피, 재료나눔 탭)에서 다시 마이페이지로 돌아왔을 경우
  3. 위로 당겨서 새로고침한 경우

위 3가지 경우 호출 순서는 onCreateView -> onResume 입니다.

반면 Fragment_4_Mypost와 같은 레벨의 프래그먼트에 갔다가 다시 돌아오는 경우는 onResume만 호출됩니다.

데이터가 업데이트되었을 경우를 생각해 onResume에서 데이터를 받고 RecyclerView 어댑터를 생성했습니다.

어댑터 적용은 서버에서 데이터를 다 받아오고 난 후 하였습니다. recyclerView.setAdapter(adapter);

- GridLayoutManager : RecyclerView.LayoutManager를 상속받은 클래스, 생성자의 인자로 컨텍스트와 한 행을 구성할 열의 수를 넣었습니다. (RecyclerView.LayoutManager 설명 아래 참조)

By changing the LayoutManager a RecyclerView can be used to implement a standard vertically scrolling list, a uniform grid, staggered grids, horizontally scrolling collections and more. (android developer)

- RecyclerView는 보통 리스트뷰를 확장한 뷰라고들 많이 얘기한다. 개념은 이해가 되는데 사용방법은 항상 구글과 함께 했기 때문에 100프로 이해는 못 함ㅠㅠ 사용법은 밑에 코드를 첨부해서 제가 이해한 만큼 작성해보도록 하겠습니다.

일단 위에 사진을 보면 첫번째 사진과 두번째 사진은 받아오는 데이터만 다르고 View 구성이 똑같기 때문에 같은 RecyclerView를 이용했습니다. 그러나 세번째 사진은 ImageView와 TextView로 구성되어 있다는 점이 다르고, 데이터를 담는 Item(getter와 setter가 있는 클래스)도 다르기 때문에 다른 RecyclerView를 만들어 사용했습니다.

리사이클러뷰를 사용할 땐 액티비티나 프래그먼트 / RecyclerView.Adapter를 상속받은 클래스, RecyclerView.ViewHolder를 상속받은 클래스 총 3개의 클래스와 recyclerview를 포함한 layout / item을 포함한 layout 2개가 필요합니다.

RecyclerView는 Viewholder라는 패턴을 함께 사용한다고 합니다.

ViewHolder의 개념은 내가 이해한 바로는 다음과 같습니다. 어탭터에 속해있어야 한다. 뷰에 적용될 데이터를 저장한다.

android developer

(리사이클러뷰의 개념공부를 할 때 참고했던 글 추천드립니다 아주 정리가 잘되어 있어요..!)

recipes4dev.tistory.com/154

 

안드로이드 리사이클러뷰 기본 사용법. (Android RecyclerView)

1. 안드로이드 리사이클러뷰(RecyclerView) 리사이클러뷰(RecyclerView)는, "많은 수의 데이터 집합을, 제한된 영역 내에서 유연하게(flexible) 표시할 수 있도록 만들어주는 위젯"입니다. [안드로이드 개발

recipes4dev.tistory.com


mypage_post_layout.xml : SwipeRefreshLayout과 RecyclerView로 구성되어있는 레이아웃

<?xml version="1.0" encoding="utf-8"?>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
    android:id="@+id/swipePostlayout"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/postRecyclerView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

mypage_pl_item_layout.xml :  아이템 하나를 구성하는 레이아웃, 이미지 하나만 필요하기 때문에 아주 간결하다.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content"
    android:layout_height="wrap_content">
    <ImageView
        android:id="@+id/postImgView"
        android:scaleType="fitXY"
        android:layout_width="120dp"
        android:layout_height="120dp"
        android:layout_marginBottom="3dp"/>
</LinearLayout>

PostRecyclerViewAdapter.java : RecyclerView.Adapter를 상속받은 클래스

public class PostRecyclerViewAdapter extends RecyclerView.Adapter<ViewHolderPagePost> {
    private List<WriteItem> items = new Vector<>();
    private Context context;
    public PostRecyclerViewAdapter(List<WriteItem> items, Context context){
        this.items = items;
        this.context = context;
    }
    @NonNull
    @Override
    public ViewHolderPagePost onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.mypage_pl_item_layout, parent,false);
        return new ViewHolderPagePost(view);
    }

    @Override
    public void onBindViewHolder(@NonNull ViewHolderPagePost holder, int position) {
        if(holder instanceof ViewHolderPagePost){
            ViewHolderPagePost viewHolderPage = (ViewHolderPagePost)holder;
            viewHolderPage.onBind(items,position,context);
        }
    }

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

- 여기서는 하나의 아이템 뷰를 전개하고(onCreateViewHolder) ViewHolder에 position에 위치하는 데이터를 표시(?)하는 역할을 하는 것..같다..


ViewHolderPagePost.java : RecyclerView.ViewHolder를 상속받는 클래스

public class ViewHolderPagePost extends RecyclerView.ViewHolder {
    private ImageView postImgView;
    private Context context;
    private String no;
    public ViewHolderPagePost(@NonNull View itemView) {
        super(itemView);
        postImgView = itemView.findViewById(R.id.postImgView);
    }
    public void onBind(List<WriteItem> items, int position, Context context){
        this.context = context;
        no = items.get(position).getMr_no();//이미지를 클릭하면 해당 게시글로 넘어가기 위해 필요
        Picasso.get().load(items.get(position).getMr_attachfile()).placeholder(R.drawable.gray).into(postImgView);

        postImgView.setOnClickListener(new View.OnClickListener() {
        //이미지뷰를 클릭했을 때 해당 게시글로 넘어가기 위한 메소드
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(context, RecipeViewActivity.class);
                Bundle bundle = new Bundle();
                bundle.putString("no",items.get(position).getMr_no());
                bundle.putString("writer",items.get(position).getEmail());
                intent.putExtras(bundle);
                context.startActivity(intent);
            }
        });
    }
}

- onBind는 데이터를 하나의 아이템뷰에 할당하기 위한 메소드이다. 나의 경우엔 items에는 서버에서 받아온 데이터가 저장되어 있다.

- 이미지를 뷰에 넣기 위해 Piccaso 라이브러리를 이용했습니다.

implementation 'com.squareup.picasso:picasso:2.71828' 사용법은 아래 깃허브 참조

github.com/square/picasso

 

square/picasso

A powerful image downloading and caching library for Android - square/picasso

github.com

Picasso.get().load(items.get(position).getMr_attachfile()).placeholder(R.drawable.gray).into(postImgView);

  • 간단하게 설명하자면 load()에는 이미지 url / 파일 객체 / 리소스 아이디 등을 넣어준다.
  • placeholder()에는 이미지 로딩될 동안 대체로 보여줄 이미지를 넣는다. 나는 회색 이미지를 넣어놓았다. (서버에서 받아온 이미지 url를 띄워줄 때 사용자가 볼 수 있을 정도로 로딩시간이 있는 편이다.)
  • into()에는 이미지뷰를 넣어준다.

 

글이 너무 장황해진 것 같아 민망하다.. 간결하고 전달력 있는 글을 쓰고 싶지만 항상 실패..

728x90
반응형