From 462554a6f77a6350d427ccc217468a0dbcdd7744 Mon Sep 17 00:00:00 2001 From: WooJin Kong Date: Mon, 18 Mar 2024 09:42:32 +0900 Subject: [PATCH 1/6] feat: Migrate board, network and home to MVI architecture --- .../ui/activity/ArticleActivity.kt | 10 ++- .../koreatechboard/ui/base/BaseViewModel.kt | 20 +++++ .../kongjak/koreatechboard/ui/base/UiEvent.kt | 3 + .../kongjak/koreatechboard/ui/base/UiState.kt | 3 + .../kongjak/koreatechboard/ui/board/Board.kt | 45 +++++----- .../koreatechboard/ui/board/BoardEvent.kt | 8 ++ .../koreatechboard/ui/board/BoardInitEvent.kt | 8 ++ .../koreatechboard/ui/board/BoardInitState.kt | 8 ++ .../ui/board/BoardInitViewModel.kt | 26 +++--- .../koreatechboard/ui/board/BoardState.kt | 14 +++ .../koreatechboard/ui/board/BoardViewModel.kt | 54 +++++------ .../kongjak/koreatechboard/ui/home/Home.kt | 72 ++++++++++----- .../koreatechboard/ui/home/HomeBoardEvent.kt | 8 ++ .../koreatechboard/ui/home/HomeBoardState.kt | 12 +++ .../ui/home/HomeBoardViewModel.kt | 90 ++++++++++--------- .../koreatechboard/ui/network/NetworkEvent.kt | 8 ++ .../koreatechboard/ui/network/NetworkState.kt | 7 ++ .../ui/network/NetworkViewModel.kt | 40 +++++++++ .../koreatechboard/ui/state/NetworkState.kt | 7 -- .../ui/viewmodel/NetworkViewModel.kt | 38 -------- .../koreatechboard/util/NetworkUtil.kt | 7 +- 21 files changed, 306 insertions(+), 182 deletions(-) create mode 100644 app/src/main/java/com/kongjak/koreatechboard/ui/base/BaseViewModel.kt create mode 100644 app/src/main/java/com/kongjak/koreatechboard/ui/base/UiEvent.kt create mode 100644 app/src/main/java/com/kongjak/koreatechboard/ui/base/UiState.kt create mode 100644 app/src/main/java/com/kongjak/koreatechboard/ui/board/BoardEvent.kt create mode 100644 app/src/main/java/com/kongjak/koreatechboard/ui/board/BoardInitEvent.kt create mode 100644 app/src/main/java/com/kongjak/koreatechboard/ui/board/BoardInitState.kt create mode 100644 app/src/main/java/com/kongjak/koreatechboard/ui/board/BoardState.kt create mode 100644 app/src/main/java/com/kongjak/koreatechboard/ui/home/HomeBoardEvent.kt create mode 100644 app/src/main/java/com/kongjak/koreatechboard/ui/home/HomeBoardState.kt create mode 100644 app/src/main/java/com/kongjak/koreatechboard/ui/network/NetworkEvent.kt create mode 100644 app/src/main/java/com/kongjak/koreatechboard/ui/network/NetworkState.kt create mode 100644 app/src/main/java/com/kongjak/koreatechboard/ui/network/NetworkViewModel.kt delete mode 100644 app/src/main/java/com/kongjak/koreatechboard/ui/state/NetworkState.kt delete mode 100644 app/src/main/java/com/kongjak/koreatechboard/ui/viewmodel/NetworkViewModel.kt diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/activity/ArticleActivity.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/activity/ArticleActivity.kt index 45846b7e..58db8a41 100644 --- a/app/src/main/java/com/kongjak/koreatechboard/ui/activity/ArticleActivity.kt +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/activity/ArticleActivity.kt @@ -21,6 +21,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment @@ -32,9 +33,9 @@ import androidx.hilt.navigation.compose.hiltViewModel import com.kongjak.koreatechboard.R import com.kongjak.koreatechboard.ui.article.ArticleScreen import com.kongjak.koreatechboard.ui.article.ArticleViewModel -import com.kongjak.koreatechboard.ui.state.NetworkState +import com.kongjak.koreatechboard.ui.network.NetworkEvent import com.kongjak.koreatechboard.ui.theme.KoreatechBoardTheme -import com.kongjak.koreatechboard.ui.viewmodel.NetworkViewModel +import com.kongjak.koreatechboard.ui.network.NetworkViewModel import com.kongjak.koreatechboard.ui.viewmodel.ThemeViewModel import com.kongjak.koreatechboard.util.findActivity import dagger.hilt.android.AndroidEntryPoint @@ -120,8 +121,9 @@ fun Toolbar( } ) { contentPadding -> Column(modifier = Modifier.padding(contentPadding)) { - val networkState by networkViewModel.networkState.observeAsState() - if (networkState == NetworkState.Connected) { + val networkState by networkViewModel.uiState.collectAsState() + val isNetworkConnected = networkState.isConnected + if (isNetworkConnected) { ArticleScreen( articleViewModel = articleViewModel, themeViewModel = themeViewModel, diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/base/BaseViewModel.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/base/BaseViewModel.kt new file mode 100644 index 00000000..f3882470 --- /dev/null +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/base/BaseViewModel.kt @@ -0,0 +1,20 @@ +package com.kongjak.koreatechboard.ui.base + +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +open class BaseViewModel(initialState: S) : ViewModel() { + private val _uiState = MutableStateFlow(initialState) + val uiState get() = _uiState.asStateFlow() + + fun sendEvent(event: E) { + reduce(_uiState.value, event) + } + + fun setState(newState: S) { + _uiState.value = newState + } + + open fun reduce(oldState: S, event: E) {} +} \ No newline at end of file diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/base/UiEvent.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/base/UiEvent.kt new file mode 100644 index 00000000..c0335d2e --- /dev/null +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/base/UiEvent.kt @@ -0,0 +1,3 @@ +package com.kongjak.koreatechboard.ui.base + +interface UiEvent \ No newline at end of file diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/base/UiState.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/base/UiState.kt new file mode 100644 index 00000000..8e06d683 --- /dev/null +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/base/UiState.kt @@ -0,0 +1,3 @@ +package com.kongjak.koreatechboard.ui.base + +interface UiState \ No newline at end of file diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/board/Board.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/board/Board.kt index 95eca30a..78abe5bd 100644 --- a/app/src/main/java/com/kongjak/koreatechboard/ui/board/Board.kt +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/board/Board.kt @@ -48,6 +48,7 @@ import androidx.compose.material3.pullrefresh.rememberPullRefreshState import androidx.compose.material3.rememberBottomSheetScaffoldState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf @@ -76,17 +77,18 @@ import com.kongjak.koreatechboard.ui.activity.ArticleActivity import com.kongjak.koreatechboard.ui.activity.SearchActivity import com.kongjak.koreatechboard.ui.settings.deptList import com.kongjak.koreatechboard.ui.settings.fullDeptList -import com.kongjak.koreatechboard.ui.state.NetworkState +import com.kongjak.koreatechboard.ui.network.NetworkEvent import com.kongjak.koreatechboard.ui.theme.boardItemSubText import com.kongjak.koreatechboard.ui.theme.boardItemTitle -import com.kongjak.koreatechboard.ui.viewmodel.NetworkViewModel +import com.kongjak.koreatechboard.ui.network.NetworkViewModel import com.kongjak.koreatechboard.util.routes.Department import kotlinx.coroutines.launch @Composable fun BoardScreen(boardInitViewModel: BoardInitViewModel = hiltViewModel()) { - val initDepartment by boardInitViewModel.initDepartment.observeAsState(0) - val userDepartment by boardInitViewModel.userDepartment.observeAsState(0) + val uiState by boardInitViewModel.uiState.collectAsState() + val initDepartment = uiState.initDepartment + val userDepartment = uiState.userDepartment BottomSheetScaffold(fullDeptList[initDepartment], userDepartment) } @@ -140,23 +142,24 @@ fun Board(contentPadding: PaddingValues, department: Department) { fun BoardContent( department: Department, page: Int, - boardViewModel: BoardViewModel = hiltViewModel(), networkViewModel: NetworkViewModel = hiltViewModel() ) { - val lazyPostList = + val boardViewModel = + hiltViewModel(key = "${department.name}:${department.boards[page].board}") + + LaunchedEffect(key1 = department.name, key2 = department.boards[page].board) { boardViewModel.getAPI(department.name, department.boards[page].board) - .collectAsLazyPagingItems() + } + + val uiState by boardViewModel.uiState.collectAsState() + val lazyPostList = uiState.boardItemsMap.collectAsLazyPagingItems() val pullRefreshState = rememberPullRefreshState(lazyPostList.loadState.refresh is LoadState.Loading, { - boardViewModel.cleanUpCachedData( - site = department.name, - board = department.boards[page].board - ) lazyPostList.refresh() }) - val showArticleNumber by boardViewModel.showNumber.observeAsState(true) + val showArticleNumber = uiState.showNumber val context = LocalContext.current val snackbarHostState = remember { SnackbarHostState() } @@ -169,13 +172,13 @@ fun BoardContent( SnackbarHost(hostState = snackbarHostState) }, content = { contentPadding -> - val prevNetworkState by networkViewModel.prevNetworkState.observeAsState(NetworkState.Unknown) - val networkState by networkViewModel.networkState.observeAsState(NetworkState.Unknown) + val networkState by networkViewModel.uiState.collectAsState() + val isNetworkConnected = networkState.isConnected LaunchedEffect(key1 = networkState) { - if (networkState == NetworkState.Disconnected) { + if (!isNetworkConnected) { snackbarHostState.showSnackbar(context.getString(R.string.network_unavailable)) - } else if (networkState == NetworkState.Connected && prevNetworkState == NetworkState.Disconnected) { + } else if (isNetworkConnected) { lazyPostList.retry() } } @@ -233,24 +236,24 @@ fun BoardContent( } lazyPostList.apply { - when (networkState) { - NetworkState.Connected -> { + when (isNetworkConnected) { + true -> { when { loadState.refresh is LoadState.Error -> { val errorMessage = (loadState.refresh as LoadState.Error).error.localizedMessage - item { BoardError(errorMessage) } + item { BoardError(errorMessage ?: "") } } loadState.append is LoadState.Error -> { val errorMessage = (loadState.append as LoadState.Error).error.localizedMessage - item { BoardError(errorMessage) } + item { BoardError(errorMessage ?: "") } } } } - else -> item { NetworkUnavailable() } + false -> item { NetworkUnavailable() } } } } diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/board/BoardEvent.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/board/BoardEvent.kt new file mode 100644 index 00000000..54a3e17d --- /dev/null +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/board/BoardEvent.kt @@ -0,0 +1,8 @@ +package com.kongjak.koreatechboard.ui.board + +import com.kongjak.koreatechboard.ui.base.UiEvent + +sealed class BoardEvent : UiEvent { + data class ShowNumberUpdated(val showNumber: Boolean) : BoardEvent() + data class FetchData(val department: String, val board: String) : BoardEvent() +} \ No newline at end of file diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/board/BoardInitEvent.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/board/BoardInitEvent.kt new file mode 100644 index 00000000..051236d6 --- /dev/null +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/board/BoardInitEvent.kt @@ -0,0 +1,8 @@ +package com.kongjak.koreatechboard.ui.board + +import com.kongjak.koreatechboard.ui.base.UiEvent + +sealed class BoardInitEvent : UiEvent { + data class InitDepartmentUpdated(val value: Int) : BoardInitEvent() + data class UserDepartmentUpdated(val value: Int) : BoardInitEvent() +} \ No newline at end of file diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/board/BoardInitState.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/board/BoardInitState.kt new file mode 100644 index 00000000..9699d8da --- /dev/null +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/board/BoardInitState.kt @@ -0,0 +1,8 @@ +package com.kongjak.koreatechboard.ui.board + +import com.kongjak.koreatechboard.ui.base.UiState + +data class BoardInitState( + val initDepartment: Int = 0, + val userDepartment: Int = 0 +) : UiState diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/board/BoardInitViewModel.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/board/BoardInitViewModel.kt index b6342e73..d80c441e 100644 --- a/app/src/main/java/com/kongjak/koreatechboard/ui/board/BoardInitViewModel.kt +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/board/BoardInitViewModel.kt @@ -1,11 +1,9 @@ package com.kongjak.koreatechboard.ui.board -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.kongjak.koreatechboard.domain.usecase.GetInitDepartmentUseCase import com.kongjak.koreatechboard.domain.usecase.GetUserDepartmentUseCase +import com.kongjak.koreatechboard.ui.base.BaseViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch @@ -15,16 +13,7 @@ import javax.inject.Inject class BoardInitViewModel @Inject constructor( private val getUserDepartmentUseCase: GetUserDepartmentUseCase, private val getInitDepartmentUseCase: GetInitDepartmentUseCase -) : - ViewModel() { - - private val _initDepartment = MutableLiveData(0) - val initDepartment: LiveData - get() = _initDepartment - - private val _userDepartment = MutableLiveData(0) - val userDepartment: LiveData - get() = _userDepartment +) : BaseViewModel(BoardInitState()) { init { getInitDepartment() @@ -34,7 +23,7 @@ class BoardInitViewModel @Inject constructor( private fun getInitDepartment() { viewModelScope.launch { getInitDepartmentUseCase().collectLatest { - _initDepartment.value = it + sendEvent(BoardInitEvent.InitDepartmentUpdated(it)) } } } @@ -42,8 +31,15 @@ class BoardInitViewModel @Inject constructor( private fun getUserDepartment() { viewModelScope.launch { getUserDepartmentUseCase().collectLatest { - _userDepartment.value = it + sendEvent(BoardInitEvent.UserDepartmentUpdated(it)) } } } + + override fun reduce(oldState: BoardInitState, event: BoardInitEvent) { + when (event) { + is BoardInitEvent.InitDepartmentUpdated -> setState(oldState.copy(initDepartment = event.value)) + is BoardInitEvent.UserDepartmentUpdated -> setState(oldState.copy(userDepartment = event.value)) + } + } } diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/board/BoardState.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/board/BoardState.kt new file mode 100644 index 00000000..f916c06e --- /dev/null +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/board/BoardState.kt @@ -0,0 +1,14 @@ +package com.kongjak.koreatechboard.ui.board + +import androidx.paging.PagingData +import com.kongjak.koreatechboard.domain.model.BoardData +import com.kongjak.koreatechboard.ui.base.UiState +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow + +data class BoardState( + val department: String = "", + val board: String = "", + val showNumber: Boolean = true, + val boardItemsMap: Flow> = emptyFlow(), +) : UiState diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/board/BoardViewModel.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/board/BoardViewModel.kt index 3d28da26..1df9f97e 100644 --- a/app/src/main/java/com/kongjak/koreatechboard/ui/board/BoardViewModel.kt +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/board/BoardViewModel.kt @@ -1,16 +1,11 @@ package com.kongjak.koreatechboard.ui.board -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import androidx.paging.PagingData import androidx.paging.cachedIn -import com.kongjak.koreatechboard.domain.model.BoardData import com.kongjak.koreatechboard.domain.usecase.GetBoardUseCase import com.kongjak.koreatechboard.domain.usecase.GetShowArticleNumberUseCase +import com.kongjak.koreatechboard.ui.base.BaseViewModel import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import javax.inject.Inject @@ -19,36 +14,43 @@ import javax.inject.Inject class BoardViewModel @Inject constructor( private val getBoardUseCase: GetBoardUseCase, private val getShowArticleNumberUseCase: GetShowArticleNumberUseCase -) : - ViewModel() { - - private val boardItemsMap = mutableMapOf>?>() - - private val _showNumber = MutableLiveData(true) - val showNumber: LiveData - get() = _showNumber +) : BaseViewModel(BoardState()) { init { getShowArticleNumber() } - fun getAPI(site: String, board: String): Flow> { - val key = "$site:$board" - if (boardItemsMap[key] == null) { - boardItemsMap[key] = getBoardUseCase(site, board).cachedIn(viewModelScope) - } - return boardItemsMap[key]!! - } - - fun cleanUpCachedData(site: String, board: String) { - val key = "$site:$board" - boardItemsMap[key] = null + fun getAPI(department: String, board: String) { + sendEvent(BoardEvent.FetchData(department, board)) } private fun getShowArticleNumber() { viewModelScope.launch { getShowArticleNumberUseCase().collectLatest { - _showNumber.value = it + sendEvent(BoardEvent.ShowNumberUpdated(it)) + } + } + } + + override fun reduce(oldState: BoardState, event: BoardEvent) { + when (event) { + is BoardEvent.ShowNumberUpdated -> { + setState(oldState.copy(showNumber = event.showNumber)) + } + + is BoardEvent.FetchData -> { + if (oldState.department == event.department && oldState.board == event.board) { + return + } + setState( + oldState.copy( + department = event.department, + board = event.board, + boardItemsMap = getBoardUseCase(event.department, event.board).cachedIn( + viewModelScope + ) + ) + ) } } } diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/home/Home.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/home/Home.kt index ce2b4a88..0b6ccd58 100644 --- a/app/src/main/java/com/kongjak/koreatechboard/ui/home/Home.kt +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/home/Home.kt @@ -1,6 +1,7 @@ package com.kongjak.koreatechboard.ui.home import android.content.Intent +import android.util.Log import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -22,6 +23,7 @@ import androidx.compose.material3.Tab import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf @@ -39,16 +41,19 @@ import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import com.kongjak.koreatechboard.R import com.kongjak.koreatechboard.ui.activity.ArticleActivity +import com.kongjak.koreatechboard.ui.network.NetworkViewModel import com.kongjak.koreatechboard.ui.settings.deptList -import com.kongjak.koreatechboard.ui.state.NetworkState -import com.kongjak.koreatechboard.ui.viewmodel.NetworkViewModel import com.kongjak.koreatechboard.util.routes.Department import kotlinx.coroutines.launch @Composable -fun HomeScreen(homeViewModel: HomeViewModel = hiltViewModel(), networkViewModel: NetworkViewModel = hiltViewModel()) { - val networkState by networkViewModel.networkState.observeAsState() - if (networkState == NetworkState.Connected) { +fun HomeScreen( + homeViewModel: HomeViewModel = hiltViewModel(), + networkViewModel: NetworkViewModel = hiltViewModel() +) { + val networkState by networkViewModel.uiState.collectAsState() + val isNetworkConnected = networkState.isConnected + if (isNetworkConnected) { Column( modifier = Modifier .verticalScroll(rememberScrollState()) @@ -61,7 +66,11 @@ fun HomeScreen(homeViewModel: HomeViewModel = hiltViewModel(), networkViewModel: BoardInMain(department = selectedDepartment) } } else { - Column(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { Text(text = stringResource(id = R.string.network_unavailable)) } } @@ -75,12 +84,6 @@ fun BoardInMain( ) { val context = LocalContext.current - var key by remember { - mutableStateOf(department.boards[0].board) - } - val isSuccess by homeBoardViewModel.isSuccess.observeAsState(true) - val isLoaded by homeBoardViewModel.isLoaded.observeAsState(false) - val pagerState = rememberPagerState( initialPage = 0 ) { @@ -91,10 +94,20 @@ fun BoardInMain( val tabIndex = pagerState.currentPage + var key by remember { + mutableStateOf(department.boards[0].board) + } + LaunchedEffect(key1 = tabIndex) { homeBoardViewModel.getApi(department.name, key) } + val uiState = homeBoardViewModel.uiState.collectAsState() + val isSuccess = uiState.value.isSuccess + val isLoaded = uiState.value.isLoaded + val boardList = uiState.value.boardList + val statusCode = uiState.value.statusCode + Card( modifier = Modifier .fillMaxWidth() @@ -132,7 +145,6 @@ fun BoardInMain( verticalAlignment = Alignment.Top ) { page -> key = department.boards[page].board - homeBoardViewModel.getApi(department.name, key) if (!isLoaded) { Column( @@ -144,13 +156,14 @@ fun BoardInMain( CircularProgressIndicator() } } else { - if (isSuccess && homeBoardViewModel.statusCode.value!! == 200) { + if (isSuccess && statusCode == 200) { Column { - homeBoardViewModel.boardList[key]!!.value!!.forEach { data -> + boardList[key]!!.forEach { data -> Box( modifier = Modifier .clickable { - val intent = Intent(context, ArticleActivity::class.java) + val intent = + Intent(context, ArticleActivity::class.java) intent.putExtra("site", department.name) intent.putExtra("uuid", data.uuid.toString()) context.startActivity(intent) @@ -168,20 +181,33 @@ fun BoardInMain( HorizontalDivider(thickness = 0.5.dp, color = Color.Gray) } } - } else if (isSuccess && homeBoardViewModel.statusCode.value!! != 200) { - Column(modifier = Modifier.fillMaxWidth().padding(8.dp)) { - Text(text = stringResource(R.string.error_server_down, homeBoardViewModel.statusCode.value!!)) + } else if (isSuccess && statusCode != 200) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) { + Text( + text = stringResource( + R.string.error_server_down, + statusCode + ) + ) Button(onClick = { - homeBoardViewModel.getApi(department.name, key, true) + homeBoardViewModel.getApi(department.name, key) }) { Text(text = stringResource(id = R.string.error_retry)) } } } else if (!isSuccess) { - Column(modifier = Modifier.fillMaxWidth().padding(8.dp)) { - Text(text = homeBoardViewModel.error.value!!) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) { + Text(text = uiState.value.error) Button(onClick = { - homeBoardViewModel.getApi(department.name, key, true) + homeBoardViewModel.getApi(department.name, key) }) { Text(text = stringResource(id = R.string.error_retry)) } diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/home/HomeBoardEvent.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/home/HomeBoardEvent.kt new file mode 100644 index 00000000..03f4e2ea --- /dev/null +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/home/HomeBoardEvent.kt @@ -0,0 +1,8 @@ +package com.kongjak.koreatechboard.ui.home + +import com.kongjak.koreatechboard.ui.base.UiEvent +import com.kongjak.koreatechboard.util.routes.BoardItem + +sealed class HomeBoardEvent : UiEvent { + data class FetchData(val department: String, val board: String) : HomeBoardEvent() +} \ No newline at end of file diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/home/HomeBoardState.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/home/HomeBoardState.kt new file mode 100644 index 00000000..321fcb50 --- /dev/null +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/home/HomeBoardState.kt @@ -0,0 +1,12 @@ +package com.kongjak.koreatechboard.ui.home + +import com.kongjak.koreatechboard.domain.model.BoardData +import com.kongjak.koreatechboard.ui.base.UiState + +data class HomeBoardState( + val isLoaded: Boolean = false, + val isSuccess: Boolean = false, + val boardList: Map> = emptyMap(), + val statusCode: Int = 0, + val error: String = "" +) : UiState diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/home/HomeBoardViewModel.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/home/HomeBoardViewModel.kt index be15f6c2..46c4ba4f 100644 --- a/app/src/main/java/com/kongjak/koreatechboard/ui/home/HomeBoardViewModel.kt +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/home/HomeBoardViewModel.kt @@ -1,62 +1,64 @@ package com.kongjak.koreatechboard.ui.home -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.kongjak.koreatechboard.domain.base.ResponseResult -import com.kongjak.koreatechboard.domain.model.BoardData import com.kongjak.koreatechboard.domain.usecase.GetBoardMinimumUseCase +import com.kongjak.koreatechboard.ui.base.BaseViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel -class HomeBoardViewModel @Inject constructor(private val getBoardMinimumUseCase: GetBoardMinimumUseCase) : ViewModel() { - private val _isLoaded = MutableLiveData(false) - val isLoaded: LiveData - get() = _isLoaded +class HomeBoardViewModel @Inject constructor(private val getBoardMinimumUseCase: GetBoardMinimumUseCase) : + BaseViewModel(HomeBoardState()) { - private val _isSuccess = MutableLiveData(true) - val isSuccess: LiveData - get() = _isSuccess - - private val _boardList = mutableMapOf>>() - val boardList: Map>> - get() = _boardList - - private val _statusCode = MutableLiveData(200) - val statusCode: LiveData - get() = _statusCode + fun getApi(site: String, board: String) { + sendEvent(HomeBoardEvent.FetchData(site, board)) + } - private val _error = MutableLiveData("") - val error: LiveData - get() = _error + override fun reduce(oldState: HomeBoardState, event: HomeBoardEvent) { + when (event) { + is HomeBoardEvent.FetchData -> { + setState( + oldState.copy( + isLoaded = false + ) + ) + viewModelScope.launch { + runCatching { + getBoardMinimumUseCase(event.department, event.board) + }.onSuccess { + when (it) { + is ResponseResult.Success -> { + setState( + oldState.copy( + isSuccess = true, + boardList = (oldState.boardList + (event.board to (it.data.boardData)!!)), + statusCode = it.data.statusCode, + isLoaded = true + ) + ) + } - fun getApi(site: String, board: String, isReload: Boolean = false) { - if (!boardList.containsKey(board) || isReload) { - _isLoaded.value = false - _boardList[board] = MutableLiveData(emptyList()) - viewModelScope.launch { - runCatching { - getBoardMinimumUseCase(site, board) - }.onSuccess { - _isSuccess.postValue(true) - when (it) { - is ResponseResult.Success -> { - _boardList[board]!!.postValue(it.data.boardData) - _statusCode.value = it.data.statusCode - _isLoaded.postValue(true) - } - is ResponseResult.Error -> { - _statusCode.value = it.errorType.statusCode - _isLoaded.postValue(true) + is ResponseResult.Error -> { + setState( + oldState.copy( + isSuccess = false, + statusCode = it.errorType.statusCode, + error = it.errorType.statusCode.toString() + ) + ) + } } + }.onFailure { + setState( + oldState.copy( + isSuccess = false, + error = it.localizedMessage ?: "", + isLoaded = true + ) + ) } - }.onFailure { - _isLoaded.postValue(true) - _isSuccess.postValue(false) - _error.value = it.localizedMessage } } } diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/network/NetworkEvent.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/network/NetworkEvent.kt new file mode 100644 index 00000000..5521a9dc --- /dev/null +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/network/NetworkEvent.kt @@ -0,0 +1,8 @@ +package com.kongjak.koreatechboard.ui.network + +import com.kongjak.koreatechboard.ui.base.UiEvent + +sealed class NetworkEvent : UiEvent { + object Connected : NetworkEvent() + object Disconnected : NetworkEvent() +} diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/network/NetworkState.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/network/NetworkState.kt new file mode 100644 index 00000000..91265933 --- /dev/null +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/network/NetworkState.kt @@ -0,0 +1,7 @@ +package com.kongjak.koreatechboard.ui.network + +import com.kongjak.koreatechboard.ui.base.UiState + +data class NetworkState( + val isConnected: Boolean = true +) : UiState \ No newline at end of file diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/network/NetworkViewModel.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/network/NetworkViewModel.kt new file mode 100644 index 00000000..091be5a4 --- /dev/null +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/network/NetworkViewModel.kt @@ -0,0 +1,40 @@ +package com.kongjak.koreatechboard.ui.network + +import androidx.lifecycle.viewModelScope +import com.kongjak.koreatechboard.ui.base.BaseViewModel +import com.kongjak.koreatechboard.util.NetworkUtil +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class NetworkViewModel @Inject constructor( + private val networkUtil: NetworkUtil +) : BaseViewModel(NetworkState()) { + + init { + getNetworkState() + } + + private fun getNetworkState() { + viewModelScope.launch { + networkUtil.networkState().collectLatest { networkConnected -> + if (networkConnected) sendEvent(NetworkEvent.Connected) + else sendEvent(NetworkEvent.Disconnected) + } + } + } + + override fun reduce(oldState: NetworkState, event: NetworkEvent) { + when (event) { + is NetworkEvent.Connected -> { + setState(oldState.copy(isConnected = true)) + } + + is NetworkEvent.Disconnected -> { + setState(oldState.copy(isConnected = false)) + } + } + } +} diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/state/NetworkState.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/state/NetworkState.kt deleted file mode 100644 index 897e9193..00000000 --- a/app/src/main/java/com/kongjak/koreatechboard/ui/state/NetworkState.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.kongjak.koreatechboard.ui.state - -sealed class NetworkState { - object Connected : NetworkState() - object Disconnected : NetworkState() - object Unknown : NetworkState() -} diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/viewmodel/NetworkViewModel.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/viewmodel/NetworkViewModel.kt deleted file mode 100644 index 4589239f..00000000 --- a/app/src/main/java/com/kongjak/koreatechboard/ui/viewmodel/NetworkViewModel.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.kongjak.koreatechboard.ui.viewmodel - -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.kongjak.koreatechboard.ui.state.NetworkState -import com.kongjak.koreatechboard.util.NetworkUtil -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch -import javax.inject.Inject - -@HiltViewModel -class NetworkViewModel @Inject constructor( - private val networkUtil: NetworkUtil -) : ViewModel() { - private val _prevNetworkState = MutableLiveData() - val prevNetworkState: LiveData - get() = _prevNetworkState - - private val _networkState = MutableLiveData() - val networkState: LiveData - get() = _networkState - - init { - getNetworkState() - } - - private fun getNetworkState() { - viewModelScope.launch { - networkUtil.networkState().collectLatest { - _prevNetworkState.value = _networkState.value - _networkState.value = it - } - } - } -} diff --git a/app/src/main/java/com/kongjak/koreatechboard/util/NetworkUtil.kt b/app/src/main/java/com/kongjak/koreatechboard/util/NetworkUtil.kt index b33e7261..974a0204 100644 --- a/app/src/main/java/com/kongjak/koreatechboard/util/NetworkUtil.kt +++ b/app/src/main/java/com/kongjak/koreatechboard/util/NetworkUtil.kt @@ -3,7 +3,6 @@ package com.kongjak.koreatechboard.util import android.content.Context import android.net.ConnectivityManager import android.net.Network -import com.kongjak.koreatechboard.ui.state.NetworkState import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.callbackFlow import javax.inject.Inject @@ -14,15 +13,15 @@ class NetworkUtil @Inject constructor(context: Context) { fun networkState() = callbackFlow { val networkCallback = object : ConnectivityManager.NetworkCallback() { override fun onAvailable(network: Network) { - trySend(NetworkState.Connected) + trySend(true) } override fun onLost(network: Network) { - trySend(NetworkState.Disconnected) + trySend(false) } override fun onUnavailable() { - trySend(NetworkState.Disconnected) + trySend(false) } } From 4cbbe9473dca0a194a6d4b29ff66658be78d1e40 Mon Sep 17 00:00:00 2001 From: WooJin Kong Date: Mon, 18 Mar 2024 10:18:59 +0900 Subject: [PATCH 2/6] Migrate article and search to MVI architecture --- .../ui/activity/ArticleActivity.kt | 16 ++-- .../koreatechboard/ui/article/Article.kt | 19 ++-- .../koreatechboard/ui/article/ArticleEvent.kt | 8 ++ .../koreatechboard/ui/article/ArticleState.kt | 18 ++++ .../ui/article/ArticleViewModel.kt | 96 +++++++++---------- .../koreatechboard/ui/search/Search.kt | 12 ++- .../koreatechboard/ui/search/SearchEvent.kt | 8 ++ .../koreatechboard/ui/search/SearchState.kt | 16 ++++ .../ui/search/SearchViewModel.kt | 28 +++++- 9 files changed, 147 insertions(+), 74 deletions(-) create mode 100644 app/src/main/java/com/kongjak/koreatechboard/ui/article/ArticleEvent.kt create mode 100644 app/src/main/java/com/kongjak/koreatechboard/ui/article/ArticleState.kt create mode 100644 app/src/main/java/com/kongjak/koreatechboard/ui/search/SearchEvent.kt create mode 100644 app/src/main/java/com/kongjak/koreatechboard/ui/search/SearchState.kt diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/activity/ArticleActivity.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/activity/ArticleActivity.kt index 58db8a41..93fb203e 100644 --- a/app/src/main/java/com/kongjak/koreatechboard/ui/activity/ArticleActivity.kt +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/activity/ArticleActivity.kt @@ -38,6 +38,7 @@ import com.kongjak.koreatechboard.ui.theme.KoreatechBoardTheme import com.kongjak.koreatechboard.ui.network.NetworkViewModel import com.kongjak.koreatechboard.ui.viewmodel.ThemeViewModel import com.kongjak.koreatechboard.util.findActivity +import com.kongjak.koreatechboard.util.routes.Department import dagger.hilt.android.AndroidEntryPoint import java.util.UUID @@ -48,18 +49,18 @@ class ArticleActivity : ComponentActivity() { super.onCreate(savedInstanceState) val uuid = UUID.fromString(intent.getStringExtra("uuid")) - val site = intent.getStringExtra("site")!! + val department = intent.getStringExtra("site")!! setContent { - ArticleMain(site = site, uuid = uuid) + ArticleMain(department = department, uuid = uuid) } } } @Composable -fun ArticleMain(articleViewModel: ArticleViewModel = hiltViewModel(), site: String, uuid: UUID) { +fun ArticleMain(articleViewModel: ArticleViewModel = hiltViewModel(), department: String, uuid: UUID) { val context = LocalContext.current - Toolbar(articleViewModel = articleViewModel, context = context, site = site, uuid = uuid) + Toolbar(articleViewModel = articleViewModel, context = context, department = department, uuid = uuid) } @OptIn(ExperimentalMaterial3Api::class) @@ -69,10 +70,11 @@ fun Toolbar( themeViewModel: ThemeViewModel = hiltViewModel(), networkViewModel: NetworkViewModel = hiltViewModel(), context: Context, - site: String, + department: String, uuid: UUID ) { - val articleUrl by articleViewModel.url.observeAsState() + val uiState by articleViewModel.uiState.collectAsState() + val articleUrl = uiState.url val isDynamicColor by themeViewModel.isDynamicTheme.observeAsState(true) val isDarkTheme by themeViewModel.isDarkTheme.observeAsState() @@ -127,7 +129,7 @@ fun Toolbar( ArticleScreen( articleViewModel = articleViewModel, themeViewModel = themeViewModel, - site = site, + department = department, uuid = uuid ) } else { diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/article/Article.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/article/Article.kt index 875caedf..97e225c7 100644 --- a/app/src/main/java/com/kongjak/koreatechboard/ui/article/Article.kt +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/article/Article.kt @@ -18,6 +18,7 @@ import androidx.compose.material3.pullrefresh.pullRefresh import androidx.compose.material3.pullrefresh.rememberPullRefreshState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.key import androidx.compose.runtime.livedata.observeAsState @@ -40,23 +41,23 @@ import java.util.UUID fun ArticleScreen( articleViewModel: ArticleViewModel, themeViewModel: ThemeViewModel, - site: String, + department: String, uuid: UUID ) { - val isLoading by articleViewModel.isLoading.observeAsState(false) + val uiState by articleViewModel.uiState.collectAsState() + + val isLoading = uiState.isLoading val pullRefreshState = - rememberPullRefreshState(isLoading, { articleViewModel.getArticleData() }) + rememberPullRefreshState(isLoading, { articleViewModel.getArticleData(department, uuid) }) - val data by articleViewModel.article.observeAsState() + val data = uiState.article val context = LocalContext.current val contentTextView = remember { TextView(context) } val filesTextView = remember { TextView(context) } LaunchedEffect(key1 = Unit) { - articleViewModel.setSiteData(site) - articleViewModel.setUUIDData(uuid) - articleViewModel.getArticleData() + articleViewModel.getArticleData(department, uuid) } Box( @@ -114,7 +115,7 @@ fun ArticleScreen( .padding(16.dp) .fillMaxSize(), update = { - it.htmlText = data?.content + it.htmlText = data.content it.textSize = 16F it.autoLinkMask = 0x0f it.setTextColor(textColor.toArgb()) @@ -126,7 +127,7 @@ fun ArticleScreen( modifier = Modifier .padding(16.dp), update = { - it.fileText = data?.files + it.fileText = data.files it.setTextColor(textColor.toArgb()) } ) diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/article/ArticleEvent.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/article/ArticleEvent.kt new file mode 100644 index 00000000..0189f06f --- /dev/null +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/article/ArticleEvent.kt @@ -0,0 +1,8 @@ +package com.kongjak.koreatechboard.ui.article + +import com.kongjak.koreatechboard.ui.base.UiEvent +import java.util.UUID + +sealed class ArticleEvent : UiEvent { + data class FetchData(val department: String, val uuid: UUID) : ArticleEvent() +} \ No newline at end of file diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/article/ArticleState.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/article/ArticleState.kt new file mode 100644 index 00000000..aee6b960 --- /dev/null +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/article/ArticleState.kt @@ -0,0 +1,18 @@ +package com.kongjak.koreatechboard.ui.article + +import com.kongjak.koreatechboard.domain.model.Article +import com.kongjak.koreatechboard.ui.base.UiState +import com.kongjak.koreatechboard.util.routes.Department +import java.util.UUID + +data class ArticleState( + val isLoading: Boolean = false, + val isLoaded: Boolean = false, + val isSuccess: Boolean = false, + val article: Article? = null, + val uuid: UUID = UUID.randomUUID(), + val department: String = Department.School.name, + val statusCode: Int = 200, + val url: String = "", + val error: String = "" +) : UiState \ No newline at end of file diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/article/ArticleViewModel.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/article/ArticleViewModel.kt index 64d936d8..8ced7262 100644 --- a/app/src/main/java/com/kongjak/koreatechboard/ui/article/ArticleViewModel.kt +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/article/ArticleViewModel.kt @@ -1,12 +1,10 @@ package com.kongjak.koreatechboard.ui.article -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.kongjak.koreatechboard.domain.base.ResponseResult -import com.kongjak.koreatechboard.domain.model.Article import com.kongjak.koreatechboard.domain.usecase.GetArticleUseCase +import com.kongjak.koreatechboard.ui.base.BaseViewModel +import com.kongjak.koreatechboard.util.routes.Department import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import java.util.UUID @@ -15,62 +13,56 @@ import javax.inject.Inject @HiltViewModel class ArticleViewModel @Inject constructor( private val getArticleUseCase: GetArticleUseCase -) : - ViewModel() { - private val _article = MutableLiveData
() - val article: LiveData
- get() = _article +) : BaseViewModel(ArticleState()) { - private val _uuid = MutableLiveData() - val uuid: LiveData - get() = _uuid - - private val _site = MutableLiveData() - val site: LiveData - get() = _site - - private val _isLoading = MutableLiveData(false) - val isLoading: LiveData - get() = _isLoading - - private val _statusCode = MutableLiveData(200) - val statusCode: LiveData - get() = _statusCode - - private val _url = MutableLiveData() - val url: LiveData - get() = _url - - fun setUUIDData(uuid: UUID) { - _uuid.value = uuid + fun getArticleData(department: String, uuid: UUID) { + sendEvent(ArticleEvent.FetchData(department, uuid)) } - fun setSiteData(site: String) { - _site.value = site - } - - fun getArticleData() { - viewModelScope.launch { - _isLoading.value = true + override fun reduce(oldState: ArticleState, event: ArticleEvent) { + when (event) { + is ArticleEvent.FetchData -> { + viewModelScope.launch { + setState(oldState.copy(isLoading = true, isLoaded = false)) - runCatching { - getArticleUseCase(site.value!!, uuid.value!!) - }.onSuccess { - when (it) { - is ResponseResult.Success -> { - _article.value = it.data!! - _statusCode.value = it.data.statusCode - _url.value = it.data.articleUrl - } + runCatching { + getArticleUseCase(event.department, event.uuid) + }.onSuccess { + when (it) { + is ResponseResult.Success -> { + setState( + oldState.copy( + isSuccess = true, + article = it.data, + isLoading = false, + isLoaded = true + ) + ) + } - is ResponseResult.Error -> { - _statusCode.value = it.errorType.statusCode + is ResponseResult.Error -> { + setState( + oldState.copy( + isSuccess = false, + isLoading = false, + statusCode = it.errorType.statusCode, + error = it.errorType.statusCode.toString() + ) + ) + } + } + }.onFailure { + setState( + oldState.copy( + isSuccess = false, + isLoading = false, + error = it.localizedMessage ?: "", + isLoaded = true + ) + ) } } - }.onFailure { } - - _isLoading.value = false } } } diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/search/Search.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/search/Search.kt index 65999a3a..ba23343d 100644 --- a/app/src/main/java/com/kongjak/koreatechboard/ui/search/Search.kt +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/search/Search.kt @@ -20,6 +20,9 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -81,8 +84,13 @@ fun SearchContent( ) { val context = LocalContext.current - val lazyPostList = - searchViewModel.getAPI(site, board, title).collectAsLazyPagingItems() + val uiState by searchViewModel.uiState.collectAsState() + + LaunchedEffect(key1 = Unit) { + searchViewModel.getAPI(site, board, title) + } + + val lazyPostList = uiState.boardData.collectAsLazyPagingItems() LazyColumn( modifier = Modifier diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/search/SearchEvent.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/search/SearchEvent.kt new file mode 100644 index 00000000..9887e086 --- /dev/null +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/search/SearchEvent.kt @@ -0,0 +1,8 @@ +package com.kongjak.koreatechboard.ui.search + +import com.kongjak.koreatechboard.ui.base.UiEvent + +sealed class SearchEvent : UiEvent { + data class FetchData(val department: String, val board: String, val title: String) : + SearchEvent() +} \ No newline at end of file diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/search/SearchState.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/search/SearchState.kt new file mode 100644 index 00000000..698c2c44 --- /dev/null +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/search/SearchState.kt @@ -0,0 +1,16 @@ +package com.kongjak.koreatechboard.ui.search + +import androidx.paging.PagingData +import com.kongjak.koreatechboard.domain.model.BoardData +import com.kongjak.koreatechboard.ui.base.UiState +import com.kongjak.koreatechboard.util.routes.BoardItem +import com.kongjak.koreatechboard.util.routes.Department +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow + +data class SearchState( + val department: String = Department.School.name, + val board: String = BoardItem.Notice.board, + val title: String = "", + val boardData: Flow> = emptyFlow() +) : UiState diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/search/SearchViewModel.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/search/SearchViewModel.kt index 1f993928..de04f2ef 100644 --- a/app/src/main/java/com/kongjak/koreatechboard/ui/search/SearchViewModel.kt +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/search/SearchViewModel.kt @@ -1,19 +1,39 @@ package com.kongjak.koreatechboard.ui.search -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.paging.PagingData import androidx.paging.cachedIn import com.kongjak.koreatechboard.domain.model.BoardData import com.kongjak.koreatechboard.domain.usecase.SearchBoardWithTitleUseCase +import com.kongjak.koreatechboard.ui.base.BaseViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.Flow import javax.inject.Inject @HiltViewModel class SearchViewModel @Inject constructor(private val searchBoardWithTitleUseCase: SearchBoardWithTitleUseCase) : - ViewModel() { + BaseViewModel(SearchState()) { - fun getAPI(site: String, board: String, title: String): Flow> = - searchBoardWithTitleUseCase(site, board, title).cachedIn(viewModelScope) + fun getAPI(department: String, board: String, title: String) { + sendEvent(SearchEvent.FetchData(department, board, title)) + } + + override fun reduce(oldState: SearchState, event: SearchEvent) { + when (event) { + is SearchEvent.FetchData -> { + setState( + oldState.copy( + department = event.department, + board = event.board, + title = event.title, + boardData = searchBoardWithTitleUseCase( + event.department, + event.board, + event.title + ).cachedIn(viewModelScope) + ) + ) + } + } + } } From 731fe226217ad679c48a9b9ef14e5c3e08d2769b Mon Sep 17 00:00:00 2001 From: WooJin Kong Date: Mon, 18 Mar 2024 10:40:38 +0900 Subject: [PATCH 3/6] Migrate Settings to MVI architecture --- .../koreatechboard/ui/settings/Settings.kt | 42 +++--- .../ui/settings/SettingsEvent.kt | 16 +++ .../ui/settings/SettingsState.kt | 12 ++ .../ui/settings/SettingsViewModel.kt | 131 +++++++----------- 4 files changed, 100 insertions(+), 101 deletions(-) create mode 100644 app/src/main/java/com/kongjak/koreatechboard/ui/settings/SettingsEvent.kt create mode 100644 app/src/main/java/com/kongjak/koreatechboard/ui/settings/SettingsState.kt diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/settings/Settings.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/settings/Settings.kt index 68462f36..1e2a6495 100644 --- a/app/src/main/java/com/kongjak/koreatechboard/ui/settings/Settings.kt +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/settings/Settings.kt @@ -9,8 +9,8 @@ import androidx.browser.customtabs.CustomTabsIntent import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -63,45 +63,46 @@ val darkThemeString = listOf( @Composable fun SettingsScreen(settingsViewModel: SettingsViewModel = hiltViewModel()) { val context = LocalContext.current + val uiState by settingsViewModel.uiState.collectAsState() Column(modifier = Modifier.fillMaxSize()) { - val userDepartment by settingsViewModel.userDepartment.observeAsState() + val userDepartment = uiState.userDepartment ListPreference( title = stringResource(id = R.string.setting_user_department_title), - summary = stringResource(id = deptListString[userDepartment ?: 0]), + summary = stringResource(id = deptListString[userDepartment]), itemStringResource = deptListString, itemValue = deptListName, - selectedIndex = userDepartment ?: 0 + selectedIndex = userDepartment ) { item -> - settingsViewModel.setUserDepartment(item) + settingsViewModel.sendEvent(SettingsEvent.SetUserDepartment(item)) } - val initDepartment by settingsViewModel.initDepartment.observeAsState() + val initDepartment = uiState.initDepartment val initDeptList = listOf( Department.School, Department.Dorm, - deptList[userDepartment ?: 0] + deptList[userDepartment] ) ListPreference( title = stringResource(id = R.string.setting_default_board_title), - summary = stringResource(id = initDeptList.map { it.stringResource }[initDepartment ?: 0]), + summary = stringResource(id = initDeptList.map { it.stringResource }[initDepartment]), itemStringResource = initDeptList.map { it.stringResource }, itemValue = initDeptList.map { it.name }, - selectedIndex = initDepartment ?: 0 + selectedIndex = initDepartment ) { item -> - settingsViewModel.setInitDepartment(item) + settingsViewModel.sendEvent(SettingsEvent.SetInitDepartment(item)) } - val showNumber by settingsViewModel.showNumber.observeAsState(true) + val showNumber = uiState.showNumber SwitchPreference( title = stringResource(id = R.string.setting_show_article_number_title), summary = stringResource(id = R.string.setting_show_article_number_summary), checked = showNumber, - onCheckedChange = { settingsViewModel.setShowArticleNumber(it) } + onCheckedChange = { settingsViewModel.sendEvent(SettingsEvent.SetShowArticleNumber(it)) } ) PreferenceHeader(title = stringResource(id = R.string.setting_header_info)) @@ -128,7 +129,10 @@ fun SettingsScreen(settingsViewModel: SettingsViewModel = hiltViewModel()) { Intent.EXTRA_EMAIL, arrayOf(context.getString(R.string.setting_mail_address)) ) - mailIntent.putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.setting_mail_subject)) + mailIntent.putExtra( + Intent.EXTRA_SUBJECT, + context.getString(R.string.setting_mail_subject) + ) mailIntent.putExtra( Intent.EXTRA_TEXT, context.getString(R.string.setting_mail_text, Build.MODEL, Build.VERSION.SDK_INT) @@ -147,27 +151,27 @@ fun SettingsScreen(settingsViewModel: SettingsViewModel = hiltViewModel()) { PreferenceHeader(title = stringResource(id = R.string.setting_header_theme)) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - val isChecked by settingsViewModel.isDynamicTheme.observeAsState(true) + val isChecked = uiState.isDynamicTheme SwitchPreference( title = stringResource(id = R.string.setting_dynamic_theme_title), checked = isChecked, - onCheckedChange = { settingsViewModel.setDynamicTheme(it) } + onCheckedChange = { settingsViewModel.sendEvent(SettingsEvent.SetDynamicTheme(it)) } ) } - val isDarkTheme by settingsViewModel.isDarkTheme.observeAsState() + val isDarkTheme = uiState.isDarkTheme ListPreference( title = stringResource(id = R.string.setting_dark_theme_title), summary = stringResource( - id = darkThemeString[isDarkTheme ?: DARK_THEME_SYSTEM_DEFAULT] + id = darkThemeString[isDarkTheme] ), itemStringResource = darkThemeString, itemValue = darkTheme, - selectedIndex = isDarkTheme ?: DARK_THEME_SYSTEM_DEFAULT + selectedIndex = isDarkTheme ) { theme -> - settingsViewModel.setDarkTheme(theme) + settingsViewModel.sendEvent(SettingsEvent.SetDarkTheme(theme)) } } } diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/settings/SettingsEvent.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/settings/SettingsEvent.kt new file mode 100644 index 00000000..ba3d903f --- /dev/null +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/settings/SettingsEvent.kt @@ -0,0 +1,16 @@ +package com.kongjak.koreatechboard.ui.settings + +import com.kongjak.koreatechboard.ui.base.UiEvent + +sealed class SettingsEvent : UiEvent { + object GetUserDepartment : SettingsEvent() + data class SetUserDepartment(val index: Int) : SettingsEvent() + object GetInitDepartment : SettingsEvent() + data class SetInitDepartment(val index: Int) : SettingsEvent() + object GetDynamicTheme : SettingsEvent() + data class SetDynamicTheme(val state: Boolean) : SettingsEvent() + object GetDarkTheme : SettingsEvent() + data class SetDarkTheme(val index: Int) : SettingsEvent() + object GetShowArticleNumber : SettingsEvent() + data class SetShowArticleNumber(val state: Boolean) : SettingsEvent() +} \ No newline at end of file diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/settings/SettingsState.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/settings/SettingsState.kt new file mode 100644 index 00000000..08d332ea --- /dev/null +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/settings/SettingsState.kt @@ -0,0 +1,12 @@ +package com.kongjak.koreatechboard.ui.settings + +import com.kongjak.koreatechboard.domain.DARK_THEME_SYSTEM_DEFAULT +import com.kongjak.koreatechboard.ui.base.UiState + +data class SettingsState( + val userDepartment: Int = 0, + val initDepartment: Int = 0, + val isDynamicTheme: Boolean = true, + val isDarkTheme: Int = DARK_THEME_SYSTEM_DEFAULT, + val showNumber: Boolean = true +): UiState \ No newline at end of file diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/settings/SettingsViewModel.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/settings/SettingsViewModel.kt index 1bf9f424..31c322f0 100644 --- a/app/src/main/java/com/kongjak/koreatechboard/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/settings/SettingsViewModel.kt @@ -1,11 +1,7 @@ package com.kongjak.koreatechboard.ui.settings import android.os.Build -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.kongjak.koreatechboard.domain.DARK_THEME_SYSTEM_DEFAULT import com.kongjak.koreatechboard.domain.usecase.GetDarkThemeUseCase import com.kongjak.koreatechboard.domain.usecase.GetDynamicThemeUseCase import com.kongjak.koreatechboard.domain.usecase.GetInitDepartmentUseCase @@ -16,6 +12,7 @@ import com.kongjak.koreatechboard.domain.usecase.SetDynamicThemeUseCase import com.kongjak.koreatechboard.domain.usecase.SetInitDepartmentUseCase import com.kongjak.koreatechboard.domain.usecase.SetShowArticleNumberUseCase import com.kongjak.koreatechboard.domain.usecase.SetUserDepartmentUseCase +import com.kongjak.koreatechboard.ui.base.BaseViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch @@ -33,104 +30,74 @@ class SettingsViewModel @Inject constructor( private val setDarkThemeUseCase: SetDarkThemeUseCase, private val getShowArticleNumberUseCase: GetShowArticleNumberUseCase, private val setShowArticleNumberUseCase: SetShowArticleNumberUseCase -) : ViewModel() { - private val _userDepartment = MutableLiveData(0) - val userDepartment: LiveData - get() = _userDepartment - - private val _initDepartment = MutableLiveData(0) - val initDepartment: LiveData - get() = _initDepartment - - private val _isDynamicTheme = MutableLiveData(true) - val isDynamicTheme: LiveData - get() = _isDynamicTheme - - private val _isDarkTheme = MutableLiveData(DARK_THEME_SYSTEM_DEFAULT) - val isDarkTheme: LiveData - get() = _isDarkTheme - - private val _showNumber = MutableLiveData(true) - val showNumber: LiveData - get() = _showNumber +) : BaseViewModel(SettingsState()) { init { - getUserDepartment() - getInitDepartment() + sendEvent(SettingsEvent.GetUserDepartment) + sendEvent(SettingsEvent.GetInitDepartment) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - getDynamicTheme() + sendEvent(SettingsEvent.GetDynamicTheme) } - getDarkTheme() - getShowArticleNumber() + sendEvent(SettingsEvent.GetDarkTheme) + sendEvent(SettingsEvent.GetShowArticleNumber) } - private fun getUserDepartment() { - viewModelScope.launch { - getUserDepartmentUseCase().collectLatest { - _userDepartment.value = it + override fun reduce(oldState: SettingsState, event: SettingsEvent) { + when (event) { + SettingsEvent.GetDarkTheme -> viewModelScope.launch { + getDarkThemeUseCase().collectLatest { + setState(oldState.copy(isDarkTheme = it)) + } } - } - } - fun setUserDepartment(index: Int) { - viewModelScope.launch { - setUserDepartmentUseCase(index) - } - } + SettingsEvent.GetDynamicTheme -> viewModelScope.launch { + getDynamicThemeUseCase().collectLatest { + setState(oldState.copy(isDynamicTheme = it)) + } + } - private fun getInitDepartment() { - viewModelScope.launch { - getInitDepartmentUseCase().collectLatest { - _initDepartment.value = it + SettingsEvent.GetInitDepartment -> viewModelScope.launch { + getInitDepartmentUseCase().collectLatest { + setState(oldState.copy(initDepartment = it)) + } } - } - } - fun setInitDepartment(index: Int) { - viewModelScope.launch { - setInitDepartmentUseCase(index) - } - } + SettingsEvent.GetShowArticleNumber -> viewModelScope.launch { + getShowArticleNumberUseCase().collectLatest { + setState(oldState.copy(showNumber = it)) + } + } - private fun getDynamicTheme() { - viewModelScope.launch { - getDynamicThemeUseCase().collectLatest { - _isDynamicTheme.value = it + SettingsEvent.GetUserDepartment -> viewModelScope.launch { + getUserDepartmentUseCase().collectLatest { + setState(oldState.copy(userDepartment = it)) + } } - } - } - fun setDynamicTheme(state: Boolean) { - viewModelScope.launch { - setDynamicThemeUseCase(state) - } - } + is SettingsEvent.SetDarkTheme -> viewModelScope.launch { + setDarkThemeUseCase(event.index) + sendEvent(SettingsEvent.GetDarkTheme) + } - private fun getDarkTheme() { - viewModelScope.launch { - getDarkThemeUseCase().collectLatest { - _isDarkTheme.value = it + is SettingsEvent.SetDynamicTheme -> viewModelScope.launch { + setDynamicThemeUseCase(event.state) + sendEvent(SettingsEvent.GetDynamicTheme) } - } - } - fun setDarkTheme(theme: Int) { - viewModelScope.launch { - setDarkThemeUseCase(theme) - } - } + is SettingsEvent.SetInitDepartment -> viewModelScope.launch { + setInitDepartmentUseCase(event.index) + sendEvent(SettingsEvent.GetInitDepartment) + } - private fun getShowArticleNumber() { - viewModelScope.launch { - getShowArticleNumberUseCase().collectLatest { - _showNumber.value = it + is SettingsEvent.SetShowArticleNumber -> viewModelScope.launch { + setShowArticleNumberUseCase(event.state) + sendEvent(SettingsEvent.GetShowArticleNumber) } - } - } - fun setShowArticleNumber(state: Boolean) { - viewModelScope.launch { - setShowArticleNumberUseCase(state) + is SettingsEvent.SetUserDepartment -> viewModelScope.launch { + setUserDepartmentUseCase(event.index) + sendEvent(SettingsEvent.GetUserDepartment) + } } } } From 8009bfa7f58cc76448999d276c8f620213a0d326 Mon Sep 17 00:00:00 2001 From: WooJin Kong Date: Mon, 18 Mar 2024 10:41:42 +0900 Subject: [PATCH 4/6] chore: Fix ktlint --- .../kongjak/koreatechboard/ui/activity/ArticleActivity.kt | 4 +--- .../java/com/kongjak/koreatechboard/ui/article/Article.kt | 1 - .../com/kongjak/koreatechboard/ui/article/ArticleEvent.kt | 2 +- .../com/kongjak/koreatechboard/ui/article/ArticleState.kt | 2 +- .../kongjak/koreatechboard/ui/article/ArticleViewModel.kt | 1 - .../com/kongjak/koreatechboard/ui/base/BaseViewModel.kt | 4 ++-- .../java/com/kongjak/koreatechboard/ui/base/UiEvent.kt | 2 +- .../java/com/kongjak/koreatechboard/ui/base/UiState.kt | 2 +- .../main/java/com/kongjak/koreatechboard/ui/board/Board.kt | 4 +--- .../java/com/kongjak/koreatechboard/ui/board/BoardEvent.kt | 2 +- .../com/kongjak/koreatechboard/ui/board/BoardInitEvent.kt | 2 +- .../java/com/kongjak/koreatechboard/ui/board/BoardState.kt | 2 +- .../main/java/com/kongjak/koreatechboard/ui/home/Home.kt | 1 - .../com/kongjak/koreatechboard/ui/home/HomeBoardEvent.kt | 3 +-- .../com/kongjak/koreatechboard/ui/network/NetworkState.kt | 2 +- .../kongjak/koreatechboard/ui/network/NetworkViewModel.kt | 7 +++++-- .../com/kongjak/koreatechboard/ui/search/SearchEvent.kt | 2 +- .../kongjak/koreatechboard/ui/search/SearchViewModel.kt | 3 --- .../kongjak/koreatechboard/ui/settings/SettingsEvent.kt | 2 +- .../kongjak/koreatechboard/ui/settings/SettingsState.kt | 2 +- 20 files changed, 21 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/activity/ArticleActivity.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/activity/ArticleActivity.kt index 93fb203e..e96a92fa 100644 --- a/app/src/main/java/com/kongjak/koreatechboard/ui/activity/ArticleActivity.kt +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/activity/ArticleActivity.kt @@ -33,12 +33,10 @@ import androidx.hilt.navigation.compose.hiltViewModel import com.kongjak.koreatechboard.R import com.kongjak.koreatechboard.ui.article.ArticleScreen import com.kongjak.koreatechboard.ui.article.ArticleViewModel -import com.kongjak.koreatechboard.ui.network.NetworkEvent -import com.kongjak.koreatechboard.ui.theme.KoreatechBoardTheme import com.kongjak.koreatechboard.ui.network.NetworkViewModel +import com.kongjak.koreatechboard.ui.theme.KoreatechBoardTheme import com.kongjak.koreatechboard.ui.viewmodel.ThemeViewModel import com.kongjak.koreatechboard.util.findActivity -import com.kongjak.koreatechboard.util.routes.Department import dagger.hilt.android.AndroidEntryPoint import java.util.UUID diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/article/Article.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/article/Article.kt index 97e225c7..30d9a7c5 100644 --- a/app/src/main/java/com/kongjak/koreatechboard/ui/article/Article.kt +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/article/Article.kt @@ -21,7 +21,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.key -import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/article/ArticleEvent.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/article/ArticleEvent.kt index 0189f06f..74167a27 100644 --- a/app/src/main/java/com/kongjak/koreatechboard/ui/article/ArticleEvent.kt +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/article/ArticleEvent.kt @@ -5,4 +5,4 @@ import java.util.UUID sealed class ArticleEvent : UiEvent { data class FetchData(val department: String, val uuid: UUID) : ArticleEvent() -} \ No newline at end of file +} diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/article/ArticleState.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/article/ArticleState.kt index aee6b960..ad37eb47 100644 --- a/app/src/main/java/com/kongjak/koreatechboard/ui/article/ArticleState.kt +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/article/ArticleState.kt @@ -15,4 +15,4 @@ data class ArticleState( val statusCode: Int = 200, val url: String = "", val error: String = "" -) : UiState \ No newline at end of file +) : UiState diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/article/ArticleViewModel.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/article/ArticleViewModel.kt index 8ced7262..5c196a22 100644 --- a/app/src/main/java/com/kongjak/koreatechboard/ui/article/ArticleViewModel.kt +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/article/ArticleViewModel.kt @@ -4,7 +4,6 @@ import androidx.lifecycle.viewModelScope import com.kongjak.koreatechboard.domain.base.ResponseResult import com.kongjak.koreatechboard.domain.usecase.GetArticleUseCase import com.kongjak.koreatechboard.ui.base.BaseViewModel -import com.kongjak.koreatechboard.util.routes.Department import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import java.util.UUID diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/base/BaseViewModel.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/base/BaseViewModel.kt index f3882470..4692f151 100644 --- a/app/src/main/java/com/kongjak/koreatechboard/ui/base/BaseViewModel.kt +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/base/BaseViewModel.kt @@ -4,7 +4,7 @@ import androidx.lifecycle.ViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -open class BaseViewModel(initialState: S) : ViewModel() { +open class BaseViewModel(initialState: S) : ViewModel() { private val _uiState = MutableStateFlow(initialState) val uiState get() = _uiState.asStateFlow() @@ -17,4 +17,4 @@ open class BaseViewModel(initialState: S) : ViewModel() } open fun reduce(oldState: S, event: E) {} -} \ No newline at end of file +} diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/base/UiEvent.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/base/UiEvent.kt index c0335d2e..c0a5d920 100644 --- a/app/src/main/java/com/kongjak/koreatechboard/ui/base/UiEvent.kt +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/base/UiEvent.kt @@ -1,3 +1,3 @@ package com.kongjak.koreatechboard.ui.base -interface UiEvent \ No newline at end of file +interface UiEvent diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/base/UiState.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/base/UiState.kt index 8e06d683..cab2aa83 100644 --- a/app/src/main/java/com/kongjak/koreatechboard/ui/base/UiState.kt +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/base/UiState.kt @@ -1,3 +1,3 @@ package com.kongjak.koreatechboard.ui.base -interface UiState \ No newline at end of file +interface UiState diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/board/Board.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/board/Board.kt index 78abe5bd..f5efdbd4 100644 --- a/app/src/main/java/com/kongjak/koreatechboard/ui/board/Board.kt +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/board/Board.kt @@ -50,7 +50,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -75,12 +74,11 @@ import androidx.paging.compose.collectAsLazyPagingItems import com.kongjak.koreatechboard.R import com.kongjak.koreatechboard.ui.activity.ArticleActivity import com.kongjak.koreatechboard.ui.activity.SearchActivity +import com.kongjak.koreatechboard.ui.network.NetworkViewModel import com.kongjak.koreatechboard.ui.settings.deptList import com.kongjak.koreatechboard.ui.settings.fullDeptList -import com.kongjak.koreatechboard.ui.network.NetworkEvent import com.kongjak.koreatechboard.ui.theme.boardItemSubText import com.kongjak.koreatechboard.ui.theme.boardItemTitle -import com.kongjak.koreatechboard.ui.network.NetworkViewModel import com.kongjak.koreatechboard.util.routes.Department import kotlinx.coroutines.launch diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/board/BoardEvent.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/board/BoardEvent.kt index 54a3e17d..0b81734a 100644 --- a/app/src/main/java/com/kongjak/koreatechboard/ui/board/BoardEvent.kt +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/board/BoardEvent.kt @@ -5,4 +5,4 @@ import com.kongjak.koreatechboard.ui.base.UiEvent sealed class BoardEvent : UiEvent { data class ShowNumberUpdated(val showNumber: Boolean) : BoardEvent() data class FetchData(val department: String, val board: String) : BoardEvent() -} \ No newline at end of file +} diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/board/BoardInitEvent.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/board/BoardInitEvent.kt index 051236d6..b24b7bd7 100644 --- a/app/src/main/java/com/kongjak/koreatechboard/ui/board/BoardInitEvent.kt +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/board/BoardInitEvent.kt @@ -5,4 +5,4 @@ import com.kongjak.koreatechboard.ui.base.UiEvent sealed class BoardInitEvent : UiEvent { data class InitDepartmentUpdated(val value: Int) : BoardInitEvent() data class UserDepartmentUpdated(val value: Int) : BoardInitEvent() -} \ No newline at end of file +} diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/board/BoardState.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/board/BoardState.kt index f916c06e..75193580 100644 --- a/app/src/main/java/com/kongjak/koreatechboard/ui/board/BoardState.kt +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/board/BoardState.kt @@ -10,5 +10,5 @@ data class BoardState( val department: String = "", val board: String = "", val showNumber: Boolean = true, - val boardItemsMap: Flow> = emptyFlow(), + val boardItemsMap: Flow> = emptyFlow() ) : UiState diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/home/Home.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/home/Home.kt index 0b6ccd58..98bc1ab4 100644 --- a/app/src/main/java/com/kongjak/koreatechboard/ui/home/Home.kt +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/home/Home.kt @@ -1,7 +1,6 @@ package com.kongjak.koreatechboard.ui.home import android.content.Intent -import android.util.Log import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/home/HomeBoardEvent.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/home/HomeBoardEvent.kt index 03f4e2ea..49f96c84 100644 --- a/app/src/main/java/com/kongjak/koreatechboard/ui/home/HomeBoardEvent.kt +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/home/HomeBoardEvent.kt @@ -1,8 +1,7 @@ package com.kongjak.koreatechboard.ui.home import com.kongjak.koreatechboard.ui.base.UiEvent -import com.kongjak.koreatechboard.util.routes.BoardItem sealed class HomeBoardEvent : UiEvent { data class FetchData(val department: String, val board: String) : HomeBoardEvent() -} \ No newline at end of file +} diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/network/NetworkState.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/network/NetworkState.kt index 91265933..dc6bb783 100644 --- a/app/src/main/java/com/kongjak/koreatechboard/ui/network/NetworkState.kt +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/network/NetworkState.kt @@ -4,4 +4,4 @@ import com.kongjak.koreatechboard.ui.base.UiState data class NetworkState( val isConnected: Boolean = true -) : UiState \ No newline at end of file +) : UiState diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/network/NetworkViewModel.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/network/NetworkViewModel.kt index 091be5a4..db7dd12c 100644 --- a/app/src/main/java/com/kongjak/koreatechboard/ui/network/NetworkViewModel.kt +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/network/NetworkViewModel.kt @@ -20,8 +20,11 @@ class NetworkViewModel @Inject constructor( private fun getNetworkState() { viewModelScope.launch { networkUtil.networkState().collectLatest { networkConnected -> - if (networkConnected) sendEvent(NetworkEvent.Connected) - else sendEvent(NetworkEvent.Disconnected) + if (networkConnected) { + sendEvent(NetworkEvent.Connected) + } else { + sendEvent(NetworkEvent.Disconnected) + } } } } diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/search/SearchEvent.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/search/SearchEvent.kt index 9887e086..90d210f0 100644 --- a/app/src/main/java/com/kongjak/koreatechboard/ui/search/SearchEvent.kt +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/search/SearchEvent.kt @@ -5,4 +5,4 @@ import com.kongjak.koreatechboard.ui.base.UiEvent sealed class SearchEvent : UiEvent { data class FetchData(val department: String, val board: String, val title: String) : SearchEvent() -} \ No newline at end of file +} diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/search/SearchViewModel.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/search/SearchViewModel.kt index de04f2ef..6a75b2d4 100644 --- a/app/src/main/java/com/kongjak/koreatechboard/ui/search/SearchViewModel.kt +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/search/SearchViewModel.kt @@ -1,13 +1,10 @@ package com.kongjak.koreatechboard.ui.search import androidx.lifecycle.viewModelScope -import androidx.paging.PagingData import androidx.paging.cachedIn -import com.kongjak.koreatechboard.domain.model.BoardData import com.kongjak.koreatechboard.domain.usecase.SearchBoardWithTitleUseCase import com.kongjak.koreatechboard.ui.base.BaseViewModel import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.Flow import javax.inject.Inject @HiltViewModel diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/settings/SettingsEvent.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/settings/SettingsEvent.kt index ba3d903f..733d411b 100644 --- a/app/src/main/java/com/kongjak/koreatechboard/ui/settings/SettingsEvent.kt +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/settings/SettingsEvent.kt @@ -13,4 +13,4 @@ sealed class SettingsEvent : UiEvent { data class SetDarkTheme(val index: Int) : SettingsEvent() object GetShowArticleNumber : SettingsEvent() data class SetShowArticleNumber(val state: Boolean) : SettingsEvent() -} \ No newline at end of file +} diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/settings/SettingsState.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/settings/SettingsState.kt index 08d332ea..96f736b3 100644 --- a/app/src/main/java/com/kongjak/koreatechboard/ui/settings/SettingsState.kt +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/settings/SettingsState.kt @@ -9,4 +9,4 @@ data class SettingsState( val isDynamicTheme: Boolean = true, val isDarkTheme: Int = DARK_THEME_SYSTEM_DEFAULT, val showNumber: Boolean = true -): UiState \ No newline at end of file +) : UiState From 0dd8bbba40adad1002c040218bf60b700eee43eb Mon Sep 17 00:00:00 2001 From: WooJin Kong Date: Mon, 18 Mar 2024 11:00:51 +0900 Subject: [PATCH 5/6] fix: Fix NPE on Home --- .../java/com/kongjak/koreatechboard/ui/home/HomeBoardState.kt | 2 +- .../com/kongjak/koreatechboard/ui/home/HomeBoardViewModel.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/home/HomeBoardState.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/home/HomeBoardState.kt index 321fcb50..c199a305 100644 --- a/app/src/main/java/com/kongjak/koreatechboard/ui/home/HomeBoardState.kt +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/home/HomeBoardState.kt @@ -6,7 +6,7 @@ import com.kongjak.koreatechboard.ui.base.UiState data class HomeBoardState( val isLoaded: Boolean = false, val isSuccess: Boolean = false, - val boardList: Map> = emptyMap(), + val boardList: Map?> = emptyMap(), val statusCode: Int = 0, val error: String = "" ) : UiState diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/home/HomeBoardViewModel.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/home/HomeBoardViewModel.kt index 46c4ba4f..b18a9b74 100644 --- a/app/src/main/java/com/kongjak/koreatechboard/ui/home/HomeBoardViewModel.kt +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/home/HomeBoardViewModel.kt @@ -33,7 +33,7 @@ class HomeBoardViewModel @Inject constructor(private val getBoardMinimumUseCase: setState( oldState.copy( isSuccess = true, - boardList = (oldState.boardList + (event.board to (it.data.boardData)!!)), + boardList = (oldState.boardList + (event.board to (it.data.boardData))), statusCode = it.data.statusCode, isLoaded = true ) From 023b885c0e4eb8d5f2e2ad28f010e48d517eddf4 Mon Sep 17 00:00:00 2001 From: WooJin Kong Date: Mon, 18 Mar 2024 11:45:48 +0900 Subject: [PATCH 6/6] chore: Fix ktlint --- .../com/kongjak/koreatechboard/ui/home/Home.kt | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/kongjak/koreatechboard/ui/home/Home.kt b/app/src/main/java/com/kongjak/koreatechboard/ui/home/Home.kt index 73ed24ac..bce6efd8 100644 --- a/app/src/main/java/com/kongjak/koreatechboard/ui/home/Home.kt +++ b/app/src/main/java/com/kongjak/koreatechboard/ui/home/Home.kt @@ -191,9 +191,11 @@ fun BoardInMain( } } } else if (isSuccess && statusCode != 200) { - Column(modifier = Modifier - .fillMaxWidth() - .padding(8.dp)) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) { Text(text = stringResource(R.string.error_server_down, statusCode)) Button(onClick = { homeBoardViewModel.getApi(department.name, key) @@ -202,9 +204,11 @@ fun BoardInMain( } } } else if (!isSuccess) { - Column(modifier = Modifier - .fillMaxWidth() - .padding(8.dp)) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) { Text(text = uiState.value.error) Button(onClick = { homeBoardViewModel.getApi(department.name, key)