안드로이드 바텀 네비게이션 바

PEPPERMINT100·2020년 12월 31일
0
post-thumbnail
post-custom-banner

서론

일반적인 웹 페이지에는 다양한 방식의 네비게이션이 존재한다. 페이지의 가장 상단에 수평 방향으로 깔려져 있는 경우가 있고 햄버거 메뉴에 숨겨져 있는 경우도 있고 버튼을 눌렀을 때 튀어나오는 경우도 있고 아주 다양하다. 하지만 모바일 기기는 화면의 크기가 한정되어 있기 때문에 UI/UX 적인 화려한 방법 또는 색다른 방법을 택하는 것보다 가장 컴팩트하고 유용한 방법을 쓰는 것이 일반적이다. 그 중 가장 많이 사용되는 방법인 바텀 네비게이션을 구현 하는 방법에 대해 알아보자.

안드로이드 개발에서 대부분은 코틀린을 사용하고 저도 코틀린으로 많이 배우려고 하지만 자바로도 할 줄 아는 것이 중요하다 생각하여 이번엔 자바로 진행하겠습니다.

방식

먼저 하나의 액티비티의 하단에 바텀 네비게이션 바를 그리고 네비게이션 바의 버튼을 터치할 때마다 각각 다른 프래그먼트를 보여주는 방식이 가장 무난한 것 같았다. 안드로이드를 처음 배울 때 findViewById에 익숙해지는 것이 싫어서 view binding을 바로 공부하였는데 찾아보니 data binding 이라는 방법도 많이 사용하는 것 같았다. 각각 장단점이 있지만 data bindingview binding 보다 빌드 효율이 떨어진다고 하여 view binding을 사용하는 것이 베스트지만 이번엔 data binding을 액티비티와 프래그먼트에 묶어보려고 했다. 하지만 data binding을 다시 잘 살펴보니 따로 살펴 봐야 할 정도로 러닝 커브가 있는 것을 깨달았다. 이번 글에선 data bindingenable 시키는 것에만 의의를 두고 다음에 제대로 다뤄볼 것이다.

전체적인 네비게이팅 방식은 하나의 액티비티에 프래그먼트를 띄울 레이아웃을 정하고 터치한 버튼의 id에 따라서 따로 만들어 놓은 xml 프래그먼트 클래스를 띄우는 방식으로 진행이 된다.

설정

먼저 gradle 모듈 파일에 다음과 같은 코드를 작성하고 싱크를 해준다.

   dataBinding {
        enabled = true
    }

그리고 디펜던시에는

implementation 'com.google.android.material:material:1.1.0'

바텀 네비게이션의 스타일을 위한 메테리얼 디자인을 받아준다.

XML

가장 먼저 메뉴바를 작성한다. res 폴더에 menu라는 리소스 디렉토리를 생성하고 안에 bottom_nav.xml이라는 파일을 생성하고 아래와 같이 작성한다.

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:title="Home"
        android:id="@+id/menu_home"
        android:icon="@drawable/ic_baseline_home_24"
        />
    <item
        android:title="About"
        android:id="@+id/menu_about"
        android:icon="@drawable/ic_baseline_home_24"
        />
    <item
        android:title="Profile"
        android:id="@+id/menu_profile"
        android:icon="@drawable/ic_baseline_home_24"
        />
</menu>

네비게이션 바의 디자인을 생성하는 부분인데 icondrawable 폴더에 원하는 vertor icon을 가져와서 붙여주면 된다. screen을 보면 조금 이상하게 생겼지만 액티비티 xml에 붙이면 달라진다.

이 후 activity_main.xml을 작성한다.

<?xml version="1.0" encoding="utf-8"?>
<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"
    >
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:id="@+id/fragment_frame"
            android:layout_alignParentTop="true"
            android:layout_centerHorizontal="true"
            />

        <com.google.android.material.bottomnavigation.BottomNavigationView
            app:menu="@menu/bottom_nav"
            android:id="@+id/bottom_nav"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:layout_centerHorizontal="true"
            />
    </RelativeLayout>
</layout>

참고로 data binding은 전체 xml을 layout으로 감싸주어야 한다. 그리고 프래그먼트가 들어갈 FrameLayoutBottomNavigationView를 불러와서 botton_nav.xml과 붙여준다.

그리고 프래그먼트 xml 파일들을 만든다. 필자는 각각 home_fragment.xml,about_fragment.xml, profile_fragment.xml 이라고 작성하고 안에 구분을 위한 TextView를 넣어주었다.

이렇게 하면 xml 설정은 끝난다.

Fragment 생성

이제 HomeFragment, AboutFragment, ProfileFragment라는 이름으로 자바 클래스를 생성하고 아래와 같이 작성한다.

import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.databinding.DataBindingUtil;
import androidx.fragment.app.Fragment;

import com.example.bottomnavigationwithjava.databinding.HomeFragmentBinding;

public class HomeFragment extends Fragment {
    private final String TAG = "로그";

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(TAG, "onCreate: Home Fragment onCreate");
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        HomeFragmentBinding binding = DataBindingUtil.inflate(inflater, R.layout.home_fragment, container, false);
        View view = binding.getRoot();
        return view;
    }
}

FragmentActivity와 라이프 사이클이 살짝 다른데 바인딩을 onCreateView라는 곳에서 해주어야 한다. 여기서 Fragmentxml 파일을 각각 묶어준다. 이 방식으로 각기 다른 Fragment 자바 파일을 작성해준다. 그리고 MainActivity를 아래와 같이 작성한다.

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.databinding.DataBindingUtil;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;

import android.os.Bundle;
import android.util.Log;
import android.view.MenuItem;

import com.example.bottomnavigationwithjava.databinding.ActivityMainBinding;
import com.google.android.material.bottomnavigation.BottomNavigationView;

public class MainActivity extends AppCompatActivity {
    private final String TAG = "로그";

    private FragmentManager fragmentManager = getSupportFragmentManager();

    private HomeFragment homeFragment;
    private AboutFragment aboutFragment;
    private ProfileFragment profileFragment;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);

        Log.d(TAG, "onCreate: mainActiviy onCreated");
        homeFragment = new HomeFragment();
        aboutFragment = new AboutFragment();
        profileFragment = new ProfileFragment();

        FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
        fragmentTransaction.replace(R.id.fragment_frame, homeFragment).commitAllowingStateLoss();

        binding.bottomNav.setOnNavigationItemSelectedListener(
                new BottomNavigationView.OnNavigationItemSelectedListener() {
                    @Override
                    public boolean onNavigationItemSelected(@NonNull MenuItem item) {
                        switch(item.getItemId()){
                            case R.id.menu_home:
                                Log.d(TAG, "onNavigationItemSelected: home button clicked");
                                fragmentManager.beginTransaction().replace(R.id.fragment_frame, homeFragment).commit();
                                return true;
                            case R.id.menu_about:
                                Log.d(TAG, "onNavigationItemSelected: about button clicked");
                                fragmentManager.beginTransaction().replace(R.id.fragment_frame, aboutFragment).commit();
                                return true;
                            case R.id.menu_profile:
                                Log.d(TAG, "onNavigationItemSelected: profile button clicked");
                                fragmentManager.beginTransaction().replace(R.id.fragment_frame, profileFragment).commit();
                                return true;
                            default:
                                return false;
                        }
                    }
                }
        );
    }
}

먼저 각각 프래그먼트를 가져오고 getSupportFragmentManager()도 불러온다.


        FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
        fragmentTransaction.replace(R.id.fragment_frame, homeFragment).commitAllowingStateLoss();

위 코드를 통해 액티비티가 처음 실행되었을 때 어떤 프래그먼트를 먼저 보여줄지 설정할 수 있다. 우리는 여기서 homeFragment를 보여주기로 설정한다. 마지막에 commit 메소드를 실행함으로서 화면을 전환시켜주는데 보면 stateLoss 를 할 수도 있고 안 할수도 있다. 궁금해서 이에 대해 간단히 구글링을 해보았는데,

>commit():
Schedules a commit of this transaction. The commit does not happen immediately; it will be scheduled as work on the main thread to be done the next time that thread is ready.
commitAllowingStateLoss():
A transaction can only be committed with this method prior to its containing activity saving its state. If the commit is attempted after that point, an exception will be thrown. This is because the state after the commit can be lost if the activity needs to be restored from its state. See commitAllowingStateLoss() for situations where it may be okay to lose the commit.
If you do commit() after onSaveInstance(), you will get below exception:

stateLoss는 액티비티 화면을 그리는데 state를 필요로 하지 않는 경우에 사용을 하면 된다고 한다. 만약 commit이 실행되었는데 액티비티에서 state를 필요로 한다면 예외를 던져준다고 한다. 아직 필자는 두 가지를 구분하여 사용할 정도로 안드로이드에 대해 알지 못하니 이 정도로 하고 넘어가도록 하겠다.

binding.bottomNav.setOnNavigationItemSelectedListener(
                new BottomNavigationView.OnNavigationItemSelectedListener() {
                    @Override
                    public boolean onNavigationItemSelected(@NonNull MenuItem item) {
                        switch(item.getItemId()){
                            case R.id.menu_home:
                                Log.d(TAG, "onNavigationItemSelected: home button clicked");
                                fragmentManager.beginTransaction().replace(R.id.fragment_frame, homeFragment).commit();
                                return true;
                            case R.id.menu_about:
                                Log.d(TAG, "onNavigationItemSelected: about button clicked");
                                fragmentManager.beginTransaction().replace(R.id.fragment_frame, aboutFragment).commit();
                                return true;
                            case R.id.menu_profile:
                                Log.d(TAG, "onNavigationItemSelected: profile button clicked");
                                fragmentManager.beginTransaction().replace(R.id.fragment_frame, profileFragment).commit();
                                return true;
                            default:
                                return false;
                        }
                    }
                }
        );

이제 activity_main.xmlbottom_nav에 이벤트 리스너를 달아준다. 메뉴 xml 파일에 있는 <item>의 id를 받아서 어떤 프래그먼트를 띄워줄지 정해주는 부분이다. 안드로이드 스튜디오에 위 처럼 작성하면 간단하게 람다 함수로 바꿀 수 있다고 IDE가 알려주는데,

binding.bottomNav.setOnNavigationItemSelectedListener(
                item -> {
                    switch(item.getItemId()){
                        case R.id.menu_home:
                            Log.d(TAG, "onNavigationItemSelected: home button clicked");
                            fragmentManager.beginTransaction().replace(R.id.fragment_frame, homeFragment).commit();
                            return true;
                        case R.id.menu_about:
                            Log.d(TAG, "onNavigationItemSelected: about button clicked");
                            fragmentManager.beginTransaction().replace(R.id.fragment_frame, aboutFragment).commit();
                            return true;
                        case R.id.menu_profile:
                            Log.d(TAG, "onNavigationItemSelected: profile button clicked");
                            fragmentManager.beginTransaction().replace(R.id.fragment_frame, profileFragment).commit();
                            return true;
                        default:
                            return false;
                    }
                }
        );
    }

alt+enter를 통해 자동완성 기능으로 다음과 같이 바꿔주었다. Javascript의 콜백함수 처럼 작성이 되는데 파라미터로 들어가는 BottomNavigationView.OnNavigationItemSelectedListener() 메소드를 자동으로 Java에서 인식하므로 코드를 간단히 줄일 수 있는 것이다.

결론


더닝 크루거 곡선의 한 저 정도 위치에 있는 것 같다. 이번 기회를 통해 평소에 관심이 많았던 모바일 개발도 해보고 있고 또 내 자신 스스로가 새로운 기술을 배우는 것이 얼마나 빠르고 얼마나 정확한지에 대한 실험을 해보고 있다.

React로 클라이언트 개발을 해보아서 어떤 것을 배워야 할지 어떤 어려움에 다음에 다가올지 어느정도 예측이 가서 아직까지는 안드로이드 공부하기가 수월한 것 같다. 이런 상황일수록 더더욱 기초와 원리에 기반한 공부방법을 통해 안드로이드 개발에 접근하는 것이 중요하다는 생각이 든다. 이 후에는 data bindingsqlite를 통한 내부 데이터베이스 활용, 그리고 이 후엔 HTTP 통신으로 API를 불러오는 방식으로 공부를 한 후 간단한 프로젝트를 진행해보면 어떨까 싶다.

profile
기억하기 위해 혹은 잊어버리기 위해 글을 씁니다.
post-custom-banner

0개의 댓글