adb shell screencap -p /sdcard/screen.png
adb pull /sdcard/screen.png
adb shell rm /sdcard/screen.png

출처 : https://blog.shvetsov.com/2013/02/grab-android-screenshot-to-computer-via.html

배달앱 클론 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
}
  • 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()

사진 및 데이터 FirestoreFirestorage 에 등록 흐름

  1. 사진 등록 버튼 이벤트 등록

  2. 피커 열기 (카메라, 갤러리) 후 완료후 이미지 뷰에 업데이트

  3. RxImagePicker 이용해서 이미지 Uri 획득

  4. 제목 입력 후 등록 버튼 클릭

  5. 클릭과 동시 ProgressBar를 보여주기

  6. Presenter에 register 함수 실행

  7. ImageUpload 스트림 과 register 스트림을 조합

  8. Repository에서 FirebaseRepositoy을 이용해서 리턴

  9. FirebaseRepository 에서 RxFirestoreage 와 RxFirestore 를 이용해서 등록

  10. Presenter에서 정상 입력 콜백 받아서 UI로 업데이트

  11. Progressbar 종료 한 후 업데이트

  12. 등록 완료 메세지 보여주고 다시 리스트 화면으로 이동 하기

사진 등록 버튼 이벤트 등록

    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)
    }
  1. 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)
    }
  1. Presenter에서 정상 입력 콜백 받아서 UI로 업데이트

  • 정상적으로 받아서 UI 에 등록 완료 푸시

    ...
   mView?.registerDone()
  ...    
  1. 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 로 리턴 받음

      • Maybehttp://reactivex.io/RxJava/javadoc/io/reactivex/Maybe.html 에서 자세히 볼수 있다.

      • 값이 나올수도 있거나 null 이 올수 있다는 뜻이다.

      • SingleMaybe 과 차이점은 Singlenull 값을 받을 경우 에러로 리턴된다.

      • 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 콘솔에 가서 직접 확인 필요

    1. Presenter에서 getStores 를 호출

          override fun getStores(type: String) {
             val disposable = repository.getStores(type)
                .subscribeBy(
                     onSuccess = {
                         Timber.d("list : $it")
                         mView?.updateList(it)
                    }
                )

             compositeDisposable.add(disposable)
        }
    2. RepositoryImpl 에서 getStore 호출해서 firebase 에서 호출

          fun getStores(type: String) : Maybe<MutableList<Store>>{
             return FirebaseRepository.getStores(type)
        }
    3. FirebaseRepository 에서 RxFirestore 이용해서 데이터를 가져와서 리턴함

      override fun getStores(type: String): Maybe<MutableList<Store>> {
         val collectionRef = firestoreApp.collection(type)
         return RxFirestore.getCollection(collectionRef, Store::class.java)
      }
    4. Presenter 에서 UI 단으로 업데이트 처리

      ...
         mView?.updateList(it)
      ...
    5. ListFrag에서 adapter에서 리스트 처리

          override fun updateList(items: MutableList<Store>) {
             listRecylerViewAdapter.updateItem(items)
        }
    6. Adapter에서 notifyDataSetChanged() (갱신) 호출

          fun updateItem(items : List<Store>) {
             this.items = items
             notifyDataSetChanged()
        }

이상으로 3주차 배달앱 클론 스터디 내용입니다.

다음시간에는(마지막시간) 복습 후에 마지막 메인쪽 왼쪽 서랍 메뉴 커스텀과 삭제 기능 및 디테일을 좀 더 다를 예정입니다.



2주차 배달앱 강의 스터디 수업 내용

1주차 복습 내용

http://javaexpert.tistory.com/971?category=720102

2주차 클론 수업 내용

  1. Category 데이터 클래스 구현
  2. List<Category> 구현
  3. 리사이클러뷰를 그리드 레이아웃 매니저로 구현 및 데이터 연결
  4. GPS 기능 켜져있는지 체크
  5. 유저 위치 가져오기 위해 퍼미션 작업
  6. Presenter 함수 구현
  7. Address Object Data 클래스 구현
  8. 퍼미션 승인 후 트래킹 진행
  9. 좌표를 가져온 후 네이버 지오코딩 api 를 통해 자세한 동을 가져온다.
  10. Retrofit Api 구현
  11. converter-moshi 를 통해 JSON -> Address Class 로 변환
  12. RxJava Converter 구현
  13. RxJava Subscribe 를 통해 화면에 푸시
  14. 최종적으로 화면에 동이름을 뿌리도록 진행

Category 데이터 클래스 구현

  • resId 는 아이템에 들어갈 이미지 drawable 위치
  • background 는 아이템 뒤에 들어갈 백그라운드 이미지 ( drawable xml 이미지)
  • type 은 서브 페이지로 넘길때 필요한 파라미터
data class Category (
        val no: Int,
        val resId: Int,
        val background: Int = R.drawable.bdt_btn_white,
        val title: String = "",
        val type: String = ""
)

리사이클러뷰를 그리드 레이아웃 매니저로 구현

  • content_main.xml 에서 RecyclerView 를 구현
   <android.support.v7.widget.RecyclerView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/mainRecyclerView"
        android:clipToPadding="false"
        android:padding="@dimen/item_offset"
        />

List<Category> 구현해서 리스트에 데이터로 연결

  • 메인화면에 표시될 배열 미리 정의
    val items = arrayOf(
            Category(0, R.drawable.bdt_home_a_ctgr_all, title = "전체"),
            Category(1, R.drawable.bdt_home_a_ctgr_seasonal, R.drawable.bdt_btn_brown, title = "계절메뉴"),
            Category(2, R.drawable.bdt_home_a_ctgr_chicken, title = "치킨"),
            Category(3, R.drawable.bdt_home_a_ctgr_chinese, title = "중식"),
            Category(4, R.drawable.bdt_home_a_ctgr_pizza, title = "피자"),
            Category(5, R.drawable.bdt_home_a_ctgr_bossam, title = "족발,보쌈"),
            Category(6, R.drawable.bdt_home_a_ctgr_burger, title = "도시락"),
            Category(7, R.drawable.bdt_home_a_ctgr_japanese, title = "일식")
    )

리사이클러뷰를 그리드 레이아웃 매니저로 구현 및 데이터 연결

    ...
    private fun initRecyclerView() {
        mainRecyclerView.layoutManager = GridLayoutManager(getContext(), 2)
        mainRecyclerView.adapter = MainRecylerViewAdapter(items, getContext(), ::handleItem)
        val itemDecoration = ItemOffsetDecoration(getContext(), R.dimen.item_offset)
        mainRecyclerView.addItemDecoration(itemDecoration)
    }

GPS 기능 켜져있는지 체크

  • 기능이 꺼져있는 경우 강제로 GPS 설정화면으로 이동시킴
fun checkGPS() {
        val service = getSystemService(Context.LOCATION_SERVICE) as LocationManager
        val enabled = service
                .isProviderEnabled(LocationManager.GPS_PROVIDER)
        
        if (!enabled) {
            val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)
            startActivity(intent)
        }
    }

유저 위치 가져오기 위한 퍼미션 체크

    private fun checkPermission() {

        if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_FINE_LOCATION)
                == PackageManager.PERMISSION_GRANTED) {
            updateMyLocation()
        } else {
            ActivityCompat.requestPermissions(this,arrayOf(android.Manifest.permission.ACCESS_FINE_LOCATION), 1);
        }
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
        if (permissions.size == 1 &&
                permissions[0] == android.Manifest.permission.ACCESS_FINE_LOCATION &&
                grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
                return
            }
        }else {
            updateMyLocation()
        }
    }

위치 가져온 후 내 위치 트래킹

  • 정상적으로 트래킹 후 Presenter로 한국 주소를 얻기 위해 좌표를 보냄
fun updateMyLocation() {
        locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
        val criteria = Criteria()
        // 정확도
        criteria.setAccuracy(Criteria.NO_REQUIREMENT);
        // 전원 소비량
        criteria.setPowerRequirement(Criteria.NO_REQUIREMENT);
        // 고도, 높이 값을 얻어 올지를 결정
        criteria.setAltitudeRequired(false);
        // provider 기본 정보(방위, 방향)
        criteria.setBearingRequired(false);
        // 속도
        criteria.setSpeedRequired(false);
        // 위치 정보를 얻어 오는데 들어가는 금전적 비용
        criteria.setCostAllowed(true);

        provider = locationManager.getBestProvider(criteria, false);
        val location = locationManager.getLastKnownLocation(provider);
        if(location != null) {
            Log.d("KTH","${location.latitude},${location.longitude}")
            mPresenter.getAddr(lng = location.longitude, lat = location.latitude)
        }
    }

Presenter 구현

  • 좌표를 repository 에 다시 보내서 Observable<Address> 형태로 리턴 받음
  • compositeDisposable 는 메모리릭을 위해 구독된 disposable 을 detach 때 제거를 해준다.
override fun getAddr(lng: Double, lat: Double) {        
        val disposable = repository.convertAddr(lng, lat)
                .observeOn(AndroidSchedulers.mainThread())
                .subscribeBy(
                        onNext = {                            
                            mView?.updateAddress(it.items[0].addressDetail.dongmyun)
                        },
                        onError = {
                            it.printStackTrace()
                        }
                )

        compositeDisposable.add(disposable)
    }

Adress 데이터 클래스 구현

  • 서버로 부터 결과값을 가져온 후 Address 데이터로 변환하기 위해 필요하다.
import com.squareup.moshi.Json

data class ResponseAddress(
        @field:Json(name = "result") val result : Address
)

data class Address (
        @field:Json(name = "total") val total : Int,
        @field:Json(name = "items") val items : List<AddressItem>
)

data class AddressItem (
        @field:Json(name = "address") val address : String,
        @field:Json(name = "addrdetail") val addressDetail : AddressDetail
)

data class AddressDetail (
        @field:Json(name = "country") val country : String,
        @field:Json(name = "sido") val sido : String,
        @field:Json(name = "sigugun") val sigugun : String,
        @field:Json(name = "dongmyun") val dongmyun : String,
        @field:Json(name = "rest") val rest : String
)

Source 클래스 구현

  • Repository 에서 구현될 모델 클래스들의 공통적인 함수 모음
interface Source {
    
    fun getConvertedAddr(lat : Double, lng : Double ) : Observable<Address>

}
  • FirebaseRepository by Source
class FirebaseRepository : Source{
    val firestoreApp by lazy {
        FirebaseFirestore.getInstance()
    }
    
    override fun getConvertedAddr(lat: Double, lng: Double) : Observable<Address> {
        TODO("not implemented") //To change body of created functions use File | Settings | File
    }
}
  • NetworkRepository by Source
object NetworkRepsitory : Source {
    
    override fun getConvertedAddr(lng: Double, lat: Double): Observable<Address> {
        //129.075090,35.179632
        return ApiManager.getAddressFromLatLng(lng, lat)
    }
}
  • RepositoryImpl 를 통해서 Presenter와 통신한다.
    • NetworkRepository 에서 주소를 변환하도록 지시
class RepositoryImpl {

    fun convertAddr(lng: Double, lat: Double): Observable<Address> = NetworkRepsitory.getConvertedAddr(lng, lat)
}

Retrofit API Manager

  • Retrofit Builder 구현
object BaseApiManager {
    fun initRetrofit(Server : String): Retrofit {
        val interceptor = HttpLoggingInterceptor().apply {
            level = HttpLoggingInterceptor.Level.BODY
        }

        val client = OkHttpClient.Builder().apply {
            networkInterceptors().add(Interceptor { chain ->
                val original = chain.request()
                val request = original.newBuilder()
                        .method(original.method(), original.body())
                        .build()
                chain.proceed(request)
            })
            addInterceptor(interceptor)
        }

        return Retrofit.Builder().baseUrl(Server)
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .addConverterFactory(BaseApiManager.createMoshiConverter())
                .client(client.build())
                .build()
    }

    private fun createMoshiConverter(): MoshiConverterFactory = MoshiConverterFactory.create()
  • 중요한 부분

    • RxJava2CallAdapterFactory 를 통해서 컨버팅 된 결과물을 Observable 를 스트림으로 바꿔준다.
    • BaseApiManager.createMoshiConverter() 을 통해 GSON 컨버팅을 진행한다.
            return Retrofit.Builder().baseUrl(Server)
                  .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                  .addConverterFactory(BaseApiManager.createMoshiConverter())
                  .client(client.build())
                  .build()

네이버 지코코딩을 통해 좌표 -> 주소로 변환

  • subscribeOn 은 RxJava 구독 시작시 쓰레드를 결정시킨다.
  • Network 를 사용시 io 쓰레드 이용해서 통신
  • 결과물은 Observable<Address> 으로 리턴된다.
object ApiManager {
    private const val BASE_SERVER: String = "https://openapi.naver.com/v1/map/"
    var retrofit: Retrofit

    init {        
        retrofit = BaseApiManager.initRetrofit(BASE_SERVER)
    }

    fun getAddressFromLatLng(lng: Double, lat: Double) : Observable<Address> {
        val addressService = retrofit.create(AddressService::class.java)
        
        return addressService.getAddress(query = "$lat,$lng")
                .map{ it.result } //ResponseAddress -> Address
                .subscribeOn(Schedulers.io())
    }
}

API 인터페이스를 통해서 통신 처리

  • ResponseAddress -> Address -> AddressItem -> AddressDetail 으로 오브젝트들을 변환시킨다.
interface AddressService {

    /*
    위도 : latitude
    경도 : longtitude
    "https://openapi.naver.com/v1/map/reversegeocode?encoding=utf-8&coordType=latlng&query=";
     */
    @Headers(
            "X-Naver-Client-Id:네이버API",
            "X-Naver-Client-Secret:네이버API"
    )
    @GET("reversegeocode?encoding=utf-8&coordType=latlng")
    fun getAddress(@Query("query") query : String) : Observable<ResponseAddress>
}

Presenter로 통해서 결과값을 UI 로 처리

  • observeOn(AndroidSchedulers.mainThread()) 을 통해서 해당 밑으로는 Android UI Thread 로 처리
  • mView 를 통해서 updateAddress 해줌으로써 데이터를 가져와서 UI 로 올려보내는 작업은 끝났다.
override fun getAddr(lng: Double, lat: Double) {        
        val disposable = repository.convertAddr(lng, lat)
                .observeOn(AndroidSchedulers.mainThread())
                .subscribeBy(
                        onNext = {                            
                            mView?.updateAddress(it.items[0].addressDetail.dongmyun)
                        },
                        onError = {
                            it.printStackTrace()
                        }
                )

        compositeDisposable.add(disposable)
    }

RecyclerViewAdapter 이벤트 리스너 구현

  • Adapter에 리스너 붙이는 작업
  • Kotlin 확장함수를 통해서 ViewHolder 클래스를 이용해서 listen 함수 구현
fun <T : RecyclerView.ViewHolder> T.listen(event: (position: Int, type: Int) -> Unit): T {
    itemView.setOnClickListener {
        event.invoke(adapterPosition, itemViewType)
    }
    return this
}
  • Adapter 부분
    • clickListener는 MainActivity에서 콜백 구현
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainRecylerViewAdapter.ViewHolder {
        val view = LayoutInflater.from(context).inflate(R.layout.main_recyclerview_item, parent, false)
        return ViewHolder(view).listen{ pos, type ->
            clickListener(items[pos])
        }
    }
  • MainActivity
    • MainActivity 에서 아이템 클릭시 리스트 화면으로 넘어가기 위해 필요
...
mainRecyclerView.adapter = MainRecylerViewAdapter(items, getContext(), ::handleItem)
...


fun handleItem(item: Category) {
    startActivity(
        Intent(this, ListActivity::class.java).apply {
            this.putExtra("item", item)
        }
    )
}

이상으로 2주차 부산 배달앱 클론앱 스터디 내용이었습니다.

다음주에는 3주차로 리스트 화면 상세 구현 및 등록 등을 진행할 예정입니다.

Android 에서 WebView사용시 정말 XX 한게 파일 업로드가 아닐수 없다. 

이부분에서 괜찮은 소스가 보여서 공유 해본다. 

webView.setWebChromeClient(new WebChromeClient()
        {
            //The undocumented magic method override
            //Eclipse will swear at you if you try to put @Override here
            // For Android 3.0+
            public void openFileChooser(ValueCallback<Uri> uploadMsg) {

                mUploadMessage = uploadMsg;
                Intent i = new Intent(Intent.ACTION_GET_CONTENT);
                i.addCategory(Intent.CATEGORY_OPENABLE);
                i.setType("image/*");
                activity.startActivityForResult(Intent.createChooser(i,"File Chooser"), FILECHOOSER_RESULTCODE);

            }

            // For Android 3.0+
            public void openFileChooser( ValueCallback uploadMsg, String acceptType ) {
                mUploadMessage = uploadMsg;
                Intent i = new Intent(Intent.ACTION_GET_CONTENT);
                i.addCategory(Intent.CATEGORY_OPENABLE);
                i.setType("*/*");
               activity.startActivityForResult(
                        Intent.createChooser(i, "File Browser"),
                        FILECHOOSER_RESULTCODE);
            }

            //For Android 4.1
            public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture){
                mUploadMessage = uploadMsg;
                Intent i = new Intent(Intent.ACTION_GET_CONTENT);
                i.addCategory(Intent.CATEGORY_OPENABLE);
                i.setType("image/*");
                activity.startActivityForResult( Intent.createChooser( i, "File Chooser" ), FILECHOOSER_RESULTCODE );

            }

            //For Android 5.0+
            public boolean onShowFileChooser(
                    WebView webView, ValueCallback<Uri[]> filePathCallback,
                    FileChooserParams fileChooserParams){
                if(mUploadMessageArray != null){
                    mUploadMessageArray.onReceiveValue(null);
                }
                mUploadMessageArray = filePathCallback;

                Intent contentSelectionIntent = new Intent(Intent.ACTION_GET_CONTENT);
                contentSelectionIntent.addCategory(Intent.CATEGORY_OPENABLE);
                contentSelectionIntent.setType("*/*");
                Intent[] intentArray;
                intentArray = new Intent[0];

                Intent chooserIntent = new Intent(Intent.ACTION_CHOOSER);
                chooserIntent.putExtra(Intent.EXTRA_INTENT, contentSelectionIntent);
                chooserIntent.putExtra(Intent.EXTRA_TITLE, "Image Chooser");
                chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, intentArray);
                activity.startActivityForResult(chooserIntent, FILECHOOSER_RESULTCODE);
                return true;
            }
        });


Android 배달앱 클론 스터디


부산에서 진행한 배달앱 클론을 진행하면서 공부한 내용을 정리 ( 1주차 )

목표

Android로 배달 앱을 클론 하면서 개발하는 방법과 패턴, RxJava등 사용법을 배우게 된다.

MVP flow

mvp flow mvp flow

Base MVP

View

화면 단위를 뜻한다. Fragment, Adapter, Activity

Model

데이터 연결 하는 부분을 담당한다. Sqlite, Retrofit등

Presenter

View <-> Model 간의 연결을 도와주는 역할을 한다.

Base 작성

BaseMvpView

interface BaseMvpView {

    fun getContext(): Context

    fun showError(error: String?)

    fun showError(@StringRes stringResId: Int)

    fun showMessage(@StringRes strResId: Int)

    fun showMessage(message: String)
}

BaseMvpPresenter

interface BaseMvpPresenter<in V : BaseMvpView> {

    fun attachView(view: V)

    fun detachView()
}

BaseMvpPresenerImpl

  • BaseMvpPresenter를 상속받아서 구현합니다.
open class BaseMvpPresenterImpl<T: BaseMvpView> : BaseMvpPresenter<T> {

    protected var mView: T? = null

    override fun attachView(view: T) {
        mView = view
    }

    override fun detachView() {
        mView = null
    }

}

BaseMvpActivity

abstract class BaseMvpActivity<in V : BaseMvpView, T : BaseMvpPresenter<V>>
    : AppCompatActivity(), BaseMvpView {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mPresenter.attachView(this as V)

        title = ""
    }

    override fun getContext(): Context = this

    protected abstract var mPresenter: T

    override fun showError(error: String?) {
        Toast.makeText(this, error, Toast.LENGTH_LONG).show()
    }

    override fun showError(stringResId: Int) {
        Toast.makeText(this, stringResId, Toast.LENGTH_LONG).show()
    }

    override fun showMessage(srtResId: Int) {
        Toast.makeText(this, srtResId, Toast.LENGTH_LONG).show()
    }

    override fun showMessage(message: String) {
        Toast.makeText(this, message, Toast.LENGTH_LONG).show()
    }

    override fun onDestroy() {
        super.onDestroy()
        mPresenter.detachView()
    }
}

그럼 간단한 앱을 만들어보자. 

전체적인 흐름은 다음과 같다.

  1. mvp 코어들을 상속받은 메인 화면을 만든다.
  2. 그안에 버튼을 삽입한다.
  3. 버튼클릭시 카운트가 하나씩 올라간다.
  4. 카운트는 View -> Presenter -> Model 로 내려갔다가 반대로 View까지 푸시되어서 화면이 update 되는 과정을 거친다.
  5. 카운트는 Firebase 에서 제공해주는 Firestore를 이용한다.

MVP 코어들을 이용해서 메인 화면을 만든다. 

  • MainContract
  • MainPresenter
  • MainActivity

MainContract를 만든다.

  • object 클래스는 싱글톤 클래스를 뜻한다.
object MainContract {
    interface View : BaseMvpView {
        fun updateView(count : Int)
    }

    interface Presenter : BaseMvpPresenter<View> {
        fun addCount(count : Int)
    }
}

MainPresenter를 만든다.

  • BaseMvpPresenterImpl를 상속받아서 만드는 걸 유의하자.
  • repository 패턴을 이용한다.
  • addCount 를 통해서 repository.addCount 를 통해서 값을 증가 시킨다.
  • 그리고 repository 를 통해서 값을 가져온다.
    • 일부러 버그 유발 코드를 만들어서 오류를 확인해본다..
  • 마지막으로 화면에 업데이트 한다.
    • 지금 구조에서는 값이 실시간으로 파베에 저장이 되고 가져오질 않는다.
    • 비동기라는 걸 이해하자.
class MainPresenter : BaseMvpPresenterImpl<MainContract.View>(), MainContract.Presenter {

    private val repository : Repository by lazy {
        RepositoryImpl()
    }

    override fun addCount(count: Int) {
        repository.addCount(count)

        val result = repository.getCount()

        mView?.updateView(result)
    }
}

MainActivity를 만든다.

  • BaseMvpActivity 를 상속받는다.
  • MainContract.View 를 상속받아서 updateView를 구현하도록 한다.
  • BaseMvpActivity에서 추상화된 mPresenter 인스턴스를 생성하게끔 한다.
  • 버튼을 클릭시 mPresenter.addCount(Integer.parseInt(resultTxt.text.toString())) 를 통해서 presenter로 값을 추가한다.
  • updateView 를 통해서 View 화면을 업데이트 하도록 한다.
class MainActivity : BaseMvpActivity<MainContract.View, MainContract.Presenter>(), MainContract.View, NavigationView.OnNavigationItemSelectedListener {

    override var mPresenter: MainContract.Presenter = MainPresenter()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        setSupportActionBar(toolbar)

        val toggle = ActionBarDrawerToggle(
                this, drawer_layout, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close)
        drawer_layout.addDrawerListener(toggle)
        toggle.syncState()

        nav_view.setNavigationItemSelectedListener(this)

        button.setOnClickListener {
            mPresenter.addCount(Integer.parseInt(resultTxt.text.toString()))
        }
    }

    override fun onBackPressed() {
        if (drawer_layout.isDrawerOpen(GravityCompat.START)) {
            drawer_layout.closeDrawer(GravityCompat.START)
        } else {
            super.onBackPressed()
        }
    }

    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        // Inflate the menu; this adds items to the action bar if it is present.
        menuInflater.inflate(R.menu.main, menu)
        return true
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        when (item.itemId) {
            R.id.action_settings -> return true
            else -> return super.onOptionsItemSelected(item)
        }
    }

    override fun onNavigationItemSelected(item: MenuItem): Boolean {
        // Handle navigation view item clicks here.
        when (item.itemId) {
            R.id.nav_camera -> {
                // Handle the camera action
            }
            R.id.nav_gallery -> {

            }
            R.id.nav_slideshow -> {

            }
            R.id.nav_manage -> {

            }
            R.id.nav_share -> {

            }
            R.id.nav_send -> {

            }
        }

        drawer_layout.closeDrawer(GravityCompat.START)
        return true
    }

    override fun updateView(count: Int) {
        resultTxt.text = count.toString()
    }
}

Firebase 에 있는 Firestore를 이용해 보자.

  1. https://firebase.google.com/ 를 통해서 로그인 후 프로젝트를 만든다.
  2. 콘솔로 가서 프로젝트 설정을 통해서 앱 추가를 한다.
  3. 패키지명을 적어주고 google-services.json 파일을 받는다.
  4. 다시 프로젝트로 와서 app 폴더에 방금 받은 파일을 추가해준다.
  5. https://firebase.google.com/docs/android/setup?authuser=0 을 참고해서 dependencies를 추가한다.
  6. firebase-core 를 꼭 추가해주는 걸 잊지 말자.
  7. firestore 문서로 가서 android 를 클릭해서 라이버러리를 추가해준다.
    1. compile 'com.google.firebase:firebase-firestore:15.0.0 을 추가 해주는 걸 잊지 말자.

Model 부분 추가

Repository 를 추가해보자.

  1. Repository 사용될 함수들 인터페이스 정의
  2. RepositoryImpl 구현
  3. FirebaseRepository 를 구현

Repository

interface Repository {

    fun addCount(count: Int)
    fun getCount() : Int

}

RepositoryImpl

class RepositoryImpl : Repository{

    val firebaseRepo : Repository by lazy {
        FirebaseRepository()
    }

    var result = 0

    override fun addCount(count: Int) {
        result = count + 1

        firebaseRepo.addCount(result);

    }

    override fun getCount() : Int {
        return result
    }
}

FirebaseRepository

  • map 형태로 넣을 수 있다.
  • 콜백형태로 값을 핸들링 가능핟.
class FirebaseRepository : Repository{
    val firestoreApp by lazy {
        FirebaseFirestore.getInstance()
    }

    override fun addCount(count: Int) {
        val map = mapOf("value" to count)

        firestoreApp.collection("test").document("count")
        .set(map)
        .addOnSuccessListener {
            Log.d("firebase", "success add with $count")
        }
        .addOnFailureListener{
            Log.d("firebase", "Error adding count")
        }

    }

    override fun getCount() : Int {
        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
    }
}


+ Recent posts