시배's Android

Design Patterns | Mavericks로 MVI 패턴 찍먹해보기 (3) 본문

Android/Design Patterns

Design Patterns | Mavericks로 MVI 패턴 찍먹해보기 (3)

si8ae 2023. 7. 24. 23:50

Mavericks + Compose + Naver API

Android 개발에서 최신 기술인 Mavericks Compose 활용하여 Naver API 사용하여 책을 검색하는 앱을 구현해보겠습니다.

MavericksState

data class SearchUiState(
    val keyword: String = "",
    val books: Async<List<Item>> = Uninitialized
) : MavericksState

검색 앱에서 사용할 SearchUiState 클래스는 Mavericks MavericksState 인터페이스를 구현합니다. 클래스는 검색 UI 상태를 관리하고, 검색어 키워드와 검색 결과인 목록을 저장합니다. Async 클래스는 비동기 작업을 처리하기 위해 Mavericks에서 제공하는 유틸리티 클래스이며, Uninitialized 상태에서 시작하여 검색 결과가 로딩 중인지, 성공적으로 로드되었는지, 혹은 오류가 발생했는지를 나타냅니다.

 

MavericksViewModel

class SearchViewModel @AssistedInject constructor(
    @Assisted initialState: SearchUiState,
    private val bookRepository: BookRepository
) : MavericksViewModel<SearchUiState>(initialState) {

    private lateinit var searchJob: Job

    fun fetchBookByKeyword(keyword: String) {
        setState {
            copy(keyword = keyword)
        }

        if (::searchJob.isInitialized && searchJob.isActive) {
            searchJob.cancel()
        }

        searchJob = suspend {
            delay(500)
            bookRepository.getBookByKeyword(keyword)
        }.execute { copy(books = it) }
    }

    @AssistedFactory
    interface Factory : AssistedViewModelFactory<SearchViewModel, SearchUiState> {
        override fun create(state: SearchUiState): SearchViewModel
    }

    companion object : MavericksViewModelFactory<SearchViewModel, SearchUiState> by hiltMavericksViewModelFactory()

}

fetchBookByKeyword 함수는 검색어 키워드를 인자로 받아 책을 검색하는 역할을 합니다. 우선, setState 함수를 사용하여 SearchUiState의 keyword 필드를 업데이트합니다. 그리고 이미 검색 작업이 진행 중인 경우, 이전 작업을 취소하고 새로운 검색 작업을 시작합니다.

searchJob은 검색 작업을 수행하는 코루틴의 Job을 저장합니다. 검색 요청을 받을 때마다 이전 작업이 취소되며, delay(500)을 통해 0.5초 딜레이를 적용한 후 bookRepository.getBookByKeyword(keyword) 함수를 호출하여 실제 책 검색을 수행합니다.

검색 결과는 execute { copy(books = it) } 통해 Async 감싸진 List<Item> 형태로 상태를 업데이트합니다.

 

@AssistedInject

@AssistedInject 어노테이션은 주입할 의존성이 있는 클래스의 생성자에 붙입니다. 이 어노테이션이 붙은 생성자는 Dagger Hilt에서 자동으로 의존성 주입을 처리합니다. 일반적으로 Dagger Hilt를 사용하면, @Inject 어노테이션만으로도 의존성 주입을 처리할 수 있습니다. 하지만 Mavericks에서는 @AssistedInject 어노테이션을 함께 사용하는 이유는 Mavericks의 상태 복원과 관련이 있습니다.

 

Mavericks는 상태를 자동으로 복원하는 기능을 제공하는데, 이를 위해 ViewModel 생성자의 인자를 미리 지정한 초기 상태로 초기화해야 합니다. 그러나 ViewModel 생성자의 인자가 상태와 관련된 데이터를 미리 받을 수 없는 경우가 있습니다. 예를 들어, 책 검색 앱에서 사용자가 검색어를 입력한 후에야 검색을 수행해야 합니다. 따라서 검색어가 입력되기 전까지는 검색 결과가 없는 상태를 가지고 있어야 합니다.

이때, @AssistedInject 어노테이션을 사용하면 Mavericks가 ViewModel의 의존성을 관리하는 동시에, 초기 상태를 미리 지정하지 않고 생성자에 추가적인 인자를 받을 수 있게 됩니다. 이렇게 함으로써 초기 상태를 비워두거나 기본 값을 설정하고, 검색어와 같은 필요한 데이터가 입력되는 시점에 상태를 업데이트할 수 있습니다.

 

@AssistedFactory

@AssistedFactory 어노테이션은 ViewModel의 팩토리 인터페이스를 정의할 때 사용됩니다. Mavericks는 ViewModel의 상태를 복원하기 위해 팩토리 인터페이스를 활용합니다. 일반적으로 Dagger Hilt를 사용할 때는 ViewModel을 생성하는 데에만 @Inject 어노테이션을 사용하면 됩니다. 그러나 Mavericks에서는 상태를 복원하기 위해 ViewModel의 팩토리 인터페이스를 정의해야 합니다.

@AssistedFactory 어노테이션을 사용하면, 상태를 복원하기 위해 Mavericks ViewModel 생성하고 관리하는 동시에, 팩토리 인터페이스를 통해 초기 상태를 전달할 있게 됩니다. 이를 통해 ViewModel 초기 상태를 외부에서 주입할 있으며, Mavericks 상태 관리 기능을 더욱 유연하게 사용할 있습니다.

 

 

ViewModelModule

@Module
@InstallIn(MavericksViewModelComponent::class)
interface ViewModelModule {

    @Binds
    @IntoMap
    @ViewModelKey(SearchViewModel::class)
    fun searchViewModelFactory(factory: SearchViewModel.Factory): AssistedViewModelFactory<*, *>
}

ViewModelModule SearchViewModel 대한 의존성 주입 정보를 제공합니다. SearchViewModel.Factory AssistedViewModelFactory<*, *> 바인딩하고 있으며, 이를 통해 SearchViewModel 초기 상태를 관리할 있습니다.

MavericksView

@Composable
fun SearchScreen(
    viewModel: SearchViewModel = mavericksViewModel()
) {
    val uiState by viewModel.collectAsState()

    SearchScreen(
        keyword = uiState.keyword,
        uiState = uiState,
        onValueChange = {
            viewModel.fetchBookByKeyword(it)
        }
    )
}

@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
@Composable
fun SearchScreen(
    keyword: String,
    uiState: SearchUiState,
    onValueChange: (String) -> Unit,
) {
    val keyboardController = LocalSoftwareKeyboardController.current

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(12.dp)
    ) {
        OutlinedTextField(
            modifier = Modifier.fillMaxWidth(),
            value = keyword,
            onValueChange = onValueChange,
            label = {
                Text(text = "책")
            },
            placeholder = {
                Text(text = "검색어를 입력하세요.")
            },
            keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
            keyboardActions = KeyboardActions(
                onSearch = {
                    keyboardController?.hide()
                }
            )
        )

        Spacer(modifier = Modifier.height(12.dp))

        Column(
            modifier = Modifier
                .fillMaxSize(),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            when (uiState.books) {
                is Loading -> {
                    CircularProgressIndicator()
                }

                is Success -> {
                    val books = uiState.books.invoke()
                    if (books.isEmpty()) {
                        Text("검색 결과가 없습니다.")
                    } else {
                        LazyColumn(modifier = Modifier.fillMaxSize()) {
                            items(books) {
                                Text(text = it.title)
                            }
                        }
                    }
                }

                is Fail -> {
                    Text(uiState.books.error.message ?: "네트워크를 확인하세요")
                }

                is Uninitialized -> {
                    Text("상단의 검색바를 통해 검색을 시작해보세요.")
                }
            }
        }
    }
}

코드는 SearchScreen Composable 함수에서 SearchViewModel 상태를 구독하고 있습니다. viewModel.collectAsState() 통해 ViewModel 상태를 Composable 함수에서 관찰할 있게 되며, 이를 통해 ViewModel 상태가 변경될 때마다 UI 자동으로 업데이트할 있습니다.

 

 

Design Patterns | Mavericks로 MVI 패턴 찍먹해보기 (1)

 

Design Patterns | Mavericks로 MVI 패턴 찍먹해보기 (1)

안드로이드 앱 개발에서 MVI (Model-View-Intent) 아키텍처는 현재 많은 개발자들에게 인기가 높아지고 있는 패턴입니다. MVI는 앱의 상태 관리와 UI 처리를 효과적으로 분리하여 앱의 유지보수성을 향

si8ae.tistory.com

Design Patterns | Mavericks로 MVI 패턴 찍먹해보기 (2)

 

Design Patterns | Mavericks로 MVI 패턴 찍먹해보기 (2)

Mavericks 매버릭스(Mavericks)는 Airbnb, Tonal 및 기타 대형 앱에서 사용되는 안드로이드 MVI 프레임워크로, 쉽게 배울 수 있으면서도 복잡한 플로우를 지원할만큼 강력합니다. 매버릭스를 만들 때 목표

si8ae.tistory.com

 

전체 구현 코드는 하단의 링크에서 확인할 수 있습니다.

 

GitHub - koreatlwls/Mavericks-MVI-Compose: Mavericks-MVI-Compose

Mavericks-MVI-Compose. Contribute to koreatlwls/Mavericks-MVI-Compose development by creating an account on GitHub.

github.com