목차

1. 구상

2. 프로젝트 생성

3. 코드 작성(메뉴 - 영화 목록 : 뷰페이저2)

더보기

3-1. app_bar_main.xml (툴바+프래그먼트)

3-2. fragment_movie_list.xml ('영화 목록' 레이아웃)

3-3. MovieListFragment.java ('영화 목록' 자바 코드)

4. 코드 작성(바로가기 메뉴)

더보기

4-1. activity_main_drawer.xml (메뉴바 화면)

4-2.mobile_navigation.xml (프래그먼트 집합)

4-3. mainActivity.java

5. 실행 결과

6. 발생했던 문제들

7. 참고한 링크

 

1. 구상

책의 예제는 바로가기 메뉴 + 뷰페이저 + 하단탭 을 합친 앱 화면을 만드는 것이지만 여기서 하단탭을 제외하고 만듦.

대충 아래와 같은 형태로 만들 계획.

(대충 설명)

더보기

(양 사이드에 다음 프래그먼트가 살짝 보이는 것은 Carousel을 이용해서 구현해야 하는듯. 예제대로 뷰페이저를 사용하기 위해 저런 형태는 따라하지 않음.

또한 상세 페이지, 메뉴바에 '설정' 메뉴 만드는 것은 일단 패스.

'영화목록' 메뉴를 중점적으로 만들고, 나머지 프래그먼트는 걍 화면에 생성만 시키는 걸로.)

 

2. 프로젝트 생성

Navigationi Drawer Views Activity로 프로젝트를 생성한다.

 

3. 코드 작성(매뉴 - 영화 목록 : ViewPager2)

3-1. app_bar_main.xml

오른쪽 하단에 배치된 fab 버튼을 제거했다.(주석처리)

더보기
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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">

    <com.google.android.material.appbar.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/Theme.DoitMissioin101.AppBarOverlay">

        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            app:popupTheme="@style/Theme.DoitMissioin101.PopupOverlay" />

    </com.google.android.material.appbar.AppBarLayout>

    <include layout="@layout/content_main" />

    <!--
    <com.google.android.material.floatingactionbutton.FloatingActionButton
        android:id="@+id/fab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:layout_marginEnd="@dimen/fab_margin"
        android:layout_marginBottom="16dp"
        app:srcCompat="@android:drawable/ic_dialog_email" />-->

</androidx.coordinatorlayout.widget.CoordinatorLayout>

 

3-2. fragment_movie_list.xml : content_main에 들어갈 프래그먼트. '영화 목록' 화면의 레이아웃 구상

프래그먼트 안에 뷰페이저2를 넣어준다. 프래그먼트 안에서 프래그먼트를 전환하는 것이다.

더보기
<?xml version="1.0" encoding="utf-8"?>
    <LinearLayout 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"
        android:orientation="vertical"
        android:weightSum="15"
        tools:context=".Fragment1">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="10"
        android:gravity="center"
        android:orientation="vertical">

        <androidx.viewpager2.widget.ViewPager2
            android:id="@+id/pager2"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_gravity="center"
            tools:layout_editor_absoluteX="1dp"
            tools:layout_editor_absoluteY="1dp" />
    </LinearLayout>

    <TextView
        android:id="@+id/text_title"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="2"
        android:gravity="bottom"
        android:text="영화제목"
        android:textAlignment="center"
        android:textSize="48sp" />

        <TextView
            android:id="@+id/text_info"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"
            android:text="영화정보"
            android:textAlignment="center"
            android:textSize="20sp" />

        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="2">

            <Button
                android:id="@+id/button"
                android:layout_width="180dp"
                android:layout_height="60dp"
                android:background="@drawable/button_sangsae"
                android:text="상세보기"
                android:textSize="20sp"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                android:textColor="@color/white"/>
        </androidx.constraintlayout.widget.ConstraintLayout>

    </LinearLayout>

 

3-3. MovieListFragment.java

binding을 통해 뷰에 접근하고, FragmentStateAdapter를 사용한다.

더보기
package com.example.doitmissioin_10_1.ui.moivelist;

public class MovieListFragment extends Fragment {

    ViewPager2 pager2;

    private FragmentMovieListBinding binding;

    public View onCreateView(@NonNull LayoutInflater inflater,
                             ViewGroup container, Bundle savedInstanceState) {
        HomeViewModel homeViewModel =
                new ViewModelProvider(this).get(HomeViewModel.class);

        binding = FragmentMovieListBinding.inflate(inflater, container, false);
        View root = binding.getRoot();
        
        pager2 = binding.pager2;
        pager2.setOffscreenPageLimit(3);

        ArrayList<Fragment> fragments = new ArrayList<>();
        Fragment1 fragment1 = new Fragment1();
        Fragment2 fragment2 = new Fragment2();
        Fragment3 fragment3 = new Fragment3();

        fragments.add(fragment1);
        fragments.add(fragment2);
        fragments.add(fragment3);

        FragmentStateAdapter adapter = new MyPagerAdapter(this, fragments);

        //페이지가 바뀔 때마다 아래쪽 글자 변경
        pager2.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
            @Override
            public void onPageSelected(int position) {
                super.onPageSelected(position);
                updatePageInfo(position);
            }
        });

        pager2.setAdapter(adapter);
        //pager2.setSaveEnabled(false); // 상태 유지하는? 이 코드를 작성했더니 에러 해결. 에러 : fragment no longer exists for key f#0

        return root;
    }

    private void updatePageInfo(int position) {
        if (binding == null) return; // binding이 null인지 확인
        switch (position) {
            case 0:
                binding.textTitle.setText("라라랜드");
                binding.textInfo.setText("예매율 80% | 15세 관람가 | 평점 4.9");
                break;
            case 1:
                binding.textTitle.setText("시카고");
                binding.textInfo.setText("예매율 70% | 19세 관람가 | 평점 3.9");
                break;
            case 2:
                binding.textTitle.setText("위키드");
                binding.textInfo.setText("예매율 90% | 12세 관람가 | 평점 5.0");
                break;
        }
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        binding = null;
    }

    // 어댑터 클래스를 정의할 때 액티비티에서 정의하는 것이랑 프래그먼트에서 정의하는 차이를 확인하자.
    // extends, FragmentManager, 재정의하는 함수가 달라진다.
    class MyPagerAdapter extends FragmentStateAdapter {
        ArrayList<Fragment> items = new ArrayList<Fragment>();

        // 생성자: 프래그먼트 리스트를 전달받음
        public MyPagerAdapter(@NonNull MovieListFragment movieListFragment, ArrayList<Fragment> fragments) {
            super(movieListFragment);
            this.items = fragments;
        }

        @Override
        public int getItemCount() {
            return items.size(); // 프래그먼트의 총 개수 반환
        }

        @NonNull
        @Override
        public Fragment createFragment(int position) {
            return items.get(position); // 특정 위치의 프래그먼트 반환
        }
    }
}

 

3-3.(1) onCreateView 기본 생성 코드

더보기
public View onCreateView(@NonNull LayoutInflater inflater,
                         ViewGroup container, Bundle savedInstanceState) {
    HomeViewModel homeViewModel =
            new ViewModelProvider(this).get(HomeViewModel.class);

    binding = FragmentMovieListBinding.inflate(inflater, container, false);
    View root = binding.getRoot();

HomeViewModel은 기본으로 생성되는 프래그먼트이다. 원래 이 프래그먼트의 이름은 HomeFragment였고, 거기에 짝지어져서 같이 만들어짐 프래그먼트임. (MovieListViewModel로 이름을 고치려고 했지만 귀찮아서 안하고 넘어감)

원래 이 뷰모델에 'This is ~~ Fragment'라는 텍스트가 나타나는 코드가 존재하는데, 없애버림.

이번 프로젝트에선 딱히 중요한 파트는 아님

 

Fragment@@Binding.inflate 메서드로 binding을 설정해준다.

binding을 사용하면 v.findViewById(ID) 로 뷰를 찾을 필요없이 그냥 binding.ID라고 작성하면 된다.

 

binding.getRoot 메서드를 사용함. onCreateView의 마지막엔 여기서 할당한 root를 return한다.

3-3.(2) 각각의 영화 목록 프래그먼트 생성

더보기
ArrayList<Fragment> fragments = new ArrayList<>();
Fragment1 fragment1 = new Fragment1();
Fragment2 fragment2 = new Fragment2();
Fragment3 fragment3 = new Fragment3();

fragments.add(fragment1);
fragments.add(fragment2);
fragments.add(fragment3);

Fragment1,2,3을 만들고 위 코드를 작성한다.

<Fragment> 타입이 들어가는 ArrayList 배열을 만든다.(배열명:fragments)

add 메서드로 배열에 fragment1,2,3을 넣어준다.

fragment1,2,3의 xml 레이아웃

 

3-3.(3) 어댑터

어댑터 생성

더보기

onCreateView 내부

FragmentStateAdapter adapter = new MyPagerAdapter(this, fragments);

 

 FragmentStateAdapter 타입인 어댑터를 생성한다.

여기서 내가 새로 작성하는 MyPagerAdapter 클래스 메서드를 사용하여 생성한다.

어댑터 클래스 작성

더보기

MyPagerAdapter 클래스

주의할 점 : FragmentStateAdapter 타입이라는 것. (ViewPager2를 사용하는 경우 FragmentStatePagerAdapter는 호환되지 않는다.)

class MyPagerAdapter extends FragmentStateAdapter {
    ArrayList<Fragment> items = new ArrayList<Fragment>();

    // 생성자: 프래그먼트 리스트를 전달받음
    public MyPagerAdapter(@NonNull MovieListFragment movieListFragment, ArrayList<Fragment> fragments){
        super(movieListFragment);
        this.items = fragments;
    }

    @Override
    public int getItemCount() {
        return items.size(); // 프래그먼트의 총 개수 반환
    }

    @NonNull
    @Override
    public Fragment createFragment(int position) {
        return items.get(position); // 특정 위치의 프래그먼트 반환
    }
}

 

배열 items를 생성.

 

클래스 안에 MyPagerAdapter 메서드가 존재한다. 이 메서드를 통해 프래그먼트 리스트를 전달받을 수 있음.

getItemCount, creatFragment 메서드를 재정의하고 return 값을 적절하게 바꿔서 작성한다.

3-3.(4) Pager

더보기

onCreateView 내부

pager2 = binding.pager2;
pager2.setOffscreenPageLimit(3);

binding으로 뷰페이저 설정.

setOffscreenPageLimit 메서드로 최대 페이지 수를 설정한다.

 

//페이지가 바뀔 때마다 아래쪽 글자 변경
pager2.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
    @Override
    public void onPageSelected(int position) {
        super.onPageSelected(position);
        updatePageInfo(position);
    }
});

pager2.setAdapter(adapter);
//pager2.setSaveEnabled(false);

registerOnPageChangeCallback (나 아직 콜백이 정확히 뭔지 모름. 리스너랑 무슨 차이지)

암튼 페이지가 바뀌면, updatePageInfo 메서드가 실행된다. 이 메서드는 내가 새로 작성한 것이다.

setAdapter로 뷰페이저와 어댑터를 연결한다.

 

(마지막에 주석처리된 부분은 fragment no longer exists for key f#0 라는 에러가 발생하길래 집어넣었던 코드인데, 계속 이것저것 작성하다보니 없어도 무관했다. 그래도 어떤 메서드인지는 알아두면 좋을 듯 하다..)

 

updatePageInfo 메서드

페이지가 바뀔 때마다 아랫쪽의 제목과 설명 텍스트가 변경되는 메서드이다.

private void updatePageInfo(int position) {
    if (binding == null) return; // binding이 null인지 확인
    switch (position) {
        case 0:
            binding.textTitle.setText("라라랜드");
            binding.textInfo.setText("예매율 80% | 15세 관람가 | 평점 4.9");
            break;
        case 1:
            binding.textTitle.setText("시카고");
            binding.textInfo.setText("예매율 70% | 19세 관람가 | 평점 3.9");
            break;
        case 2:
            binding.textTitle.setText("위키드");
            binding.textInfo.setText("예매율 90% | 12세 관람가 | 평점 5.0");
            break;
    }
}

 

4. 코드 작성(바로가기 메뉴)

4-1. activity_main_drawer.xml

더보기

메뉴바의 아이콘과 타이틀을 변경해준다.

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    tools:showIn="navigation_view">

    <group android:checkableBehavior="single">
        <item
            android:id="@+id/nav_movie_list"
            android:icon="@android:drawable/ic_dialog_dialer"
            android:title="영화 목록" />
        <item
            android:id="@+id/nav_reservation"
            android:icon="@android:drawable/checkbox_on_background"
            android:title="영화 예매" />
        <item
            android:id="@+id/nav_slideshow"
            android:icon="@drawable/ic_menu_slideshow"
            android:title="@string/menu_slideshow" />
    </group>
</menu>

4-2. mobile_navigation.xml

더보기

디자인 화면이 아래와 같이 뜨면 된다. 만약 내가 작성한 프래그먼트가 뜨지 않을 경우,

ID 값을 잘 확인하자. (ID 값을 변경할 경우 여기저기서 ID에 맞지 않는 코드가 존재해 에러메시지가 뜰 수 있음. 변경 후엔 제대로 모두 바꿔주기)

startDestination에 들어가는 ID도 잘 들어갔는지 보자.

4-3. MainActivity.java

더보기
package com.example.doitmissioin_10_1;

public class MainActivity extends AppCompatActivity {

    MovieListFragment movieListFragment;
    ReservationFragment reservationFragment;
    SlideshowFragment slideshowFragment;

    private AppBarConfiguration mAppBarConfiguration;
    private ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

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

        setSupportActionBar(binding.appBarMain.toolbar);

        DrawerLayout drawer = binding.drawerLayout;
        NavigationView navigationView = binding.navView;
        mAppBarConfiguration = new AppBarConfiguration.Builder(
                R.id.nav_movie_list, R.id.nav_reservation, R.id.nav_slideshow)
                .setOpenableLayout(drawer)
                .build();
        
        // Navigation Component 설정
        NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_main);
        NavigationUI.setupActionBarWithNavController(this, navController, mAppBarConfiguration);
        NavigationUI.setupWithNavController(navigationView, navController);

        setSupportActionBar(binding.appBarMain.toolbar);
        //navigationView.setNavigationItemSelectedListener(this); 초기 프래그먼트 생성 코드가 있기 때문에, 이걸 쓰면 프래그먼트가 중복 생성됨.

        movieListFragment = new MovieListFragment();
        reservationFragment = new ReservationFragment();
        slideshowFragment = new SlideshowFragment();

        // ActionBarDrawerToggle 설정 (GPT)
        ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(
                this, binding.drawerLayout, binding.appBarMain.toolbar,
                R.string.navigation_drawer_open,
                R.string.navigation_drawer_close
        );

        // DrawerLayout에 토글 설정 (GPT)
        binding.drawerLayout.addDrawerListener(toggle);
        toggle.syncState();

    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }

    @Override
    public boolean onSupportNavigateUp() {
        NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_content_main);
        return NavigationUI.navigateUp(navController, mAppBarConfiguration)
                || super.onSupportNavigateUp();
    }
}

4-3.(1) 액션바 설정

더보기

AndroidMenifest.xml을 열면, NoActionBar 테마인 것을 확인할 수 있다.

setSupportActionBar(binding.appBarMain.toolbar);

그러므로 setSupportActionBar로 액션바를 설정해준다.

 

그리고 햄버거 모양 토글(메뉴 열기 버튼)이 보이지 않을 수 있다. 이때 아래 코드를 작성하면 다시 토글이 나타난다.

// ActionBarDrawerToggle 설정 (GPT)
ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(
        this, binding.drawerLayout, binding.appBarMain.toolbar,
        R.string.navigation_drawer_open,
        R.string.navigation_drawer_close
);

// DrawerLayout에 토글 설정 (GPT)
binding.drawerLayout.addDrawerListener(toggle);
toggle.syncState();

 

5. 실행결과

다음과 같이 화면을 넘기면 포스터가 바뀌고, 아래에 그에 맞는 제목과 설명이 나타난다.

 

햄버거 버튼을 눌러서 다른 메뉴를 누를 시 해당 메뉴로 이동한다.

 

 

6. 발생했던 문제들

  •  뷰페이저를 이용하여 프래그먼트를 전환하는데 아래 텍스트가 바뀌지 않는 문제
더보기
(오른쪽) 잘못짠 코드

 

원인 : createFragment 메서드에 텍스트 바꾸는 코드 작성함.

해당 메서드는 '생성'만 담당함. 프래그먼트가 바뀌었는지 아닌지는 여기서 알 수 없음. 이 메서드는 프래그먼트 묶음 만들고 끝임.

해결 : updatePageInfo라는 새로운 메서드를 작성함. registerOnPageChangeCallback의 onPageSelected 메서드를 재정의한 다음, updatePageInfo()를 여기에 넣음으로써 페이지 전환에 따라 텍스트가 바뀌도록 만듦.

  • 메뉴화면이 겹침
더보기

아래와 같이 '영화 목록' 메뉴와 '영화 예매' 메뉴가 겹친 것이 보인다.

원인 : 간단했다. '영화 예매' 프래그먼트의 background 색을 지정해주지 않아서 그런 것.

해결 : '영화 목록' 메뉴 위에 올라가는 것이 되기 때문에, 색을 지정해줘야한다. background color를 하얀색으로 지정해줌

  • 영화 목록 내부의 프래그먼트가 이중 생성됨
더보기

원인 : 이미 Navigation Component를 설정하는 코드가 있는데(NavigationUI.setup...) 

navigationView.setNavigationItemSelectedListener(this); 코드를 코드에 작성함.

 

해결 : navigationView 설정하는 코드를 제거

 

7. 참고한 링크

 

안드로이드 앱 프로그래밍

부스트코스 무료 강의

www.boostcourse.org

 

[부스트코스] 안드로이드 프로그래밍 - 프로젝트 D 코드 리뷰

난이도가 확 뛰었던 프로젝트 D였습니다. 만들면서도 이렇게 해도 되나 싶은 생각이 참 많이 들었던 기억이 납니다. 일단 모로 가도 서울로 가는 코드로 Pass는 했지만 아직은 많이 부족하다는 걸

duda-programming.tistory.com

 

DoItAndroidRev7/mission/DoItMission10/app/src/main/res/menu/main_drawer.xml at master · mike-jung/DoItAndroidRev7

Do it! 안드로이드 앱 프로그래밍(개정7판)의 소스 코드. Contribute to mike-jung/DoItAndroidRev7 development by creating an account on GitHub.

github.com

 

ViewPager2로 프래그먼트 간 슬라이드  |  Views  |  Android Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. ViewPager2로 프래그먼트 간 슬라이드 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Compose 방식 사용해

developer.android.com

 

java.lang.IllegalStateException: Fragment no longer exists for key f0: index 1

I am having couple of fragment in an Activity. After doing some process I am closing the fragment using the below code. if (getActivity().getSupportFragmentManager().getBackStackEntryCount() > ...

stackoverflow.com

 

 

 

어후.. 이번 거는 꽤 힘들었다. 뷰페이저를 어디다 넣어야하는지도 잘 모르겠고

뷰페이저랑 바로가기메뉴가 합쳐지니까 좀 헷갈렸음. 소스 코드도 이전보다 많아서 원하는 파일을 찾는 것도 바로바로 안되서 좀 현타왔지만.. 그래도 챗지피티 덕분에 막히는 부분에서 나름 빨리 해결할 수 있었다.