국비교육(22-23)

74일차(4)/Android App(28) : Bottom Navigation 예제

서리/Seori 2023. 1. 24. 14:18

74일차(4)/Android App(28) : Bottom Navigation 예제

 

- 새 모듈 생성

Step11bottomnavi

 

 

- Bottom Navigation 이라는 Activity 선택 : 하단에 네비메뉴가 있다!

 

 

- 홈 / 대시보드 / 알림이 기본 레이아웃이다. 이것도 모두 프래그먼트이다.

- 이것을 기본 틀로 해서 변경해서 쓰라는 뜻이다.

 

- 이렇게 프래그먼트가 만들어져 있다. 하나하나의 프래그먼트가 화면을 제어하고 있다.

 

- 이렇게 Fragment별로 ui에 패키지가 따로 있다.

 

- 모듈의 구조 보기. Bottom Navigation으로 이동할 수 있는 3개의 탭별로

 Fragment와 ViewModel이 각각의 클래스로 만들어져 있다.

 

- HomeFragment 에서 사용하는 데이터는 HomeViewModel 에서 가지고있다!

 

 

- BottomNavigationView 객체를 가져와서 home, dashboard, notification의 id를 얻어와서

  NavController에 가져온 참조값 넣어주기

 

- acitivity_main.xml을 보면, 이 nav_view 가 프래그먼트를 하나하나 바꿔주고 있다.

 


 

button_nav_menu.xml

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

    <item
        android:id="@+id/navigation_home"
        android:icon="@drawable/ic_home_black_24dp"
        android:title="@string/title_home" />

    <item
        android:id="@+id/navigation_dashboard"
        android:icon="@drawable/ic_dashboard_black_24dp"
        android:title="@string/title_dashboard" />

    <item
        android:id="@+id/navigation_notifications"
        android:icon="@drawable/ic_notifications_black_24dp"
        android:title="@string/title_notifications" />

</menu>

 

- menu 폴더안에 들어있는 xml 파일!

 

- 옵션 만들때만 사용하는건 아니고 뭔가 다른것들을 만들 때 사용할 수도 있다.

- navbar의 아이콘, 타이틀 정보가 들어있다.

 

- 아이콘 : drawable 폴더에 들어있다.

- 제목 : values 폴더에 들어있다.

 

- menu 에서 이것들을 모두 가져다 쓸 수 있다.

- 이미 존재하는 것을 참조할 때는 @ 로 시작하고,

 없는 것을 새로 만들 때에는 @+ 으로 시작한다.(새로 등록하는 개념)

 

 

- drawable 폴더에는 이런 벡터 그래픽이 들어있다.

- 안드로이드에서 이런 벡터 그래픽을 사용하고 싶으면, drawable 안에 벡터 그래픽이 들어있는 xml문서를 넣어놓고

 이미지처럼 가져다 쓰면 된다!

 

 

* 벡터아이콘 다운받을수있는 곳 : 링크링크2

- 웹용, 안드로이드용 구분해서 다운받을 수 있다. 그 문서를 drawable에 넣어놓고 사용하면 된다.

 

- 직접 선택해서 만들고 싶다면 res/drawable => 우클릭 => new Vector Asset 에서 만들수도있다. 

- 색상, 투명도, 개별 아이콘에 대한 outline 설정 등등도 따로 할 수 있다.

 

 

- 하단 아이콘을 이렇게 바꿔볼 수 있다. xml문서에서 다른 아이콘을 참조하도록 바꾸기만 하면 된다!

 


 

MainActivity

package com.example.step11bottomnavi;

import android.os.Bundle;

import androidx.appcompat.app.AppCompatActivity;
import androidx.navigation.NavController;
import androidx.navigation.Navigation;
import androidx.navigation.ui.AppBarConfiguration;
import androidx.navigation.ui.NavigationUI;

import com.example.step11bottomnavi.databinding.ActivityMainBinding;
import com.google.android.material.bottomnavigation.BottomNavigationView;
/*
    icon으로 사용할 vector xml 문서 다운받는 곳

    https://fonts.google.com/icons?icon.platform=android
    https://materialdesignicons.com/

    - 직접 선택해서 만들고 싶다면
      res/drawable => 우클릭 => new Vector Asset
 */
public class MainActivity extends AppCompatActivity {

    private ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //화면 레이아웃 구성하기
        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        //하단 네비바의 참조값 얻어오기
        BottomNavigationView navView = findViewById(R.id.nav_view);
        //하단 메뉴바 설정 객체의 참조값 얻어오기
        AppBarConfiguration appBarConfiguration = new AppBarConfiguration.Builder(
                R.id.navigation_home, R.id.navigation_dashboard, R.id.navigation_notifications)
                .build();
        //네비게이션 컨트롤러와 하단 메뉴바가 동작하기 위한 초기화 작업하기
        NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_activity_main);
        NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration);
        NavigationUI.setupWithNavController(binding.navView, navController);
    }

}

 

- binding 객체로 화면 꽉 채우도록 구성하기

 

acitivity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingTop="?attr/actionBarSize">

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/nav_view"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="0dp"
        android:layout_marginEnd="0dp"
        android:background="?android:attr/windowBackground"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:menu="@menu/bottom_nav_menu" />

    <fragment
        android:id="@+id/nav_host_fragment_activity_main"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:layout_constraintBottom_toTopOf="@id/nav_view"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:navGraph="@navigation/mobile_navigation" />

</androidx.constraintlayout.widget.ConstraintLayout>

 

 

- 하단 네비에 메뉴로 쓸 참조값들을 전달함 (xml에서)

 

- 생성자를 보면 가변 인자이다. 인자로 받는 갯수의 제한이 없다는 뜻이다.

- 인자를 하나 전달해도 되고, 여러개 전달해도 상관없다는 뜻!

- 인자의 개수, 이름, 어떤 동작을 할 것인지 정도만 변경해서 사용하면 된다.

 

- 주로 수정하는건 fragment가 될 것이다! fragment 의 동작과 레이아웃을 정의하면 된다.

 


 

HomeFragment

package com.example.step11bottomnavi.ui.home;

import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider;

import com.example.step11bottomnavi.databinding.FragmentHomeBinding;

public class HomeFragment extends Fragment {

    private FragmentHomeBinding binding;

    public View onCreateView(@NonNull LayoutInflater inflater,
                             ViewGroup container, Bundle savedInstanceState) {
        //HomeViewModel 을 사용할 준비
        //new ViewModelProvider( ViewModelStoreOwner type )
        //ViewModelStoreOwner interface type => 프래그먼트 or 액티비티
        HomeViewModel homeViewModel =
                new ViewModelProvider(this).get(HomeViewModel.class);

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

        final TextView textView = binding.textHome;
        homeViewModel.getText().observe(getViewLifecycleOwner(), new Observer<String>() {
            @Override
            public void onChanged(String s) {
                textView.setText(s);
            }
        });

        textView.setOnClickListener(v -> {
            homeViewModel.setText("Clicked!");
        });

        return root;
    }

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

 

HomeViewModel

package com.example.step11bottomnavi.ui.home;

import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;

public class HomeViewModel extends ViewModel {

    private final MutableLiveData<String> mText;

    public HomeViewModel() {
        mText = new MutableLiveData<>();
        mText.setValue("This is home fragment");
    }
    //라이브 데이터를 수정하는 메소드
    public void setText(String text){
        mText.setValue(text);

    }

    public LiveData<String> getText() {
        return mText;
    }
}

 

 

- 폰에서 네비요소가 밑에 있는 이유는? 손으로 사용하니까. 손으로 누르기 편하라고!

- 네비바를 손으로 누르면 컨텐츠가 바뀌는 구조이다.

 

- TextView를 클릭하면 바꿀 수 있도록 LiveData를 바꾸면 된다.

 


 

- HomeFragment에서는 ViewModel을 사용할 준비를 하는 것

→ ViewModelProvider에 owner를 전달해줘야 한다.

 

- ViewModelStoreOwner 를 받아야 하는데, fragment가 ViewModelStoreOwner 인터페이스를 구현했기 때문에 this로 받을 수 있다.

- ViewModelStoreOwner 인터페이스 타입으로 주로 사용되는 것은 fragment 또는 Activity이다.

 

- activity, fragment 는 활성화될 수도 있고 비활성화될 수도 있다.

- 그런 lifecycle을 고려해서 UI를 업데이트 해주겠다는 것. 이것이 ViewModel 을 쓰는 이유!

 

- Activity도 ViewModelStoreOwner 인터페이스 타입이 될 수 있다.

- AppCompatActivity가 FragmentActivity 상속

 → FragmentActivity가 ComponentActivity 상속

 → ComponentActivity가 ViewModelStoreOwner 상속

- 그래서 Activity에서도 ViewModel을 this 로 전달해서 사용할 수 있다.

 

- ViewModelProvider 객체를 생성해서 ViewModel 클래스를 전달하면 사용할 준비가 된다.

 

textView : : setText

- 위 표현식의 줄인 표현!

 

- setText를 호출하면서 바꾸겠다는 구조이다.

 

 

- HomeViewModel 메소드 추가

 

- 이 메소드를 호출하면서 새로운 문자열을 전달하면 수정될 것

- ViewModel에 변화를 가하면 자동으로 실행 순서가 들어온다.

 

 

- ViewModel이 가지고 있는 라이브 데이터 내용이 바뀌면 onchanged가 자동으로 호출된다.

 

- 이것을 람다식으로 바꿔보겠다!

 

- 이러한 형태가 된다. 이 안에 setText 메소드 넣어주기

 

DashboardFragment 수정

- 이렇게 한줄로 쓸 수도 있다.

 

//1번
dashboardViewModel.getText().observe(getViewLifecycleOwner(), s->{
	textView.setText(s)
});

//2번
dashboardViewModel.getText().observe(getViewLifecycleOwner(), s-> textView.setText(s) );

//3번
dashboardViewModel.getText().observe(getViewLifecycleOwner(), textView::setText);

- 위 세가지는 모두 같다.

 

- 람다식에서 매개변수를 중복으로 쓰는 불편함을 없애는 이중콜론 :: 연산자
- textView::setText : 인자로 전달받은 값을 textView 객체의 setText 메소드를 호출하면서 전달을 해라 라는 의미

 


 

DashBoardFragment

package com.example.step11bottomnavi.ui.dashboard;

import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;

import com.example.step11bottomnavi.databinding.FragmentDashboardBinding;

public class DashboardFragment extends Fragment {

    private FragmentDashboardBinding binding;

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

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

        final TextView textView = binding.textDashboard;
        //dashboardViewModel.getText().observe(getViewLifecycleOwner(), s->{textView.setText(s)});
        //람다식에서 매개변수를 중복으로 쓰는 불편함을 없애는 이중콜론 :: 연산자
        //textView::setText 인자로 전달받은 값을 textView 객체의 setText 메소드를 호출하면서 전달을 해라 라는 의미
        dashboardViewModel.getText().observe(getViewLifecycleOwner(), textView::setText);

        textView.setOnClickListener(v -> {
            dashboardViewModel.setText("Clicked!");
        });
        return root;
    }

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

 

DashBoardViewModel

package com.example.step11bottomnavi.ui.dashboard;

import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;

public class DashboardViewModel extends ViewModel {

    private final MutableLiveData<String> mText;

    public DashboardViewModel() {
        mText = new MutableLiveData<>();
        mText.setValue("This is dashboard fragment");
    }

    public void setText(String text){
        mText.setValue(text);
    }

    public LiveData<String> getText() {
        return mText;
    }
}

 

 NotificationFragment

package com.example.step11bottomnavi.ui.notifications;

import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;

import com.example.step11bottomnavi.databinding.FragmentNotificationsBinding;

public class NotificationsFragment extends Fragment {

    private FragmentNotificationsBinding binding;

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

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

        final TextView textView = binding.textNotifications;
        notificationsViewModel.getText().observe(getViewLifecycleOwner(), textView::setText);

        textView.setOnClickListener(v -> {
            notificationsViewModel.setText("Clicked!");
        });
        return root;
    }

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

 

NotificationViewModel

package com.example.step11bottomnavi.ui.notifications;

import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;

public class NotificationsViewModel extends ViewModel {

    private final MutableLiveData<String> mText;

    public NotificationsViewModel() {
        mText = new MutableLiveData<>();
        mText.setValue("This is notifications fragment");
    }

    public void setText(String text){
        mText.setValue(text);
    }

    public LiveData<String> getText() {
        return mText;
    }
}

 

- 다른 fragment 페이지에서도 setText메소드를 동일하게 처리해주면 된다.

 

 

- 그러면 모든 탭에서 텍스트뷰를 클릭하면 Clicked! 라는 메시지가 뜬다.