
# Databinding
앱의 데이터 소스와 레이아웃의 구성요소를 결합할 수 있게 해주는 Android JetPack의 라이브러리 중 하나.
Class에서 view들을 정의해서 사용하지 않아도 되고, Data를 View에 연결시켜 두면 Data가 변할 때 따로 세팅해주지 않아도 변경되게 할 수 있다.
다음 검색 api를 이용하여 웹문서를 검색하는 방법으로 실습을 진행해보자!
https://developers.kakao.com/docs/latest/ko/daum-search/dev-guide
Kakao Developers
카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.
developers.kakao.com
1. build.gradle 설정
데이터 바인딩 설정은 build.gradle 파일 내 buildFeatrues에서 설정해준다.
dependencies에 lifecycle과 retrofit 라이브러리를 추가해준다.
buildFeatures {
dataBinding true
}
dependencies {
def lifecycle_version = "2.2.0"
// https://github.com/square/retrofit
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:retrofit-mock:2.9.0'
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
//lifecycle
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"
}
2. fragment_search.xml
기존의 레이아웃과 달리 루트태그를 으로 설정하여 전체 레이아웃파일을 감싸준다.
태그는 하나의 레이아웃만 자식뷰로 가질 수 있으며, 두 개 이상의 레이아웃을 자식뷰로 선언할 경우 다음과 같은 오류가 발생한다.
data binding error **msg:Only one layout element** and one data
element are allowed.
fragment_search.xml 파일에 검색어를 입력할 EditText, 검색버튼으로 사용할 ImageView, 검색 결과를 받아올 RecyclerView를 추가해주었다.
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".search.SearchFragment">
<ImageView android:id="@+id/button_friends"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginEnd="28dp"
android:layout_marginRight="28dp"
android:background="@drawable/ic_people_selected"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText android:id="@+id/edittext_search"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="search"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.512"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.16"
app:layout_constraintWidth_percent="0.9" />
<ImageView android:id="@+id/button_search"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/ic_search_selected"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintTop_toTopOf="@id/edittext_search"
app:layout_constraintBottom_toBottomOf="@id/edittext_search"
app:layout_constraintEnd_toEndOf="@id/edittext_search"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview_search"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/edittext_search"
tools:listitem="@layout/item_search_result"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:orientation="vertical"
android:paddingHorizontal="16dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
2. SearchFragment.kt 세팅
프래그먼트에서 바인딩을 사용하는 방법은 액티비티에서 사용하는 방법과 약간 다르다.
class SearchFragment : Fragment() {
private lateinit var binding: FragmentSearchBinding
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? {
binding = FragmentSearchBinding.inflate(inflater, container, false)
return binding.root
}
// binding은 2가지 방법으로 사용할 수 있다.
// 1. binding = FragmentSearchBinding.inflate(inflater, container, false)
// 2. binding = DataBindingUtil.inflate(inflater, R.layout.fragment_search, container, false)
Activity에서 DataBinding 사용하는 방법
- binding = ActivityMainBinding.inflate(LayoutInflater.from(this))
- binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
3. Data Class 만들기
Daum 웹문서의 검색요청 결과(Response)는 다음과 같은 형식으로 받아오게 된다.
다음과 같은 형식으로 Response Data Class를 만들어준다.
(Documents데이터 클래스에 붙어있는 어노테이션들은 Room 라이브러리를 이용하여 로컬에 데이터를 저장하기 위해서 쓴 것이니 무시해도 된다. id 변수도 안써도 됨.)
data class SearchResponse(
val meta: Meta,
val documents: MutableList<Documents>
) {
data class Meta(
val total_count: Int,
val pageable_count: Int,
val is_end: Boolean
)
@Entity(tableName = "search_table")
data class Documents(
@PrimaryKey(autoGenerate = true)
var id: Int? = null,
@ColumnInfo(name = "datetime")
val datetime: String,
@ColumnInfo(name = "contents")
val contents: String,
@ColumnInfo(name = "title")
val title: String,
@ColumnInfo(name = "url")
val url: String
)
}
4. item_search.xml
RecyclerView에 들어갈 item을 만들어 준다. itme_search 또한 태그로 레이아웃 파일 전체를 감싸준다. 이후 태그를 사용하여 그안에 태그로 변수를 선언해준다.
Response로 넘어온 데이터 중에서 Documets의 Title, Contents, DateTime만 아이템으로 띄워줄 예정이기 때문에 다음과 같이 xml을 구성하였다.
<?xml version="1.0" encoding="utf-8"?>
<layout 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">
<data>
<variable
name="data"
type="com.example.recyclerviewdatabinding.search.SearchResponse.Documents" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/constraintlayout_item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/box_search_result"
android:paddingBottom="20dp">
<TextView android:id="@+id/textview_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textSize="25sp"
android:text="@{data.title}"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:text="Title"/>
<TextView android:id="@+id/textview_contents"
android:layout_width="match_parent"
android:layout_height="0dp"
android:textSize="18sp"
android:text="@{data.contents}"
android:layout_marginTop="10dp"
app:layout_constraintTop_toBottomOf="@id/textview_title"
app:layout_constraintStart_toStartOf="@id/textview_title"
tools:text="Contents"/>
<TextView
android:id="@+id/textview_datetime"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="10dp"
android:textSize="13sp"
android:text="@{data.datetime}"
android:inputType="datetime"
app:layout_constraintTop_toBottomOf="@id/textview_contents"
app:layout_constraintStart_toStartOf="@id/textview_contents"
tools:text="Date Time"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
5. API 인터페이스 구현
interface RetrofitInterface {
@GET("/v2/search/web")
fun searchInterface(@Header("Authorization") token: String, @Query("query") title: String): Call<SearchResponse>
}
6. Retrofit Service생성
object RetrofitClient {
private val BASE_URL = "https://dapi.kakao.com"
private fun getInstance(): Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
val retrofitInterface = getInstance().create(RetrofitInterface::class.java)
}
7. ViewModel 생성
아래의 코드에서는 Room라이브러리를 이용하기 위하여 AndroidViewModel로 만들어주었지만 그냥 파라미터가 없는 ViewModel()로 만들어주어도 된다.
AndroidViewModel은 Application을 상속받기 때문에 Memory Leak이 발생할 수 있다.
때문에 context작업이 필요한 경우가 아니라면 ViewModel() 사용을 권장한다.
ViewModel 안에 LiveData가 있는 모습이다. 객체의 값이 변경될 경우에는 MutableLiveData로 선언해준다.
class SearchViewModel(application: Application) : AndroidViewModel(application) {
private val _docsList = MutableLiveData<ArrayList<SearchResponse.Documents>>()
val docsList: LiveData<ArrayList<SearchResponse.Documents>>
get() = _docsList
fun changeDocsList(docsList: ArrayList<SearchResponse.Documents>){
_docsList.value = docsList
}
fun searchDocs(searchWord: String) {
var searchList = mutableListOf<SearchResponse.Documents>()
val token = "KakaoAK bccddec2477515123ee06ae249c39f95"
val call: Call<SearchResponse> = RetrofitClient.
retrofitInterface.searchInterface(
token,
searchWord
)
call.enqueue(object: Callback<SearchResponse>{
override fun onResponse(
call: Call<SearchResponse>,
response: Response<SearchResponse>
) {
response.takeIf { it.isSuccessful }
?.body()
?.let {
searchList = it.documents
changeDocsList(searchList as ArrayList<SearchResponse.Documents>)
} ?: Log.d("error: ", "${response.errorBody()}")
}
override fun onFailure(call: Call<SearchResponse>, t: Throwable) {
Log.d("error: ", t.toString())
}
})
}
}
8. RecyclerView Adapter 생성
아이템 클릭시 문서 url로 이동하도록 설정하기 위해 context를 파라미터로 넣어주었다.
ViewHolder를 inner class로 생성해주고 그 안에서 position값을 갖고 있는 onBind함수를 만들어준다.
data binding 라이브러리는 모듈 패키지에 BR이라는 클래스를 생성한다.
이 클래스에는 데이터 바인딩에 사용된 Resource의 Id가 포함되어 있다.
때문에 간편하게 데이터들을 리소스와 바인딩 시켜줄 수 있다.
class SearchAdapter(private val context: Context): RecyclerView.Adapter<SearchAdapter.SearchViewHolder>() {
var datas = mutableListOf<SearchResponse.Documents>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchViewHolder {
val binding = ItemSearchResultBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return SearchViewHolder(binding)
}
override fun onBindViewHolder(holder: SearchViewHolder, position: Int) {
holder.onBind(position)
holder.binding.constraintlayoutItem.setOnClickListener{
val openUrl = Intent(Intent.ACTION_VIEW, Uri.parse(datas[position].url))
ContextCompat.startActivity(context,openUrl, null)
}
}
override fun getItemCount() = datas.size
inner class SearchViewHolder(var binding: ItemSearchResultBinding): RecyclerView.ViewHolder(binding.root){
fun onBind(position: Int) {
binding.setVariable(BR.data, datas[position])
}
}
}
9. Search_Fragment에 ViewModel과 Adapter를 연결
private lateinit var viewModel: SearchViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
viewModel = ViewModelProvider(requireActivity()).get(SearchViewModel::class.java)
initRecyclerView(view)
searchDocs()
}
ViewModel을 선언하는 방법은 사용목적에 따라 다양하다.
초기화 코드에 아무것도 필요하지 않으면 아래와 같이 간단하게 사용할 수 있다.
//NewInstanceFactory 사용법 (안드로이드에서 기본적으로제공해주는 Factory Class)
private lateinit var viewModel: SearchViewModel
viewModel = ViewModelProvider(this, ViewModelProvider.NewInstanceFactory()).get(SearchViewModel::class.java)
//androidx.fragment:fragment-ktx 사용시
private val viewModel: SearchViewModel by viewModels()
파라미터가 있는 ViewModel객체의 인스턴스를 만들고 싶다면 ViewModelProvider.Factory를 구현하면 된다.
class HasParamViewModelFactory(private val param: String) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return if (modelClass.isAssignableFrom(HasParamViewModel::class.java)) {
HasParamViewModel(param) as T
} else {
throw IllegalArgumentException()
}
}
}
사용방법
class MainActivity : AppCompatActivity() {
private lateinit var hasParamViewModel: HasParamViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val sampleParam = "Ready Story"
hasParamViewModel = ViewModelProvider(this, HasParamViewModelFactory(sampleParam))
.get(HasParamViewModel::class.java)
}
}
Adapter를 연결
fun initRecyclerView(view: View) {
adapter = SearchAdapter(view.context)
binding.recyclerviewSearch.adapter = adapter
binding.recyclerviewSearch.addItemDecoration(VerticalItemDecoration(10))
}
검색 버튼을 눌렀을 때 ViewModel의 _docsList의 value를 변경시켜준다.
view model의 docsList가 변경되면 adapter의 data를 변경시켜준다.
fun searchDocs() {
binding.buttonSearch.setOnClickListener {
viewModel.searchDocs(binding.edittextSearch.text.toString())
viewModel.docsList.observe(viewLifecycleOwner, Observer {
adapter.datas = it
adapter.notifyDataSetChanged()
})
}
}'Android' 카테고리의 다른 글
| [Android] Naver Map API 띄우기 (kotlin) (0) | 2021.03.19 |
|---|---|
| Navigation으로 Fragment를 전환시켜보자! (0) | 2021.03.10 |
| Android Studio WebView (0) | 2020.09.21 |
| Android CheckBox (회원가입 화면) (0) | 2020.09.21 |
| ViewPager를 이용한 Fragment전환, TabLayout(2) - Indicator (0) | 2020.09.21 |