Words는 간단한 사전 앱으로, 문자 목록과 각 문자 관련 단어, 브라우저에서 각 단어의 정의를 찾아보는 기능이 포함되어 있다.
프로젝트 주소에서 프로젝트를 받아 시작하면 된다. 링크 그대로 starter 브랜치로 시작해야 한다.
코드랩에서 앱을 만들 때는 보통 미완성된 프로젝트가 제공된다.
프로젝트를 열어보면 모든 화면이 구현되어 있지만 아직 한 화면에서 다른 화면으로 이동할 수 없다. 해야 할 작업은 처음부터 모든 항목을 빌드하지 않고도 전체 프로젝트가 작동하도록 인텐트를 사용하는 것이다.
Words 앱은 MainActivity
, 2개의 Fragment와 Adapter로 구성되어 있다.
작업할 목록은 아래와 같다.
LetterAdapter
는 MainActivity
의 RecyclerView
에서 사용한다. 각 알파벳 버튼은 현재 비어 있는 onClickListener
가 포함된 버튼이다. 여기서 버튼을 누르면 DetailActivity
로 이동한다.WordAdapter
는 RecyclerView
의 DetailActivity
에서 단어 목록을 표시하는 데 사용한다. 아직 이 화면으로 이동할 수 없지만 각 단어에도 onClickListener
상응하는 버튼이 있다. 여기에서 단어의 정의를 표시하기 위해 브라우저로 이동하는 코드를 추가한다.MainActivity
에도 몇 가지 변경이 필요하다. 여기에서 옵션 메뉴를 구현하여 사용자가 목록 및 그리드 레이아웃 간에 전환할 수 있는 버튼을 표시한다.실행할 작업을 나타내는 객체로 Activity를 실행하는 데 가장 많이 사용되지만 다른 용도도 있다.
일반적으로 자체 앱에서 Activity를 표시할 때 명시적 인텐트를 사용한다. 그러나 현재 앱과 관련이 없는 작업의 경우 암시적 인텐트를 사용해야 한다.
예를 들어, Android 문서 페이지를 읽다가 공유를 할 때 암시적 인텐트를 사용해야 한다. 페이지를 공유하는 데 사용할 앱을 묻는 메뉴 표시가 그 예시로 볼 수 있다.
개발자는 앱에서 작업이나 화면 표시에 명시적 인텐트를 사용하고 전체 프로세스를 책임진다. 일반적으로 암시적 인텐트는 다른 앱이 관련된 작업을 실행하는 데 사용하고 시스템이 최종 결과를 결정한다.
우리가 완성시킬 Words 앱에서는 두 유형의 인텐트를 모두 사용해볼 것이다.
첫 번째 화면에서 사용자가 알파벳 버튼을 탭하면 단어 목록이 있는 화면으로 이동해야 한다. WordListFragment
는 이미 구현되어 있기 때문에 인텐트를 사용하여 실행하기만 하면 된다.
LetterAdapter.kt
onBindViewHolder()
를 수정한다.
context
참조를 가져와 인텐트에 담아 이동할 Activity인 DetailActivity
로 보낸다.
실제 DetailActivity
객체는 백그라운드에서 만들어진다.
putExtra
메서드를 호출하여 "letter"를 첫 번째 인수로 전달하고 버튼의 텍스트를 문자열로 전달한다.
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에 관한 단어가 표시되는데 그 이유는 DetailActivity
의 onCreate
메서드에서 letterId
에 "A"
가 들어가 있기 때문이다. 이 부분을 수정해야 한다.
앞서 말한 letterId
코드를 수정한다.
val letterId = intent?.extras?.getString("letter").toString()
extras
는 Bundle
유형이고 인텐트에 전달된 모든 extras
에 접근하는 방법을 제공한다.
intent
와 extras
는 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
onCreate
바로 위에 companion object
를 추가한다.
중괄호 내에 문자 상수 속성을 추가한다.
const val LETTER = "letter"
새 상수를 사용하려면 아래처럼 onCreate()
에서 하드 코딩 문자 호출을 업데이트한다.
val letterId = intent?.extras?.getString(LETTER).toString()
LetterAdapter.kt
onBindViewHolder
에서 사용된 putExtra
호출을 수정한다.intent.putExtra(DetailActivity.LETTER, holder.button.text.toString())
대분의 경우 자체 앱에서 Activity를 표시한다. 그러나 실행하려는 Activity나 앱을 모를 때도 있다. DetailActivity
는 Google 검색에서 제공되는 사전 기능을 사용할 것이다. 새로운 Activity에서 여는 것이 아니라 스마트폰에 설치된 브라우저를 실행하여 검색 페이지를 표시할 것이다.
companion object {
const val LETTER = "letter"
const val SEARCH_PREFIX = "https://www.google.com/search?q="
}
WordAdapter.kt
onBindViewHolder()
메서드의 버튼에서 setOnClickListener()
를 호출한다. 먼저 검색어의 URI를 만들어야한다. 그 이유는 앞서 추가한 Google 검색 URL을 해석할 필요가 있기 때문이다.parse()
를 호출하여 String
에서 URI
를 만들 때 문자열 형식을 사용하여 단어가 SEARCH_PREFIX
에 추가되도록 한다.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
: 전화 걸기
좀 더 자세한 내용을 알고 싶다면 여기에서 찾아보면 된다.
startActivity()
를 호출하고 intent
를 전달하여 시스템에 다른 앱을 실행할 수 있게 한다. (바로 밑에 그대로 작성해준다)context.startActivity(intent)
이제 메뉴 옵션을 추가해 사용자가 알파벳 리스트/그리드 레이아웃 전환이 가능하도록 한다.
그림에서 보이듯 상단의 AppBar 부분을 다뤄볼 것이다. 우선 닮은 아이콘 2개를 Vector Asset을 통해 drawable에 추가한다.
단순히 아이콘을 폴더에 추가하는 것 뿐만 아니라 시스템에도 알려야 한다.
res에 새 리소스 파일을 만들고 Resouce Type을 Menu
로 설정하고 직관적으로 알 수 있게 파일명을 설정해준다. (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
에 코드를 추가해 메뉴가 작동하도록 해야한다.
true
로 하고 Linear Layout Manager를 기본으로 한다.private var isLinearLayoutManager = true
GridLayoutManager
를 사용한다.private fun chooseLayout() {
if (isLinearLayoutManager) {
recyclerView.layoutManager = LinearLayoutManger(this)
} else {
recyclerView.layouManager = GridLayoutManager(this, 4)
}
recyclerView.adapter = LetterAdapter()
}
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()
호출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
}
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
을 사용해서 어떤 메뉴 항목을 탭하는지 확인한다. id
가 action_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()
}
이제 완성됐다. 솔루션 코드는 여기에서 확인하면 된다.