※ 인스타그램 클론을 제작하면서 공부한 내용을 정리한 글입니다.
이렇게 복잡하게 되어있는 코드들을
Code > Reformat Code 누르면 정렬할 수 있다.
TextInputLayout은 TextInputEditText에 입력된 텍스트에 반응하는 레이아웃이다.
app 우클릭 > Open Module > Settings
+ 버튼> Library Dependency
material 검색, com.android.material 추가한다. (직접 추가해줘도 됨.)
com.google.android.material.textfield.TextInputLayout으로 텍스트를 감싸준다.
Tools > Firebase
Authentication 클릭하여 구글계정과 연결
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()
}
}
}
developers.facebook.com/?locale=ko_KR
접속 후 로그인
내앱 > 앱 만들기
대시보드 > 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() }
이제 버튼에 페이스북 로그인 함수를 연결해준다.
타이틀과 바텀네비게이션 사이에 프레임 레이아웃을 추가하고 싶을 때, 그냥 추가해주면 이렇게 전체를 차지하게 된다.
여기서
constraintlayout 속성에서
ConstrainedWidth, ConstrainedHeight
속성을 설정해주면, 화면 안에 요소가 전부 보이도록 높이 or 너비를 수정해준다.
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" />
매니패스트에 저장소 권한 추가해준다.
이제 파이어베이스 콘솔의 스토리지를 시작 해주면 된다.
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;
조건 추가 해주면 끝
Firestore 라이브러리를 추가하고 실행하는 과정에서, 오류가 발생하였는데
앱 내에서 참조될 수 있는 메소드의 총 개수가 65,536(64K)개를 초과해서 발생하는 오류이다.
minSdkVersion 21 이상에서는 이 오류가 발생하지 않으므로
버전을 21이상으로 변경해줌으로써 해결할 수 있다.
파이어베이스에서 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에서 사용한 스냅샷을 제거해줌으로써
오류를 방지할 수 있다.
사용될 로고이미지를 넣어주고
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 형식으로 사용할 수 있다.
이제 푸시요청을 위해 만든 함수를
이벤트 발생하는 위치에서 호출해 줌으로써 푸시이벤트를 발생시킬 수 있다.