배달앱 클론 3주차 내용
메인화면에서 ListActivity 로 이동
메인화면에 RecyclerView
를 등록 한 후에 GridLayoutManager
를 등록한 후 일것이다.
그럼 Adapter
에 클릭 이벤트를 등록해서 ListAcitivity
로 이동 처리 한다.
fun <T : RecyclerView.ViewHolder> T.listen(event: (position: Int, type: Int) -> Unit): T {
itemView.setOnClickListener {
event.invoke(adapterPosition, itemViewType)
}
return this
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(context).inflate(R.layout.main_recyclerview_item, parent, false)
return ViewHolder(view).listen{ pos, type ->
clickListener(items[pos])
}
}
ListActivity
생성 및 ViewPager
등록
category = intent.getParcelableExtra("item")
val pagerAdapter = TabPageAdapter(supportFragmentManager, items)
pager.adapter = pagerAdapter
pager.addOnPageChangeListener(TabLayout.TabLayoutOnPageChangeListener(tabLayout))
tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener{
override fun onTabReselected(tab: TabLayout.Tab?) {
}
override fun onTabUnselected(tab: TabLayout.Tab?) {
}
override fun onTabSelected(tab: TabLayout.Tab) {
pager.currentItem = tab.position
}
})
pager.currentItem = items.indexOf(category?.title)
class ListRecylerViewAdapter(
private var items: List<Store>,
private val context: Context,
private val clickListener : (item: Store) -> Unit
) : RecyclerView.Adapter<ListRecylerViewAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(context).inflate(R.layout.list_recyclerview_item, parent, false)
return ViewHolder(view).listen{ pos, type ->
clickListener(items[pos])
}
}
override fun getItemCount(): Int = items.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.title.text = items[position].name
val requestOptions = RequestOptions().apply{
this.placeholder(R.drawable.place_hoder_icon)
this.error(R.drawable.no_image)
this.circleCrop()
}
Glide.with(holder.itemView)
.setDefaultRequestOptions(requestOptions)
.load(items[position].thumbnail)
.into(holder.thumbnail)
}
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val title = view.list_title
val thumbnail = view.list_iv
}
fun updateItem(items : List<Store>) {
this.items = items
notifyDataSetChanged()
}
}
data class Category (
val no: Int,
val resId: Int,
val background: Int = R.drawable.bdt_btn_white,
val title: String = "",
val type: String = ""
) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.readInt(),
parcel.readInt(),
parcel.readInt(),
parcel.readString(),
parcel.readString()) {
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeInt(no)
parcel.writeInt(resId)
parcel.writeInt(background)
parcel.writeString(title)
parcel.writeString(type)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<Category> {
override fun createFromParcel(parcel: Parcel): Category {
return Category(parcel)
}
override fun newArray(size: Int): Array<Category?> {
return arrayOfNulls(size)
}
}
}
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
supportActionBar?.elevation = 0f
플로팅 액션 버튼 등록
탭 및 viewPager 등록
private fun initTab() {
items.forEach {
tabLayout.addTab(tabLayout.newTab().setText(it))
}
}
private fun initViewPager() {
val pagerAdapter = TabPageAdapter(supportFragmentManager, items)
pager.adapter = pagerAdapter
pager.addOnPageChangeListener(TabLayout.TabLayoutOnPageChangeListener(tabLayout))
tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener{
override fun onTabReselected(tab: TabLayout.Tab?) {
}
override fun onTabUnselected(tab: TabLayout.Tab?) {
}
override fun onTabSelected(tab: TabLayout.Tab) {
pager.currentItem = tab.position
}
})
pager.currentItem = items.indexOf(category?.title)
}
class ListActivity : AppCompatActivity() {
var category : Category? = null
lateinit var items : Array<String>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_list)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
supportActionBar?.elevation = 0f
category = intent.getParcelableExtra("item")
title = category?.title
fab.setOnClickListener {
startActivity(Intent(this, RegisterActivity::class.java))
}
items = resources.getStringArray(R.array.menus)
initTab()
initViewPager()
}
private fun initTab() {
items.forEach {
tabLayout.addTab(tabLayout.newTab().setText(it))
}
}
private fun initViewPager() {
val pagerAdapter = TabPageAdapter(supportFragmentManager, items)
pager.adapter = pagerAdapter
pager.addOnPageChangeListener(TabLayout.TabLayoutOnPageChangeListener(tabLayout))
tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener{
override fun onTabReselected(tab: TabLayout.Tab?) {
}
override fun onTabUnselected(tab: TabLayout.Tab?) {
}
override fun onTabSelected(tab: TabLayout.Tab) {
pager.currentItem = tab.position
}
})
pager.currentItem = items.indexOf(category?.title)
}
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
if(item?.itemId == android.R.id.home)
finish()
return super.onOptionsItemSelected(item)
}
}
ListFragment
, ListFragContract
, ListFragPresente
생성
companion object {
private const val ARG_PARAM = "type"
fun newInstance(type : String) : ListFrag {
val listFrag = ListFrag()
val args = Bundle()
args.putString(ARG_PARAM, type)
listFrag.arguments = args
return listFrag
}
}
Fragment 가 열리고 firestore 로 부터 카테고리에 맞는 리스트를 가져오기 위해서 presenter 에 getStories 를 호출한다.
arguments
?.getString(ARG_PARAM)
?.let{
mPresenter.getStores(it)
}
ListFrag 전체 소스
class ListFrag : BaseMvpFragment<ListFragContract.View, ListFragContract.Presenter>(), ListFragContract.View {
private var items : ArrayList<Store> = ArrayList()
override var mPresenter: ListFragContract.Presenter = ListFragPresenter()
lateinit var act : Activity
private lateinit var listRecylerViewAdapter : ListRecylerViewAdapter
companion object {
private const val ARG_PARAM = "type"
fun newInstance(type : String) : ListFrag {
val listFrag = ListFrag()
val args = Bundle()
args.putString(ARG_PARAM, type)
listFrag.arguments = args
return listFrag
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.frag_list, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
act = activity as Activity
initRecyclerView()
arguments
?.getString(ARG_PARAM)
?.let{
mPresenter.getStores(it)
}
}
private fun initRecyclerView() {
listRecyclerView.layoutManager = LinearLayoutManager(context)
listRecylerViewAdapter = ListRecylerViewAdapter(items, act, ::handleItem)
listRecyclerView.adapter = listRecylerViewAdapter
val itemDecoration = ItemOffsetDecoration(act, R.dimen.list_item_offset)
listRecyclerView.addItemDecoration(itemDecoration)
listRecyclerView.setEmptyView(empty_view)
}
fun handleItem(item: Store) {
}
override fun updateList(items: MutableList<Store>) {
listRecylerViewAdapter.updateItem(items)
}
}
RegisterActivity 생성
Register 화면에서 사진 및 내용을 등록하는게 목표
RegisterActivity 생성시 Contract, Presetenter 도 같이 생성
class RegisterActivity : BaseMvpActivity<RegisterContract.View, RegisterContract.Presenter>(), RegisterContract.View{
Presenter 생성
override var mPresenter: RegisterContract.Presenter = RegisterPresenter()
사진 및 데이터 Firestore
및 Firestorage
에 등록 흐름
사진 등록 버튼 이벤트 등록
피커 열기 (카메라, 갤러리) 후 완료후 이미지 뷰에 업데이트
RxImagePicker 이용해서 이미지 Uri 획득
제목 입력 후 등록 버튼 클릭
클릭과 동시 ProgressBar를 보여주기
Presenter에 register 함수 실행
ImageUpload 스트림 과 register 스트림을 조합
Repository에서 FirebaseRepositoy을 이용해서 리턴
FirebaseRepository 에서 RxFirestoreage 와 RxFirestore 를 이용해서 등록
Presenter에서 정상 입력 콜백 받아서 UI로 업데이트
Progressbar 종료 한 후 업데이트
등록 완료 메세지 보여주고 다시 리스트 화면으로 이동 하기
사진 등록 버튼 이벤트 등록
btnRegister.setOnClickListener {
registerProc()
}
btnGallery.setOnClickListener {
openImagePicker(Sources.GALLERY)
}
btnCamera.setOnClickListener {
openImagePicker(Sources.CAMERA)
}
피커 열기 (카메라, 갤러리) 후 완료후 이미지 뷰에 업데이트
RxImagePicker 이용해서 이미지 Uri 획득
이미지 피커를 열어서 카메라 또는 갤러리에서 이미지를 선택하게끔 한다.
RxImagePicker
를 이용해서 최종 선택된 Uri 를 가져온다.
Glide
은 서버, 로컬이미지를 안드로이드에 바인딩 하기 위한 라이버러리 중 하나이다. (추천)
fun openImagePicker(source: Sources) {
val disposable = RxImagePicker.with(fragmentManager).requestImage(source)
.subscribeBy(
onNext = {
this.uri = it
val options = RequestOptions()
options.circleCrop()
Glide.with(this)
.load(it)
.apply(options)
.into(thumbnail)
}
)
compositeDisposable.add(disposable)
}
compositeDisposable.clear()
제목 입력 후 등록 버튼 클릭
fun registerProc() {
btnRegister.visibility = View.GONE
registerPogressBar.visibility = View.VISIBLE
val store = Store(
name=tv_title.text.toString(),
categoryName = spinner.selectedItem.toString()
)
Timber.d("$store")
mPresenter.register(uri = this.uri, store = store)
}
Presenter
register
함수 실행
ImageUpload 스트림 과 register 스트림을 조합
ImageUpload Stream -> Firebase storeage
val imageObservable : Maybe<Uri>? = uri?.let{
repository.uploadImage(uri)
}
Register Stream -> Firestore
val registerObservable = imageObservable
?.map {
store.apply {
this.thumbnail = it.toString()
}
}
?.flatMapCompletable(::registerProc)
?:registerProc(store)
override fun register(uri : Uri?, store: Store) {
store.apply {
id = getUUID()
}
val imageObservable : Maybe<Uri>? = uri?.let{
repository.uploadImage(uri)
}
val registerObservable = imageObservable
?.map {
store.apply {
this.thumbnail = it.toString()
}
}
?.flatMapCompletable(::registerProc)
?:registerProc(store)
val disposable = registerObservable
.subscribeBy(
onComplete = {
mView?.registerDone()
}
)
compositeDisposable.add(disposable)
}
fun registerProc(store : Store): Completable {
return repository.register(store)
}
Repository에서 FirebaseRepositoy을 이용해서 리턴
Image 등록
fun uploadImage(uri: Uri): Maybe<Uri>? {
return FirebaseRepository.uploadImage(uri)
}
Firestore 에 이미지 링크와 데이터 등록
fun register(store: Store) : Completable {
return FirebaseRepository.register(store)
}
FirebaseRepository 에서 RxFirestoreage 와 RxFirestore 를 이용해서 등록
FirebaseRepository 에서 uploadImage 함수 구현
override fun uploadImage(uri: Uri): Maybe<Uri>? {
val ref = firebaseStorage.reference.child(getUUID())
return RxFirebaseStorage.putFile(ref, uri)
.flatMapMaybe {
RxFirebaseStorage.getDownloadUrl(ref)
}
}
Firebase firestore 에 데이터 입력
override fun register(store: Store): Completable {
val document = firestoreApp.collection(store.categoryName ?: "").document( store.id ?: throw Exception("Empty ID") )
return RxFirestore.setDocument(document, store)
}
Presenter에서 정상 입력 콜백 받아서 UI로 업데이트
...
mView?.registerDone()
...
Progressbar 종료 한 후 업데이트
override fun registerDone() {
btnRegister.visibility = View.VISIBLE
registerPogressBar.visibility = View.GONE
Toast.makeText(this, "등록완료", Toast.LENGTH_SHORT).show()
finish()
}
주의 해야 할 부분
interface Source {
fun getConvertedAddr(lat : Double, lng : Double ) : Observable<Address>
fun register(store: Store) : Completable
fun uploadImage(uri: Uri) : Maybe<Uri>?
fun getStores(type: String) : Maybe<MutableList<Store>>
}
rxfirestore 및 rxfirebaseStorage 디펜시즈 추가
//RxFirebase
implementation 'com.github.FrangSierra:RxFirebase:1.5.0'
implementation 'com.oakwoodsc.rxfirestore:rxfirestore:1.1.0'
implementation 'com.oakwoodsc.rxfirestore:rxfirestorekt:1.1.0'
Firebase storage에 버킷 주소 꼭 확인 후 맞는 걸 넣어줘야 함
업체 등록 후 List 화면에서 정상적인 업체가 나오는지 확인
이상으로 3주차 배달앱 클론 스터디 내용입니다.
다음시간에는(마지막시간) 복습 후에 마지막 메인쪽 왼쪽 서랍 메뉴 커스텀과 삭제 기능 및 디테일을 좀 더 다를 예정입니다.