What is compose multiplatform: Compose Multiplatform is a declarative framework for sharing UIs across multiple platforms with Kotlin. It is based on Jetpack Compose and developed by JetBrains and open-source contributors.
You can choose the platforms across which to share your UIs using Compose Multiplatform:
Movie App (Home, Movie Detail ) Let’s start making a simple movie app step by step
Step-1: Download compose starter multiplatform template as shown in figure
Step-2 Rename package name as kmm-movie according to the given project structure
Project structure Step-3 Add given gradle dependency in Project-Kmm-movie build.gradle.kts
plugins { kotlin("multiplatform" ).apply(false ) id("com.android.application" ).apply(false ) id("com.android.library" ).apply(false ) id("org.jetbrains.compose" ).apply(false ) } Add given gradle dependency to Module:androidApp build.gradle.kts
plugins { kotlin ("multiplatform" ) id ("com.android.application" ) id ("org.jetbrains.compose" ) } kotlin { android () sourceSets { val androidMain by getting { dependencies { implementation (project (":shared" )) } } } } android { compileSdk = (findProperty ("android.compileSdk" ) as String ).toInt () namespace = "com.kmm_movie" sourceSets["main" ].manifest .srcFile ("src/androidMain/AndroidManifest.xml" ) defaultConfig { applicationId = "com.kmm_movie.KmmMovie" minSdk = (findProperty ("android.minSdk" ) as String ).toInt () targetSdk = (findProperty ("android.targetSdk" ) as String ).toInt () versionCode = 1 versionName = "1.0" } compileOptions { sourceCompatibility = JavaVersion .VERSION_11 targetCompatibility = JavaVersion .VERSION_11 } kotlin { jvmToolchain (11 ) } } Add given gradle dependency to Module:Shared build.gradle.kts
plugins { kotlin("multiplatform" ) kotlin("native.cocoapods" ) kotlin("plugin.serialization" ) id("com.android.library" ) id("org.jetbrains.compose" ) } kotlin { android() iosX64() iosArm64() iosSimulatorArm64() cocoapods { version = "1.0.0" summary = "Some description for the Shared Module" homepage = "Link to the Shared Module homepage" ios.deploymentTarget = "14.1" podfile = project.file("../iosApp/Podfile" ) framework { baseName = "shared" isStatic = true } extraSpecAttributes["resources" ] = "['src/commonMain/resources/**', 'src/iosMain/resources/**']" } sourceSets { val commonMain by getting { dependencies { implementation(compose.runtime) implementation(compose.foundation) implementation(compose.animation) implementation(compose.material) @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class) implementation(compose.components.resources) api(compose.materialIconsExtended) implementation("io.ktor:ktor-client-core:2.3.0" ) implementation("io.ktor:ktor-client-logging:2.3.0" ) implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.0" ) implementation("io.ktor:ktor-client-content-negotiation:2.3.0" ) implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1" ) api("io.github.qdsfdhvh:image-loader:1.4.4" ) api("moe.tlaster:precompose:1.4.1" ) api("moe.tlaster:precompose-viewmodel:1.4.1" ) } } val androidMain by getting { dependencies { api("androidx.activity:activity-compose:1.7.2" ) api("androidx.appcompat:appcompat:1.6.1" ) api("androidx.core:core-ktx:1.10.1" ) implementation("io.ktor:ktor-client-okhttp:2.3.0" ) } } val iosX64Main by getting val iosArm64Main by getting val iosSimulatorArm64Main by getting val iosMain by creating { dependsOn(commonMain) iosX64Main.dependsOn(this ) iosArm64Main.dependsOn(this ) iosSimulatorArm64Main.dependsOn(this ) dependencies { implementation("io.ktor:ktor-client-darwin:2.3.0" ) implementation("io.ktor:ktor-client-ios:2.3.0" ) } } } } android { compileSdk = (findProperty("android.compileSdk" ) as String).toInt() namespace = "com.kmm_movie.common" sourceSets["main" ].manifest.srcFile("src/androidMain/AndroidManifest.xml" ) sourceSets["main" ].res.srcDirs("src/androidMain/res" ) sourceSets["main" ].resources.srcDirs("src/commonMain/resources" ) defaultConfig { minSdk = (findProperty("android.minSdk" ) as String).toInt() targetSdk = (findProperty("android.targetSdk" ) as String).toInt() } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } kotlin { jvmToolchain(17 ) } } Add given dependency to Project Properties gradle.properties
org.gradle.jvmargs =-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M" kotlin.code.style =off icialkotlin.mpp.stability.nowarn =true kotlin.mpp.enableCInteropCommonization =true kotlin.mpp.androidSourceSetLayoutVersion =2 org.jetbrains.compose.experimental.uikit.enabled =true kotlin.native.cacheKind =noneandroid.useAndroidX =true android.compileSdk =33 android.targetSdk =33 android.minSdk =24 kotlin.version =1.8 .20 agp.version =7.4 .2 compose.version =1.4 .0 Add given dependency to settings.gradle.kts
rootProject.name = "Kmm-movie" include (":androidApp" )include (":shared" ) pluginManagement { repositories { gradlePluginPortal () maven ("https://maven.pkg.jetbrains.space/public/p/compose/dev" ) google () } plugins { val kotlinVersion = extra["kotlin.version" ] as String val agpVersion = extra["agp.version" ] as String val composeVersion = extra["compose.version" ] as String kotlin ("jvm" ).version (kotlinVersion) kotlin ("multiplatform" ).version (kotlinVersion) kotlin ("android" ).version (kotlinVersion) id ("com.android.application" ).version (agpVersion) id ("com.android.library" ).version (agpVersion) id ("org.jetbrains.compose" ).version (composeVersion) kotlin ("plugin.serialization" ).version (kotlinVersion) } } dependencyResolutionManagement { repositories { google () mavenCentral () maven ("https://maven.pkg.jetbrains.space/public/p/compose/dev" ) } } Step-4 Define ktor API client for HTTP api request inside shared → commonMain →kotlin → data → remote apiClient.kt
package data.remoteimport io.ktor.client.*import io.ktor.client.plugins.*import io.ktor.client.plugins.contentnegotiation.*import io.ktor.client.plugins.logging.DEFAULTimport io.ktor.client.plugins.logging.LogLevelimport io.ktor.client.plugins.logging.Loggerimport io.ktor.client.plugins.logging.Loggingimport io.ktor.http.takeFromimport io.ktor.serialization.kotlinx.json.*import kotlinx.serialization.json.Jsonimport utils.AppConstantval client = HttpClient { defaultRequest { url { takeFrom(AppConstant.BASE_URL) parameters.append("api_key" , AppConstant.API_KEY) } } expectSuccess = true install (HttpTimeout) { val timeout = 30000L connectTimeoutMillis = timeout requestTimeoutMillis = timeout socketTimeoutMillis = timeout } install(Logging) { logger = Logger.DEFAULT level = LogLevel.HEADERS } install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) } }Define API interface for api request inside shared → commonMain →kotlin → data → remote → ApiInterface.kt
package data .remoteimport data .model.BaseModelimport data .model.BaseModelV2import data .model.moviedetail.MovieDetailinterface ApiInterface { suspend fun nowPlayingMovieList ( page: Int ) : BaseModel suspend fun popularMovieList ( page: Int ) : BaseModelV2 suspend fun topRatedMovieList ( page: Int ) : BaseModelV2 suspend fun upcomingMovieList ( page: Int ) : BaseModel suspend fun movieDetail ( movieId: Int ) : MovieDetail suspend fun movieSearch ( searchKey: String ) : BaseModelV2 }Add API Implementation inside shared → commonMain → kotlin → data → remote → apiImpl.kt
package data .remoteimport data .model.BaseModelimport data .model.BaseModelV2import data .model.moviedetail.MovieDetailimport io.ktor.client.call.bodyimport io.ktor.client.request.HttpRequestBuilderimport io.ktor.client.request.get import io.ktor.http.encodedPathclass ApiImpl : ApiInterface { private fun HttpRequestBuilder.nowPlayingMovie ( page: Int ) { url { encodedPath = "3/movie/now_playing" parameters.append("page" , page.toString()) } } private fun HttpRequestBuilder.popularMovie ( page: Int ) { url { encodedPath = "3/movie/popular" parameters.append("page" , page.toString()) } } private fun HttpRequestBuilder.topRatedMovie ( page: Int ) { url { encodedPath = "3/movie/top_rated" parameters.append("page" , page.toString()) } } private fun HttpRequestBuilder.upcomingMovie ( page: Int , ) { url { encodedPath = "3/movie/upcoming" parameters.append("page" , page.toString()) } } private fun HttpRequestBuilder.movieDetail ( movieId: Int , ) { url { encodedPath = "3/movie/$movieId " } } private fun HttpRequestBuilder.movieSearch ( searchKey: String , ) { url { encodedPath = "3/search/movie" parameters.append("query" , searchKey) } } override suspend fun nowPlayingMovieList ( page: Int , ) : BaseModel { return client.get { nowPlayingMovie(page) }.body() } override suspend fun popularMovieList ( page: Int , ) : BaseModelV2 { return client.get { popularMovie(page) }.body() } override suspend fun topRatedMovieList ( page: Int , ) : BaseModelV2 { return client.get { topRatedMovie(page) }.body() } override suspend fun upcomingMovieList ( page: Int , ) : BaseModel { return client.get { upcomingMovie(page) }.body() } override suspend fun movieDetail (movieId: Int ) : MovieDetail { return client.get { movieDetail(movieId) }.body() } override suspend fun movieSearch (searchKey: String ) : BaseModelV2 { return client.get { movieSearch(searchKey) }.body() } }Add repository for api implementation inside shared → commonMain →kotlin → data → repository → movieRepository.kt
package data .repositoryimport data .remote.ApiImplimport kotlinx.coroutines.flow.flowimport utils.network.DataStateclass MovieRepository { private val api = ApiImpl() fun nowPlayingMovie (page: Int ) = flow { emit(DataState.Loading) try { val result = api.nowPlayingMovieList(page) emit(DataState.Success(result.results)) } catch (e: Exception) { emit(DataState.Error(e)) } } fun popularMovie (page: Int ) = flow { emit(DataState.Loading) try { val result = api.popularMovieList(page) emit(DataState.Success(result.results)) } catch (e: Exception) { emit(DataState.Error(e)) } } fun topRatedMovie (page: Int ) = flow { emit(DataState.Loading) try { val result = api.topRatedMovieList(page) emit(DataState.Success(result.results)) } catch (e: Exception) { emit(DataState.Error(e)) } } fun upComingMovie (page: Int ) = flow { emit(DataState.Loading) try { val result = api.upcomingMovieList(page) emit(DataState.Success(result.results)) } catch (e: Exception) { emit(DataState.Error(e)) } } fun movieDetail (movieId: Int ) = flow { emit(DataState.Loading) try { val result = api.movieDetail(movieId) emit(DataState.Success(result)) } catch (e: Exception) { emit(DataState.Error(e)) } } fun searchMovie (searchKey: String ) = flow { emit(DataState.Loading) try { val result = api.movieSearch(searchKey) emit(DataState.Success(result)) } catch (e: Exception) { emit(DataState.Error(e)) } } }Step-5 Let’s define a navigation graph for screen navigation inside shared → commonMain → kotlin → navigation → NavGraph.kt
package navigationimport androidx.compose.foundation.clickableimport androidx.compose.runtime.Composableimport androidx.compose.runtime.collectAsStateimport androidx.compose.ui.Modifierimport moe.tlaster.precompose.navigation.NavHostimport moe.tlaster.precompose.navigation.Navigatorimport moe.tlaster.precompose.navigation.pathimport ui.popular.Popularimport ui.detail.MovieDetailimport ui.home.HomeScreenimport ui.toprated.TopRatedimport ui.upcoming.Upcoming@Composable fun Navigation (navigator: Navigator ) { NavHost( navigator = navigator, initialRoute = NavigationScreen.Home.route, ) { scene(route = NavigationScreen.Home.route) { HomeScreen(navigator) } scene(route = NavigationScreen.Popular.route) { Popular(navigator) } scene(route = NavigationScreen.TopRated.route) { TopRated(navigator) } scene(route = NavigationScreen.Upcoming.route) { Upcoming(navigator) } scene(route = NavigationScreen.MovieDetail.route.plus(NavigationScreen.MovieDetail.objectPath)) { backStackEntry -> val id: Int ? = backStackEntry.path<Int >(NavigationScreen.MovieDetail.objectName) id?.let { MovieDetail(navigator, it) } } } }@Composable fun currentRoute (navigator: Navigator ) : String? { return navigator.currentEntry.collectAsState(null ).value?.route?.route }Define navigation and bottomNavigation for each screen inside shared → commonMain → kotlin → navigation → NavigationScreen.kt
package navigationimport androidx.compose.foundation.layout.offsetimport androidx.compose.foundation.layout.paddingimport androidx.compose.material.Iconimport androidx.compose.material.icons.Iconsimport androidx.compose.material.icons.filled.Homeimport androidx.compose.material.icons.filled.KeyboardArrowDownimport androidx.compose.material.icons.filled.Starimport androidx.compose.material.icons.filled.Timelineimport androidx.compose.runtime.Composableimport androidx.compose.ui.Modifierimport androidx.compose.ui.unit.dpimport utils.AppStringsealed class NavigationScreen ( val route: String, val title: String = AppString.APP_TITLE, val navIcon: (@Composable () -> Unit ) = { Icon( Icons.Filled.Home, contentDescription = "home" ) }, val objectName: String = "" , val objectPath: String = "" ) { object Home : NavigationScreen("home_screen" ) object Popular : NavigationScreen("popular_screen" ) object TopRated : NavigationScreen("top_rated_screen" ) object Upcoming : NavigationScreen("upcoming_screen" ) object MovieDetail : NavigationScreen("movie_detail_screen" , objectName = "id" , objectPath = "/{id}" ) object HomeNav : NavigationScreen("home_screen" , title = "Home" , navIcon = { Icon( Icons.Filled.Home, contentDescription = "search" , modifier = Modifier .padding(end = 16. dp) .offset(x = 10. dp) ) }) object PopularNav : NavigationScreen("popular_screen" , title = "Popular" , navIcon = { Icon( Icons.Filled.Timeline, contentDescription = "search" , modifier = Modifier .padding(end = 16. dp) .offset(x = 10. dp) ) }) object TopRatedNav : NavigationScreen("top_rated_screen" , title = "Top rated" , navIcon = { Icon( Icons.Filled.Star, contentDescription = "search" , modifier = Modifier .padding(end = 16. dp) .offset(x = 10. dp) ) }) object UpcomingNav : NavigationScreen("upcoming_screen" , title = "Upcoming" , navIcon = { Icon( Icons.Filled.KeyboardArrowDown, contentDescription = "search" , modifier = Modifier .padding(end = 16. dp) .offset(x = 10. dp) ) }) }Step-6 Lets start with UI component inside shared → commonMain → kotlin → ui → component → text → BiograpyText.kt
package ui.component.textimport androidx.compose.material.MaterialThemeimport androidx.compose.material.Textimport androidx.compose.runtime.Composableimport theme.bioGrapyText@Composable fun BioGraphyText (text:String ) { Text( text = text, style = MaterialTheme.typography.bioGrapyText ) }shared → commonMain → kotlin → ui → component → text → SubTitlePrimary.kt
package ui.component.textimport androidx.compose.material.MaterialThemeimport androidx.compose.material.Textimport androidx.compose.runtime.Composableimport theme.subTitlePrimary@Composable fun SubtitlePrimary (text: String ) { Text( text = text, style = MaterialTheme.typography.subTitlePrimary ) }shared → commonMain → kotlin → ui → component → text → SubTitle.kt
package ui.component.textimport androidx.compose.material.MaterialThemeimport androidx.compose.material.Textimport androidx.compose.runtime.Composableimport theme.subTitleSecondary@Composable fun SubtitleSecondary (text:String ) { Text( text = text, style = MaterialTheme.typography.subTitleSecondary ) }shared → commonMain → kotlin → ui → component → AppBarWithArrow.kt
package ui.componentimport androidx.compose.foundation.Imageimport androidx.compose.foundation.clickableimport androidx.compose.foundation.layout.Rowimport androidx.compose.foundation.layout.Spacerimport androidx.compose.foundation.layout.heightimport androidx.compose.foundation.layout.paddingimport androidx.compose.foundation.layout.widthimport androidx.compose.material.MaterialThemeimport androidx.compose.material.Textimport androidx.compose.material.TopAppBarimport androidx.compose.material.icons.Iconsimport androidx.compose.material.icons.filled.ArrowBackimport androidx.compose.runtime.Composableimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.graphics.Colorimport androidx.compose.ui.graphics.ColorFilterimport androidx.compose.ui.unit.dpimport theme.Purple500@Composable fun AppBarWithArrow ( title: String ?, isBackEnable: Boolean = false , pressOnBack: () -> Unit ) { TopAppBar( elevation = 6. dp, backgroundColor = Purple500, modifier = Modifier.height(58. dp) ) { Row { Spacer(modifier = Modifier.width(10. dp)) if (isBackEnable) { Image( imageVector = Icons.Filled.ArrowBack, colorFilter = ColorFilter.tint(Color.White), contentDescription = null , modifier = Modifier .align(Alignment.CenterVertically) .clickable { pressOnBack() } ) } Spacer(modifier = Modifier.width(12. dp)) Text( modifier = Modifier .padding(8. dp) .align(Alignment.CenterVertically), text = title ?: "" , style = MaterialTheme.typography.h6, color = Color.White ) } } }shared → commonMain → kotlin → ui → component → MovieList.kt
package ui.componentimport androidx.compose.foundation.Imageimport androidx.compose.foundation.clickableimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.paddingimport androidx.compose.foundation.layout.sizeimport androidx.compose.foundation.lazy.grid.GridCellsimport androidx.compose.foundation.lazy.grid.LazyVerticalGridimport androidx.compose.foundation.lazy.grid.itemsimport androidx.compose.foundation.shape.RoundedCornerShapeimport androidx.compose.runtime.Composableimport androidx.compose.ui.Modifierimport androidx.compose.ui.layout.ContentScaleimport androidx.compose.ui.unit.dpimport com.seiko.imageloader.rememberAsyncImagePainterimport data .model.MovieItemimport utils.AppConstantimport utils.cornerRadius@Composable internal fun MovieList (listItems: List <MovieItem >, onclick: (id : Int ) -> Unit ) { LazyVerticalGrid(columns = GridCells.Fixed(2 ), modifier = Modifier.padding(start = 5. dp, end = 5. dp, top = 10. dp), content = { items(listItems) { Column( modifier = Modifier.padding( start = 5. dp, end = 5. dp, top = 0. dp, bottom = 10. dp ) ) { Image( painter = rememberAsyncImagePainter( AppConstant.IMAGE_URL.plus( it.poster_path ) ), contentDescription = it.poster_path, modifier = Modifier.size(250. dp).cornerRadius(10 ).shimmerBackground( RoundedCornerShape(5. dp) ).clickable { onclick(it.id) }, contentScale = ContentScale.Crop, ) } } }) }shared → commonMain → kotlin → ui → component → ProgressIndicator.kt
package ui.componentimport androidx.compose.foundation.layout.Boximport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.material.CircularProgressIndicatorimport androidx.compose.runtime.Composableimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifier@Composable internal fun ProgressIndicator (isVisible: Boolean = true ) { if (isVisible) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { CircularProgressIndicator() } } }shared → commonMain → kotlin → ui → component → SearchBar.kt
package ui.componentimport androidx.compose.foundation.Imageimport androidx.compose.foundation.backgroundimport androidx.compose.foundation.clickableimport androidx.compose.foundation.layout.Rowimport androidx.compose.foundation.layout.Spacerimport androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.foundation.layout.offsetimport androidx.compose.foundation.layout.paddingimport androidx.compose.foundation.layout.widthimport androidx.compose.material.Iconimport androidx.compose.material.TextFieldimport androidx.compose.material.TextFieldDefaultsimport androidx.compose.material.icons.Iconsimport androidx.compose.material.icons.filled.ArrowBackimport androidx.compose.material.icons.filled.Clearimport androidx.compose.material.icons.filled.Searchimport androidx.compose.runtime.*import androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.focus.FocusRequesterimport androidx.compose.ui.focus.focusRequesterimport androidx.compose.ui.graphics.Colorimport androidx.compose.ui.graphics.ColorFilterimport androidx.compose.ui.unit.dpimport kotlinx.coroutines.ExperimentalCoroutinesApiimport theme.Blueimport ui.AppViewModel@ExperimentalCoroutinesApi @Composable fun SearchBar (viewModel: AppViewModel , pressOnBack: () -> Unit ) { var text by remember { mutableStateOf("" ) } val focusRequester = FocusRequester() Row(Modifier.background(color = Blue)) { Spacer(modifier = Modifier.width(10. dp)) Image( imageVector = Icons.Filled.ArrowBack, colorFilter = ColorFilter.tint(Color.White), contentDescription = null , modifier = Modifier .align(Alignment.CenterVertically) .clickable { pressOnBack() } ) Spacer(modifier = Modifier.width(5. dp)) TextField(modifier = Modifier.fillMaxWidth().focusRequester(focusRequester), value = text, colors = TextFieldDefaults.textFieldColors( backgroundColor = Blue, cursorColor = Color.Black, disabledLabelColor = Blue, focusedIndicatorColor = Color.Transparent, unfocusedIndicatorColor = Color.Transparent ), onValueChange = { text = it viewModel.searchApi(it) }, singleLine = true , trailingIcon = { if (text.trim().isNotEmpty()) { Icon(Icons.Filled.Clear, contentDescription = "clear text" , modifier = Modifier.padding(end = 16. dp).offset(x = 10. dp).clickable { text = "" }) } else { Icon(Icons.Filled.Search, contentDescription = "search" , modifier = Modifier.padding(end = 16. dp).offset(x = 10. dp).clickable { }) } }) LaunchedEffect(Unit ) { focusRequester.requestFocus() } } }shared → commonMain → kotlin → ui → component → SearchUI.kt
package ui.componentimport androidx.compose.foundation.Imageimport androidx.compose.foundation.backgroundimport androidx.compose.foundation.clickableimport androidx.compose.foundation.layout.*import androidx.compose.foundation.lazy.LazyColumnimport androidx.compose.foundation.lazy.itemsimport androidx.compose.foundation.shape.RoundedCornerShapeimport androidx.compose.material.Textimport androidx.compose.runtime.Composableimport androidx.compose.runtime.MutableStateimport androidx.compose.ui.Modifierimport androidx.compose.ui.draw.clipimport androidx.compose.ui.layout.ContentScaleimport androidx.compose.ui.text.font.FontWeightimport androidx.compose.ui.unit.dpimport androidx.compose.ui.unit.spimport com.seiko.imageloader.rememberAsyncImagePainterimport data .model.BaseModelV2import moe.tlaster.precompose.navigation.Navigatorimport navigation.NavigationScreenimport theme.DefaultBackgroundColorimport theme.FontColorimport theme.SecondaryFontColorimport utils.AppConstantimport utils.AppStringimport utils.cornerRadiusimport utils.network.DataStateimport utils.roundTo@Composable fun SearchUI ( navController: Navigator , searchData: MutableState <DataState <BaseModelV2 >?>, itemClick: () -> Unit ) { LazyColumn( modifier = Modifier .fillMaxWidth() .heightIn(0. dp, 350. dp) .clip(RoundedCornerShape(bottomStart = 15. dp, bottomEnd = 15. dp)) .background(color = DefaultBackgroundColor) .padding(top = 8. dp) ) { searchData.value?.let { if (it is DataState.Success<BaseModelV2>) { items(items = it.data .results, itemContent = { item -> Row(modifier = Modifier .padding(bottom = 8. dp, start = 8. dp, end = 8. dp) .clickable { itemClick.invoke() navController.navigate( NavigationScreen.MovieDetail.route.plus( "/${item.id} " ) ) }) { Image( painter = rememberAsyncImagePainter( AppConstant.IMAGE_URL.plus( item.backdrop_path ) ), contentDescription = item.backdrop_path, modifier = Modifier .height(100. dp) .width(80. dp).cornerRadius(8 ).shimmerBackground(RoundedCornerShape(5. dp)), contentScale = ContentScale.Crop, ) Column { Text( text = item.title, modifier = Modifier.padding( start = 8. dp, top = 4. dp ), fontWeight = FontWeight.SemiBold ) Text( text = item.release_date, color = FontColor, fontSize = 16. sp, modifier = Modifier.padding(start = 8. dp) ) Text( text = "${AppString.RATING_SEARCH} ${ item.vote_average.roundTo( 1 ) } " , color = SecondaryFontColor, fontSize = 12. sp, modifier = Modifier.padding(start = 8. dp) ) } } }) } } } }shared → commonMain → kotlin → ui → component → ShimmerBackground.kt
package ui.componentimport androidx.compose.animation.core.LinearOutSlowInEasingimport androidx.compose.animation.core.RepeatModeimport androidx.compose.animation.core.animateFloatimport androidx.compose.animation.core.infiniteRepeatableimport androidx.compose.animation.core.rememberInfiniteTransitionimport androidx.compose.animation.core.tweenimport androidx.compose.foundation.backgroundimport androidx.compose.runtime.getValueimport androidx.compose.ui.Modifierimport androidx.compose.ui.composedimport androidx.compose.ui.geometry.Offsetimport androidx.compose.ui.graphics.Brushimport androidx.compose.ui.graphics.Colorimport androidx.compose.ui.graphics.RectangleShapeimport androidx.compose.ui.graphics.Shapeimport androidx.compose.ui.graphics.TileModefun Modifier.shimmerBackground (shape: Shape = RectangleShape) : Modifier = composed { val transition = rememberInfiniteTransition() val translateAnimation by transition.animateFloat( initialValue = 0f , targetValue = 400f , animationSpec = infiniteRepeatable( tween(durationMillis = 1500 , easing = LinearOutSlowInEasing), RepeatMode.Restart ), ) val shimmerColors = listOf( Color.LightGray.copy(alpha = 0.9f ), Color.LightGray.copy(alpha = 0.4f ), ) val brush = Brush.linearGradient( colors = shimmerColors, start = Offset(translateAnimation, translateAnimation), end = Offset(translateAnimation + 100f , translateAnimation + 100f ), tileMode = TileMode.Mirror, ) return @composed this .then(background(brush, shape)) }Step-7 Now define all screens for bottom navigation as well as movie detail
shared → commonMain → kotlin → ui → home → HomeScreen.kt
package ui.homeimport androidx.compose.foundation.layout.*import androidx.compose.material.Textimport androidx.compose.runtime.*import androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport data .model.MovieItemimport moe.tlaster.precompose.navigation.Navigatorimport navigation.NavigationScreenimport ui.component.MovieListimport ui.component.ProgressIndicatorimport utils.AppStringimport utils.network.DataState@Composable fun HomeScreen ( navigator: Navigator , viewModel: NowPlayingViewModel = NowPlayingViewModel() ) { LaunchedEffect(true ) { viewModel.nowPlayingView(1 ) } Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { viewModel.nowPlayingResponse.collectAsState().value?.let { when (it) { is DataState.Loading -> { ProgressIndicator() } is DataState.Success<List<MovieItem>> -> { MovieList(it.data ) { movieId -> navigator.navigate(NavigationScreen.MovieDetail.route.plus("/$movieId " )) } } is DataState.Error -> { Text("${AppString.ERROR_TEXT} ${it.exception} " ) } } } } }shared → commonMain → kotlin → ui → home → NowPlayingViewModel.kt
package ui.homeimport data .model.MovieItemimport data .repository.MovieRepositoryimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.flow.MutableStateFlowimport kotlinx.coroutines.flow.collectLatestimport kotlinx.coroutines.launchimport moe.tlaster.precompose.viewmodel.ViewModelimport utils.network.DataStateclass NowPlayingViewModel : ViewModel () { private val viewModelScope = CoroutineScope(Dispatchers.Main) private val repo = MovieRepository() val nowPlayingResponse = MutableStateFlow<DataState<List<MovieItem>>?>(DataState.Loading) fun nowPlayingView (page: Int ) { viewModelScope.launch(Dispatchers.Main) { repo.nowPlayingMovie(page).collectLatest { nowPlayingResponse.value = it } } } }shared → commonMain → kotlin → ui → popular → Popular.kt
package ui.popularimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.material.Textimport androidx.compose.runtime.Composableimport androidx.compose.runtime.LaunchedEffectimport androidx.compose.runtime.collectAsStateimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport data .model.MovieItemimport moe.tlaster.precompose.navigation.Navigatorimport navigation.NavigationScreenimport ui.component.MovieListimport ui.component.ProgressIndicatorimport utils.AppStringimport utils.network.DataState@Composable fun Popular (navigator: Navigator , viewModel: PopularViewModel = PopularViewModel() ) { LaunchedEffect(true ) { viewModel.nowPlayingView(1 ) } Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { viewModel.popularMovieResponse.collectAsState().value?.let { when (it) { is DataState.Loading -> { ProgressIndicator() } is DataState.Success<List<MovieItem>> -> { MovieList(it.data ) { movieId -> navigator.navigate(NavigationScreen.MovieDetail.route.plus("/$movieId " )) } } is DataState.Error ->{ Text("${AppString.ERROR_TEXT} ${it.exception} " ) } } } } }shared → commonMain → kotlin → ui → popular → PopularViewModel.kt
package ui.popularimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.material.Textimport androidx.compose.runtime.Composableimport androidx.compose.runtime.LaunchedEffectimport androidx.compose.runtime.collectAsStateimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport data .model.MovieItemimport moe.tlaster.precompose.navigation.Navigatorimport navigation.NavigationScreenimport ui.component.MovieListimport ui.component.ProgressIndicatorimport utils.AppStringimport utils.network.DataState@Composable fun Popular (navigator: Navigator , viewModel: PopularViewModel = PopularViewModel() ) { LaunchedEffect(true ) { viewModel.nowPlayingView(1 ) } Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { viewModel.popularMovieResponse.collectAsState().value?.let { when (it) { is DataState.Loading -> { ProgressIndicator() } is DataState.Success<List<MovieItem>> -> { MovieList(it.data ) { movieId -> navigator.navigate(NavigationScreen.MovieDetail.route.plus("/$movieId " )) } } is DataState.Error ->{ Text("${AppString.ERROR_TEXT} ${it.exception} " ) } } } } }shared → commonMain → kotlin → ui → toprated → TopRated.kt
package ui.topratedimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.material.Textimport androidx.compose.runtime.Composableimport androidx.compose.runtime.LaunchedEffectimport androidx.compose.runtime.collectAsStateimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport data .model.MovieItemimport moe.tlaster.precompose.navigation.Navigatorimport navigation.NavigationScreenimport ui.component.MovieListimport ui.component.ProgressIndicatorimport utils.AppStringimport utils.network.DataState@Composable fun TopRated (navigator: Navigator , viewModel: TopRatedViewModel = TopRatedViewModel() ) { LaunchedEffect(true ) { viewModel.nowPlayingView(1 ) } Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { viewModel.topRatedMovieResponse.collectAsState().value?.let { when (it) { is DataState.Loading -> { ProgressIndicator() } is DataState.Success<List<MovieItem>> -> { MovieList(it.data ) { movieId -> navigator.navigate(NavigationScreen.MovieDetail.route.plus("/$movieId " )) } } is DataState.Error -> { Text("${AppString.ERROR_TEXT} ${it.exception} " ) } } } } }shared → commonMain → kotlin → ui → toprated → TopRatedViewModel.kt
package ui.topratedimport data .model.MovieItemimport data .repository.MovieRepositoryimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.flow.MutableStateFlowimport kotlinx.coroutines.flow.collectLatestimport kotlinx.coroutines.launchimport moe.tlaster.precompose.viewmodel.ViewModelimport utils.network.DataStateclass TopRatedViewModel : ViewModel () { private val viewModelScope = CoroutineScope(Dispatchers.Main) private val repo = MovieRepository() val topRatedMovieResponse = MutableStateFlow<DataState<List<MovieItem>>?>(DataState.Loading) fun nowPlayingView (page: Int ) { viewModelScope.launch(Dispatchers.Main) { repo.topRatedMovie(page).collectLatest { topRatedMovieResponse.value = it } } } }shared → commonMain → kotlin → ui → upcoming → Upcoming.kt
package ui.upcomingimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.material.Textimport androidx.compose.runtime.Composableimport androidx.compose.runtime.LaunchedEffectimport androidx.compose.runtime.collectAsStateimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport data .model.MovieItemimport moe.tlaster.precompose.navigation.Navigatorimport navigation.NavigationScreenimport ui.component.MovieListimport ui.component.ProgressIndicatorimport utils.AppStringimport utils.network.DataState@Composable fun Upcoming (navigator: Navigator , viewModel: UpcomingViewModel = UpcomingViewModel() ) { LaunchedEffect(true ) { viewModel.nowPlayingView(1 ) } Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { viewModel.upComingMovieResponse.collectAsState().value?.let { when (it) { is DataState.Loading -> { ProgressIndicator() } is DataState.Success<List<MovieItem>> -> { MovieList(it.data ) { movieId -> navigator.navigate(NavigationScreen.MovieDetail.route.plus("/$movieId " )) } } is DataState.Error -> { Text("${AppString.ERROR_TEXT} ${it.exception} " ) } } } } }shared → commonMain → kotlin → ui → upcoming → UpcomingViewModel.kt
package ui.upcomingimport data .model.MovieItemimport data .repository.MovieRepositoryimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.flow.MutableStateFlowimport kotlinx.coroutines.flow.collectLatestimport kotlinx.coroutines.launchimport moe.tlaster.precompose.viewmodel.ViewModelimport utils.network.DataStateclass UpcomingViewModel : ViewModel () { private val viewModelScope = CoroutineScope(Dispatchers.Main) private val repo = MovieRepository() val upComingMovieResponse = MutableStateFlow<DataState<List<MovieItem>>?>(DataState.Loading) fun nowPlayingView (page: Int ) { viewModelScope.launch(Dispatchers.Main) { repo.upComingMovie(page).collectLatest { upComingMovieResponse.value = it } } } }shared → commonMain → kotlin → ui → detail → MovieDetail.kt
package ui.detailimport androidx.compose.foundation.*import androidx.compose.foundation.layout.*import androidx.compose.foundation.shape.RoundedCornerShapeimport androidx.compose.material.Textimport androidx.compose.runtime.*import androidx.compose.ui.Modifierimport androidx.compose.ui.layout.ContentScaleimport androidx.compose.ui.text.font.FontWeightimport androidx.compose.ui.text.style.TextOverflowimport androidx.compose.ui.unit.dpimport androidx.compose.ui.unit.spimport ui.component.text.SubtitlePrimaryimport ui.component.text.SubtitleSecondaryimport com.seiko.imageloader.rememberAsyncImagePainterimport data .model.moviedetail.MovieDetailimport moe.tlaster.precompose.navigation.Navigatorimport theme.DefaultBackgroundColorimport theme.FontColorimport ui.component.ProgressIndicatorimport ui.component.shimmerBackgroundimport utils.AppConstantimport utils.AppStringimport utils.hourMinutesimport utils.network.DataStateimport utils.roundTo@Composable fun MovieDetail ( navigator: Navigator , movieId: Int , movieDetailViewModel: MovieDetailViewModel = MovieDetailViewModel() ) { LaunchedEffect(true ) { movieDetailViewModel.movieDetail(movieId) } Column( modifier = Modifier .fillMaxSize() .background( DefaultBackgroundColor ) ) { movieDetailViewModel.movieDetail.collectAsState().value.let { when (it) { is DataState.Loading -> { ProgressIndicator() } is DataState.Success<MovieDetail> -> { Column(modifier = Modifier.verticalScroll(rememberScrollState())) { Image( painter = rememberAsyncImagePainter( AppConstant.IMAGE_URL.plus( it.data .poster_path ) ), contentDescription = it.data .poster_path, modifier = Modifier .fillMaxWidth() .height(300. dp).shimmerBackground( RoundedCornerShape(5. dp) ), contentScale = ContentScale.Crop, ) Column( modifier = Modifier .fillMaxSize() .padding(start = 10. dp, end = 10. dp) ) { Text( text = it.data .title, modifier = Modifier.padding(top = 10. dp), color = FontColor, fontSize = 30. sp, fontWeight = FontWeight.W700, maxLines = 1 , overflow = TextOverflow.Ellipsis ) Row( modifier = Modifier .fillMaxWidth() .padding(bottom = 10. dp, top = 10. dp) ) { Column(Modifier.weight(1f )) { SubtitlePrimary( text = it.data .original_language, ) SubtitleSecondary( text = AppString.LANGUAGE ) } Column(Modifier.weight(1f )) { SubtitlePrimary( text = it.data .vote_average.roundTo(1 ).toString(), ) SubtitleSecondary( text = AppString.RATING ) } Column(Modifier.weight(1f )) { SubtitlePrimary( text = it.data .runtime.hourMinutes() ) SubtitleSecondary( text = AppString.DURATION ) } Column(Modifier.weight(1f )) { SubtitlePrimary( text = it.data .release_date ) SubtitleSecondary( text = AppString.RELEASE_DATE ) } } Text( text = AppString.DESCRIPTION, color = FontColor, fontSize = 17. sp, fontWeight = FontWeight.SemiBold ) Text(text = it.data .overview) } } } is DataState.Error -> { Text("Error :${it.exception} " ) } else -> { } } } } }shared → commonMain → kotlin → ui → detail → MovieDetailViewModel.kt
package ui.detailimport data .model.moviedetail.MovieDetailimport data .repository.MovieRepositoryimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.flow.MutableStateFlowimport kotlinx.coroutines.flow.collectLatestimport kotlinx.coroutines.launchimport moe.tlaster.precompose.viewmodel.ViewModelimport utils.network.DataStateclass MovieDetailViewModel : ViewModel () { private val viewModelScope = CoroutineScope(Dispatchers.Main) private val repo = MovieRepository() val movieDetail = MutableStateFlow<DataState<MovieDetail>?>(DataState.Loading) fun movieDetail (movieId: Int ) { viewModelScope.launch(Dispatchers.Main) { repo.movieDetail(movieId).collectLatest { movieDetail.value = it } } } }shared → commonMain → kotlin → ui → AppViewModel.kt
package uiimport androidx.compose.runtime.MutableStateimport androidx.compose.runtime.mutableStateOfimport data .model.BaseModelV2import data .repository.MovieRepositoryimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.ExperimentalCoroutinesApiimport kotlinx.coroutines.FlowPreviewimport kotlinx.coroutines.flow.debounceimport kotlinx.coroutines.flow.distinctUntilChangedimport kotlinx.coroutines.flow.filterimport kotlinx.coroutines.flow.flatMapLatestimport kotlinx.coroutines.flow.flowOfimport kotlinx.coroutines.launchimport moe.tlaster.precompose.viewmodel.ViewModelimport utils.network.DataState@ExperimentalCoroutinesApi class AppViewModel : ViewModel () { private val viewModelScope = CoroutineScope(Dispatchers.Main) private val repo = MovieRepository() val searchData: MutableState<DataState<BaseModelV2>?> = mutableStateOf(null ) @ExperimentalCoroutinesApi @FlowPreview fun searchApi (searchKey: String ) { viewModelScope.launch { flowOf(searchKey).debounce(300 ) .filter { it.trim().isEmpty().not() } .distinctUntilChanged() .flatMapLatest { repo.searchMovie(it) }.collect { if (it is DataState.Success){ it.data } searchData.value = it } } } }Step-8 Now define the main component of the app
shared → commonMain → kotlin → App.kt
import androidx.compose.foundation.layout.Columnimport androidx.compose.material.BottomNavigationimport androidx.compose.material.BottomNavigationItemimport androidx.compose.material.FloatingActionButtonimport androidx.compose.material.Iconimport androidx.compose.material.MaterialThemeimport androidx.compose.material.Scaffoldimport androidx.compose.material.Textimport androidx.compose.material.icons.Iconsimport androidx.compose.material.icons.filled.Searchimport androidx.compose.runtime.*import androidx.compose.ui.graphics.Colorimport kotlinx.coroutines.ExperimentalCoroutinesApiimport moe.tlaster.precompose.navigation.BackHandlerimport moe.tlaster.precompose.navigation.NavOptionsimport moe.tlaster.precompose.navigation.Navigatorimport moe.tlaster.precompose.navigation.rememberNavigatorimport navigation.Navigationimport navigation.NavigationScreenimport navigation.currentRouteimport theme.FloatingActionBackgroundimport ui.AppViewModelimport ui.component.AppBarWithArrowimport ui.component.ProgressIndicatorimport ui.component.SearchBarimport ui.component.SearchUIimport utils.AppStringimport utils.pagingLoadingState@OptIn(ExperimentalCoroutinesApi::class) @Composable internal fun App (viewModel: AppViewModel = AppViewModel() ) { val navigator = rememberNavigator() val isAppBarVisible = remember { mutableStateOf(true ) } val searchProgressBar = remember { mutableStateOf(false ) } BackHandler(isAppBarVisible.value.not()) { isAppBarVisible.value = true } MaterialTheme { Scaffold(topBar = { if (isAppBarVisible.value.not()) { SearchBar(viewModel) { isAppBarVisible.value = true } } else { AppBarWithArrow( AppString.APP_TITLE, isBackEnable = isBackButtonEnable(navigator) ) { navigator.goBack() } } }, floatingActionButton = { when (currentRoute(navigator)) { NavigationScreen.Home.route, NavigationScreen.Popular.route, NavigationScreen.TopRated.route, NavigationScreen.Upcoming.route -> { FloatingActionButton( onClick = { isAppBarVisible.value = false }, backgroundColor = FloatingActionBackground ) { Icon(Icons.Filled.Search, "" , tint = Color.White) } } } }, bottomBar = { when (currentRoute(navigator)) { NavigationScreen.Home.route, NavigationScreen.Popular.route, NavigationScreen.TopRated.route, NavigationScreen.Upcoming.route -> { BottomNavigationUI(navigator) } } }) { Navigation(navigator) if (currentRoute(navigator) !== NavigationScreen.MovieDetail.route) { Column { if (isAppBarVisible.value.not()) { SearchUI(navigator, viewModel.searchData) { isAppBarVisible.value = true } ProgressIndicator(searchProgressBar.value) } viewModel.searchData.pagingLoadingState { searchProgressBar.value = it } } } } } }@Composable fun BottomNavigationUI (navigator: Navigator ) { BottomNavigation { val items = listOf( NavigationScreen.HomeNav, NavigationScreen.PopularNav, NavigationScreen.TopRatedNav, NavigationScreen.UpcomingNav, ) items.forEach { BottomNavigationItem(label = { Text(text = it.title) }, selected = it.route == currentRoute(navigator), icon = it.navIcon, onClick = { navigator.navigate( it.route, NavOptions( launchSingleTop = true , ), ) }) } } }@Composable fun isBackButtonEnable (navigator: Navigator ) : Boolean { return when (currentRoute(navigator)) { NavigationScreen.Home.route, NavigationScreen.Popular.route, NavigationScreen.TopRated.route, NavigationScreen.Upcoming.route -> { false } else -> { true } } }Step-9: Let’s define utility functions in utils
shared → commonMain → kotlin → utils → network → DataState.kt
package utils.networksealed class DataState <out R > { data class Success <out T >(val data: T ) : DataState <T >() data class Error (val exception: Exception ) : DataState <Nothing >() object Loading : DataState <Nothing >() } shared → commonMain → kotlin → utils → AppConstant.kt
package utilsobject AppConstant { const val API_KEY = "59cd6896d8432f9c69aed9b86b9c2931" const val BASE_URL = "https://api.themoviedb.org/" const val IMAGE_URL = "https://image.tmdb.org/t/p/w342" }shared → commonMain → kotlin → utils → AppString.kt
package utilsobject AppString { const val APP_TITLE = "Movie World" const val LANGUAGE = "Language" const val RATING = "Rating" const val DURATION = "Duration" const val RELEASE_DATE = "Release Date" const val DESCRIPTION = "Description" const val RATING_SEARCH = "Rating :" const val ERROR_TEXT = "Error :" }shared → commonMain → kotlin → utils → CommonExtension.kt
package utilsimport kotlin.math.powimport kotlin.math.roundToIntimport kotlin.time.Duration.Companion.minutesfun Int .hourMinutes () : String { return "${this.minutes.inWholeHours} h ${this % 60 } m" }fun Int .genderInString () : String { return when (this ) { 1 -> "Female" 2 -> "Male" else -> "" } }fun Double .roundTo (numFractionDigits: Int ) : Double { val factor = 10.0 .pow(numFractionDigits.toDouble()) return (this * factor).roundToInt() / factor }shared → commonMain → kotlin → utils → UIExtension.kt
package utilsimport androidx.compose.foundation.shape.RoundedCornerShapeimport androidx.compose.runtime.MutableStateimport androidx.compose.ui.Modifierimport androidx.compose.ui.graphics.graphicsLayerimport androidx.compose.ui.unit.dpimport utils.network.DataStatefun Modifier.cornerRadius (radius: Int ) = graphicsLayer(shape = RoundedCornerShape(radius.dp), clip = true )fun <T : Any> MutableState<DataState<T> ?>.pagingLoadingState (isLoaded: (pagingState : Boolean ) -> Unit ) { when (this .value) { is DataState.Success<T> -> { isLoaded(false ) } is DataState.Loading -> { isLoaded(true ) } is DataState.Error -> { isLoaded(false ) } else -> { isLoaded(false ) } } }Step-10 Let’s add theme for the app.
shared → commonMain → kotlin → theme → Color.kt
package themeimport androidx.compose.ui.graphics.Colorval Purple200 = Color(0xFFBB86FC )val Purple500 = Color(0xFF6200EE )val Purple700 = Color(0xFF3700B3 )val Teal200 = Color(0xFF03DAC5 )val FontColor = Color(0xFF212121 )val SecondaryFontColor = Color(0xFF757575 )val DefaultBackgroundColor = Color(0xFFFAFAFA )val Blue = Color(0xff76a9ff )val FloatingActionBackground = Color(0xffFBC02D )val LinkColor = Color(0xff64B5F6 )shared → commonMain → kotlin → theme → Shape.kt
package themeimport androidx.compose.foundation.shape.RoundedCornerShapeimport androidx.compose.material.Shapesimport androidx.compose.ui.Modifierimport androidx.compose.ui.graphics.graphicsLayerimport androidx.compose.ui.unit.dpval Shapes = Shapes( small = RoundedCornerShape(4. dp), medium = RoundedCornerShape(4. dp), large = RoundedCornerShape(0. dp) )fun Modifier.cornerRadius (radius: Int ) = graphicsLayer(shape = RoundedCornerShape(radius.dp), clip = true )shared → commonMain → kotlin → theme → Theme.kt
package themeimport androidx.compose.foundation.isSystemInDarkThemeimport androidx.compose.material.MaterialThemeimport androidx.compose.material.darkColorsimport androidx.compose.material.lightColorsimport androidx.compose.runtime.Composableprivate val DarkColorPalette = darkColors( primary = Purple200, primaryVariant = Purple700, secondary = Teal200 )private val LightColorPalette = lightColors( primary = Purple500, primaryVariant = Purple700, secondary = Teal200 )@Composable fun HiltMVVMComposeMovieTheme ( darkTheme: Boolean = isSystemInDarkTheme() , content: @Composable() () -> Unit ) { val colors = if (darkTheme) { DarkColorPalette } else { LightColorPalette } MaterialTheme( colors = colors, typography = Typography, shapes = Shapes, content = content ) }shared → commonMain → kotlin → theme → Theme.kt
package themeimport androidx.compose.material.Typographyimport androidx.compose.runtime.Composableimport androidx.compose.ui.text.TextStyleimport androidx.compose.ui.text.font.FontFamilyimport androidx.compose.ui.text.font.FontWeightimport androidx.compose.ui.unit.spval Typography = Typography( body1 = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, fontSize = 16. sp ), button = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.W500, fontSize = 14. sp ), caption = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, fontSize = 12. sp ) )val Typography.subTitlePrimary: TextStyle @Composable get () { return TextStyle( fontFamily = FontFamily.Default, color = FontColor, fontSize = 14. sp, fontWeight = FontWeight.Medium ) }val Typography.subTitleSecondary: TextStyle @Composable get () { return TextStyle( fontFamily = FontFamily.Default, color = SecondaryFontColor, fontSize = 10. sp, ) }val Typography.bioGrapyText: TextStyle @Composable get () { return TextStyle( fontFamily = FontFamily.Default, color = SecondaryFontColor, fontSize = 14. sp, ) }Step-11 Before running don't forget to add data classes inside this package shared → commonMain → kotlin → data → model, from GitHub reference. Now run the app and app home screen and movie detail should be following the screenshot
Github ref:https://github.com/piashcse/kmm-movie Medium Ref: https://piashcse.medium.com/compose-multiplatform-movie-app-4752cd445e95