Navigation Graph를 통해서 BottomNavigation과 Navigation Graph를 활용합니다. 이때, 단순하게 Navigation Graph를 사용하기도 하지만, 필요에 따라서 Navigaiton Graph가 중첩된 Nested Navigation Graph를 사용하기도 합니다. 

 

 

가장 먼저 Navigation Graph를 구성합니다. 본 그래프의 형태는 include 태그를 활용하여 중첩 네비게이션으로 사용합니다. 각 그래프의 형태는 다음 사진과 같습니다.

<?xml version="1.0" encoding="utf-8"?>
<navigation 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:id="@+id/bottom_nav_graph"
    app:startDestination="@id/fragment_refrigerator">

    <fragment
        android:id="@+id/fragment_refrigerator"
        android:name="com.angdroid.refrigerator_manament.presentation.home.fragment.RefrigeratorFragment"
        android:label="@string/my_refrigerator"
        tools:layout="@layout/fragment_refrigerator" />

    <include app:graph="@navigation/recipe_nav_graph"/>

</navigation>

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/recipe_nav_graph"
    app:startDestination="@id/fragment_recipe">

    <fragment
        android:id="@+id/fragment_recipe"
        android:name="com.angdroid.refrigerator_manament.presentation.home.fragment.RecipeFragment"
        android:label="@string/recipe" >
        <action
            android:id="@+id/action_recipeFragment_to_searchFragment"
            app:destination="@id/fragment_search" />
    </fragment>
    <fragment
        android:id="@+id/fragment_search"
        android:name="com.angdroid.refrigerator_manament.presentation.home.fragment.SearchFragment"
        android:label="@string/search_recipe" />
</navigation>

 

다음과 같이 recpie_nav_graph가 include 태그를 통해서 bottom_nav_graph에 중첩되어 있는 형태입니다.

 

이때, 주의해야 할 점은 bottomNavigationView에 쓰일 menu.xml Item에 한쪽에는 fragment 한쪽에는 navigation을 넣어줘야 정상적으로 작동합니다.

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/fragment_refrigerator"
        android:iconTint="@color/gray2"
        android:icon="@drawable/ic_refrigerator"
        android:title="@string/refrigerator" />

    <item
        android:id="@+id/recipe_nav_graph"
        android:iconTint="@color/gray2"
        android:icon="@drawable/ic_recipe"
        android:title="@string/recipe" />

</menu>

다음과 같이 한쪽 item은 fragment의 id 한쪽 에는 recipe_nav_graph라는 navigation의 id가 들어 가 있습니다. 

 

그후, XML상에서 menu를 적용시켜줍니다. 

<com.google.android.material.bottomnavigation.BottomNavigationView
    android:id="@+id/bottom_nav_home"
    android:layout_width="match_parent"
    android:layout_height="@dimen/app_nav_space"
    app:itemIconTint="@drawable/selector_bottom_navigation"
    app:itemRippleColor="@color/primary_skyblue"
    app:itemTextColor="@drawable/selector_bottom_navigation"
    app:layout_constraintBottom_toBottomOf="parent"
    app:menu="@menu/bottom_nav_menu" />

마지막으로 다음과 같이 Host가 되는 Activity에서 navController를 얻은 후 BottomNavigationView에 controller를 달아 줍니다. 

        val navHostFragment =
            supportFragmentManager.findFragmentById(R.id.nav_container) as NavHostFragment
        val navController = navHostFragment.navController
        val navGraph = navController.navInflater.inflate(R.navigation.bottom_nav_graph)
        navController.graph = navGraph

        binding.bottomNavHome.setupWithNavController(navController)

 

Chain이란?

constraint_layout에서 뷰들을 연결시켜 배치 하는 것입니다. 연결된 View들은 chain 속성에 따라서

여백을 조절하거나 LinearLayout처럼 특정 View의 Weight를 조절 할 수도 있습니다.

Chain Style

chain의 style은 총 세가지가 있습니다.

app:layout_constraintHorizontal_chainStyle=””
app:layout_constraintVertical_chainStyle=””

과 같은 형식으로 지정 가능합니다.

  • spread (Default)

기본값이며, View들간 공백을 균등하게 배분합니다.

  • spread_inside

View들간 공백을 균등하게 배분하되, 가장 첫번째, 마지막 View를 각각 부모와 여백이 없도록 밀착 시킨 후 배분합니다.

  • packed

Chaining된 View들 간 뭉치게 됩니다.

 

Weighted

Linearlayout에서도 활용 되었던 부분으로 Chainig된 View들은 0dp(match constraints)로 설정됩니다. 공간에 View들의 균등하게 배분되며

layout_constraintHorizontal_weight 혹은 layout_constraintVertical_weight 를 통해서 특정 View의 크기를 조절 할 수 있습니다.

 

 

이미지 출처

https://constraintlayout.com/basics/create_chains.html

 

ConstraintLayout

Chains v2.4 alpha 7 All of the examples in this article have been created using Android Studio v2.4 alpha 7. You may see differences if you are using a different version. v1.1+ All of the examples in this article have been created using ConstraintLayout v1

constraintlayout.com

 

Activity

우리가 어플을 동작시킬때 마다 안드로이드 어플리케이션에서는 UI가 그려집니다. 해당 화면을 우리는 Activity라고 부릅니다. 하나의 어플리케이션에는 최소 1개 이상의 액티비티가 존재해야합니다.

 

 지메일 어플리케이션을 예로 든다면 우리는 지메일에서는 우리의 메일을 보여줍니다. 이러한 메일은 액티비티에서 보여지고, 다른 이메일을 본다면 다른 액티비티에서 이메일을 보여줄것 입니다.

즉, 안드로이드에서 액티비티는 마치 메인메소드와 비슷하며 액티비티는 유저와 상호작용을 하는 경우가 대부분입니다.

생명주기(Life Cycle)

사람이 어린이를 거쳐 어른이 되는 것 처럼 Activity도 다음과 같은 생명주기를 거치게 된다.

다음과 같은 과정으로 설명 할 수 있다. 그리고 각각 주기 마다 콜백 메소드를 선언 할 수 있다.

각 생명주기를 보기에 앞서 왜 이러한 생명주기에 따른 콜백 메소드가 제공되는 것일까?

 

https://developer.android.com/guide/components/activities/activity-lifecycle

 

활동 수명 주기에 관한 이해  |  Android 개발자  |  Android Developers

활동은 사용자가 전화 걸기, 사진 찍기, 이메일 보내기 또는 지도 보기와 같은 작업을 하기 위해 상호작용할 수 있는 화면을 제공하는 애플리케이션 구성요소입니다. 각 활동에는 사용자 인터페

developer.android.com

안드로이드 공식문서에 따르면

  • 사용자가 앱을 사용하는 도중에 전화가 걸려오거나 다른 앱으로 전환할 때 비정상 종료되는 문제
  • 사용자가 앱을 활발하게 사용하지 않는 경우 귀중한 시스템 리소스가 소비되는 문제
  • 사용자가 앱에서 나갔다가 나중에 돌아왔을 때 사용자의 진행 상태가 저장되지 않는 문제
  • 화면이 가로 방향과 세로 방향 간에 회전할 경우, 비정상 종료되거나 사용자의 진행 상태가 저장되지 않는 문제

다음과 같은 상황을 예방 할 수 있도록 생명주기의 각 상황에 대처 하도록 할 수 있다고 합니다.

 

Activity의 LifeCycle

 

 

onCreate()

 액티비티가 생성되면서 해당 과정에서는 데이터를 바인딩하고 액티비티를 ViewModel과 연결합니다.

혹은 saveInstanceState 매개변수를 수신하여 이동 액티비티 정보가 포함된 Bundle객체를 받습니다. 처음 생성된 액티비티을 경우 Bundle은 null값을 가집니다.

 

 또한 사용자 인터페이스를 구성한 XML파일 보통은 R.layout.activity와 같은 리소스 ID를 setContentView() 함수에 전달합니다. setContentView는 xml의 내용을 파싱하여 뷰를 생성하는 역할을 합니다.

 다만, findViewById()는 단점을 많이 가지고 있기 때문에 ViewBinding을 주로 사용하며 ViewBinding역시 OnCreate에서 inflate를 호출하여 ViewBinding을 설정해줍니다.

private lateinit var binding: ActivityMainBinding

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityMainBinding.inflate(layoutInflater)
    setContentView(binding.root)
}

 다음과 같은 기본 형태로 Bundle형태의 savedInstanceState를 받고 setContentView를 통해서 View를 지정해줍니다. 

 

 주의할 점은 OnCreate 함수는 액티비를 처음 생성할때만 호출되는 것이 아닌 화면을 다시 그려야할때 그 중에서 화면을 회전시키는 경우에도 호출이 된다. 이와 관련하여 saveInstancestate를 활용하는 방법이 있습니다.

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    	binding = ActivityMainBinding.inflate(layoutInflater)
    	setContentView(binding.root)

        if(savedInstanceState != null){
            binding.textView.text = savedInstanceState.getString("TEST")
        }
    }

    override fun onSaveInstanceState(bundle: Bundle) {
        bundle.putString("TEST","테스트")
        super.onSaveInstanceState(bundle)
    }
}

 onSaveInstanceState에서 액티비티가 파괴 되기전 "Test"라는 key와 "테스트" 라는 Value갑을 bundle에 넣어 줍니다. 이후, 화면 회전에 의해 액티비티가 다시 생성되어 OnCreate가 호출 되면서 

 아래의 그림과 같이 onSaveInstanceState에서 저장해놓은 값을 OnCreate에서 불러와서 텍스트를 변경해주는 것을 확인 할 수 있습니다.

 

Portrait에서 LandScape 전환 된 결과

 

 

onStart()

 OnCretae함수 이후에 사용자에게 View가 보여지기 직전 호출 되는 함수 입니다. 활용에 있어서 주로 회원가입 혹은 BroadCast Reciever를 해당 함수에 적용합니다.

다만, OnStart()의 경우 매우 빠르게 과정이 완료되므로 많은 동작을 하는 것은 바람직 하지 않습니다.

 

 

 

onResume()

 사용자와 상호작용을 하는 단계입니다.

 Activity에 포커스가 없어질때 까지 상태가 지속되며, 또한 액티비티가 사용자의 요구에 따라서 계속해서 변화하게(주로 OnPause <-> OnResume) 된다. 간단히 말하자면 숨겨진 액티비티가 다시 표시 될때(ForeGround 상태가 될 때) 호출됩니다.

class CameraComponent : LifecycleObserver {

    ...

    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    fun initializeCamera() {
        if (camera == null) {
            getCamera()
        }
    }

    ...
}

 공식문서의 예로 들자면, OnResume 상태에 진입하면 카메라를 활성화 하는 코드 입니다. 이때, 멀티 윈도우를 사용하여 다른 창을 탭하면 기존 액티비티가 OnPause로 접어들되고, 사용자를 위해 카메라를 비활성화 시켜야합니다.

 즉, 사용자가 해당 액티비티를 사용 중일때만 카메라를 활성화 시키고 싶을때, OnResume 생명주기를 이용한다면 멀티 윈도우, 액티비티가 백그라운드에 들어갔을 때 등등 여러 상황에 대처 할 수 있습니다.

 

 

onPause()

onResume함수와 가장 관련이 많은 함수 입니다.

override fun void onResume(){
	super.onResume();
    
    SharedPreferences pref = getSharedPreferences("pref", Activity.MODE_PRIVATE);
    if(pref != null) // 저장된 데이터가 있는경우
    {
    	String name = pref.getString("name", ""); // 두번째 parameter: 저장된 name값이 없는경우 사용할 default 값. 현재는 빈 문자열로 세팅됨
        
        // 불러온 데이터 활용
        // Toast.makeText(this, "복구된 이름: "+name, Toast.LENGTH_LONG).show();
    }
}

 다음과 같이 액티비티가 임시로 다른 액티비티에 가려서 보여지지 않는 경우가 있을텐데 이때, sharedPreferences , application, context등을 사용하여 임시로 값들을 저장한다.

 다만, 아주 잠깐 실행되므로 데이터저장, 네트워크 호출, 데이터베이스 사용 과 같은 시간이 많이 걸리는 작업은 호출하면 안됩니다.

 

 

onStop()

onStart와 짝을 이루며, onPause보다는 나중에 호출됩니다.

 배터리,CPU등에 영향이 될 만한 작업들은 해당 함수에서 중지해야 합니다. 주로 액티비티가 중지되었을때 필요하지 않는 자원들을 해제합니다.

onPause와 비교하였을때 부하가 큰 작업들을 실행하는 경우가 많습니다.

 

onStop과 onPause 차이점

 다음과 같이 다른 activity가 기존 activity에 가려서 보이지 않게 되면 onStop을 호출합니다.

 

 

 

 

onDestroy()

 액티비티가 시스템에서 소멸될때 호출됩니다. finish()함수호출, 기기 회전등에 의한 일시적인 소멸 둘다 해당 함수가 호출됩니다.

 소멸시 미처 못한 자원들에 대한 할당 해제(ViewBinding등) 종료후에 간단한 노티피케이션등을 실행합니다.

class MainActivity : AppCompatActivity() {
	private var _binding: ActivityMainBinding? = null
	val binding get() = _binding!!

	override fun onCreate(savedInstanceState: Bundle?){
    	_binding = ActivityMainBinding.inflate(layoutInflater)
    	setContentView(binding.root)
	}

	override fun onDestroy() {
    	super.onDestroy()
    	_binding = null
	}
}

 다음과 같이 onDestory이후 쓰이지 않을 ViewBinding객체를 해제시켜 줍니다.

+ Recent posts