Compose Multiplatform Movie App
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:
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
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
#Gradle
org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M"
#Kotlin
kotlin.code.style=official
#MPP
kotlin.mpp.stability.nowarn=true
kotlin.mpp.enableCInteropCommonization=true
kotlin.mpp.androidSourceSetLayoutVersion=2
#Compose
org.jetbrains.compose.experimental.uikit.enabled=true
kotlin.native.cacheKind=none
#Android
android.useAndroidX=true
android.compileSdk=33
android.targetSdk=33
android.minSdk=24
#Versions
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.remote
import io.ktor.client.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.logging.DEFAULT
import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logger
import io.ktor.client.plugins.logging.Logging
import io.ktor.http.takeFrom
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
import utils.AppConstant
val 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.remote
import data.model.BaseModel
import data.model.BaseModelV2
import data.model.moviedetail.MovieDetail
interface 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.remote
import data.model.BaseModel
import data.model.BaseModelV2
import data.model.moviedetail.MovieDetail
import io.ktor.client.call.body
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.get
import io.ktor.http.encodedPath
class 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.repository
import data.remote.ApiImpl
import kotlinx.coroutines.flow.flow
import utils.network.DataState
class 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 navigation
import androidx.compose.foundation.clickable
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import moe.tlaster.precompose.navigation.NavHost
import moe.tlaster.precompose.navigation.Navigator
import moe.tlaster.precompose.navigation.path
import ui.popular.Popular
import ui.detail.MovieDetail
import ui.home.HomeScreen
import ui.toprated.TopRated
import 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 navigation
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Icon
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.Star
import androidx.compose.material.icons.filled.Timeline
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import utils.AppString
sealed 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.text
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import 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.text
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import 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.text
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import theme.subTitleSecondary
@Composable
fun SubtitleSecondary(text:String) {
Text(
text = text,
style = MaterialTheme.typography.subTitleSecondary
)
}
shared → commonMain → kotlin → ui → component → AppBarWithArrow.kt
package ui.component
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.unit.dp
import 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.component
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import com.seiko.imageloader.rememberAsyncImagePainter
import data.model.MovieItem
import utils.AppConstant
import 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.component
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import 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.component
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.Icon
import androidx.compose.material.TextField
import androidx.compose.material.TextFieldDefaults
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.Search
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.ExperimentalCoroutinesApi
import theme.Blue
import 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)
},
//shape = RoundedCornerShape(8.dp),
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.component
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.seiko.imageloader.rememberAsyncImagePainter
import data.model.BaseModelV2
import moe.tlaster.precompose.navigation.Navigator
import navigation.NavigationScreen
import theme.DefaultBackgroundColor
import theme.FontColor
import theme.SecondaryFontColor
import utils.AppConstant
import utils.AppString
import utils.cornerRadius
import utils.network.DataState
import utils.roundTo
@Composable
fun SearchUI(
navController: Navigator,
searchData: MutableState<DataState<BaseModelV2>?>,
itemClick: () -> Unit
) {
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.heightIn(0.dp, 350.dp) // define max height
.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.component
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.TileMode
fun 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.home
import androidx.compose.foundation.layout.*
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import data.model.MovieItem
import moe.tlaster.precompose.navigation.Navigator
import navigation.NavigationScreen
import ui.component.MovieList
import ui.component.ProgressIndicator
import utils.AppString
import 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.home
import data.model.MovieItem
import data.repository.MovieRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import moe.tlaster.precompose.viewmodel.ViewModel
import utils.network.DataState
class 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.popular
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import data.model.MovieItem
import moe.tlaster.precompose.navigation.Navigator
import navigation.NavigationScreen
import ui.component.MovieList
import ui.component.ProgressIndicator
import utils.AppString
import 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.popular
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import data.model.MovieItem
import moe.tlaster.precompose.navigation.Navigator
import navigation.NavigationScreen
import ui.component.MovieList
import ui.component.ProgressIndicator
import utils.AppString
import 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.toprated
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import data.model.MovieItem
import moe.tlaster.precompose.navigation.Navigator
import navigation.NavigationScreen
import ui.component.MovieList
import ui.component.ProgressIndicator
import utils.AppString
import 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.toprated
import data.model.MovieItem
import data.repository.MovieRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import moe.tlaster.precompose.viewmodel.ViewModel
import utils.network.DataState
class 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.upcoming
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import data.model.MovieItem
import moe.tlaster.precompose.navigation.Navigator
import navigation.NavigationScreen
import ui.component.MovieList
import ui.component.ProgressIndicator
import utils.AppString
import 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.upcoming
import data.model.MovieItem
import data.repository.MovieRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import moe.tlaster.precompose.viewmodel.ViewModel
import utils.network.DataState
class 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.detail
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import ui.component.text.SubtitlePrimary
import ui.component.text.SubtitleSecondary
import com.seiko.imageloader.rememberAsyncImagePainter
import data.model.moviedetail.MovieDetail
import moe.tlaster.precompose.navigation.Navigator
import theme.DefaultBackgroundColor
import theme.FontColor
import ui.component.ProgressIndicator
import ui.component.shimmerBackground
import utils.AppConstant
import utils.AppString
import utils.hourMinutes
import utils.network.DataState
import 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.detail
import data.model.moviedetail.MovieDetail
import data.repository.MovieRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import moe.tlaster.precompose.viewmodel.ViewModel
import utils.network.DataState
class 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 ui
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import data.model.BaseModelV2
import data.repository.MovieRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch
import moe.tlaster.precompose.viewmodel.ViewModel
import 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.Column
import androidx.compose.material.BottomNavigation
import androidx.compose.material.BottomNavigationItem
import androidx.compose.material.FloatingActionButton
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search
import androidx.compose.runtime.*
import androidx.compose.ui.graphics.Color
import kotlinx.coroutines.ExperimentalCoroutinesApi
import moe.tlaster.precompose.navigation.BackHandler
import moe.tlaster.precompose.navigation.NavOptions
import moe.tlaster.precompose.navigation.Navigator
import moe.tlaster.precompose.navigation.rememberNavigator
import navigation.Navigation
import navigation.NavigationScreen
import navigation.currentRoute
import theme.FloatingActionBackground
import ui.AppViewModel
import ui.component.AppBarWithArrow
import ui.component.ProgressIndicator
import ui.component.SearchBar
import ui.component.SearchUI
import utils.AppString
import 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.network
/**
* Data state for processing api response Loading, Success and Error
*/
sealed 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 utils
object 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 utils
object 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 utils
import kotlin.math.pow
import kotlin.math.roundToInt
import kotlin.time.Duration.Companion.minutes
fun 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 utils
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.dp
import utils.network.DataState
fun 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 theme
import androidx.compose.ui.graphics.Color
val 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 theme
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Shapes
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.dp
val 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 theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material.MaterialTheme
import androidx.compose.material.darkColors
import androidx.compose.material.lightColors
import androidx.compose.runtime.Composable
private val DarkColorPalette = darkColors(
primary = Purple200,
primaryVariant = Purple700,
secondary = Teal200
)
private val LightColorPalette = lightColors(
primary = Purple500,
primaryVariant = Purple700,
secondary = Teal200
/* Other default colors to override
background = Color.White,
surface = Color.White,
onPrimary = Color.White,
onSecondary = Color.Black,
onBackground = Color.Black,
onSurface = Color.Black,
*/
)
@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 theme
import androidx.compose.material.Typography
import androidx.compose.runtime.Composable
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val 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
Blogpost: https://piashcse.blogspot.com/2023/05/compose-multiplatform-movie-app.html