From f7070899e3fd21a38d2931e7366ad69f5a78b8a7 Mon Sep 17 00:00:00 2001 From: Antoine Robiez Date: Fri, 15 Mar 2024 15:57:13 +0100 Subject: [PATCH] Moved Android code to shared --- .../androidmakers/AndroidMakersApplication.kt | 19 +- .../paug/androidmakers/di/ViewModelModule.kt | 6 +- .../fr/paug/androidmakers/ui/MainActivity.kt | 3 +- .../androidmakers/ui/components/AVALayout.kt | 39 ++-- .../androidmakers/ui/components/MainLayout.kt | 5 +- .../ui/components/about/AboutLayout.kt | 183 ---------------- .../ui/components/speakers/SpeakerScreen.kt | 195 ------------------ .../ui/components/sponsors/SponsorScreen.kt | 93 --------- .../ui/theme/AndroidMakersTheme.kt | 13 +- gradle/libs.versions.toml | 4 + iosApp/RobotConf/AppDelegate.swift | 2 +- iosApp/RobotConf/UI/ContentView.swift | 4 +- .../RobotConf/UI/Speakers/SpeakersView.swift | 7 +- shared/build.gradle.kts | 3 + shared/ui/build.gradle.kts | 3 + .../com/androidmakers/di/ViewModelModule.kt | 10 + .../androidmakers/ui/common/LceViewModel.kt | 52 +++++ .../ui/speakers/SpeakerListScreen.kt | 17 +- .../ui/speakers/SpeakerListViewModel.kt | 9 +- .../ui/sponsors/SponsorScreen.kt | 16 ++ .../ui/sponsors/SponsorsViewModel.kt | 19 ++ .../ui/theme/AndroidMakersTheme.kt | 13 +- .../commonMain/moko-resources/fr/strings.xml | 2 +- .../ui/speakers/SpeakerListViewController.kt | 17 +- .../ui/sponsors/SponsorViewController.kt | 7 +- 25 files changed, 172 insertions(+), 569 deletions(-) delete mode 100644 androidApp/src/main/java/fr/paug/androidmakers/ui/components/about/AboutLayout.kt delete mode 100644 androidApp/src/main/java/fr/paug/androidmakers/ui/components/speakers/SpeakerScreen.kt delete mode 100644 androidApp/src/main/java/fr/paug/androidmakers/ui/components/sponsors/SponsorScreen.kt create mode 100644 shared/ui/src/commonMain/kotlin/com/androidmakers/di/ViewModelModule.kt create mode 100644 shared/ui/src/commonMain/kotlin/com/androidmakers/ui/common/LceViewModel.kt rename androidApp/src/main/java/fr/paug/androidmakers/ui/components/speakers/SpeakerViewModel.kt => shared/ui/src/commonMain/kotlin/com/androidmakers/ui/speakers/SpeakerListViewModel.kt (71%) create mode 100644 shared/ui/src/commonMain/kotlin/com/androidmakers/ui/sponsors/SponsorsViewModel.kt diff --git a/androidApp/src/main/java/fr/paug/androidmakers/AndroidMakersApplication.kt b/androidApp/src/main/java/fr/paug/androidmakers/AndroidMakersApplication.kt index 6bd2a842..57a385d3 100644 --- a/androidApp/src/main/java/fr/paug/androidmakers/AndroidMakersApplication.kt +++ b/androidApp/src/main/java/fr/paug/androidmakers/AndroidMakersApplication.kt @@ -1,25 +1,12 @@ package fr.paug.androidmakers import android.app.Application +import com.androidmakers.di.viewModelModule import fr.androidmakers.di.DependenciesBuilder -import fr.androidmakers.domain.interactor.GetAfterpartyVenueUseCase -import fr.androidmakers.domain.interactor.GetAgendaUseCase -import fr.androidmakers.domain.interactor.GetConferenceVenueUseCase -import fr.androidmakers.domain.interactor.OpenCocUseCase -import fr.androidmakers.domain.interactor.OpenFaqUseCase -import fr.androidmakers.domain.interactor.OpenXAccountUseCase -import fr.androidmakers.domain.interactor.OpenXHashtagUseCase -import fr.androidmakers.domain.interactor.OpenYoutubeUseCase -import fr.androidmakers.domain.interactor.SyncBookmarksUseCase -import fr.androidmakers.domain.repo.BookmarksRepository -import fr.androidmakers.domain.utils.UrlOpener -import fr.androidmakers.store.local.createDataStore -import fr.androidmakers.store.local.BookmarksDataStoreRepository -import fr.paug.androidmakers.di.viewModelModule +import fr.paug.androidmakers.di.androidViewModelModule import io.openfeedback.android.OpenFeedback - class AndroidMakersApplication : Application() { lateinit var openFeedback: OpenFeedback @@ -29,7 +16,7 @@ class AndroidMakersApplication : Application() { super.onCreate() DependenciesBuilder(this).inject( - listOf(viewModelModule) + listOf(androidViewModelModule, viewModelModule) ) openFeedback = OpenFeedback( diff --git a/androidApp/src/main/java/fr/paug/androidmakers/di/ViewModelModule.kt b/androidApp/src/main/java/fr/paug/androidmakers/di/ViewModelModule.kt index 7a2a7529..a94bb2bc 100644 --- a/androidApp/src/main/java/fr/paug/androidmakers/di/ViewModelModule.kt +++ b/androidApp/src/main/java/fr/paug/androidmakers/di/ViewModelModule.kt @@ -2,21 +2,17 @@ package fr.paug.androidmakers.di import fr.paug.androidmakers.ui.MainActivityViewModel import fr.paug.androidmakers.ui.components.AgendaLayoutViewModel -import fr.paug.androidmakers.ui.components.PartnersViewModel import fr.paug.androidmakers.ui.components.agenda.AgendaPagerViewModel import fr.paug.androidmakers.ui.components.session.SessionDetailViewModel -import fr.paug.androidmakers.ui.components.speakers.SpeakerViewModel import fr.paug.androidmakers.ui.components.speakers.details.SpeakerDetailsViewModel import fr.paug.androidmakers.ui.components.venue.VenueViewModel import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module -val viewModelModule = module { +val androidViewModelModule = module { viewModel { MainActivityViewModel(get(), get(), get(), get(), get(), get(), get(), get()) } - viewModel { PartnersViewModel(get()) } viewModel { AgendaPagerViewModel(get(), get(), get()) } viewModel { AgendaLayoutViewModel(get()) } - viewModel { SpeakerViewModel(get()) } viewModel { SpeakerDetailsViewModel(get(), get()) } viewModel { SessionDetailViewModel(get(), get(), get(), get(), get(), get())} viewModel { VenueViewModel(get(), get()) } diff --git a/androidApp/src/main/java/fr/paug/androidmakers/ui/MainActivity.kt b/androidApp/src/main/java/fr/paug/androidmakers/ui/MainActivity.kt index f59adf70..3b2a9b6c 100644 --- a/androidApp/src/main/java/fr/paug/androidmakers/ui/MainActivity.kt +++ b/androidApp/src/main/java/fr/paug/androidmakers/ui/MainActivity.kt @@ -16,6 +16,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.staticCompositionLocalOf import androidx.core.view.WindowCompat import androidx.lifecycle.lifecycleScope +import com.androidmakers.ui.about.AboutActions import com.google.android.gms.auth.api.signin.GoogleSignIn import com.google.android.gms.auth.api.signin.GoogleSignInAccount import com.google.android.gms.auth.api.signin.GoogleSignInOptions @@ -30,9 +31,7 @@ import fr.androidmakers.domain.model.Partner import fr.androidmakers.store.firebase.toUser import fr.paug.androidmakers.R import fr.paug.androidmakers.ui.components.MainLayout -import fr.paug.androidmakers.ui.components.about.AboutActions import fr.paug.androidmakers.ui.theme.AndroidMakersTheme -import fr.paug.androidmakers.util.CustomTabUtil import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch diff --git a/androidApp/src/main/java/fr/paug/androidmakers/ui/components/AVALayout.kt b/androidApp/src/main/java/fr/paug/androidmakers/ui/components/AVALayout.kt index a8eb3836..4ca51cf1 100644 --- a/androidApp/src/main/java/fr/paug/androidmakers/ui/components/AVALayout.kt +++ b/androidApp/src/main/java/fr/paug/androidmakers/ui/components/AVALayout.kt @@ -42,34 +42,31 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import at.asitplus.KmmResult +import com.androidmakers.ui.about.AboutActions +import com.androidmakers.ui.about.AboutScreen +import com.androidmakers.ui.speakers.SpeakerListViewModel +import com.androidmakers.ui.speakers.SpeakerScreen +import com.androidmakers.ui.sponsors.SponsorsScreen import fr.androidmakers.domain.interactor.GetPartnersUseCase import fr.androidmakers.domain.model.PartnerGroup import fr.androidmakers.domain.model.SpeakerId import fr.androidmakers.domain.model.User -import fr.androidmakers.domain.repo.PartnersRepository -import fr.paug.androidmakers.AndroidMakersApplication +import fr.paug.androidmakers.BuildConfig import fr.paug.androidmakers.R import fr.paug.androidmakers.ui.MR -import fr.paug.androidmakers.ui.components.about.AboutActions -import fr.paug.androidmakers.ui.components.about.AboutLayout import fr.paug.androidmakers.ui.components.agenda.AgendaLayout -import fr.paug.androidmakers.ui.components.speakers.SpeakerScreen -import fr.paug.androidmakers.ui.components.speakers.SpeakerViewModel -import fr.paug.androidmakers.ui.components.sponsors.SponsorsScreen import fr.paug.androidmakers.ui.navigation.AVANavigationRoute import fr.paug.androidmakers.ui.util.stringResource import fr.paug.androidmakers.ui.viewmodel.LceViewModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch +import moe.tlaster.precompose.koin.koinViewModel import org.koin.androidx.compose.koinViewModel /** @@ -262,23 +259,23 @@ private fun AVANavHost( composable(route = AVANavigationRoute.SPEAKERS.name) { - val speakerViewModel: SpeakerViewModel = koinViewModel() + val speakerViewModel: SpeakerListViewModel = koinViewModel() SpeakerScreen( - speakerViewModel = speakerViewModel, + viewModel = speakerViewModel, navigateToSpeakerDetails = navigateToSpeakerDetails ) } composable(route = AVANavigationRoute.ABOUT.name) { - AboutLayout( + AboutScreen( + versionCode = BuildConfig.VERSION_CODE.toString(), + versionName = BuildConfig.VERSION_NAME, aboutActions = aboutActions ) } composable(route = AVANavigationRoute.SPONSORS.name) { - val partnerList by koinViewModel().values.collectAsState() SponsorsScreen( - partnerList = partnerList, onSponsorClick = aboutActions.onSponsorClick ) } @@ -286,18 +283,6 @@ private fun AVANavHost( } } -class PartnersViewModel( - private val getPartnersUseCase: GetPartnersUseCase -) : LceViewModel>() { - override fun produce(): Flow>> { - return getPartnersUseCase() - } - - init { - launch(false) - } -} - @Preview @Composable private fun AVALayoutPreview() { diff --git a/androidApp/src/main/java/fr/paug/androidmakers/ui/components/MainLayout.kt b/androidApp/src/main/java/fr/paug/androidmakers/ui/components/MainLayout.kt index aaa6f220..7752a800 100644 --- a/androidApp/src/main/java/fr/paug/androidmakers/ui/components/MainLayout.kt +++ b/androidApp/src/main/java/fr/paug/androidmakers/ui/components/MainLayout.kt @@ -4,18 +4,15 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.tooling.preview.Preview -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument +import com.androidmakers.ui.about.AboutActions import fr.androidmakers.domain.model.SpeakerId import fr.androidmakers.domain.model.User -import fr.paug.androidmakers.ui.components.about.AboutActions import fr.paug.androidmakers.ui.components.session.SessionDetailLayout import fr.paug.androidmakers.ui.components.session.SessionDetailViewModel import fr.paug.androidmakers.ui.components.speakers.details.SpeakerDetailsRoute diff --git a/androidApp/src/main/java/fr/paug/androidmakers/ui/components/about/AboutLayout.kt b/androidApp/src/main/java/fr/paug/androidmakers/ui/components/about/AboutLayout.kt deleted file mode 100644 index fbb3cead..00000000 --- a/androidApp/src/main/java/fr/paug/androidmakers/ui/components/about/AboutLayout.kt +++ /dev/null @@ -1,183 +0,0 @@ -package fr.paug.androidmakers.ui.components.about - -import androidx.compose.foundation.Image -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Card -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import fr.androidmakers.domain.model.Partner -import fr.paug.androidmakers.BuildConfig -import fr.paug.androidmakers.R -import fr.paug.androidmakers.ui.MR -import fr.paug.androidmakers.ui.util.stringResource - -class AboutActions( - val onFaqClick: () -> Unit = {}, - val onCodeOfConductClick: () -> Unit = {}, - val onXHashtagClick: () -> Unit = {}, - val onXLogoClick: () -> Unit = {}, - val onYouTubeLogoClick: () -> Unit = {}, - val onSponsorClick: (partner: Partner) -> Unit = {}, -) - -@Composable -fun AboutLayout( - aboutActions: AboutActions, -) { - Column( - modifier = Modifier - .fillMaxSize() - .verticalScroll(state = rememberScrollState()) - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically) - ) { - IntroCard( - onFaqClick = aboutActions.onFaqClick, - onCocClick = aboutActions.onCodeOfConductClick - ) - - SocialCard( - aboutActions.onXHashtagClick, - aboutActions.onXLogoClick, - aboutActions.onYouTubeLogoClick - ) - - Text( - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center, - text = stringResource( - MR.strings.version, - BuildConfig.VERSION_NAME, - BuildConfig.VERSION_CODE - ), - ) - } -} - -@Composable -private fun IntroCard( - onFaqClick: () -> Unit, - onCocClick: () -> Unit -) { - Column(Modifier.padding(vertical = 8.dp)) { - Image( - modifier = Modifier - .height(64.dp) - .fillMaxWidth(), - painter = painterResource(R.drawable.logo_android_makers), - contentDescription = "Logo" - ) - Text( - modifier = Modifier.padding(16.dp), - text = stringResource(MR.strings.about_android_makers) - ) - Row( - Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center - - ) { - ClickableText( - text = stringResource(MR.strings.faq), - onClick = onFaqClick - ) - ClickableText( - text = stringResource(MR.strings.code_of_conduct), - onClick = onCocClick - ) - } - } -} - -@Composable -private fun SocialCard( - onXHashtagClick: () -> Unit, - onXLogoClick: () -> Unit, - onYouTubeLogoClick: () -> Unit -) { - Card(Modifier.fillMaxWidth()) { - Column(Modifier.padding(8.dp)) { - Row( - Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center - ) { - ClickableText(stringResource(MR.strings.x_hashtag), onXHashtagClick) - } - Row( - Modifier - .fillMaxWidth() - .padding(top = 8.dp), - horizontalArrangement = Arrangement.Center - ) { - Icon( - modifier = Modifier - .size(96.dp, 64.dp) - .clickable(onClick = onXLogoClick) - .padding(12.dp), - painter = painterResource(R.drawable.ic_network_x), - tint = Color(0xFF000000), - contentDescription = "X" - ) - Icon( - modifier = Modifier - .size(96.dp, 64.dp) - .clickable(onClick = onYouTubeLogoClick), - painter = painterResource(R.drawable.ic_network_youtube), - tint = Color(0xffff0000), - contentDescription = "YouTube" - ) - } - } - } -} - -@Composable -private fun ClickableText( - text: String, - onClick: () -> Unit, -) { - Text( - modifier = Modifier - .clickable(onClick = onClick) - .padding(8.dp), - text = text, - color = MaterialTheme.colorScheme.primary - ) -} - - -@Preview -@Composable -private fun AboutLayoutLoadingPreview() { - AboutLayout( - aboutActions = AboutActions() - ) -} - - -@Preview -@Composable -private fun AboutLayoutLoadedPreview() { - AboutLayout( - aboutActions = AboutActions() - ) -} diff --git a/androidApp/src/main/java/fr/paug/androidmakers/ui/components/speakers/SpeakerScreen.kt b/androidApp/src/main/java/fr/paug/androidmakers/ui/components/speakers/SpeakerScreen.kt deleted file mode 100644 index 94d2f637..00000000 --- a/androidApp/src/main/java/fr/paug/androidmakers/ui/components/speakers/SpeakerScreen.kt +++ /dev/null @@ -1,195 +0,0 @@ -package fr.paug.androidmakers.ui.components.speakers - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Search -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.ListItem -import androidx.compose.material3.ListItemDefaults -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.SearchBar -import androidx.compose.material3.SearchBarDefaults -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.isTraversalGroup -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.compose.ui.zIndex -import coil.compose.AsyncImage -import coil.request.ImageRequest -import fr.androidmakers.domain.model.Speaker -import fr.androidmakers.domain.model.SpeakerId -import fr.paug.androidmakers.R -import fr.paug.androidmakers.ui.MR -import fr.paug.androidmakers.ui.components.LoadingLayout -import fr.paug.androidmakers.ui.util.stringResource -import fr.paug.androidmakers.ui.viewmodel.Lce - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun SpeakerScreen( - modifier: Modifier = Modifier, - speakerViewModel: SpeakerViewModel, - navigateToSpeakerDetails: (SpeakerId) -> Unit, -) { - - val speakersUiState by speakerViewModel.uiState.collectAsState( - initial = Lce.Loading - ) - - when (val state = speakersUiState) { - Lce.Loading -> LoadingLayout() - Lce.Error -> { - - } - - is Lce.Content -> { - - var text by rememberSaveable { mutableStateOf("") } - var active by rememberSaveable { mutableStateOf(false) } - - Box(Modifier.fillMaxSize()) { - Box(Modifier - .semantics { isTraversalGroup = true } - .background(MaterialTheme.colorScheme.background) - .padding(bottom = 8.dp) - .zIndex(1f) - .fillMaxWidth()) { - - SearchBar( - modifier = Modifier.align(Alignment.TopCenter), - query = text, - onQueryChange = { text = it }, - onSearch = { active = false }, - active = active, - colors = SearchBarDefaults.colors( - containerColor = MaterialTheme.colorScheme.surface, - dividerColor = MaterialTheme.colorScheme.primary, -// inputFieldColors = SearchBarDefaults.inputFieldColors( -// focusedTextColor = MaterialTheme.colorScheme.surface, -//// unfocusedTextColor =, -//// disabledTextColor =, -//// cursorColor =, -//// selectionColors =, -//// focusedLeadingIconColor =, -//// unfocusedLeadingIconColor =, -//// disabledLeadingIconColor =, -//// focusedTrailingIconColor =, -//// unfocusedTrailingIconColor =, -//// disabledTrailingIconColor =, -//// focusedPlaceholderColor =, -//// unfocusedPlaceholderColor =, -//// disabledPlaceholderColor = -// ), - ), - onActiveChange = { - active = it - }, - windowInsets = WindowInsets(0, 0, 0, 0), - placeholder = { Text(stringResource(MR.strings.speaker_search_placeholder)) }, - leadingIcon = { Icon(Icons.Rounded.Search, contentDescription = null) } - ) { - LazyColumn { - items(state.content.speakers.filter { it.name?.contains(text, ignoreCase = true) == true }) { speaker -> - SpeakerItem( - speaker = speaker, - navigateToSpeakerDetails = navigateToSpeakerDetails, - ) - } - } - } - } - - AnimatedVisibility( - visible = !active, - enter = fadeIn(), - exit = fadeOut(), - ) { - LazyColumn( - contentPadding = PaddingValues(start = 0.dp, top = 72.dp, end = 0.dp, bottom = 16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - items(state.content.speakers.filter { it.name?.contains(text, ignoreCase = true) == true }) { speaker -> - SpeakerItem( - speaker = speaker, - navigateToSpeakerDetails = navigateToSpeakerDetails - ) - } - } - } - } - } - - } - -} - -@Composable -fun SpeakerItem( - modifier: Modifier = Modifier, - speaker: Speaker, - navigateToSpeakerDetails: (SpeakerId) -> Unit, -) { - - ListItem( - modifier = modifier.clickable(onClick = { navigateToSpeakerDetails(speaker.id) }), - colors = ListItemDefaults.colors( - containerColor = MaterialTheme.colorScheme.background, - ), - headlineContent = { - Text( - text = speaker.name.orEmpty(), - style = MaterialTheme.typography.titleLarge.copy( - fontWeight = FontWeight.Bold - ) - ) - }, - supportingContent = speaker.company?.let { company -> - { - Text( - text = company, - style = MaterialTheme.typography.labelMedium, - ) - } - }, - leadingContent = { - AsyncImage( - modifier = Modifier - .size(64.dp) - .clip(CircleShape), - model = ImageRequest.Builder(LocalContext.current) - .data(speaker.photoUrl) - .build(), - contentDescription = stringResource(MR.strings.speakers) - ) - }, - trailingContent = {}, - ) -} diff --git a/androidApp/src/main/java/fr/paug/androidmakers/ui/components/sponsors/SponsorScreen.kt b/androidApp/src/main/java/fr/paug/androidmakers/ui/components/sponsors/SponsorScreen.kt deleted file mode 100644 index dd9afe3e..00000000 --- a/androidApp/src/main/java/fr/paug/androidmakers/ui/components/sponsors/SponsorScreen.kt +++ /dev/null @@ -1,93 +0,0 @@ -package fr.paug.androidmakers.ui.components.sponsors - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.GridItemSpan -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import coil.compose.AsyncImage -import fr.androidmakers.domain.model.Partner -import fr.androidmakers.domain.model.PartnerGroup -import fr.paug.androidmakers.ui.viewmodel.Lce -import java.util.Locale - -@Composable -fun SponsorsScreen( - partnerList: Lce>, - onSponsorClick: (partner: Partner) -> Unit -) { - when (partnerList) { - is Lce.Loading, Lce.Error -> { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(8.dp), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator() - } - } - - is Lce.Content -> { - - LazyVerticalGrid( - columns = GridCells.Adaptive(minSize = 128.dp), - contentPadding = PaddingValues(start = 12.dp, end = 12.dp, bottom = 8.dp) - ) { - - for (partnerGroup in partnerList.content) { - - // Sponsor "group" (e.g. Organizers, Gold, etc.) - item(span = { - GridItemSpan(maxLineSpan) - }) { - Text( - modifier = Modifier - .padding(top = 32.dp, bottom = 8.dp) - .fillMaxWidth(), - textAlign = TextAlign.Start, - text = partnerGroup.title.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }, - style = MaterialTheme.typography.titleMedium.copy( - color = MaterialTheme.colorScheme.primary, - fontWeight = FontWeight.Bold, - fontSize = 18.sp - ) - ) - - } - - // Sponsor logo - for (partner in partnerGroup.partners) { - item { - AsyncImage( - modifier = Modifier - .fillMaxWidth() - .height(80.dp) - .clickable { - onSponsorClick(partner) - }, - model = partner.logoUrl, - contentDescription = partner.name - ) - } - } - } - } - } - - } -} diff --git a/androidApp/src/main/java/fr/paug/androidmakers/ui/theme/AndroidMakersTheme.kt b/androidApp/src/main/java/fr/paug/androidmakers/ui/theme/AndroidMakersTheme.kt index 258666b0..67ff9b43 100644 --- a/androidApp/src/main/java/fr/paug/androidmakers/ui/theme/AndroidMakersTheme.kt +++ b/androidApp/src/main/java/fr/paug/androidmakers/ui/theme/AndroidMakersTheme.kt @@ -11,6 +11,7 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp import fr.paug.androidmakers.R +import moe.tlaster.precompose.PreComposeApp val GillSans = FontFamily( Font(R.font.gill_sans_light, FontWeight.Light), @@ -138,9 +139,11 @@ fun AndroidMakersTheme( val colorSchemeColors = if (!useDarkTheme) LightDefaultColorScheme else DarkDefaultColorScheme - MaterialTheme( - colorScheme = colorSchemeColors, - typography = AndroidMakersTypography, - content = content, - ) + PreComposeApp { + MaterialTheme( + colorScheme = colorSchemeColors, + typography = AndroidMakersTypography, + content = content, + ) + } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ad8ab91c..44fe78e5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,6 +20,7 @@ datastore = "1.1.0-beta01" skie = "0.6.1" firebase = "1.11.1" koin = "3.5.3" +precompose = "1.6.0-rc02" compose-plugin = "1.6.0" [libraries] @@ -67,6 +68,9 @@ androidx-datastore-preferences = { module = "androidx.datastore:datastore-prefer androidx-datastore-preferences-core = { group = "androidx.datastore", name = "datastore-preferences-core", version.ref = "datastore" } firebase-installations = { module = "dev.gitlive:firebase-installations", version.ref = "firebase" } firebase-auth = { module = "dev.gitlive:firebase-auth", version.ref = "firebase" } +precompose = { module = "moe.tlaster:precompose", version.ref = "precompose" } +precompose-viewmodel = { module = "moe.tlaster:precompose-viewmodel", version.ref = "precompose" } +precompose-koin = { module = "moe.tlaster:precompose-koin", version.ref = "precompose" } koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" } koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koin" } diff --git a/iosApp/RobotConf/AppDelegate.swift b/iosApp/RobotConf/AppDelegate.swift index 85315e18..8460da5f 100644 --- a/iosApp/RobotConf/AppDelegate.swift +++ b/iosApp/RobotConf/AppDelegate.swift @@ -28,7 +28,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { print("⚠️ Firebase descriptor for the main purpose is not embedded, crashlytics disabled.") } - DependenciesBuilder().inject(platformModules: []) + DependenciesBuilder().inject(platformModules: [viewModelModule]) return true } diff --git a/iosApp/RobotConf/UI/ContentView.swift b/iosApp/RobotConf/UI/ContentView.swift index d42b6ad6..4d9e98dd 100644 --- a/iosApp/RobotConf/UI/ContentView.swift +++ b/iosApp/RobotConf/UI/ContentView.swift @@ -26,7 +26,9 @@ struct ContentView: View { } }.tag(1) - SpeakersView() + SpeakersView( + onSpeakerClick: { _ in } + ) .tabItem { VStack { Image(systemName: "person.3.fill") diff --git a/iosApp/RobotConf/UI/Speakers/SpeakersView.swift b/iosApp/RobotConf/UI/Speakers/SpeakersView.swift index 6de433e0..3942ce29 100644 --- a/iosApp/RobotConf/UI/Speakers/SpeakersView.swift +++ b/iosApp/RobotConf/UI/Speakers/SpeakersView.swift @@ -11,8 +11,13 @@ import SwiftUI import shared struct SpeakersView: UIViewControllerRepresentable { + + let onSpeakerClick: (String) -> Void + func makeUIViewController(context: Context) -> UIViewController { - return SpeakerListViewController() + return SpeakerListViewController( + onSpeakerClick: onSpeakerClick + ) } func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 3da63a70..40969a54 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -27,6 +27,9 @@ kotlin { sourceSets { commonMain.dependencies { api(libs.moko.resources) + + // Temporary + api(libs.precompose.koin) api(project(":shared:ui")) api(project(":shared:domain")) api(project(":shared:di")) diff --git a/shared/ui/build.gradle.kts b/shared/ui/build.gradle.kts index a04a784e..47a8de86 100644 --- a/shared/ui/build.gradle.kts +++ b/shared/ui/build.gradle.kts @@ -33,6 +33,9 @@ kotlin { implementation(project(":shared:domain")) implementation(project(":shared:di")) api("io.github.qdsfdhvh:image-loader:1.7.8") + api(libs.precompose) + api(libs.precompose.viewmodel) + api(libs.precompose.koin) } } } diff --git a/shared/ui/src/commonMain/kotlin/com/androidmakers/di/ViewModelModule.kt b/shared/ui/src/commonMain/kotlin/com/androidmakers/di/ViewModelModule.kt new file mode 100644 index 00000000..a5c4137b --- /dev/null +++ b/shared/ui/src/commonMain/kotlin/com/androidmakers/di/ViewModelModule.kt @@ -0,0 +1,10 @@ +package com.androidmakers.di + +import com.androidmakers.ui.speakers.SpeakerListViewModel +import com.androidmakers.ui.sponsors.SponsorsViewModel +import org.koin.dsl.module + +val viewModelModule = module { + factory { SpeakerListViewModel(get()) } + factory { SponsorsViewModel(get()) } +} diff --git a/shared/ui/src/commonMain/kotlin/com/androidmakers/ui/common/LceViewModel.kt b/shared/ui/src/commonMain/kotlin/com/androidmakers/ui/common/LceViewModel.kt new file mode 100644 index 00000000..c67fa022 --- /dev/null +++ b/shared/ui/src/commonMain/kotlin/com/androidmakers/ui/common/LceViewModel.kt @@ -0,0 +1,52 @@ +package com.androidmakers.ui.common + +import at.asitplus.KmmResult +import com.androidmakers.ui.model.Lce +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.launch +import moe.tlaster.precompose.viewmodel.ViewModel +import moe.tlaster.precompose.viewmodel.viewModelScope + + +abstract class LceViewModel : ViewModel() { + abstract fun produce(): Flow> + + private val _mutableSharedState = MutableStateFlow>(Lce.Loading) + val values = _mutableSharedState.asStateFlow() + + private val _isRefreshing = MutableStateFlow(false) + val isRefreshing = _isRefreshing.asStateFlow() + + private var job: Job? = null + + protected fun launch(isRefresh: Boolean) { + job?.cancel() + job = viewModelScope.launch { + produce().map { + it.fold( + onSuccess = { Lce.Content(it) }, + onFailure = { Lce.Error } + ) + }.onStart { + if (!isRefresh) { + emit(Lce.Loading) + } + }.collect { + _mutableSharedState.value = it + if (it != Lce.Loading) { + _isRefreshing.value = false + } + } + } + } + + fun refresh() { + _isRefreshing.value = true + launch(true) + } +} diff --git a/shared/ui/src/commonMain/kotlin/com/androidmakers/ui/speakers/SpeakerListScreen.kt b/shared/ui/src/commonMain/kotlin/com/androidmakers/ui/speakers/SpeakerListScreen.kt index e12f23b9..c3abc3f9 100644 --- a/shared/ui/src/commonMain/kotlin/com/androidmakers/ui/speakers/SpeakerListScreen.kt +++ b/shared/ui/src/commonMain/kotlin/com/androidmakers/ui/speakers/SpeakerListScreen.kt @@ -28,6 +28,7 @@ import androidx.compose.material3.SearchBar import androidx.compose.material3.SearchBarDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable @@ -51,20 +52,23 @@ import fr.paug.androidmakers.ui.MR @Composable fun SpeakerScreen( modifier: Modifier = Modifier, - content: Lce>, + viewModel: SpeakerListViewModel, navigateToSpeakerDetails: (String) -> Unit, ) { - when (content) { + val state by viewModel.uiState.collectAsState(Lce.Loading) + + when (state) { Lce.Loading -> LoadingLayout() Lce.Error -> { } - is Lce.Content<*> -> { + is Lce.Content -> { var text by rememberSaveable { mutableStateOf("") } var active by rememberSaveable { mutableStateOf(false) } + val content = (state as Lce.Content).content Box(Modifier.fillMaxSize()) { Box(Modifier @@ -92,8 +96,8 @@ fun SpeakerScreen( leadingIcon = { Icon(Icons.Rounded.Search, contentDescription = null) } ) { LazyColumn { - val items = content as Lce.Content> - items(items.content.filter { it.name?.contains(text, ignoreCase = true) == true }) { speaker -> + val speakers = content.speakers + items(speakers.filter { it.name?.contains(text, ignoreCase = true) == true }) { speaker -> SpeakerItem( speaker = speaker, navigateToSpeakerDetails = navigateToSpeakerDetails, @@ -112,8 +116,7 @@ fun SpeakerScreen( contentPadding = PaddingValues(start = 0.dp, top = 72.dp, end = 0.dp, bottom = 16.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { - val items = content as Lce.Content> - items(items.content.filter { it.name?.contains(text, ignoreCase = true) == true }) { speaker -> + items(content.speakers.filter { it.name?.contains(text, ignoreCase = true) == true }) { speaker -> SpeakerItem( speaker = speaker, navigateToSpeakerDetails = navigateToSpeakerDetails diff --git a/androidApp/src/main/java/fr/paug/androidmakers/ui/components/speakers/SpeakerViewModel.kt b/shared/ui/src/commonMain/kotlin/com/androidmakers/ui/speakers/SpeakerListViewModel.kt similarity index 71% rename from androidApp/src/main/java/fr/paug/androidmakers/ui/components/speakers/SpeakerViewModel.kt rename to shared/ui/src/commonMain/kotlin/com/androidmakers/ui/speakers/SpeakerListViewModel.kt index 16239dc2..5bacdc50 100644 --- a/androidApp/src/main/java/fr/paug/androidmakers/ui/components/speakers/SpeakerViewModel.kt +++ b/shared/ui/src/commonMain/kotlin/com/androidmakers/ui/speakers/SpeakerListViewModel.kt @@ -1,13 +1,12 @@ -package fr.paug.androidmakers.ui.components.speakers +package com.androidmakers.ui.speakers -import androidx.lifecycle.ViewModel +import com.androidmakers.ui.model.Lce import fr.androidmakers.domain.model.Speaker import fr.androidmakers.domain.repo.SpeakersRepository -import fr.paug.androidmakers.AndroidMakersApplication -import fr.paug.androidmakers.ui.viewmodel.Lce import kotlinx.coroutines.flow.map +import moe.tlaster.precompose.viewmodel.ViewModel -class SpeakerViewModel( +class SpeakerListViewModel( speakersRepository: SpeakersRepository ) : ViewModel() { diff --git a/shared/ui/src/commonMain/kotlin/com/androidmakers/ui/sponsors/SponsorScreen.kt b/shared/ui/src/commonMain/kotlin/com/androidmakers/ui/sponsors/SponsorScreen.kt index 585f8d91..f4ac2280 100644 --- a/shared/ui/src/commonMain/kotlin/com/androidmakers/ui/sponsors/SponsorScreen.kt +++ b/shared/ui/src/commonMain/kotlin/com/androidmakers/ui/sponsors/SponsorScreen.kt @@ -14,6 +14,8 @@ import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight @@ -24,9 +26,23 @@ import com.androidmakers.ui.model.Lce import com.seiko.imageloader.rememberImagePainter import fr.androidmakers.domain.model.Partner import fr.androidmakers.domain.model.PartnerGroup +import moe.tlaster.precompose.koin.koinViewModel @Composable fun SponsorsScreen( + onSponsorClick: (partner: Partner) -> Unit +) { + val viewModel = koinViewModel(SponsorsViewModel::class) + val sponsors by viewModel.values.collectAsState() + + SponsorsView( + partnerList = sponsors, + onSponsorClick = onSponsorClick + ) +} + +@Composable +fun SponsorsView( partnerList: Lce>, onSponsorClick: (partner: Partner) -> Unit ) { diff --git a/shared/ui/src/commonMain/kotlin/com/androidmakers/ui/sponsors/SponsorsViewModel.kt b/shared/ui/src/commonMain/kotlin/com/androidmakers/ui/sponsors/SponsorsViewModel.kt new file mode 100644 index 00000000..52b6dcbe --- /dev/null +++ b/shared/ui/src/commonMain/kotlin/com/androidmakers/ui/sponsors/SponsorsViewModel.kt @@ -0,0 +1,19 @@ +package com.androidmakers.ui.sponsors + +import at.asitplus.KmmResult +import com.androidmakers.ui.common.LceViewModel +import fr.androidmakers.domain.interactor.GetPartnersUseCase +import fr.androidmakers.domain.model.PartnerGroup +import kotlinx.coroutines.flow.Flow + +class SponsorsViewModel( + private val getPartnersUseCase: GetPartnersUseCase +) : LceViewModel>() { + override fun produce(): Flow>> { + return getPartnersUseCase() + } + + init { + launch(false) + } +} diff --git a/shared/ui/src/commonMain/kotlin/com/androidmakers/ui/theme/AndroidMakersTheme.kt b/shared/ui/src/commonMain/kotlin/com/androidmakers/ui/theme/AndroidMakersTheme.kt index 19aaaa69..6a98967d 100644 --- a/shared/ui/src/commonMain/kotlin/com/androidmakers/ui/theme/AndroidMakersTheme.kt +++ b/shared/ui/src/commonMain/kotlin/com/androidmakers/ui/theme/AndroidMakersTheme.kt @@ -11,6 +11,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp import dev.icerock.moko.resources.compose.fontFamilyResource import fr.paug.androidmakers.ui.MR +import moe.tlaster.precompose.PreComposeApp import org.jetbrains.compose.resources.ExperimentalResourceApi @Composable @@ -114,10 +115,12 @@ fun AndroidMakersTheme( ) { val colorSchemeColors = if (!useDarkTheme) LightDefaultColorScheme else DarkDefaultColorScheme + PreComposeApp { + MaterialTheme( + colorScheme = colorSchemeColors, + typography = androidMakersTypography(), + content = content, + ) + } - MaterialTheme( - colorScheme = colorSchemeColors, - typography = androidMakersTypography(), - content = content, - ) } diff --git a/shared/ui/src/commonMain/moko-resources/fr/strings.xml b/shared/ui/src/commonMain/moko-resources/fr/strings.xml index 57d97369..4a74bb18 100644 --- a/shared/ui/src/commonMain/moko-resources/fr/strings.xml +++ b/shared/ui/src/commonMain/moko-resources/fr/strings.xml @@ -2,7 +2,7 @@ Android Makers by droidcon - Version %s (%d) + Version %s (%s) Agenda Lieu diff --git a/shared/ui/src/iosMain/kotlin/com/androidmakers/ui/speakers/SpeakerListViewController.kt b/shared/ui/src/iosMain/kotlin/com/androidmakers/ui/speakers/SpeakerListViewController.kt index 4a0f4aa4..19f156b9 100644 --- a/shared/ui/src/iosMain/kotlin/com/androidmakers/ui/speakers/SpeakerListViewController.kt +++ b/shared/ui/src/iosMain/kotlin/com/androidmakers/ui/speakers/SpeakerListViewController.kt @@ -1,25 +1,18 @@ package com.androidmakers.ui.speakers -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue import androidx.compose.ui.window.ComposeUIViewController -import at.asitplus.KmmResult -import com.androidmakers.ui.model.toLce import com.androidmakers.ui.theme.AndroidMakersTheme -import fr.androidmakers.di.DepContainer +import moe.tlaster.precompose.koin.koinViewModel import platform.UIKit.UIViewController -fun SpeakerListViewController(): UIViewController = +fun SpeakerListViewController(onSpeakerClick: (String) -> Unit): UIViewController = ComposeUIViewController { - val depContainter = DepContainer() - val speakersRepository = depContainter.speakersRepository - val speakers by speakersRepository.getSpeakers().collectAsState(KmmResult.success(emptyList())) - val content = speakers.toLce() AndroidMakersTheme { + val viewModel = koinViewModel(vmClass = SpeakerListViewModel::class) SpeakerScreen( - content = content, - navigateToSpeakerDetails = {} + viewModel = viewModel, + navigateToSpeakerDetails = onSpeakerClick ) } } diff --git a/shared/ui/src/iosMain/kotlin/com/androidmakers/ui/sponsors/SponsorViewController.kt b/shared/ui/src/iosMain/kotlin/com/androidmakers/ui/sponsors/SponsorViewController.kt index 1f47852c..2ff7275e 100644 --- a/shared/ui/src/iosMain/kotlin/com/androidmakers/ui/sponsors/SponsorViewController.kt +++ b/shared/ui/src/iosMain/kotlin/com/androidmakers/ui/sponsors/SponsorViewController.kt @@ -13,13 +13,8 @@ import platform.UIKit.UIViewController fun SponsorViewController(): UIViewController = ComposeUIViewController { val depContainter = DepContainer() - val getPartnerUseCase = depContainter.getPartnersUseCase - val partners by getPartnerUseCase().collectAsState(KmmResult.success(emptyList())) - - val partnerlist = partners.getOrNull() ?: emptyList() - AndroidMakersTheme { - SponsorsScreen(Lce.Content(partnerlist)) { partner -> + SponsorsScreen { partner -> depContainter.openPartnerLinkUseCase(partner) } }