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)

 

프래그먼트의 생명주기는 다음과 같은 그림으로 설명 할 수 있습니다. 기본적으로 Activity의 생명주기를 따라가되, 추가적인 생명주기를 가지고 있습니다. 

 

Lifecycle State Enum

  • [INITIALIZED](INITIALIZED) → onCreate() 호출 전 상태
  • [CREATED](CREATED) → onCreate() 호출 후, onStop() 호출 바로 직전
    • onStop() → onStart()로 가면 onCreate() 가 호출 된 이후 상태가 되기 때문에
  • [STARTED](STARTED) → onStart() 호출 이후, onPause() 호출 바로 전
    • onPause() 이후 onResume()으로 갈 수 있기 때문
  • [RESUMED](RESUMED) → onResume() 호출 이후
  • [DESTROYED](DESTROYED) → onDestroy() 호출 전

으로 lifecycle state enum을 정리 할 수 있습니다.

 

 

onCreate()

먼저, Fragment 만 CREATED 가 된 상황입니다.

이는 FragmentManager 에 add 됐을 때 도달하며 onCreate() 콜백함수를 호출합니다. 다만, 주의할 점은 onCreate() 이전에 onAttach() 가 먼저 호출된다는 것입니다.

그리고 이 시점에는 아직 Fragment View 가 생성되지 않았기 때문에 Fragment 의 View 와 관련된 작업을 두기에 적절하지 않습니다.

 

onCreateView(), onViewCreated()

onCreate() 이후에는 onCreateView() 와 onViewCreated() 콜백함수가 이어서 호출됩니다. onCreateView() 의 반환값으로 정상적인 Fragment View 객체를 제공했을 때만 Fragment View 의 Lifecycle 이 생성됩니다. Fragment View 의 Lifecycle 이 INITIALIZED 상태로 업데이트 됐기 때문에 View 의 초기값을 설정해주거나 (inflate 관련)

LiveData 옵저빙, RecyclerView 또는 ViewPager2 에 사용될 Adapter 세팅 등은 onViewCreated() 에서 해주는 것이 적절하겠습니다.

 

onViewStateRestored()

onViewStateRestored() 함수는 저장해둔 모든 state 값이 Fragment 의 View 계층구조에 복원 됐을 때 호출됩니다. 따라서 여기서부터는 체크박스 위젯이 현재 체크 되어있는지 등 각 뷰의 상태값을 체크할 수 있습니다.

 

onStart()

Fragment 가 사용자에게 보여질 수 있을 때 호출됩니다. 이는 주로 Fragment 가 attach 되어있는 Activity 의 onStart() 시점과 유사합니다. 이 시점부터는 Fragment 의 childFragmentManager 통해 FragmentTransaction 을 안전하게 수행할 수 있습니다.

 

onResume()

Fragment 가 보이는 상태에서 모든 Animator 와 Transition 효과가 종료되고, 프래그먼트가 사용자와 상호작용할 수 있을 때 onResume() 콜백이 호출됩니다. onStart() 와 마찬가지로 주로 Activity 의 onResume() 시점과 유사합니다.

Resumed 상태가 됐다는 것은 사용자가 프래그먼트와 상호작용 하기에 적절한 상태가 됐다고 했는데, 이는 반대로 onResume() 이 호출되지 않은 시점에서는 입력을 시도하거나 포커스를 설정하는 등의 작업을 임의로 하면 안된다는 것을 의미합니다.

 

onPause()

사용자가 Fragment 를 떠나기 시작했지만 Fragment 는 여전히 visible 일 때 onPause() 가 호출됩니다.

 

onStop()

Fragment 가 더이상 화면에 보여지지 않게 되면 Fragment 와 View 의 Lifecycle 은 CREATED 상태가 되고, onStop() 콜백 함수가 호출되게 됩니다. 이 상태는 부모 액티비티나 프래그먼트가 중단됐을 때 뿐만 아니라, 부모 액티비티나 프래그먼트의 상태가 저장될 때도 호출됩니다.

 

onDestroyView()

모든 exit animation 과 transition 이 완료되고, Fragment 가 화면으로부터 벗어났을 경우 Fragment View 의 Lifecycle 은 DESTROYED 가 되고 onDestroy() 가 호출됩니다.

이 시점부터는 getViewLifecycleOwnerLiveData() 의 리턴값으로 null 이 반환됩니다.

그리고 해당 시점에서는 가비지 컬렉터에 의해 수거될 수 있도록 Fragment View에 대한 모든 참조가 제거되어야 합니다.

onDestroy()

Fragment 가 제거되거나 FragmentManager 가 destroy 됐을 경우, 프래그먼트의 Lifecycle 은 DESTROYED 상태가 되고, onDestroy() 콜백 함수가 호출됩니다. 해당 지점은 Fragment Lifecycle 의 끝을 알립니다.

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객체를 해제시켜 줍니다.

 

 

https://www.acmicpc.net/problem/11652

 

11652번: 카드

준규는 숫자 카드 N장을 가지고 있다. 숫자 카드에는 정수가 하나 적혀있는데, 적혀있는 수는 -262보다 크거나 같고, 262보다 작거나 같다. 준규가 가지고 있는 카드가 주어졌을 때, 가장 많이 가지

www.acmicpc.net

입력받는 숫자를 HashMap<Long, Int> 형태로 key에 숫자 value에 나온 횟수를 각각 갱신해 주어서 HashMap의 최대값을 찾아서 출력하기만 하면 되는 간단한 문제였습니다.

 

import java.io.BufferedReader
import java.io.InputStreamReader

fun main() = with(BufferedReader(InputStreamReader(System.`in`))) {
    val cards = HashMap<Long,Int>()

    repeat(readLine().toInt()){
        val key = readLine().toLong()
        if(cards[key] == null) cards[key] = 1
        else cards[key] = cards[key]!!+1
    }
    print(cards.filterValues { it == cards.maxOf { it.value }}.keys.minOrNull())
}

최초 답안은 다음과 같이 작성하였습니다. 각각 HashMap에 입력받은 값을 갱신하고 HashMap의 value의 최대값을 가지고 있는 key리스트를 분류하고 그중에서도 가장 작은 값의 key를 출력해주었습니다. 

 

하지만, 다음과 같이 시간초과가 나오게 되었습니다. 입력값의 범위가 -2^62 ~ 2^62 이므로 굉장히 크기때문에 발생하는 문제였습니다. 

 

다음과 같이 코드를 바꾸었습니다. 

import java.io.BufferedReader
import java.io.InputStreamReader

fun main() = with(BufferedReader(InputStreamReader(System.`in`))) {
    val cards = HashMap<Long,Int>()

    repeat(readLine().toInt()){
        val key = readLine().toLong()
        if(cards[key] == null) cards[key] = 1
        else cards[key] = cards[key]!!+1
    }
    val max = cards.maxOf { it.value }
    val keys = cards.filterValues { it == max }.keys
    print(keys.minOrNull())
}

filterValues 내부에서 계속해서 maxOf()함수를 불러서 O(n)만큼의 동작을 계속 해주었기때문에 그만큼 계속해서 탐색 시간이 길어졌고 이로 인해서 같은 동작이지만, 동작 시간이 굉장히 늘어났던 것입니다. 

 

간단한 코드 수정이지만, 평소에도 쓸데없는 동작을 피하는 코드작성을 하도록 유의해야겠습니다!

위와 같은 검색창을 만들 때 아래와 같은 조합을 사용하였습니다.

        <com.google.android.material.textfield.TextInputLayout
            android:id="@+id/layout_et_search"
            android:layout_width="match_parent"
            android:layout_height="35dp"
            android:layout_marginHorizontal="24dp"
            android:layout_marginVertical="6dp"
            app:boxStrokeWidth="0dp"
            app:boxStrokeWidthFocused="0dp"
            app:hintEnabled="false"
            android:gravity="center_vertical"
            app:layout_constraintBottom_toTopOf="@+id/layout_friend_content"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:startIconDrawable="@drawable/ic_bottom_search">

            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/et_search"
                style="@style/Widget.TextView.Noto12_Gray2_Medium.Style"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:textColor="@color/on_surface"
                android:textColorHint="@color/grey_2"
                android:hint="@string/nickname_search"
                android:gravity="center_vertical"
                android:background="@drawable/shape_rectangle_grey_1_16dp"
                app:hintTextColor="@color/grey_2" />

        </com.google.android.material.textfield.TextInputLayout>

그런데 지금 검색창은 제가 TextInputEditText에 적용한 hint속성도 보이지 않고,

타이핑을 해도 TextInputEditText에는 글씨가 보이지 않는다.

 

실제 기기에서 사용할때도 타이핑한 글씨가 보이지 않습니다.


현재 TextInputLayout이 조금 작은 상태인데 크기를 조금 늘려보면

기존 35dp에서 110dp로 height를 조정한 상태

다음과 같이 TextInputEditText의 hint가 약간 하단에 위치하고 있습니다. 기본적으로 TextInputLayout에는 hintLabel이 존재하고 이때문에 Label만큼의 크기가 확보되지 않으면 위 처럼 실제 타이핑하는 글씨와 hint text가 보이지 않는 것입니다.

 

따라서 TextInputEditText에 아래 속성을 추가해 줍니다. 

android:paddingVertical="0dp"

 

paddingVertical = 0을 적용한 상태

크기를 다시 작제 조정했음에도 처음과는 다르게 hintText까지 잘  보이게 되었습니다

paddingVertical = 0을 적용한 상태 (110dp->35dp)

 

타이핑 한 결과도 잘 적용되었습니다.

 

결론

TextInputEditText에 아래 속성을 적용해보자!

android:paddingVertical="0dp"

 

+ Recent posts