Cocktail Directory App made with Kotlin Multiplatform Mobile
- Kotlinx Coroutines
- KMP-NativeCoroutines
This makes it possible to useStateFlow
on iOS
- KMP-NativeCoroutines
- Jetpack Viewmodel
- KMM-ViewModel
This makes it possible to write commonViewModel
code for iOS and Android
- KMM-ViewModel
- Ktor
UsingOkHttp
engine for Android andDarwin
engine for iOS - Kotlinx Serialization
class CocktailListViewModel : KMMViewModel() {
private val api = CocktailAPI()
private val _cocktailState = MutableStateFlow<CocktailListState>(CocktailListState.Empty)
@NativeCoroutinesState
val cocktailState: StateFlow<CocktailListState> = _cocktailState.stateIn(
viewModelScope, SharingStarted.WhileSubscribed(), CocktailListState.Empty
)
fun updateCocktailList() {
_cocktailState.value = CocktailListState.Loading
viewModelScope.coroutineScope.launch {
try {
val response = api.getCocktails()
_cocktailState.value = CocktailListState.Success(response)
} catch (e: Exception) {
_cocktailState.value = CocktailListState.Error(e)
}
}
}
}
View Code
struct CocktailItemRowView: View {
var drink: Drink
var body: some View {
HStack {
Text(drink.strDrink!)
.font(.title)
.bold()
.padding(10)
Spacer()
Text((drink.strIngredient1 ?? "") + "," + (drink.strIngredient2 ?? ""))
.font(.caption)
.padding(10)
Spacer()
Text(drink.strCategory!)
.font(.body)
.foregroundColor(.gray)
.padding(10)
}
}
}
View Code
struct CocktailListView: View {
var state: CocktailListState
var onRefresh: () -> Void
var body: some View {
VStack {
switch state {
case is CocktailListState.Error:
Text((state as! CocktailListState.Error).error.message ?? "Error")
.foregroundColor(.red);
case is CocktailListState.Empty:
VStack {
Text("No cocktails found")
.foregroundColor(.gray)
Button(action: onRefresh) {
Text("Refresh")
}
}
case is CocktailListState.Loading:
ProgressView()
case is CocktailListState.Success:
List((state as! CocktailListState.Success).cocktails.drinks, id: \.idDrink) { drink in
CocktailItemRowView(drink: drink)
}
default:
Text("Unknown state")
}
}
}
}
View Code
struct ContentView: View {
@ObservedViewModel var viewModel = CocktailListViewModel()
var body: some View {
CocktailListView(state: viewModel.cocktailState, onRefresh: {
viewModel.updateCocktailList()
})
}
}
View Code
@Composable
fun CocktailItemRowView(drink: Drink) {
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(4.dp),
elevation = 4.dp,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = drink.strDrink ?: "", Modifier.padding(12.dp),
style = KocktailUIConfig.typography().subtitle1
)
Column(
modifier = Modifier.weight(1f),
) {
Text(
text = arrayOf(
drink.strIngredient1,
drink.strIngredient2,
drink.strIngredient3
).joinToString(",") { it ?: "" },
Modifier.padding(12.dp),
style = KocktailUIConfig.typography().caption,
)
}
Text(
text = drink.strCategory ?: "", Modifier.padding(12.dp),
style = KocktailUIConfig.typography().subtitle2
)
}
}
}
* includes Pull to Refresh functionality
View Code
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun CocktailListView(
state: CocktailListState,
onRefresh: () -> Unit,
pullRefreshState: PullRefreshState = rememberPullRefreshState(
refreshing = state is CocktailListState.Loading,
onRefresh = onRefresh
),
) {
Box(
modifier = Modifier
.fillMaxSize()
.pullRefresh(
state = pullRefreshState,
enabled = true
)
) {
when (state) {
is CocktailListState.Success -> {
LazyColumn {
items(
items = state.cocktails.drinks,
itemContent = { CocktailItemRowView(drink = it) },
key = { it.idDrink!! },
)
}
}
is CocktailListState.Error -> {
Text(text = "Error: ${state.error.message}")
}
CocktailListState.Loading -> {
PullRefreshIndicator(
modifier = Modifier.align(Alignment.Center),
refreshing = true,
state = pullRefreshState
)
}
CocktailListState.Empty -> {
Button(
onClick = onRefresh,
modifier = Modifier.align(Alignment.Center)
) {
Text(text = "Tap to refresh")
}
}
}
}
}
View Code
class MainActivity : ComponentActivity() {
val viewModel: CocktailListViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
KocktailUIConfig.Theme {
val state = viewModel.cocktailState.collectAsState()
CocktailListView(
state = state.value,
onRefresh = { viewModel.updateCocktailList() }
)
}
}
}
}