Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feature/semin7]: 7주차 과제제출 #13

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.android.go.sopt.data.model.music


import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.android.go.sopt.domain.entity.music.SoptPostMusicData

@Serializable
data class SoptPostMusicResponse(
@SerialName("data")
val data: Data? = null,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SerialName을 사용한다면 변수명을 다른 이름으로 지정해도 되지 않을까요?

@SerialName("message")
val message: String? = null,
@SerialName("status")
val status: Int? = null
) {

@Serializable
data class Data(
@SerialName("singer")
val singer: String? = null,
@SerialName("title")
val title: String? = null,
@SerialName("url")
val url: String? = null
)
}

fun SoptPostMusicResponse.toSoptPostMusicData() = SoptPostMusicData(
message = message.orEmpty(),
status = status ?: -1,
singer = data?.singer.orEmpty(),
title = data?.title.orEmpty(),
url = data?.url.orEmpty()
)
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,24 @@ package org.android.go.sopt.data.repository

import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import okhttp3.MultipartBody
import okhttp3.RequestBody
import org.android.go.sopt.data.dao.MusicDao
import org.android.go.sopt.data.model.music.toMusicData
import org.android.go.sopt.data.model.music.toMusicDataEntity
import org.android.go.sopt.domain.repository.MusicRepository
import org.android.go.sopt.data.model.music.toSoptGetMusicData
import org.android.go.sopt.data.model.music.toSoptPostMusicData
import org.android.go.sopt.data.service.sopt.SoptMusicService
import org.android.go.sopt.domain.entity.music.MusicData
import org.android.go.sopt.domain.entity.music.SoptGetMusicData
import org.android.go.sopt.domain.entity.music.SoptPostMusicData
import org.android.go.sopt.domain.repository.MusicRepository
import javax.inject.Inject

class MusicRepositoryImpl @Inject constructor(private val musicDao: MusicDao) : MusicRepository {
class MusicRepositoryImpl @Inject constructor(
private val musicDao: MusicDao,
private val soptMusicService: SoptMusicService
Comment on lines +20 to +21

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

) : MusicRepository {
override suspend fun getAll(): Flow<List<MusicData>> {
return musicDao.getAll().map { musicList ->
musicList.map { it.toMusicData() }
Expand All @@ -31,4 +41,12 @@ class MusicRepositoryImpl @Inject constructor(private val musicDao: MusicDao) :
override suspend fun update(musicData: MusicData) {
return musicDao.update(musicData.toMusicDataEntity())
}

override suspend fun getMusicList(id: String): Result<SoptGetMusicData> {
return runCatching { soptMusicService.getMusic(id).toSoptGetMusicData() }
}

override suspend fun postMusic(id: String, image: MultipartBody.Part, title: RequestBody, singer: RequestBody): Result<SoptPostMusicData> {
return runCatching { soptMusicService.postMusic(id, image, title, singer).toSoptPostMusicData() }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package org.android.go.sopt.data.service.sopt

import okhttp3.MultipartBody
import okhttp3.RequestBody
import org.android.go.sopt.data.model.music.SoptGetMusicListResponse
import org.android.go.sopt.data.model.music.SoptPostMusicResponse
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part
import retrofit2.http.Path

interface SoptMusicService {

@Multipart
@POST("/music")
suspend fun postMusic(
@Header("id") id: String,
@Part image: MultipartBody.Part,
@Part("title") title: RequestBody,
@Part("singer") singer: RequestBody,
): SoptPostMusicResponse

@GET("/{id}/music")
suspend fun getMusic(
@Path("id") id: String
): SoptGetMusicListResponse
}
12 changes: 12 additions & 0 deletions app/src/main/java/org/android/go/sopt/di/NetworkModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import org.android.go.sopt.data.service.kakao.KakaoService
import org.android.go.sopt.data.service.reqres.ReqresService
import org.android.go.sopt.data.service.sopt.SoptMusicService
import org.android.go.sopt.data.service.sopt.SoptService
import org.android.go.sopt.util.UrlInfo
import retrofit2.Converter
Expand Down Expand Up @@ -56,6 +57,17 @@ class NetworkModule {
.create()
}

@Provides
@Singleton
fun provideMusicService(jsonConverter:Converter.Factory, client: OkHttpClient): SoptMusicService {
return Retrofit.Builder()
.baseUrl(UrlInfo.SOPT_BASE_URL)
.addConverterFactory(jsonConverter)
.client(client)
.build()
.create()
}

@Provides
@Singleton
fun provideJsonConverterFactory(): Converter.Factory {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.android.go.sopt.domain.entity.music


import kotlinx.serialization.Serializable

data class SoptGetMusicData(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://developer.android.com/topic/architecture/data-layer?hl=ko#business-models

이 부분 참고해서 데이터 클래스 네이밍 지어봐도 괜찮을 것 같네요

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 감사합니다!!

val message: String,
val status: Int,
val musicList: List<Music>
) {
data class Music(
val singer: String,
val title: String,
val url: String
)
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.android.go.sopt.domain.entity.music

import okhttp3.MultipartBody

data class SoptPostMusicBody(
val singer: String,
val title: String,
val image: MultipartBody.Part
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.android.go.sopt.domain.entity.music



data class SoptPostMusicData(
val message: String,
val status: Int,
val singer: String,
val title: String,
val url: String
)
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package org.android.go.sopt.domain.repository

import kotlinx.coroutines.flow.Flow
import okhttp3.MultipartBody
import okhttp3.RequestBody
import org.android.go.sopt.domain.entity.music.MusicData
import org.android.go.sopt.domain.entity.music.SoptGetMusicData
import org.android.go.sopt.domain.entity.music.SoptPostMusicData

interface MusicRepository {

Expand All @@ -14,4 +18,8 @@ interface MusicRepository {
suspend fun delete(musicData: MusicData)

suspend fun update(musicData: MusicData)

suspend fun getMusicList(id: String): Result<SoptGetMusicData>

suspend fun postMusic(id: String, image: MultipartBody.Part, title: RequestBody, singer: RequestBody): Result<SoptPostMusicData>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.android.go.sopt.extension

import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody.Companion.toRequestBody

class CommonExtension {
private fun Int.toRequestBody() = toString().toRequestBody("text/plain".toMediaTypeOrNull())
}
Comment on lines +6 to +8

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
class CommonExtension {
private fun Int.toRequestBody() = toString().toRequestBody("text/plain".toMediaTypeOrNull())
}
private fun Int.toRequestBody() = toString().toRequestBody("text/plain".toMediaTypeOrNull())

코틀린이니까 파일안에 함수만 있어도 될듯

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

file로 만든게 아니라 class로 만들었었네요 이런 실수를 ㅋㅎㅋㅎㅋㅎ 감사합니다

Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.android.go.sopt.presentation.main.home

import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
Expand All @@ -14,16 +15,18 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.android.go.sopt.databinding.FragmentHomeBinding
import org.android.go.sopt.presentation.state.UIState
import org.android.go.sopt.presentation.main.home.reqres.ReqresAdapter
import org.android.go.sopt.presentation.main.home.music.SoptMusicAdapter
import org.android.go.sopt.presentation.main.player.MusicFragment
import org.android.go.sopt.presentation.main.player.dialog.AddDialog
import org.android.go.sopt.presentation.main.player.dialog.MusicDialog

@AndroidEntryPoint
class HomeFragment : Fragment() {

private var _binding: FragmentHomeBinding? = null
private val binding get() = _binding!!

private var reqresAdapter: ReqresAdapter? = null
private var soptMusicAdapter: SoptMusicAdapter? = null
private val viewModel by viewModels<HomeViewModel>()

override fun onCreateView(
Expand All @@ -41,34 +44,51 @@ class HomeFragment : Fragment() {

override fun onDestroyView() {
_binding = null
reqresAdapter = null
soptMusicAdapter = null
super.onDestroyView()
}

private fun initObserve() {
viewModel.userListStateFlow.flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED).onEach {
viewModel.homeStateFlow.flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED).onEach {
when (it) {
is UIState.UnInitialized -> {
is HomeState.UnInitialized -> {
initView()
initRecyclerView()
viewModel.getUsers(2)
viewModel.getMusicList()
}

is UIState.Loading -> {}
is HomeState.Loading -> {}

is UIState.Success -> {
reqresAdapter?.submitList(it.data.data)
is HomeState.SuccessGetMusicList -> {
soptMusicAdapter?.submitList(it.data.musicList)
}

is UIState.Error -> {}
is HomeState.SuccessPostMusic -> {
viewModel.getMusicList()
}

is HomeState.Error -> {
Log.e("HomeFragment", "Error")
}
}
}.launchIn(viewLifecycleOwner.lifecycleScope)
}

private fun initView() {
binding.fbAdd.setOnClickListener {
MusicDialog.getInstance().apply {
setOnItemClickListener {
viewModel.postMusic(it)
}
}.let { parentFragmentManager.beginTransaction().add(it, MusicFragment.ADD_DIALOG).commitAllowingStateLoss() }
}
}

private fun initRecyclerView() {
reqresAdapter = ReqresAdapter()
soptMusicAdapter = SoptMusicAdapter()

binding.rvReqres.apply {
adapter = reqresAdapter
adapter = soptMusicAdapter
layoutManager = LinearLayoutManager(requireContext())
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.android.go.sopt.presentation.main.home

import org.android.go.sopt.domain.entity.music.SoptGetMusicData

sealed class HomeState {
object UnInitialized: HomeState()

object Loading: HomeState()

data class SuccessGetMusicList(
val data: SoptGetMusicData
): HomeState()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

의진님은 state 관리 따로, data 관리 따로 하는 것이 좋은 것 같나요?
아니면 지금처럼 state에서 response 값도 같이 받아주는 것이 좋다고 생각하나요?
저도 처음에는 state에서 response 값도 받아서 같이 관리해주는 것이 좋다고 생각했었는데
대환님 말씀 들어보니까 뭔가 state에서 response 관리하는 것이 state의 역할을 조금 흐리는? 느낌이 드는 것 같아서요!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 State 관리와 data 관리를 따로 한다는 관점에 대해서는 깊게 생각해보지 않았는데요. 현재 제가 사용하고 있는 UI State의 사용 이유가 결국 View단에서 State에 따라서만 View가 보여져야하는 형태를 핸들링 할 수 있다는 부분이 유지보수 측면에서 매력적이기 때문입니다. 결국 View가 State에 따라 변경되게 하려면 비즈니스 로직에서 받아오는 data가 State내에 필수적으로 필요할 것 같습니다. 어떻게 보면 MVI의 느낌의 패턴이라고 생각하시면 됩니다.

만약 데이터를 따로 관리하게 된다면, Success인 State에서 data를 따로 observable한 변수를 통해 처리하는 방법을 생각하시는 걸까요?? 이 부분에 있어서 다른 패턴을 적용할 수 있는 예제를 보여주신다면 저도 확인해보겠습니다!!


object SuccessPostMusic: HomeState()

object Error: HomeState()
}
Original file line number Diff line number Diff line change
@@ -1,30 +1,58 @@
package org.android.go.sopt.presentation.main.home

import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import org.android.go.sopt.domain.entity.reqres.ReqresEntity
import org.android.go.sopt.domain.repository.ReqresRepository
import org.android.go.sopt.presentation.state.UIState
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import org.android.go.sopt.domain.entity.music.SoptPostMusicBody
import org.android.go.sopt.domain.repository.MusicRepository
import retrofit2.http.Multipart
import javax.inject.Inject


@HiltViewModel
class HomeViewModel @Inject constructor(private val reqresRepository: ReqresRepository) : ViewModel() {
class HomeViewModel @Inject constructor(
private val musicRepository: MusicRepository
) : ViewModel() {

private val _homeStateFlow = MutableStateFlow<HomeState>(HomeState.UnInitialized)
val homeStateFlow: StateFlow<HomeState> get() = _homeStateFlow

private val _userListStateFlow = MutableStateFlow<UIState<ReqresEntity>>(UIState.UnInitialized)
val userListStateFlow: StateFlow<UIState<ReqresEntity>> get() = _userListStateFlow
private val id = "admin5"

fun getUsers(page: Int) {
fun getMusicList() {
viewModelScope.launch {
_userListStateFlow.value = UIState.Loading
reqresRepository.getUsers(page)?.let {
_userListStateFlow.value = UIState.Success(it)
} ?: kotlin.run {
_userListStateFlow.value = UIState.Error
}
musicRepository.getMusicList(id)
.onSuccess {
_homeStateFlow.emit(HomeState.SuccessGetMusicList(it))
}.onFailure {
Log.e("HomeViewModel", "getMusicList() error: $it")
_homeStateFlow.emit(HomeState.Error)
}
}
}

fun postMusic(soptPostMusicBody: SoptPostMusicBody) {
viewModelScope.launch {
val image = soptPostMusicBody.image
val title = soptPostMusicBody.title.toRequestBody("text/plain".toMediaType())
val singer = soptPostMusicBody.singer.toRequestBody("text/plain".toMediaType())

musicRepository.postMusic(id, image, title, singer)
.onSuccess {
_homeStateFlow.emit(HomeState.SuccessPostMusic)
}.onFailure {
Log.e("HomeViewModel", "postMusic() error: $it")
_homeStateFlow.emit(HomeState.Error)
}
}
}
}
Loading