메인화면에서 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
}
MainRecyclerViewAdapter
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
등록
ListActivity 생성
Main 으로 부터
category
데이터를parcelable
형태로 데이터 전달 받는다.
category = intent.getParcelableExtra("item")
ViewPager
등록
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)
ListRecyclerViewAdapter 생성해서 바인딩 해준다.
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()
}
}
Category 데이터 클래스 등록
Parcelable
을 상속받아서 전달할 수 있게끔 해준다.
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)
}
ListActivity 전체 소스
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
생성
newInstance 는 Fragment 생성시 static 으로 데이터를 생성하는 방법중 하나
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 는 disposable 을 정리하기 위한 disposeBag 이라고 생각하면 된다. 즉 subscribe 한 후에 구독한 자원을 클리어 하기 위해서 사용된다.
compositeDisposable.clear()
제목 입력 후 등록 버튼 클릭
클릭과 동시 ProgressBar를 보여주기
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)
regiser()
전체 소스
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로 업데이트
정상적으로 받아서 UI 에 등록 완료 푸시
...
mView?.registerDone()
...
Progressbar 종료 한 후 업데이트
override fun registerDone() {
btnRegister.visibility = View.VISIBLE
registerPogressBar.visibility = View.GONE
Toast.makeText(this, "등록완료", Toast.LENGTH_SHORT).show()
finish()
}
주의 해야 할 부분
Source 인터페이스에서 네트워크와 파베 리포지토리에 등록할 공통 함수 구현
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'
FirebaseRepository by Source
RxFirestore 이용해서 등록 후
Completable
형태로 리턴받음RxFirebaseStorage 에 파일을 등록 하면
Maybe
로 리턴 받음Maybe
는 http://reactivex.io/RxJava/javadoc/io/reactivex/Maybe.html 에서 자세히 볼수 있다.값이 나올수도 있거나
null
이 올수 있다는 뜻이다.Single
과Maybe
과 차이점은Single
은null
값을 받을 경우 에러로 리턴된다.flatMapMaybe
은 옵저버블을 리턴시Maybe
로 리턴이 되는 경우이다.
object FirebaseRepository : Source {
val firestoreApp by lazy {
FirebaseFirestore.getInstance()
}
val firebaseStorage by lazy {
FirebaseStorage.getInstance("버킷 주소")
}
override fun register(store: Store): Completable {
val document = firestoreApp.collection(store.categoryName ?: "").document( store.id ?: throw Exception("Empty ID") )
return RxFirestore.setDocument(document, store)
}
override fun uploadImage(uri: Uri): Maybe<Uri>? {
val ref = firebaseStorage.reference.child(getUUID())
return RxFirebaseStorage.putFile(ref, uri)
.flatMapMaybe {
RxFirebaseStorage.getDownloadUrl(ref)
}
}
override fun getStores(type: String): Maybe<MutableList<Store>> {
val collectionRef = firestoreApp.collection(type)
return RxFirestore.getCollection(collectionRef, Store::class.java)
.doOnError {
Timber.e(it)
}
}
}
Firebase storage에 버킷 주소 꼭 확인 후 맞는 걸 넣어줘야 함
업체 등록 후 List 화면에서 정상적인 업체가 나오는지 확인
이미 등록한 내용에 대해서 잘 나오는 지 firestore 콘솔에 가서 직접 확인 필요
Presenter에서 getStores 를 호출
override fun getStores(type: String) {
val disposable = repository.getStores(type)
.subscribeBy(
onSuccess = {
Timber.d("list : $it")
mView?.updateList(it)
}
)
compositeDisposable.add(disposable)
}RepositoryImpl 에서 getStore 호출해서 firebase 에서 호출
fun getStores(type: String) : Maybe<MutableList<Store>>{
return FirebaseRepository.getStores(type)
}FirebaseRepository 에서 RxFirestore 이용해서 데이터를 가져와서 리턴함
override fun getStores(type: String): Maybe<MutableList<Store>> {
val collectionRef = firestoreApp.collection(type)
return RxFirestore.getCollection(collectionRef, Store::class.java)
}Presenter 에서 UI 단으로 업데이트 처리
...
mView?.updateList(it)
...ListFrag에서 adapter에서 리스트 처리
override fun updateList(items: MutableList<Store>) {
listRecylerViewAdapter.updateItem(items)
}Adapter에서 notifyDataSetChanged() (갱신) 호출
fun updateItem(items : List<Store>) {
this.items = items
notifyDataSetChanged()
}
이상으로 3주차 배달앱 클론 스터디 내용입니다.
다음시간에는(마지막시간) 복습 후에 마지막 메인쪽 왼쪽 서랍 메뉴 커스텀과 삭제 기능 및 디테일을 좀 더 다를 예정입니다.
'스터디 > Android' 카테고리의 다른 글
Android 스크린샷 adb를 통해서 가져오기 (0) | 2018.10.31 |
---|---|
[Android] 배달앱 클론 스터디 2주차 (0) | 2018.10.14 |
Android Webview FileUpload os별 설정 (0) | 2018.10.10 |
[Android] 배달앱 클론 스터디 1주차 (0) | 2018.10.10 |