[Android Codelab] Words - Intent

jihyo·2022년 2월 3일
0

Codelab

목록 보기
1/2

Words 앱 소개

Words는 간단한 사전 앱으로, 문자 목록과 각 문자 관련 단어, 브라우저에서 각 단어의 정의를 찾아보는 기능이 포함되어 있다.

프로젝트 주소에서 프로젝트를 받아 시작하면 된다. 링크 그대로 starter 브랜치로 시작해야 한다.

코드랩에서 앱을 만들 때는 보통 미완성된 프로젝트가 제공된다.

프로젝트를 열어보면 모든 화면이 구현되어 있지만 아직 한 화면에서 다른 화면으로 이동할 수 없다. 해야 할 작업은 처음부터 모든 항목을 빌드하지 않고도 전체 프로젝트가 작동하도록 인텐트를 사용하는 것이다.

Words 앱 개요

Words 앱은 MainActivity, 2개의 Fragment와 Adapter로 구성되어 있다.

작업할 목록은 아래와 같다.

  1. LetterAdapterMainActivityRecyclerView에서 사용한다. 각 알파벳 버튼은 현재 비어 있는 onClickListener가 포함된 버튼이다. 여기서 버튼을 누르면 DetailActivity로 이동한다.
  2. WordAdapterRecyclerViewDetailActivity에서 단어 목록을 표시하는 데 사용한다. 아직 이 화면으로 이동할 수 없지만 각 단어에도 onClickListener 상응하는 버튼이 있다. 여기에서 단어의 정의를 표시하기 위해 브라우저로 이동하는 코드를 추가한다.
  3. MainActivity에도 몇 가지 변경이 필요하다. 여기에서 옵션 메뉴를 구현하여 사용자가 목록 및 그리드 레이아웃 간에 전환할 수 있는 버튼을 표시한다.

인텐트

실행할 작업을 나타내는 객체로 Activity를 실행하는 데 가장 많이 사용되지만 다른 용도도 있다.

인텐트 유형

  • 명시적 인텐트 : 매우 구체적이며 실행할 Activity를 정확하게 알 수 있고 자체 앱의 화면인 경우가 많다.
  • 암시적 인텐트 : 추상적이며 시스템에 링크 열기나 이메일 작성, 전화 걸기와 같은 작업 유형을 알려주고 시스템은 요청 처리 방법을 파악해야한다.

일반적으로 자체 앱에서 Activity를 표시할 때 명시적 인텐트를 사용한다. 그러나 현재 앱과 관련이 없는 작업의 경우 암시적 인텐트를 사용해야 한다.

예를 들어, Android 문서 페이지를 읽다가 공유를 할 때 암시적 인텐트를 사용해야 한다. 페이지를 공유하는 데 사용할 앱을 묻는 메뉴 표시가 그 예시로 볼 수 있다.

개발자는 앱에서 작업이나 화면 표시에 명시적 인텐트를 사용하고 전체 프로세스를 책임진다. 일반적으로 암시적 인텐트는 다른 앱이 관련된 작업을 실행하는 데 사용하고 시스템이 최종 결과를 결정한다.
우리가 완성시킬 Words 앱에서는 두 유형의 인텐트를 모두 사용해볼 것이다.

Words 수정 시작

명시적 인텐트 설정

첫 번째 화면에서 사용자가 알파벳 버튼을 탭하면 단어 목록이 있는 화면으로 이동해야 한다. WordListFragment는 이미 구현되어 있기 때문에 인텐트를 사용하여 실행하기만 하면 된다.

LetterAdapter.kt

  1. onBindViewHolder()를 수정한다.

  2. context 참조를 가져와 인텐트에 담아 이동할 Activity인 DetailActivity로 보낸다.

실제 DetailActivity 객체는 백그라운드에서 만들어진다.

  1. putExtra 메서드를 호출하여 "letter"를 첫 번째 인수로 전달하고 버튼의 텍스트를 문자열로 전달한다.

  2. context 객체에서 startActivity() 메서드를 호출하여 intent를 전달한다.

holder.button.setOnClickListener {
    val context = holder.view.context
    val intent = Intent(context, DetailActivity::class.java)

    intent.putExtra("letter", holder.button.text.toString())
    
    context.startActivity(intent)
}

extra는 나중에 검색이 가능하도록 이름이 지정된 데이터이다. 함수를 호출할 때 인수를 전달하는 것과 비슷한데 보낼 때와 마찬가지로 받는 DetailActivity에도 알려줘야 한다.

이제 앱을 실행하고 알파벳 버튼을 눌러보면 Detail 화면이 표시된다. 그러나 A가 아닌 다른 알파벳 버튼을 눌러도 A에 관한 단어가 표시되는데 그 이유는 DetailActivityonCreate 메서드에서 letterId"A"가 들어가 있기 때문이다. 이 부분을 수정해야 한다.

DetailActivity 수정

앞서 말한 letterId 코드를 수정한다.

val letterId = intent?.extras?.getString("letter").toString()

extrasBundle 유형이고 인텐트에 전달된 모든 extras에 접근하는 방법을 제공한다.

intentextras는 null을 허용하기 때문에 ?로 처리해야 한다. intent 속성은 실제로 Intent가 아닐 수 있다(Activity가 인텐트에서 실행되지 않은 경우). 마찬가지로 extras 속성도 실제로 Bundle이 아니라 null일 수 있다. 만약 null인 객체에서 그냥 함수를 호출하려 한다면 앱이 종료될 수 있다. 이를 방지하기 위해 ?를 추가한다.

위의 경우는 intent가 null이면 앱은 extras 속성에 접근 시도를 하지 않으며 extras가 null이면 코드에서 getString()을 호출하려고 시도조차 하지 않는다.

이제 앱을 실행하면 각 문자의 단어 목록이 표시된다.

지금과 같은 방식은 체계적으로 유지하기 어렵다. 이런 점을 개선하기 위해 companion object를 사용하면 클래스의 특정 인스턴스 없이 상수를 구분하여 사용할 수 있다. companion object는 인스턴스와 비슷한데 companion object의 인스턴스는 하나만 존재하므로 싱글톤 패턴이라 한다.
DetailActivity 외부에서 액세스할 수 있도록 하는 방법으로 companion object를 사용한다. 먼저 companion object를 사용하여 'letter' extra의 코드를 리팩터링할 것이다.

DetailActivity

  1. onCreate 바로 위에 companion object를 추가한다.

  2. 중괄호 내에 문자 상수 속성을 추가한다.

    const val LETTER = "letter"
  3. 새 상수를 사용하려면 아래처럼 onCreate() 에서 하드 코딩 문자 호출을 업데이트한다.

val letterId = intent?.extras?.getString(LETTER).toString()

LetterAdapter.kt

  1. 새 상수를 사용하도록 onBindViewHolder에서 사용된 putExtra 호출을 수정한다.
intent.putExtra(DetailActivity.LETTER, holder.button.text.toString())

암시적 인텐트 설정

대분의 경우 자체 앱에서 Activity를 표시한다. 그러나 실행하려는 Activity나 앱을 모를 때도 있다. DetailActivity는 Google 검색에서 제공되는 사전 기능을 사용할 것이다. 새로운 Activity에서 여는 것이 아니라 스마트폰에 설치된 브라우저를 실행하여 검색 페이지를 표시할 것이다.

  1. Google에서 단어를 검색하는 앱을 만들 것이기 때문에 Google 검색 URL을 넣어줄 것이다.
companion object {
    const val LETTER = "letter"
    const val SEARCH_PREFIX = "https://www.google.com/search?q="
}

WordAdapter.kt

  1. onBindViewHolder() 메서드의 버튼에서 setOnClickListener()를 호출한다. 먼저 검색어의 URI를 만들어야한다. 그 이유는 앞서 추가한 Google 검색 URL을 해석할 필요가 있기 때문이다.
    parse()를 호출하여 String에서 URI를 만들 때 문자열 형식을 사용하여 단어가 SEARCH_PREFIX에 추가되도록 한다.
    그리고 context와 Activity를 전달하는 대신 인텐트에 해석된 URL을 같이 실어 보내주면 된다.
holder.button.setOnClickListener {
    val queryUrl: Uri = Uri.parse("${DetailActivity.SEARCH_PREFIX}${item}")
    val intent = Intent(Intent.ACTION_VIEW, queryUrl)
}

URI와 URL에 대한 설명은 다음 포스트에서 짧게 알아볼 것이다.

ACTION_VIEW는 URI를 사용하는 인텐트이다. 사용자의 웹 브라우저에서 URI를 열어 인텐트를 처리할 수 있다. 아래처럼 다른 유형들도 있다.

  • CATEGORY_APP_MAPS : 지도 앱 실행
  • CATEGORY_APP_MAIL : 이메일 앱 실행
  • CATEGORY_APP_GALLERY : 갤러리(사진) 앱 실행
  • ACTION_SET_ALARM : 백그라운드에서 알람을 설정
  • ACTION_DIAL : 전화 걸기

좀 더 자세한 내용을 알고 싶다면 여기에서 찾아보면 된다.

  1. 마지막으로 앱에서 특정 Activity를 실행하지 않아도 startActivity()를 호출하고 intent를 전달하여 시스템에 다른 앱을 실행할 수 있게 한다. (바로 밑에 그대로 작성해준다)
context.startActivity(intent)

메뉴 및 아이콘 설정

이제 메뉴 옵션을 추가해 사용자가 알파벳 리스트/그리드 레이아웃 전환이 가능하도록 한다.

그림에서 보이듯 상단의 AppBar 부분을 다뤄볼 것이다. 우선 닮은 아이콘 2개를 Vector Asset을 통해 drawable에 추가한다.

단순히 아이콘을 폴더에 추가하는 것 뿐만 아니라 시스템에도 알려야 한다.
res에 새 리소스 파일을 만들고 Resouce TypeMenu로 설정하고 직관적으로 알 수 있게 파일명을 설정해준다. (ex:layout_menu)

만들어진 layout_menu의 코드를 아래처럼 수정한다.

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <item android:id="@+id/action_switch_layout"
        android:title="@string/action_switch_layout"
        android:icon="@drawable/ic_baseline_view_list_24"
        app:showAsAction="always" />
</menu>

그러면 아래처럼 보이게 된다.

메뉴 파일의 구조는 개별 옵션을 포함하는 menu 태그로 시작한다. 버튼은 하나이며 속성은 여러가지가 있다.

  • id : 뷰와 마찬가지로 코드에서 참조가 가능하도록 id를 가진다.
  • title : 실제로 title에 적힌 텍스트대로 보이진 않지만 스크린 리더에서 메뉴를 식별하는데 도움이 된다.
  • showAsAction : 시스템에 버튼 표시 방법을 알려준다. 항상 보이도록 설정되어 있기 때문에 이 버튼은 AppBar에 항상 표시되고 더보기 메뉴에 소속되지 않는다.

이제 MainActivity.kt에 코드를 추가해 메뉴가 작동하도록 해야한다.

메뉴 버튼 구현

  1. 앱의 레이아웃 상태를 추적하는(리스트/그리드) 속성을 만들어야 한다. 기본값을 true로 하고 Linear Layout Manager를 기본으로 한다.
private var isLinearLayoutManager = true
  1. 사용자가 그리드로 전환할 때, 따라서 GridLayoutManager를 사용한다.
private fun chooseLayout() {
    if (isLinearLayoutManager) {
        recyclerView.layoutManager = LinearLayoutManger(this)
    } else {
        recyclerView.layouManager = GridLayoutManager(this, 4)
    }
    recyclerView.adapter = LetterAdapter()
}
  1. xml로 메뉴를 설정할 때 정적 아이콘을 이용했다. 레이아웃이 전환될 때 아이콘도 전환되어야 한다. 이에 대한 구현은 다음 번에 탭할 때 버튼이 다시 전환될 레이아웃에 따라 리스트/그리드 레이아웃 아이콘을 설정하기만 하면된다.
private fun setIcon(menuItem: MenuItem?) {
    if (menuItem == null)
        return
    menuItem.icon = 
    if (isLinearLayoutManager)
        ContextCompat.getDrawable(this, R.drawable.ic_grid_layout)
    else ContextCompat.getDrawable(this, R.drawable.ic_linear_layout)
}

이제 아이콘은 isLinearLayoutManager 속성에 따라 설정된다.

앱이 실제로 메뉴를 사용하려면 두 메서드를 재정의해야 한다.

  • onCreateOptionsMenu : 옵션 메뉴를 확장하여 추가 설정을 실행
  • onOptionsItemSelected : 버튼이 선택될 때 실제로 chooseLayout() 호출
  1. onCreateOptionsMenu 재정의. 레이아웃을 확장하고 레이아웃에 따라 setIcon()을 호출하여 아이콘이 올바른지 확인한다. 옵션 메뉴를 만들어야 하기 때문에 true를 반환한다.
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
    menuInflater.inflate(R.menu.layout_menu, menu)

    val layoutButton = menu?.findItem(R.id.action_switch_layout)
    // RecyclerView의 LinearLayoutManager를 기반으로 아이콘을 설정하는 코드를 호출
    setIcon(layoutButton)

    return true
}
  1. onOptionsItemSelected
override fun onOptionsItemSelected(item: MenuItem): Boolean {
    return when (item.itemId) {
        R.id.action_switch_layout -> {
            // isLinearLayoutManager(Boolean)를 반대 값으로 설정
            isLinearLayoutManager = !isLinearLayoutManager
            // 레이아웃 및 아이콘 설정
            chooseLayout()
            setIcon(item)

            return true
        }
        //  그렇지 않으면 아무 것도 하지 않고 핵심 이벤트 처리를 사용

        // when 절에서는 가능한 모든 경로를 명시적으로 설명
        // 예를 들어 값이 부울이면 참과 거짓 모두. else는 처리되지 않은 모든 경우를 포착
        else -> super.onOptionsItemSelected(item)
    }
}

when을 사용해서 어떤 메뉴 항목을 탭하는지 확인한다. idaction_switch_layout 메뉴 항목과 일치하면 isLinearLayoutManager의 값을 무효화합니다. 그런 다음 chooseLayout()setIcon()을 호출하여 적절하게 UI를 업데이트합니다.

레이아웃 관리자와 어댑터가 이제 chooseLayout()에서 설정되므로 onCreate()에서 코드를 수정하여 새 메서드를 호출해야 한다.

변경 전

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    val binding = ActivityMainBinding.inflate(layoutInflater)
    setContentView(binding.root)

    recyclerView = binding.recyclerView
    // recyclerView의 LinearLayoutManager를 설정
    recyclerView.layoutManager = LinearLayoutManager(this)
    recyclerView.adapter = LetterAdapter()
}

변경 후

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    val binding = ActivityMainBinding.inflate(layoutInflater)
    setContentView(binding.root)

    recyclerView = binding.recyclerView
    // recyclerView의 LinearLayoutManager를 설정
    chooseLayout()
}

이제 완성됐다. 솔루션 코드는 여기에서 확인하면 된다.

0개의 댓글