웹 뷰는 앱에서 웹 페이지 화면을 띄우는 기능이다.
오늘은 웹 뷰 기능을 이용해서 EditText에 URL을 입력하고 인터넷 화면을 세 가지 방법으로 띄워볼 것이다.
레이아웃 파일에 메인 액티비티, 메인 프래그먼트 파일이 하나씩 있다.
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/container"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
</androidx.constraintlayout.widget.ConstraintLayout>
메인 액티비티 파일은 프래그먼트를 담을 용도이므로 그냥 제약 레이아웃 하나만 존재한다.
fragment_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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white">
<TextView
android:id="@+id/text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="@string/app_name"
android:textColor="@color/black"
android:textSize="27sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/url_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="10dp"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintTop_toBottomOf="@id/text_view"
app:layout_constraintBottom_toTopOf="@id/web_view_in_fragment_btn">
<TextView
android:id="@+id/https_text_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/https"
android:textColor="@color/black"
android:textSize="20sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<EditText
android:id="@+id/url_edit_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:padding="5dp"
android:textColor="@color/black"
android:textSize="20sp"
android:background="@drawable/radius_square_background"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/https_text_view"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<Button
android:id="@+id/web_view_in_fragment_btn"
android:layout_width="210dp"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:background="@color/black"
android:text="@string/webViewFragment"
android:textSize="18sp"
android:textColor="@color/white"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/url_layout"
app:layout_constraintBottom_toTopOf="@id/web_view_in_activity_btn"/>
<Button
android:id="@+id/web_view_in_activity_btn"
android:layout_width="210dp"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:background="@color/black"
android:text="@string/webViewActivity"
android:textSize="18sp"
android:textColor="@color/white"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/web_view_in_fragment_btn"
app:layout_constraintBottom_toTopOf="@id/web_view_in_dialog_btn"/>
<Button
android:id="@+id/web_view_in_dialog_btn"
android:layout_width="210dp"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:background="@color/black"
android:text="@string/webViewDialog"
android:textSize="18sp"
android:textColor="@color/white"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/web_view_in_activity_btn"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
프래그먼트는 위 화면처럼 구성되어있다.
manifest에는 인터넷 권한을 사용한다고 명시해준다.
이제 본격적으로 소스 코드를 살펴보자.
MyWebView 클래스 파일
package com.khs.webviewexampleproject
import android.annotation.SuppressLint
import android.content.Context
import android.webkit.JavascriptInterface
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/*
* 웹뷰 클래스
* url과 context를 매개변수로 받아서 웹뷰를 띄워준다.
* */
@SuppressLint("SetJavaScriptEnabled", "ViewConstructor")
class MyWebView(context: Context, url: String): WebView(context) {
init {
settings.apply {
javaScriptEnabled = true //자바스크립트 허용.
javaScriptCanOpenWindowsAutomatically = true // 웹 뷰에서 창을 자동으로 열 수 있도록 한다.
setSupportMultipleWindows(true) //웹뷰에서 새 창을 열 수 있도록 허용하는 메서드.
}
//만일 서버와 데이터를 주고받을 일이 있을 경우.
addJavascriptInterface(object: Any() {
@JavascriptInterface
fun getData(data: String) {
CoroutineScope(Dispatchers.Default).launch {
//비동기로 데이터를 가져옴.
withContext(CoroutineScope(Dispatchers.Main).coroutineContext) {
//데이터 가져온 후 할 일.
}
}
}
}, "{Name}")
webViewClient = object: WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView?,
request: WebResourceRequest?
): Boolean {
return false // 브라우저를 사용하지 않고, 웹뷰에서 URL을 열도록 한다.
}
}
//webChromeClient 등도 필요에 따라 설정 가능하다.
loadUrl(url) // 입력받은 url을 웹뷰에서 로드한다.
}
}
WebView를 생성할 때 context가 필요하며, 어떤 url을 Load할 지 알아야 하므로 두 개를 매개변수로 받는다.
init{}을 이용해서 MyWebView 인스턴스가 생성될 때 위와같이 초기화되도록 해준다.
- settings.javaScriptEnabled 속성을 true로 바꿔서 자바스크립트를 허용해준다.
웹 화면에서 UI를 눌렀을 때 반응할 수 있도록 해준다.
또한 seetings.setSupportMultipleWindows 속성을 true로 바꿔서 웹 뷰에서 새 창을 열 수 있도록 허용했다. - webViewClient를 사용하기 위해서 WebViewClient를 상속받은 익명 클래스를 하나 만든다.
그리고 shouldOverrideUrlLoading 메서드를 오버라이딩한 후에 false가 return되도록 한다.
이렇게 하면 URL 로드 시, 크롬이나 인터넷 같은 웹 브라우저가 탐색하지 않고
우리가 원하는 웹뷰가 URL을 로드한다. - loadUrl 메서드로 URL을 로드한다.
사실 이 프로젝트에서는 서버와 값을 주고받을 일은 없기 때문에 자바스크립트 인터페이스 부분은 필요없지만,
나중에 필요로 할 수도 있기 때문에 넣어놨다.
서버가 구현해놓은 양식에 맞게 메서드를 구현해서 사용하면 된다.
MainActivity 클래스 파일
package com.khs.webviewexampleproject
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.KeyEvent
import com.khs.webviewexampleproject.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setUpFragment()
}
private fun setUpFragment() {
supportFragmentManager.beginTransaction().replace(binding.container.id, MainFragment()).commit()
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
val currentFragment = supportFragmentManager.findFragmentById(binding.container.id)
//현재 컨테이너에 보이는 프래그먼트 보여주기
if(currentFragment is WebViewFragment) { // 웹뷰 프래그먼트 웹뷰 뒤로가기 history 적용.
if(currentFragment.webView.canGoBack()) {
currentFragment.webView.goBack()
return true
}
}
return super.onKeyDown(keyCode, event)
}
}
메인액티비티에서는 container에 MainFragment()를 띄워준다.
그리고 onKeyDown 메서드를 오버라이딩 해서 만일 현재 프래그먼트가
WebViewFragment, 즉 프래그먼트로 웹 뷰를 띄운 상황이고 현재 프래그먼트의 웹 뷰가
인터넷 뒤로가기가 가능하다면 인터넷 뒤로가기를 한다.
아니라면 원래 액티비티의 뒤로가기를 띄운다.
액티비티에서 구현한 이유는 프래그먼트에서는 onKeyDown 메서드를 오버라이딩할 수가 없기 때문이다.
MainFragment 클래스 파일
package com.khs.webviewexampleproject
import android.app.Dialog
import android.content.Intent
import android.os.Bundle
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.khs.webviewexampleproject.databinding.FragmentMainBinding
class MainFragment: Fragment() {
lateinit var binding: FragmentMainBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentMainBinding.inflate(inflater, container, false)
setUpBtnListener(container) // 프래그먼트가 담긴 container ViewGroup을 매개변수로 넘겨주고, 버튼 리스너 설정.
return binding.root
}
// 버튼 리스너 설정 메서드.
private fun setUpBtnListener(container: ViewGroup?) {
binding.webViewInFragmentBtn.setOnClickListener { startWebViewFragmentBtn(container) }
binding.webViewInActivityBtn.setOnClickListener { startWebViewActivityBtn() }
binding.webViewInDialogBtn.setOnClickListener { startWebViewDialogBtn() }
}
// 프래그먼트 교체 후 웹뷰를 보여주게 하는 메서드.
private fun startWebViewFragmentBtn(container: ViewGroup?) {
val url = binding.httpsTextView.text.toString() + binding.urlEditText.text.toString()
val bundle = Bundle().apply { putString("url", url) }
val mWebViewFragment = WebViewFragment().apply { arguments = bundle }
container?.id?.let {
containerID ->
requireActivity().supportFragmentManager
.beginTransaction()
.replace(containerID, mWebViewFragment)
.addToBackStack(null)
.commit()
}
}
// 새로운 액티비티를 띄워서 그 액티비티에 웹뷰를 보여주게 하는 메서드.
private fun startWebViewActivityBtn() {
val url = binding.httpsTextView.text.toString() + binding.urlEditText.text.toString()
val intent = Intent(requireContext(), WebViewActivity::class.java).apply { putExtra("url", url) }
startActivity(intent)
}
// 다이얼로그 창을 띄워서 웹뷰를 보여주게 하는 메서드.
private fun startWebViewDialogBtn() {
val url = binding.httpsTextView.text.toString() + binding.urlEditText.text.toString()
val mWebView = MyWebView(requireContext(), url)
val webViewDialog = Dialog(requireContext()).apply {
window?.attributes?.apply {
width = ViewGroup.LayoutParams.MATCH_PARENT
height = ViewGroup.LayoutParams.MATCH_PARENT
}
setContentView(mWebView)
//뒤로가기 설정. 만일 webView의 history가 쌓여있다면 webView 뒤로가기. 아니라면 dismiss()되도록 했다.
setOnKeyListener { _, keyCode, _ ->
if(keyCode == KeyEvent.KEYCODE_BACK && mWebView.canGoBack()) {
mWebView.goBack()
true
} else {
false
}
}
}
webViewDialog.show()
}
}
Fragment에서는 버튼 리스너 관련 메서드만 구현했다.
버튼 리스너들은 모두 공통적으로 "https:// + {에딧텍스트에 입력된 url}"을 변수로 만들고 넘겨준다.
- setUpBtnListener는 버튼 세 개의 리스너를 설정해준다.
매개변수로 container를 넘겨받는데,
프래그먼트를 교체해서 웹 뷰 프래그먼트를 띄울 때는 어디에 프래그먼트를 띄울지 알아야 하므로
container를 넘겨받는다. - startWebViewFragmentBtn 메서드로 프래그먼트를 웹 뷰 프래그먼트로 교체하는 기능을 구현해준다.
번들 객체를 이용해서 url을 넘겨주고, addToBackStack()메서드로 메인 프래그먼트를 백스택에 넣어준다.
WebViewFragment 클래스 파일
initWebView 메서드로 번들 객체로 넘겨받은 url을 받아서 웹 뷰 전역변수를 초기화해준다.package com.khs.webviewexampleproject import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment class WebViewFragment: Fragment() { lateinit var webView: MyWebView override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { initWebView() return webView // 나타나는 뷰를 웹뷰로 설정한다. } //웹뷰 초기화 메서드. private fun initWebView() { val url = arguments?.getString("url") // 메인 액티비티에서 입력한 주소를 받아온다. webView = MyWebView(requireContext(), url.toString()) } }
후에 onCreateView 메서드에서 webView 변수를 return해서 프래그먼트가 웹 뷰를 띄워주도록 한다.
웹뷰프래그먼트에 레이아웃파일이 없는 이유가 바로 이것이다. - startWebViewActivityBtn 메서드로 웹 뷰 액티비티를 띄우는 기능을 만든다.
intent 객체에 putExtra로 url을 넘겨주고 startActivity() 메서드로 웹 뷰 액티비티를 띄운다.
WebViewActivity 클래스 파일
프래그먼트와 마찬가지로 전역변수로 웹 뷰 변수를 하나 만들어주고package com.khs.webviewexampleproject import android.os.Bundle import android.view.KeyEvent import androidx.appcompat.app.AppCompatActivity class WebViewActivity: AppCompatActivity() { lateinit var webView: MyWebView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) initWebView() setContentView(webView) } //웹뷰 초기화 메서드. private fun initWebView() { val url = intent.getStringExtra("url") webView = MyWebView(baseContext, url.toString()) } //뒤로가기 버튼 로직 설정. override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { // Check if the key event was the Back button and if there's history if (keyCode == KeyEvent.KEYCODE_BACK && webView.canGoBack()) { // 입력한 키값이 뒤로가기 버튼이고 웹뷰의 history가 쌓여있다면 웹뷰에서 뒤로가기 처리를 한다. webView.goBack() return true } // If it wasn't the Back key or there's no web page history, bubble up to the default // system behavior (probably exit the activity) return super.onKeyDown(keyCode, event) // 아니라면 원래 키 이벤트 처리를 한다. } }
initWebView 메서드로 웹 뷰 변수를 초기화해준다.
후에 setContentView 메서드로 웹 뷰를 띄워주도록 만든다.
메인액티비티와 마찬가지로 onKeyDown 메서드를 오버라이딩했다.
인터넷에서 뒤로가기가 가능할 때엔 인터넷 뒤로가기를 하도록 만들었다. - startWebViewDialogBtn 메서드로 다이얼로그 창으로 웹뷰를 나타나게 하는 기능을 구현한다.
webViewDialog 변수를 만들어서
window.attribute에서 width와 height를 ViewGroup.layoutparams.MATCH_PARENT로 설정한다.
그러고 난 후에 setContentView를 webView 변수로 해서 웹 뷰를 띄워주도록 만든다.
setOnKeyListener를 람다식으로 만들어서, 인터넷에서 뒤로가기가 가능하다면
웹 뷰에서 뒤로가기를 하도록 만들었다.
후에 webViewDialog.show()를 해서 다이얼로그를 보여주도록 만든다.
이제 실제 앱 실행화면을 살펴보자.
앱 구동화면
메인화면
프래그먼트에서 띄운 웹뷰화면
액티비티에서 띄운 웹뷰화면
다이얼로그에서 띄운 웹뷰화면
웹 뷰는 요즘 앱에서 정말 많이 사용하는 기능이다.
결제, 주소검색 등에서 사용한다.
예제 프로젝트는 그냥 url을 띄우는데에만 초점을 뒀지만
실제로는 서버에서 제공하는 url로 웹 뷰를 띄운 후에 상호작용을 하는 경우가 더 많다.
코드 전문은 위에서 확인이 가능하다.