Skip to content

Commit

Permalink
feat: ページネーションしながら取得できるようにした
Browse files Browse the repository at this point in the history
  • Loading branch information
pantasystem committed Jul 3, 2023
1 parent 3784d4f commit d794eec
Show file tree
Hide file tree
Showing 5 changed files with 191 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ class SignUpActivity : AppCompatActivity() {
onSelected = signUpViewModel::onSelected,
onNavigateUp = {
finish()
},
onBottomReached = {
signUpViewModel.onBottomReached()
}
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,39 @@
package net.pantasystem.milktea.auth

import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.material.Button
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.Search
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.distinctUntilChanged
import net.pantasystem.milktea.api.misskey.infos.InstanceInfosResponse
import net.pantasystem.milktea.auth.viewmodel.SignUpUiState
import net.pantasystem.milktea.common.ResultState
import net.pantasystem.milktea.common.StateContent
import net.pantasystem.milktea.common.ui.isScrolledToTheEnd
import net.pantasystem.milktea.model.instance.InstanceInfoType

@Composable
Expand All @@ -27,8 +43,20 @@ fun SignUpScreen(
onInputKeyword: (String) -> Unit,
onNextButtonClicked: (InstanceInfoType) -> Unit,
onSelected: (InstanceInfosResponse.InstanceInfo) -> Unit,
onNavigateUp: () -> Unit
onNavigateUp: () -> Unit,
onBottomReached: () -> Unit,
) {

val listState = rememberLazyListState()
LaunchedEffect(Unit) {
snapshotFlow {
listState.isScrolledToTheEnd()
}.distinctUntilChanged().collect {
if (it) {
onBottomReached()
}
}
}
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
Expand Down Expand Up @@ -78,7 +106,8 @@ fun SignUpScreen(
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.weight(1f),
state = listState,
) {
items(uiState.filteredInfos) { instance ->
MisskeyInstanceInfoCard(
Expand All @@ -103,10 +132,11 @@ fun SignUpScreen(
Button(
shape = RoundedCornerShape(32.dp),
onClick = {
when(val content = uiState.instanceInfo.content) {
when (val content = uiState.instanceInfo.content) {
is StateContent.Exist -> {
onNextButtonClicked(content.rawContent)
}

is StateContent.NotExist -> Unit
}
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package net.pantasystem.milktea.auth.suggestions

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import net.pantasystem.milktea.api.misskey.InstanceInfoAPIBuilder
import net.pantasystem.milktea.api.misskey.infos.InstanceInfosResponse
import net.pantasystem.milktea.common.PageableState
import net.pantasystem.milktea.common.paginator.EntityConverter
import net.pantasystem.milktea.common.paginator.PaginationState
import net.pantasystem.milktea.common.paginator.PreviousLoader
import net.pantasystem.milktea.common.paginator.PreviousPagingController
import net.pantasystem.milktea.common.paginator.StateLocker
import net.pantasystem.milktea.common.runCancellableCatching
import net.pantasystem.milktea.common.throwIfHasError
import javax.inject.Inject

class InstanceSuggestionsPagingModel @Inject constructor(
private val instancesInfoAPIBuilder: InstanceInfoAPIBuilder,
) : StateLocker,
PaginationState<InstanceInfosResponse.InstanceInfo>,
PreviousLoader<InstanceInfosResponse.InstanceInfo>,
EntityConverter<InstanceInfosResponse.InstanceInfo, InstanceInfosResponse.InstanceInfo> {

private var _offset = 0
private var _name: String = ""
private val _state =
MutableStateFlow<PageableState<List<InstanceInfosResponse.InstanceInfo>>>(PageableState.Loading.Init())

private var _job: Job? = null

override suspend fun convertAll(list: List<InstanceInfosResponse.InstanceInfo>): List<InstanceInfosResponse.InstanceInfo> {
return list
}

override val state: Flow<PageableState<List<InstanceInfosResponse.InstanceInfo>>>
get() = _state

override fun getState(): PageableState<List<InstanceInfosResponse.InstanceInfo>> {
return _state.value
}

override fun setState(state: PageableState<List<InstanceInfosResponse.InstanceInfo>>) {
_state.value = state
}

override suspend fun loadPrevious(): Result<List<InstanceInfosResponse.InstanceInfo>> =
runCancellableCatching {
instancesInfoAPIBuilder.build().getInstances(
offset = _offset,
name = _name,
).throwIfHasError().body()!!.also {
_offset += it.size
}
}

suspend fun setQueryName(name: String) {
_job?.cancel()
mutex.withLock {
_name = name

}
setState(PageableState.Loading.Init())
}

override val mutex: Mutex = Mutex()

private val previousPagingController = PreviousPagingController(
this,
this,
this,
this
)

fun onLoadNext(scope: CoroutineScope) {
_job?.cancel()
_job = scope.launch {
previousPagingController.loadPrevious()
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,43 +5,46 @@ import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import net.pantasystem.milktea.api.misskey.InstanceInfoAPIBuilder
import net.pantasystem.milktea.api.misskey.infos.InstanceInfosResponse
import net.pantasystem.milktea.auth.suggestions.InstanceSuggestionsPagingModel
import net.pantasystem.milktea.common.*
import net.pantasystem.milktea.model.instance.InstanceInfoService
import net.pantasystem.milktea.model.instance.InstanceInfoType
import javax.inject.Inject

@HiltViewModel
class SignUpViewModel @Inject constructor(
private val instancesInfosAPIBuilder: InstanceInfoAPIBuilder,
private val instanceInfoService: InstanceInfoService,
private val instancePagingModel: InstanceSuggestionsPagingModel,
loggerFactory: Logger.Factory,
) : ViewModel() {

private val logger by lazy {
loggerFactory.create("SignUpViewModel")
}

private var _keyword = MutableStateFlow("")
val keyword = _keyword.asStateFlow()

@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
private val instancesInfosResponse = keyword.flatMapLatest { name ->
suspend {
requireNotNull(
instancesInfosAPIBuilder.build().getInstances(
name = name
).throwIfHasError()
.body()
).distinctBy {
it.url
}
}.asFlow()
}.catch {
logger.error("インスタンス情報の取得に失敗", it)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null)
// @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
// private val instancesInfosResponse = keyword.flatMapLatest { name ->
// suspend {
// requireNotNull(
// instancesInfosAPIBuilder.build().getInstances(
// name = name
// ).throwIfHasError()
// .body()
// ).distinctBy {
// it.url
// }
// }.asFlow()
// }.catch {
// logger.error("インスタンス情報の取得に失敗", it)
// }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null)

private val instancesInfosResponse = instancePagingModel.state.map {
(it.content as? StateContent.Exist)?.rawContent
}.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5_000),
emptyList(),
)


private var _selectedInstanceUrl = MutableStateFlow<String?>("misskey.io")
Expand Down Expand Up @@ -83,13 +86,25 @@ class SignUpViewModel @Inject constructor(
SignUpUiState()
)

init {
instancePagingModel.onLoadNext(viewModelScope)
}

fun onInputKeyword(value: String) {
_keyword.value = value
viewModelScope.launch {
_keyword.value = value
instancePagingModel.setQueryName(value)
instancePagingModel.onLoadNext(this)
}
}

fun onSelected(instancesInfosResponse: InstanceInfosResponse.InstanceInfo) {
_selectedInstanceUrl.value = instancesInfosResponse.url
}

fun onBottomReached() {
instancePagingModel.onLoadNext(viewModelScope)
}
}

data class SignUpUiState(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
Expand All @@ -27,10 +26,10 @@ import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import net.pantasystem.milktea.api.misskey.InstanceInfoAPIBuilder
import net.pantasystem.milktea.api.misskey.MisskeyAPIServiceBuilder
import net.pantasystem.milktea.api.misskey.auth.UserKey
import net.pantasystem.milktea.app_store.account.AccountStore
import net.pantasystem.milktea.auth.suggestions.InstanceSuggestionsPagingModel
import net.pantasystem.milktea.common.Logger
import net.pantasystem.milktea.common.ResultState
import net.pantasystem.milktea.common.StateContent
Expand Down Expand Up @@ -61,7 +60,7 @@ class AppAuthViewModel @Inject constructor(
private val getAccessToken: GetAccessToken,
private val clientIdRepository: ClientIdRepository,
private val syncMetaExecutor: SyncMetaExecutor,
private val instancesInfoAPIBuilder: InstanceInfoAPIBuilder,
private val instanceSuggestionsPagingModel: InstanceSuggestionsPagingModel,
) : ViewModel() {

private val logger = loggerFactory.create("AppAuthViewModel")
Expand Down Expand Up @@ -93,21 +92,29 @@ class AppAuthViewModel @Inject constructor(
// private val instances = instanceInfoRepository.observeAll()
// .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())

@OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
private val misskeyInstances = instanceDomain.flatMapLatest { name ->
suspend {
requireNotNull(
instancesInfoAPIBuilder.build().getInstances(
name = name
).throwIfHasError()
.body()
).distinctBy {
it.url
}
}.asFlow()
}.catch {
logger.error("インスタンス情報の取得に失敗", it)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null)
// @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class)
// private val misskeyInstances = instanceDomain.flatMapLatest { name ->
// suspend {
// requireNotNull(
// instancesInfoAPIBuilder.build().getInstances(
// name = name
// ).throwIfHasError()
// .body()
// ).distinctBy {
// it.url
// }
// }.asFlow()
// }.catch {
// logger.error("インスタンス情報の取得に失敗", it)
// }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null)
//
private val misskeyInstances = instanceSuggestionsPagingModel.state.map {
(it.content as? StateContent.Exist)?.rawContent ?: emptyList()
}.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5_000),
emptyList()
)

private val isPrivacyPolicyAgreement = MutableStateFlow(false)
private val isTermsOfServiceAgreement = MutableStateFlow(false)
Expand Down Expand Up @@ -263,7 +270,7 @@ class AppAuthViewModel @Inject constructor(
},
waiting4ApproveState = waiting4Approve,
clientId = "clientId: ${clientIdRepository.getOrCreate().clientId}",
misskeyInstanceInfosResponse = misskeyInstances ?: emptyList(),
misskeyInstanceInfosResponse = misskeyInstances,
)
}.stateIn(
viewModelScope,
Expand All @@ -278,6 +285,10 @@ class AppAuthViewModel @Inject constructor(


init {
instanceDomain.onEach {
instanceSuggestionsPagingModel.setQueryName(it)
instanceSuggestionsPagingModel.onLoadNext(viewModelScope)
}.launchIn(viewModelScope)

waiting4UserApprove.mapNotNull {
(it.content as? StateContent.Exist)?.rawContent
Expand Down

0 comments on commit d794eec

Please sign in to comment.