├── .gitignore ├── README.md ├── hn-android-client ├── .gitignore ├── app │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── prof18 │ │ │ └── hn │ │ │ └── android │ │ │ └── client │ │ │ └── ExampleInstrumentedTest.kt │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── prof18 │ │ │ │ └── hn │ │ │ │ └── android │ │ │ │ └── client │ │ │ │ ├── adapter │ │ │ │ └── NewsAdapter.kt │ │ │ │ ├── data │ │ │ │ ├── HNApiService.kt │ │ │ │ └── model │ │ │ │ │ ├── AppState.kt │ │ │ │ │ ├── News.kt │ │ │ │ │ └── NewsState.kt │ │ │ │ └── ui │ │ │ │ ├── MainActivity.kt │ │ │ │ └── MainViewModel.kt │ │ └── res │ │ │ ├── drawable-v24 │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── drawable │ │ │ └── ic_launcher_background.xml │ │ │ ├── layout │ │ │ ├── activity_main.xml │ │ │ └── item_news_card.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── values-night │ │ │ └── themes.xml │ │ │ └── values │ │ │ ├── colors.xml │ │ │ ├── strings.xml │ │ │ └── themes.xml │ │ └── test │ │ └── java │ │ └── com │ │ └── prof18 │ │ └── hn │ │ └── android │ │ └── client │ │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle ├── hn-backend ├── .gitignore ├── build.gradle.kts ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── resources │ ├── application.conf │ └── logback.xml ├── settings.gradle.kts ├── src │ ├── Application.kt │ ├── NewsRepository.kt │ └── NewsRepositoryImpl.kt └── test │ └── ApplicationTest.kt ├── hn-foundation ├── .gitignore ├── build.gradle.kts ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── local.properties ├── settings.gradle.kts └── src │ ├── androidMain │ ├── kotlin │ │ └── .gitkeep │ └── resources │ │ └── .gitkeep │ ├── androidTest │ ├── kotlin │ │ └── .gitkeep │ └── resources │ │ └── .gitkeep │ ├── commonMain │ ├── kotlin │ │ └── com │ │ │ └── prof18 │ │ │ └── hn │ │ │ └── dto │ │ │ ├── BaseDTO.kt │ │ │ ├── NewsDTO.kt │ │ │ └── NewsListDTO.kt │ └── resources │ │ └── .gitkeep │ ├── commonTest │ ├── kotlin │ │ └── .gitkeep │ └── resources │ │ └── .gitkeep │ ├── iosMain │ ├── kotlin │ │ └── .gitkeep │ └── resources │ │ └── .gitkeep │ ├── iosTest │ ├── kotlin │ │ └── .gitkeep │ └── resources │ │ └── .gitkeep │ ├── jvmMain │ ├── kotlin │ │ └── .gitkeep │ └── resources │ │ └── .gitkeep │ ├── jvmTest │ ├── kotlin │ │ └── .gitkeep │ └── resources │ │ └── .gitkeep │ └── main │ └── AndroidManifest.xml ├── hn-ios-client └── HN Client │ ├── HN Client.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ ├── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcuserdata │ │ │ └── marco.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ └── xcuserdata │ │ ├── marco.xcuserdatad │ │ └── xcschemes │ │ │ └── xcschememanagement.plist │ │ └── marcogomiero.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist │ ├── HN Client.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcuserdata │ │ ├── marco.xcuserdatad │ │ ├── UserInterfaceState.xcuserstate │ │ └── xcdebugger │ │ │ └── Breakpoints_v2.xcbkptlist │ │ └── marcogomiero.xcuserdatad │ │ └── UserInterfaceState.xcuserstate │ ├── HN Client │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ ├── Info.plist │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── SceneDelegate.swift │ ├── data │ │ ├── CustomSerializer.swift │ │ └── model │ │ │ ├── AppState.swift │ │ │ ├── News.swift │ │ │ └── NewsState.swift │ └── ui │ │ ├── MainView.swift │ │ ├── MainViewModel.swift │ │ └── components │ │ ├── ErrorView.swift │ │ ├── LoadingView.swift │ │ ├── NewsCard.swift │ │ └── NewsList.swift │ ├── HN ClientTests │ ├── HN_ClientTests.swift │ └── Info.plist │ ├── Podfile │ ├── Podfile.lock │ └── Pods │ ├── Alamofire │ ├── LICENSE │ ├── README.md │ └── Source │ │ ├── AFError.swift │ │ ├── Alamofire.swift │ │ ├── AlamofireExtended.swift │ │ ├── AuthenticationInterceptor.swift │ │ ├── CachedResponseHandler.swift │ │ ├── Combine.swift │ │ ├── DispatchQueue+Alamofire.swift │ │ ├── EventMonitor.swift │ │ ├── HTTPHeaders.swift │ │ ├── HTTPMethod.swift │ │ ├── MultipartFormData.swift │ │ ├── MultipartUpload.swift │ │ ├── NetworkReachabilityManager.swift │ │ ├── Notifications.swift │ │ ├── OperationQueue+Alamofire.swift │ │ ├── ParameterEncoder.swift │ │ ├── ParameterEncoding.swift │ │ ├── Protected.swift │ │ ├── RedirectHandler.swift │ │ ├── Request.swift │ │ ├── RequestInterceptor.swift │ │ ├── RequestTaskMap.swift │ │ ├── Response.swift │ │ ├── ResponseSerialization.swift │ │ ├── Result+Alamofire.swift │ │ ├── RetryPolicy.swift │ │ ├── ServerTrustEvaluation.swift │ │ ├── Session.swift │ │ ├── SessionDelegate.swift │ │ ├── StringEncoding+Alamofire.swift │ │ ├── URLConvertible+URLRequestConvertible.swift │ │ ├── URLEncodedFormEncoder.swift │ │ ├── URLRequest+Alamofire.swift │ │ ├── URLSessionConfiguration+Alamofire.swift │ │ └── Validation.swift │ ├── HNFoundation │ ├── HNFoundation.xcframework │ │ ├── Info.plist │ │ ├── ios-arm64 │ │ │ └── HNFoundation.framework │ │ │ │ ├── HNFoundation │ │ │ │ ├── Headers │ │ │ │ └── HNFoundation.h │ │ │ │ ├── Info.plist │ │ │ │ └── Modules │ │ │ │ └── module.modulemap │ │ └── ios-x86_64-simulator │ │ │ └── HNFoundation.framework │ │ │ ├── HNFoundation │ │ │ ├── Headers │ │ │ └── HNFoundation.h │ │ │ ├── Info.plist │ │ │ └── Modules │ │ │ └── module.modulemap │ └── README.md │ ├── Local Podspecs │ └── HNFoundation.podspec.json │ ├── Manifest.lock │ ├── Pods.xcodeproj │ ├── project.pbxproj │ └── xcuserdata │ │ └── marcogomiero.xcuserdatad │ │ └── xcschemes │ │ ├── Alamofire.xcscheme │ │ ├── HNFoundation.xcscheme │ │ ├── Pods-HN Client.xcscheme │ │ └── xcschememanagement.plist │ └── Target Support Files │ ├── Alamofire │ ├── Alamofire-Info.plist │ ├── Alamofire-dummy.m │ ├── Alamofire-prefix.pch │ ├── Alamofire-umbrella.h │ ├── Alamofire.debug.xcconfig │ ├── Alamofire.modulemap │ └── Alamofire.release.xcconfig │ ├── HNFoundation │ ├── HNFoundation-copy-dsyms-input-files.xcfilelist │ ├── HNFoundation-copy-dsyms-output-files.xcfilelist │ ├── HNFoundation-copy-dsyms.sh │ ├── HNFoundation-xcframeworks-input-files.xcfilelist │ ├── HNFoundation-xcframeworks-output-files.xcfilelist │ ├── HNFoundation-xcframeworks.sh │ ├── HNFoundation.debug.xcconfig │ └── HNFoundation.release.xcconfig │ └── Pods-HN Client │ ├── Pods-HN Client-Info.plist │ ├── Pods-HN Client-acknowledgements.markdown │ ├── Pods-HN Client-acknowledgements.plist │ ├── Pods-HN Client-dummy.m │ ├── Pods-HN Client-frameworks-Debug-input-files.xcfilelist │ ├── Pods-HN Client-frameworks-Debug-output-files.xcfilelist │ ├── Pods-HN Client-frameworks-Release-input-files.xcfilelist │ ├── Pods-HN Client-frameworks-Release-output-files.xcfilelist │ ├── Pods-HN Client-frameworks.sh │ ├── Pods-HN Client-umbrella.h │ ├── Pods-HN Client.debug.xcconfig │ ├── Pods-HN Client.modulemap │ └── Pods-HN Client.release.xcconfig └── utils ├── hacker_news.json └── news-generator.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Shared code between Android, iOS and Backend 2 | A sample of Android app, iOs app, and backend that share some common code via Kotlin Multiplatform. 3 | 4 | There are two branches: 5 | 6 | - [main](https://github.com/prof18/shared-hn-android-ios-backend/tree/main): the iOS code is shared using an [XCFramework](https://developer.apple.com/videos/play/wwdc2019/416/) 7 | - [with-ios-fatframework](https://github.com/prof18/shared-hn-android-ios-backend/tree/with-ios-fatframework): the iOS code is shared using a FatFramework. 8 | 9 | This sample project is used as reference during my talks, to show how to introduce Kotlin Multiplatform into an existing project: 10 | 11 | - ["And that, folks, is how we shared code between Android, iOS and the Backend - droidcon EMEA"](https://speakerdeck.com/prof18/and-that-folks-is-how-we-shared-code-between-android-ios-and-the-backend-droidcon-emea) -> [with-ios-fatframework](https://github.com/prof18/shared-hn-android-ios-backend/tree/with-ios-fatframework) branch 12 | - ["And that, folks, is how we shared code between Android, iOS and the Backend - FOSDEM"](https://www.marcogomiero.com/talks/2021/shared-code-kmp-fosdem/) -> [with-ios-fatframework](https://github.com/prof18/shared-hn-android-ios-backend/tree/with-ios-fatframework) branch 13 | - ["Introducing Kotlin Multiplatform in an existing project"](https://www.marcogomiero.com/talks/2021/kmp-existing-project-droidcon-berlin.md/) -> main branch 14 | 15 | 16 | ## Repo Structure 17 | 18 | ### hn-foundation 19 | 20 | This folder contains the Kotlin Multiplatform library that it's shared between Android, iOs, and the Backend. For Android and the backend, the library is distributed using a local Maven repository. For iOs instead, the library is distributed using two CocoaPod repositories hosted on GitHub: 21 | 22 | - [hn-foundation-cocoa](https://github.com/prof18/hn-foundation-cocoa): contains the library code in a FatFramework 23 | - [hn-foundation-cocoa-xcframework](https://github.com/prof18/hn-foundation-cocoa-xcframework): contains the library code in an XCFramework 24 | 25 | #### Publish artifacts for Android and Backend 26 | 27 | ```bas 28 | ./gradlew publishToMavenLocal 29 | ``` 30 | 31 | #### Publish Debug iOS Framework 32 | 33 | ```bash 34 | ./gradlew publishDevFramework 35 | ``` 36 | 37 | #### Publish Release iOS Framework 38 | ```bash 39 | ./gradlew publishFramework 40 | ``` 41 | 42 | N.B. if you want to publish to a different CocoaPod repo, you must have a folder organized like that (where the hn-foundation-cocoa is the repo that contains the framework): 43 | 44 | ```bash 45 | . 46 | ├── hn-foundation-cocoa 47 | └── shared-hn-android-ios-backend 48 | ├── hn-android-client 49 | ├── hn-backend 50 | ├── hn-foundation 51 | └── hn-ios-client 52 | ``` 53 | 54 | If you change the location, remember to customize the path declared in the task on the build.gradle.kts file 55 | 56 | ### hn-backend 57 | 58 | The folder contains a backend written with [Ktor](https://ktor.io/). After publishing the artifacts to the local Maven repository you can start the backend on your machine. 59 | 60 | ### hn-android-client 61 | 62 | Thet folder contains the Android client. After publishing the artifacts to the local Maven repository and after starting the backend, you can try the android app. Remember to change the base address of the backend [here](https://github.com/prof18/shared-hn-android-ios-backend/blob/master/hn-android-client/app/src/main/java/com/prof18/hn/android/client/ui/MainViewModel.kt#L23) 63 | 64 | ### hn-ios-client 65 | 66 | The project contains the iOS client. You should be able to try the iOs app as is since I've published the Pods. Of course, you can create your CocoaPod repository and try yourself the publishing process. Remember to change the base address of the backend [here](https://github.com/prof18/shared-hn-android-ios-backend/blob/master/hn-ios-client/HN%20Client/HN%20Client/ui/MainViewModel.swift#L20). 67 | -------------------------------------------------------------------------------- /hn-android-client/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | /.idea 17 | -------------------------------------------------------------------------------- /hn-android-client/app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /hn-android-client/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | } 5 | 6 | android { 7 | compileSdkVersion 30 8 | 9 | defaultConfig { 10 | applicationId "com.prof18.hn.android.client" 11 | minSdkVersion 23 12 | targetSdkVersion 30 13 | versionCode 1 14 | versionName "1.0" 15 | 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | } 26 | 27 | dependencies { 28 | 29 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 30 | implementation 'androidx.core:core-ktx:1.6.0' 31 | implementation 'androidx.appcompat:appcompat:1.3.1' 32 | implementation 'com.google.android.material:material:1.4.0' 33 | implementation 'androidx.constraintlayout:constraintlayout:2.1.0' 34 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1" 35 | implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1" 36 | implementation "androidx.activity:activity-ktx:1.3.1" 37 | 38 | implementation 'com.squareup.retrofit2:retrofit:2.9.0' 39 | implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0") 40 | implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.2") 41 | 42 | implementation "com.prof18.hn.foundation:hn-foundation-android:2.0.0" 43 | 44 | testImplementation 'junit:junit:4.13.2' 45 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 46 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 47 | } -------------------------------------------------------------------------------- /hn-android-client/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /hn-android-client/app/src/androidTest/java/com/prof18/hn/android/client/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.prof18.hn.android.client 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.prof18.hn.android.client", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /hn-android-client/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /hn-android-client/app/src/main/java/com/prof18/hn/android/client/adapter/NewsAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.prof18.hn.android.client.adapter 2 | 3 | import android.content.Intent 4 | import android.net.Uri 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import android.widget.TextView 9 | import androidx.recyclerview.widget.RecyclerView 10 | import com.google.android.material.card.MaterialCardView 11 | import com.prof18.hn.android.client.R 12 | import com.prof18.hn.android.client.data.model.News 13 | 14 | class NewsAdapter(var items: List = listOf()) : 15 | RecyclerView.Adapter() { 16 | 17 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder( 18 | LayoutInflater.from(parent.context).inflate( 19 | R.layout.item_news_card, 20 | parent, 21 | false 22 | ) 23 | ) 24 | 25 | override fun getItemCount() = items.size 26 | 27 | override fun onBindViewHolder(holder: ViewHolder, position: Int) = 28 | holder.bind(items[position]) 29 | 30 | class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { 31 | 32 | private val title = itemView.findViewById(R.id.news_title) 33 | private val date = itemView.findViewById(R.id.news_date) 34 | private val card = itemView.findViewById(R.id.news_card) 35 | 36 | fun bind(item: News) { 37 | title.text = item.title 38 | date.text = item.formattedDate 39 | card.setOnClickListener { 40 | itemView.context.startActivity( 41 | Intent( 42 | Intent.ACTION_VIEW, 43 | Uri.parse(item.url) 44 | ) 45 | ) 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /hn-android-client/app/src/main/java/com/prof18/hn/android/client/data/HNApiService.kt: -------------------------------------------------------------------------------- 1 | package com.prof18.hn.android.client.data 2 | 3 | import com.prof18.hn.dto.NewsListDTO 4 | import retrofit2.http.GET 5 | 6 | interface HNApiService { 7 | 8 | @GET("hn/topStories") 9 | suspend fun getTopStories(): NewsListDTO 10 | 11 | } -------------------------------------------------------------------------------- /hn-android-client/app/src/main/java/com/prof18/hn/android/client/data/model/AppState.kt: -------------------------------------------------------------------------------- 1 | package com.prof18.hn.android.client.data.model 2 | 3 | data class AppState( 4 | var newsState: NewsState 5 | ) -------------------------------------------------------------------------------- /hn-android-client/app/src/main/java/com/prof18/hn/android/client/data/model/News.kt: -------------------------------------------------------------------------------- 1 | package com.prof18.hn.android.client.data.model 2 | 3 | data class News( 4 | val title: String, 5 | val formattedDate: String, 6 | val url: String 7 | ) -------------------------------------------------------------------------------- /hn-android-client/app/src/main/java/com/prof18/hn/android/client/data/model/NewsState.kt: -------------------------------------------------------------------------------- 1 | package com.prof18.hn.android.client.data.model 2 | 3 | sealed class NewsState { 4 | object Loading: NewsState() 5 | class Success(val news: List): NewsState() 6 | class Error(val reason: String): NewsState() 7 | } -------------------------------------------------------------------------------- /hn-android-client/app/src/main/java/com/prof18/hn/android/client/ui/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.prof18.hn.android.client.ui 2 | 3 | import androidx.appcompat.app.AppCompatActivity 4 | import android.os.Bundle 5 | import android.view.View 6 | import android.widget.Button 7 | import android.widget.ProgressBar 8 | import android.widget.TextView 9 | import androidx.activity.viewModels 10 | import androidx.lifecycle.Observer 11 | import androidx.recyclerview.widget.RecyclerView 12 | import com.prof18.hn.android.client.R 13 | import com.prof18.hn.android.client.adapter.NewsAdapter 14 | import com.prof18.hn.android.client.data.model.NewsState 15 | 16 | class MainActivity : AppCompatActivity() { 17 | 18 | private lateinit var recyclerView: RecyclerView 19 | private lateinit var progressBar: ProgressBar 20 | private lateinit var errorMessage: TextView 21 | private lateinit var errorButton: Button 22 | 23 | val viewModel: MainViewModel by viewModels() 24 | 25 | override fun onCreate(savedInstanceState: Bundle?) { 26 | super.onCreate(savedInstanceState) 27 | setContentView(R.layout.activity_main) 28 | // setSupportActionBar(findViewById(R.id.toolbar)) 29 | 30 | recyclerView = findViewById(R.id.recycler_view) 31 | progressBar = findViewById(R.id.progress_bar) 32 | errorMessage = findViewById(R.id.error_message) 33 | errorButton = findViewById(R.id.error_button) 34 | 35 | val adapter = NewsAdapter() 36 | recyclerView.adapter = adapter 37 | 38 | viewModel.appState.observe(this, { 39 | it?.let { appState -> 40 | 41 | when (appState.newsState) { 42 | 43 | is NewsState.Loading -> { 44 | progressBar.visibility = View.VISIBLE 45 | recyclerView.visibility = View.GONE 46 | errorMessage.visibility = View.GONE 47 | errorButton.visibility = View.GONE 48 | } 49 | 50 | is NewsState.Error -> { 51 | val errorState = appState.newsState as NewsState.Error 52 | 53 | progressBar.visibility = View.GONE 54 | recyclerView.visibility = View.GONE 55 | 56 | errorButton.visibility = View.VISIBLE 57 | errorMessage.visibility = View.VISIBLE 58 | errorMessage.text = errorState.reason 59 | errorButton.setOnClickListener { 60 | viewModel.loadData() 61 | } 62 | } 63 | 64 | is NewsState.Success -> { 65 | val successState = appState.newsState as NewsState.Success 66 | progressBar.visibility = View.GONE 67 | errorMessage.visibility = View.GONE 68 | errorButton.visibility = View.GONE 69 | 70 | recyclerView.visibility = View.VISIBLE 71 | adapter.items = successState.news 72 | adapter.notifyDataSetChanged() 73 | } 74 | } 75 | } 76 | }) 77 | viewModel.loadData() 78 | } 79 | } -------------------------------------------------------------------------------- /hn-android-client/app/src/main/java/com/prof18/hn/android/client/ui/MainViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.prof18.hn.android.client.ui 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import androidx.lifecycle.viewModelScope 7 | import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory 8 | import com.prof18.hn.android.client.data.HNApiService 9 | import com.prof18.hn.android.client.data.model.AppState 10 | import com.prof18.hn.android.client.data.model.News 11 | import com.prof18.hn.android.client.data.model.NewsState 12 | import kotlinx.coroutines.launch 13 | import kotlinx.serialization.ExperimentalSerializationApi 14 | import kotlinx.serialization.json.Json 15 | import okhttp3.MediaType 16 | import retrofit2.Retrofit 17 | import java.text.SimpleDateFormat 18 | import java.util.* 19 | 20 | @ExperimentalSerializationApi 21 | class MainViewModel : ViewModel() { 22 | 23 | private val apiService: HNApiService by lazy { 24 | val contentType = MediaType.get("application/json") 25 | Retrofit.Builder() 26 | .baseUrl("http://192.168.0.147:8080/") 27 | .addConverterFactory(Json.asConverterFactory(contentType)) 28 | .build() 29 | .create(HNApiService::class.java) 30 | } 31 | 32 | private val _appState = MutableLiveData() 33 | val appState: LiveData 34 | get() = _appState 35 | 36 | fun loadData() { 37 | viewModelScope.launch { 38 | _appState.value = AppState(newsState = NewsState.Loading) 39 | try { 40 | val newsResponse = apiService.getTopStories() 41 | val news = newsResponse.news.map { 42 | News( 43 | title = it.title, 44 | formattedDate = getStringTime(it.timestamp), 45 | url = it.url 46 | ) 47 | } 48 | _appState.value = AppState(newsState = NewsState.Success(news)) 49 | } catch (e: Exception) { 50 | e.printStackTrace() 51 | _appState.value = AppState(newsState = NewsState.Error("Something wrong here :(")) 52 | } 53 | } 54 | } 55 | 56 | private fun getStringTime(time: Long): String { 57 | val formatter = SimpleDateFormat("d MMM yyyy", Locale.getDefault()) 58 | return formatter.format(Date(time * 1000)) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /hn-android-client/app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /hn-android-client/app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /hn-android-client/app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 18 | 19 | 28 | 29 | 35 | 36 |