Skip to content

Commit

Permalink
[FEAT/#11] post image api 연결
Browse files Browse the repository at this point in the history
  • Loading branch information
b1urrrr committed May 27, 2023
1 parent f992303 commit f90ce12
Show file tree
Hide file tree
Showing 10 changed files with 266 additions and 25 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.android.go.sopt.data.repository

import okhttp3.MultipartBody
import org.android.go.sopt.data.source.remote.ImageDataSource
import org.android.go.sopt.domain.repository.ImageRepository
import javax.inject.Inject

class ImageRepositoryImpl @Inject constructor(
private val imageDataSource: ImageDataSource,
) : ImageRepository {
override suspend fun uploadImage(file: MultipartBody.Part): Result<Unit> = kotlin.runCatching {
imageDataSource.uploadImage(file)
}
}
14 changes: 14 additions & 0 deletions app/src/main/java/org/android/go/sopt/data/service/ImageService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.android.go.sopt.data.service

import okhttp3.MultipartBody
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part

interface ImageService {
@Multipart
@POST("upload")
suspend fun uploadImage(
@Part file: MultipartBody.Part,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.android.go.sopt.data.source.remote

import okhttp3.MultipartBody
import org.android.go.sopt.data.service.ImageService
import javax.inject.Inject

class ImageDataSource @Inject constructor(
private val imageService: ImageService,
) {
suspend fun uploadImage(file: MultipartBody.Part) = imageService.uploadImage(file)
}
8 changes: 8 additions & 0 deletions app/src/main/java/org/android/go/sopt/di/RepositoryModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.android.go.sopt.data.repository.AuthRepositoryImpl
import org.android.go.sopt.data.repository.FollowerRepositoryImpl
import org.android.go.sopt.data.repository.ImageRepositoryImpl
import org.android.go.sopt.data.repository.RepoRepositoryImpl
import org.android.go.sopt.domain.repository.AuthRepository
import org.android.go.sopt.domain.repository.FollowerRepository
import org.android.go.sopt.domain.repository.ImageRepository
import org.android.go.sopt.domain.repository.RepoRepository
import javax.inject.Singleton

Expand All @@ -32,4 +34,10 @@ abstract class RepositoryModule {
abstract fun bindsFollowerRepository(
followerRepositoryImpl: FollowerRepositoryImpl,
): FollowerRepository

@Binds
@Singleton
abstract fun bindsImageRepository(
imageRepositoryImpl: ImageRepositoryImpl,
): ImageRepository
}
6 changes: 6 additions & 0 deletions app/src/main/java/org/android/go/sopt/di/ServiceModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import org.android.go.sopt.data.service.AuthService
import org.android.go.sopt.data.service.FollowerService
import org.android.go.sopt.data.service.ImageService
import org.android.go.sopt.di.RetrofitModule.Retrofit2
import org.android.go.sopt.util.type.BaseUrlType
import retrofit2.Retrofit
Expand All @@ -19,6 +20,11 @@ object ServiceModule {
fun providesAuthService(@Retrofit2(BaseUrlType.SOPT) retrofit: Retrofit): AuthService =
retrofit.create(AuthService::class.java)

@Provides
@Singleton
fun providesImageService(@Retrofit2(BaseUrlType.SOPT) retrofit: Retrofit): ImageService =
retrofit.create(ImageService::class.java)

@Provides
@Singleton
fun providesFollowerService(@Retrofit2(BaseUrlType.REQRES) retrofit: Retrofit): FollowerService =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.android.go.sopt.domain.repository

import okhttp3.MultipartBody

interface ImageRepository {
suspend fun uploadImage(file: MultipartBody.Part): Result<Unit>
}
Original file line number Diff line number Diff line change
@@ -1,36 +1,71 @@
package org.android.go.sopt.presentation.main.gallery

import android.net.Uri
import android.os.Bundle
import android.view.View
import android.widget.ImageView
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.viewModels
import coil.load
import dagger.hilt.android.AndroidEntryPoint
import org.android.go.sopt.R
import org.android.go.sopt.databinding.FragmentGalleryBinding
import org.android.go.sopt.util.ContentUriRequestBody
import org.android.go.sopt.util.binding.BindingFragment
import org.android.go.sopt.util.extension.setOnSingleClickListener
import org.android.go.sopt.util.extension.showSnackbar
import org.android.go.sopt.util.state.RemoteUiState.Error
import org.android.go.sopt.util.state.RemoteUiState.Failure
import org.android.go.sopt.util.state.RemoteUiState.Success


@AndroidEntryPoint
class GalleryFragment : BindingFragment<FragmentGalleryBinding>(R.layout.fragment_gallery) {
private var imageAdapter: ImageAdapter? = null
private val viewModel by viewModels<GalleryViewModel>()
private val launcher =
registerForActivityResult(ActivityResultContracts.PickMultipleVisualMedia(maxItems = 3)) { imageUriList: List<Uri> ->
with(binding) {
if (imageUriList.size !in 1..3) {
requireContext().showSnackbar(binding.root, "최소 1개 최대 3개의 이미지를 선택해주세요.")
return@registerForActivityResult
}
for (i in imageUriList.indices) {
(layoutGallery.getChildAt(i) as ImageView).load(imageUriList[i])
}
viewModel.setImageFile(ContentUriRequestBody(requireContext(), imageUriList[0]))
}
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.vm = viewModel

initImageAdapter()
initSelectBtnClickListener()
setupPostImageState()
}

private fun initImageAdapter() {
imageAdapter = ImageAdapter()
binding.vpGallery.adapter = imageAdapter
imageAdapter?.submitList(
listOf(
R.drawable.img_main_profile,
R.drawable.img_main_profile,
R.drawable.img_main_profile,
),
)
private fun initSelectBtnClickListener() {
binding.btnGallerySelect.setOnSingleClickListener {
launcher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageAndVideo))
}
}

override fun onDestroyView() {
super.onDestroyView()
imageAdapter = null
private fun setupPostImageState() {
viewModel.postImageState.observe(viewLifecycleOwner) { state ->
when (state) {
is Success -> {
requireContext().showSnackbar(binding.root, "이미지가 전송되었습니다.")
}

is Failure -> {
requireContext().showSnackbar(binding.root, "이미지 전송에 실패하였습니다.")
}

is Error -> {
requireContext().showSnackbar(binding.root, "서버에 연결할 수 없습니다.")
}
}
}
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package org.android.go.sopt.presentation.main.gallery

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import okhttp3.MultipartBody
import org.android.go.sopt.domain.repository.ImageRepository
import org.android.go.sopt.util.ContentUriRequestBody
import org.android.go.sopt.util.state.RemoteUiState
import org.android.go.sopt.util.state.RemoteUiState.Failure
import org.android.go.sopt.util.state.RemoteUiState.Success
import retrofit2.HttpException
import timber.log.Timber
import javax.inject.Inject

@HiltViewModel
class GalleryViewModel @Inject constructor(
private val imageRepository: ImageRepository,
) : ViewModel() {
private val _postImageState = MutableLiveData<RemoteUiState>()
val postImageState: LiveData<RemoteUiState>
get() = _postImageState

private val _imageFile = MutableLiveData<ContentUriRequestBody>()
val imageFile: LiveData<ContentUriRequestBody>
get() = _imageFile

fun setImageFile(requestBody: ContentUriRequestBody) {
_imageFile.value = requestBody
}

fun postImage() {
viewModelScope.launch {
// TODO: null 처리
imageRepository.uploadImage(imageFile.value!!.toFormData())
.onSuccess { response ->
Timber.d("POST IMAGE SUCCESS : $response")
_postImageState.value = Success
}
.onFailure { throwable ->
if (throwable is HttpException) {
Timber.e("POST IMAGE FAILURE(${throwable.code()}) : ${throwable.message()}")
_postImageState.value = Failure(null)
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package org.android.go.sopt.util

import android.content.Context
import android.net.Uri
import android.provider.MediaStore
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody
import okio.BufferedSink
import okio.source

class ContentUriRequestBody(
context: Context,
private val uri: Uri,
) : RequestBody() {
private val contentResolver = context.contentResolver

private var fileName = ""
private var size = -1L

init {
contentResolver.query(
uri,
arrayOf(MediaStore.Images.Media.SIZE, MediaStore.Images.Media.DISPLAY_NAME),
null,
null,
null,
)?.use { cursor ->
if (cursor.moveToFirst()) {
size = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.SIZE))
fileName =
cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME))
}
}
}

fun getFileName() = fileName

override fun contentLength(): Long = size

override fun contentType(): MediaType? =
contentResolver.getType(uri)?.toMediaTypeOrNull()

override fun writeTo(sink: BufferedSink) {
contentResolver.openInputStream(uri)?.source()?.use { source ->
sink.writeAll(source)
}
}

fun toFormData() = MultipartBody.Part.createFormData("file", getFileName(), this)
}
61 changes: 52 additions & 9 deletions app/src/main/res/layout/fragment_gallery.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,66 @@

<data>

<variable
name="vm"
type="org.android.go.sopt.presentation.main.gallery.GalleryViewModel" />
</data>

<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/layout_gallery"
android:layout_width="match_parent"
android:layout_height="match_parent">

<androidx.viewpager2.widget.ViewPager2
android:id="@+id/vp_gallery"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginHorizontal="50dp"
android:layout_marginVertical="30dp"
app:layout_constraintBottom_toBottomOf="parent"
<LinearLayout
android:id="@+id/layout_gallery"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="40dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toTopOf="parent">

<ImageView
android:id="@+id/iv_gallery_first"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@color/gray_200" />

<ImageView
android:id="@+id/iv_gallery_second"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginHorizontal="30dp"
android:background="@color/gray_200" />

<ImageView
android:id="@+id/iv_gallery_third"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@color/gray_200" />
</LinearLayout>

<Button
android:id="@+id/btn_gallery_send"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp"
android:layout_marginBottom="12dp"
android:onClick="@{()->vm.postImage()}"
android:text="이미지 전송하기"
app:layout_constraintBottom_toTopOf="@id/btn_gallery_select"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />

<Button
android:id="@+id/btn_gallery_select"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp"
android:layout_marginBottom="50dp"
android:paddingVertical="10dp"
android:text="이미지 선택하기"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

0 comments on commit f90ce12

Please sign in to comment.