경험의 기록

※ 인스타그램 클론을 제작하면서 공부한 내용을 정리한 글입니다.

 

  • Reformat Code ( 코드 정렬 )
  • TextInputLayout
  • Firebase 연결
  • Google 계정 로그인 연결
  • Facebook 계정 로그인 연결
  • ConstrainedWidth, ConstrainedHeight
  • Firebase Storage에 이미지 업로드
  • Firestore 데이터베이스 추가
  • Cannot fit requested classes in a single dex file 오류
  • Snapshot remove
  • 대표화면(Splash 화면) 만들기
  • 푸시 라이브러리를 활용한 푸시이벤트 만들기

 

 

github.com/HanYeop/Stagram

 

HanYeop/Stagram

Instagram Clone. Contribute to HanYeop/Stagram development by creating an account on GitHub.

github.com

 


▶ Reformat Code ( 코드 정렬 )

더보기

이렇게 복잡하게 되어있는 코드들을

Code > Reformat Code 누르면 정렬할 수 있다.


▶ TextInputLayout

더보기

TextInputLayout은 TextInputEditText에 입력된 텍스트에 반응하는 레이아웃이다.

app 우클릭 > Open Module > Settings

+ 버튼> Library Dependency

material 검색, com.android.material 추가한다. (직접 추가해줘도 됨.)

com.google.android.material.textfield.TextInputLayout으로 텍스트를 감싸준다.


▶ Firebase 연결

더보기

Tools > Firebase 

Authentication 클릭하여 구글계정과 연결


▶ Google 계정 로그인 연결

더보기
var auth : FirebaseAuth? = null

액티비티에 FirebaseAuth을 추가해주고

auth = FirebaseAuth.getInstance()

Oncreate에서 FirebaseAuth의 인스턴스를 받아오는 코드를 추가해서

파이어베이스 연결 후

 

파이어베이스 콘솔에서 구글 로그인 사용설정으로 변경

 

우측의 Gradle > signingReport 더블클릭

결과창의 SHA1 값 복사하여

구글 프로젝트 설정 클릭

 

 

하단의 디지털 지문 추가 클릭하여 복사한 SHA1 값 추가

 

play-services-auth 검색하여 추가

 

    var googleSignInClient : GoogleSignInClient? = null
    var GOOGLE_LOGIN_CODE = 9001

액티비티에 GoogleSignInClient와 요청시 필요한 코드를 추가

 

    fun googleLogin(){
        var signInIntent = googleSignInClient?.signInIntent
        startActivityForResult(signInIntent,GOOGLE_LOGIN_CODE)
    }

구글 로그인 함수를 작성해주고

google_sign_in_button.setOnClickListener { googleLogin() }
        // 구글 로그인 버튼에 googleLogin 연결

       var gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
            .requestIdToken(getString(R.string.default_web_client_id))
            .requestEmail()
            .build()
        googleSignInClient = GoogleSignIn.getClient(this,gso)

로그인하기 위한 코드를 구성해준다.

    fun firebaseAuthWithGoogle(account : GoogleSignInAccount?){
        var credential = GoogleAuthProvider.getCredential(account?.idToken,null)
        auth?.signInWithCredential(credential)
            ?.addOnCompleteListener{
                    task ->
                if(task.isSuccessful){
                    // 아이디, 비밀번호 맞을 때
                    moveMainPage(task.result?.user)
                }else{
                    // 틀렸을 때
                    Toast.makeText(this,task.exception?.message,Toast.LENGTH_SHORT).show()
                }
            }
    }

그 후에 인증을 위한 firebaseAuthWithGoogle 함수를 구현

 

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if(requestCode == GOOGLE_LOGIN_CODE){
            var result = Auth.GoogleSignInApi.getSignInResultFromIntent(data)!!
            // 구글API가 넘겨주는 값 받아옴

            if(result.isSuccess) {
                var accout = result.signInAccount
                firebaseAuthWithGoogle(accout)
                Toast.makeText(this,"로그인 성공",Toast.LENGTH_SHORT).show()
            }
            else{
                Toast.makeText(this,"로그인 실패",Toast.LENGTH_SHORT).show()
            }
        }
    }

onActivityResult 함수를 오버라이드해서 구글 API가 받아온 값을 firebaseAuthWithGoogle 에 넘겨준다.

 

전체코드

class LoginActivity : AppCompatActivity() {
    var auth : FirebaseAuth? = null
    var googleSignInClient : GoogleSignInClient? = null
    var GOOGLE_LOGIN_CODE = 9001

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login)
        auth = FirebaseAuth.getInstance()

        google_sign_in_button.setOnClickListener { googleLogin() }
        // 구글 로그인 버튼에 googleLogin 연결

       var gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
            .requestIdToken(getString(R.string.default_web_client_id))
            .requestEmail()
            .build()
        googleSignInClient = GoogleSignIn.getClient(this,gso)
    }

    fun googleLogin(){
        var signInIntent = googleSignInClient?.signInIntent
        startActivityForResult(signInIntent,GOOGLE_LOGIN_CODE)
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if(requestCode == GOOGLE_LOGIN_CODE){
            var result = Auth.GoogleSignInApi.getSignInResultFromIntent(data)!!
            // 구글API가 넘겨주는 값 받아옴

            if(result.isSuccess) {
                var accout = result.signInAccount
                firebaseAuthWithGoogle(accout)
                Toast.makeText(this,"로그인 성공",Toast.LENGTH_SHORT).show()
            }
            else{
                Toast.makeText(this,"로그인 실패",Toast.LENGTH_SHORT).show()
            }
        }
    }

    fun firebaseAuthWithGoogle(account : GoogleSignInAccount?){
        var credential = GoogleAuthProvider.getCredential(account?.idToken,null)
        auth?.signInWithCredential(credential)
            ?.addOnCompleteListener{
                    task ->
                if(task.isSuccessful){
                    // 아이디, 비밀번호 맞을 때
                    moveMainPage(task.result?.user)
                }else{
                    // 틀렸을 때
                    Toast.makeText(this,task.exception?.message,Toast.LENGTH_SHORT).show()
                }
            }
    }

▶ Facebook 계정 로그인 연결

더보기

developers.facebook.com/?locale=ko_KR

 

Facebook for Developers

Facebook for Developers와 사용자를 연결할 수 있는 코드 인공 지능, 비즈니스 도구, 게임, 오픈 소스, 게시, 소셜 하드웨어, 소셜 통합, 가상 현실 등 다양한 주제를 둘러보세요. Facebook의 글로벌 개발

developers.facebook.com

접속 후 로그인

내앱 > 앱 만들기

 

대시보드 > Facebook 로그인 > 설정

안드로이드 클릭. 이제 이곳을 보고 코드를 작성할 것이다.

 

써있는대로

mavenCentral() 추가

 

implementation 'com.facebook.android:facebook-android-sdk:[4,5)' 추가

 

패키지명 추가해주기

 

    private fun getHashKey() {
        var packageInfo: PackageInfo? = null
        try {
            packageInfo =
                packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES)
        } catch (e: PackageManager.NameNotFoundException) {
            e.printStackTrace()
        }
        if (packageInfo == null) Log.e("KeyHash", "KeyHash:null")
        for (signature in packageInfo!!.signatures) {
            try {
                val md = MessageDigest.getInstance("SHA")
                md.update(signature.toByteArray())
                Log.d(
                    "KeyHash",
                    Base64.encodeToString(md.digest(), Base64.DEFAULT)
                )
            } catch (e: NoSuchAlgorithmException) {
                Log.e(
                    "KeyHash",
                    "Unable to get MessageDigest. signature=$signature",
                    e
                )
            }
        }
    }

페이스북 sdk 연동을 위해 해시값을 추가해주어야 하는데, 위 getHashKey 함수를 추가하고

oncreate에서 호출해줌으로써 해쉬값을 얻을 수 있다.

Logcat에서 KeyHash 태그로 검색해보면 해쉬값을 찾을 수 있다.

 

알아낸 해시값을 추가해준다.

 

허가해주고 넘어간다.

 

매뉴얼에 써있는대로 코드를 추가해준다.

파이어베이스 콘솔로 와서

리디렉션값 복사 후

 

페이스북 설정에서 리디렉션값에 추가

 

그 후 기본설정에서 ID와 시크릿코드를 복사하여

추가해줌으로써 사용 설정 할 수 있다.

 

 

이제 설정은 마쳤으니, 액티비티로 와서

var callbackManager : CallbackManager? = null

페이스북에서의 결과값을 받아올 콜백메소드를 선언해준다.

 

 

그 후 oncreate에서

callbackManager = CallbackManager.Factory.create()

선언

 

onActivityResult 에서

callbackManager?.onActivityResult(requestCode,resultCode,data)

추가, 결과값은 onActivityResult에 넘어오기 때문에 onActivityResult에서 콜백메소드로 넘겨준다.

 

 

fun facebookLogin(){
        LoginManager.getInstance()
            .logInWithReadPermissions(this, Arrays.asList("public_profile","email"))

        LoginManager.getInstance()
            .registerCallback(callbackManager, object : FacebookCallback<LoginResult>{
                override fun onSuccess(result: LoginResult?) {
                    // 로그인 성공시
                    handleFacebookAccessToken(result?.accessToken)
                    // 파이어베이스로 로그인 데이터를 넘겨줌
                }

                override fun onCancel() {

                }

                override fun onError(error: FacebookException?) {

                }
            })
    }

이제 페이스북 로그인 함수를 작성해준다.

로그인 성공시 페이스북 데이터를 파이어베이스로 넘겨준다.

 

fun handleFacebookAccessToken(token : AccessToken?){
        var credential = FacebookAuthProvider.getCredential(token?.token!!)

        auth?.signInWithCredential(credential)
            ?.addOnCompleteListener{
                    task ->
                if(task.isSuccessful){
                    // 아이디, 비밀번호 맞을 때
                    moveMainPage(task.result?.user)
                    Toast.makeText(this,"로그인 성공",Toast.LENGTH_SHORT).show()
                }else{
                    // 틀렸을 때
                    Toast.makeText(this,task.exception?.message,Toast.LENGTH_SHORT).show()
                }
            }

    }

받아온 토큰을 받아와서 구글 로그인과 동일한 방법으로 처리해준다.

 

facebook_login_button.setOnClickListener { facebookLogin() }

이제 버튼에 페이스북 로그인 함수를 연결해준다.


▶ ConstrainedWidth, ConstrainedHeight

더보기

타이틀과 바텀네비게이션 사이에 프레임 레이아웃을 추가하고 싶을 때, 그냥 추가해주면 이렇게 전체를 차지하게 된다.

 

여기서

constraintlayout 속성에서

 

ConstrainedWidth, ConstrainedHeight

속성을 설정해주면, 화면 안에 요소가 전부 보이도록 높이 or 너비를 수정해준다.

 

 


▶ Firebase Storage에 이미지 업로드

더보기
implementation 'com.google.firebase:firebase-storage-ktx:19.2.0'

build.gradle(app)에 추가해준다.

 

class AddPhotoActivity : AppCompatActivity() {
    var PICK_IMAGE_FROM_ALBUM = 0
    var storage : FirebaseStorage? = null
    var photoUri : Uri? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_add_photo)

        // 스토리지 초기화
        storage = FirebaseStorage.getInstance()

        // 앨범 열기
        var photoPickerIntent = Intent(Intent.ACTION_PICK)
        photoPickerIntent.type = "image/*"
        startActivityForResult(photoPickerIntent,PICK_IMAGE_FROM_ALBUM)

        add_photo_button.setOnClickListener { contentUpload() }
        // 업로드 버튼에 contentUpload 연결
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if(requestCode == PICK_IMAGE_FROM_ALBUM){
            if (resultCode == Activity.RESULT_OK){
                // 이미지를 선택 했을 때
                photoUri = data?.data
                add_photo_image.setImageURI(photoUri)
                // 이미지 화면에 선택된 이미지 불러오기
            }
            else{
                // 취소 되었을 때
                finish()
            }
        }
    }

    fun contentUpload(){
        var timestamp = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
        var imageFileName = "IMAGE_" + timestamp + "_.png"
        // 이미지 이름을 현재시간으로 정해줘서 중복 방지

        var storageRef = storage?.reference?.child("images")?.child(imageFileName)

        // 이미지 업로드
        storageRef?.putFile(photoUri!!)?.addOnSuccessListener {
            Toast.makeText(this,getString(R.string.upload_success),Toast.LENGTH_SHORT).show()
        }
    }
}

이미지 추가를 위한 액티비티를 만들어준다.

 

 

ActivityCompat.requestPermissions(this, arrayOf(android.Manifest.permission.READ_EXTERNAL_STORAGE),1)

메인액티비티에서 저장소 권한을 요청해준다.

 

R.id.add_photo ->{
                if(ContextCompat.checkSelfPermission(this,android.Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED){
                    // 권한 체크해서 권한이 있을 때
                    startActivity(Intent(this,AddPhotoActivity::class.java))
                }
                return true
            }

사진 추가 버튼을 눌렀을 때

권한체크해주고 사진 추가 액티비티를 띄워준다.

 

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

매니패스트에 저장소 권한 추가해준다.

 

이제 파이어베이스 콘솔의 스토리지를 시작 해주면 된다.


▶ Firestore 데이터베이스 추가

더보기

 

 

 

 

implementation 'com.google.firebase:firebase-firestore:21.2.1'

build.gradle(app)에 추가

 

var auth : FirebaseAuth? = null
var firestore : FirebaseFirestore? = null

유저정보를 가져오기 위한 auth추가,

firestore 추가

 

auth = FirebaseAuth.getInstance()
firestore = FirebaseFirestore.getInstance()

oncreate에서 초기화

 

        // 이미지 업로드
        storageRef?.putFile(photoUri!!)?.addOnSuccessListener {
            Toast.makeText(this,getString(R.string.upload_success),Toast.LENGTH_SHORT).show()
            
            // 이미지 주소 받아오기
            storageRef.downloadUrl.addOnSuccessListener { uri ->
                var contentDTO = ContentDTO()

                // 이미지 주소 넣어주기
                contentDTO.imageUrl = uri.toString()

                // 유저 uid 넣어주기
                contentDTO.uid = auth?.currentUser?.uid

                // 유저 아이디 넣어주기
                contentDTO.userId = auth?.currentUser?.email

                // 설명 넣어주기
                contentDTO.explain = add_photo_edit.text.toString()

                // 타임스태프 넣어주기
                contentDTO.timestamp = System.currentTimeMillis()

                // 값 넘겨주기
                firestore?.collection("images")?.document()?.set(contentDTO)

                setResult(Activity.RESULT_OK)
                finish()
            }
        }

 만들어놓은 data class에 값을 넘겨주고 그 값을 firestore에 넘겨줌

콘솔의 firestore에서

if request.auth.uid != null;

조건 추가 해주면 끝


▶ Cannot fit requested classes in a single dex file 오류

더보기

Firestore 라이브러리를 추가하고 실행하는 과정에서, 오류가 발생하였는데

앱 내에서 참조될 수 있는 메소드의 총 개수가 65,536(64K)개를 초과해서 발생하는 오류이다.

 

minSdkVersion 21 이상에서는 이 오류가 발생하지 않으므로

버전을 21이상으로 변경해줌으로써 해결할 수 있다.


▶ Snapshot remove

더보기

파이어베이스에서 SnapshotListener를 활용하여 스냅샷을 불러왔을 때,

그 액티비티의 생명주기에 맞춰서 스냅샷리스너를 remove해주지 않으면 오류가 발생한다.

 

override fun onStart() {
        super.onStart()

        // 팔로우, 팔로워 수 불러오기
        loadfollow = firestore?.collection("users")?.document(uid!!)?.addSnapshotListener{ documentSnapshot, firebaseFirestoreException ->
            if(documentSnapshot == null) return@addSnapshotListener
            var followDTO = documentSnapshot.toObject(FollowDTO::class.java)
            if(followDTO?.followerCount != null){
                fragmentView?.account_follower_count?.text = followDTO?.followerCount?.toString()
            }
            if(followDTO?.followingCount != null){
                fragmentView?.account_following_count?.text = followDTO?.followingCount?.toString()
                if(followDTO?.followers?.containsKey(currentUserUid!!)){
                    fragmentView?.account_profile_button?.text = getString(R.string.follow_cancel)
                    fragmentView?.account_profile_button?.background?.setColorFilter(ContextCompat.getColor(activity!!,R.color.colorLightGray),PorterDuff.Mode.MULTIPLY)
                }
                else{
                    if(uid!= currentUserUid){
                        fragmentView?.account_profile_button?.text = getString(R.string.follow)
                        fragmentView?.account_profile_button?.background?.colorFilter = null
                    }
                }
            }
        }

        // 프로필 사진 이미지 불러오기
        loadprofileimage = firestore?.collection("profileImages")?.document(uid!!)?.addSnapshotListener { documentSnapshot, firebaseFirestoreException ->
            if(documentSnapshot == null) return@addSnapshotListener
            if(documentSnapshot.data != null){
                var url = documentSnapshot?.data!!["image"]
                Glide.with(activity!!).load(url).apply(RequestOptions().circleCrop()).into(fragmentView?.account_profile_imageView!!)
            }
        }
    }

    override fun onStop() {
        super.onStop()
        // 스냅샷 제거(오류 방지)
        loadfollow?.remove()
        loadprofileimage?.remove()
    }

 

예시로, onStart에 불러올 정보를 담고

 

onStop에서 사용한 스냅샷을 제거해줌으로써

오류를 방지할 수 있다.


▶ 대표화면(Splash 화면) 만들기

더보기

사용될 로고이미지를 넣어주고

xml을 하나 만들어 배경색과 이미지를 지정해준다.

 

value의 style에서 새로운 테마를 하나 추가해준다.

 

package org.techtown.stagram

import android.content.Intent
import android.os.Bundle
import android.os.Handler
import androidx.appcompat.app.AppCompatActivity

class SplashActivity : AppCompatActivity() {
    val SPLASH_VIEW_TIME: Long = 1000 // 1초간 스플래시 화면을 보여줌

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        Handler().postDelayed({
            startActivity(Intent(this, LoginActivity::class.java))
            finish()
        }, SPLASH_VIEW_TIME)
    }
}

스플래시액티비티를 만들어준다.

       <activity android:name=".SplashActivity"
            android:theme="@style/SplashTheme">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>

매니패스트에서

방금만튼 액티비티를 등록해주고 메인으로 설정해준다.

 


▶ 푸시 라이브러리를 활용한 푸시이벤트 만들기

더보기
implementation 'com.google.firebase:firebase-messaging:20.0.0'
implementation 'com.google.code.gson:gson:2.8.5'
implementation 'com.squareup.okhttp3:okhttp:3.4.1'

bulid.gradle(app)에 파이어베이스 메세지, Gson, okhttp 라이브러리를 추가해준다. 

 

매니패스트에서

메타데이터를 생성해준다.

default_notification_icon 에는 푸시메세지에 나올 아이콘을 등록해주고

default_notification_color 에는 아이콘 색을 등록해준다.

 

 fun registerPushToken(){
        FirebaseInstanceId.getInstance().instanceId.addOnCompleteListener {
            task ->
            val token = task.result?.token
            val uid = FirebaseAuth.getInstance().currentUser?.uid
            val map = mutableMapOf<String,Any>()
            map["pushToken"] = token!!

            FirebaseFirestore.getInstance().collection("pushtokens").document(uid!!).set(map)
        }
    }

특정기기에 푸시이벤트를 발생시키기 위해

토큰을 생성해주는 코드를 작성하고 oncreate에서 실행하여 어플 실행시 토큰이 추가되도록 한다.

 

data class PushDTO (
    var to : String? = null,
    var notification : Notification = Notification()
){
    data class Notification(
        var body : String? = null,
        var title : String? = null
    )
}

푸시를 보내기위한 데이터 클래스를 만들어준다.

class FcmPush {

    var JSON = MediaType.parse("application/json; charset=utf-8")
    var url = "https://fcm.googleapis.com/fcm/send"
    var serverKey = BuildConfig.API_KEY
    var gson : Gson? = null
    var okHttpClient : OkHttpClient? = null

    companion object{
        var instance = FcmPush()
    }

    init{
        gson = Gson()
        okHttpClient = OkHttpClient()
    }

    fun sendMessage(destinationUid : String,  title : String, message : String){
        FirebaseFirestore.getInstance().collection("pushtokens").document(destinationUid).get().addOnCompleteListener {
            task ->
            if(task.isSuccessful){
                var token = task?.result?.get("pushToken").toString()

                var pushDTO = PushDTO()
                pushDTO.to = token
                pushDTO.notification.title = title
                pushDTO.notification.body = message

                var body = RequestBody.create(JSON,gson?.toJson(pushDTO))
                var request = Request.Builder()
                    .addHeader("Content-Type","application/json")
                    .addHeader("Authorization","key="+serverKey)
                    .url(url)
                    .post(body)
                    .build()

                okHttpClient?.newCall(request)?.enqueue(object : Callback{
                    override fun onFailure(call: Call?, e: IOException?) {

                    }

                    override fun onResponse(call: Call?, response: Response?) {
                        println(response?.body()?.string())
                    }
                })
            }
        }
    }

}

 푸시이벤트 요청하기 위한 클래스를 만들어주고, var serverKey 에는 api Key가 들어가야한다.

설정의 서버키에서 확인할 수 있으며 키는 깃이나 다른곳에 업로드시 노출되면 안되기 때문에

gitignore 처리 되어있는 local.properties 에 

google.api.key = "API키"

형식으로 작성해주고

def Properties properties = new Properties()
properties.load(project.rootProject.file('local.properties').newDataInputStream())

bulid.gradle(app) 에 작성해준 후

android{ 안에

buildConfigField("String", "API_KEY", properties.getProperty("google.api.key"))

 

작성해주면

 

BuildConfig.API_KEY 형식으로 사용할 수 있다.

 

이제 푸시요청을 위해 만든 함수를

이벤트 발생하는 위치에서 호출해 줌으로써 푸시이벤트를 발생시킬 수 있다.


 

반응형

공유하기

facebook twitter kakaoTalk kakaostory naver band
loading