์ง์ | ํจ์ก | ํ์ค |
---|---|---|
![]() |
![]() |
![]() |
Contact:parkjinsu4755@gmail.com GitHub:jinsu4755 |
Contact:gythd1998@gmail.com GitHub:hyooosong |
Contact: leehj1232@naver.com GitHub:LEE-HYUNGJUN |
- ํ์์คํ
ํ ์นด๋ฉ๋ผ
- ๋ก๊ทธ์ธ
- ์จ๋ณด๋ฉ
- ์๋ฒ ์ฐ๊ฒฐ ๋ก์ง ๊ตฌํ
- ๊ทธ๋ฃน ํญ
- ์บ๋ฆฐ๋ ๋ทฐ
- sharedPreferences ์ฑ๊ธํค ๊ฐ์ฒด ๊ตฌํ
- ํ ๋ฉ์ธํ์ด์ง
- ์นด๋ ๋ทฐ ์ ๋๋ฉ์ด์
- ๋ง์ด ํผ๋, ๊ทธ๋ฃน ํผ๋
- ํผ๋ ์์ธ๋ณด๊ธฐ ๋ทฐ
- [Andromeaning Conventions]
- [Andromeaning Coding Style]
- [Code Review Guideline]
- [Git]
๊ธฐ์์๊ฐ์ด ๋ฌ๋ผ์ง๋ค๋ฉด, ๋น์ ๋ ๋ณํ ์ ์์ต๋๋ค.
โ๋ดโ๊ฐ ๋ ๋จ๋ ์๊ฐ์ด ์๋, โํดโ๊ฐ ๋จ๋ ์๊ฐ๋ถํฐ ํ๋ฃจ๋ฅผ ์์ํ๋ ๋ฏธ๋ผํด ๋ชจ๋.
๋ฏธ๋์ ํตํด ๋ฏธ๋ผํด ๋ชจ๋์ ๋์ ํ๋ฉฐ ๋น์ ๋ง์ ์๋ฏธ์๋ ์์นจ์ ๋ง๋ค์ด ๋๊ฐ๋ณด์ธ์.
์ผ์ฐ ์ผ์ด๋๋ ์ต๊ด์ผ๋ก ํ๋ฃจ๋ฅผ ๊ธธ๊ฒ ๋ณด๋ด๋ฉด, ์ฑ์ฅ์ ๋ฐํ์ ๋ง๋ จํ ์ ์์ต๋๋ค.
๋ฏธ๋๊ณผ ํจ๊ป ์ฒด๊ณ์ ์ธ ๊ณํ์ ์ธ์ฐ๊ณ ์ด๋ฅผ ๊ท์น์ ์ผ๋ก ์ค์ฒํ๋ฉด์ ์ฑ์ทจ๊ฐ์ ์ป์ด๋ณด์ธ์.
์ฑ์ฅ์งํฅ์ ์ธ ๊ทธ๋ฃน์๊ณผ ๋ชฉํ๋ฅผ ๊ณต์ ํ๋ค๋ฉด ์ฐ๋ฆฌ๋ ํจ๊ป, ๋ ๋ฉ๋ฆฌ ๊ฐ ์ ์์ต๋๋ค.
Name | Gradle |
kotlin | org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version |
Android KTX | implementation 'androidx.core:core-ktx:1.3.2 |
Design | androidx.appcompat:appcompat:1.2.0 |
com.google.android.material:material:1.2.1 | |
androidx.constraintlayout:constraintlayout:2.0.4 | |
androidx.legacy:legacy-support-v4:1.0.0 | |
viewModel init support | androidx.activity:activity-ktx:1.1.0 |
androidx.fragment:fragment-ktx:1.2.5 | |
LiveData and ViewModel (Arch components) | androidx.lifecycle:lifecycle-livedata-ktx:2.2.0 |
androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0 | |
retrofit | com.squareup.retrofit2:retrofit:2.9.0 |
com.squareup.retrofit2:converter-gson:2.9.0 | |
com.squareup.okhttp3:logging-interceptor:4.9.0 | |
Gson | com.google.code.gson:gson:2.8.6 |
CameraX core library using camera2 implementation | androidx.camera:camera-core:$camerax_version |
androidx.camera:camera-camera2:$camerax_version | |
CameraX Lifecycle Library | androidx.camera:camera-lifecycle:$camerax_version |
CameraX View class | androidx.camera:camera-view:1.0.0-alpha20 |
Test | junit:junit:4.13.1 |
androidx.test.ext:junit:1.1.2 | |
androidx.test.espresso:espresso-core:3.3.0 | |
image load | com.github.bumptech.glide:glide:4.11.0 |
com.github.bumptech.glide:compiler:4.11.0 | |
splash lottie | com.airbnb.android:lottie:3.5.0 |
-
Material Design Component ๊ตฌ๊ธ Material Design์ ์ฝ๊ฒ ์ฌ์ฉํ ์ ์๋ ๊ตฌํ์ฒด ์ ๊ณต ๋ผ์ด๋ธ๋ฌ๋ฆฌ, UI์ ์ฌ์ฉํ์์ต๋๋ค.
-
Glide url ํ์ ์ด๋ฏธ์ง๋ฅผ
ImageView
์ ํ์ํ๊ธฐ ์ํด ์ฌ์ฉํ์์ต๋๋ค. -
AAC Lifecycle Live Data, Lifecycle, ViewModel ๊ณผ ๊ฐ์ ์๋ช ์ฃผ๊ธฐ์ ์ฐ๋๋ ์ปดํฌ๋ํธ๋ค๊ณผ ํด๋์ค ์ ๊ณต
-
Coroutine ๋น๋๊ธฐ ์์ ์ ์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก ํ์์คํ ํ ์นด๋ฉ๋ผ์์ ์ค์๊ฐ์ผ๋ก ์๊ฐ์ ๋ณ๊ฒฝ์ ๋น๋๊ธฐ๋ก ์ฒ๋ฆฌํ๊ธฐ ์ํด ์ฌ์ฉ.
-
Activity, Fragment ktx ViewModel์ onCreate์์ ์ด๊ธฐํ ํ๋๊ฒฝ์ฐ ์ฌ๋ฌ๋ฒ ์์ฑํน์ ์ํ ์์ค์ ๋ง๊ธฐ ์ํด lazy delegate ์์ ์ผ๋ก viewModel ๊ฐ์ฒด๋ฅผ ๋ฐ์์ ์ฌ์ฉ.
-
Retrofit ์๋๋ก์ด๋ REST API ํต์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ. AsyncTask ์์ด Background Thread์์ ์คํ๋๋ฉฐ callback์ ํตํด Main Thread์์์ UI ์ ๋ฐ์ดํธ๋ฅผ ๊ฐ๋จํ๊ฒ ํ ์ ์๋๋ก ์ ๊ณต. ์๋ฒ ํต์ ์ ์ํด ์ฌ์ฉ.
-
CameraX CameraX๋ ์นด๋ฉ๋ผ ์ฑ ๊ฐ๋ฐ์ ๋ ์ฝ๊ฒ ํ ์ ์๋๋ก ๋ง๋ค์ด์ง Jetpack ์ง์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ, ํ์์คํ ํ ์นด๋ฉ๋ผ ๋ถ๋ถ์์ ์ฌ์ฉ.
-
Lottie Splash ๋ฐ Login ๋ฐฐ๊ฒฝ์ผ๋ก ์ฌ์ฉ
-
feat
: ์๋ก์ด ๊ธฐ๋ฅ ์ถ๊ฐํ๊ธฐfix
: ๋ฒ๊ทธ ์์ ํ๋ ๊ฒฝ์ฐstyle
: ์์ ๋ณ๊ฒฝ, ํฐํธ ๋ณ๊ฒฝ ๋ฑ์ด ์๋ ๊ฒฝ์ฐrefactor
: ์ฝ๋ ๋ฆฌํฉํ ๋ง ํ๋ ๊ฒฝ์ฐupload
: ํ์ผ ์์ฑํ๋ ๊ฒฝ์ฐdocs
: ๋ฌธ์ ์์ ํ๋ ๊ฒฝ์ฐ
-
- Github Action ์๋ ๋น๋๊ฐ ์ฑ๊ณตํ ๊ฒฝ์ฐ.
- Github Action ์๋๋น๋๋ ์ฑ๊ณตํ์ผ๋ ํ์ผ ์ ๋ก๋ ๊ณผ์ ์ ๋ฌธ์ ๊ฐ ์๊ธด ๊ฒฝ์ฐ
- Github Action ์๋ ๋น๋๋ ์คํจํ ๊ฒฝ์ฐ
name: MeaningAndroid Builder on: push: branches: [ develop ] defaults: run: shell: bash working-directory: . jobs: build: name: Generate APK runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 - name: Gradle cache uses: actions/cache@v2 with: path: | ~/.gradle/caches ~/.gradle/wrapper key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }} restore-keys: | ${{ runner.os }}-gradle- - name: set up JDK 1.8 uses: actions/setup-java@v1 with: java-version: 1.8 - name: Change gradlew permissions run: chmod +x ./gradlew - name: Build with Gradle run: ./gradlew assembleDebug - name: On Failed, Notify in Slack if: ${{ failure() }} uses: rtCamp/action-slack-notify@v2 env: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }} SLACK_TITLE: 'nneaning/Anroid Debug build Failโ' SLACK_COLOR: '#FF5733' MSG_MINIMAL: true SLACK_MESSAGE: '์๋ฌ๋ฅผ ํ์ธํด์ฃผ์ธ์' - name: Upload APK if: ${{ success() }} uses: actions/upload-artifact@v2 with: name: apk path: app/build/outputs/apk/debug/ upload: needs: [build] name: upload to Slack runs-on: ubuntu-latest steps: - name: download Article uses: actions/download-artifact@v2 with: name: apk - name: Update Release apk name if: ${{ success() }} run: | mv app-debug.apk ๋ฏธ๋-Debug.apk echo 'apk=๋ฏธ๋-Debug.apk' >> $GITHUB_ENV - name: Upload APK at Slack if: ${{ success() }} run: | curl -X POST \ -F file=@$apk \ -F channels=${{secrets.SLACK_CHANNEL_ID}} \ -H "Authorization: Bearer ${{secrets.SLACK_BOT_TOKEN}}" \ https://slack.com/api/files.upload - name: On Success if: ${{ success() }} uses: rtCamp/action-slack-notify@v2 env: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }} SLACK_TITLE: 'nneaning/Anroid Debug build Successโ ' SLACK_COLOR: '#5BFF33' MSG_MINIMAL: true SLACK_MESSAGE: 'apk ์์ฑ ์๋ฃ! ' - name: On Success but Fail if: ${{ failure() }} uses: rtCamp/action-slack-notify@v2 env: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }} SLACK_TITLE: 'nneaning/Anroid Debug build Successโ ' SLACK_COLOR: '#FFF233' MSG_MINIMAL: true SLACK_MESSAGE: '๋น๋๋ ์๋ฃ ๋์์ผ๋ apk์ ๋ก๋ ์๋ฌ'
MVC์ MVVM์ ํผํฉ ์ํคํ ์ฒ๋ก ๊ฐ๋ฐ ํ์์ต๋๋ค.
- ** AAC DataBinding, ViewModel **
private lateinit var binding: ActivityLoginBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_login)
binding.viewModel = loginViewModel
binding.lifecycleOwner = this
initView()
}
private val loginViewModel: LoginViewModel by viewModels {
object : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T =
LoginViewModel(MeaningStorage.getInstance(this@LoginActivity)) as T
}
}
-
Coroutine - ๋น๋๊ธฐ ์์
fun runCurrentTimer() = viewModelScope.launch() { while (isEnableTimer) { _currentTime.value = SimpleDateFormat(TIME_FORMAT, Locale.KOREA) .format(System.currentTimeMillis()) _currentDate.value = SimpleDateFormat(DATE_FORMAT, Locale.KOREA) .format(System.currentTimeMillis()) delay(10000) } }
-
CameraX
private fun startCamera() { val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext()) cameraProviderFuture.addListener( cameraProvideFutureListener(cameraProviderFuture), getMainExecutor() ) } private fun cameraProvideFutureListener( cameraProviderFuture: ListenableFuture<ProcessCameraProvider> ) = Runnable { val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get() val preview = getCameraPreview() val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA setImageCapture() try { cameraProvider.unbindAll() cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture) } catch (failBindException: Exception) { Log.e(TAG, "Use case binding failed", failBindException) } } private fun getCameraPreview(): Preview = Preview.Builder() .build() .also { it.setSurfaceProvider(binding.cameraViewFinder.surfaceProvider) } private fun setImageCapture() { imageCapture = ImageCapture.Builder() .build() } private fun getMainExecutor() = ContextCompat.getMainExecutor(requireContext()) private fun takePhoto() { val imageCapture = imageCapture ?: return imageCapture.takePicture( getMainExecutor(), getImageCapturedCallback() ) } private fun getImageCapturedCallback(): TimeStampCameraCallback = TimeStampCameraCallback().apply { setOnCaptureSuccessListener { imageCaptureEvent(it) } } private fun imageCaptureEvent(image: Bitmap) { cameraViewModel.image = image cameraViewModel.isEnableTimer = false (requireActivity() as TimeStampCameraActivity).changeFragment( CameraResultFragment(), null ) }
๐
meaning.morning
โฃ ๐data
โฃ ๐network
โ โฃ ๐request
โ โฃ ๐response
โฃ ๐presentation
โ โฃ ๐adapter
โ โ โฃ ๐feed
โ โ โฃ ๐group
โ โ โฃ ๐home
โ โฃ ๐camera
โ โฃ ๐group
โ โ โฃ ๐feed
โ โฃ ๐home
โ โ โฃ ๐card
โ โ โฃ ๐feed
โ โฃ ๐login
โ โ ๐onboarding
โ๐utils
object๋ฅผ ์ฌ์ฉํ์ง ์๊ณ ์์ฑํ๊ธฐ.
Multi-Thread Safeํ๋๋ก ๋ง๋ค๊ธฐ.
SharedPreference์ง๋ง ๋ณด๋ค ์ง๊ด์ ์ธ ์ด๋ฆ์ ์ฌ์ฉํ๊ธฐ.
class MeaningStorage(context: Context) {
/* ... */
companion object {
private var instance: MeaningStorage? = null
fun getInstance(context: Context) = instance ?: synchronized(this) {
instance ?: MeaningStorage(context).apply {
instance = this
}
}
}
}
- Camera Permission
private fun initTimeStampCamera() {
if (allPermissionGranted()) {
loadCameraView()
return
}
requestPermission()
}
private fun allPermissionGranted() = REQUIRED_PERMISSIONS.all {
ContextCompat.checkSelfPermission(
applicationContext,
it
) == PackageManager.PERMISSION_GRANTED
}
private fun requestPermission() {
ActivityCompat.requestPermissions(
this,
REQUIRED_PERMISSIONS,
CameraViewModel.REQUEST_CODE_PERMISSIONS
)
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
if (requestCode == CameraViewModel.REQUEST_CODE_PERMISSIONS) {
permissionResponseEvent()
}
}
private fun permissionResponseEvent() {
if (allPermissionGranted()) {
loadCameraView()
return
}
permissionDeniedEvent()
}
private fun permissionDeniedEvent() {
showToast("๊ถํ์ ์น์ธํ์ง ์์ผ๋ฉด ๋น์ ์ ๋ฏธ๋ผํด ๋ชจ๋์ ๊ธฐ๋กํ ์ ์์ด์!")
finish()
}
private fun loadCameraView() {
changeFragment(CameraFragment())
}
private fun changeFragment(initFragment: Fragment) {
val transaction = supportFragmentManager.beginTransaction()
transaction.apply {
replace(R.id.fragment_camera, initFragment)
commit()
}
}
private fun startCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext())
cameraProviderFuture.addListener(
cameraProvideFutureListener(cameraProviderFuture),
getMainExecutor()
)
}
private fun cameraProvideFutureListener(
cameraProviderFuture: ListenableFuture<ProcessCameraProvider>
) = Runnable {
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
val preview = getCameraPreview()
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
setImageCapture()
try {
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture)
} catch (failBindException: Exception) {
Log.e(TAG, "Use case binding failed", failBindException)
}
}
private fun getCameraPreview(): Preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(binding.cameraViewFinder.surfaceProvider)
}
private fun setImageCapture() {
imageCapture = ImageCapture.Builder()
.build()
}
private fun getMainExecutor() = ContextCompat.getMainExecutor(requireContext())
private fun takePhoto() {
val imageCapture = imageCapture ?: return
imageCapture.takePicture(
getMainExecutor(),
getImageCapturedCallback()
)
}
private fun getImageCapturedCallback(): TimeStampCameraCallback =
TimeStampCameraCallback().apply {
setOnCaptureSuccessListener { imageCaptureEvent(it) }
}
private fun imageCaptureEvent(image: Bitmap) {
cameraViewModel.image = image
cameraViewModel.isEnableTimer = false
/* ... */
}
๋ค์๊ณผ ๊ฐ์ด ๋ง๋ค์ด์ง ์นด๋ฉ๋ผ๋ฅผ ๋ทฐ๋ชจ๋ธ์ ์ ์ฅํ์ฌ ๊ฒฐ๊ณผ ์ฐฝ์ผ๋ก ๋๊ธฐ๊ณ ๊ฒฐ๊ณผ์ฐฝ์์๋ ํด๋น ๋ทฐ๋ฅผ Bitmap์ผ๋ก ๋ณํํ์ฌ ์ ์ฅํ๋ค.
class TimeStampImageCreator(private val context: Context) {
/* ... */
fun saveOf(viewGroup: ConstraintLayout) {
val width = viewGroup.width
val height = viewGroup.height
removeViewEvent(viewGroup)
val bitmapBuffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmapBuffer)
viewGroup.draw(canvas)
saveImage(bitmapBuffer)
}
private fun removeViewEvent(viewGroup: ConstraintLayout) {
viewGroup.apply {
clearFocus()
isPressed = false
invalidate()
}
}
private fun getOutputDirectory(): File {
val mediaDir = context.externalMediaDirs.firstOrNull()?.let {
File(it, context.resources.getString(R.string.app_name)).apply {
mkdirs()
}
}
return if (mediaDir != null && mediaDir.exists()) mediaDir else context.filesDir
}
private fun saveImage(bitmapBuffer: Bitmap) {
photo = getPhotoFile()
try {
val outputStream = FileOutputStream(photo)
bitmapBuffer.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
outputStream.close()
galleryAddPicture()
} catch (errorMessage: FileNotFoundException) {
errorMessage.stackTrace
} catch (errorMessage: IOException) {
errorMessage.stackTrace
} finally {
bitmapBuffer.recycle()
}
}
private fun getPhotoFile() = File(
getOutputDirectory(),
SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss",
Locale.KOREA
).format(System.currentTimeMillis()) + ".jpeg"
)
}
๋ง๋ ํ์ผ์ ๊ธ์ฐ๊ธฐ ํ๋ฉด์ผ๋ก ๋๊ธด๋ค.
์์ดํ ํด๋ฆญ ์ด๋ฒคํธ๋ฅผ ์ธํฐํ์ด์ค๋ก ๋ถ๋ฆฌ.
class MyFeedPictureAdapter : RecyclerView.Adapter<MyFeedPictureAdapter.MyFeedPictureViewHolder>() {
var data = mutableListOf<MyFeedPictureData>()
private lateinit var itemClickListener : ItemClickListener
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyFeedPictureViewHolder {
val binding = FeedItemListBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return MyFeedPictureViewHolder(binding)
}
override fun getItemCount(): Int {
return data.size
}
override fun onBindViewHolder(holder: MyFeedPictureViewHolder, position: Int) {
holder.onBind(data[position])
holder.itemView.setOnClickListener {
itemClickListener.onClick(it,position)
}
}
fun submitData(list : List<MyFeedPictureData>){
data.addAll(list)
notifyDataSetChanged()
}
class MyFeedPictureViewHolder(val binding: FeedItemListBinding) : RecyclerView.ViewHolder(binding.root) {
fun onBind(data: MyFeedPictureData) {
binding.feedItemList = data
}
}
interface ItemClickListener{
fun onClick(view : View, position: Int)
}
fun setItemClickListener(itemClickListener: ItemClickListener){
this.itemClickListener = itemClickListener
}
}
-
Layout ์ฌ์ฉ
๋ฐ์ดํฐ ๋ฐ์ธ๋ฉ์ผ๋ก ์ฌ์ฉ์ผ๋ก ๋ชจ๋ ๋ทฐ์ ์ต์์๊ฐ Layout ํ๊ทธ ์๋ ์์
-
coordinatorlayout, NestedScrollView ์ฌ์ฉ ์คํฌ๋กค ์ด๋ฒคํธ ๋ฐ์์ behavior๋ฅผ ์ด์ฉํ์ฌ ๋ทฐ์ ๋ณ๊ฒฝ์ ํ๊ธฐ ์ํด์ ์ฌ์ฉ.
- fragment_group.xml - activity_my_feed_main.xml - activity_group_settting.xml
-
๋จ์ ๋ํ ์์ - ์บ๋ฆฐ๋ ๋ทฐ ์๋ ์
radius ํ์ธ์ด ๋ถ๊ฐ๋ฅํ์ฌ ๋์์ด๋์๊ฒ ์์ฒญํ ์์ ์ผ๋ก ๋ฐ๊ธฐ๋กํจ
- HomeFragment
-
์ ๋ ํฌ๊ธฐ ์ง์
- feed_item_list.xml - dialog_group_recycler.xml - dialog_group_detail.xml - fragment_home.xml
- feed_item_list : ํผ๋ ์์ดํ ์ผ๋ก ๋ค์ด์ฌ ์ฌ์ง ํฌ๊ธฐ๊ฐ ๊ธฐ๊ธฐ๋ณ๋ก ๋ค๋ฅผ ๊ฒฝ์ฐ๋ฅผ ๋ฐ๋ผ ์ ๋ ํฌ๊ธฐ ์ง์
- dialog : ํ๋ฉด ๋น์จ์ ๋ฐ๋ผ๊ฐ ์๋ ๋ค์ด์ผ๋ก๊ทธ ์ฐฝ์ ํฌ๊ธฐ ๊ณ ์ ์ ์ํด์ ์ฌ์ฉ
- fragement_home.xml : > ๋ชจ์ ์์ ํฌ๊ธฐ๊ฐ ๋๋ฌด ์๋ค๋ ์์ฒญ์ ์ ๋ํฌ๊ธฐ๋ก ์ฝ๊ฐ ํฌ๊ธฐ ์ฆ๊ฐ ์ง์ .