Skip to content

championswimmer/Kocktail_KMP_App

Repository files navigation

Kocktail

Cocktail Directory App made with Kotlin Multiplatform Mobile

Preview

gif preview

Libraries Used

Code Overview

Shared ViewModel

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)
            }
        }
    }
}

iOS View

CocktailItemRowView - view to render each list item

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)
        }
    }
}

CocktailListView - view for rendering the list (or errors/loading)

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")
            }

        }
    }
}

ContentView - main screen of the app

View Code
struct ContentView: View {
	@ObservedViewModel var viewModel = CocktailListViewModel()

	var body: some View {
		CocktailListView(state: viewModel.cocktailState, onRefresh: {
			viewModel.updateCocktailList()
		})
	}
}

Android View

CocktailItemRowView - view to render each list item

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
            )
        }
    }
}

CocktailListView - view for rendering the list (or errors/loading)

* 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")
                }
            }
        }
    }
}

MainActivity - main screen of the app

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() }
                )
            }
        }
    }
}