Skip to content

Commit

Permalink
feat: search vm + android view impl
Browse files Browse the repository at this point in the history
  • Loading branch information
nathanfallet committed Mar 12, 2024
1 parent 0a6f8a2 commit 185ffca
Show file tree
Hide file tree
Showing 9 changed files with 161 additions and 71 deletions.
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package me.nathanfallet.extopy.features.timelines

import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
Expand All @@ -23,6 +24,7 @@ import me.nathanfallet.extopy.R
import me.nathanfallet.extopy.models.users.User
import me.nathanfallet.extopy.ui.components.posts.PostCard
import me.nathanfallet.extopy.ui.components.users.UserCard
import me.nathanfallet.extopy.viewmodels.timelines.SearchViewModel
import me.nathanfallet.extopy.viewmodels.timelines.TimelineViewModel
import org.koin.androidx.compose.koinViewModel
import org.koin.core.parameter.parametersOf
Expand All @@ -39,6 +41,7 @@ fun TimelineView(
val viewModel = koinViewModel<TimelineViewModel>(
parameters = { parametersOf(id) }
)
val searchViewModel = koinViewModel<SearchViewModel>()

LaunchedEffect(id) {
viewModel.fetchTimeline()
Expand All @@ -47,69 +50,24 @@ fun TimelineView(
val timeline by viewModel.timeline.collectAsState()
val users by viewModel.users.collectAsState()
val posts by viewModel.posts.collectAsState()
val search by viewModel.search.collectAsState()

val search by searchViewModel.search.collectAsState()
val searchUsers by searchViewModel.users.collectAsState()
val searchPosts by searchViewModel.posts.collectAsState()

LazyColumn(
modifier
) {
item {
TopAppBar(
title = {
search?.let { search ->
TextField(
value = search,
onValueChange = viewModel::updateSearch,
placeholder = {
Text(
text = stringResource(id = R.string.timeline_search_field),
color = Color.LightGray
)
},
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Search
),
keyboardActions = KeyboardActions(
onSearch = {
viewModel.viewModelScope.coroutineScope.launch {
viewModel.doSearch()
}
}
)
)
} ?: run {
Text(stringResource(R.string.timeline_title))
}
},
title = { Text(stringResource(R.string.timeline_title)) },
actions = {
if (search != null) {
IconButton(
onClick = { viewModel.updateSearch(null) }
) {
Icon(
painter = painterResource(id = R.drawable.ic_baseline_close_24),
contentDescription = stringResource(id = R.string.timeline_search_cancel)
)
}
} else {
IconButton(
onClick = { navigate("timelines/compose") }
) {
Icon(
painter = painterResource(id = R.drawable.ic_baseline_create_24),
contentDescription = stringResource(id = R.string.timeline_compose_title)
)
}
}
IconButton(
onClick = {
viewModel.viewModelScope.coroutineScope.launch {
viewModel.doSearch()
}
}
onClick = { navigate("timelines/compose") }
) {
Icon(
painter = painterResource(id = R.drawable.ic_baseline_search_24),
contentDescription = stringResource(id = R.string.timeline_search_title)
painter = painterResource(id = R.drawable.ic_baseline_create_24),
contentDescription = stringResource(id = R.string.timeline_compose_title)
)
}
}
Expand All @@ -118,7 +76,25 @@ fun TimelineView(
item {
Spacer(modifier = Modifier.height(12.dp))
}
items(users ?: listOf()) {
item {
TextField(
value = search,
onValueChange = searchViewModel::updateSearch,
placeholder = {
Text(
text = stringResource(id = R.string.timeline_search_field),
color = Color.LightGray
)
},
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Search
),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
)
}
items(searchUsers?.takeIf { it.isNotEmpty() } ?: users ?: listOf()) {
UserCard(
user = it,
viewedBy = viewedBy,
Expand Down Expand Up @@ -148,7 +124,7 @@ fun TimelineView(
}
)
}
items(posts ?: listOf()) {
items(searchPosts?.takeIf { it.isNotEmpty() } ?: posts ?: listOf()) {
PostCard(
post = it,
navigate = navigate,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import me.nathanfallet.extopy.viewmodels.auth.AuthViewModel
import me.nathanfallet.extopy.viewmodels.notifications.NotificationsViewModel
import me.nathanfallet.extopy.viewmodels.posts.PostViewModel
import me.nathanfallet.extopy.viewmodels.root.RootViewModel
import me.nathanfallet.extopy.viewmodels.timelines.SearchViewModel
import me.nathanfallet.extopy.viewmodels.timelines.TimelineComposeViewModel
import me.nathanfallet.extopy.viewmodels.timelines.TimelineViewModel
import me.nathanfallet.extopy.viewmodels.users.ProfileViewModel
Expand Down Expand Up @@ -50,13 +51,15 @@ val useCaseModule = module {
single<IFetchTimelinePostsUseCase> { FetchTimelinePostsUseCase(get()) }

// Users
single<IFetchUsersUseCase> { FetchUsersUseCase(get()) }

Check warning on line 54 in shared/src/commonMain/kotlin/me/nathanfallet/extopy/di/SharedModule.kt

View check run for this annotation

Codecov / codecov/patch

shared/src/commonMain/kotlin/me/nathanfallet/extopy/di/SharedModule.kt#L54

Added line #L54 was not covered by tests
single<IFetchUserUseCase> { FetchUserUseCase(get(), get()) }
single<IUpdateFollowInUserUseCase> { UpdateFollowInUserUseCase(get(), get()) }
single<IFetchUserPostsUseCase> { FetchUserPostsUseCase(get(), get()) }

// Posts
single<ICreatePostUseCase> { CreatePostUseCase(get(), get()) }
single<IUpdateLikeInPostUseCase> { UpdateLikeInPostUseCase(get(), get()) }
single<IFetchPostsUseCase> { FetchPostsUseCase(get()) }

Check warning on line 62 in shared/src/commonMain/kotlin/me/nathanfallet/extopy/di/SharedModule.kt

View check run for this annotation

Codecov / codecov/patch

shared/src/commonMain/kotlin/me/nathanfallet/extopy/di/SharedModule.kt#L62

Added line #L62 was not covered by tests
single<IFetchPostUseCase> { FetchPostUseCase(get(), get()) }
single<IFetchPostRepliesUseCase> { FetchPostRepliesUseCase(get(), get()) }
}
Expand All @@ -66,6 +69,7 @@ val viewModelModule = module {
factory { AuthViewModel(get(), get(), get(), get(), get()) }
factory { TimelineViewModel(it[0], get(), get(), get(), get()) }
factory { TimelineComposeViewModel(it[0], it[1], it[2], get()) }
factory { SearchViewModel(get(), get()) }

Check warning on line 72 in shared/src/commonMain/kotlin/me/nathanfallet/extopy/di/SharedModule.kt

View check run for this annotation

Codecov / codecov/patch

shared/src/commonMain/kotlin/me/nathanfallet/extopy/di/SharedModule.kt#L72

Added line #L72 was not covered by tests
factory { PostViewModel(it[0], get(), get(), get()) }
factory { ProfileViewModel(it[0], get(), get(), get(), get()) }
factory { NotificationsViewModel() }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package me.nathanfallet.extopy.usecases.posts

import me.nathanfallet.extopy.client.IExtopyClient
import me.nathanfallet.extopy.models.posts.Post
import me.nathanfallet.usecases.pagination.Pagination

class FetchPostsUseCase(
private val client: IExtopyClient,

Check warning on line 8 in shared/src/commonMain/kotlin/me/nathanfallet/extopy/usecases/posts/FetchPostsUseCase.kt

View check run for this annotation

Codecov / codecov/patch

shared/src/commonMain/kotlin/me/nathanfallet/extopy/usecases/posts/FetchPostsUseCase.kt#L7-L8

Added lines #L7 - L8 were not covered by tests
) : IFetchPostsUseCase {

override suspend fun invoke(input: Pagination): List<Post> = client.posts.list(input)

Check warning on line 12 in shared/src/commonMain/kotlin/me/nathanfallet/extopy/usecases/posts/FetchPostsUseCase.kt

View check run for this annotation

Codecov / codecov/patch

shared/src/commonMain/kotlin/me/nathanfallet/extopy/usecases/posts/FetchPostsUseCase.kt#L11-L12

Added lines #L11 - L12 were not covered by tests
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package me.nathanfallet.extopy.usecases.posts

import me.nathanfallet.extopy.models.posts.Post
import me.nathanfallet.usecases.base.ISuspendUseCase
import me.nathanfallet.usecases.pagination.Pagination

interface IFetchPostsUseCase : ISuspendUseCase<Pagination, List<Post>>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package me.nathanfallet.extopy.usecases.users

import me.nathanfallet.extopy.client.IExtopyClient
import me.nathanfallet.extopy.models.users.User
import me.nathanfallet.usecases.pagination.Pagination

class FetchUsersUseCase(
private val client: IExtopyClient,

Check warning on line 8 in shared/src/commonMain/kotlin/me/nathanfallet/extopy/usecases/users/FetchUsersUseCase.kt

View check run for this annotation

Codecov / codecov/patch

shared/src/commonMain/kotlin/me/nathanfallet/extopy/usecases/users/FetchUsersUseCase.kt#L7-L8

Added lines #L7 - L8 were not covered by tests
) : IFetchUsersUseCase {

override suspend fun invoke(input: Pagination): List<User> = client.users.list(input)

Check warning on line 11 in shared/src/commonMain/kotlin/me/nathanfallet/extopy/usecases/users/FetchUsersUseCase.kt

View check run for this annotation

Codecov / codecov/patch

shared/src/commonMain/kotlin/me/nathanfallet/extopy/usecases/users/FetchUsersUseCase.kt#L11

Added line #L11 was not covered by tests

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package me.nathanfallet.extopy.usecases.users

import me.nathanfallet.extopy.models.users.User
import me.nathanfallet.usecases.base.ISuspendUseCase
import me.nathanfallet.usecases.pagination.Pagination

interface IFetchUsersUseCase : ISuspendUseCase<Pagination, List<User>>
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,105 @@ package me.nathanfallet.extopy.viewmodels.timelines

import com.rickclephas.kmm.viewmodel.KMMViewModel
import com.rickclephas.kmm.viewmodel.MutableStateFlow
import com.rickclephas.kmm.viewmodel.coroutineScope
import com.rickclephas.kmp.nativecoroutines.NativeCoroutines
import com.rickclephas.kmp.nativecoroutines.NativeCoroutinesState
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.launch
import me.nathanfallet.extopy.models.application.SearchOptions
import me.nathanfallet.extopy.models.posts.Post
import me.nathanfallet.extopy.models.users.User
import me.nathanfallet.extopy.usecases.posts.IFetchPostsUseCase
import me.nathanfallet.extopy.usecases.users.IFetchUsersUseCase
import me.nathanfallet.usecases.pagination.Pagination

@OptIn(FlowPreview::class)
class SearchViewModel(

private val fetchUsersUseCase: IFetchUsersUseCase,
private val fetchPostsUseCase: IFetchPostsUseCase,
) : KMMViewModel() {

// Properties

private val _search = MutableStateFlow(viewModelScope, "")

Check warning on line 27 in shared/src/commonMain/kotlin/me/nathanfallet/extopy/viewmodels/timelines/SearchViewModel.kt

View check run for this annotation

Codecov / codecov/patch

shared/src/commonMain/kotlin/me/nathanfallet/extopy/viewmodels/timelines/SearchViewModel.kt#L27

Added line #L27 was not covered by tests

private val _users = MutableStateFlow<List<User>?>(viewModelScope, null)
private val _posts = MutableStateFlow<List<Post>?>(viewModelScope, null)

Check warning on line 30 in shared/src/commonMain/kotlin/me/nathanfallet/extopy/viewmodels/timelines/SearchViewModel.kt

View check run for this annotation

Codecov / codecov/patch

shared/src/commonMain/kotlin/me/nathanfallet/extopy/viewmodels/timelines/SearchViewModel.kt#L29-L30

Added lines #L29 - L30 were not covered by tests

private val _hasMoreUsers = MutableStateFlow(viewModelScope, true)
private val _hasMorePosts = MutableStateFlow(viewModelScope, true)

Check warning on line 33 in shared/src/commonMain/kotlin/me/nathanfallet/extopy/viewmodels/timelines/SearchViewModel.kt

View check run for this annotation

Codecov / codecov/patch

shared/src/commonMain/kotlin/me/nathanfallet/extopy/viewmodels/timelines/SearchViewModel.kt#L32-L33

Added lines #L32 - L33 were not covered by tests

@NativeCoroutinesState
val search = _search.asStateFlow()

Check warning on line 36 in shared/src/commonMain/kotlin/me/nathanfallet/extopy/viewmodels/timelines/SearchViewModel.kt

View check run for this annotation

Codecov / codecov/patch

shared/src/commonMain/kotlin/me/nathanfallet/extopy/viewmodels/timelines/SearchViewModel.kt#L36

Added line #L36 was not covered by tests

// Methods
@NativeCoroutinesState
val users = _users.asStateFlow()

Check warning on line 39 in shared/src/commonMain/kotlin/me/nathanfallet/extopy/viewmodels/timelines/SearchViewModel.kt

View check run for this annotation

Codecov / codecov/patch

shared/src/commonMain/kotlin/me/nathanfallet/extopy/viewmodels/timelines/SearchViewModel.kt#L39

Added line #L39 was not covered by tests

@NativeCoroutinesState
val posts = _posts.asStateFlow()

Check warning on line 42 in shared/src/commonMain/kotlin/me/nathanfallet/extopy/viewmodels/timelines/SearchViewModel.kt

View check run for this annotation

Codecov / codecov/patch

shared/src/commonMain/kotlin/me/nathanfallet/extopy/viewmodels/timelines/SearchViewModel.kt#L42

Added line #L42 was not covered by tests

@NativeCoroutinesState
val hasMoreUsers = _hasMoreUsers.asStateFlow()

Check warning on line 45 in shared/src/commonMain/kotlin/me/nathanfallet/extopy/viewmodels/timelines/SearchViewModel.kt

View check run for this annotation

Codecov / codecov/patch

shared/src/commonMain/kotlin/me/nathanfallet/extopy/viewmodels/timelines/SearchViewModel.kt#L45

Added line #L45 was not covered by tests

@NativeCoroutinesState
val hasMorePosts = _hasMorePosts.asStateFlow()

Check warning on line 48 in shared/src/commonMain/kotlin/me/nathanfallet/extopy/viewmodels/timelines/SearchViewModel.kt

View check run for this annotation

Codecov / codecov/patch

shared/src/commonMain/kotlin/me/nathanfallet/extopy/viewmodels/timelines/SearchViewModel.kt#L48

Added line #L48 was not covered by tests

// Setters

fun updateSearch(value: String) {
_search.value = value

Check warning on line 53 in shared/src/commonMain/kotlin/me/nathanfallet/extopy/viewmodels/timelines/SearchViewModel.kt

View check run for this annotation

Codecov / codecov/patch

shared/src/commonMain/kotlin/me/nathanfallet/extopy/viewmodels/timelines/SearchViewModel.kt#L53

Added line #L53 was not covered by tests
}

// Methods

init {
viewModelScope.coroutineScope.launch {
_search.debounce(500L).collect {
fetchUsers(true)
fetchPosts(true)

Check warning on line 62 in shared/src/commonMain/kotlin/me/nathanfallet/extopy/viewmodels/timelines/SearchViewModel.kt

View check run for this annotation

Codecov / codecov/patch

shared/src/commonMain/kotlin/me/nathanfallet/extopy/viewmodels/timelines/SearchViewModel.kt#L58-L62

Added lines #L58 - L62 were not covered by tests
}
}
}

Check warning on line 65 in shared/src/commonMain/kotlin/me/nathanfallet/extopy/viewmodels/timelines/SearchViewModel.kt

View check run for this annotation

Codecov / codecov/patch

shared/src/commonMain/kotlin/me/nathanfallet/extopy/viewmodels/timelines/SearchViewModel.kt#L65

Added line #L65 was not covered by tests

@NativeCoroutines
suspend fun fetchUsers(reset: Boolean = false) {

Check warning on line 68 in shared/src/commonMain/kotlin/me/nathanfallet/extopy/viewmodels/timelines/SearchViewModel.kt

View check run for this annotation

Codecov / codecov/patch

shared/src/commonMain/kotlin/me/nathanfallet/extopy/viewmodels/timelines/SearchViewModel.kt#L68

Added line #L68 was not covered by tests
val search = search.value.trim().takeIf { it.isNotBlank() } ?: return
_users.value = if (reset) fetchUsersUseCase(Pagination(25, 0, SearchOptions(search))).also {
_hasMoreUsers.value = it.isNotEmpty()

Check warning on line 71 in shared/src/commonMain/kotlin/me/nathanfallet/extopy/viewmodels/timelines/SearchViewModel.kt

View check run for this annotation

Codecov / codecov/patch

shared/src/commonMain/kotlin/me/nathanfallet/extopy/viewmodels/timelines/SearchViewModel.kt#L71

Added line #L71 was not covered by tests
} else (_users.value ?: emptyList()) + fetchUsersUseCase(
Pagination(25, users.value?.size?.toLong() ?: 0, SearchOptions(search))
).also {
_hasMoreUsers.value = it.isNotEmpty()
}

Check warning on line 76 in shared/src/commonMain/kotlin/me/nathanfallet/extopy/viewmodels/timelines/SearchViewModel.kt

View check run for this annotation

Codecov / codecov/patch

shared/src/commonMain/kotlin/me/nathanfallet/extopy/viewmodels/timelines/SearchViewModel.kt#L74-L76

Added lines #L74 - L76 were not covered by tests
}

fun loadMoreUsers() {
if (!hasMoreUsers.value) return
viewModelScope.coroutineScope.launch {
fetchUsers()

Check warning on line 82 in shared/src/commonMain/kotlin/me/nathanfallet/extopy/viewmodels/timelines/SearchViewModel.kt

View check run for this annotation

Codecov / codecov/patch

shared/src/commonMain/kotlin/me/nathanfallet/extopy/viewmodels/timelines/SearchViewModel.kt#L81-L82

Added lines #L81 - L82 were not covered by tests
}
}

@NativeCoroutines
suspend fun fetchPosts(reset: Boolean = false) {

Check warning on line 87 in shared/src/commonMain/kotlin/me/nathanfallet/extopy/viewmodels/timelines/SearchViewModel.kt

View check run for this annotation

Codecov / codecov/patch

shared/src/commonMain/kotlin/me/nathanfallet/extopy/viewmodels/timelines/SearchViewModel.kt#L87

Added line #L87 was not covered by tests
val search = search.value.trim().takeIf { it.isNotBlank() } ?: return
_posts.value = if (reset) fetchPostsUseCase(Pagination(25, 0, SearchOptions(search))).also {
_hasMorePosts.value = it.isNotEmpty()

Check warning on line 90 in shared/src/commonMain/kotlin/me/nathanfallet/extopy/viewmodels/timelines/SearchViewModel.kt

View check run for this annotation

Codecov / codecov/patch

shared/src/commonMain/kotlin/me/nathanfallet/extopy/viewmodels/timelines/SearchViewModel.kt#L90

Added line #L90 was not covered by tests
} else (_posts.value ?: emptyList()) + fetchPostsUseCase(
Pagination(25, _posts.value?.size?.toLong() ?: 0, SearchOptions(search))
).also {
_hasMorePosts.value = it.isNotEmpty()
}

Check warning on line 95 in shared/src/commonMain/kotlin/me/nathanfallet/extopy/viewmodels/timelines/SearchViewModel.kt

View check run for this annotation

Codecov / codecov/patch

shared/src/commonMain/kotlin/me/nathanfallet/extopy/viewmodels/timelines/SearchViewModel.kt#L93-L95

Added lines #L93 - L95 were not covered by tests
}

fun loadMorePosts() {
if (!hasMorePosts.value) return
viewModelScope.coroutineScope.launch {
fetchPosts()

Check warning on line 101 in shared/src/commonMain/kotlin/me/nathanfallet/extopy/viewmodels/timelines/SearchViewModel.kt

View check run for this annotation

Codecov / codecov/patch

shared/src/commonMain/kotlin/me/nathanfallet/extopy/viewmodels/timelines/SearchViewModel.kt#L100-L101

Added lines #L100 - L101 were not covered by tests
}
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ class TimelineViewModel(
private val _timeline = MutableStateFlow<Timeline?>(viewModelScope, null)
private val _users = MutableStateFlow<List<User>?>(viewModelScope, null)
private val _posts = MutableStateFlow<List<Post>?>(viewModelScope, null)
private val _search = MutableStateFlow<String?>(viewModelScope, null)

@NativeCoroutinesState
val timeline = _timeline.asStateFlow()
Expand All @@ -40,9 +39,6 @@ class TimelineViewModel(
@NativeCoroutinesState
val posts = _posts.asStateFlow()

@NativeCoroutinesState
val search = _search.asStateFlow()

private var hasMore = true

// Methods
Expand All @@ -64,10 +60,6 @@ class TimelineViewModel(
}
}

fun updateSearch(search: String?) {
_search.value = search
}

@NativeCoroutines
suspend fun onLikeClicked(post: Post) {
updateLikeInPostUseCase(post)?.let {
Expand All @@ -86,11 +78,6 @@ class TimelineViewModel(
}
}

@NativeCoroutines
suspend fun doSearch() {

}

fun loadMoreIfNeeded(postId: String) {
if (!hasMore || posts.value?.lastOrNull()?.id != postId) return
viewModelScope.coroutineScope.launch {
Expand Down
3 changes: 3 additions & 0 deletions shared/src/iosMain/kotlin/me/nathanfallet/extopy/di/Koin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package me.nathanfallet.extopy.di
import me.nathanfallet.extopy.viewmodels.auth.AuthViewModel
import me.nathanfallet.extopy.viewmodels.posts.PostViewModel
import me.nathanfallet.extopy.viewmodels.root.RootViewModel
import me.nathanfallet.extopy.viewmodels.timelines.SearchViewModel
import me.nathanfallet.extopy.viewmodels.timelines.TimelineComposeViewModel
import me.nathanfallet.extopy.viewmodels.timelines.TimelineViewModel
import me.nathanfallet.extopy.viewmodels.users.ProfileViewModel
Expand All @@ -28,6 +29,8 @@ fun Koin.timelineComposeViewModel(body: String, repliedToId: String?, repostOfId
parametersOf(body, repliedToId, repostOfId)
}

val Koin.searchViewModel: SearchViewModel get() = get()

fun Koin.postViewModel(id: String): PostViewModel = get {
parametersOf(id)
}
Expand Down

0 comments on commit 185ffca

Please sign in to comment.