A Kotlin Multiplatform Mobile library to Share Utility Classes between Android, IOS Apps to Lead Mobile Apps by Domain Layer and UseCases
This Library Share Common Parts between Android and IOS Applications and the Idea of this Library is to Control the Applications from UseCases and Domain Layer No Logic on UI or ViewModel Level, the Whole Logic Should be Inside UseCases and for Api Part Sopy Use Ktor Client as a Http Client
The Main Idea of this Library is to Build common Implementation of the Domain Layer between Android, IOS Apps and Share Same Logic by UseCases, in Sopy the UseCases are the Start Point in each Feature
- Android
- IOS (Darwin, Legacy Darwin)
- Local Storage (Shared Prefs, UserDefaults)
- UseCases and Constraints
- Api Requests (OneRequest, Crud Operations)
- Ktor Client Configurations (Android, IOS, IOS Darwin Legacy)
- Ktor Version: 2.3.0
- Java Version: 17
- Kotlin Coroutines: 1.6.4
- Android Lifecycle ViewModels: 2.5.1
- Arm32
- Arm64
- x64
Builds Location in Releases Tab
Library Builds Available to Download on Android, IOS by Build Files
- Android Builds .aar File (Debug, Release) Available
- IOS Builds .zip Available for All Supported Architectures (Debug, Release)
- Android Min SDK Version: 21
- Dependencies Details
dependencies {
// Common Main
implementation "com.yazantarifi:sopy:1.0.1"
// Android
implementation "com.yazantarifi:sopy-android:1.0.1"
// IOS
implementation "com.yazantarifi:sopy-iosx64:1.0.1"
implementation "com.yazantarifi:sopy-iosarm64:1.0.1"
implementation "com.yazantarifi:sopy-iosarm32:1.0.1"
}
- KMM Apps Details
sourceSets {
val commonMain by getting {
dependencies {
implementation("com.yazantarifi:sopy:1.0.1")
}
}
val androidMain by getting {
dependencies {
implementation("com.yazantarifi:sopy-android:1.0.1")
}
}
val iosX64Main by getting {
dependencies {
implementation("com.yazantarifi:sopy-iosx64:1.0.1")
}
}
val iosArm64Main by getting {
dependencies {
implementation("com.yazantarifi:sopy-iosarm64:1.0.1")
}
}
val iosSimulatorArm64Main by getting
val iosMain by creating {
dependencies {
implementation("io.ktor:ktor-client-ios:2.2.1")
implementation("io.ktor:ktor-client-darwin:2.2.1")
}
dependsOn(commonMain)
iosX64Main.dependsOn(this)
iosArm64Main.dependsOn(this)
iosSimulatorArm64Main.dependsOn(this)
}
}
- SopyOneRequest: Execute Ktor Api Request and Handle Response to UseCase
- SopyCrudRequest: Execute Ktor Api Request (Crud Operations) and Handle Response to UseCase
- SopyStorageKeyValue: Local Storage Key Value (SharedPrefs, UserDefaults)
- SopyUseCase: UseCases Implementation with Ktor Client and State Management (Success, Failed, Loading)
- SopyDirectUseCase: UseCases Implementation with Ktor Client like SopyUseCase but without Input Type, UseCases to Return Results Only
- SopyUseCaseAlias: Singleton Instances Helpful in Hilt Scopes in Android Apps Only
- SopyViewModel: ViewModel Implementation with UseCases and Lifecycle
- SopyRequestInterceptor: Ktor Request Interceptor to Send Common Headers in All Apis
- SopyApplicationConfigurations: Application Configurations and General Configs : Registered in App Delegate and Application Scope Only
- SopyBaseViewModel: Base ViewModels Implementation (IOS Only)
- SopyHttpBaseClient: Ktor Darwin Engine : x64, Arm64
- SopyLegacyHttpBaseClient: Ktor Darwin Legacy: Arm32 Only
- Simple UseCase Implementation of a Loading, Failed, Success State and Api Requests
class GetHomeScreenItemsUseCase constructor(): SopyUseCase<GetHomeScreenItemsUseCase.RequestValue, List<RadioHomeItem>>() {
private val featuredListApiClient: SpotifyFeaturedPlaylistsApiRequest by lazy {
SpotifyFeaturedPlaylistsApiRequest()
}
override fun isConstraintsSupported(): Boolean {
return false
}
override suspend fun build(requestValue: RequestValue) {
val screenItems = arrayListOf<RadioHomeItem>()
onSubmitLoadingState(true)
screenItems.add(HomeHeaderItem(
RadioApplicationMessages.getMessage("home_title"),
RadioApplicationMessages.getMessage("home_des"),
))
screenItems.add(HomeLayoutDesignItem(
HomeLayoutDesignItem.SCROLL_H,
RadioApplicationMessages.getMessage("home_change_design"),
))
if (featuredListApiClient.isRequestListenerAttachNeeded()) {
featuredListApiClient.addHttpClient(getHttpClientInstance())
featuredListApiClient.addRequestListener(object :
SopyRequestListener<SpotifyFeaturedPlaylistsResponse> {
override fun onSuccess(responseValue: SpotifyFeaturedPlaylistsResponse) {
val playlists = ArrayList<RadioPlaylist>()
responseValue.playlists?.items?.let {
playlists.addAll(it.map { playlist ->
var imageUrl: String = ""
playlist.images?.forEach {
imageUrl = it.url ?: ""
}
RadioPlaylist(
id = playlist.id ?: "",
image = imageUrl,
name = playlist.name ?: "",
ownerName = playlist.owner?.name,
numberOfTracks = playlist.tracks?.total ?: 0,
RadioApplicationMessages.getMessage("loading_image")
)
})
}
screenItems.add(HomePlaylistsItem(
RadioApplicationMessages.getMessage("featured_playlists"),
RadioApplicationMessages.getMessage("loading_image"),
playlists
))
}
override fun onError(error: Throwable) {
// Will Not Show the Item
}
})
}
playlistsCategoryApiClient.executeRequest(SpotifyGetCategoryPlaylistApiRequest.RequestParams(
RadioApplicationMessages.getMessage("top_pop"),
"0JQ5DAqbMKFEC4WFtoNRpw"
), headers)
screenItems.add(HomeOpenSpotifyAppItem(
RadioApplicationMessages.getMessage("spotify_app_open_title"),
RadioApplicationMessages.getMessage("spotify_app_open_message"),
RadioApplicationMessages.getMessage("spotify_app_open_button"),
))
onSubmitLoadingState(false)
onSubmitSuccessState(screenItems)
}
data class RequestValue(
val token: String,
val isNotificationPermissionShouldShow: Boolean,
val isNotificationsEnabled: Boolean
)
override fun clear() {
super.clear()
featuredListApiClient.clear()
}
}
- Api Request Implementation with Ktor Client
class SpotifyFeaturedPlaylistsApiRequest: SopyOneRequest<Unit, SpotifyFeaturedPlaylistsResponse>() {
override suspend fun executeRequest(
requestBody: Unit,
headers: List<Pair<String, String>>,
params: HashMap<String, String>
) {
try {
val response = httpClient?.get(getRequestUrl()) {
headers.forEach {
header(it.first, it.second)
}
}
if (isSuccessResponse(response?.status ?: HttpStatusCode.BadRequest)) {
response?.body<SpotifyFeaturedPlaylistsResponse>()?.let {
requestListener?.onSuccess(it)
}
} else {
requestListener?.onError(Throwable(response?.bodyAsText()))
}
} catch (ex: Exception) {
requestListener?.onError(ex)
}
}
override fun getRequestUrl(): String {
return "https://api.spotify.com/v1/browse/featured-playlists"
}
}
- ViewModel Implementation (Hilt Dependency Injection)
@HiltViewModel
class HomeViewModel @Inject constructor(
private val getHomeScreenItems: GetHomeScreenItemsUseCase,
private val categoriesUseCase: GetCategoriesUseCase,
private val accountInfoUseCase: GetAccountTreeInfoUseCase,
private val storageProvider: SopyStorageProvider
): SopyViewModel<HomeAction>() {
var selectedLayoutDesignMode: Int = HomeLayoutDesignItem.SCROLL_H
val feedLoadingListener by lazy { mutableStateOf(false) }
var feedContentListener = mutableStateListOf<RadioHomeItem>()
val categoriesListListener: MutableState<ArrayList<RadioCategoryItem?>> by lazy { mutableStateOf(arrayListOf()) }
val categoriesLoadingListener by lazy { mutableStateOf(false) }
val accountLoadingListener by lazy { mutableStateOf(false) }
val accountInfoListListener: MutableState<ArrayList<RadioAccountItem>> by lazy { mutableStateOf(arrayListOf()) }
override suspend fun onNewActionTriggered(action: HomeAction) {
when (action) {
is HomeAction.GetFeed -> onGetHomeScreenFeedInfo(action.context)
is HomeAction.GetCategoriesAction -> onGetCategories()
is HomeAction.GetAccountInfoAction -> onGetAccountInfo()
is HomeAction.RemoveNotificationPermissionAction -> onRemoveNotificationPermissionItem()
}
}
private fun onGetCategories() {
if (categoriesListListener.value.isNotEmpty()) return
categoriesUseCase.execute(
storageProvider.getAccessToken(),
object : SopyUseCaseListener {
override fun onStateUpdated(newState: SopyState) {
scope.launch(Dispatchers.Main) {
when (newState) {
is SopyEmptyState -> {}
is SopyLoadingState -> categoriesLoadingListener.value = newState.isLoading
is SopyErrorState -> errorMessageListener.value = newState.exception.message ?: ""
is SopySuccessState -> (newState.payload as? List<RadioCategoryItem>)?.let {
categoriesListListener.value.addAll(it)
}
}
}
}
}
)
}
private fun onGetHomeScreenFeedInfo(context: Context) {
if (feedContentListener.isNotEmpty()) return
getHomeScreenItems.execute(
GetHomeScreenItemsUseCase.RequestValue(storageProvider.getAccessToken(), android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R, NotificationManagerCompat.from(context).areNotificationsEnabled()),
object : SopyUseCaseListener {
override fun onStateUpdated(newState: SopyState) {
scope.launch(Dispatchers.Main) {
when (newState) {
is SopyEmptyState -> {}
is SopyLoadingState -> feedLoadingListener.value = newState.isLoading
is SopyErrorState -> errorMessageListener.value = newState.exception.message ?: ""
is SopySuccessState -> (newState.payload as? List<RadioHomeItem>)?.let {
feedContentListener.addAll(it)
}
}
}
}
}
)
}
override fun getSupportedUseCases(): ArrayList<SopyUseCaseType> {
return arrayListOf(getHomeScreenItems, categoriesUseCase, accountInfoUseCase)
}
}